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
28 changes: 26 additions & 2 deletions docs/design-system/GATES.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ than CI, two of them by the second reader; review does not scale past two carefu

| Check | What it enforces | Label |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `npm run check:design-system-contract` (token baseline + adoption + design-sync contracts) | Raw colour literals (ratchet against `design-system-contract-baseline.json`), literal `shadow-[…]` classes, the legacy tap class, interactive tap-size literals, therapy-compass `tc-btn` base-class use; deterministic adoption truth; local source/export/preview/design-sync parity. Mockups exempt. | implemented-blocking |
| `npm run check:design-system-contract` (token baseline + adoption + design-sync contracts) | Raw colour literals (ratchet against `design-system-contract-baseline.json`), literal `shadow-[…]` classes, the legacy tap class, interactive tap-size literals, therapy-compass `tc-btn` base-class use; `dark:` colour overrides and legacy palette utilities (both pinned at **zero**, not ratcheted); deterministic adoption truth; local source/export/preview/design-sync parity. Mockups exempt. | implemented-blocking |
| `tests/ckb-v2-token-contract.test.ts` | v2 layer stays class-scoped (no `:root` leak) · `--border-lux` solid and no lighter than `--border`, both themes · dark surface ramp monotonic and separated · `--surface-subtle` aliases up · text/muted/heading ≥4.5:1 on the light shell, muted ≥4.5:1 dark · `--text-soft` pinned **below** 4.5:1 and ≥3:1 (both sides, so the tier cannot be "fixed" away) · command pair ≥4.5:1 both themes · `--shadow-inset` true inset · elevation ladder carries no baked 1px hairline · tap floor ≠ chip height ≠ compact row · reduced-motion zeroes durations · every type step has its own line-height and tracking | implemented-blocking |
| `tests/design-token-contract.test.ts` | Live-layer token relationships (companion to the v2 contract) | implemented-blocking |
| `tests/source-badges-off-vocab.dom.test.tsx` | Enum resilience: off-vocabulary `clinical_validation_status` degrades to the neutral triad, logs once, never throws | implemented-blocking |
Expand DownExpand Up@@ -107,7 +107,7 @@ theme-list parity, and remote design-project publication remain separate concern
| Hand-rolled rows replacing `AccessibleTable` without declaring it | — | **manual** (declaration convention) |
| Drop the AI verification disclaimer | PR 6 required prop | implemented-blocking in `AnswerCard`; production shell wording remains separately guarded during adoption |
| Fix a collapsed column by nudging pixels | 320px snapshot | planned (held harness) |
| `dark:` colour override | Lint | planned (3 live instances to burn down) |
| `dark:` colour override | `check:design-system-contract` — `darkColorOverrides`, baseline 0| **implemented-blocking** (7 Aug 2026) — all 3 instances retired; see §4 |
| Truncate a page title, dialog title, drug name or review warning | — | **manual** |
| Restate a token value in prose | — | **manual** (eleven divergences came from this; TOKENS.md is the only inventory) |

Expand DownExpand Up@@ -136,3 +136,27 @@ Recorded at export (31 July 2026), **not re-run for this document set**:

The honest sentence includes the failure. Exit code 0 alone is never proof — quote the
"N passed" line.

### `dark:` colour override — closed 7 August 2026

The §3 row moved from **planned** to **implemented-blocking**. What that took:

