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
12 changes: 12 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,15 @@ a394d76631c7ea3405037baf58091b1c4c47fa3f:tests/codex-cloud-setup.test.ts:generic
14535ffc5f05d559f6bccac84099f9e6874bba6a:.gitleaksignore:generic-api-key:49
# Reviewed false positive: a localhost example URL with a dummy API key in a removed handoff document.
92097e53e096bc33c8b23f73874981b2e3214af3:docs/ed-care-plans/claude-build-handover-2026-08-21.md:generic-api-key:360
# Reviewed false positive: the exported browser-storage key name for the Therapy
# compare set. It is a `localStorage` key, visible in any browser's devtools and
# never a credential; `generic-api-key` matched it only because the identifier
# ends in "Key" and the hyphenated slug measures 3.99 Shannon entropy. Its five
# siblings (answer-thread, recent-queries, saved-*, search-pins) are the same
# shape and sit at 3.72-3.78, just under the rule's threshold, so this is a
# margin artefact rather than a new class of finding. Rewording the slug to duck
# the threshold was rejected: it is tuning an unverifiable number (gitleaks is
# not installed locally) and would leave a worse name behind. The value is not
# quoted in this comment, because a previous allowlist entry that quoted its own
# pattern tripped the same rule (see line 49 above).
9b34a0149759b2c2f1e8ed5d37f02ba1ac35cf39:src/lib/therapy-compare-memory.ts:generic-api-key:24
16 changes: 12 additions & 4 deletions data/repo-awareness-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"version": "repo-awareness-snapshot-v1",
"captured_revision": {
"sha": "6cd6ba816d2eef8a4642529701ed7d7656c09510",
"committed_at": "2026-08-26T10:14:41+08:00"
"sha": "ecae1752e52dd2cd363bfaaafe2491df24769606",
"committed_at": "2026-08-26T04:01:20+00:00"
},
"routes": {
"modes": [
Expand Down Expand Up @@ -3665,6 +3665,14 @@
},
"review_state": {
"records": [
{
"date": "2026-08-26",
"ref": "claude/therapy-compare-tray",
"head": "9b34a0149759b2c2f1e8ed5d37f02ba1ac35cf39",
"scope": "therapy compare tray: phone dock addon, add-in-place, stacked comparison, device memory",
"outcome": "built and verified; verify:cheap exit 0 (876 files / 10547 tests), verify:phone-chrome escalated to full Chromium 521 passed",
"checks": "verify:cheap, verify:phone-chrome (full verify:ui), lint, typecheck"
},
{
"date": "2026-08-26",
"ref": "codex/medication-risk-highlights",
Expand Down Expand Up @@ -24611,8 +24619,8 @@
}
],
"counts": {
"records": 2618,
"refs": 1597
"records": 2619,
"refs": 1598
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| 2026-08-26 | claude/therapy-compare-tray | 9b34a0149759b2c2f1e8ed5d37f02ba1ac35cf39 | therapy compare tray: phone dock addon, add-in-place, stacked comparison, device memory | built and verified; verify:cheap exit 0 (876 files / 10547 tests), verify:phone-chrome escalated to full Chromium 521 passed | verify:cheap, verify:phone-chrome (full verify:ui), lint, typecheck |
4 changes: 2 additions & 2 deletions docs/design-system/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,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 | 14 |
| `Button` | controls | yes | yes | inherited-global-root | yes | no | 16 |
| `Checkbox` | controls | yes | yes | no | yes | no | 0 |
| `Chip` | controls | yes | yes | inherited-global-root | yes | no | 5 |
| `ChoiceChip` | controls | yes | yes | inherited-global-root | yes | no | 4 |
Expand Down Expand Up @@ -1010,7 +1010,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `SearchField` | controls | yes | yes | no | yes | no | 0 |
| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 8 |
| `Select` | controls | yes | yes | inherited-global-root | yes | no | 2 |
| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 29 |
| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 30 |
| `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 6 |
| `SourceDesignationBadge` | source | yes | yes | inherited-global-root | yes | no | 4 |
| `SourceProvenance` | source | yes | yes | inherited-global-root | yes | no | 1 |
Expand Down
6 changes: 6 additions & 0 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,15 @@
"src/components/AccessibleTable.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/privacy-quiet-signal-page.tsx",
"src/components/therapy-compass/record/compare-action.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/search-screen.tsx",
"src/components/therapy-compass/therapy-card.tsx",
"src/components/therapy-compass/therapy-compare-tray.tsx",
"src/components/therapy-compass/workspace.tsx",
"src/components/tools/tools-search-results-page.tsx",
"src/components/ui/confirm-dialog.tsx",
Expand All @@ -297,13 +299,15 @@
"src/components/AccessibleTable.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/privacy-quiet-signal-page.tsx",
"src/components/therapy-compass/record/compare-action.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/search-screen.tsx",
"src/components/therapy-compass/therapy-card.tsx",
"src/components/therapy-compass/therapy-compare-tray.tsx",
"src/components/therapy-compass/workspace.tsx",
"src/components/tools/tools-search-results-page.tsx",
"src/components/ui/confirm-dialog.tsx",
Expand Down Expand Up @@ -1660,6 +1664,7 @@
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/mode-nav/mode-nav.tsx",
"src/components/therapy-compass/record/key-facts.tsx",
"src/components/therapy-compass/therapy-compare-tray.tsx",
"src/components/tools/tools-search-results-page.tsx",
"src/components/ui/confirm-dialog.tsx",
"src/components/ward-management/ward-management-navigation.tsx"
Expand Down Expand Up @@ -1692,6 +1697,7 @@
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/mode-nav/mode-nav.tsx",
"src/components/therapy-compass/record/key-facts.tsx",
"src/components/therapy-compass/therapy-compare-tray.tsx",
"src/components/tools/tools-search-results-page.tsx",
"src/components/ui/confirm-dialog.tsx"
],
Expand Down
29 changes: 22 additions & 7 deletions docs/search-chrome-behaviour.md
Original file line number Diff line number Diff line change
Expand Up @@ -764,12 +764,13 @@ floating element: it portals into a slot rendered _inside_ the dock's `<form>`
z-index, safe-area padding and scroll-hide transform. There is no bottom-offset
arithmetic and no second scroll listener anywhere in an addon.

Two claimants exist, and they are mutually exclusive by surface:
Three claimants exist, and they are mutually exclusive by surface:

| Addon kind | Slot id | Claimed by |
| ----------------------- | ----------------------------------------- | ----------------------------------------------------------- |
| `differentials-compare` | `differentials-mobile-compare-addon-slot` | Differentials submitted search / `/differentials/diagnoses` |
| `patient-details` | `patient-details-addon-slot` | Prescribing submitted search (dashboard-owned) |
| Addon kind | Slot id | Claimed by |
| ----------------------- | ----------------------------------------- | ----------------------------------------------------------------------------- |
| `differentials-compare` | `differentials-mobile-compare-addon-slot` | Differentials submitted search / `/differentials/diagnoses` |
| `patient-details` | `patient-details-addon-slot` | Prescribing submitted search (dashboard-owned) |
| `therapy-compare` | `therapy-compare-addon-slot` | Therapy Compass dock routes, **and only while the URL carries a compare set** |

Rules:

Expand All @@ -788,13 +789,27 @@ Rules:
opens a blank band at the bottom. `/medications` is a standalone mode home with
the composer in the hero and no dock at all; `/medications/[slug]` already opens
the patient sheet from its own nav header, so neither claims the addon.
- **An addon that can be empty must gate its claim on being non-empty.** The
therapy compare tray renders nothing until something is in the comparison, so
claiming the slot on every therapy route would reserve a tray-sized band under
a row that is not there. The shell therefore reads the set out of the URL
(`readTherapyCompareSlugCount`) and claims only when it is non-empty — the
claim and the render have to agree, in both directions.
- **Keep an addon exactly one row tall.** The clearance is a static token, so a
dock that grows covers page content by exactly its own growth. Anything that
needs more room opens a bottom `Sheet` instead of a taller bar; that is why
the compare tray's expanded state is a sheet and the Patient details panel is
one too.
- **Gate the portal at 639px**, matching `.phone-footer-layer`'s `sm:fixed`. The two
Compare bars gate at 1023px, which between 640–1023px portals into a slot on a
form that is not fixed. Do not copy that.

Coverage: `tests/phone-dock-addon-contract.test.ts` (registry, exclusivity, CSS/TS
value parity), `tests/patient-details-dock-action.dom.test.tsx` (portal target,
breakpoint, sheet wiring).
value parity, therapy route/claim gating), `tests/patient-details-dock-action.dom.test.tsx`
(portal target, breakpoint, sheet wiring), `tests/therapy-compare-tray.dom.test.tsx`
(portal target, breakpoint, empty-set silence, sheet wiring),
`tests/ui-therapy-nav-scroll.spec.ts` (the tray hides with the composer and
releases its reserve to `0rem`).

## Motion & Animation Preferences (#S4K1GA)

Expand Down
129 changes: 123 additions & 6 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -584,10 +584,20 @@
clears roughly 3.5rem less than the Compare bar. */
--phone-dock-patient-details-clearance: 9rem;
--phone-dock-patient-details-compact-clearance: 8.75rem;
/* Therapy compare tray: one row + 0.5rem gap, same geometry as Patient
details. The tray is required to stay exactly one row tall (its expanded
state is a sheet), which is what lets a single static number stay correct. */
--phone-dock-therapy-compare-clearance: 9rem;
--phone-dock-therapy-compare-compact-clearance: 8.75rem;
--patient-details-fab-gap: 0.5rem;
--patient-details-fab-padding: 0.3125rem 0.5rem 0.3125rem 1rem;
--patient-details-fab-count-size: 1.625rem;
--patient-details-fab-count-padding: 0.375rem;
--therapy-compare-tray-gap: 0.5rem;
--therapy-compare-tray-pip-gap: 0.125rem;
--therapy-compare-tray-pip-size: 1.5rem;
--therapy-compare-tray-padding: 0.25rem 0.5rem;
--therapy-compare-tray-summary-inset: 0.25rem;
/* Radius tokens are the single source of truth in @theme above (they also
generate the rounded-* utilities); do not redefine them here or var() and
the utilities drift apart. */
Expand Down Expand Up @@ -1459,6 +1469,101 @@ summary::-webkit-details-marker {
font-weight: 800;
}

/* Therapy Compass compare tray: the row above the search pill that says what is
in the comparison. Same in-flow trick as the two rows above, so scroll-hide
carries it with the composer.

It must stay ONE row tall at every state — the dock's content clearance is a
static token, so a dock that grows covers page content by exactly its growth.
The expanded state is a bottom sheet for that reason, not a taller bar.

Interaction states are NOT redeclared here: the summary row carries
`interactiveRowBase` and the actions are the shared `Button`, so hover, focus
and disabled behaviour come from the design system, per COMPONENTS.md 9.1. */
.therapy-compare-tray {
position: relative;
z-index: var(--z-raised, 10);
display: flex;
width: 100%;
align-items: center;
gap: var(--therapy-compare-tray-gap);
margin-bottom: var(--therapy-compare-tray-gap);
border-radius: var(--radius-lg);
border: 1px solid var(--border-strong);
background: color-mix(in srgb, var(--surface) 94%, transparent);
padding: var(--therapy-compare-tray-padding);
box-shadow: var(--e3);
}

.therapy-compare-tray__summary {
min-width: 0;
flex: 1 1 auto;
gap: var(--therapy-compare-tray-gap);
border-radius: var(--radius-md);
padding-inline: var(--therapy-compare-tray-summary-inset);
}

.therapy-compare-tray__pips {
display: flex;
flex-shrink: 0;
gap: var(--therapy-compare-tray-pip-gap);
}

.therapy-compare-tray__pip {
display: grid;
height: var(--therapy-compare-tray-pip-size);
width: var(--therapy-compare-tray-pip-size);
place-items: center;
border: 1px dashed var(--border-strong);
border-radius: var(--radius-pill, 9999px);
color: var(--text-muted);
font-size: 0.6875rem;
font-weight: 900;
transition: transform var(--duration-moderate) var(--ease-out-soft);
}

.therapy-compare-tray__pip[data-filled="true"] {
border-color: transparent;
background: var(--clinical-accent);
color: var(--clinical-accent-contrast);
}

/* The arriving slot pulses once. Derived from the set, so it fires whichever
control added the therapy. */
.therapy-compare-tray__pip[data-landed="true"] {
transform: scale(1.25);
}

@media (prefers-reduced-motion: reduce) {
.therapy-compare-tray__pip {
transition: none;
}

.therapy-compare-tray__pip[data-landed="true"] {
transform: none;
}
}

.therapy-compare-tray__names {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-heading);
font-size: 0.8125rem;
font-weight: 800;
}

.therapy-compare-tray__count {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-muted);
font-size: 0.6875rem;
font-weight: 700;
}

/* Bottom-docked composer: hint row + upward dropdown grow above the pill. */
.answer-footer-search-edge {
overflow: visible;
Expand Down Expand Up @@ -2794,6 +2899,17 @@ summary::-webkit-details-marker {
height: max(var(--phone-dock-patient-details-compact-clearance), calc(var(--safe-area-bottom) + 7.5rem));
}

/* Therapy Compass: the compare tray sits above the search pill in the dock.
Heights track --phone-dock-therapy-compare-* (same as content reserve). */
.answer-footer-search-dock[data-footer-addon="therapy-compare"] .answer-footer-search-backdrop {
height: max(var(--phone-dock-therapy-compare-clearance), calc(var(--safe-area-bottom) + 7.75rem));
}

.answer-footer-search-dock.document-mobile-search-compact[data-footer-addon="therapy-compare"]
.answer-footer-search-backdrop {
height: max(var(--phone-dock-therapy-compare-compact-clearance), calc(var(--safe-area-bottom) + 7.5rem));
}

.document-mobile-search-pill {
min-height: 3.6rem;
border-color: var(--border-strong);
Expand Down Expand Up @@ -2884,6 +3000,13 @@ summary::-webkit-details-marker {
transform: translateY(calc(100% + 0.5rem + var(--safe-area-bottom)));
}

/* Therapy compare tray: shell dock (.document-mobile-search-edge), one row,
so the same 0.5rem overshoot as Patient details. Without it the tray's top
border can leave a subpixel peep at the viewport edge while hidden. */
.answer-footer-search-dock.document-mobile-search-edge.answer-footer-search-edge[data-footer-addon="therapy-compare"][data-scroll-hidden="true"] {
transform: translateY(calc(100% + 0.5rem + var(--safe-area-bottom)));
}

/* Document viewer owns its floating composer (not the shared dock). Lift it
with the same --keyboard-height signal so overlay/visual keyboards do not
cover the focused input on document detail/source routes. */
Expand Down Expand Up @@ -4840,12 +4963,6 @@ html.theme-transitioning *:after {
}

@media (max-width: 640px) {
[data-therapy-scroll-sm] {
overflow-x: auto !important;
overscroll-behavior-inline: contain;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
.therapy-pathway-list {
border-right: 0 !important;
border-bottom: 1px solid var(--border);
Expand Down
Loading
Loading