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
56 changes: 56 additions & 0 deletions .design-sync/NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,59 @@ absence of both before calling it a defect.
NOT component source. A component-source or token change surfaces instead as
render churn (`canary`/`[SPOT_CHECK]`) plus `styling: true`. Grade the
spot-check sheets — the churn is real even though nothing is listed "changed".

## v2 design-system pass (2026-07-31)

Scope was explicitly **design system only, no site-wide changes**, so the app
surfaces (ClinicalDashboard, DocumentViewer, mode homes) were not touched.

What landed:

- **Browser crash (P1).** `source-metadata.ts` no longer imports the server
logger. It reached the browser through the source badges, and its unguarded
`process.env.LOG_LEVEL` read threw a ReferenceError there, so any
off-vocabulary metadata value unmounted the whole React tree instead of
falling back. The trace now goes through `sourceMetadataDiagnostics.warn`
(`console.warn`, spy-able the way `logger.warn` was), and `logger.ts` reads
the environment defensively as defence in depth. Regressions:
`tests/source-metadata-browser-safety.test.ts`,
`tests/source-badges-off-vocab.dom.test.tsx`.
The `ds-safety-shim.js` in the design project (`window.process = { env: {} }`)
can be deleted once the next bundle ships.
- **`.ckb-v2` token layer** — `src/app/ckb-v2-tokens.css`, imported from
`globals.css`. Values verbatim from the design project's `ckb-v2-tokens.css`,
but **everything is class-scoped**, including the structural half that the
source file puts on `:root`. That deviation is deliberate: on `:root` it would
repaint the live app, which was out of scope. Promoting the structural tokens
to `:root` is a separate change and needs its own visual-regression pass.
- **Twelve new components** (`src/components/ui/`): `Button`, `TextField`,
`SearchField`, `Chip`, `ToastProvider`/`ToastRegion`/`useToast`, `Tabs`,
`Tooltip`, `Pagination`, `ConfirmDialog`, `PageHeader`, `Breadcrumb`,
and the answer surface trio `AnswerCard` / `DoseLine` / `AnswerFooter`.
Components referencing v2-only tokens carry v1 fallbacks
(`var(--pad-panel,1.5rem)`) so they render with or without the class.
- **Four orphans documented** — `AsyncButton`, `IconButton`, `Skeleton`,
`SourceDesignationBadge` now have previews and config entries instead of
shipping undocumented.
- **`AccessibleTable`** — per-column alignment (`columnAlign` / `numericColumns`,
auto-detecting numeric columns by default), `aria-controls` on the expander,
sticky header in the expanded view, and a real warning treatment for an
unverified extraction instead of a muted grey line.
- **`Sheet`** gained an optional `id` so an opener can advertise `aria-controls`.
- **`EmptyState`** accepts `description` as a deprecated alias for `body`;
passing `PanelHeading`'s prop name used to render nothing, silently.

Component count went 10 → 28, so the next sync writes a much larger bundle.

### Still design-app-side, not fixable here

