diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 7c94ea76d..4165e5929 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -614,3 +614,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. | | 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #890, plan Phase 4; single-batch implementation+ledger commit — Phase 3 landed as squash 44a4c511bc1381168474794d6f89273564659ead) | ceabc04d75fd0cea2a120d504418d44edd704bfb | App-wide performance pass: audit-then-fix + budget ratchet (plan Phase 4) | Audit on post-#872 main: enforced gzip budget +3.13% over the 1,363,382-byte baseline; analyzer treemap showed pdfjs-dist (123 KB) and cross-mode-differentials data (121 KB) already correctly lazy, and disproved the suspected forms-catalog dashboard leak (type-only import). Confirmed one real defect: the 143-line `/services` client home page value-imported `defaultServiceSlug` from `@/lib/services`, compiling the ~915 KB services snapshot (~100 KB gzip) into its route chunk. Fixed by computing the slug in the server page (`src/app/services/page.tsx`) and passing it as a prop; the client component builds its task cards from the prop. Measured result: budget swung from +3.13% to −3.97%; baseline ratcheted down 1,363,382 → 1,309,286 bytes gzip so CI locks the win. pdfjs/differentials/#718 paths deliberately untouched per audit rules. Two transient `.next/dev` generated-type corruptions from the long-lived dev server were resolved by stopping the server and setting the generated dir aside (reversible); nothing hand-deleted. | Build + `check:bundle-budget --json` before/after (artifacts in session scratchpad); `check:bundle-budget -- --update` for the ratchet; `npm run test` 2789 passed/1 failed (known container-only `pdf-extraction-budget` artifact, hosted-CI-green through #826/#835/#872); `verify:cheap` green to the same artifact; `verify:ui` 219 passed/2 failed (the two long-established container artifacts; ui-tools spec covering the services surface passed); `verify:pr-local` unit stage identical; typecheck/lint/format clean. Lighthouse not run (dev-server churn; bundle evidence sufficed for this fix set). No provider-backed checks run. | | 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | bb85b546e + ledger closeout | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production for both prototype items and set suggestions, separated local no-auth upload capability from Favourites demo treatment, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; the final demo/upload boundary selection passed 4/4; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed 236/237 with one hydration-timing failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | +| 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #896, plan Phase 5; content commit + this ledger follow-up) | a6c2b4e92374e9002fb00c547eb5677d01ce538c | Design-polish sweep: audit-then-fix (plan Phase 5, final phase) | Audit on post-#890 main: three strict design guards clean; full re-run of the 07-token-adoption-audit grep method shows all July 3 debt resolved (M1–M3 done, L4 reduced to the deliberate theme-aware `ring-white/N dark:ring-white/10` glass idiom, L5/L7 gone; production hex all legitimate print/brand/console/comment classes); 43-capture live sweep across 15 routes × desktop/phone + 320px spots + dark/reduced-motion/forced-colors spots found 0 overflow and 0 console errors. Three defects found and fixed: (1) forced-colors solid-button labels rendered as blank Canvas-on-Canvas backplate boxes (axe-invisible) — command controls flattened to the native HCM ButtonFace/ButtonText pairing and accent glyph tokens flipped to ButtonText inside the existing forced-colors block, regression-locked by a new ui-accessibility test; (2) tools desktop 6-up quick-action rail truncated card titles at 1440×1000 — card metrics tightened, all six titles verified unclipped; (3) privacy page rendered "systemand" from a JSX newline-adjacent-to-tag drop — explicit space, locked by a privacy-ui assertion. Dated July 18 run appended to docs/redesign/07-token-adoption-audit.md (archived design-qa.md not resurrected). | Guards + focused vitest 14/14; `verify:cheap` chain green to the known container-only pdf-extraction-budget artifact (2806/2809); `verify:ui` 220 passed/2 failed (the two long-baselined container artifacts, hosted-CI-green through #826/#835/#872/#890); `test:e2e:accessibility` 8/8 incl. the new forced-colors token test; production build + client-bundle secret scan passed; `check:bundle-budget` within tolerance vs the Phase 4 ratchet (1290.6 vs 1278.6 KiB baseline); `verify:pr-local` runtime/format/lint/typecheck/build/rag-fixtures green with the same sole unit-suite artifact. `verify:release` not run (provider-backed; awaits explicit confirmation). No provider-backed checks run. | diff --git a/docs/redesign/07-token-adoption-audit.md b/docs/redesign/07-token-adoption-audit.md index bb7e6562d..683f331b3 100644 --- a/docs/redesign/07-token-adoption-audit.md +++ b/docs/redesign/07-token-adoption-audit.md @@ -78,3 +78,69 @@ findings as "clean before the pattern spreads," not shipped-primitive regression - Raw palette: `rg '\b(bg|text|border|ring|from|to|via|fill|stroke)-(gray|slate|…|rose)-(50|…|950)\b' src/components -g '*.tsx'` - Dead bridge: `rg '("|'"'"'| )(bg-surface|text-muted|text-heading|…)( |"|'"'"')' src` → 0 hits. - Arbitrary values ranked: `rg -oN '…-\[[^]]+\]' src/components -g '*.tsx' | sort | uniq -c | sort -rn` + +## July 18 run — Phase 5 design-polish sweep (re-score + live route sweep) + +Scope: full re-run of the July 3 grep method on current `main`, plus a live +route sweep — 15 production routes × (1440×1000 desktop + 390×844 phone), +320×844 spot checks on the 4 densest routes, and dark / reduced-motion / +forced-colors desktop spots on `/`, `/differentials`, `/documents/search`. +Server identity confirmed via `/api/local-project-id` before attaching. + +**Score: 92/100** — 0 critical · 0 medium · 2 low (naming consistency and the +mockup-file palette leaks, both unchanged P3 charter exclusions). + +### Re-score of July 3 findings + +| Item | July 3 | July 18 | +| --------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| M1 dead `@theme inline` bridge | Fix | ✅ Resolved (bridge deleted). | +| M2 raw-palette tone helpers | Done on branch | ✅ Landed; production `src/components` has 0 raw-palette classes (3 mockup files only). | +| M3 ~295 hardcoded px font sizes | Partly unblocked | ✅ Adopted; `check:type-scale --strict` passes with 0 arbitrary sizes in `src`. | +| L4 hardcoded white/black neutrals | Polish | ✅ Down to 4 sites, all the deliberate `ring-1 ring-white/N dark:ring-white/10` glass-highlight idiom with explicit dark variants. 0 accent-button `text-white`. | +| L5 `border-blue-400` | Polish | ✅ Gone. | +| L6 tap-target magic numbers | Done | ✅ Holds (0 arbitrary tap tokens). | +| L7 `rounded-[var(--radius-lg)]` | Polish | ✅ Gone. | +| L8 recipe contract docs | Done | ✅ Holds (`09-ui-primitives-recipes.md`). | + +Hex audit note: every current hex hit in production components is a legitimate +class — print-handout surfaces (`FactsheetPrintSheet`, therapy-compass sheet +`@media print`), official provider brand marks (Google/Microsoft), web-vitals +console-log colors, PR-number code comments (regex false positives), and the +white switch-knob-on-accent-track pattern. + +### Route sweep result + +43 captures: 0 horizontal overflow at any viewport (including 320px), 0 +console errors, 0 failed network requests. Mode-home template routes render +consistently across desktop/phone/320; dark theme and reduced-motion spots +clean. + +### New findings (all fixed in this pass) + +1. **Forced-colors blank button labels** (`globals.css` forced-colors block). + Chromium paints a Canvas backplate behind every glyph run in forced-colors + mode, so glyph tokens that resolved to the Canvas/ButtonFace family + (`--command-contrast`, `--primary-contrast`, `--clinical-accent-contrast`, + `--danger-solid-contrast`) rendered solid-button labels as blank boxes — + invisible to axe, which reads CSS rather than painted pixels. SVG strokes + get no backplate, so the dark `--command` fill also swallowed ButtonText + icons. Fix: command controls flatten to the native HCM pairing + (ButtonFace fill / ButtonText glyphs); accent fills keep their system + colors and flip only their glyph tokens to ButtonText. Regression-locked by + a new `ui-accessibility.spec.ts` test asserting the glyph tokens never + resolve to the Canvas color. +2. **Tools quick-action rail title truncation at 1440×1000** + (`applications-launcher-page.tsx`). The desktop 6-up rail left ~85px for + text against ~92px titles ("Ask eviden…", "Safety che…"). Fix: tightened + card metrics (icon column 2.25rem→2rem, gap-3→gap-2, px-3→px-2.5, icon + h-9→h-8); all six titles now render whole, descriptions remain + designed-supplementary ellipsis (full copy lives in the All-tools cards and + aria-labels). Phone icon-grid variant untouched. +3. **Privacy-page microcopy** (`src/app/privacy/page.tsx`). JSX drops a + newline adjacent to a tag, rendering "…patient-record systemand does not + ask…". Fixed with an explicit `{" "}`; locked by a `privacy-ui.test.ts` + assertion. + +Observation, no action: a sub-perceptual glass-header backdrop-gradient ghost +at the content top-left (visible only at 4× zoom) — by-design translucency. diff --git a/src/app/globals.css b/src/app/globals.css index be324e945..48dc21e36 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2335,16 +2335,24 @@ html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .sou --primary: LinkText; --primary-strong: LinkText; --primary-soft: Canvas; - --primary-contrast: Canvas; + --primary-contrast: ButtonText; --surface-chrome: Canvas; - --command: ButtonText; - --command-hover: ButtonText; - --command-contrast: ButtonFace; + /* Chromium paints a Canvas backplate behind every glyph run in + forced-colors mode, so label/glyph tokens must never resolve to the + Canvas/ButtonFace family — Canvas-on-Canvas glyphs vanish and solid + buttons render blank label boxes. SVG icon strokes get no backplate at + all, so the dark command fill also cannot survive: it would swallow + ButtonText icons. Command controls therefore flatten to the native HCM + button pairing (ButtonFace fill, ButtonText glyphs); accent fills keep + their system colors and flip only their glyph tokens. */ + --command: ButtonFace; + --command-hover: ButtonFace; + --command-contrast: ButtonText; --clinical-accent: LinkText; --clinical-accent-hover: LinkText; --clinical-accent-soft: Canvas; --clinical-accent-border: ButtonBorder; - --clinical-accent-contrast: Canvas; + --clinical-accent-contrast: ButtonText; --info: LinkText; --info-soft: Canvas; --success: CanvasText; @@ -2354,7 +2362,9 @@ html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .sou --danger: Mark; --danger-soft: Canvas; --danger-solid: Mark; - --danger-solid-contrast: Canvas; + /* Mark's palette-paired foreground — ButtonText is not guaranteed to + contrast a Mark fill in arbitrary user schemes. */ + --danger-solid-contrast: MarkText; --tone-purple: CanvasText; --tone-indigo: CanvasText; --tone-rose: CanvasText; diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index a5de27ca2..0375be696 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -25,9 +25,8 @@ const SECTIONS: Section[] = [ body: ( <> Clinical KB is a knowledge base over clinical reference material. It is{" "} - not a patient-record system - and does not ask for patient data. The main privacy risk is incidental patient information entered into a - free-text question or uploaded document. + not a patient-record system and does not ask for patient data. The main privacy risk is + incidental patient information entered into a free-text question or uploaded document. ), }, diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index bceb8cb4b..256bd1a0b 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -287,14 +287,14 @@ function QuickActions({ onSelect, mobile }: { onSelect: (id: string) => void; mo focusRing, mobile ? "grid h-14 min-w-0 place-items-center gap-0.5 rounded-lg px-1 py-1.5 text-center" - : "grid min-h-14 grid-cols-[2.25rem_minmax(0,1fr)] items-center gap-3 rounded-lg px-3 py-2.5", + : "grid min-h-14 grid-cols-[2rem_minmax(0,1fr)] items-center gap-2 rounded-lg px-2.5 py-2.5", )} > diff --git a/tests/privacy-ui.test.ts b/tests/privacy-ui.test.ts index 1192f575d..6c3f70392 100644 --- a/tests/privacy-ui.test.ts +++ b/tests/privacy-ui.test.ts @@ -32,6 +32,9 @@ describe("privacy UI", () => { ]) { expect(markup).toContain(heading); } + // JSX drops a newline adjacent to a tag, so the space after the bolded + // phrase must be explicit or this renders as "systemand". + expect(markup).toContain("not a patient-record system and does not ask for patient data"); expect(markup).toContain("Generated answer text is also omitted from durable query logs by default"); expect(markup).toContain("application service in Singapore"); expect(markup).toContain("Railway in Singapore"); diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts index 3253ec31d..4574c417d 100644 --- a/tests/ui-accessibility.spec.ts +++ b/tests/ui-accessibility.spec.ts @@ -253,6 +253,48 @@ test.describe("Clinical KB accessibility coverage", () => { await expectNoPageHorizontalOverflow(page); }); + test("solid-button label tokens stay legible with forced colors", async ({ page }) => { + // Chromium paints a Canvas backplate behind every glyph run in forced-colors + // mode: a label whose color resolves into the Canvas/ButtonFace family + // disappears into its own backplate (axe cannot see this — it reads CSS, + // not painted pixels). Lock the glyph tokens to a non-Canvas resolution. + await page.emulateMedia({ forcedColors: "active" }); + await page.setViewportSize({ width: 1440, height: 1000 }); + await mockMinimalDashboardApi(page); + await gotoApp(page); + await expectDashboardUsable(page); + + const newChat = page.getByRole("button", { name: "New chat" }).first(); + await expect(newChat).toBeVisible(); + const { canvas, buttonLabelColor, tokenColors } = await newChat.evaluate((button) => { + const probe = document.createElement("span"); + document.body.append(probe); + const resolveColor = (value: string) => { + probe.style.color = ""; + probe.style.color = value; + return getComputedStyle(probe).color; + }; + const resolvedCanvas = resolveColor("Canvas"); + const glyphTokens = [ + "--command-contrast", + "--primary-contrast", + "--clinical-accent-contrast", + "--danger-solid-contrast", + ].map((token) => [token, resolveColor(`var(${token})`)] as const); + probe.remove(); + return { + canvas: resolvedCanvas, + buttonLabelColor: getComputedStyle(button).color, + tokenColors: glyphTokens, + }; + }); + + expect(buttonLabelColor).not.toBe(canvas); + for (const [token, color] of tokenColors) { + expect(`${token}: ${color}`).not.toBe(`${token}: ${canvas}`); + } + }); + test("dashboard remains usable at 200 percent zoom", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 800 }); await mockMinimalDashboardApi(page);