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

9.3 KiB
Raw Blame History

name description metadata
odoo-web-design 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.
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.

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:

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.