Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/designs/822-mui-revamp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<p>`. | 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 |

Expand Down Expand Up @@ -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.

Expand All @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
79 changes: 61 additions & 18 deletions web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
text-underline-offset: 2px;
}

:focus-visible {
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 <label> wrappers that set font-weight 500
(see the form-label rule); matching 500 keeps the trigger's line box —
and therefore its total height — identical, or the form-grid's bottom
Expand All @@ -3335,10 +3376,12 @@ button.named-picker-trigger {
letter-spacing: 0;
}

/* #834 — a form control's hover edge, not a link: --stat-accent for the
same reason as .help-open/.help-link's hover rings above. */
button.named-picker-trigger:hover:not(:disabled) {
background: var(--surface);
color: var(--ink);
border-color: var(--link);
border-color: var(--stat-accent);
text-decoration: none;
}

Expand Down
Loading
Loading