diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 561937e177..63010521a1 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -575,10 +575,12 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-31 | claude/top-search-design-mockups-w53znc | c09eb64918ae2956968d35f3253c405ed9b915d5 | therapy-compass filter sheet + documents review fixes (C2) | Therapy Compass's phone Topics/availability selects were faking multi-select (value pinned to "", literal check-prefix in option text, a Clear-filters action among the options); replaced with one sheet of aria-pressed toggles matching the wide viewport, in the tc- idiom. Also lands two CodeRabbit findings from merged PR #1542: sheet footer primary action to sm:min-h-12, and the dialog test now pins aria-expanded/aria-controls. Third finding (unmount skips focus restore) dispositioned no-change with a test pinning the scroll lock is not leaked. | verify:cheap exit 0 (458 files / 4785 tests); ui-accessibility.spec.ts 15 passed chromium incl. the rewritten Therapy Compass case; ui-smoke documents journey 1 passed chromium | | 2026-08-01 | claude/top-search-design-mockups-w53znc | 4f4440fd6776742f5de203ee15295f372205321d | search results bar: scope-system deletion, filter shelf, bar anatomy | Handoff for PR #1555. Deleted the inert command-scope system (voided props, six modes' scope config, three matchers, four no-op call sites, the original shelf) — behaviour-preserving because every matcher early-returned true on a permanently-empty array. Rebuilt the applied-filter shelf prop-driven on live facet data, scoped to documents and therapy-compass. Landed the bar anatomy: tile spinner and funnel states, Filter to the right edge, Sort inboard. Study step 6 (remove the library button) deliberately declined — the nav route clears the query via onSearchModeChange. Ledger #182 closed. | verify:pr-local exit 0 (460 files / 4796 tests, production build, client-bundle secret scan, RAG fixtures 36 cases / 23 suites); ui-tools 87 passed; ui-smoke + ui-accessibility 108 passed 1 failed (pre-existing PDF-canvas test, fails identically stashed, Chromium 1194 vs pinned 1228); mutation-tested the shelf's survives-loading guard | | 2026-08-01 | claude/sentry-agent-monitoring-eri94v | 86983f344b45e42310e9f167a5adb0a56e46ddb5 | pr-1551 | merge-ready-pending-ci: merged origin/main; fixed outstanding-issues blank-line/#183 orphan + renumbered npm row to #204; kept worker+wizard error-tracking sections; qodo claim-spam thread already fixed on prior tip and resolved | check:outstanding-issues pass; merge-tree clean vs origin/main; prior tip Static PR failed on outstanding-issues; push 86983f344 | -| 2026-08-01 | claude/ds-v2-tooling-loop | 40181192519fddc9405523d06bd3e691096cda6c | PR-0 tooling loop: Context7 + Chrome DevTools MCP wiring, design-sync and mockup-capture scripts, docs/env-example updates (11 files, +1005/-11, no clinical or RAG surfaces) | gates green | verify:pr-local: format/lint/typecheck/lock-parity green; unit 4803 passed, 6 env-class WSL relay failures in ci-cache-safety.test.ts (Ubuntu distro stopped), focused rerun 13/13 green after WSL boot; check:rag:fixtures 36 golden cases green; build skipped by selector | +| 2026-08-01 | work | d4a4463172671cff3fd333feb685fa397e4f95d0 | latency, lazy loading, React and Next.js frontend performance | P2 eager document administration bundle boundary fixed; no P0/P1 findings | lint; typecheck; 463 files/4811 tests; production build; bundle budget | | 2026-08-01 | codex/cloud-connected-profile-boundary | 6fddcfc780b237b6b2cd524dbebd7b1de70d9701 | Cloud connected profile credential boundary | No high-confidence issues after least-privilege MCP hardening and portable Git fixture fix | Cloud static PASS; focused Vitest 15/15; full format PASS; Bash syntax PASS; PR-local dry-run | | 2026-08-01 | codex/cloud-connected-profile-boundary | 55b08496a5ee3495eed8a7436e8f69ae7b6612d8 | Cloud connected profile credential boundary | Reviewer findings fixed: cross-tenant service-role credential scrubbed and duplicate Supabase MCP parameters rejected | Cloud static PASS; focused Vitest 15/15; Bash syntax PASS; targeted Prettier PASS | -| 2026-08-01 | work | d4a4463172671cff3fd333feb685fa397e4f95d0 | latency, lazy loading, React and Next.js frontend performance | P2 eager document administration bundle boundary fixed; no P0/P1 findings | lint; typecheck; 463 files/4811 tests; production build; bundle budget | | 2026-08-01 | codex/review-latency-and-lazy-loading-optimizations | 5f069a7fec4e6ada47a0074aa5f2ea2c9dc97830 | pr-1562 unblock | unblocked: merge-tree was clean behind-by-4; merged origin/main; no unresolved threads; prior tip CI green including Production UI + PR required | merge-tree clean vs origin/main; gh mergeable was CONFLICTING/DIRTY (staleness); unresolved threads 0; auto-merge off | +| 2026-08-01 | claude/ds-v2-therapy-teardown | f9cf75fefcf78a2049fa756322265ecb64845254 | PR-T therapy-compass CSS teardown: delete therapy-compass.css, migrate tc-* to design-system control recipes, resolve #205/#016(e); focusRing local after ui-primitives rename | gates green | check:design-system-contract green; docs-surface (links/scripts/inventory/index) green; test:e2e:critical 15/15; verify:ui 344 passed; verify:pr-local green (build+client-bundle+rag fixtures 36/36) | +| 2026-08-01 | claude/ds-v2-therapy-teardown | 4c96b90aa3e99f3e3aec81a005a1da6bf7a8792f | PR-T therapy-compass CSS teardown: delete therapy-compass.css, migrate tc-* to design-system control recipes, resolve #205/#016(e); focusRing local after ui-primitives rename | gates green | check:design-system-contract green; docs-surface (links/scripts/inventory/index) green; test:e2e:critical 15/15; verify:ui 344 passed; verify:pr-local green (build+client-bundle+rag fixtures 36/36); tip includes ledger append | +| 2026-08-01 | claude/ds-v2-tooling-loop | 40181192519fddc9405523d06bd3e691096cda6c | PR-0 tooling loop: Context7 + Chrome DevTools MCP wiring, design-sync and mockup-capture scripts, docs/env-example updates (11 files, +1005/-11, no clinical or RAG surfaces) | gates green | verify:pr-local: format/lint/typecheck/lock-parity green; unit 4803 passed, 6 env-class WSL relay failures in ci-cache-safety.test.ts (Ubuntu distro stopped), focused rerun 13/13 green after WSL boot; check:rag:fixtures 36 golden cases green; build skipped by selector | | 2026-08-01 | claude/ds-v2-tooling-loop (PR #1568) | f6317ea48355c26e080ce45b6f574159e8397e24 | PR #1568 unblock/fix | merged origin/main (behind-but-clean; merge-tree was clean); cleared GitHub DIRTY/CONFLICTING; advisory review threads left open (not merge/CI blockers); auto-merge remains armed | merge-tree clean; ahead 7 behind 0; no provider-backed checks; no product code fix needed | | 2026-08-01 | claude/ds-v2-tooling-loop (PR #1568) | b76bcc9fbc39f2f986a52869c810ba3cb89994f2 | PR #1568 review-thread resolve | fixed and resolved all 13 review threads (fail-closed project identity, inventory exit code, provenance demo id, buildCmd execution, chrome pin, context7 rollback); ledger row for superseded 93d41c1 dispositioned | node --check scripts; design-sync --dry-run; no provider-backed checks | diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 3bc8da74d7..9bfd6ab8b5 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -130,7 +130,7 @@ removed after current-main verification; it is not missing recommended work. | 81 | `#204` | A2 | High — install/CI integrity | Next dependency or verify:pr-local change | 1–3 hours | npm 11.6.2 regenerates a lockfile its own `npm ci` rejects, reddening every CI job — add `npm ci --dry-run` (or lockfile-sync assertion) to `verify:pr-local` when package.json/lock change; do not regenerate with `npm install` to "fix". Distinct from #149. | - + ## Open items > **Merged-main canary update (2026-07-23, run `30018289898`):** the new structured report correctly recorded evaluated tree `c24f2e8f2d30d0c59fc1eba025d3dcd63478137e`, run/attempt identity and `cross-region-runner` latency context. Golden retrieval remained 36/36 with document/content recall 1.0 and no failed cases. The 44-case answer gate had grounded-supported and unsupported-correct rates of 1.0, but failed because `neuroleptic-side-effect-escalation` again returned one citation where two are required (citation-failure rate 0.0227). `admission-discharge-comparison` again omitted the specific AKG admission document after `comparison_source_extractive_fallback`; `admission-discharge-coverage-paraphrase` was advisory-only at 24,870 ms. Answer cost was reported as `$0.234736`. Do not retry immediately: retain this as the first structured datapoint, compare it with the scheduled 2026-07-26 report, and keep retrieval/ranking unchanged. @@ -154,7 +154,7 @@ removed after current-main verification; it is not missing recommended work. | #057 | P2 | task | Complete staging soak and rollback rehearsal | After #056, run the documented soak and rollback against an exact candidate; retain latency/error/rollback evidence. Stop on unsafe data, identity mismatch, or an unowned rollback decision. | `docs/launch-operator-runbook.md`; `docs/capacity-review.md` | 2026-07-24 | | #011 | P3 | task | Auth DB-connection allocation is operator-only | Supabase Auth (GoTrue) is capped at ~10 absolute DB connections (Supabase perf advisor). Switch to **percentage-based** allocation in the Supabase **dashboard** before the first compute scale-up — **not settable via SQL/MCP** (operator-owned). Verify via a staging soak + an approval-gated read-only advisor re-check. | `docs/auth-connection-cap-runbook.md`; `docs/process-hardening.md` (Known follow-up debts) | 2026-07-21 | | #013 | P3 | rec | Route-chunk + mockup catalogue JSON weight | Keep this recommendation open and measurement-gated. `build:analyze` still finds route-scoped catalogue modules: `/specifiers` ships `specifiers-search-index.json` (~180 KB parsed), `/forms` ships `forms-catalog.json` (~132 KB), and `/formulation` ships `formulation-content.json` (~52 KB; client-side local search needs an index/full split or a search endpoint). The approved Lighthouse sample covered `/forms`, but `/specifiers` and `/formulation` remain unmeasured, so the precommitted `#017` rule does not permit archiving their payload work. Development-only `*-mockups.tsx` chunks are not an initial production bundle and production returns 404 for `/mockups/*`; do not restructure them without deploy-artifact or cold-start evidence. **Next:** collect route-specific LCP/CLS and CrUX INP evidence for `/specifiers` and `/formulation`, then close only the routes that meet every `#017` threshold. | session 2026-07-21 (`build:analyze`); PR #1470 review | 2026-07-21 | -| #016 | P3 | rec | "Big but not easy" structural + motion perf | Deferred larger levers: (a) nonce-CSP forces every product route to `╞Æ Dynamic` (zero static generation) — evaluate Partial Prerendering / static shells for the static clinical catalogues (DSM/differentials/therapy/specifiers/formulation); (b) sidebar expand/collapse animates `grid-template-columns` (biggest smoothness cost, motion-gated — needs a transform-overlay rethink); (c) Therapy Compass fetches 692 KB / 2.5 MB JSON client-side (defer until interaction + confirm brotli); (d) settings/setup/admin dialogs static-imported into the home chunk (`next/dynamic` them); (e) `src/app/(search-app)/layout.tsx:4` imports 71.6 KB of Therapy-Compass-only CSS in the ROUTE-GROUP layout, making it render-blocking on `/`, `/documents`, `/forms`, `/dsm` and every mode home; (f) `shared-search-app-shell.tsx:8` statically imports the `therapy-compass` barrel, pulling `workspace.tsx` + `bindings.tsx` + `nav.tsx` into every `(search-app)` route; (g) three client waterfalls (`use-app-preferences.ts:156-182`, `ClinicalDashboard.tsx:977-1069`, `signed-image.tsx:60-84` + `use-signed-image-url.ts:39`) and the paint offenders in `globals.css` beyond the sidebar grid — three stacked `backdrop-filter` passes on an always-mounted translating element (`:709-748`), `box-shadow` inside a `transition` list (`:677-684`), and `@keyframes shimmer` animating `background-position` on the shared `Skeleton` (`:2289-2296`). **CORRECTED 2026-07-29 on (c):** the Therapy Compass filenames are unversioned and Next serves `/public` with an ETag, so only the FIRST visit pays 690.6 KB / 2,470 KB — repeat visits pay ~4 revalidation round trips. The fix is content-hashed filenames + `immutable` (touching `scripts/build-therapies-index.mjs` and `check:therapy-data-index`), NOT a bare `Cache-Control` line. See `docs/audit/latency-audit-2026-07-28.md` L3-1/L3-2/L3-3/L3-6/L3-7. | session 2026-07-21 (build route table + design audit) | 2026-07-21 | +| #016 | P3 | rec | "Big but not easy" structural + motion perf | Deferred larger levers: (a) nonce-CSP forces every product route to `╞Æ Dynamic` (zero static generation) — evaluate Partial Prerendering / static shells for the static clinical catalogues (DSM/differentials/therapy/specifiers/formulation); (b) sidebar expand/collapse animates `grid-template-columns` (biggest smoothness cost, motion-gated — needs a transform-overlay rethink); (c) Therapy Compass fetches 692 KB / 2.5 MB JSON client-side (defer until interaction + confirm brotli); (d) settings/setup/admin dialogs static-imported into the home chunk (`next/dynamic` them); (e) **DONE 2026-08-01 in PR-T (ds-v2 therapy teardown):** deleted `therapy-compass.css` and removed its route-group layout import — no longer render-blocking on `/`, `/documents`, `/forms`, `/dsm` and every mode home; (f) `shared-search-app-shell.tsx:8` statically imports the `therapy-compass` barrel, pulling `workspace.tsx` + `bindings.tsx` + `nav.tsx` into every `(search-app)` route; (g) three client waterfalls (`use-app-preferences.ts:156-182`, `ClinicalDashboard.tsx:977-1069`, `signed-image.tsx:60-84` + `use-signed-image-url.ts:39`) and the paint offenders in `globals.css` beyond the sidebar grid — three stacked `backdrop-filter` passes on an always-mounted translating element (`:709-748`), `box-shadow` inside a `transition` list (`:677-684`), and `@keyframes shimmer` animating `background-position` on the shared `Skeleton` (`:2289-2296`). **CORRECTED 2026-07-29 on (c):** the Therapy Compass filenames are unversioned and Next serves `/public` with an ETag, so only the FIRST visit pays 690.6 KB / 2,470 KB — repeat visits pay ~4 revalidation round trips. The fix is content-hashed filenames + `immutable` (touching `scripts/build-therapies-index.mjs` and `check:therapy-data-index`), NOT a bare `Cache-Control` line. See `docs/audit/latency-audit-2026-07-28.md` L3-1/L3-2/L3-3/L3-6/L3-7. | session 2026-07-21 (build route table + design audit) | 2026-07-21 | | #018 | P2 | task | Split the lithium, ADHD and metabolic residuals by mechanism | Current evidence keeps the mechanisms separate. **Lithium — closed within this item:** the row/atom-aware subject guard, foreign-parameter rejection and query-specific range promotion returned `0.5–1.0 mmol/L` with correct targeting/citation; the full retrieval canary remained 36/36 with recall 1.0 and zero per-case RR regressions, and the full answer canary passed every blocking gate. **ADHD — open corpus debt:** `CG.MHSP.ADHD.pdf` is absent from the hosted corpus and the retrieved chart exposes `accessible_table_count=0`; repair corpus/fixture or ingestion evidence rather than weakening extractive budgets. **Metabolic — open structured-evidence debt:** the standalone plural classifier worsened the live answer and was reverted; obtain auditable schedule text/table evidence before another candidate. | targeted live lithium/ADHD/metabolic evidence 2026-07-27; `docs/evidence/rag-reliability-evidence-2026-07-27.md`; refuted approaches | 2026-07-21 | | #022 | P2 | task | Source-governance metadata refresh (operator) | The selected policy is now encoded locally as auditable `third_party_reference_attested` evidence with policy version, reviewer qualification, evidence references and append-only review history. It deliberately preserves `clinical_validation_status=unverified`; malformed, stale or non-BMJ evidence remains review debt. Migration `20260727010000_bmj_third_party_source_attestation.sql` is prepared but was **not applied**. The ten most visible local-document candidates are captured in `docs/evidence/rag-top-local-review-manifest-2026-07-26.json` with `attestation_applied=false`; qualified human review, deliberate hosted apply/attestation, and warning-rate remeasurement remain operator work. | governance worklist; local policy/migration tests; top-ten evidence manifest | 2026-07-21 | | #023 | P2 | task | Complete scheduled browser and labeling disposition | **Partial 2026-07-30:** `release-browser-matrix` no longer depends on `pr-required`, so a blocking scheduled dependency audit cannot skip Firefox/WebKit. Still need one green matrix datapoint + human irrelevant-at-10 disposition. The 2026-07-26 retrieval and answer artifacts are read and compared under resolved #051. Scheduled CI run `30216361999` failed its existing production dependency audit before Firefox/WebKit, while production Chromium passed. After that audit is green, capture one scheduled/manual browser-matrix datapoint; separately record the human decision for the stable irrelevant-at-10 set. #084 now makes each top-10 grade and matched signal reproducible, but it does not substitute for the human disposition. Do not rerun or spend on RAG for this item. | runs `30216191889`/`30216361999`; per-rank diagnostics #084; session 2026-07-27 | 2026-07-21 | @@ -362,5 +362,6 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #185 | task | Confirm hosted migration history row for 20260731150000 db-query-perf | Confirmed on live Clinical KB Database (sjrfecxgysukkwxsowpy): supabase_migrations.schema_migrations has version 20260731150000 name db_query_perf_rate_limit_and_image_indexes; document_images_searchable_doc_page_relevance_idx present. No re-apply needed. | 2026-07-31 | | #184 | task | Regenerate supabase/drift-manifest.json after db-query-perf migration | Regenerated supabase/drift-manifest.json via Docker npm run drift:manifest on host; includes document_images_searchable_doc_page_relevance_idx and updated consume_api_*_rate_limit inventory; schema_sha256 matches schema.sql. | 2026-07-31 | | #182 | issue | The command-scope system is inert — chips are configured and matchers exist, but nothing can ever set a scope | Deleted. Took the option the row recommended: dropped the two voided props from universal-search-command-surface, the scope state and callbacks from both shells and the master header, the scopes config arrays and CommandScopeChip type, the three matcher functions, the four no-op consumer filters (services, forms, prescribing, favourites), and the band's scope shelf plus its empty-state Clear-scopes button. Net 453 deletions / 23 insertions across 13 files. Behaviour-preserving by construction: every matcher early-returned true on the permanently-empty array, so each call site was already a no-op. SearchCommandContext survives with query/modeId — applications-launcher-page still consumes it. Verified verify:cheap exit 0 (460 files / 4792 tests) and ui-smoke 93 passed, the one failure being the pre-existing PDF-canvas test that fails identically without this change. | 2026-07-31 | +| #205 | task | Retire therapy-compass.css (tc- class system) in favour of design-system classes | Resolved in PR-T (claude/ds-v2-therapy-teardown): therapy-compass.css deleted, all tc- classes migrated to design-system classes (therapyBtn/outlineControl/softControl), layout import removed, filter-sheet migrated; design-system contract check green. qtc- (cardiac QTc) identifiers deliberately untouched. | 2026-08-01 | diff --git a/scripts/check-design-system-contract.mjs b/scripts/check-design-system-contract.mjs index 3910ddae52..132ad429ad 100644 --- a/scripts/check-design-system-contract.mjs +++ b/scripts/check-design-system-contract.mjs @@ -71,7 +71,15 @@ function findTherapyButtonsWithoutBaseClass(file) { (attribute) => ts.isJsxAttribute(attribute) && attribute.name.getText(source) === "className", ); const classText = classAttribute && ts.isJsxAttribute(classAttribute) ? jsxClassText(classAttribute) : ""; - if (!classText.includes("tc-btn")) { + const classSource = classAttribute && ts.isJsxAttribute(classAttribute) ? classAttribute.getText(source) : ""; + // Recipes from controls.ts all include therapyBtn; accept either the base + // export or a named control recipe in the className expression text. + const hasTherapyInteraction = + /\btherapyBtn\b/.test(classText) || + /\b(?:therapyBtn|accentControl|commandControl|outlineControl|softControl|iconControl|linkButton)\b/.test( + classSource, + ); + if (!hasTherapyInteraction) { const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1; findings.push(`${file.relativePath}:${line}`); } @@ -131,7 +139,10 @@ const therapyInlineStyleFindings = therapyFiles.flatMap(({ relativePath }) => { .filter(({ line }) => /style=\{/.test(line)) .filter(({ line }) => { if (relativePath.endsWith("/icons.tsx")) return !/style=\{style\}/.test(line); - if (relativePath.endsWith("/ui.tsx")) return !/--tc-meter-width/.test(line); + // The completeness meter's fill is a data-driven percentage, which no utility + // class can express. `--tc-meter-width` only existed to hand that value to + // therapy-compass.css; as that stylesheet retires, the width is set directly. + if (relativePath.endsWith("/ui.tsx")) return !/--tc-meter-width|width: `\$\{v\}%`/.test(line); if (relativePath.endsWith("/screens/compare-screen.tsx")) return !/--tc-compare-columns/.test(line); return true; }) @@ -159,21 +170,33 @@ assert( assert(!/outline\s*:\s*none/i.test(therapySource), "Therapy Compass suppresses a focus outline"); assert(!therapySource.toLowerCase().includes("#8a94a3"), "the low-contrast patient-sheet gray returned"); -const therapyCss = textAt("src/components/therapy-compass/therapy-compass.css"); -assert(!/(?:^|[^0-9])44px/.test(therapyCss), "Therapy Compass CSS contains a literal 44px tap target"); -assert(therapyCss.includes("--tc-paper-muted: #5b6472"), "the fixed paper palette must keep its accessible muted ink"); +// Therapy's parallel stylesheet is retired. Printable paper tokens, print +// isolation, and interaction recipes live in globals.css + controls.ts. assert( - therapyCss.includes('.tc-paper [contenteditable="true"]:focus-visible'), + !fs.existsSync(path.join(ROOT, "src/components/therapy-compass/therapy-compass.css")), + "therapy-compass.css must stay deleted — residuals live in globals.css", +); +const globalsForTherapy = textAt("src/app/globals.css"); +assert( + globalsForTherapy.includes("--tc-paper-muted: #5b6472"), + "the fixed paper palette must keep its accessible muted ink", +); +assert( + globalsForTherapy.includes('[data-therapy-paper] [contenteditable="true"]:focus-visible'), "patient-sheet editing needs a visible focus state", ); -assert(therapyCss.includes(".tc-btn:hover:not(:disabled)"), "Therapy buttons need a hover state"); -assert(therapyCss.includes(".tc-btn:disabled"), "Therapy buttons need a disabled state"); +assert(globalsForTherapy.includes("body:has([data-therapy-root])"), "Therapy print isolation must stay in globals.css"); +assert(globalsForTherapy.includes("[data-therapy-no-print]"), "Therapy no-print hooks must stay in globals.css"); +const controlsSource = textAt("src/components/therapy-compass/controls.ts"); +assert(controlsSource.includes("hover:enabled:"), "Therapy buttons need a hover state"); +assert(controlsSource.includes("disabled:"), "Therapy buttons need a disabled state"); +assert(controlsSource.includes("export const therapyBtn"), "Therapy shared button recipe is missing"); -const paperStart = therapyCss.indexOf(".tc-root .tc-screens-sheets-screen-023"); -const paperEnd = therapyCss.indexOf(".tc-root .tc-screens-sheets-screen-050"); -const hasPaperBoundaries = paperStart >= 0 && paperEnd > paperStart; +const paperBlockStart = globalsForTherapy.indexOf("[data-therapy-paper] {"); +const paperBlockEnd = globalsForTherapy.indexOf("[data-therapy-paper] [contenteditable"); +const hasPaperBoundaries = paperBlockStart >= 0 && paperBlockEnd > paperBlockStart; assert(hasPaperBoundaries, "patient-sheet paper rule boundaries are missing or misordered"); -const paperRules = hasPaperBoundaries ? therapyCss.slice(paperStart, paperEnd) : therapyCss; +const paperRules = hasPaperBoundaries ? globalsForTherapy.slice(paperBlockStart, paperBlockEnd) : ""; assert( !/var\(--(?:background|surface|border|text|clinical|command|focus)/.test(paperRules), "patient-sheet paper rules leaked theme-reactive application tokens", diff --git a/scripts/design-system-contract-utils.mjs b/scripts/design-system-contract-utils.mjs index 0c4f642ab0..2f9d55cc74 100644 --- a/scripts/design-system-contract-utils.mjs +++ b/scripts/design-system-contract-utils.mjs @@ -39,11 +39,6 @@ export const RAW_COLOR_EXEMPTIONS = [ pattern: /^src\/lib\/theme\.ts$/, scope: "app-theme-colors", }, - { - category: "printable Therapy paper", - pattern: /^src\/components\/therapy-compass\/therapy-compass\.css$/, - scope: "therapy-paper", - }, { category: "printable factsheet paper", pattern: /^src\/components\/factsheets\/factsheet-detail-page\.tsx$/, @@ -256,15 +251,6 @@ export function rawColorContractSource(relativePath, source, reportFailure = () if (!exemption) return source; if (exemption.scope === "whole-file") return ""; - if (exemption.scope === "therapy-paper") { - const ranges = [balancedBlockRange(source, ".tc-paper {"), balancedBlockRange(source, "@media print {")]; - if (ranges.some((range) => !range)) { - reportFailure("printable Therapy paper raw-color boundaries are missing"); - return source; - } - return maskRanges(source, ranges); - } - if (exemption.scope === "app-theme-colors") { // Anchored on the declaration keyword, not a bare identifier, so a later // *reference* to APP_THEME_COLORS can never be mistaken for the boundary. diff --git a/src/app/(search-app)/layout.tsx b/src/app/(search-app)/layout.tsx index 2423edbdfe..428133b5b4 100644 --- a/src/app/(search-app)/layout.tsx +++ b/src/app/(search-app)/layout.tsx @@ -1,7 +1,6 @@ import type { ReactNode } from "react"; import { SharedSearchAppShell } from "@/components/clinical-dashboard/shared-search-app-shell"; -import "@/components/therapy-compass/therapy-compass.css"; /** * Shared search chrome for mode homes and related routes. Keeping GlobalSearchShell diff --git a/src/app/globals.css b/src/app/globals.css index 305afaf3e0..89bf81f9ae 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3367,3 +3367,184 @@ html.theme-transitioning *:after { border-color: CanvasText; } } + +/* ============================================================================= + * Therapy Compass residuals — irreducible rules that cannot become utilities. + * Print isolation, printable paper tokens, CSS-var comparison grids, clinician + * toggle ::before track, and thin scrollbars. Everything else in the old + * therapy-compass.css parallel system is token-backed Tailwind on the components. + * ============================================================================= */ + +/* Portaled under the universal header on phones: the collapse row owns + stacking/hide, so sticky would fight the shared chrome geometry. */ +#phone-header-collapse-addon-slot [data-testid="therapy-compass-section-nav"] { + position: relative; + top: auto; + z-index: auto; +} + +[data-therapy-scroll] { + scrollbar-color: var(--border-strong) transparent; + scrollbar-width: thin; +} + +[data-therapy-scroll]::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +[data-therapy-scroll]::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 8px; + background: var(--border-strong); + background-clip: content-box; +} + +.therapy-compare-grid { + display: grid; + grid-template-columns: var(--tc-compare-columns); + border-bottom: 1px solid var(--border); +} + +.therapy-compare-cell, +.therapy-compare-row-label { + padding: var(--tc-compare-cell-padding); +} + +[data-therapy-clinician-track] { + position: relative; + width: var(--spacing-tap); + height: var(--spacing-tap); + flex: none; + border: 0; + border-radius: 999px; + background: transparent; + padding: 0; + cursor: pointer; +} + +[data-therapy-clinician-track]::before { + position: absolute; + top: 50%; + left: 50%; + width: 42px; + height: 24px; + border-radius: 12px; + background: var(--border-strong); + content: ""; + transform: translate(-50%, -50%); + transition: background-color 150ms ease; +} + +[data-therapy-clinician-track][data-active="true"]::before { + background: var(--clinical-accent); +} + +[data-therapy-clinician-knob] { + position: absolute; + z-index: 1; + top: 50%; + left: calc(50% - 18px); + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--surface-raised); + box-shadow: var(--shadow-tight); + transform: translate(0, -50%); + transition: transform 150ms var(--ease-spring); +} + +[data-therapy-clinician-track][data-active="true"] [data-therapy-clinician-knob] { + transform: translate(18px, -50%); +} + +[data-therapy-paper] { + /* Fixed print palette — deliberately NOT theme-reactive. Accent mirrors Clinical Sky. */ + --tc-paper-background: #ffffff; + --tc-paper-ink: #0f1720; + --tc-paper-body: #2b3444; + --tc-paper-muted: #5b6472; + --tc-paper-accent: #185c99; + --tc-paper-accent-strong: #1d6fb8; + --tc-paper-accent-background: #f4f7fb; + --tc-paper-accent-border: #d7e3f0; + --tc-paper-border: #d6dce5; + --tc-paper-border-subtle: #e6e9ee; + --tc-paper-warning: #8a5a12; + --tc-paper-warning-background: #fbf6ee; + --tc-paper-warning-border: #f0e2c8; + --tc-paper-focus: #1d6fb8; + --tc-paper-shadow: 0 2px 6px rgb(15 23 32 / 8%), 0 18px 42px rgb(15 23 32 / 10%); + background: var(--tc-paper-background); + color: var(--tc-paper-ink); +} + +[data-therapy-paper] [contenteditable="true"]:focus-visible { + outline-color: var(--tc-paper-focus); +} + +@media (max-width: 640px) { + [data-therapy-scroll-sm], + .therapy-compare-table { + overflow-x: auto !important; + overscroll-behavior-inline: contain; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + } + + .therapy-compare-tabs { + gap: 18px !important; + overflow-x: auto; + overscroll-behavior-inline: contain; + scrollbar-width: thin; + } + + .therapy-compare-tabs > * { + flex: none; + } + + .therapy-pathway-list { + border-right: 0 !important; + border-bottom: 1px solid var(--border); + } + + [data-therapy-paper] { + padding: 28px 22px !important; + } +} + +@media print { + @page therapy-compass-sheet { + size: a4 portrait; + margin: 14mm; + } + + body:has([data-therapy-root]) { + background: #ffffff !important; + } + + body:has([data-therapy-root]) * { + visibility: hidden !important; + } + + body:has([data-therapy-root]) [data-therapy-paper], + body:has([data-therapy-root]) [data-therapy-paper] * { + visibility: visible !important; + } + + body:has([data-therapy-root]) [data-therapy-paper] { + position: absolute !important; + top: 0; + left: 0; + width: 100% !important; + max-width: none !important; + margin: 0 !important; + border: 0 !important; + box-shadow: none !important; + page: therapy-compass-sheet; + } + + body:has([data-therapy-root]) [data-therapy-no-print] { + display: none !important; + } +} diff --git a/src/components/therapy-compass/bindings.tsx b/src/components/therapy-compass/bindings.tsx index 9247f44f9a..fc5e9bdfbd 100644 --- a/src/components/therapy-compass/bindings.tsx +++ b/src/components/therapy-compass/bindings.tsx @@ -186,16 +186,44 @@ export type TcBindings = { const TcContext = createContext(null); function navStyle(active: boolean): string { - return `tc-nav-control${active ? " tc-is-active" : ""}`; + return [ + "inline-flex min-h-tap flex-none items-center justify-center gap-2 whitespace-nowrap rounded-[10px] border border-transparent bg-transparent px-[13px] py-2 text-sm-minus font-medium text-[color:var(--text-muted)] no-underline", + "hover:enabled:border-[color:var(--border-strong)] hover:enabled:bg-[color:var(--surface-subtle)] hover:enabled:text-[color:var(--text)]", + active + ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] font-semibold text-[color:var(--clinical-accent-hover)]" + : "", + ] + .filter(Boolean) + .join(" "); } function tabStyle(active: boolean): string { - return `tc-tab-control${active ? " tc-is-active" : ""}`; + return [ + "inline-flex min-h-tap items-center justify-center border-0 border-b-2 border-b-transparent bg-transparent px-1 py-2.5 text-sm font-medium text-[color:var(--text-muted)]", + "hover:enabled:text-[color:var(--text)]", + active ? "border-b-[color:var(--clinical-accent)] font-semibold text-[color:var(--clinical-accent-hover)]" : "", + ] + .filter(Boolean) + .join(" "); } function segStyle(active: boolean): string { - return `tc-segment-control${active ? " tc-is-active" : ""}`; + return [ + "inline-flex min-h-tap items-center justify-center rounded-md border-0 bg-transparent px-4 py-[7px] text-sm-minus font-semibold text-[color:var(--text-muted)]", + "hover:enabled:bg-[color:var(--surface-subtle)] hover:enabled:text-[color:var(--text)]", + active ? "bg-[color:var(--surface)] text-[color:var(--clinical-accent-hover)] shadow-[var(--shadow-tight)]" : "", + ] + .filter(Boolean) + .join(" "); } function chipStyle(active: boolean): string { - return `tc-chip-control${active ? " tc-is-active" : ""}`; + return [ + "inline-flex min-h-tap items-center justify-center rounded-[10px] border border-[color:var(--border)] bg-[color:var(--surface)] px-3.5 py-2 text-sm-minus font-semibold text-[color:var(--text-muted)]", + "hover:enabled:border-[color:var(--border-strong)] hover:enabled:bg-[color:var(--surface-subtle)] hover:enabled:text-[color:var(--text)]", + active + ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] font-semibold text-[color:var(--clinical-accent-hover)]" + : "", + ] + .filter(Boolean) + .join(" "); } export function TcProvider({ children }: { children: ReactNode }) { @@ -478,8 +506,8 @@ export function TcProvider({ children }: { children: ReactNode }) { toggleContacts: () => toggleSection("contacts"), sheetClinician, toggleClinician: () => setSheetClinician((prev) => !prev), - clinicianTrack: `tc-clinician-track${sheetClinician ? " tc-is-active" : ""}`, - clinicianKnob: `tc-clinician-knob${sheetClinician ? " tc-is-active" : ""}`, + clinicianTrack: "", + clinicianKnob: "", printSheet: () => { if (typeof window !== "undefined") window.print(); }, diff --git a/src/components/therapy-compass/controls.ts b/src/components/therapy-compass/controls.ts index 24dfe0a6fd..66ffaeaf8d 100644 --- a/src/components/therapy-compass/controls.ts +++ b/src/components/therapy-compass/controls.ts @@ -1,9 +1,75 @@ -// Semantic class recipes. The route stylesheet owns all visual/state rules. -export const accentControl = "tc-control tc-control-accent"; -export const commandControl = "tc-control tc-control-command"; -export const outlineControl = "tc-control tc-control-outline"; -export const softControl = "tc-control tc-control-soft"; -export const iconControl = "tc-control-icon"; -export const linkButton = "tc-link-button"; -export const card = "tc-card"; -export const heroCard = "tc-card tc-hero-card"; +import { cn } from "@/components/ui-primitives"; + +/** + * Therapy control recipes — token-backed Tailwind only (Clinical White / Sky Graphite). + * Selected / pressed states use aria-pressed / aria-current so call sites do not need a + * parallel `tc-is-*` class. Hover states use `hover:enabled:` so disabled controls stay quiet. + * + * Focus ring is local (same token string as ui-primitives `searchFocusRing`) because the + * shared export was renamed away from `focusRing` on main; therapy buttons are not search chrome. + */ +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +export const therapyBtn = cn( + "cursor-pointer font-[inherit] transition-[background-color,border-color,color,box-shadow,transform] duration-150", + "hover:enabled:-translate-y-px active:enabled:translate-y-px", + "disabled:cursor-not-allowed disabled:opacity-55", + focusRing, +); + +const controlBase = cn( + "inline-flex min-h-tap items-center justify-center gap-2 rounded-[11px] text-sm-minus font-semibold", + therapyBtn, +); + +export const accentControl = cn( + controlBase, + "border-0 bg-[color:var(--clinical-accent)] px-[18px] text-[color:var(--clinical-accent-contrast)]", + "hover:enabled:bg-[color:var(--clinical-accent-hover)] hover:enabled:shadow-[var(--shadow-hover)]", +); + +export const commandControl = cn( + controlBase, + "gap-[9px] border-0 bg-[color:var(--command)] px-5 text-sm text-[color:var(--command-contrast)] shadow-[var(--shadow-tight)]", + "hover:enabled:bg-[color:var(--command-hover)] hover:enabled:shadow-[var(--shadow-hover)]", +); + +export const outlineControl = cn( + controlBase, + "border border-[color:var(--border-strong)] bg-[color:var(--surface)] px-4 text-[color:var(--text)]", + "hover:enabled:border-[color:var(--border-strong)] hover:enabled:bg-[color:var(--surface-subtle)] hover:enabled:shadow-[var(--shadow-hover)]", + "aria-pressed:border-[color:var(--clinical-accent)] aria-pressed:text-[color:var(--clinical-accent-hover)]", +); + +export const softControl = cn( + controlBase, + "border border-[color:var(--border)] bg-[color:var(--surface)] px-4 font-medium text-[color:var(--text-muted)]", + "hover:enabled:border-[color:var(--border-strong)] hover:enabled:bg-[color:var(--surface-subtle)] hover:enabled:text-[color:var(--text)] hover:enabled:shadow-[var(--shadow-hover)]", + "aria-pressed:border-[color:var(--clinical-accent-border)] aria-pressed:bg-[color:var(--clinical-accent-soft)] aria-pressed:font-semibold aria-pressed:text-[color:var(--clinical-accent-hover)]", + "data-[tone=success]:border-[color:var(--success-border)] data-[tone=success]:bg-[color:var(--success-bg)] data-[tone=success]:font-semibold data-[tone=success]:text-[color:var(--success-text)]", +); + +export const iconControl = cn( + therapyBtn, + "inline-flex h-tap w-tap items-center justify-center rounded-[9px] border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-soft)]", + "hover:enabled:border-[color:var(--border-strong)] hover:enabled:bg-[color:var(--surface-subtle)] hover:enabled:text-[color:var(--text)]", +); + +export const linkButton = cn( + therapyBtn, + "border-0 bg-transparent p-0 text-sm-minus font-semibold text-[color:var(--clinical-accent)]", + "hover:enabled:text-[color:var(--clinical-accent-hover)] hover:enabled:underline hover:enabled:underline-offset-[3px]", +); + +export const card = + "rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-soft)]"; + +export const heroCard = cn(card, "border-l-[3px] border-l-[color:var(--clinical-accent)]"); + +export const flexControl = "min-w-[150px] flex-1"; + +export const compactControl = "px-[13px] text-xs"; + +/** Shared hover wash for list rows. */ +export const therapyRow = "transition-colors duration-100 hover:bg-[color:var(--surface-subtle)]"; diff --git a/src/components/therapy-compass/filter-sheet.tsx b/src/components/therapy-compass/filter-sheet.tsx index 231551fc93..021671fea8 100644 --- a/src/components/therapy-compass/filter-sheet.tsx +++ b/src/components/therapy-compass/filter-sheet.tsx @@ -19,6 +19,8 @@ import { CheckIcon, SlidersIcon, XIcon } from "./icons"; * * The controls here are the same `aria-pressed` toggles the wide viewport has * always used, so both breakpoints now describe the same state the same way. + * Selected / success visuals come from the softControl recipe (`aria-pressed` + * and `data-tone=success`) — no parallel `tc-is-*` classes. */ export function TherapyFilterSheet({ open, @@ -65,12 +67,7 @@ export function TherapyFilterSheet({ testId="therapy-filter-panel" headerActions={ clearableCount > 0 ? ( - @@ -81,7 +78,7 @@ export function TherapyFilterSheet({ {resultCount} therap{resultCount === 1 ? "y" : "ies"} - @@ -98,7 +95,7 @@ export function TherapyFilterSheet({ type="button" onClick={() => onToggleTopic(topic)} aria-pressed={on} - className={`tc-btn min-h-12 ${softControl}${on ? " tc-is-selected" : ""}`} + className={`${softControl} min-h-12`} > {on ? : null} {topic} @@ -115,17 +112,13 @@ export function TherapyFilterSheet({ type="button" onClick={onToggleReviewed} aria-pressed={reviewedOnly} - className={`tc-btn min-h-12 ${softControl}${reviewedOnly ? " tc-is-success" : ""}`} + data-tone={reviewedOnly ? "success" : undefined} + className={`${softControl} min-h-12`} > {reviewedOnly ? : null} Reviewed only - @@ -152,11 +145,12 @@ export function TherapyFilterTrigger({ type="button" onClick={onToggle} aria-expanded={open} + aria-pressed={activeCount > 0} aria-haspopup="dialog" aria-controls={open ? panelId : undefined} data-testid="therapy-filter-trigger" title="Filter therapies" - className={`tc-btn ${softControl}${activeCount > 0 ? " tc-is-selected" : ""}`} + className={softControl} > Filter diff --git a/src/components/therapy-compass/nav.tsx b/src/components/therapy-compass/nav.tsx index bf80ea968d..ec754f59bb 100644 --- a/src/components/therapy-compass/nav.tsx +++ b/src/components/therapy-compass/nav.tsx @@ -6,17 +6,26 @@ import { PhoneHeaderCollapsePortal } from "@/components/clinical-dashboard/phone import { ModeNav, type ModeNavItem } from "@/components/mode-nav/mode-nav"; import { MAX_COMPARE, useTcBindings } from "./bindings"; +import { therapyBtn } from "./controls"; /** Core Therapy destinations for non-home screens. */ export function TherapyCompassNav() { const b = useTcBindings(); const navShell = ( -
-
-
- -
-
+
{/* records list */} -
-