From e01a9a5aa9ea890fc7e7971aa9032255a92d74a9 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 2 Aug 2026 10:49:50 +0800 Subject: [PATCH 1/6] DS V2 PR-V 5a: close out token values (colour, elevation, ink) No value deltas remain for slice 5a. 59e4c3dfc landed --shadow-well, the spine and status-mark families and the dark --clinical-chat-document fix; PR 3 (#1570) landed the remaining ink roles. The v2 light and dark blocks declare the same colour set, and the design-system contract reports rawColorLiterals 2 / literalShadowClasses 0 / legacyTapClasses 0 against an unchanged baseline. The two surviving #0f766e literals restate a Postgres column default for a per-record accent colour, so they are data rather than design tokens and stay put; SPEC now records why, to stop a later reader 'fixing' the app default away from the database default. --- docs/design-system/SPEC.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/design-system/SPEC.md b/docs/design-system/SPEC.md index 23d3a7406..0b9ba0126 100644 --- a/docs/design-system/SPEC.md +++ b/docs/design-system/SPEC.md @@ -640,11 +640,20 @@ and adoption.** Status keys as in the header; "done" entries cite their commit. ### Phase 2 — values, split three ways -| PR | Contents | Status | -| --------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| PR 5a · Token values, no geometry | Colour, elevation, ink roles | **partially done** — `59e4c3dfc` landed the `--shadow-well` rename, the spine and status-mark families, and the dark `--clinical-chat-document` fix; the remaining colour/ink deltas are open | -| PR 5b · Tap 44→48 in `@theme` | The 407-site geometry change; contract-test pin update; visual QA pass; `--tap-min` becomes the alias | open | -| PR 5c · Radius step | Every `rounded-md` moves; its own visual diff | open | +| PR | Contents | Status | +| --------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| PR 5a · Token values, no geometry | Colour, elevation, ink roles | **done** — `59e4c3dfc` landed the `--shadow-well` rename, the spine and status-mark families, and the dark `--clinical-chat-document` fix; the remaining ink-role deltas landed with PR 3 (`--text-placeholder`, eyebrows and placeholders off the decoration tier, the disabled encoding). The v2 light and dark blocks now declare the same colour roles, and the only raw colour literals left in `src/` are the two `#0f766e` accent defaults, which are **not** design tokens — see the note below | +| PR 5b · Tap 44→48 in `@theme` | The 407-site geometry change; contract-test pin update; visual QA pass; `--tap-min` becomes the alias | open | +| PR 5c · Radius step | Every `rounded-md` moves; its own visual diff | open | + +**`#0f766e` is data, not a token.** The two remaining raw colour literals +(`src/lib/medications.ts`, `src/lib/medication-records.ts`) restate a Postgres column +default (`accent text not null default '#0f766e'`) for a per-record, user-chosen accent +colour. Changing the application default without migrating the database default would +diverge the two, so both stay. The raw-colour ratchet in +`scripts/design-system-contract-baseline.json` is a **ceiling, not a target**: it exists to +stop new literals appearing, and a value that is persisted data rather than design intent +is outside the token system entirely. ### Phase 3 — safety structure From d927c64b23feb385876f0af2a123a7fb1ef7f3a4 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 2 Aug 2026 11:03:39 +0800 Subject: [PATCH 2/6] DS V2 PR-V 5b: move the tap knob to 48px --spacing-tap goes 2.75rem -> 3rem in the globals @theme block, which is the only place it may be set (SPEC 4.10, DECISIONS C2). The v2 layer's --tap-min stops carrying its own value and becomes the pure alias var(--spacing-tap), so the app cannot render a 44/48 split. This moves 426 *-tap call sites across 90-odd files at once. Pins flip in this commit, not a follow-up: ckb-v2-token-contract now asserts the alias, that the v2 layer never declares the knob, and that the @theme value is at least 48px (checked on resolved rem, since a string compare would pass on an alias); ui-style-contract raises its rendered Chromium floor to 48; ui-tools expects the mode-home hero tile at 48. Two follow-through rules came out of the sweep and are written into SPEC 4.10. A grid track that holds a tap-sized child reads var(--spacing-tap) instead of copying the number - seven hardcoded 2.75rem tracks were audited and the launcher search row, the one with no gap between columns, was rebound; the other six absorb the extra 4px into an existing gap or the row's own padding. And the phone composer keeps a deliberate 44px below 431px, because its height is part of the search-chrome contract and 4px per button would re-tune the dock reserve and the phone CLS budget for no accessibility gain. Also retires two min-h-[44px] literals on interactive links onto min-h-tap, and rewrites the comments that restated 44px so they name the token instead. GATES gate 2 moves to implemented-blocking. --- docs/design-system.md | 15 ++-- docs/design-system/DECISIONS.md | 14 +++- docs/design-system/GATES.md | 30 ++++---- docs/design-system/SPEC.md | 30 +++++--- docs/design-system/TOKENS.md | 76 +++++++++---------- src/app/ckb-v2-tokens.css | 6 +- src/app/globals.css | 38 ++++++---- src/components/applications-launcher-page.tsx | 5 +- .../clinical-output-helpers.tsx | 2 +- .../clinical-dashboard/evidence-panels.tsx | 2 +- src/components/mode-nav/mode-nav.tsx | 2 +- src/components/ui-primitives.tsx | 4 +- src/components/ui/button.tsx | 2 +- src/components/ui/chip.tsx | 2 +- src/components/ui/citation.tsx | 2 +- tests/ckb-v2-token-contract.test.ts | 26 ++++++- tests/ui-style-contract.spec.ts | 6 +- tests/ui-tools.spec.ts | 6 +- 18 files changed, 168 insertions(+), 100 deletions(-) diff --git a/docs/design-system.md b/docs/design-system.md index 6a421d394..af80d224e 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -121,16 +121,19 @@ Icon **glyphs** use the parallel `--spacing-icon-*` scale in `@theme`: a few roles (nav, composer, hero, panel headings); most icons stay one fixed size. - **Not** for container tiles (`iconTilePremium` / panel-heading tile h-9, empty-state tile h-10) or non-icon boxes (the `ToggleSwitch` knob, status dots) — those keep the integer spacing - scale. Icon glyph size is independent of the 44px tap target (§3), which stays on + scale. Icon glyph size is independent of the tap target (§3), which stays on `--spacing-tap`. ## 3. Spacing & tap targets - 4px grid via Tailwind spacing; safe-area env paddings on shell edges. -- Interactive targets use the `--spacing-tap` token (44px): `min-h-tap` / `min-w-tap` / - `size-tap`. Do **not** hand-write `min-h-11` / `h-[44px]` for tap semantics. -- Exception (documented in `globals.css`): controls scrolled deep inside sheets stay on - `min-h-12` (48px) to satisfy the ui-smoke sub-pixel tap check — do not "fix" them down. +- Interactive targets use the `--spacing-tap` token (48px): `min-h-tap` / `min-w-tap` / + `size-tap`. Do **not** hand-write `min-h-11` / `h-[44px]` for tap semantics, and size a grid + track that holds a tap target with `var(--spacing-tap)` rather than a copy of the number. +- `min-h-12` sites now match the token instead of exceeding it — leave them; never "fix" a tap + target down. +- Exception (documented in `globals.css`): the phone composer icon buttons stay 44px below + 431px, because the dock height is part of the search-chrome contract. ## 4. Radius & shadows @@ -172,7 +175,7 @@ rung — never a new number. the applications-launcher DetailDialog migration is the template for converting one. - Empty and loading states use `EmptyState` / `LoadingPanel`, not bespoke markup. - **Icon-only buttons use `IconButton`** (`ui-primitives.tsx`): its `label` prop is required and - renders `aria-label` + an `aria-hidden` glyph + a 44px tap target, so an unlabeled icon button + renders `aria-label` + an `aria-hidden` glyph + a `--spacing-tap` hit area, so an unlabeled icon button cannot be written by accident. Pass a recipe (`toolbarButton`, …) via `className` for chrome. - Composer-chrome caveat: the `answer-footer-search-*` / `desktop-home-search-*` classes are intentionally **unlayered** and beat Tailwind utilities on the same element — check the class diff --git a/docs/design-system/DECISIONS.md b/docs/design-system/DECISIONS.md index cf628fe78..8e6f621a5 100644 --- a/docs/design-system/DECISIONS.md +++ b/docs/design-system/DECISIONS.md @@ -42,9 +42,17 @@ anything that cannot be verified is listed as blocked rather than guessed. `globals.css` — the token the `min-h-tap` / `h-tap` / `size-tap` utilities compile against. `--tap-min` in the v2 layer becomes a pure alias (`var(--spacing-tap)`), and the v2 layer never declares `--spacing-tap`. What moves with it, in the same change: the contract-test -pin (`ckb-v2-token-contract.test.ts:181-185`), the `Button` comment that still claims 44px, -and a dedicated visual-QA pass — it is a 407-call-site geometry change and ships as its own -PR (5b). No existing 48px target (`min-h-12`, 53 sites) is touched. +pins, the component comments that still claim 44px, and a dedicated visual-QA pass — it is a +426-call-site geometry change and ships as its own commit (5b). No existing 48px target +(`min-h-12`) is touched. + +**Landed (PR 5b).** Two follow-through rules came out of doing it, both now in SPEC §4.10. +A grid track sized to hold a tap-sized child reads `var(--spacing-tap)` instead of copying +its value, or the track and its child desync on the next move — seven hardcoded `2.75rem` +tracks were audited, and the one with no gap between columns (the launcher search row) was +rebound; the other six absorb the extra 4px into an existing gap or the row's own padding. +And the phone composer keeps a written 44px exception below 431px, because its height is +part of the search-chrome contract and 44px already clears WCAG 2.5.5. **Rejected.** Declaring the value in the v2 layer. In the design side's `:root`-structural copy it silently **loses** a same-specificity cascade tie to `@theme` (later wins), leaving diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md index 8e7273fc4..30079567c 100644 --- a/docs/design-system/GATES.md +++ b/docs/design-system/GATES.md @@ -37,20 +37,20 @@ those rules as gates. ## 2 · The twelve system gates, labelled -| # | Gate | Status | Evidence / path to blocking | -| --- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | Contrast ≥4.5:1 for every text/background pair, both themes, live **and** v2; `--decoration-soft` asserted below 4.5 and never on a text node | **implemented-blocking** | v2 pairs + `--text-placeholder` + `--decoration-soft` tier + recipe AST (`decoration-on-text.contract.test.ts`); live full matrix remains follow-on. | -| 2 | Tap targets ≥48px interactive; static pills never carry `min-h-tap`; no production target reduced | **implemented-partial** | Legacy-class and literal ratchets run; the 48px value itself lands in PR 5b (until then the token honestly says 44); geometry assertions live in the held visual harness. | -| 3 | Focus outline present, `--focus`, no companion ring | **planned** | `--focus` is referenced nowhere in the DS export (finding N3). Global fix first, then the check. | -| 4 | Non-colour encoding on every status indicator | **manual → planned** | `MissingValue`/`DoseLine` text marks arrive in PR 6; contract test follows. | -| 5 | Tables: semantic caption, associated headers, `aria-controls` on the expander | **implemented-partial** | Expander and alignment tested; semantic-caption rule fails today (div caption, optional). | -| 6 | Enum resilience — neutral fallback, never throws | **implemented-blocking** | `source-badges-off-vocab.dom.test.tsx`. | -| 7 | Elevation monotonicity — no child heavier than its parent | **implemented-partial** | Token-ramp monotonicity is tested; the per-surface child/parent rule is planned (needs a render-tree check). | -| 8 | Edge ownership — no `border-*` + `ring-*` on one surface; no 1px spread in a drop shadow | **implemented-partial** | Ladder is tested spread-free; the per-surface co-occurrence lint and spread-term check are planned (PR 9). The spread form was chosen _because_ it is mechanically lintable. | -| 9 | No layout-property animation; no hardcoded durations | **planned** | PR 9 wires `--duration-*`/`--ease-*` and adds the lint; ten known layout-animation sites are the seed list. | -| 10 | Named z and motion tokens have real consumers; no unapproved `z-` value | **implemented-partial** | `require-z-index-ladder` lints z discipline; motion-token consumption is planned (one consumer today against 25 hardcoded durations). | -| 11 | Published-type conformance — every `dtsPropsFor` entry assignable from source props | **planned** | PR 12 generates declarations from real types; known drift: `confirmPhraseLabel`, Sheet omissions, narrowed callbacks. | -| 12 | Component coverage — every registered component has a direct behavioural test | **implemented-partial** | Tested subset in COMPONENTS §0; registry-wide requirement lands with PR 12. | +| # | Gate | Status | Evidence / path to blocking | +| --- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Contrast ≥4.5:1 for every text/background pair, both themes, live **and** v2; `--decoration-soft` asserted below 4.5 and never on a text node | **implemented-blocking** | v2 pairs + `--text-placeholder` + `--decoration-soft` tier + recipe AST (`decoration-on-text.contract.test.ts`); live full matrix remains follow-on. | +| 2 | Tap targets ≥48px interactive; static pills never carry `min-h-tap`; no production target reduced | **implemented-blocking** | `--spacing-tap` is 48px and pinned from both sides: `ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias, `ui-style-contract` measures the rendered floor in Chromium, and the legacy-class and literal ratchets still run. Two carve-outs are written down rather than gated away: the phone composer below 431px (SPEC §4.10) and per-surface geometry, which stays in the held visual harness. | +| 3 | Focus outline present, `--focus`, no companion ring | **planned** | `--focus` is referenced nowhere in the DS export (finding N3). Global fix first, then the check. | +| 4 | Non-colour encoding on every status indicator | **manual → planned** | `MissingValue`/`DoseLine` text marks arrive in PR 6; contract test follows. | +| 5 | Tables: semantic caption, associated headers, `aria-controls` on the expander | **implemented-partial** | Expander and alignment tested; semantic-caption rule fails today (div caption, optional). | +| 6 | Enum resilience — neutral fallback, never throws | **implemented-blocking** | `source-badges-off-vocab.dom.test.tsx`. | +| 7 | Elevation monotonicity — no child heavier than its parent | **implemented-partial** | Token-ramp monotonicity is tested; the per-surface child/parent rule is planned (needs a render-tree check). | +| 8 | Edge ownership — no `border-*` + `ring-*` on one surface; no 1px spread in a drop shadow | **implemented-partial** | Ladder is tested spread-free; the per-surface co-occurrence lint and spread-term check are planned (PR 9). The spread form was chosen _because_ it is mechanically lintable. | +| 9 | No layout-property animation; no hardcoded durations | **planned** | PR 9 wires `--duration-*`/`--ease-*` and adds the lint; ten known layout-animation sites are the seed list. | +| 10 | Named z and motion tokens have real consumers; no unapproved `z-` value | **implemented-partial** | `require-z-index-ladder` lints z discipline; motion-token consumption is planned (one consumer today against 25 hardcoded durations). | +| 11 | Published-type conformance — every `dtsPropsFor` entry assignable from source props | **planned** | PR 12 generates declarations from real types; known drift: `confirmPhraseLabel`, Sheet omissions, narrowed callbacks. | +| 12 | Component coverage — every registered component has a direct behavioural test | **implemented-partial** | Tested subset in COMPONENTS §0; registry-wide requirement lands with PR 12. | **Computed-style suites (planned, PR 1–2):** Tailwind conflict order · v2 cascade, ancestor **and** same-node forms · HCM remapping under all three v2 selectors · z-index · target @@ -84,7 +84,7 @@ as design tokens · export/preview parity. | Hardcode a transition duration | Gate 9 | planned | | A `z-` value outside the named rungs | `require-z-index-ladder` (ESLint) | implemented-blocking | | Ship a registered component without a direct test | Gate 12 | implemented-partial | -| Lower a production tap target below 48px | Gate 2 + `ui-smoke` flake history + held visual probe | implemented-partial | +| Lower a production tap target below 48px | Gate 2 + `ui-smoke` flake history + held visual probe | implemented-blocking (token floor); per-surface geometry still held | | Construct an enabled control with no action, destination or name | PR 4 discriminated unions + DOM contracts; `require-button-wiring` covers raw `