- The generated half of the published `README.md` lists a `tokens/*.css` folder
that this DS does not ship — and contradicts itself two lines later ("this DS
ships one compiled stylesheet rather than separate token files"). That text
comes from the converter, not from `conventions.md`, so it needs a fix in
`resync.mjs` / the design app, not in this repo.
- The stale `_ds_manifest.json` (236 tokens indexed vs 341 declared, `themes: []`,
`--tw-*` runtime vars published as _spacing_, 126 Tailwind utility classes read
as theme scopes) is likewise classifier-side. Re-running the sync regenerates
it from the compiled CSS; the misclassification rules themselves are not in
this repo.
47 changes: 44 additions & 3 deletions .design-sync/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,63 @@
"docs/redesign/permanent-colour-direction.md"
],
"dtsPropsFor": {
"Button": "variant?: \"primary\" | \"secondary\" | \"toolbar\" | \"ghost\" | \"danger\"; size?: \"sm\" | \"md\" | \"lg\"; children: React.ReactNode; icon?: LucideIcon; trailingIcon?: LucideIcon; block?: boolean; busy?: boolean; busyLabel?: string; disabled?: boolean; className?: string; type?: \"button\" | \"submit\" | \"reset\"; onClick?: () => void;",
"TextField": "label: string; hint?: React.ReactNode; error?: React.ReactNode; hideLabel?: boolean; icon?: LucideIcon; fieldClassName?: string; className?: string; placeholder?: string; value?: string; defaultValue?: string; disabled?: boolean; onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;",
"SearchField": "label: string; hint?: React.ReactNode; error?: React.ReactNode; hideLabel?: boolean; onClear?: () => void; clearLabel?: string; fieldClassName?: string; className?: string; placeholder?: string; value?: string; onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;",
"Chip": "children: React.ReactNode; tone?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\"; dot?: boolean; icon?: LucideIcon; onRemove?: () => void; removeLabel?: string; className?: string;",
"ToastRegion": "",
"Tabs": "items: Array<{ id: string; label: string; icon?: LucideIcon; count?: number; disabled?: boolean }>; value: string; onChange: (id: string) => void; label: string; variant?: \"tabs\" | \"segmented\"; className?: string; children?: React.ReactNode;",
"Tooltip": "children: React.ReactElement; content: React.ReactNode; placement?: \"top\" | \"bottom\"; className?: string;",
"Pagination": "page: number; pageCount: number; onPageChange: (page: number) => void; label?: string; summary?: string; className?: string;",
"ConfirmDialog": "open: boolean; onCancel: () => void; onConfirm: () => void; title: string; description: React.ReactNode; confirmLabel?: string; cancelLabel?: string; tone?: \"danger\" | \"primary\"; busy?: boolean; busyLabel?: string; confirmPhrase?: string; confirmPhraseLabel?: React.ReactNode;",
"PageHeader": "title: string; eyebrow?: string; description?: React.ReactNode; icon?: LucideIcon; breadcrumb?: Array<{ label: string; href?: string }>; actions?: React.ReactNode; meta?: React.ReactNode; className?: string;",
"Breadcrumb": "items: Array<{ label: string; href?: string }>; className?: string;",
"AnswerCard": "children: React.ReactNode; header?: React.ReactNode; footer?: React.ReactNode; className?: string;",
"DoseLine": "rows: Array<{ drug: string; qualifier?: string; value: string; unit?: string; overdue?: boolean }>; caption?: string; className?: string;",
"AnswerFooter": "publisher?: string | null; version?: string | null; reviewDate?: string | null; generatedAt?: string | null; className?: string;",
"InlineNotice": "tone: \"success\" | \"info\" | \"warning\" | \"danger\" | \"neutral\"; children?: React.ReactNode; onDismiss?: () => void; dismissLabel?: string; animated?: boolean; className?: string;",
"ToggleSwitch": "enabled: boolean; onToggle?: () => void; disabled?: boolean; className?: string; \"aria-label\"?: string;",
"AsyncButton": "busy: boolean; busyLabel: string; children: React.ReactNode; idleIcon?: React.ReactNode; disabled?: boolean; className?: string; type?: \"button\" | \"submit\" | \"reset\"; onClick?: () => void;",
"IconButton": "label: string; icon: LucideIcon; className?: string; iconClassName?: string; disabled?: boolean; type?: \"button\" | \"submit\" | \"reset\"; onClick?: () => void;",
"Skeleton": "className?: string; animationDelay?: string;",
"SourceStatusBadge": "metadata?: { document_status?: \"current\" | \"review_due\" | \"outdated\" | \"unknown\" }; className?: string; showTitle?: boolean;",
"SourceDesignationBadge": "metadata?: { publisher?: string; publisher_code?: string; jurisdiction?: string; source_kind?: \"document\" | \"registry_record\" }; className?: string;",
"SourceProvenance": "metadata?: { clinical_validation_status?: \"unverified\" | \"locally_reviewed\" | \"approved\"; review_date?: string; jurisdiction?: string; extraction_quality?: \"good\" | \"partial\" | \"poor\" | \"unknown\" };",
"PanelHeading": "icon?: React.ComponentType<{ className?: string }>; title: string; description?: string;",
"LoadingPanel": "label: string; variant?: \"spinner\" | \"skeleton\"; lines?: number;",
"EmptyState": "icon?: React.ComponentType<{ className?: string }>; title: string; body: string;",
"EmptyState": "icon?: React.ComponentType<{ className?: string }>; title: string; body?: string; /** @deprecated alias for body */ description?: string; actions?: React.ReactNode; live?: \"polite\" | \"assertive\"; tone?: \"neutral\" | \"info\" | \"danger\";",
"Sheet": "open: boolean; onClose: () => void; title?: string; description?: string; children?: React.ReactNode; footer?: React.ReactNode; closeLabel?: string; headerActions?: React.ReactNode; placement?: \"default\" | \"left\"; mobilePlacement?: \"bottom\" | \"top\" | \"fullscreen\"; mobileSize?: \"content\" | \"viewport\"; portal?: boolean; contentClassName?: string; bodyClassName?: string;",
"SafeBoldText": "text: string;",
"AccessibleTable": "caption?: string | null; markdown?: string | null; rows?: string[][] | null; columns?: string[] | null; compact?: boolean; expandOnMobile?: boolean; previewRows?: number; hidePreviewCaption?: boolean; hidePreviewRowCount?: boolean; densePreview?: boolean; dialogTitle?: string | null; clinicalOnly?: boolean; rowActions?: Array<React.ReactNode | null>; actionsHeader?: string; lowConfidenceFallback?: React.ReactNode;"
"AccessibleTable": "caption?: string | null; markdown?: string | null; rows?: string[][] | null; columns?: string[] | null; compact?: boolean; expandOnMobile?: boolean; previewRows?: number; hidePreviewCaption?: boolean; hidePreviewRowCount?: boolean; densePreview?: boolean; dialogTitle?: string | null; clinicalOnly?: boolean; rowActions?: Array<React.ReactNode | null>; actionsHeader?: string; lowConfidenceFallback?: React.ReactNode; columnAlign?: Array<\"start\" | \"end\" | \"auto\">; numericColumns?: number[];"
},
"overrides": {
"Sheet": { "cardMode": "single", "primaryStory": "OpenDialog", "viewport": "480x640" },
"AccessibleTable": { "cardMode": "column" }
"AccessibleTable": { "cardMode": "column" },
"ConfirmDialog": { "cardMode": "single", "primaryStory": "Destructive", "viewport": "480x640" },
"ToastRegion": { "cardMode": "single", "primaryStory": "Interactive", "viewport": "480x360" },
"AnswerCard": { "cardMode": "column" },
"DoseLine": { "cardMode": "column" },
"PageHeader": { "cardMode": "column" }
},
"componentSrcMap": {
"Button": "src/components/ui/button.tsx",
"TextField": "src/components/ui/text-field.tsx",
"SearchField": "src/components/ui/text-field.tsx",
"Chip": "src/components/ui/chip.tsx",
"ToastRegion": "src/components/ui/toast.tsx",
"Tabs": "src/components/ui/tabs.tsx",
"Tooltip": "src/components/ui/tooltip.tsx",
"Pagination": "src/components/ui/pagination.tsx",
"ConfirmDialog": "src/components/ui/confirm-dialog.tsx",
"PageHeader": "src/components/ui/page-header.tsx",
"Breadcrumb": "src/components/ui/page-header.tsx",
"AnswerCard": "src/components/ui/answer-card.tsx",
"DoseLine": "src/components/ui/answer-card.tsx",
"AnswerFooter": "src/components/ui/answer-card.tsx",
"AsyncButton": "src/components/ui-primitives.tsx",
"IconButton": "src/components/ui-primitives.tsx",
"Skeleton": "src/components/ui-primitives.tsx",
"SourceDesignationBadge": "src/components/ui-primitives.tsx",
"InlineNotice": "src/components/ui-primitives.tsx",
"ToggleSwitch": "src/components/ui-primitives.tsx",
"SourceStatusBadge": "src/components/ui-primitives.tsx",
Expand Down
39 changes: 38 additions & 1 deletion .design-sync/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,40 @@ arbitrary-value form — never hardcoded colours:
focus owner by design; a second ring both stacks a halo and wipes the control's resting elevation.

Radius rules: `rounded-md` chips/pills, `rounded-lg` controls/cards/panels,
`rounded-xl` sheets/dialogs. Tap targets: `min-h-tap` / `h-tap w-tap` (44px).
`rounded-xl` sheets/dialogs. Tap targets: `min-h-tap` / `h-tap w-tap` (44px) —
interactive controls only. A static chip is 28px text, not a touch target;
putting `min-h-tap` on one is why dense tables scrolled so much.
Dark mode is automatic via the `.dark` class — the variables flip; never write
`dark:` colour overrides yourself.

## Colour boundaries

Three layers, and they do not borrow from each other:

1. **Clinical state** — `success` / `warning` / `danger`. Reserved for currency,
validation and safety. Never decorative. Amber and red appear only in
`SourceStatusBadge`, `InlineNotice`, `ConfirmDialog`, the extraction-quality
row, and a `DoseLine` row whose cited source is overdue.
2. **Information** — `info` plus the accent. Neutral emphasis, not a verdict.
3. **Identity** — the muted `--type-*` hues that tell record kinds apart.

`--command` is the one filled action colour, and a surface carries at most one
filled `--command` button. `--danger-solid` has exactly one home: the `danger`
variant of `Button`, i.e. a destructive confirmation. Importance is `primary`.

`--text-soft` is around 3.2:1 on white — decoration only (dots, dividers,
glyphs). Label and caption **text** uses `--text-muted`.

## Opt-in v2 token layer

`.ckb-v2` is an opt-in class that swaps in the v2 shell: white surfaces, a blue
`--command`, a crisper `--e1`…`--e4` ladder, a 7-step type scale with per-step
line-height and tracking, semantic spacing (`--gap-*`, `--pad-*`), density
(`--tap-min`, `--chip-height`, `--row-*`), icon sizes (`--icon-*`) and motion
durations. Add `ckb-v2` (plus `dark` for the dark ramp) to a subtree to adopt it;
without the class nothing changes. Components that reference v2-only tokens carry
a v1 fallback (`var(--pad-panel,1.5rem)`) so they render correctly either way.

## Class-string vocabulary (exported constants)

The bundle exports ready-made class strings — compose them instead of
Expand All @@ -52,6 +82,13 @@ chat/search composer and tone recipes documented in
`docs/redesign/09-ui-primitives-recipes.md`. Join with the exported `cn(...)`
helper.

Prefer a component over a recipe where one now exists. `Button` supersedes
hand-composing `primaryControl` / `floatingControl` / `toolbarButton` on a raw
`<button>`; `TextField` / `SearchField` supersede hand-wiring `fieldLabel` +
`fieldControl*` + `aria-describedby`; `Chip` supersedes `metadataPill` /
`subtleStatusPill` for filter and status chips; `AnswerCard` supersedes
`answerSurface`, which was only `rounded-lg bg-transparent`.

Module-private helpers (`insetCard`, `iconTile`, `compactMetadataRow`,
`toneWarningQuiet`, `statusDotBase`, `chatComposerShellDelta`)
power components inside `ui-primitives.tsx` and are **not** part of the import
Expand Down
15 changes: 15 additions & 0 deletions .design-sync/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ export { Sheet } from "@/components/ui/sheet";
export { SafeBoldText } from "@/components/SafeBoldText";
export { AccessibleTable } from "@/components/AccessibleTable";

// Components added in the v2 pass. They close the gaps the design review found:
// nothing in the system owned actions, text entry, filter chips, async outcome
// announcements, in-page section switching, supplementary help text, long-list
// paging, destructive confirmation, or page-level titling.
export { Button } from "@/components/ui/button";
export { TextField, SearchField } from "@/components/ui/text-field";
export { Chip } from "@/components/ui/chip";
export { ToastProvider, ToastRegion, useToast } from "@/components/ui/toast";
export { Tabs } from "@/components/ui/tabs";
export { Tooltip } from "@/components/ui/tooltip";
export { Pagination } from "@/components/ui/pagination";
export { ConfirmDialog } from "@/components/ui/confirm-dialog";
export { PageHeader, Breadcrumb } from "@/components/ui/page-header";
export { AnswerCard, AnswerFooter, DoseLine } from "@/components/ui/answer-card";

// Curated lucide-react icon set: consumers of the synced bundle have no
// lucide-react install, so the icons PanelHeading/EmptyState-style `icon`
// props need must ship with the bundle itself.
Expand Down
43 changes: 43 additions & 0 deletions .design-sync/previews/AnswerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AnswerCard, AnswerFooter, DoseLine, Chip } from "prompt-for-codex-medical-knowledge-base";

