addons-cm/.claude/skills/odoo-web-design/references/css-architecture.md

160 lines
5.9 KiB
Markdown

# 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.
```css
/* ❌ 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 two places: the `prefers-reduced-motion` reset, and a documented
override of a core rule you cannot scope — with a comment saying which rule and why.
## Ordering inside a file
```css
.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:
```css
: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.