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

191 lines
5.6 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.

# Responsive layout
## Breakpoints — Bootstrap 5, and only these
| Name | `min-width` | Typical device |
| --- | --- | --- |
| (base) | — | phones, 320575 px |
| `sm` | 576px | large phones |
| `md` | 768px | tablets |
| `lg` | 992px | small laptops |
| `xl` | 1200px | desktops |
| `xxl` | 1400px | large desktops |
Mobile-first, `min-width` only:
```css
/* ✅ base = mobile, enhance upwards */
.products-grid {
display: grid;
gap: var(--ac-space-md);
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.products-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1200px) {
.products-grid {
grid-template-columns: repeat(4, 1fr);
}
}
```
```css
/* ❌ desktop-first bands: overlapping ranges, unclear cascade, private breakpoints */
@media (max-width: 1599px) and (min-width: 1400px) { }
@media (max-width: 720px) { }
```
## Prefer intrinsic layout — most grids need no media query at all
```css
.products-grid {
display: grid;
gap: var(--ac-space-md);
grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
}
```
`min(100%, 16rem)` is what keeps it from overflowing at 320 px. Reach for a media query only when the
layout must genuinely *change* (sidebar moves, table becomes cards), not merely resize.
## Fluid type and space
```css
:root {
--ac-text-sm: clamp(0.875rem, 0.85rem + 0.1vw, 0.9375rem);
--ac-text-base: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
--ac-text-lg: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
--ac-text-xl: clamp(1.5rem, 1.3rem + 1vw, 2.25rem);
--ac-space-md: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
}
```
One continuous scale beats three step-changes at breakpoints. Never redefine a token inside a media
query to make it smaller.
Body copy: 1rem minimum, line-height ≥ 1.5, measure 4575 characters (`max-width: 65ch`).
## Touch targets and pointer
WCAG 2.2 AA (§2.5.8 Target Size, Minimum) asks for **24×24 px**. The familiar **44×44 px** is §2.5.5,
level **AAA** — treat it as the house rule for anything a thumb hits, applied by pointer type rather
than by viewport width: a 1024 px tablet needs the big target, a 1024 px laptop doesn't.
```css
.qty-control .btn,
.tag-filter-badge {
min-width: 2.25rem; /* 36px — comfortable with a mouse */
min-height: 2.25rem;
}
@media (pointer: coarse) {
.qty-control .btn,
.tag-filter-badge {
min-width: 2.75rem; /* 44px */
min-height: 2.75rem;
}
}
@media (hover: hover) and (pointer: fine) {
.product-card:hover {
transform: translateY(-2px);
box-shadow: var(--ac-shadow-md);
}
}
```
Hover effects belong inside `@media (hover: hover)` — on touch they stick after a tap and look broken.
## Tables → cards, without restructuring
```css
@media (max-width: 767.98px) {
.checkout-summary-table thead {
/* keep it in the a11y tree; hide visually only */
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.checkout-summary-table tr {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--ac-space-xs);
padding: var(--ac-space-sm) 0;
border-bottom: 1px solid var(--ac-border-light);
}
}
```
This is the one place a `max-width` query is honest: it's an override of the desktop table. Keep the
markup semantic — never emit different HTML per viewport.
## Sticky bars (cart / checkout actions on mobile)
```css
.checkout-actions {
position: sticky;
bottom: 0;
z-index: var(--ac-z-sticky);
padding: var(--ac-space-sm);
padding-bottom: calc(var(--ac-space-sm) + env(safe-area-inset-bottom, 0px));
background: var(--ac-surface, #fff);
box-shadow: 0 -2px 8px rgb(0 0 0 / 8%);
}
```
`env(safe-area-inset-bottom)` keeps the button above the iOS home indicator. Sticky elements must not
eat more than ~20 % of a 568 px-tall viewport.
## Images
```xml
<img class="product-card-img" t-att-src="product['image_url']" t-att-alt="product['name']"
loading="lazy" decoding="async" width="400" height="400"/>
```
- `width`/`height` always — they reserve the box and prevent layout shift (CLS).
- `loading="lazy"` for everything below the fold; **never** on the LCP image (first product row,
hero) — that delays the largest paint.
- CSS: `max-width: 100%; height: auto;` plus `object-fit: cover` with a fixed `aspect-ratio` when
cards must align.
- Odoo serves resized variants: `/web/image/product.product/<id>/image_256` etc. Ask for the size you
render, not `image_1920` scaled down in CSS.
## Container and page rhythm
```css
.eskaera-page {
width: 100%;
max-width: 75rem;
margin-inline: auto;
padding-inline: clamp(1rem, 4vw, 2rem);
}
```
Use logical properties (`margin-inline`, `padding-block`, `inset-inline-start`) — Odoo auto-generates
an RTL stylesheet, and logical properties survive the flip without a second rule.
## Test matrix
| Width | Why |
| --- | --- |
| 320 px | smallest supported; the one that breaks first |
| 360 / 390 px | real Android / iPhone |
| 768 px | tablet portrait, first breakpoint |
| 1024 px | tablet landscape / small laptop |
| 1280 / 1440 px | desktop |
| 1280 px @ 200 % zoom | WCAG 1.4.4 reflow — equivalent to 640 px |
Run the stack (`docker-compose up -d`, http://localhost:8070) and check, per viewport: no horizontal
scroll, no clipped text, touch targets still ≥ 44 px on coarse pointers, sticky bars not covering
content, images not stretched.
DevTools device toolbar is enough for layout; check touch targets by actually tapping on a phone once
before calling it done.