export const WithFooter = () => (
<div className="w-[40rem]">
<AnswerCard
header={
<div className="flex flex-wrap items-center gap-1.5">
<Chip tone="success">Current</Chip>
<Chip tone="info">WA</Chip>
</div>
}
footer={
<AnswerFooter
publisher="Sir Charles Gairdner Hospital"
version="4.2"
reviewDate="18/05/2026"
generatedAt="31/07/2026 13:04"
/>
}
>
<p>
Start clozapine at a low dose and titrate against tolerability, with haematological monitoring weekly for the
first 18 weeks.
</p>
</AnswerCard>
</div>
);

export const PlainAnswer = () => (
<div className="w-[40rem]">
<AnswerCard>
<p>No local guideline covers this question. The linked sources are the closest available.</p>
</AnswerCard>
</div>
);

export const MissingReviewDate = () => (
<div className="w-[40rem]">
<AnswerCard footer={<AnswerFooter publisher="RANZCP" generatedAt="31/07/2026 13:04" />}>
<p>Review status is unknown for this source, so the footer says so rather than staying blank.</p>
</AnswerCard>
</div>
);
20 changes: 20 additions & 0 deletions .design-sync/previews/AnswerFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AnswerFooter } from "prompt-for-codex-medical-knowledge-base";

export const Complete = () => (
<div className="w-[40rem] overflow-hidden rounded-lg border border-[color:var(--border)]">
<AnswerFooter
publisher="Sir Charles Gairdner Hospital"
version="4.2"
reviewDate="18/05/2026"
generatedAt="31/07/2026 13:04"
/>
</div>
);

