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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| 2026-08-18 | claude/therapy-mode-consistency-a466b0 | 244accbde60c5b39f9a91c7f2096053a2f2c5a65 | Therapy Compass shared-component convergence B3/C/D: control recipes to shared Button, 16 card surfaces to card-recipes cardSurface/heroCard, six page headers to shared PageHeader, plus two strengthened responsive-contract assertions | Approved for review as PR #2122 — 19 button call sites converted with per-screen one-filled-command checks, all 16 card surfaces converged (compare table scroll frame deliberately excluded as a frame not a card), six of nine h1 converted (detail hero, patient-handout title and placeholder hero left; PageHeader renders a header element that print CSS hides unconditionally). therapyBtn retained: still the affordance for ~20 non-Button raw call sites. Ratchets fell, none rose. | eslint clean; tsc --noEmit clean; verify:pr-local Test Files 673 passed (673), Tests 7274 passed \| 4 skipped (7278); build re-run standalone exit 0 after BUILD_REFUSED_DEV_SERVER; check:bundle-budget all routes within tolerance; verify:ui 447 passed (20.4m) on Playwright-matched Chromium 1234; check:design-system-contract passed |
4 changes: 2 additions & 2 deletions docs/design-system/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `AnswerFooter` | answer | yes | yes | inherited-global-root | yes | no | 1 |
| `AsyncButton` | controls | yes | yes | inherited-global-root | yes | no | 4 |
| `Breadcrumb` | layout | yes | yes | inherited-global-root | yes | no | 1 |
| `Button` | controls | yes | yes | inherited-global-root | yes | no | 3 |
| `Button` | controls | yes | yes | inherited-global-root | yes | no | 11 |
| `Checkbox` | controls | yes | yes | no | yes | no | 0 |
| `Chip` | controls | yes | yes | inherited-global-root | yes | no | 3 |
| `Citation` | source | yes | yes | no | yes | no | 0 |
Expand All @@ -990,7 +990,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `LoadingPanel` | feedback | yes | yes | inherited-global-root | yes | no | 11 |
| `MissingValue` | feedback | yes | yes | inherited-global-root | yes | no | 3 |
| `OverlayRoot` | layout | yes | yes | inherited-global-root | yes | no | 1 |
| `PageHeader` | layout | yes | yes | inherited-global-root | yes | no | 2 |
| `PageHeader` | layout | yes | yes | inherited-global-root | yes | no | 8 |
| `Pagination` | controls | yes | yes | no | yes | no | 0 |
| `PanelHeading` | layout | yes | yes | inherited-global-root | yes | no | 2 |
| `Progress` | feedback | yes | yes | no | yes | no | 0 |
Expand Down
45 changes: 42 additions & 3 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,28 @@
"directImportFiles": [
"src/components/AccessibleTable.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/therapy-compass/screens/brief-screen.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
"src/components/therapy-compass/screens/detail-screen.tsx",
"src/components/therapy-compass/screens/other-screen.tsx",
"src/components/therapy-compass/screens/pathways-screen.tsx",
"src/components/therapy-compass/screens/recommend-screen.tsx",
"src/components/therapy-compass/therapy-card.tsx",
"src/components/therapy-compass/workspace.tsx",
"src/components/ui/confirm-dialog.tsx",
"src/components/ui/print-output.tsx"
],
"productImportFiles": [
"src/components/AccessibleTable.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/therapy-compass/screens/brief-screen.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
"src/components/therapy-compass/screens/detail-screen.tsx",
"src/components/therapy-compass/screens/other-screen.tsx",
"src/components/therapy-compass/screens/pathways-screen.tsx",
"src/components/therapy-compass/screens/recommend-screen.tsx",
"src/components/therapy-compass/therapy-card.tsx",
"src/components/therapy-compass/workspace.tsx",
"src/components/ui/print-output.tsx"
],
"designSync": {
Expand All @@ -298,6 +314,7 @@
"tests/accessible-table.dom.test.tsx",
"tests/design-sync-visual-exports.test.ts",
"tests/design-system-adoption.test.ts",
"tests/therapy-compass-responsive-contract.test.ts",
"tests/ui-v2-components.dom.test.tsx"
],
"baseline": {
Expand Down Expand Up @@ -1129,15 +1146,37 @@
"source": "src/components/ui/page-header.tsx",
"sourceExported": true,
"entryExported": true,
"directImportFiles": ["src/components/dsm/dsm-page-header.tsx", "src/components/information-page-shell.tsx"],
"productImportFiles": ["src/components/dsm/dsm-page-header.tsx", "src/components/information-page-shell.tsx"],
"directImportFiles": [
"src/components/dsm/dsm-page-header.tsx",
"src/components/information-page-shell.tsx",
"src/components/therapy-compass/screens/brief-screen.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
"src/components/therapy-compass/screens/other-screen.tsx",
"src/components/therapy-compass/screens/pathways-screen.tsx",
"src/components/therapy-compass/screens/recommend-screen.tsx",
"src/components/therapy-compass/screens/sheets-screen.tsx"
],
"productImportFiles": [
"src/components/dsm/dsm-page-header.tsx",
"src/components/information-page-shell.tsx",
"src/components/therapy-compass/screens/brief-screen.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
"src/components/therapy-compass/screens/other-screen.tsx",
"src/components/therapy-compass/screens/pathways-screen.tsx",
"src/components/therapy-compass/screens/recommend-screen.tsx",
"src/components/therapy-compass/screens/sheets-screen.tsx"
],
"designSync": {
"listedInSourceMap": true,
"listedInDtsProps": true,
"preview": ".design-sync/previews/PageHeader.tsx",
"previewValid": true
},
"testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"],
"testFiles": [
"tests/design-sync-visual-exports.test.ts",
"tests/therapy-compass-responsive-contract.test.ts",
"tests/ui-v2-components.dom.test.tsx"
],
"baseline": {
"targetLayer": "v2",
"liveLayer": "v2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 2,
"id": "197cb013-5541-4877-9d56-c37a4953d79f",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "task",
"summary": "One hand-drawn SVG checkmark survived the Therapy Compass lucide sweep",
"detail": "recommend-screen.tsx:75 still inlines a 7-line hand-drawn <svg> tick inside the QUICK CONSTRAINTS pills; commit 7ce820f2c converted the other 32 glyphs in therapy-compass/** to lucide-react and deleted icons.tsx. It is a one-line swap to lucide Check, which the same file already imports for its copy buttons. Left out of the B3 Button conversion deliberately to keep that diff purely mechanical. Stop rule: it is cosmetic only - the pill's pressed state is already carried by aria-pressed plus border and text tokens, so nothing is colour-only while this waits.",
"issueUlid": "01M0ADWJHYTAQKCNFSXW52CDSG"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 2,
"id": "654217d2-416b-4de8-a847-6e2812c3653d",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "task",
"summary": "Three Therapy Compass h1 elements still sit outside PageHeader, and the patient-sheet builder renders two of them at once",
"detail": "Stage D converted six therapy page headers to the shared PageHeader. Three h1 elements were deliberately left. (1) detail-screen.tsx:88 is the therapy record name inside the hero card, interleaved with an aria-live notice, aliases and a TagRow that PageHeader has no slots for - and PageHeader renders a <header>, which globals.css hides unconditionally under @media print, so adopting it would delete the record name from a printed therapy record. (2) sheets-screen.tsx:178 is the contentEditable title of the generated patient handout on --tc-paper-ink tokens; it is document content, not page chrome. (3) other-screen.tsx:28 is the centred placeholder hero, already at text-2xl. Consequence worth fixing: sheets-screen now renders the PageHeader h1 and the paper h1 simultaneously, so that page has two h1 elements - pre-existing, not introduced here, and the likely fix is demoting the paper title to h2 since the builder page is the document. Stop rule: do not solve (1) by adding a print exception for <header>; the element-name print rule is documented as transitional and must not be extended.",
"issueUlid": "01M0AF9GB3D6G8TCE6EPRNJAF0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 2,
"id": "8bfa1d6f-e8ae-496b-9ad1-15c1ba718831",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "rec",
"summary": "Five independent SectionHeading implementations exist across modes with no shared recipe",
"detail": "clinical-dashboard/dashboard-shell.tsx:17, clinical-dashboard/search-pins-menu.tsx:97, formulation/formulation-ui.tsx:81, specifiers/specifier-ui.tsx:237, plus a fifth in therapy-compass/ui.tsx deleted unreferenced in 7d2c84a11. Same shape as the problem card-recipes.ts solved for cards, but spanning four modes. Two of the five already share an identical {eyebrow, title, body} signature - the natural shared contract. Stop rule: do not fold these into ui-primitives.tsx; COMPONENTS.md section 0.4 lists it as over-budget and slated to split.",
"issueUlid": "01M0AD3SBBZF006GJ5MJW1E4BB"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 2,
"id": "8c14c6a0-f893-4659-b84e-62823d832c97",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P2",
"type": "task",
"summary": "Therapy Compass has three shared-component stages left: Button call sites, card-recipes adoption, and page headers",
"detail": "Branch claude/therapy-mode-consistency-a466b0 landed A/B1/B2/E/F/G in 7ce820f2c..7d2c84a11. Remaining: B3 (18 control-recipe call sites across 9 files), C (card/heroCard plus the inline fork at therapy-card.tsx:71 bypass card-recipes.ts), D (7 hand-rolled h1, 3 at text-3xl-minus). Stop rule: one browser pass and one baseline refresh after all three, never per stage.",
"issueUlid": "01M0AD3GDAFEWQZ5HN2TWP3V1S"
}
}
108 changes: 49 additions & 59 deletions src/components/therapy-compass/controls.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { cardSurface, focusRing } from "@/components/card-recipes";
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.
*
* The three button recipes that used to live here (`commandControl`, `outlineControl`,
* `iconControl`) are gone: every action they dressed is now the shared `Button` from
* `@/components/ui/button`, per COMPONENTS.md section 9.1. What remains is the affordance
* layer for the controls that are *not* buttons in the design-system sense — list rows,
* disclosure headers and chips that need therapy's focus/hover behaviour without claiming
* a Button variant.
*
* Hover states use `hover:not-aria-disabled:enabled:` so unavailable controls stay quiet
* under BOTH disabled encodings. `:enabled` alone was enough while every unavailable
Expand All @@ -12,12 +18,11 @@ import { cn } from "@/components/ui-primitives";
* its `title` is unreachable — see `ignoreUnavailableActivation` in ui-primitives), and
* such a button *is* `:enabled`, so it would otherwise light up on hover.
*
* 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.
* Focus affordance comes from the shared `focusRing` in `card-recipes.ts`. It used to be
* redeclared here, justified by the shared export having been "renamed away from `focusRing`
* on main" — that was already untrue: `card-recipes.ts` exports it, and the local copy meant a
* change to the focus contract had to find this file too.
*/
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-[var(--duration-quick)]",
"hover:not-aria-disabled:enabled:-translate-y-px active:not-aria-disabled:enabled:translate-y-px",
Expand All @@ -26,58 +31,43 @@ export const therapyBtn = cn(
focusRing,
);

