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>
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.cssshould shrink to zero as components are touched. website_sale.csscontains 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-cardlocks the markup). !importantis allowed in exactly three places, each with a comment saying why: theprefers-reduced-motionreset; 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.