// Missing segments are dropped rather than filled with "Unknown" - except the
// review date, which stays explicit because its absence is itself a signal.
export const SparseMetadata = () => (
<div className="w-[40rem] overflow-hidden rounded-lg border border-[color:var(--border)]">
<AnswerFooter publisher="RANZCP" generatedAt="31/07/2026 13:04" />
</div>
);
28 changes: 28 additions & 0 deletions .design-sync/previews/AsyncButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AsyncButton, primaryControl, controlBase } from "prompt-for-codex-medical-knowledge-base";
import { Upload } from "lucide-react";

// AsyncButton is colour-neutral by design: pass a control recipe via className so
// the busy contract (one label, spinner, disabled, aria-busy) can ride on any chrome.
export const Idle = () => (
<AsyncButton busy={false} busyLabel="Uploading…" idleIcon={<Upload className="h-4 w-4" />} className={primaryControl}>
Upload document
</AsyncButton>
);

export const Busy = () => (
<AsyncButton busy busyLabel="Uploading…" idleIcon={<Upload className="h-4 w-4" />} className={primaryControl}>
Upload document
</AsyncButton>
);

export const Secondary = () => (
<AsyncButton busy={false} busyLabel="Reindexing…" className={`${controlBase} px-5`}>
Reindex source
</AsyncButton>
);

export const Disabled = () => (
<AsyncButton busy={false} busyLabel="Uploading…" disabled className={primaryControl}>
Upload document
</AsyncButton>
);
15 changes: 15 additions & 0 deletions .design-sync/previews/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Breadcrumb } from "prompt-for-codex-medical-knowledge-base";

export const ThreeLevels = () => (
<Breadcrumb
items={[
{ label: "Documents", href: "/documents" },
{ label: "Protocols", href: "/documents" },
{ label: "Clozapine monitoring" },
]}
/>
);

export const TwoLevels = () => (
<Breadcrumb items={[{ label: "Services", href: "/services" }, { label: "Community mental health" }]} />
);
Loading
Loading