diff --git a/docs/design-system/ADOPTION.md b/docs/design-system/ADOPTION.md index b734870ab..8219dd6f8 100644 --- a/docs/design-system/ADOPTION.md +++ b/docs/design-system/ADOPTION.md @@ -4,10 +4,10 @@ of truth is `adoption-contract.json`; the generated manifest and marked tables in this document must match it exactly. -- **Date:** 5 August 2026 -- **Current state:** 53 visual references are locally registered; all 47 production page routes - are owned across 14 surface families, with 55 route/component roots scanned; every declared - root remains on the compatibility shell. +- **Date:** 12 August 2026 +- **Current state:** 54 visual references are locally registered; all 51 production page routes + are owned across 14 surface families, with 59 route/component roots scanned; every declared + root uses the v2 shell and has declared proof with no committed visual baseline. - **Phase 1 blockers resolved first, in their own commits:** `#207` ungrounded `AnswerState`, `#208` clipboard composition. See [SPEC.md](SPEC.md) §13 PR 6 clinical review, blockers 1–2. - **Companions:** [SPEC.md](SPEC.md) · [COMPONENTS.md](COMPONENTS.md) · @@ -137,7 +137,7 @@ src/components/DocumentViewer.tsx navigation, zoom, fit, rotation, the viewing aid and fullscreen, and `PdfCanvasViewer` renders source pixels only. There is exactly one toolbar and one page readout in the viewer, and the contract test `tests/document-frame-contract.test.ts` holds that. It is not yet design-sync -registered among the 53 published visual exports. Do not invent a second frame or add inversion/filters; route document renders through +registered among the 54 published visual exports. Do not invent a second frame or add inversion/filters; route document renders through the existing viewer + frame. Keep every `role="alert"` semantic; route announcements through the announcer policy rather than deleting roles, because many `role="status"` sites are implicit polite live regions with no `aria-live` attribute and removing the role without an @@ -212,6 +212,19 @@ over passages no model wrote. `composeAnswerClipboardText()` now takes an explic tier flag, which both product callers pass. Pinned in `tests/answer-clipboard-product-path.dom.test.tsx` and `tests/answer-clipboard-composition.test.ts`. +**Single answer-copy payload contract (`#234`).** The live dashboard answer, prior thread turn, +and inline answer result all route clipboard output through +`src/components/clinical-dashboard/answer-copy-payload.ts`; a fourth copy surface must reuse this +module rather than assemble answer text independently. `resolveAnswerSources` preserves the +populated cited source set and falls back only when it is absent/empty; +`answerStateForAnswer` creates the shared safety projection; `citedSourcesOnly` prevents uncited +retrieval candidates from changing attribution; `singleDocumentClipboardMetadata` emits an audit +line only for a single cited document; and `buildAnswerClipboardText` is the final composer that +preserves render text while adding attribution, state caveats, and eligible provenance. Source-only +attribution comes from the answer quality tier, not `AnswerState.kind`, because an extractive answer +may also be ungrounded. `tests/answer-copy-payload.test.ts` and +`tests/answer-clipboard-product-path.dom.test.tsx` pin both the helper and all three callers. + **Clinical-owner wording approval (2026-08-05).** The clinical owner formally approved the fixed clinician-facing `responsive-compact` phone wording in `src/components/ui/verification-notice.tsx` exactly as shipped on this branch. The approval covers @@ -225,7 +238,7 @@ clamp, hidden instruction, retrieval change, visual baseline or patient/plain-la ## 3 · Registration disposition The Phase 1 local registration gap is closed. The visual symbols below are now included in the -53-component source-derived registry; support APIs remain entry-only by design: +54-component source-derived registry; support APIs remain entry-only by design: | Module | Symbols | First adopting surface | | --------------------------------------------- | --------------------------------------------------------------- | ------------------------ | @@ -316,6 +329,25 @@ chrome differs between them. | provenance | the document-admin source metadata block · the answer support caution (pre-adoption baseline) · a differentials source-status chip · the source preview popover | | answer | a ready grounded answer · an **ungrounded** answer showing the preserved caution (`#207`) · stale-evidence and source-only banners · a copied-clipboard sample (`#208`) · progress / empty / error / streaming | +### 7.1 Adoption evidence recorded for this pass (`#221`, `#235`, `#238`, `#245`) + +This PR records executable evidence rather than committing image baselines. The generated adoption +manifest remains `baseline: not-committed`, and no Playwright snapshot PNG is an adoption claim. + +| Surface | Current adopted evidence | Focused owner/check | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| forms | `FormField` is mounted by `TextField`/`Select`; required labels carry a text marker and optional labels remain unmarked. | `tests/ui-v2-form-field.dom.test.tsx` | +| headers/catalogues | Shared mode status notices now delegate to DS `EmptyState`; therapy loading/empty wrappers delegate to `LoadingPanel`/`EmptyState`; differential and favourites chip wrappers delegate to DS `Chip` while retaining their surface-specific density/tone mapping. | `tests/design-system-target-evidence.test.ts`, `tests/mode-home-status-notice.dom.test.tsx` | +| overlays | `Sheet` portals to `OverlayRoot`'s modal host by default. Settings, the mobile Clinical Guide sidebar, and the three answer-review sheets use that default; none opts out with `portal={false}`. | `tests/sheet.dom.test.tsx`, `tests/design-system-target-evidence.test.ts` | +| answer | The three product copy paths share the payload builder described in §2.6; answer-review overlays retain their existing content, dismissal, and focus-return props while using the portal default. | `tests/answer-copy-payload.test.ts`, `tests/answer-clipboard-product-path.dom.test.tsx` | +| cross-mode links | `responsive-compact` deliberately mounts a phone chip rail and an `md+` card rail so SSR and hydration agree. CSS makes only one rail visible/in the accessibility tree; selectors and analytics must target the variant rail, while `cross-mode-links-rail` remains the phone-only contract. | `tests/design-system-target-evidence.test.ts`, focused `tests/ui-smoke.spec.ts` CrossModeLinks journeys | + +**Residual evidence requirement.** Static and DOM checks establish portal ownership and preserve +focus-return inputs, but they do not prove every nested overlay's keyboard sequence in a real browser. +Before changing focus order, ancestor-scoped overlay styling, or the portal default, capture a focused +Chromium keyboard pass for settings, sidebar, and each answer-review sheet (open, Tab/Shift+Tab, +Escape, and return focus). This is follow-up evidence, not a visual-baseline commitment. + ## Generated adoption truth diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index 09125e620..585db62f3 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -4,7 +4,7 @@ specifications.** Prop shapes are normative contracts; token references are roles only — values live in the token files (see [TOKENS.md](TOKENS.md)). -- **Date:** 5 August 2026 · companions: [SPEC.md](SPEC.md) · [DECISIONS.md](DECISIONS.md) · +- **Date:** 12 August 2026 · companions: [SPEC.md](SPEC.md) · [DECISIONS.md](DECISIONS.md) · [GATES.md](GATES.md) - **Publication vocabulary:** `registered` — exported by `.design-sync/entry.tsx`, mapped to source, covered by a source-derived prop contract, preview, and direct static contract test · @@ -20,7 +20,7 @@ derived from source, `.design-sync/config.json`, previews, direct contract cover production imports. It intentionally does not claim dark, forced-colours, 320px, print, remote publication, or product adoption without separate evidence. -### 0.1 Registered visual components (53 — local contract; remote status unverified) +### 0.1 Registered visual components (54 — local contract; remote status unverified) Every visual export has one source map entry, a source-derived public `*Props` contract (except the two zero-prop roots), a reference preview, and a direct static publication test. The generated @@ -37,11 +37,12 @@ table under **Generated maturity snapshot** is the canonical list and product-im _Remaining from the original eight in this document:_ `DocumentFrame` is built locally (`src/components/ui/document-frame.tsx`, shell-only in `DocumentViewer`) but is not yet among -the 53 design-sync registered visual exports — registration and full controls remain follow-up. +the 54 design-sync registered visual exports — registration and full controls remain follow-up. `OverlayRoot`, `SegmentedControl`, and the PR 6–8 components are built and represented by the -local publication contract. Registration proves a source/API/preview/test reference; it does -not prove root-shell mounting, remote design-project publication, or product adoption. +local publication contract. The generated snapshot records their current product-import counts; +registration still proves only a source/API/preview/test reference and does not prove remote +design-project publication or browser acceptance. _P1 reusable (specified in outline only):_ `Menu`/`Popover` · `KeyValue` · `AppliedFilters`/`FilterSheet` · `ResponsiveActionGroup` · `ScrollableStrip`/ @@ -60,32 +61,32 @@ print primitives (`PrintHeader`, `PrintFooter`, `CitationFootnote`, `PrintOnly`, ### 0.4 Open-defect ledger (existing components → closing PR) -| Component | Open defects (compressed) | Closes in | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -| Button | danger hover/active tokens, the 48px comment and `ref` forwarding (plus a `testId` passthrough — React 19 types give components no `data-*` index signature) are resolved; the client boundary remains | follow-on | -| AsyncButton | `type` applied after spread (default `button`; explicit `submit` preserved). Prefer `Button` busy API for new sites. | **done** (PR-A) | -| IconButton | disabled encoding uses `controlDisabled` (opacity retired in PR-A) | **done** (PR-A) | -| ToggleSwitch | operable branch requires `aria-label`; opacity disabled retired; knob now travels on `transform` (`translate-x-4`), tokenised duration, reduced-motion opt-out | **done** (motion) | -| Chip | final `appearance`/size API, removable label contract, tap target and full-value title | **done** | -| TextField/SearchField/Select | all three closed, and the row was stale when re-measured 2026-08-08: each folds onto `FormField`, which renders `hint` AND `error` unconditionally and merges `aria-describedby` caller → hint → error; each takes an external `id` and a `ref`. Landed in PR 13, not the PR 7 the row pointed at | **done** | -| Checkbox/RadioGroup | all four closed. Two rows were stale when re-measured on 2026-08-08: ids already sanitise through `optionId`, and the group already carries `hint`/`error`/`describedBy`. Raw dimensions moved to `size-5`/`h-0.5` (`size-4.5` retired by `check:icon-scale`) | **done** | -| Citation/CitationList | required interactive handler, stable-id list keys and static-span labelling (`aria-label` on a role-less span was dropped, losing the currency phrase on print) are resolved; route/source modes remain | follow-on | -| DoseLine | must compose `Quantity` · structured dose model · overdue text + non-colour mark + open action | **done** (PR 6) | -| StatusMark | app-type coupling resolved — `DocumentStatus` is declared here and `@/lib/types` conforms to it (asserted at compile time in the DOM suite), not the reverse; inline styles/raw geometry remain | PR 12 | -| AnswerCard | required verification/state, structured actions and five-state vocabulary landed | **done** (PR 6) | -| AnswerFooter | machine ISO values composed through `DateDisplay` + `MissingValue` landed | **done** (PR 6) | -| PageHeader/Breadcrumb | all resolved. The wrap alone was not enough — the actions track still took full max-content first, so the title column now has a `minmax(20ch,1fr)` floor and the actions track is the one that gives way | **done** | -| Tabs | both resolved. An invalid `value` no longer empties the tab order: the first enabled tab becomes reachable and owns the panel wiring. Reachability only — no `onChange` is fired to repair caller state | **done** | -| Pagination | all resolved. Props clamp (`page=0` no longer emits `onPageChange(-1)`), the control row wraps at 320px, and reaching a boundary hands focus to the current page and announces "Page N of M" through `LiveAnnouncer` | **done** | -| Links | `tone` leak, spread-overridable `download` (now type-omitted AND written after the spread) and the `gap` "animation" (`gap` is not in Tailwind's `transition` list, so it jumped; now a composited `translate-x`) are resolved; new-tab policy still implicit, and `LinkAction` silently ignores `tone` | PR 9 | -| Tooltip | composed handlers/description, string content, OverlayRoot portal, collision and delay contract | **done** (PR 10) | -| Toast | independent tone/priority/persistence, OverlayRoot portal, pause, dedupe and queue contract | **done** (PR 10) | -| Sheet/ConfirmDialog | required names/action labels, portal default, tokened layers/duration and wrapping titles | **done** (PR 10) | -| Disclosure | heading level and print resolved — a collapsed panel is `print:block`, so a section no longer prints as if the guideline never mentioned it; truncation remains. The old docstring's Ctrl-F/print claim was false | PR 11 | -| Progress/StageList | all four resolved: `scaleX`, the theme `animate-shimmer` in place of a hardcoded `1.4s`, a step index clamped to ≥1, and an sr-only `role="status"` sibling in place of `aria-live` on the whole `
    ` | **done** | -| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) | -| AccessibleTable | semantic caption, `MissingValue` cells, dense headers (clipped header keeps its full string as `title`) and the expander (now the registered `Button`, off the local ring-focus recipe) landed; content-role widths remain | PR 6/PR 12 remainder | -| ui-primitives.tsx | 698-line module mixing recipes/actions/feedback/clinical — split. Re-measured 2026-08-08: **157** production files import it (202 including mockups — the old "200 product files" was the mockup-inclusive figure mislabelled), against the 53 registered components' **30** adopted; this module, not the registry, is what the product actually runs on | PR 12 | +| Component | Open defects (compressed) | Closes in | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | +| Button | danger hover/active tokens, the 48px comment and `ref` forwarding (plus a `testId` passthrough — React 19 types give components no `data-*` index signature) are resolved; the client boundary remains | follow-on | +| AsyncButton | `type` applied after spread (default `button`; explicit `submit` preserved). Prefer `Button` busy API for new sites. | **done** (PR-A) | +| IconButton | disabled encoding uses `controlDisabled` (opacity retired in PR-A) | **done** (PR-A) | +| ToggleSwitch | operable branch requires `aria-label`; opacity disabled retired; knob now travels on `transform` (`translate-x-4`), tokenised duration, reduced-motion opt-out | **done** (motion) | +| Chip | final `appearance`/size API, removable label contract, tap target and full-value title | **done** | +| TextField/SearchField/Select | all three closed, and the row was stale when re-measured 2026-08-08: each folds onto `FormField`, which renders `hint` AND `error` unconditionally and merges `aria-describedby` caller → hint → error; each takes an external `id` and a `ref`. Landed in PR 13, not the PR 7 the row pointed at | **done** | +| Checkbox/RadioGroup | all four closed. Two rows were stale when re-measured on 2026-08-08: ids already sanitise through `optionId`, and the group already carries `hint`/`error`/`describedBy`. Raw dimensions moved to `size-5`/`h-0.5` (`size-4.5` retired by `check:icon-scale`) | **done** | +| Citation/CitationList | required interactive handler, stable-id list keys and static-span labelling (`aria-label` on a role-less span was dropped, losing the currency phrase on print) are resolved; route/source modes remain | follow-on | +| DoseLine | must compose `Quantity` · structured dose model · overdue text + non-colour mark + open action | **done** (PR 6) | +| StatusMark | app-type coupling resolved — `DocumentStatus` is declared here and `@/lib/types` conforms to it (asserted at compile time in the DOM suite), not the reverse; inline styles/raw geometry remain | PR 12 | +| AnswerCard | required verification/state, structured actions and five-state vocabulary landed | **done** (PR 6) | +| AnswerFooter | machine ISO values composed through `DateDisplay` + `MissingValue` landed | **done** (PR 6) | +| PageHeader/Breadcrumb | all resolved. The wrap alone was not enough — the actions track still took full max-content first, so the title column now has a `minmax(20ch,1fr)` floor and the actions track is the one that gives way | **done** | +| Tabs | both resolved. An invalid `value` no longer empties the tab order: the first enabled tab becomes reachable and owns the panel wiring. Reachability only — no `onChange` is fired to repair caller state | **done** | +| Pagination | all resolved. Props clamp (`page=0` no longer emits `onPageChange(-1)`), the control row wraps at 320px, and reaching a boundary hands focus to the current page and announces "Page N of M" through `LiveAnnouncer` | **done** | +| Links | `tone` leak, spread-overridable `download` (now type-omitted AND written after the spread) and the `gap` "animation" (`gap` is not in Tailwind's `transition` list, so it jumped; now a composited `translate-x`) are resolved; new-tab policy still implicit, and `LinkAction` silently ignores `tone` | PR 9 | +| Tooltip | composed handlers/description, string content, OverlayRoot portal, collision and delay contract | **done** (PR 10) | +| Toast | independent tone/priority/persistence, OverlayRoot portal, pause, dedupe and queue contract | **done** (PR 10) | +| Sheet/ConfirmDialog | required names/action labels, portal default, tokened layers/duration and wrapping titles | **done** (PR 10) | +| Disclosure | heading level and print resolved — a collapsed panel is `print:block`, so a section no longer prints as if the guideline never mentioned it; truncation remains. The old docstring's Ctrl-F/print claim was false | PR 11 | +| Progress/StageList | all four resolved: `scaleX`, the theme `animate-shimmer` in place of a hardcoded `1.4s`, a step index clamped to ≥1, and an sr-only `role="status"` sibling in place of `aria-live` on the whole `
      ` | **done** | +| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) | +| AccessibleTable | semantic caption, `MissingValue` cells, dense headers (clipped header keeps its full string as `title`) and the expander (now the registered `Button`, off the local ring-focus recipe) landed; content-role widths remain | PR 6/PR 12 remainder | +| ui-primitives.tsx | 698-line module mixing recipes/actions/feedback/clinical — split. Re-measured 2026-08-12: **157** production files import it (202 including mockups), against the 54 registered components' **31** with product imports; this module, not the registry, is what the product actually runs on | PR 12 | --- @@ -383,6 +384,13 @@ type ErrorSummaryProps = { **States.** default · focused · invalid (hint + error) · disabled (encoded via `controlBase`, not opacity) · required/optional. +**Optionality marker.** Required fields append the visible word `(required)` to the label and +set the native `required` contract on the control. Optional fields receive no suffix: `(optional)` +was deliberately removed from the product because optional is the default and repeating it made +dense clinical forms harder to scan. Do not restore an optional suffix at individual call sites; +the rule is text-first, never colour-only, and is pinned by +`tests/ui-v2-form-field.dom.test.tsx`. + **Keyboard & screen reader.** Label → control association by `htmlFor`/`id`; described-by order: caller, hint, error; Tab order unchanged by validity; summary-link activation moves focus into the field. @@ -969,7 +977,7 @@ This generated snapshot is a local source-derived inventory. It does not assert | `DisclosureGroup` | layout | yes | yes | inherited-global-root | yes | no | 1 | | `DoseLine` | answer | yes | yes | no | yes | no | 0 | | `DownloadLink` | controls | yes | yes | no | yes | no | 0 | -| `EmptyState` | feedback | yes | yes | inherited-global-root | yes | no | 12 | +| `EmptyState` | feedback | yes | yes | inherited-global-root | yes | no | 13 | | `ErrorState` | feedback | yes | yes | no | yes | no | 0 | | `ErrorSummary` | feedback | yes | yes | no | yes | no | 0 | | `ExternalTextLink` | controls | yes | yes | no | yes | no | 0 | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 0e8b0a3bd..23b3d5b8e 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -363,6 +363,7 @@ "tests/ckb-v2-token-contract.test.ts", "tests/design-sync-visual-exports.test.ts", "tests/design-system-contract-utils.test.ts", + "tests/design-system-target-evidence.test.ts", "tests/ui-v2-components.dom.test.tsx" ], "baseline": { @@ -650,7 +651,8 @@ "src/components/clinical-dashboard/medication-record-page.tsx", "src/components/clinical-dashboard/visual-evidence.tsx", "src/components/document-viewer/document-rail-panels.tsx", - "src/components/dsm/dsm-search-page.tsx" + "src/components/dsm/dsm-search-page.tsx", + "src/components/mode-home-template.tsx" ], "productImportFiles": [ "src/components/ClinicalDashboard.tsx", @@ -664,7 +666,8 @@ "src/components/clinical-dashboard/medication-record-page.tsx", "src/components/clinical-dashboard/visual-evidence.tsx", "src/components/document-viewer/document-rail-panels.tsx", - "src/components/dsm/dsm-search-page.tsx" + "src/components/dsm/dsm-search-page.tsx", + "src/components/mode-home-template.tsx" ], "designSync": { "listedInSourceMap": true, @@ -675,6 +678,7 @@ "testFiles": [ "tests/design-sync-contract.test.ts", "tests/design-sync-visual-exports.test.ts", + "tests/design-system-target-evidence.test.ts", "tests/dsm-search-empty-state.dom.test.tsx", "tests/therapy-compass-ui.dom.test.tsx", "tests/ui-primitives.dom.test.tsx" @@ -1005,7 +1009,7 @@ "preview": ".design-sync/previews/LoadingPanel.tsx", "previewValid": true }, - "testFiles": ["tests/design-sync-visual-exports.test.ts"], + "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/design-system-target-evidence.test.ts"], "baseline": { "targetLayer": "v2", "liveLayer": "v2", @@ -1077,6 +1081,8 @@ "testFiles": [ "tests/app-announcements-root.test.ts", "tests/design-sync-visual-exports.test.ts", + "tests/design-system-target-evidence.test.ts", + "tests/sheet.dom.test.tsx", "tests/ui-v2-components.dom.test.tsx" ], "baseline": { diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx index 55c821dce..613986d13 100644 --- a/src/components/clinical-dashboard/ClinicalSidebar.tsx +++ b/src/components/clinical-dashboard/ClinicalSidebar.tsx @@ -181,7 +181,6 @@ function MoreModesMenu({ description="Open a specialist mode home." closeLabel="Close more modes" returnFocusRef={triggerRef} - portal mobilePlacement="bottom" mobileSize="content" testId="sidebar-more-modes-sheet" diff --git a/src/components/clinical-dashboard/answer-result-surface.tsx b/src/components/clinical-dashboard/answer-result-surface.tsx index c485a3a8e..2723deee1 100644 --- a/src/components/clinical-dashboard/answer-result-surface.tsx +++ b/src/components/clinical-dashboard/answer-result-surface.tsx @@ -372,7 +372,6 @@ function StagedAnswerResultSurfaceImpl({ contentClassName="max-h-[88dvh] bg-[color:var(--surface-raised)] sm:max-h-[min(80dvh,36rem)] sm:max-w-md" bodyClassName="flex flex-col bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3" returnFocusRef={clinicalNotesTriggerRef} - portal > diff --git a/src/components/mode-home-template.tsx b/src/components/mode-home-template.tsx index 5d1165860..8a9e89cab 100644 --- a/src/components/mode-home-template.tsx +++ b/src/components/mode-home-template.tsx @@ -3,7 +3,7 @@ import { type ReactNode } from "react"; import { type LucideIcon, ArrowRight } from "lucide-react"; import { DesktopComposerPortalSlot } from "@/components/desktop-composer-portal-slot"; -import { cn, eyebrowText } from "@/components/ui-primitives"; +import { cn, EmptyState, eyebrowText } from "@/components/ui-primitives"; import { modeHomeComposerReservePendingValue } from "@/lib/mode-home-composer"; export type ModeHomeAction = { @@ -247,25 +247,27 @@ export function ModeHomeStatusNotice({ }) { const actionClass = "inline-flex min-h-tap items-center justify-center rounded-lg bg-[color:var(--command)] px-3 text-sm font-semibold text-[color:var(--command-contrast)] hover:bg-[color:var(--command-hover)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] lg:min-h-9"; + const action = + onAction && actionLabel ? ( + + ) : actionHref && actionLabel ? ( + + {actionLabel} + + ) : undefined; + return ( -
      - - - - {title} - {body} - - {onAction && actionLabel ? ( - - ) : actionHref && actionLabel ? ( - - {actionLabel} - - ) : null} -
      + ); } diff --git a/tests/design-system-target-evidence.test.ts b/tests/design-system-target-evidence.test.ts new file mode 100644 index 000000000..59cb0ee3a --- /dev/null +++ b/tests/design-system-target-evidence.test.ts @@ -0,0 +1,44 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +function source(path: string) { + return readFileSync(path, "utf8"); +} + +describe("design-system target adoption evidence", () => { + it("keeps local empty, loading, and chip APIs delegated to DS primitives", () => { + const therapy = source("src/components/therapy-compass/ui.tsx"); + const modeHome = source("src/components/mode-home-template.tsx"); + const differentials = source("src/components/clinical-dashboard/differentials-home.tsx"); + const favourites = source("src/components/clinical-dashboard/favourites-command-library-page.tsx"); + + expect(therapy).toContain('return '); + expect(therapy).toContain("'); + }); + + it("keeps settings, sidebar, and answer sheets on the OverlayRoot portal default", () => { + const sheet = source("src/components/ui/sheet.tsx"); + const owners = [ + source("src/components/clinical-dashboard/settings-dialog.tsx"), + source("src/components/clinical-dashboard/ClinicalSidebar.tsx"), + source("src/components/clinical-dashboard/answer-result-surface.tsx"), + ]; + + expect(sheet).toContain("portal = true"); + for (const owner of owners) expect(owner).not.toContain("portal={false}"); + }); + + it("keeps responsive CrossModeLinks declarations distinct and the generic rail phone-only", () => { + const links = source("src/components/clinical-dashboard/cross-mode-links.tsx"); + + expect(links).toContain('data-testid="cross-mode-links-rail"'); + expect(links).toContain('data-testid="cross-mode-links-card-rail"'); + expect(links).toMatch(/cross-mode-links-rail[^"\n]*md:hidden/); + expect(links).toMatch(/cross-mode-links-rail hidden[^"\n]*md:flex/); + expect(links).not.toContain('data-testid="cross-mode-links-rail-desktop"'); + }); +}); diff --git a/tests/sheet.dom.test.tsx b/tests/sheet.dom.test.tsx index 216e306fc..803d15ead 100644 --- a/tests/sheet.dom.test.tsx +++ b/tests/sheet.dom.test.tsx @@ -5,6 +5,7 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Sheet } from "@/components/ui/sheet"; +import { OverlayRoot } from "@/components/ui/overlay-root"; // jsdom (via vitest's environment) normally provides requestAnimationFrame, but // guard it so the Sheet's focus scheduling never throws if a runner omits it. @@ -25,6 +26,20 @@ afterEach(() => { } }); +it("portals into OverlayRoot's modal host by default", async () => { + render( + <> + + {}} title="Default portal sheet"> +

      Default portal body

      +
      + , + ); + + const body = await screen.findByText("Default portal body"); + expect(body.closest('[data-overlay-host="modal"]')).not.toBeNull(); +}); + function Stacked({ openA, openB,