addons-cm/.claude/skills/odoo-web-design/references/css-architecture.md
GitHub Copilot 5de7573fba [IMP] website_sale_aplicoop: accessibility and responsive polish — phase 8
Quantity control layout: changed from wrapping flex to deliberate two-row
grid so the add-to-cart button sits full-width underneath the stepper,
avoiding accidental line breaks and giving the primary action more pulsing
area. Also removed the /Kg suffix's redundant font rules.

Order card delivery row: reordered to badge-then-date (visually centred),
removed the "Delivery" label, and swapped bg-primary to text-bg-primary for
proper contrast on the home-delivery badge (3.13:1 minimum).

Tooltip translations: moved hardcoded static tooltips from data-bs-title
(untranslatable) to title (QWeb-translatable). Handled the edge case of
"Save Cart" and "Back to Cart" which had translations but no model_terms
reference in the POT, causing the merge to discard them — added the view
reference so translations now apply.

Load-from-history page: added accessibility: a visible status message
("Loading your order…"), a <noscript> fallback with link to the group
order, lang and viewport meta tags, and localised strings for all three
languages. Gave the page a minimal inline style block since it does not
inherit the token system (no website.layout).

Translations: 11 new entries (es/eu/ca) for new/reworded UI strings, plus
two code catalogue entries (products found, Close) that needed the
#. odoo-python comment for _() resolution. Documented the silent-failure
pattern in docs/TRANSLATIONS.md: the POT merge, untranslatable attributes,
and missing code comments.

Tests: 246 passing. Pre-commit: clean.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-16 11:34:38 +02:00

6 KiB

CSS architecture

File layout

static/src/css/
├── website_sale.css        # index only: @import list, no rules of its own
├── base/
│   ├── variables.css       # :root tokens — the ONLY file with hex literals
│   └── utilities.css       # tiny single-purpose helpers, when Bootstrap lacks one
├── layout/
│   ├── pages.css           # page shells, containers, backgrounds
│   └── header.css
├── components/             # one file per reusable component
│   ├── product-card.css
│   ├── order-card.css
│   ├── cart.css
│   ├── buttons.css
│   ├── quantity-control.css
│   ├── forms.css
│   ├── alerts.css
│   └── tag-filter.css
└── sections/               # page-specific composition only
    ├── products-grid.css
    ├── order-list.css
    ├── checkout.css
    └── info-cards.css

Rules of placement:

  • A rule that could appear on two pages is a component, never a section.
  • A section file may only position and space components — if it restyles a component's internals, the component is missing a modifier.
  • Each component file owns its own media queries, at the bottom. There is no global responsive file in a healthy tree; a legacy layout/responsive.css should shrink to zero as components are touched.
  • website_sale.css contains imports and comments only.

Naming

Prefix everything with eskaera- (the product name already used for page-level classes). Unprefixed names like .product-card, .cart-header, .qty-control sit in the same namespace as Bootstrap, Odoo core and every other installed addon — that collision is what forces !important later.

.eskaera-product-card                 /* block */
.eskaera-product-card__title          /* element */
.eskaera-product-card--out-of-stock   /* modifier */
.is-loading  .is-active  .has-error   /* state, always paired with a block class */

js- prefix for hooks JS queries, and never style a js- class — that way markup can be restyled without breaking behaviour, and behaviour can move without breaking styles.

When renaming existing classes, do it component by component: template + CSS + JS selectors + tests in the same commit, then grep for the old name across views/, static/src/, and README*.

Specificity — how to stop writing !important

Overriding Bootstrap globally (.list-group-item { … }, .alert-warning { … }) is a fight you win only with !important, and it leaks into every other addon. Scope the override under the page root instead: equal specificity plus one class always wins, no escape hatch needed.

/* ❌ global override, needs !important to beat Bootstrap's own later rules */
.list-group-item {
    border-radius: 0 !important;
}

/* ✅ scoped: higher specificity, no !important, no leakage */
.eskaera-checkout-page .list-group-item {
    border-radius: 0;
}

Other rules:

  • Max 3 levels of nesting in a selector. Deeper means the markup is doing the work the class should do.
  • No ID selectors for styling; IDs are for anchors and aria-* references.
  • No element selectors outside base/ (div.product-card locks the markup).
  • !important is allowed in exactly three places, each with a comment saying why: the prefers-reduced-motion reset; a genuine utility that must beat whatever component it lands on (.hidden-product, Bootstrap's own utility layer works this way); and a documented override of a core rule you cannot scope. Anything else means the selector is wrong.

Ordering inside a file

.eskaera-product-card {
    /* 1. layout */
    display: flex;
    flex-direction: column;
    /* 2. box */
    padding: var(--ac-space-md);
    border: 1px solid var(--ac-border-light);
    border-radius: var(--ac-radius-lg);
    /* 3. typography */
    font-size: var(--ac-text-base);
    color: var(--ac-text-primary);
    /* 4. paint & motion */
    background: var(--ac-surface);
    box-shadow: var(--ac-shadow-sm);
    transition: box-shadow var(--ac-transition-fast);
}

Then, in order: :hover (inside @media (hover: hover)), :focus-visible, :active, :disabled, state classes, modifiers, media queries.

Tokens

Full rules in SKILL.md §2. In short:

:root {
    /* alias Odoo/Bootstrap's unprefixed custom properties so theming still works */
    --ac-color-primary: var(--primary, #007bff);
    --ac-color-danger: var(--danger, #dc3545);
    --ac-surface: var(--body-bg, #fff);
    --ac-text-primary: var(--body-color, #1a202c);
    --ac-border-light: var(--border-color, #e2e8f0);

    /* own scales */
    --ac-space-xs: 0.25rem;
    --ac-space-sm: 0.5rem;
    --ac-space-md: 1rem;
    --ac-space-lg: 1.5rem;
    --ac-space-xl: 2rem;

    --ac-radius-sm: 0.25rem;
    --ac-radius-md: 0.5rem;
    --ac-radius-lg: 0.75rem;

    --ac-shadow-sm: 0 2px 8px rgb(0 0 0 / 8%);
    --ac-shadow-md: 0 4px 12px rgb(0 0 0 / 10%);

    --ac-transition-fast: 200ms ease;
}

Migrating a legacy token set: add the --ac- token aliased to the old name, move consumers file by file, delete the old token when grep -rn "\-\-old-name" static/src/ is empty. Don't rename all at once.

Anti-patterns seen in Odoo addons

Smell Fix
!important to beat Bootstrap scope under the page root class
Same colour as #28a745 in six files one token in variables.css
responsive.css overriding twelve components move each query into its component
style="margin-top:20px" in QWeb utility class or component spacing
.card .body .row .col span give the span a class
px for font sizes rem, so browser zoom and user font size work
Colour hardcoded instead of var(--primary) breaks the website theme editor silently
Both @import chain and manifest entries double download; pick one (see SKILL.md §6)

Formatting

Prettier handles .css via pre-commit: printWidth: 100, tabWidth: 4. Run pre-commit run --all-files (or make format) before committing; never hand-align properties, it will be reformatted.