const controlBase = cn(
"inline-flex min-h-tap items-center justify-center gap-2 rounded-md 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:not-aria-disabled:enabled:bg-[color:var(--clinical-accent-hover)] hover:not-aria-disabled: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(--e1)]",
"hover:not-aria-disabled:enabled:bg-[color:var(--command-hover)] hover:not-aria-disabled: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:not-aria-disabled:enabled:border-[color:var(--border-strong)] hover:not-aria-disabled:enabled:bg-[color:var(--surface-subtle)] hover:not-aria-disabled: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:not-aria-disabled:enabled:border-[color:var(--border-strong)] hover:not-aria-disabled:enabled:bg-[color:var(--surface-subtle)] hover:not-aria-disabled:enabled:text-[color:var(--text)] hover:not-aria-disabled: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-md border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--decoration-soft)]",
"hover:not-aria-disabled:enabled:border-[color:var(--border-strong)] hover:not-aria-disabled:enabled:bg-[color:var(--surface-subtle)] hover:not-aria-disabled: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:not-aria-disabled:enabled:text-[color:var(--clinical-accent-hover)] hover:not-aria-disabled:enabled:underline hover:not-aria-disabled: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)]");
/**
* Selected encoding for genuine `aria-pressed` toggles — the compare toggle, the favourite
* and the recommend constraint pills. The shared `secondary` Button variant deliberately
* carries no pressed state (most Buttons are not toggles), so a therapy toggle adds this
* through `className`. It was previously `cardActionPressed`, private to `therapy-card.tsx`;
* four surfaces need it, so it lives with the other therapy control recipes.
*
* Border AND text both move, and every call site also changes its label ("Compare" →
* "In compare"), so the pressed state is never carried by colour alone.
*/
export const controlPressed =
"aria-pressed:border-[color:var(--clinical-accent)] aria-pressed:text-[color:var(--clinical-accent-hover)]";