- All three live instances were the same shape — `ring-1 ring-<colour>/20
dark:ring-<colour>/10` on a lux surface (`ui-primitives.tsx` `panel`, the mobile FAB menu
in `dashboard-nav.tsx`, the PWA notice card). They are now `--ring-highlight` /
`--ring-glass`, per-theme tokens carrying the value the override used to supply, so the
utility no longer needs a variant. `legacyPaletteUtilities` (the `ring-white/35` on the
same PWA line) went to zero with them.
- **No visual change**, and that is measured rather than assumed: Tailwind emits
`color-mix(in oklab, <colour> N%, transparent)` for `ring-<colour>/N`, and mixing any
colour with `transparent` yields that colour at alpha N regardless of colour space, so the
token values are equivalent by construction. `--ring-glass` is deliberately literal white
and **not** `--neutral-0`, which inverts to `#060708` in dark and would have turned a white
glass ring near-black.
- Both metrics are pinned at **0** in `design-system-contract-baseline.json`, not ratcheted.
The check asserts `value <= baseline`, so zero is a hard floor. Mutation-verified:
reintroducing one `dark:ring-white/10` fails with `darkColorOverrides increased from 0 to
1` plus the matching per-path assertion.
- Every other metric was asserted unchanged before the baseline was rewritten — the
discipline §8 of the handover describes, so clearing one number cannot silently move
another.
1 change: 1 addition & 0 deletions docs/design-system/TOKENS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,7 @@ The v2 layer _references_ or _depends on_ these; their values stay in `live` / `
| `--danger`, `--danger-solid`, `--danger-solid-contrast` | `live` | Filled danger pairs with `--danger-solid-contrast` (HCM-mapped to `MarkText`); pairing enforced from PR 3. |
| `--success`, `--info` | `live` | Clinical/status palette — reserved channel. |
| `--focus` | `live` | The only focus outline colour; no companion ring, ever. |
| `--ring-highlight`, `--ring-glass` | `live` | Highlight rings on lux surfaces; each carries its own per-theme value, so `dark:ring-*` is gated at zero (GATES §3). |
| `--text-placeholder` | `planned` (PR 3) | New role ≥4.5:1; until it exists, placeholders use `--text-muted`. |

## 6 · Deprecations and deletions
Expand Down
14 changes: 4 additions & 10 deletions scripts/design-system-contract-baseline.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@
"onePixelShadowSpreads": 2,
"hardcodedCssMotionDurations": 42,
"rawCssZIndices": 9,
"legacyPaletteUtilities": 2,
"darkColorOverrides": 3,
"legacyPaletteUtilities": 0,
"darkColorOverrides": 0,
"legacyShadowAliases": 231,
"arbitraryTracking": 0,
"layoutTransitionExceptions": 12,
Expand DownExpand Up@@ -47,14 +47,8 @@
"rawCssZIndices": {
"src/app/globals.css": 9
},
"legacyPaletteUtilities": {
"src/components/pwa-lifecycle.tsx": 2
},
"darkColorOverrides": {
"src/components/clinical-dashboard/dashboard-nav.tsx": 1,
"src/components/pwa-lifecycle.tsx": 1,
"src/components/ui-primitives.tsx": 1
},
"legacyPaletteUtilities": {},
"darkColorOverrides": {},
"legacyShadowAliases": {
"src/app/globals.css": 9,
"src/app/layout.tsx": 1,
Expand Down
17 changes: 17 additions & 0 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,19 @@
--border-strong: #c8d2de;
/* Tracks --border-strong at 55% — re-derive both together. */
--border-lux: rgb(200 210 222 / 55%);
/* Highlight rings on lux surfaces (panel, mobile FAB menu, PWA notice). These
exist so the ring carries its own per-theme value and the call site does not
need a `dark:` override — the prohibition GATES §3 lists, now gated at zero.
Values are byte-equivalent to the utilities they replace: Tailwind emits
`color-mix(in oklab, <colour> N%, transparent)` for `ring-<colour>/N`, and
mixing any colour with `transparent` yields that colour at alpha N
regardless of space, so the glass ring is written in the plainer rgb() form
the .dark --surface-highlight already uses.
--ring-highlight tracks --border-strong, 20% light / 10% dark
--ring-glass literal white, 35% light / 10% dark — deliberately NOT
--neutral-0, which inverts to #060708 in dark */
--ring-highlight: color-mix(in oklab, var(--border-strong) 20%, transparent);
--ring-glass: rgb(255 255 255 / 35%);
--primary: var(--primary-500);
--primary-strong: var(--primary-700);
--primary-soft: #eff5fc;
Expand DownExpand Up@@ -532,6 +545,10 @@
--border: #2b3136;
--border-strong: #3b444b;
--border-lux: rgba(255, 255, 255, 0.1);
/* Dark halves both highlight rings — the exact values the retired
`dark:ring-*` overrides carried. See the :root declarations. */
--ring-highlight: color-mix(in oklab, var(--border-strong) 10%, transparent);
--ring-glass: rgb(255 255 255 / 10%);

--primary: var(--primary-500);
--primary-strong: var(--primary-700);
Expand Down
2 changes: 1 addition & 1 deletion src/components/clinical-dashboard/dashboard-nav.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,7 +274,7 @@ export function MobileSectionFab({
aria-hidden={!open}
inert={!open}
hidden={!open}
className="fixed z-40 overflow-hidden rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-[color:var(--border-strong)]/20 backdrop-blur-md dark:ring-[color:var(--border-strong)]/10"
className="fixed z-40 overflow-hidden rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-[color:var(--ring-highlight)] backdrop-blur-md"
style={{
right: "max(0.75rem, env(safe-area-inset-right))",
bottom: "calc(max(0.75rem, env(safe-area-inset-bottom)) + 4.5rem)",
Expand Down
2 changes: 1 addition & 1 deletion src/components/pwa-lifecycle.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,7 +101,7 @@ async function teardownLocalPwa() {
}

const cardClassName =
"pwa-notice-card pointer-events-auto relative rounded-2xl border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-4 text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-white/35 backdrop-blur-md dark:ring-white/10";
"pwa-notice-card pointer-events-auto relative rounded-2xl border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-4 text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-[color:var(--ring-glass)] backdrop-blur-md";
const primaryButtonClassName =
"inline-flex min-h-tap items-center justify-center rounded-lg bg-[color:var(--clinical-accent)] px-3.5 py-2 text-sm font-semibold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-tight)] transition-colors hover:bg-[color:var(--clinical-accent-hover)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
const secondaryButtonClassName =
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui-primitives.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,7 +52,7 @@ export const panelSubtle =
export const sourceCard = `${panelSubtle} transition hover:border-[color:var(--border-strong)] hover:shadow-[var(--shadow-hover)]`;
export const answerSurface = "rounded-lg bg-transparent";
export const panel =
"rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] shadow-[var(--shadow-soft)] ring-1 ring-[color:var(--border-strong)]/20 dark:ring-[color:var(--border-strong)]/10";
"rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] shadow-[var(--shadow-soft)] ring-1 ring-[color:var(--ring-highlight)]";
// Disabled is ENCODED, not faded. `opacity-50` dims the label and the fill
// together, so a disabled primary stayed a large saturated block that still read
// as available, and a disabled secondary's label dropped below 4.5:1. Instead:
Expand Down
Loading