addons-cm/.claude/skills/odoo-web-design/SKILL.md

182 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: odoo-web-design
description: Professional finish for the Odoo frontend of this repo — visual polish, responsive layout and WCAG accessibility across CSS, QWeb/HTML and JS. Use when styling or reviewing website/portal pages (website_sale_aplicoop), adding UI components, or fixing responsive/a11y/design-consistency issues.
metadata:
type: reference
---
# Web design & finish (Odoo 18 frontend)
Covers the three layers together: `static/src/css/`, `static/src/js/`, and the QWeb templates in
`views/*.xml`. For language-level conventions see `odoo-qweb-html`, `odoo-javascript`,
`odoo-xml-views`; this skill is about the **finish**: does it look designed, does it work at 320 px,
does it work without a mouse.
## Inherited non-negotiables
- No logic in QWeb — prepare everything in the controller.
- No business logic in JS — UI and events only.
- **No inline `style="..."`** in templates. A style needs a class, and the class needs a token.
- Every user-visible string is translatable (see §4).
## 1. Work order
Never start by editing a component. Always:
1. **Audit** with the commands in §7. Write down what is inconsistent before touching anything.
2. **Tokens first** (`base/variables.css`). Every value a component needs must already exist as a token.
3. **Component by component**, one file each, media queries co-located at the bottom of that file.
4. **Accessibility pass** per template, top to bottom.
5. **Verify** — lint, contrast, keyboard-only, 320 px, 200 % zoom.
Doing 3 before 2 is how a codebase ends up with 61 `!important` and three shades of the same blue.
## 2. Design tokens
Odoo 18 ships Bootstrap 5.3 with `$variable-prefix: ''`
(`/media/snt/data/odoo/ocb/addons/web/static/src/scss/bootstrap_overridden.scss:51`), so Bootstrap's
root custom properties are emitted **unprefixed** into `web.assets_frontend` (via
`import_bootstrap.scss``@import "root"`). Available in plain CSS, no SCSS needed:
```
--primary --secondary --success --danger --warning --info --light --dark
--body-color --body-bg --emphasis-color --secondary-bg --tertiary-bg --border-color
--border-radius --border-radius-sm --border-radius-lg --border-radius-xl --border-radius-pill
--link-color --link-hover-color --focus-ring-color --focus-ring-width
--box-shadow --box-shadow-sm --box-shadow-lg --font-sans-serif --gray-100 … --gray-900
```
Rules:
- **Alias, don't copy.** `--ac-color-primary: var(--primary, #007bff);` keeps the website theme
editor in control of the palette. A hardcoded brand hex breaks theming silently.
- **Hex literals live only in `base/variables.css`.** A hex anywhere else is a defect.
- **Prefix new tokens** (`--ac-…`, aplicoop). Bare names like `--primary-color` sit in Bootstrap's
unprefixed namespace and will collide as Odoo adds tokens.
- **Never redefine a token inside a media query** to shrink it — use `clamp()` so it scales
continuously instead of jumping at one width.
- Space and radius come from the scale, never from a typed-in number. If you need a value the scale
doesn't have, the scale is wrong — fix the scale.
## 3. Responsive
- **Mobile-first, `min-width` only.** Bootstrap 5 breakpoints exclusively: **576 / 768 / 992 / 1200 /
1400**. No 480/720/1600 one-offs — a private breakpoint is a design decision nobody else knows about.
- **One range per rule.** Pairing `min-width` and `max-width` to carve a band is almost always a
symptom of desktop-first thinking leaking in.
- **Media queries belong with their component**, at the bottom of its file. A central
`responsive.css` drifts out of sync with the components it overrides and forces `!important`.
Don't add to it; empty it as you touch each component.
If a rule in `responsive.css` spans multiple components or has no clear owner, move it to the
relevant layout partial (e.g. `layout/grid.css`) rather than duplicating it per component; note
any such moves in the audit output.
- **Prefer intrinsic layout to breakpoints**: `repeat(auto-fill, minmax(min(100%, 16rem), 1fr))`,
`clamp()`, `flex-wrap`. Most grids need zero media queries.
- `rem` for everything; `px` only for hairlines, borders and shadow offsets.
- Touch targets: **24×24 px is the AA floor** (WCAG 2.2 §2.5.8); **44×44 px is §2.5.5, level AAA**,
and the size a thumb actually needs — apply it under `@media (pointer: coarse)`. Quantity steppers
and icon buttons are the usual offenders.
- Must survive **320 px wide** and **200 % zoom** with no horizontal scroll.
Details and recipes: `references/responsive.md`.
## 4. Accessibility — WCAG 2.2 AA baseline
- **Semantics before ARIA**: `<button>`, `<nav>`, `<ul>`, `<label for>`. ARIA only when no element
expresses the meaning. A `<div onclick>` is never acceptable.
- **Contrast**: 4.5:1 body text, 3:1 for ≥24 px (or bold ≥19 px), 3:1 for form-control borders,
icon-only buttons and focus rings.
- **Focus**: never `outline: none` without a replacement. Style `:focus-visible`, not `:focus` — the
latter also fires on mouse click and gets removed out of frustration.
- Every input has a real `<label>`. A placeholder is not a label.
- Icon-only control → translatable `aria-label`, or `.visually-hidden` text (Bootstrap ships the class).
- Dynamic updates (cart total, quantity, result count) → `aria-live="polite"` region.
- Images: meaningful `alt`, or `alt=""` when decorative. Never omit the attribute.
- Status is never conveyed by colour alone — pair it with text or an icon.
- Every animation you add needs a `@media (prefers-reduced-motion: reduce)` escape.
**Translatable QWeb attributes** (auto-extracted, `odoo/tools/translate.py:163`), plus their
`t-attf-` forms:
```
string help placeholder alt title label data-tooltip confirm
aria-label aria-keyshortcuts aria-placeholder aria-roledescription aria-valuetext
```
`aria-description` is **not** translated — use `aria-label` or visually-hidden text instead.
Strings injected from JS go through `window.i18nManager.get(key)` (served by `/eskaera/i18n`), with
the key added server-side — never a hardcoded literal, including in live-region announcements.
Per-component patterns: `references/accessibility.md`.
## 5. JS for finish (UI only)
- **Delegated events** — content is lazy-loaded and infinite-scrolled, direct listeners die.
- After injecting nodes: move or restore focus deliberately, announce the new count in the live
region, and set `aria-busy` on the container while fetching.
- Check `matchMedia("(prefers-reduced-motion: reduce)")` before animating or smooth-scrolling.
- Toggle **classes**, don't write `element.style.*` — inline styles from JS are the same defect as
inline styles in templates, just later.
- Debounce search input ≥ 200 ms and announce "N results" politely.
- Batch DOM reads and writes; never read `offsetHeight` inside a loop that also writes.
## 6. Asset delivery
`website_sale.css` pulls its 17 partials with `@import`. Odoo rewrites those to absolute URLs and
hoists them to the top of the bundle (`odoo/addons/base/models/assetsbundle.py:909` and `:525`), so
they ship as ~17 extra render-blocking requests **outside** the minified bundle. For a production
finish, prefer listing each file in the manifest's `web.assets_frontend` (order: variables →
utilities → layout → components → sections). Only rename them to `.scss` if the maintainer
explicitly requests SCSS compilation so Odoo inlines the imports at compile time.
## 7. Audit & verify
If `/media/snt/data/addons-cm/website_sale_aplicoop` does not exist, run the audit commands from the
repository root of the current workspace instead, and report which path was used.
```bash
cd /media/snt/data/addons-cm/website_sale_aplicoop
# Breakpoint sprawl — should only ever list 576/768/992/1200/1400, all min-width
grep -rho "@media[^{]*" static/src/css | sort | uniq -c | sort -rn
# Hardcoded colours outside the token file (expect: none)
grep -rn "#[0-9a-fA-F]\{3,8\}\b" static/src/css --include="*.css" | grep -v "base/variables.css"
# Specificity debt
grep -rc "!important" static/src/css/*/*.css | grep -v ':0'
# Focus handling: :focus-visible should dominate, outline:none must be paired with a replacement
grep -rn ":focus-visible\|outline: *none\|outline: *0" static/src/css
# Motion safety (expect at least one per animating component)
grep -rn "prefers-reduced-motion" static/src/css
# Inline styles in templates (expect: none)
grep -rn 'style="' views/*.xml
# Images without alt
grep -o "<img[^>]*" views/*.xml | grep -v "alt="
# Interactive divs/spans (expect: none)
grep -rn "<div[^>]*t-on-click\|<span[^>]*t-on-click\|<div[^>]*onclick" views/*.xml
```
Then, always:
```bash
pre-commit run --all-files # prettier (width 100, indent 4) over css/js/xml + eslint
docker-compose up -d # http://localhost:8070 — check at 320/768/1280 and 200% zoom
```
Manual passes that no grep replaces: **Tab through the whole page** (visible focus everywhere, order
matches visual order, no trap), and check contrast on the actual rendered colours.
## 8. References
- `references/accessibility.md` — WCAG AA patterns as QWeb snippets, per component.
- `references/responsive.md` — breakpoints, intrinsic layout, images, test matrix.
- `references/css-architecture.md` — file layout, naming, specificity, killing `!important`.
- `references/finish-checklist.md` — the polish pass: states, rhythm, empty/loading/error.
Repo docs: `docs/QWEB_BEST_PRACTICES.md`, `docs/LAZY_LOADING.md`.