export const flexControl = "min-w-[150px] flex-1";
/**
* Favourite toggles add a filled glyph on top of `controlPressed`. The fill is a shape
* channel, so a saved favourite reads as saved without relying on the accent hue — which
* matters most for the icon-only favourite on the result card, where there is no label to
* change.
*/
export const favouritePressed = cn(controlPressed, "aria-pressed:[&_svg]:fill-current");

export const compactControl = "px-[13px] text-xs";
/**
* Prominence edge for therapy's two hero cards — the record hero on
* `detail-screen` and the top-match panel on `recommend-screen`.
*
* Deliberately NOT `cardAccentEdge`. That recipe is the *category* edge: it
* replaced the factsheet cards' inline `borderTopColor` and answers "which
* family does this card belong to". This edge answers a different question —
* "this is the principal panel on the screen" — and a therapy screen has only
* one category to begin with, so the top edge would say nothing the page does
* not already say. The side is the only thing therapy keeps; the colour comes
* through the shared `--cat-accent`, which `:root` aliases to
* `--clinical-accent`, so this renders exactly as the private token did while
* remaining remappable per category, per theme and under forced colors.
*
* The two hero cards used to be two independent literals. They are one now.
*/
export const heroAccentEdge = "border-l-[3px] border-l-[color:var(--cat-accent)]";

/** Shared hover wash for list rows. */
export const therapyRow = "transition-colors duration-[var(--duration-instant)] hover:bg-[color:var(--surface-subtle)]";
export const heroCard = cn(cardSurface, heroAccentEdge);
Loading
Loading