diff --git a/docs/designs/822-mui-revamp.md b/docs/designs/822-mui-revamp.md index 453acd4c8..f99e37ba0 100644 --- a/docs/designs/822-mui-revamp.md +++ b/docs/designs/822-mui-revamp.md @@ -190,7 +190,7 @@ All from `@mui/material` 9.4.0 (`package-lock.json`). No `@mui/x-*` package; no | 17 | `p.error` / `p.warn` `role="alert"` | `Alert severity` | `Alert` renders `role="alert"` by default; the E2E "no alert on screen" reading (`AppLayout.tsx:105-110`) depends on that, and the permanent live region at L111 must stay a plain `

`. | each screen slice | | 18 | `.update-banner` | `Snackbar` holding an `Alert` with actions | Keeps `--shadow-bar` via the elevation map (D6). | #828 | | 19 | `ThemeToggle` | `IconButton` (icon-only sites) and `Button variant="text"` (labelled) | `applyTheme` unchanged; the observer in `FarmThemeProvider` re-reads. | #829 | -| 20 | `GlossaryLink`, `.content a` | `Link` | colour from `palette.info` until #834, then `primary` (D7.1) | #833 | +| 20 | `GlossaryLink`, `.content a` | `Link` | colour from `palette.info` until #834, then `--ink` underlined in `--link-rule` (owner decision 2026-09-16, DIRECTION.md, supersedes D7.1's brand-derived proposal — see the amendment on D7.1 below) | #833 | | 21 | `DayStrip`, `StockBar`, `.meter`, `.meter-stack`, `GradingChip`, `BrandSplash`, `ProvenanceCell`, `FarmDate` | **kept as they are** | bespoke data marks and interactions (#654, #777, F134, #179); their CSS stays (§2.2). `StockBar` is a multi-grade stacked bar and `LinearProgress` is single-value; `GradingChip` is a drag source (§2.1). No alternative meets the functionality, so these are not owner-review rows. | none | | 22 | `usePagedList` "Load more" | `Button` | the hook (381 lines, ticket discipline) is not presentation and stays; there is no offset `Pagination` to adopt. | none | @@ -284,7 +284,9 @@ So adopting it re-leads every paragraph, table cell and label that does not set ### D7. The three identity questions (#656), decided here so #834 to #836 inherit a decision -**D7.1 Links derive from the brand: yes. Owner review.** `--link: var(--brand)` in light, `--link: var(--stat-accent)` in night; `--link-hover` is `--brand-press` in light and the same `--stat-accent` at 85% opacity in night. Verified with the WCAG formula over the declared values, on `--surface` and `--surface-2`, all four palettes: each palette resolved through its own `data-brand` block (the `--surface-2` value differs per palette, so a fixed value gives wrong numbers): light `--brand` on `--surface` / `--surface-2` is 14.00/12.57 (aubergine), 11.25/10.23 (forest), 11.63/10.44 (slate), 10.90/9.63 (terracotta); night `--stat-accent` is 11.05/9.99, 10.95/9.66, 10.39/9.32, 9.67/8.91. Every pair clears 4.5:1 with margin, worst case 8.91; today's blue is 6.22 light and 7.70 night, so contrast rises everywhere. Pinned in `styles.test.ts` with the repo's own `contrast()` (`test/cssTokens.ts`), not with the numbers above. The one reason this stays an owner call is legibility rather than contrast: an aubergine link can read as a visited link, and a terracotta one as an error. The alternative, if that reads badly in the #834 captures, is a fifth hand-chosen `--link` per palette block, brand-adjacent and contrast-checked like `--stat-accent`, which also means adding it to `LIGHT_REQUIRED` (L15), `DARK_REQUIRED` (L26) and `GOLDEN` (L161) in `styles.test.ts` in the same commit, or the relation test at L238 fails. Consequence stated deliberately: `palette.info.main` (`FarmThemeProvider.tsx:34`) then equals `primary`, so an `Alert severity="info"` reads as brand-coloured, which is the intended monotheism. The sidebar's `.sidebar button.link` override (L1441-1453) already forces on-brand colour there and is unaffected. +**D7.1 Links derive from the brand: superseded.** The owner decided against it on 2026-09-16 (DIRECTION.md line 15: "Links are ink, underlined in a 28% ink rule"; line 22 rejects the blue link colour): a link is `--ink` text, underlined at rest in a new `--link-rule` token (28% ink, flattened over `--surface`), full ink on hover and focus. `--link` becomes an alias for `--ink` rather than a brand derivation, so `palette.info.main` (`FarmThemeProvider.tsx`) is repointed at `--stat-accent` instead of inheriting `--link` — the same "info is brand-adjacent, not plain text" outcome the original proposal wanted, reached a different way. #834 implements this and pins it in `styles.test.ts`/`farmTheme.policy.test.ts`. + +**D7.1, original proposal, superseded, kept for history:** `--link: var(--brand)` in light, `--link: var(--stat-accent)` in night; `--link-hover` is `--brand-press` in light and the same `--stat-accent` at 85% opacity in night. Verified with the WCAG formula over the declared values, on `--surface` and `--surface-2`, all four palettes: each palette resolved through its own `data-brand` block (the `--surface-2` value differs per palette, so a fixed value gives wrong numbers): light `--brand` on `--surface` / `--surface-2` is 14.00/12.57 (aubergine), 11.25/10.23 (forest), 11.63/10.44 (slate), 10.90/9.63 (terracotta); night `--stat-accent` is 11.05/9.99, 10.95/9.66, 10.39/9.32, 9.67/8.91. Every pair clears 4.5:1 with margin, worst case 8.91; today's blue is 6.22 light and 7.70 night, so contrast rises everywhere. The one reason this stayed an owner call was legibility rather than contrast: an aubergine link can read as a visited link, and a terracotta one as an error. The alternative on the table was a fifth hand-chosen `--link` per palette block, brand-adjacent and contrast-checked like `--stat-accent`. The sidebar's `.sidebar button.link` override (L1441-1453) already forces on-brand colour there and is unaffected either way. **D7.2 Optical size: adopt it, but the premise was wrong and the cost is not zero.** The loaded face is `wght`-only (§1). #835 swaps `main.tsx:3` to `@fontsource-variable/inter/opsz.css`, which replaces the seven `wght` files with the seven `opsz` files (they also carry `wght`, `font-weight: 100 900` in both entries). Once that face is loaded, prefer `font-optical-sizing: auto` on the theme's `h1`, `h2`, on the figure class that stays in CSS (`.trend-kpi` L2045) and on the phone house card's figure (today `.capture-tile-eggs` L1778, a `Typography` variant after #829): `auto` lets the browser interpolate from a 1280 heading to a 390 one, which one fixed `"opsz" 32` cannot. Because headings are theme variants after #823, the setting reaches `Typography` without a second declaration, which is the interaction #835 flagged. Cost: +118.9 KiB precache, +24.1 KiB on the latin subset a phone actually fetches. #835 runs after #825 and is charged against its ceiling (D9); if the ceiling cannot absorb it the fallback is one display cut for `h1`/`h2` only, and the doc records which was chosen. @@ -307,7 +309,7 @@ Order is by dependency, then by what each slice teaches the next. **Deletions ar | 9 | **#831** ledgers (+ Feed, Water, Reports) | D3.3 row 3; pairs 7, 9, 10, 11, 15; delete `.order-panel` (L1728-1731, last consumer Flocks moves to #832 if Flocks converts later) and `tr.discounted .badge-warn` (L1225-1253, Sales only); **keep** `.toolbar` L1168-1193 (seventh consumer is `AuditPage.tsx:432`, #833, and deleting early reopens #653), `table.data` L1623-1695 and `tr.inactive td` L1686 (the five CRUD lists in #832 and Audit in #833 still render them), `.page-head` L1571, and `.muted`/`.error`/`.success`/`.warn` (L1195-1224; `.muted` alone has 103 sites in 24 files including `ErrorBoundary`, `EmptyState` and `AppLayout`, and maps to `Typography color="text.secondary"` screen by screen under pair 10); one PR per screen when the diff is large (Sales alone has 4,110 test lines) | each screen's table and filter bar at both widths; Sales with a drill-down open | | 10 | **#832** CRUD lists | D3.3 row 4; Flocks' `.order-panel` | each list and one open form dialog at both widths; Users' RBAC behaviour is fixed (3,872 test lines) | | 11 | **#833** tail (+ Account, SetPassword) | D3.3 row 5; pairs 16, 20; delete the login and help blocks L2264-2796 (including `.glossary-entry:target`), the farm-settings block L3090-3278 (`.update-banner` L3279-3322 went with #828), the L316-534 remainder, and as last consumer: `.toolbar` L1168-1193, `table.data` L1623-1695, `.page-head` and `.content` L1559-1622, the badge rules L1275-1296, the semantic-text rules L1195-1224, and the shared `.card` rule; retire the last CSS guards | Login in both themes, Help, Settings, Audit at both widths | -| 12 | **#834** link colour | D7.1 | Dashboard, a list screen and Login in both themes, all four palettes (`playwright.palettes.config.ts` already captures this set) | +| 12 | **#834** link colour | D7.1 (superseded by the owner's DIRECTION.md decision, 2026-09-16 — see the amendment on D7.1) | Dashboard, a list screen and Login in both themes, all four palettes (`playwright.palettes.config.ts` already captures this set) | | 13 | **#835** opsz | D7.2 | a page title and a figure at 1280 before and after, 1:1 | | 14 | **#836** mark | D7.3 | the sidebar mark, favicon at 16 and 32px, home-screen icon, all four palettes | | after | **#50** offline capture | untouched by this doc; it captures from #830's screen, so it waits for #830; D9's ceiling constrains what it may precache | n/a | @@ -363,7 +365,7 @@ Product calls this doc makes that the owner reviews before or at the slice that | `NumberField` on `TextField` + `IconButton`, or Base UI `NumberField` | D2 pair 3, #828 | the other option; measured cost decides | | Form dialogs `fullScreen` below 900px | D2 pair 2, D3.3, #827/#832 | a bottom-sheet variant on MUI `Dialog` | | Daily-entry footer buttons stack full-width, reversing F134's side-by-side pair | D3.4, #823 | keep the footer side by side and stack only `.actions` and dialog foots | -| Links take `--brand` / `--stat-accent` | D7.1, #834 | a fifth hand-chosen `--link` per palette | +| Links take `--brand` / `--stat-accent` (D7.1's original proposal, superseded) | D7.1, #834 | what #834 actually shipped: `--ink`, underlined in a new `--link-rule` (28% ink), per the owner's DIRECTION.md decision (2026-09-16) | | Ceiling 1,800 KiB | D9, #825 | 1,600 KiB and the `opsz` fallback | ## 8. Simplicity ceiling diff --git a/web/src/styles.css b/web/src/styles.css index eeacbaeda..1d2042ca9 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -17,11 +17,17 @@ --on-brand: #ffffff; --on-brand-mute: #d9bdde; - /* Ink & links (blue is the only chromatic departure from aubergine) */ + /* Ink & links (#834 retires the Slack-blue departure — DIRECTION.md, owner + decision 2026-09-16, supersedes D7.1's brand-derived `--link: var(--brand)` + proposal: a link is ink, underlined in a 28% ink rule, full ink on hover + and focus. --link stays its own name rather than folding into --ink at + every call site — it names what the colour is FOR, and it is the smaller + diff over rewriting six consumers. --link-hover is retired: "full ink on + hover" is already --ink, so no separate hover shade is needed. */ --ink: #1d1d1d; --muted: #696969; - --link: #1264a3; - --link-hover: #3860be; + --link: var(--ink); + --link-rule: #c0c0c0; /* 28% --ink flattened over --surface (#ffffff) */ /* Surfaces */ --canvas: #faf7fc; /* app background — a whisper of lavender */ @@ -197,8 +203,8 @@ :root[data-theme="dark"] { --ink: #f2ecf4; --muted: #b3a8b8; - --link: #7fb4e6; - --link-hover: #a9cef0; + --link: var(--ink); + --link-rule: #5c5560; /* 28% --ink flattened over --surface (dark) */ --canvas: #17121a; --surface: #221a26; --surface-2: #2b2231; @@ -383,7 +389,10 @@ button.btn-danger:hover:not(:disabled) { background: var(--danger-press); } -/* Inline action / navigation buttons — text, not pills. Blue, like links. */ +/* Inline action / navigation buttons — text, not pills. Ink, like links + (#834): underlined at rest in the 28% ink rule, full ink on hover and + keyboard focus (DIRECTION.md), the latter alongside the global + `:focus-visible` outline below, not instead of it. */ button.link { background: none; color: var(--link); @@ -392,12 +401,17 @@ button.link { letter-spacing: 0; padding: 0.3rem 0.4rem; border-radius: var(--r-input); + text-decoration: underline; + text-decoration-color: var(--link-rule); + text-underline-offset: 2px; } -button.link:hover:not(:disabled) { +button.link:hover:not(:disabled), +button.link:focus-visible { background: none; - color: var(--link-hover); text-decoration: underline; + text-decoration-color: var(--ink); + text-underline-offset: 2px; } :focus-visible { @@ -1508,12 +1522,16 @@ tr.discounted .badge-warn { its own single class. */ :where(.content a) { color: var(--link); - text-decoration: none; + text-decoration: underline; + text-decoration-color: var(--link-rule); + text-underline-offset: 2px; } -:where(.content a):hover { - color: var(--link-hover); +:where(.content a):hover, +:where(.content a):focus-visible { text-decoration: underline; + text-decoration-color: var(--ink); + text-underline-offset: 2px; } /* ---------------------------------------------------------------- Tables */ @@ -2256,8 +2274,11 @@ label.check { text-decoration: none; } +/* #834 — this ring is a hover highlight, not a link underline: --stat-accent + is this stylesheet's existing "interactive edge" colour (.palette-option's + checked border), not a repurposed --link. */ .help-open:hover { - border-color: var(--link); + border-color: var(--stat-accent); text-decoration: none; } @@ -2366,6 +2387,10 @@ dl.glossary { text-decoration: none; } +/* #834 — color: var(--link) already equals --ink via the alias, so this + headword keeps its rest-state colour on hover by design (only the + underline reveals the link); no rule-underline is added, since the rest + state's own "no underline until hover" is the intended affordance here. */ .glossary-entry dt a:hover { color: var(--link); text-decoration: underline; @@ -2395,9 +2420,12 @@ dl.glossary { vertical-align: middle; } +/* #834 — a small ring badge (a "?" glyph), not underlined text: the text + colour still follows --link (now ink), but the ring is a hover highlight, + not a link underline, so its edge takes --stat-accent like .help-open's. */ .help-link:hover { color: var(--link); - border-color: var(--link); + border-color: var(--stat-accent); text-decoration: none; } @@ -3306,22 +3334,35 @@ input.locked { font-size: 0.85rem; color: var(--link); padding: 0.15rem 0.4rem; + text-decoration: underline; + text-decoration-color: var(--link-rule); + text-underline-offset: 2px; } -.named-picker-loadmore:hover:not(:disabled) { - color: var(--link-hover); +.named-picker-loadmore:hover:not(:disabled), +.named-picker-loadmore:focus-visible { text-decoration: underline; + text-decoration-color: var(--ink); + text-underline-offset: 2px; } /* Trigger button: the page-owned button that opens the picker. Uses the same metrics as form inputs so it aligns with neighboring date/text fields. The button.named-picker-trigger selectors below are the load-bearing ones: - pages give the trigger the shared `link` class, and `button.link` + IF a page ever gives the trigger the shared `link` class (none does today — + every real caller passes `className="named-picker-trigger"` alone; only a + test fixture combines them, salesExpensesUS3.lifecycle.test.tsx), `button.link` (element+class, (0,1,1)) outranks bare `.named-picker-trigger` ((0,1,0)) — without the element-qualified override the link's padding/font-size win and the trigger's box comes out 12px shorter than its adjacent inputs, so - the form-grid's bottom alignment leaves its top edge sitting low. */ + the form-grid's bottom alignment leaves its top edge sitting low. + #834 (Codex review, round 4): button.link now also sets `text-decoration` + at rest, which the SAME bleed-through would apply to the trigger — a form + control, not a link — the moment that combination occurs. text-decoration: + none below closes it defensively, matching the padding/font-size defense + this rule already existed for. */ button.named-picker-trigger { + text-decoration: none; /* The adjacent inputs sit inside