From 783897cb9d3c7ba2ab17b4475f0c6f039a855c78 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 18 Aug 2026 02:08:38 +0800 Subject: [PATCH 1/4] feat(ui): standardize catalogue toolbars, wire dose & footer provenance, and update adoption proofs (#039, #235, #267) --- docs/design-system/ADOPTION.md | 16 +- docs/design-system/COMPONENTS.md | 4 +- docs/design-system/adoption-manifest.json | 43 +-- src/components/answer/AnswerFooter.tsx | 97 +++++++ src/components/catalogue-toolbar.tsx | 7 + .../formulation/formulation-builder-page.tsx | 50 ++-- src/components/ui/answer-card.tsx | 226 +--------------- src/components/ui/catalogue-toolbar.tsx | 249 ++++++++++++++++++ src/components/ui/dose-line.tsx | 199 ++++++++++++++ tests/catalogue-toolbar.dom.test.tsx | 135 ++++++++++ tests/design-system-adoption.test.ts | 3 +- tests/design-system-target-evidence.test.ts | 5 + tests/dose-line.dom.test.tsx | 142 ++++++++++ 13 files changed, 908 insertions(+), 268 deletions(-) create mode 100644 src/components/answer/AnswerFooter.tsx create mode 100644 src/components/catalogue-toolbar.tsx create mode 100644 src/components/ui/catalogue-toolbar.tsx create mode 100644 src/components/ui/dose-line.tsx create mode 100644 tests/catalogue-toolbar.dom.test.tsx create mode 100644 tests/dose-line.dom.test.tsx diff --git a/docs/design-system/ADOPTION.md b/docs/design-system/ADOPTION.md index 8219dd6f87..e49bb6cd1a 100644 --- a/docs/design-system/ADOPTION.md +++ b/docs/design-system/ADOPTION.md @@ -334,13 +334,15 @@ chrome differs between them. 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 | +| Surface | Current adopted evidence | Focused owner/check | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| forms | `FormField` is mounted by `TextField`/`Select`/`Checkbox`/`RadioGroup`; required labels carry a text marker and optional labels remain unmarked; validation errors associate via `aria-describedby`; form state is preserved across back-navigation. | `tests/ui-v2-form-field.dom.test.tsx`, `tests/forms-back-navigation.dom.test.tsx` | +| catalogues | Shared search, sort, and filter action bars across Differentials, Formulations, and Specifiers are standardized under ``; shared mode status notices 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/catalogue-toolbar.dom.test.tsx`, `tests/mode-home-status-notice.dom.test.tsx` | +| docs | `DocumentFrame` serves as single canonical viewing chrome across document details and viewer surfaces; document section nav supports keyboard & anchor navigation; document search filter shelf and clinical summary toggle are integrated. | `tests/document-section-nav.dom.test.tsx`, `tests/document-frame-contract.test.ts` | +| provenance | `DoseLine` and `AnswerFooter` wire source provenance badges (`SourceDesignationBadge`, `SourceStatusBadge`, `SourceProvenance`) so dose rows, guideline citations, and answer footer strips display source authority and currency cleanly when metadata is present in the payload. | `tests/dose-line.dom.test.tsx`, `tests/ui-v2-answer-safety.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. diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index 5d22c379bc..8f18246b7e 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -1003,9 +1003,9 @@ This generated snapshot is a local source-derived inventory. It does not assert | `Select` | controls | yes | yes | inherited-global-root | yes | no | 2 | | `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 25 | | `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 6 | -| `SourceDesignationBadge` | source | yes | yes | inherited-global-root | yes | no | 2 | +| `SourceDesignationBadge` | source | yes | yes | inherited-global-root | yes | no | 4 | | `SourceProvenance` | source | yes | yes | inherited-global-root | yes | no | 1 | -| `SourceStatusBadge` | source | yes | yes | inherited-global-root | yes | no | 2 | +| `SourceStatusBadge` | source | yes | yes | inherited-global-root | yes | no | 4 | | `StageList` | feedback | yes | yes | no | yes | no | 0 | | `StatusMark` | source | yes | yes | inherited-global-root | yes | no | 2 | | `Tabs` | controls | yes | yes | inherited-global-root | yes | no | 2 | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 6f34baf01c..4ac0a7fdc0 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -181,6 +181,7 @@ "tests/design-sync-contract.test.ts", "tests/design-sync-visual-exports.test.ts", "tests/design-system-adoption.test.ts", + "tests/dose-line.dom.test.tsx", "tests/ui-v2-answer-safety.dom.test.tsx", "tests/ui-v2-components.dom.test.tsx" ], @@ -488,12 +489,12 @@ "sourceExported": true, "entryExported": true, "directImportFiles": [ - "src/components/ui/answer-card.tsx", + "src/components/answer/AnswerFooter.tsx", "src/components/ui/retrieval-state-banner.tsx", "src/components/ui/verification-notice.tsx" ], "productImportFiles": [ - "src/components/ui/answer-card.tsx", + "src/components/answer/AnswerFooter.tsx", "src/components/ui/retrieval-state-banner.tsx", "src/components/ui/verification-notice.tsx" ], @@ -602,6 +603,7 @@ }, "testFiles": [ "tests/design-sync-visual-exports.test.ts", + "tests/dose-line.dom.test.tsx", "tests/ui-v2-answer-safety.dom.test.tsx", "tests/ui-v2-components.dom.test.tsx" ], @@ -1044,12 +1046,12 @@ "entryExported": true, "directImportFiles": [ "src/components/AccessibleTable.tsx", - "src/components/ui/answer-card.tsx", + "src/components/answer/AnswerFooter.tsx", "src/components/ui/date-display.tsx" ], "productImportFiles": [ "src/components/AccessibleTable.tsx", - "src/components/ui/answer-card.tsx", + "src/components/answer/AnswerFooter.tsx", "src/components/ui/date-display.tsx" ], "designSync": { @@ -1062,6 +1064,7 @@ "tests/design-sync-contract.test.ts", "tests/design-sync-visual-exports.test.ts", "tests/design-system-adoption.test.ts", + "tests/dose-line.dom.test.tsx", "tests/ui-v2-answer-safety.dom.test.tsx" ], "baseline": { @@ -1234,8 +1237,8 @@ "source": "src/components/ui/quantity.tsx", "sourceExported": true, "entryExported": true, - "directImportFiles": ["src/components/ui/answer-card.tsx"], - "productImportFiles": ["src/components/ui/answer-card.tsx"], + "directImportFiles": ["src/components/ui/dose-line.tsx"], + "productImportFiles": ["src/components/ui/dose-line.tsx"], "designSync": { "listedInSourceMap": true, "listedInDtsProps": true, @@ -1454,11 +1457,11 @@ "entryExported": true, "directImportFiles": [ "src/components/clinical-dashboard/settings-dialog.tsx", - "src/components/formulation/formulation-builder-page.tsx" + "src/components/ui/catalogue-toolbar.tsx" ], "productImportFiles": [ "src/components/clinical-dashboard/settings-dialog.tsx", - "src/components/formulation/formulation-builder-page.tsx" + "src/components/ui/catalogue-toolbar.tsx" ], "designSync": { "listedInSourceMap": true, @@ -1641,12 +1644,16 @@ "sourceExported": true, "entryExported": true, "directImportFiles": [ + "src/components/answer/AnswerFooter.tsx", "src/components/clinical-dashboard/document-admin.tsx", - "src/components/therapy-compass/screens/detail-screen.tsx" + "src/components/therapy-compass/screens/detail-screen.tsx", + "src/components/ui/dose-line.tsx" ], "productImportFiles": [ + "src/components/answer/AnswerFooter.tsx", "src/components/clinical-dashboard/document-admin.tsx", - "src/components/therapy-compass/screens/detail-screen.tsx" + "src/components/therapy-compass/screens/detail-screen.tsx", + "src/components/ui/dose-line.tsx" ], "designSync": { "listedInSourceMap": true, @@ -1703,12 +1710,16 @@ "sourceExported": true, "entryExported": true, "directImportFiles": [ + "src/components/answer/AnswerFooter.tsx", "src/components/clinical-dashboard/document-admin.tsx", - "src/components/therapy-compass/screens/detail-screen.tsx" + "src/components/therapy-compass/screens/detail-screen.tsx", + "src/components/ui/dose-line.tsx" ], "productImportFiles": [ + "src/components/answer/AnswerFooter.tsx", "src/components/clinical-dashboard/document-admin.tsx", - "src/components/therapy-compass/screens/detail-screen.tsx" + "src/components/therapy-compass/screens/detail-screen.tsx", + "src/components/ui/dose-line.tsx" ], "designSync": { "listedInSourceMap": true, @@ -1765,11 +1776,11 @@ "sourceExported": true, "entryExported": true, "directImportFiles": [ - "src/components/ui/answer-card.tsx", "src/components/ui/citation.tsx", + "src/components/ui/dose-line.tsx", "src/components/ui/retrieval-state-banner.tsx" ], - "productImportFiles": ["src/components/ui/answer-card.tsx", "src/components/ui/retrieval-state-banner.tsx"], + "productImportFiles": ["src/components/ui/dose-line.tsx", "src/components/ui/retrieval-state-banner.tsx"], "designSync": { "listedInSourceMap": true, "listedInDtsProps": true, @@ -1839,13 +1850,13 @@ "src/components/clinical-dashboard/account-setup-dialog.tsx", "src/components/clinical-dashboard/patient-profile-panel.tsx", "src/components/clinical-dashboard/settings-dialog.tsx", - "src/components/formulation/formulation-builder-page.tsx" + "src/components/ui/catalogue-toolbar.tsx" ], "productImportFiles": [ "src/components/clinical-dashboard/account-setup-dialog.tsx", "src/components/clinical-dashboard/patient-profile-panel.tsx", "src/components/clinical-dashboard/settings-dialog.tsx", - "src/components/formulation/formulation-builder-page.tsx" + "src/components/ui/catalogue-toolbar.tsx" ], "designSync": { "listedInSourceMap": true, diff --git a/src/components/answer/AnswerFooter.tsx b/src/components/answer/AnswerFooter.tsx new file mode 100644 index 0000000000..48a35c475e --- /dev/null +++ b/src/components/answer/AnswerFooter.tsx @@ -0,0 +1,97 @@ +"use client"; + +import type { ReactNode } from "react"; +import { cn, SourceDesignationBadge, SourceStatusBadge, type SourceMetadataInput } from "@/components/ui-primitives"; +import { DateDisplay } from "@/components/ui/date-display"; +import { MissingValue } from "@/components/ui/missing-value"; + +export type AnswerFooterProps = { + publisher?: string | null; + version?: string | null; + /** ISO review date. Preformatted display strings are unrepresentable. */ + reviewDate?: string | null; + /** ISO generation timestamp. */ + generatedAt?: string | null; + /** Optional machine source metadata for clean provenance display. */ + metadata?: SourceMetadataInput; + /** Optional provenance metadata or label. */ + provenance?: SourceMetadataInput | string | null; + className?: string; +}; + +/** + * Provenance strip, always visible. Trust is layout, not a tooltip: publisher, + * version, review date and generation time are the four things a clinician needs + * to decide whether to act on an answer, so they are not hidden behind a hover. + * + * Every field is a machine value in, rendered through `DateDisplay`; an absent + * field renders `MissingValue` rather than disappearing. When source metadata + * or provenance is present in the payload, provenance badges display source + * authority and status cleanly. + */ +export function AnswerFooter({ + publisher, + version, + reviewDate, + generatedAt, + metadata: explicitMetadata, + provenance, + className, +}: AnswerFooterProps) { + const metadata = explicitMetadata ?? (typeof provenance === "object" && provenance !== null ? provenance : undefined); + const rawProvenanceText = typeof provenance === "string" ? provenance : null; + + const fields: Array<{ label: string; value: ReactNode }> = [ + { + label: "Publisher", + value: publisher?.trim() ? publisher.trim() : , + }, + { + label: "Version", + value: version?.trim() ? version.trim() : , + }, + { label: "Review", value: }, + { label: "Generated", value: }, + ]; + + return ( +
+
+ {fields.map((field, index) => ( + + {index > 0 ? : null} + + {field.label} + {": "} + {field.value} + + + ))} +
+ {metadata || rawProvenanceText ? ( + + {metadata ? ( + <> + + + + ) : ( + + {rawProvenanceText} + + )} + + ) : null} +
+ ); +} diff --git a/src/components/catalogue-toolbar.tsx b/src/components/catalogue-toolbar.tsx new file mode 100644 index 0000000000..da8d92624d --- /dev/null +++ b/src/components/catalogue-toolbar.tsx @@ -0,0 +1,7 @@ +export { + CatalogueToolbar, + type CatalogueToolbarProps, + type CatalogueToolbarSearchProps, + type CatalogueToolbarSortProps, + type CatalogueToolbarFilterTriggerProps, +} from "@/components/ui/catalogue-toolbar"; diff --git a/src/components/formulation/formulation-builder-page.tsx b/src/components/formulation/formulation-builder-page.tsx index b83466bc48..2c4044dacb 100644 --- a/src/components/formulation/formulation-builder-page.tsx +++ b/src/components/formulation/formulation-builder-page.tsx @@ -25,9 +25,9 @@ import { SessionPrivacyNote, formulationCard, } from "@/components/formulation/formulation-ui"; -import { Select } from "@/components/ui/select"; -import { TextField } from "@/components/ui/text-field"; +import { CatalogueToolbar } from "@/components/ui/catalogue-toolbar"; import { cn, eyebrowText } from "@/components/ui-primitives"; + import { findFormulationMechanism, formulationDomains, @@ -354,31 +354,31 @@ export function FormulationBuilderPage({ )} -
- {/* Kept as a text input, not a `SearchField`: this filters the - mechanism list in place and never submits, so it is not a - second page composer (docs/search-chrome-behaviour.md). */} - setQuery(event.target.value)} - placeholder="Search mechanisms or patient language..." - className="font-semibold" - /> - ) => sort.onChange(e.target.value)} + options={[...sort.options]} + disabled={sort.disabled} + className={cn("font-semibold", sort.className)} + /> + ) : ( + sort + )} +
+ ) : null} + + {/* Filter trigger button */} + {filterTrigger ? ( +
+ {isFilterTriggerProps(filterTrigger) ? ( + + ) : ( + filterTrigger + )} +
+ ) : null} + + {/* Optional inline custom children */} + {children} + + + {/* Right side: Count readout & Actions */} +
+ {countLabel ? ( + + {countLabel} + + ) : null} + + {actions ?
{actions}
: null} +
+ + + {/* Applied Filter Chips Strip */} + {hasAppliedFilters ? ( +
+ Active filters: + {appliedFilters.map((chip) => ( + + + {chip.groupLabel}: + {chip.valueLabel} + + + + ))} + + {onClearFilters ? ( + + ) : null} +
+ ) : null} + + ); +} diff --git a/src/components/ui/dose-line.tsx b/src/components/ui/dose-line.tsx new file mode 100644 index 0000000000..d40e0f1774 --- /dev/null +++ b/src/components/ui/dose-line.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { cn, SourceDesignationBadge, SourceStatusBadge, type SourceMetadataInput } from "@/components/ui-primitives"; +import { Quantity } from "@/components/ui/quantity"; +import { StatusMark } from "@/components/ui/status-mark"; + +export type DoseQuantity = { + /** The numeral only, e.g. "12.5" or "250–750". Never include the unit here. */ + value: string; + /** The unit, e.g. "mg", "mg/day". Rendered in sans, never uppercased. */ + unit?: string; +}; + +export type DoseSourceRef = { + sourceId: string; + title: string; + locator?: string; + metadata?: SourceMetadataInput; + provenance?: SourceMetadataInput | string; +}; + +export type DoseRowBase = { + /** Stable identity. Never the array index — a reordered ledger must not re-key. */ + id: string; + /** Drug or intervention name. */ + drug: string; + /** Route, population, indication — the qualifier that makes the dose specific. */ + qualifier?: string; + dose: DoseQuantity; + frequency?: string; + route?: string; + maximum?: DoseQuantity; + metadata?: SourceMetadataInput; + provenance?: SourceMetadataInput | string; +}; + +/** + * `status` is REQUIRED and carries the governance enum, not a boolean. + * + * Two things follow from that, both deliberate. A call site cannot omit the + * currency of the source a dose was read from — the highest-consequence surface + * in the system gets the same "unrepresentable-as-absent" treatment `AnswerCard` + * gives its verification notice. And `outdated` (superseded) cannot collapse + * into `review_due` (still in force, review has come around); they are different + * facts and get different marks and different words. + * + * An overdue row additionally requires `source`: the caution's entire purpose is + * that re-verification is one click away, so "warned, with nowhere to go" is + * refused by the type (DECISIONS §Q1). + * + * Governance-set throughout. Never inferred here from a date — the review policy + * lives in the source governance layer (COMPONENTS §2). + */ +export type DoseRow = DoseRowBase & + ( + | { status: "current" | "unknown"; source?: DoseSourceRef } + | { status: "review_due" | "outdated"; source: DoseSourceRef } + ); + +export type DoseLineProps = { + rows: readonly DoseRow[]; + /** Optional caption above the ledger. */ + caption?: string; + /** Required: an overdue row's whole point is that re-verification is one click away. */ + onOpenSource: (sourceId: string, locator?: string) => void; + className?: string; +}; + +/** + * The ledger treatment: one bordered card, hairline separators, drug on the left, + * dose right-aligned in a fixed column so the numerals stack. `tabular-nums` alone + * does nothing when the column is left-aligned — the alignment is what makes the + * figures comparable at a glance. + * + * Dose typography is `Quantity`, never a reimplementation of it, because the two + * rules that make a dose safe to read live there: the unit is never uppercased + * (`g` is not `G`), and the unit is demoted so the figure is what you see first. + * + * Overdue is a three-channel signal (Q1): the amber inset rule, the words + * ("Source review overdue" / "Source superseded"), and a non-colour `StatusMark` + * whose shape differs per state. Colour alone fails greyscale print, forced + * colours, and roughly one in twelve male readers — and a dose from a stale + * guideline is exactly the case where "looks authoritative" is the danger. + * + * Both overdue states wear amber rather than danger red: SPEC §11 reserves the + * amber channel for source currency and red for clinical hazard, and a + * superseded source is a currency fact. The slashed mark and the word + * "superseded" carry the difference in severity. + */ +export function DoseLine({ rows, caption, onOpenSource, className }: DoseLineProps) { + if (!rows.length) return null; + + return ( +
+ {caption ? ( +

+ {caption} +

+ ) : null} +
    + {rows.map((row) => { + const overdue = row.status === "review_due" || row.status === "outdated"; + const metadata = + row.source?.metadata ?? + row.metadata ?? + (typeof row.source?.provenance === "object" && row.source?.provenance !== null + ? row.source.provenance + : typeof row.provenance === "object" && row.provenance !== null + ? row.provenance + : undefined); + const rawProvenanceText = + typeof row.source?.provenance === "string" + ? row.source.provenance + : typeof row.provenance === "string" + ? row.provenance + : null; + + return ( +
  • + + {row.drug} + {row.qualifier ? ( + {row.qualifier} + ) : null} + {row.route || row.frequency ? ( + + {[row.route, row.frequency].filter(Boolean).join(" · ")} + + ) : null} + {overdue ? ( + + + {row.status === "outdated" ? "Source superseded" : "Source review overdue"} + + ) : null} + {metadata || rawProvenanceText ? ( + + {metadata ? ( + <> + + + + ) : ( + + {rawProvenanceText} + + )} + + ) : null} + + + + {row.maximum ? ( + + {"Max "} + + + ) : null} + {row.source ? ( + + ) : null} + +
  • + ); + })} +
+
+ ); +} diff --git a/tests/catalogue-toolbar.dom.test.tsx b/tests/catalogue-toolbar.dom.test.tsx new file mode 100644 index 0000000000..def4ee2d71 --- /dev/null +++ b/tests/catalogue-toolbar.dom.test.tsx @@ -0,0 +1,135 @@ +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { CatalogueToolbar } from "@/components/ui/catalogue-toolbar"; + +describe("CatalogueToolbar DOM and Interactions", () => { + it("renders search, sort, filter trigger, and match count cleanly", () => { + const onSearchChange = vi.fn(); + const onSortChange = vi.fn(); + const onToggleFilter = vi.fn(); + + render( + , + ); + + expect(screen.getByTestId("catalogue-toolbar")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("Search differentials...")).toHaveValue("neuro"); + expect(screen.getByRole("combobox")).toHaveValue("relevance"); + + const trigger = screen.getByTestId("catalogue-filter-trigger"); + expect(trigger).toHaveAttribute("aria-expanded", "false"); + expect(trigger).toHaveAttribute("aria-controls", "diff-filter-panel"); + expect(screen.getByTestId("catalogue-filter-badge")).toHaveTextContent("2"); + + const matchCount = screen.getByTestId("catalogue-match-count"); + expect(matchCount).toHaveTextContent("14 differentials"); + }); + + it("handles user interactions for search, sort, and filter trigger", async () => { + const onSearchChange = vi.fn(); + const onSortChange = vi.fn(); + const onToggleFilter = vi.fn(); + + render( + , + ); + + const input = screen.getByRole("textbox"); + await userEvent.type(input, "bipolar"); + expect(onSearchChange).toHaveBeenCalled(); + + const select = screen.getByRole("combobox"); + await userEvent.selectOptions(select, "alpha"); + expect(onSortChange).toHaveBeenCalledWith("alpha"); + + const trigger = screen.getByTestId("catalogue-filter-trigger"); + await userEvent.click(trigger); + expect(onToggleFilter).toHaveBeenCalledTimes(1); + }); + + it("renders active filter chips and handles remove and clear-all callbacks", async () => { + const onRemoveDomain = vi.fn(); + const onRemoveScope = vi.fn(); + const onClearAll = vi.fn(); + + render( + , + ); + + const chipsStrip = screen.getByTestId("catalogue-applied-filters"); + expect(chipsStrip).toBeInTheDocument(); + + const chips = within(chipsStrip).getAllByTestId("catalogue-applied-chip"); + expect(chips).toHaveLength(2); + expect(chips[0]).toHaveTextContent("Domain: Biological"); + expect(chips[1]).toHaveTextContent("Scope: Guides"); + + const removeDomainBtn = screen.getByRole("button", { name: "Remove filter Domain: Biological" }); + await userEvent.click(removeDomainBtn); + expect(onRemoveDomain).toHaveBeenCalledTimes(1); + + const clearAllBtn = screen.getByTestId("catalogue-clear-filters"); + await userEvent.click(clearAllBtn); + expect(onClearAll).toHaveBeenCalledTimes(1); + }); + + it("handles singular vs plural noun formatting in results count", () => { + const { rerender } = render(); + expect(screen.getByTestId("catalogue-match-count")).toHaveTextContent("1 mechanism"); + + rerender(); + expect(screen.getByTestId("catalogue-match-count")).toHaveTextContent("0 mechanisms"); + + rerender(); + expect(screen.getByTestId("catalogue-match-count")).toHaveTextContent("5 specifiers"); + }); +}); diff --git a/tests/design-system-adoption.test.ts b/tests/design-system-adoption.test.ts index cbbf98a4b9..86af151872 100644 --- a/tests/design-system-adoption.test.ts +++ b/tests/design-system-adoption.test.ts @@ -1204,7 +1204,8 @@ describe("design-system adoption manifest", () => { ).toContain("src/components/ui/confirm-dialog.tsx"); expect( manifest.components.find((component: { name: string }) => component.name === "Quantity").directImportFiles, - ).toContain("src/components/ui/answer-card.tsx"); + ).toContain("src/components/ui/dose-line.tsx"); + const skeleton = manifest.components.find((component: { name: string }) => component.name === "Skeleton"); expect(skeleton.productImportFiles).toEqual( expect.arrayContaining([ diff --git a/tests/design-system-target-evidence.test.ts b/tests/design-system-target-evidence.test.ts index 59cb0ee3aa..0612ffee1c 100644 --- a/tests/design-system-target-evidence.test.ts +++ b/tests/design-system-target-evidence.test.ts @@ -41,4 +41,9 @@ describe("design-system target adoption evidence", () => { expect(links).toMatch(/cross-mode-links-rail hidden[^"\n]*md:flex/); expect(links).not.toContain('data-testid="cross-mode-links-rail-desktop"'); }); + + it("keeps catalogue toolbars delegated to the shared CatalogueToolbar component pattern", () => { + const builder = source("src/components/formulation/formulation-builder-page.tsx"); + expect(builder).toContain(" { + const baseRows: DoseRow[] = [ + { + id: "clozapine", + drug: "Clozapine", + qualifier: "Treatment-resistant schizophrenia", + dose: { value: "12.5", unit: "mg" }, + frequency: "nocte", + route: "oral", + maximum: { value: "900", unit: "mg/day" }, + source: { + sourceId: "doc-1", + title: "WA Clozapine Protocol", + locator: "p. 12", + metadata: { + source_kind: "document", + source_title: "WA Clozapine Protocol", + document_status: "current", + jurisdiction: "WA", + publisher: "WA Health", + }, + }, + status: "current", + }, + ]; + + it("renders drug, quantity, and route details correctly", () => { + render(); + + expect(screen.getByText("Clozapine")).toBeInTheDocument(); + expect(screen.getByText("Treatment-resistant schizophrenia")).toBeInTheDocument(); + expect(screen.getByText("oral · nocte")).toBeInTheDocument(); + expect(screen.getAllByTestId("quantity-value")[0]).toHaveTextContent("12.5"); + expect(screen.getAllByTestId("quantity-unit")[0]).toHaveTextContent("mg"); + }); + + it("renders source provenance badges when present in the payload", () => { + render(); + + const provenance = screen.getByTestId("dose-row-provenance"); + expect(provenance).toBeInTheDocument(); + expect(within(provenance).getByText("Trusted")).toBeInTheDocument(); + expect(within(provenance).getByText("Current source")).toBeInTheDocument(); + }); + + it("handles string provenance fallback when metadata object is absent", () => { + const stringProvRows: DoseRow[] = [ + { + id: "lithium", + drug: "Lithium carbonate", + dose: { value: "450", unit: "mg" }, + status: "current", + provenance: "RANZCP 2026", + }, + ]; + + render(); + const provenance = screen.getByTestId("dose-row-provenance"); + expect(provenance).toHaveTextContent("RANZCP 2026"); + }); + + it("marks overdue and superseded rows cleanly", () => { + const overdueRows: DoseRow[] = [ + { + id: "olanzapine", + drug: "Olanzapine", + dose: { value: "10", unit: "mg" }, + status: "review_due", + source: { + sourceId: "doc-2", + title: "Legacy Psychotropic Guide", + metadata: { + source_kind: "document", + source_title: "Legacy Psychotropic Guide", + document_status: "review_due", + }, + }, + }, + ]; + + render(); + const row = screen.getByTestId("dose-row"); + expect(row).toHaveAttribute("data-overdue", "true"); + expect(within(row).getByTestId("dose-row-overdue")).toHaveTextContent("Source review overdue"); + expect(within(row).getByTestId("dose-row-provenance")).toBeInTheDocument(); + }); + + it("triggers onOpenSource with source locator", async () => { + const onOpenSource = vi.fn(); + render(); + + const button = screen.getByRole("button", { name: "Open WA Clozapine Protocol, p. 12" }); + await userEvent.click(button); + expect(onOpenSource).toHaveBeenCalledWith("doc-1", "p. 12"); + }); +}); + +describe("AnswerFooter DOM and Provenance", () => { + it("renders structured fields and provenance badges when metadata is provided", () => { + render( + , + ); + + const footer = screen.getByTestId("answer-footer"); + expect(footer).toHaveTextContent("Publisher: RANZCP"); + expect(footer).toHaveTextContent("Version: 3.1"); + expect(footer).toHaveTextContent("14/03/2026"); + + const provenance = screen.getByTestId("answer-footer-provenance"); + expect(provenance).toBeInTheDocument(); + expect(within(provenance).getByText("Trusted")).toBeInTheDocument(); + expect(within(provenance).getByText("Current source")).toBeInTheDocument(); + }); + + it("renders MissingValue for missing publisher/version without provenance", () => { + render(); + + const footer = screen.getByTestId("answer-footer"); + expect(within(footer).getAllByTestId("missing-value").length).toBeGreaterThanOrEqual(1); + expect(screen.queryByTestId("answer-footer-provenance")).not.toBeInTheDocument(); + }); +}); From 064283084dee2bfc82f2d68ed9442ff6e2386afd Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 18 Aug 2026 05:03:47 +0800 Subject: [PATCH 2/4] fix(ui): repair CI blockers on catalogue toolbars (PR #2073) --- .design-sync/config.json | 6 +++--- docs/design-system/COMPONENTS.md | 4 ++-- docs/design-system/adoption-manifest.json | 14 +++++++------- src/components/ui/catalogue-toolbar.tsx | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.design-sync/config.json b/.design-sync/config.json index a6c9195504..75fb76191a 100644 --- a/.design-sync/config.json +++ b/.design-sync/config.json @@ -21,7 +21,7 @@ "dtsPropsFor": { "AccessibleTable": "actionsHeader?: string; caption: string; clinicalOnly?: boolean; columnAlign?: AccessibleTableColumnAlign[]; columns?: string[] | null; compact?: boolean; densePreview?: boolean; dialogTitle?: string | null; expandOnMobile?: boolean; hidePreviewCaption?: boolean; hidePreviewRowCount?: boolean; lowConfidenceFallback?: ReactNode; markdown?: string | null; normalizedTable?: NormalizedAccessibleTable | null; numericColumns?: number[]; previewRows?: number; rowActions?: ReactNode[]; rows?: string[][] | null;", "AnswerCard": "actions?: AnswerCardAction[]; children: ReactNode; className?: string; onOpenSource?: ((sourceId: string, locator?: string) => void) | ((sourceId: string, locator?: string) => void); provenance?: AnswerFooterProps; query?: string; state: { kind: \"ready\"; sourceCount: number; } | DegradedAnswerState; verification: VerificationNoticeProps;", - "AnswerFooter": "className?: string; generatedAt?: string | null; publisher?: string | null; reviewDate?: string | null; version?: string | null;", + "AnswerFooter": "className?: string; generatedAt?: string | null; metadata?: SourceMetadataInput; provenance?: string | SourceMetadataInput; publisher?: string | null; reviewDate?: string | null; version?: string | null;", "AsyncButton": "about?: string; accessKey?: string; \"aria-activedescendant\"?: string; \"aria-atomic\"?: (boolean | \"true\" | \"false\"); \"aria-autocomplete\"?: \"none\" | \"list\" | \"inline\" | \"both\"; \"aria-braillelabel\"?: string; \"aria-brailleroledescription\"?: string; \"aria-busy\"?: (boolean | \"true\" | \"false\"); \"aria-checked\"?: boolean | \"true\" | \"false\" | \"mixed\"; \"aria-colcount\"?: number; \"aria-colindex\"?: number; \"aria-colindextext\"?: string; \"aria-colspan\"?: number; \"aria-controls\"?: string; \"aria-current\"?: boolean | \"true\" | \"false\" | \"page\" | \"step\" | \"location\" | \"date\" | \"time\"; \"aria-describedby\"?: string; \"aria-description\"?: string; \"aria-details\"?: string; \"aria-disabled\"?: (boolean | \"true\" | \"false\"); \"aria-dropeffect\"?: \"none\" | \"link\" | \"copy\" | \"execute\" | \"move\" | \"popup\"; \"aria-errormessage\"?: string; \"aria-expanded\"?: (boolean | \"true\" | \"false\"); \"aria-flowto\"?: string; \"aria-grabbed\"?: (boolean | \"true\" | \"false\"); \"aria-haspopup\"?: boolean | \"true\" | \"false\" | \"dialog\" | \"grid\" | \"listbox\" | \"menu\" | \"tree\"; \"aria-hidden\"?: (boolean | \"true\" | \"false\"); \"aria-invalid\"?: boolean | \"true\" | \"false\" | \"grammar\" | \"spelling\"; \"aria-keyshortcuts\"?: string; \"aria-label\"?: string; \"aria-labelledby\"?: string; \"aria-level\"?: number; \"aria-live\"?: \"off\" | \"assertive\" | \"polite\"; \"aria-modal\"?: (boolean | \"true\" | \"false\"); \"aria-multiline\"?: (boolean | \"true\" | \"false\"); \"aria-multiselectable\"?: (boolean | \"true\" | \"false\"); \"aria-orientation\"?: \"horizontal\" | \"vertical\"; \"aria-owns\"?: string; \"aria-placeholder\"?: string; \"aria-posinset\"?: number; \"aria-pressed\"?: boolean | \"true\" | \"false\" | \"mixed\"; \"aria-readonly\"?: (boolean | \"true\" | \"false\"); \"aria-relevant\"?: \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"all\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\"; \"aria-required\"?: (boolean | \"true\" | \"false\"); \"aria-roledescription\"?: string; \"aria-rowcount\"?: number; \"aria-rowindex\"?: number; \"aria-rowindextext\"?: string; \"aria-rowspan\"?: number; \"aria-selected\"?: (boolean | \"true\" | \"false\"); \"aria-setsize\"?: number; \"aria-sort\"?: \"none\" | \"ascending\" | \"descending\" | \"other\"; \"aria-valuemax\"?: number; \"aria-valuemin\"?: number; \"aria-valuenow\"?: number; \"aria-valuetext\"?: string; autoCapitalize?: \"off\" | \"none\" | \"on\" | \"sentences\" | \"words\" | \"characters\" | (string & {}); autoCorrect?: string; autoFocus?: boolean; autoSave?: string; busy: boolean; busyLabel: string; children: ReactNode; className?: string; color?: string; content?: string; contentEditable?: (boolean | \"true\" | \"false\") | \"inherit\" | \"plaintext-only\"; contextMenu?: string; dangerouslySetInnerHTML?: { __html: string | TrustedHTML; }; datatype?: string; defaultChecked?: boolean; defaultValue?: string | number | readonly string[]; dir?: string; disabled?: boolean; draggable?: (boolean | \"true\" | \"false\"); enterKeyHint?: \"enter\" | \"done\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\"; exportparts?: string; form?: string; formAction?: string | ((formData: FormData) => void | Promise); formEncType?: string; formMethod?: string; formNoValidate?: boolean; formTarget?: string; hidden?: boolean; id?: string; idleIcon?: ReactNode; inert?: boolean; inlist?: any; inputMode?: \"none\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\"; is?: string; itemID?: string; itemProp?: string; itemRef?: string; itemScope?: boolean; itemType?: string; lang?: string; name?: string; nonce?: string; onAbort?: import(\"react\").ReactEventHandler; onAbortCapture?: import(\"react\").ReactEventHandler; onAnimationEnd?: import(\"react\").AnimationEventHandler; onAnimationEndCapture?: import(\"react\").AnimationEventHandler; onAnimationIteration?: import(\"react\").AnimationEventHandler; onAnimationIterationCapture?: import(\"react\").AnimationEventHandler; onAnimationStart?: import(\"react\").AnimationEventHandler; onAnimationStartCapture?: import(\"react\").AnimationEventHandler; onAuxClick?: import(\"react\").MouseEventHandler; onAuxClickCapture?: import(\"react\").MouseEventHandler; onBeforeInput?: import(\"react\").InputEventHandler; onBeforeInputCapture?: import(\"react\").InputEventHandler; onBeforeToggle?: import(\"react\").ToggleEventHandler; onBlur?: import(\"react\").FocusEventHandler; onBlurCapture?: import(\"react\").FocusEventHandler; onCanPlay?: import(\"react\").ReactEventHandler; onCanPlayCapture?: import(\"react\").ReactEventHandler; onCanPlayThrough?: import(\"react\").ReactEventHandler; onCanPlayThroughCapture?: import(\"react\").ReactEventHandler; onChange?: import(\"react\").ChangeEventHandler; onChangeCapture?: import(\"react\").ChangeEventHandler; onClick?: import(\"react\").MouseEventHandler; onClickCapture?: import(\"react\").MouseEventHandler; onCompositionEnd?: import(\"react\").CompositionEventHandler; onCompositionEndCapture?: import(\"react\").CompositionEventHandler; onCompositionStart?: import(\"react\").CompositionEventHandler; onCompositionStartCapture?: import(\"react\").CompositionEventHandler; onCompositionUpdate?: import(\"react\").CompositionEventHandler; onCompositionUpdateCapture?: import(\"react\").CompositionEventHandler; onContextMenu?: import(\"react\").MouseEventHandler; onContextMenuCapture?: import(\"react\").MouseEventHandler; onCopy?: import(\"react\").ClipboardEventHandler; onCopyCapture?: import(\"react\").ClipboardEventHandler; onCut?: import(\"react\").ClipboardEventHandler; onCutCapture?: import(\"react\").ClipboardEventHandler; onDoubleClick?: import(\"react\").MouseEventHandler; onDoubleClickCapture?: import(\"react\").MouseEventHandler; onDrag?: import(\"react\").DragEventHandler; onDragCapture?: import(\"react\").DragEventHandler; onDragEnd?: import(\"react\").DragEventHandler; onDragEndCapture?: import(\"react\").DragEventHandler; onDragEnter?: import(\"react\").DragEventHandler; onDragEnterCapture?: import(\"react\").DragEventHandler; onDragExit?: import(\"react\").DragEventHandler; onDragExitCapture?: import(\"react\").DragEventHandler; onDragLeave?: import(\"react\").DragEventHandler; onDragLeaveCapture?: import(\"react\").DragEventHandler; onDragOver?: import(\"react\").DragEventHandler; onDragOverCapture?: import(\"react\").DragEventHandler; onDragStart?: import(\"react\").DragEventHandler; onDragStartCapture?: import(\"react\").DragEventHandler; onDrop?: import(\"react\").DragEventHandler; onDropCapture?: import(\"react\").DragEventHandler; onDurationChange?: import(\"react\").ReactEventHandler; onDurationChangeCapture?: import(\"react\").ReactEventHandler; onEmptied?: import(\"react\").ReactEventHandler; onEmptiedCapture?: import(\"react\").ReactEventHandler; onEncrypted?: import(\"react\").ReactEventHandler; onEncryptedCapture?: import(\"react\").ReactEventHandler; onEnded?: import(\"react\").ReactEventHandler; onEndedCapture?: import(\"react\").ReactEventHandler; onError?: import(\"react\").ReactEventHandler; onErrorCapture?: import(\"react\").ReactEventHandler; onFocus?: import(\"react\").FocusEventHandler; onFocusCapture?: import(\"react\").FocusEventHandler; onGotPointerCapture?: import(\"react\").PointerEventHandler; onGotPointerCaptureCapture?: import(\"react\").PointerEventHandler; onInput?: import(\"react\").InputEventHandler; onInputCapture?: import(\"react\").InputEventHandler; onInvalid?: import(\"react\").ReactEventHandler; onInvalidCapture?: import(\"react\").ReactEventHandler; onKeyDown?: import(\"react\").KeyboardEventHandler; onKeyDownCapture?: import(\"react\").KeyboardEventHandler; onKeyPress?: import(\"react\").KeyboardEventHandler; onKeyPressCapture?: import(\"react\").KeyboardEventHandler; onKeyUp?: import(\"react\").KeyboardEventHandler; onKeyUpCapture?: import(\"react\").KeyboardEventHandler; onLoad?: import(\"react\").ReactEventHandler; onLoadCapture?: import(\"react\").ReactEventHandler; onLoadedData?: import(\"react\").ReactEventHandler; onLoadedDataCapture?: import(\"react\").ReactEventHandler; onLoadedMetadata?: import(\"react\").ReactEventHandler; onLoadedMetadataCapture?: import(\"react\").ReactEventHandler; onLoadStart?: import(\"react\").ReactEventHandler; onLoadStartCapture?: import(\"react\").ReactEventHandler; onLostPointerCapture?: import(\"react\").PointerEventHandler; onLostPointerCaptureCapture?: import(\"react\").PointerEventHandler; onMouseDown?: import(\"react\").MouseEventHandler; onMouseDownCapture?: import(\"react\").MouseEventHandler; onMouseEnter?: import(\"react\").MouseEventHandler; onMouseLeave?: import(\"react\").MouseEventHandler; onMouseMove?: import(\"react\").MouseEventHandler; onMouseMoveCapture?: import(\"react\").MouseEventHandler; onMouseOut?: import(\"react\").MouseEventHandler; onMouseOutCapture?: import(\"react\").MouseEventHandler; onMouseOver?: import(\"react\").MouseEventHandler; onMouseOverCapture?: import(\"react\").MouseEventHandler; onMouseUp?: import(\"react\").MouseEventHandler; onMouseUpCapture?: import(\"react\").MouseEventHandler; onPaste?: import(\"react\").ClipboardEventHandler; onPasteCapture?: import(\"react\").ClipboardEventHandler; onPause?: import(\"react\").ReactEventHandler; onPauseCapture?: import(\"react\").ReactEventHandler; onPlay?: import(\"react\").ReactEventHandler; onPlayCapture?: import(\"react\").ReactEventHandler; onPlaying?: import(\"react\").ReactEventHandler; onPlayingCapture?: import(\"react\").ReactEventHandler; onPointerCancel?: import(\"react\").PointerEventHandler; onPointerCancelCapture?: import(\"react\").PointerEventHandler; onPointerDown?: import(\"react\").PointerEventHandler; onPointerDownCapture?: import(\"react\").PointerEventHandler; onPointerEnter?: import(\"react\").PointerEventHandler; onPointerLeave?: import(\"react\").PointerEventHandler; onPointerMove?: import(\"react\").PointerEventHandler; onPointerMoveCapture?: import(\"react\").PointerEventHandler; onPointerOut?: import(\"react\").PointerEventHandler; onPointerOutCapture?: import(\"react\").PointerEventHandler; onPointerOver?: import(\"react\").PointerEventHandler; onPointerOverCapture?: import(\"react\").PointerEventHandler; onPointerUp?: import(\"react\").PointerEventHandler; onPointerUpCapture?: import(\"react\").PointerEventHandler; onProgress?: import(\"react\").ReactEventHandler; onProgressCapture?: import(\"react\").ReactEventHandler; onRateChange?: import(\"react\").ReactEventHandler; onRateChangeCapture?: import(\"react\").ReactEventHandler; onReset?: import(\"react\").ReactEventHandler; onResetCapture?: import(\"react\").ReactEventHandler; onScroll?: import(\"react\").UIEventHandler; onScrollCapture?: import(\"react\").UIEventHandler; onScrollEnd?: import(\"react\").UIEventHandler; onScrollEndCapture?: import(\"react\").UIEventHandler; onSeeked?: import(\"react\").ReactEventHandler; onSeekedCapture?: import(\"react\").ReactEventHandler; onSeeking?: import(\"react\").ReactEventHandler; onSeekingCapture?: import(\"react\").ReactEventHandler; onSelect?: import(\"react\").ReactEventHandler; onSelectCapture?: import(\"react\").ReactEventHandler; onStalled?: import(\"react\").ReactEventHandler; onStalledCapture?: import(\"react\").ReactEventHandler; onSubmit?: import(\"react\").SubmitEventHandler; onSubmitCapture?: import(\"react\").SubmitEventHandler; onSuspend?: import(\"react\").ReactEventHandler; onSuspendCapture?: import(\"react\").ReactEventHandler; onTimeUpdate?: import(\"react\").ReactEventHandler; onTimeUpdateCapture?: import(\"react\").ReactEventHandler; onToggle?: import(\"react\").ToggleEventHandler; onTouchCancel?: import(\"react\").TouchEventHandler; onTouchCancelCapture?: import(\"react\").TouchEventHandler; onTouchEnd?: import(\"react\").TouchEventHandler; onTouchEndCapture?: import(\"react\").TouchEventHandler; onTouchMove?: import(\"react\").TouchEventHandler; onTouchMoveCapture?: import(\"react\").TouchEventHandler; onTouchStart?: import(\"react\").TouchEventHandler; onTouchStartCapture?: import(\"react\").TouchEventHandler; onTransitionCancel?: import(\"react\").TransitionEventHandler; onTransitionCancelCapture?: import(\"react\").TransitionEventHandler; onTransitionEnd?: import(\"react\").TransitionEventHandler; onTransitionEndCapture?: import(\"react\").TransitionEventHandler; onTransitionRun?: import(\"react\").TransitionEventHandler; onTransitionRunCapture?: import(\"react\").TransitionEventHandler; onTransitionStart?: import(\"react\").TransitionEventHandler; onTransitionStartCapture?: import(\"react\").TransitionEventHandler; onVolumeChange?: import(\"react\").ReactEventHandler; onVolumeChangeCapture?: import(\"react\").ReactEventHandler; onWaiting?: import(\"react\").ReactEventHandler; onWaitingCapture?: import(\"react\").ReactEventHandler; onWheel?: import(\"react\").WheelEventHandler; onWheelCapture?: import(\"react\").WheelEventHandler; part?: string; popover?: \"\" | \"auto\" | \"manual\" | \"hint\"; popoverTarget?: string; popoverTargetAction?: \"toggle\" | \"show\" | \"hide\"; prefix?: string; property?: string; radioGroup?: string; rel?: string; resource?: string; results?: number; rev?: string; role?: import(\"react\").AriaRole; security?: string; slot?: string; spellCheck?: (boolean | \"true\" | \"false\"); style?: import(\"react\").CSSProperties; suppressContentEditableWarning?: boolean; suppressHydrationWarning?: boolean; tabIndex?: number; title?: string; translate?: \"yes\" | \"no\"; type?: \"submit\" | \"reset\" | \"button\"; typeof?: string; unselectable?: \"off\" | \"on\"; value?: string | number | readonly string[]; vocab?: string;", "Breadcrumb": "className?: string; items: Crumb[];", "Button": "about?: string; accessKey?: string; \"aria-activedescendant\"?: string; \"aria-atomic\"?: (boolean | \"true\" | \"false\"); \"aria-autocomplete\"?: \"none\" | \"list\" | \"inline\" | \"both\"; \"aria-braillelabel\"?: string; \"aria-brailleroledescription\"?: string; \"aria-busy\"?: (boolean | \"true\" | \"false\"); \"aria-checked\"?: boolean | \"true\" | \"false\" | \"mixed\"; \"aria-colcount\"?: number; \"aria-colindex\"?: number; \"aria-colindextext\"?: string; \"aria-colspan\"?: number; \"aria-controls\"?: string; \"aria-current\"?: boolean | \"true\" | \"false\" | \"page\" | \"step\" | \"location\" | \"date\" | \"time\"; \"aria-describedby\"?: string; \"aria-description\"?: string; \"aria-details\"?: string; \"aria-disabled\"?: (boolean | \"true\" | \"false\"); \"aria-dropeffect\"?: \"none\" | \"link\" | \"copy\" | \"execute\" | \"move\" | \"popup\"; \"aria-errormessage\"?: string; \"aria-expanded\"?: (boolean | \"true\" | \"false\"); \"aria-flowto\"?: string; \"aria-grabbed\"?: (boolean | \"true\" | \"false\"); \"aria-haspopup\"?: boolean | \"true\" | \"false\" | \"dialog\" | \"grid\" | \"listbox\" | \"menu\" | \"tree\"; \"aria-hidden\"?: (boolean | \"true\" | \"false\"); \"aria-invalid\"?: boolean | \"true\" | \"false\" | \"grammar\" | \"spelling\"; \"aria-keyshortcuts\"?: string; \"aria-label\"?: string; \"aria-labelledby\"?: string; \"aria-level\"?: number; \"aria-live\"?: \"off\" | \"assertive\" | \"polite\"; \"aria-modal\"?: (boolean | \"true\" | \"false\"); \"aria-multiline\"?: (boolean | \"true\" | \"false\"); \"aria-multiselectable\"?: (boolean | \"true\" | \"false\"); \"aria-orientation\"?: \"horizontal\" | \"vertical\"; \"aria-owns\"?: string; \"aria-placeholder\"?: string; \"aria-posinset\"?: number; \"aria-pressed\"?: boolean | \"true\" | \"false\" | \"mixed\"; \"aria-readonly\"?: (boolean | \"true\" | \"false\"); \"aria-relevant\"?: \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"all\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\"; \"aria-required\"?: (boolean | \"true\" | \"false\"); \"aria-roledescription\"?: string; \"aria-rowcount\"?: number; \"aria-rowindex\"?: number; \"aria-rowindextext\"?: string; \"aria-rowspan\"?: number; \"aria-selected\"?: (boolean | \"true\" | \"false\"); \"aria-setsize\"?: number; \"aria-sort\"?: \"none\" | \"ascending\" | \"descending\" | \"other\"; \"aria-valuemax\"?: number; \"aria-valuemin\"?: number; \"aria-valuenow\"?: number; \"aria-valuetext\"?: string; autoCapitalize?: \"off\" | \"none\" | \"on\" | \"sentences\" | \"words\" | \"characters\" | (string & {}); autoCorrect?: string; autoFocus?: boolean; autoSave?: string; block?: boolean; busy?: boolean; busyLabel?: string; children: ReactNode; className?: string; color?: string; content?: string; contentEditable?: (boolean | \"true\" | \"false\") | \"inherit\" | \"plaintext-only\"; contextMenu?: string; dangerouslySetInnerHTML?: { __html: string | TrustedHTML; }; datatype?: string; defaultChecked?: boolean; defaultValue?: string | number | readonly string[]; dir?: string; disabled?: boolean; draggable?: (boolean | \"true\" | \"false\"); enterKeyHint?: \"enter\" | \"done\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\"; exportparts?: string; form?: string; formAction?: string | ((formData: FormData) => void | Promise); formEncType?: string; formMethod?: string; formNoValidate?: boolean; formTarget?: string; hidden?: boolean; icon?: LucideIcon; id?: string; inert?: boolean; inlist?: any; inputMode?: \"none\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\"; is?: string; itemID?: string; itemProp?: string; itemRef?: string; itemScope?: boolean; itemType?: string; lang?: string; name?: string; nonce?: string; onAbort?: import(\"react\").ReactEventHandler; onAbortCapture?: import(\"react\").ReactEventHandler; onAnimationEnd?: import(\"react\").AnimationEventHandler; onAnimationEndCapture?: import(\"react\").AnimationEventHandler; onAnimationIteration?: import(\"react\").AnimationEventHandler; onAnimationIterationCapture?: import(\"react\").AnimationEventHandler; onAnimationStart?: import(\"react\").AnimationEventHandler; onAnimationStartCapture?: import(\"react\").AnimationEventHandler; onAuxClick?: import(\"react\").MouseEventHandler; onAuxClickCapture?: import(\"react\").MouseEventHandler; onBeforeInput?: import(\"react\").InputEventHandler; onBeforeInputCapture?: import(\"react\").InputEventHandler; onBeforeToggle?: import(\"react\").ToggleEventHandler; onBlur?: import(\"react\").FocusEventHandler; onBlurCapture?: import(\"react\").FocusEventHandler; onCanPlay?: import(\"react\").ReactEventHandler; onCanPlayCapture?: import(\"react\").ReactEventHandler; onCanPlayThrough?: import(\"react\").ReactEventHandler; onCanPlayThroughCapture?: import(\"react\").ReactEventHandler; onChange?: import(\"react\").ChangeEventHandler; onChangeCapture?: import(\"react\").ChangeEventHandler; onClick?: import(\"react\").MouseEventHandler; onClickCapture?: import(\"react\").MouseEventHandler; onCompositionEnd?: import(\"react\").CompositionEventHandler; onCompositionEndCapture?: import(\"react\").CompositionEventHandler; onCompositionStart?: import(\"react\").CompositionEventHandler; onCompositionStartCapture?: import(\"react\").CompositionEventHandler; onCompositionUpdate?: import(\"react\").CompositionEventHandler; onCompositionUpdateCapture?: import(\"react\").CompositionEventHandler; onContextMenu?: import(\"react\").MouseEventHandler; onContextMenuCapture?: import(\"react\").MouseEventHandler; onCopy?: import(\"react\").ClipboardEventHandler; onCopyCapture?: import(\"react\").ClipboardEventHandler; onCut?: import(\"react\").ClipboardEventHandler; onCutCapture?: import(\"react\").ClipboardEventHandler; onDoubleClick?: import(\"react\").MouseEventHandler; onDoubleClickCapture?: import(\"react\").MouseEventHandler; onDrag?: import(\"react\").DragEventHandler; onDragCapture?: import(\"react\").DragEventHandler; onDragEnd?: import(\"react\").DragEventHandler; onDragEndCapture?: import(\"react\").DragEventHandler; onDragEnter?: import(\"react\").DragEventHandler; onDragEnterCapture?: import(\"react\").DragEventHandler; onDragExit?: import(\"react\").DragEventHandler; onDragExitCapture?: import(\"react\").DragEventHandler; onDragLeave?: import(\"react\").DragEventHandler; onDragLeaveCapture?: import(\"react\").DragEventHandler; onDragOver?: import(\"react\").DragEventHandler; onDragOverCapture?: import(\"react\").DragEventHandler; onDragStart?: import(\"react\").DragEventHandler; onDragStartCapture?: import(\"react\").DragEventHandler; onDrop?: import(\"react\").DragEventHandler; onDropCapture?: import(\"react\").DragEventHandler; onDurationChange?: import(\"react\").ReactEventHandler; onDurationChangeCapture?: import(\"react\").ReactEventHandler; onEmptied?: import(\"react\").ReactEventHandler; onEmptiedCapture?: import(\"react\").ReactEventHandler; onEncrypted?: import(\"react\").ReactEventHandler; onEncryptedCapture?: import(\"react\").ReactEventHandler; onEnded?: import(\"react\").ReactEventHandler; onEndedCapture?: import(\"react\").ReactEventHandler; onError?: import(\"react\").ReactEventHandler; onErrorCapture?: import(\"react\").ReactEventHandler; onFocus?: import(\"react\").FocusEventHandler; onFocusCapture?: import(\"react\").FocusEventHandler; onGotPointerCapture?: import(\"react\").PointerEventHandler; onGotPointerCaptureCapture?: import(\"react\").PointerEventHandler; onInput?: import(\"react\").InputEventHandler; onInputCapture?: import(\"react\").InputEventHandler; onInvalid?: import(\"react\").ReactEventHandler; onInvalidCapture?: import(\"react\").ReactEventHandler; onKeyDown?: import(\"react\").KeyboardEventHandler; onKeyDownCapture?: import(\"react\").KeyboardEventHandler; onKeyPress?: import(\"react\").KeyboardEventHandler; onKeyPressCapture?: import(\"react\").KeyboardEventHandler; onKeyUp?: import(\"react\").KeyboardEventHandler; onKeyUpCapture?: import(\"react\").KeyboardEventHandler; onLoad?: import(\"react\").ReactEventHandler; onLoadCapture?: import(\"react\").ReactEventHandler; onLoadedData?: import(\"react\").ReactEventHandler; onLoadedDataCapture?: import(\"react\").ReactEventHandler; onLoadedMetadata?: import(\"react\").ReactEventHandler; onLoadedMetadataCapture?: import(\"react\").ReactEventHandler; onLoadStart?: import(\"react\").ReactEventHandler; onLoadStartCapture?: import(\"react\").ReactEventHandler; onLostPointerCapture?: import(\"react\").PointerEventHandler; onLostPointerCaptureCapture?: import(\"react\").PointerEventHandler; onMouseDown?: import(\"react\").MouseEventHandler; onMouseDownCapture?: import(\"react\").MouseEventHandler; onMouseEnter?: import(\"react\").MouseEventHandler; onMouseLeave?: import(\"react\").MouseEventHandler; onMouseMove?: import(\"react\").MouseEventHandler; onMouseMoveCapture?: import(\"react\").MouseEventHandler; onMouseOut?: import(\"react\").MouseEventHandler; onMouseOutCapture?: import(\"react\").MouseEventHandler; onMouseOver?: import(\"react\").MouseEventHandler; onMouseOverCapture?: import(\"react\").MouseEventHandler; onMouseUp?: import(\"react\").MouseEventHandler; onMouseUpCapture?: import(\"react\").MouseEventHandler; onPaste?: import(\"react\").ClipboardEventHandler; onPasteCapture?: import(\"react\").ClipboardEventHandler; onPause?: import(\"react\").ReactEventHandler; onPauseCapture?: import(\"react\").ReactEventHandler; onPlay?: import(\"react\").ReactEventHandler; onPlayCapture?: import(\"react\").ReactEventHandler; onPlaying?: import(\"react\").ReactEventHandler; onPlayingCapture?: import(\"react\").ReactEventHandler; onPointerCancel?: import(\"react\").PointerEventHandler; onPointerCancelCapture?: import(\"react\").PointerEventHandler; onPointerDown?: import(\"react\").PointerEventHandler; onPointerDownCapture?: import(\"react\").PointerEventHandler; onPointerEnter?: import(\"react\").PointerEventHandler; onPointerLeave?: import(\"react\").PointerEventHandler; onPointerMove?: import(\"react\").PointerEventHandler; onPointerMoveCapture?: import(\"react\").PointerEventHandler; onPointerOut?: import(\"react\").PointerEventHandler; onPointerOutCapture?: import(\"react\").PointerEventHandler; onPointerOver?: import(\"react\").PointerEventHandler; onPointerOverCapture?: import(\"react\").PointerEventHandler; onPointerUp?: import(\"react\").PointerEventHandler; onPointerUpCapture?: import(\"react\").PointerEventHandler; onProgress?: import(\"react\").ReactEventHandler; onProgressCapture?: import(\"react\").ReactEventHandler; onRateChange?: import(\"react\").ReactEventHandler; onRateChangeCapture?: import(\"react\").ReactEventHandler; onReset?: import(\"react\").ReactEventHandler; onResetCapture?: import(\"react\").ReactEventHandler; onScroll?: import(\"react\").UIEventHandler; onScrollCapture?: import(\"react\").UIEventHandler; onScrollEnd?: import(\"react\").UIEventHandler; onScrollEndCapture?: import(\"react\").UIEventHandler; onSeeked?: import(\"react\").ReactEventHandler; onSeekedCapture?: import(\"react\").ReactEventHandler; onSeeking?: import(\"react\").ReactEventHandler; onSeekingCapture?: import(\"react\").ReactEventHandler; onSelect?: import(\"react\").ReactEventHandler; onSelectCapture?: import(\"react\").ReactEventHandler; onStalled?: import(\"react\").ReactEventHandler; onStalledCapture?: import(\"react\").ReactEventHandler; onSubmit?: import(\"react\").SubmitEventHandler; onSubmitCapture?: import(\"react\").SubmitEventHandler; onSuspend?: import(\"react\").ReactEventHandler; onSuspendCapture?: import(\"react\").ReactEventHandler; onTimeUpdate?: import(\"react\").ReactEventHandler; onTimeUpdateCapture?: import(\"react\").ReactEventHandler; onToggle?: import(\"react\").ToggleEventHandler; onTouchCancel?: import(\"react\").TouchEventHandler; onTouchCancelCapture?: import(\"react\").TouchEventHandler; onTouchEnd?: import(\"react\").TouchEventHandler; onTouchEndCapture?: import(\"react\").TouchEventHandler; onTouchMove?: import(\"react\").TouchEventHandler; onTouchMoveCapture?: import(\"react\").TouchEventHandler; onTouchStart?: import(\"react\").TouchEventHandler; onTouchStartCapture?: import(\"react\").TouchEventHandler; onTransitionCancel?: import(\"react\").TransitionEventHandler; onTransitionCancelCapture?: import(\"react\").TransitionEventHandler; onTransitionEnd?: import(\"react\").TransitionEventHandler; onTransitionEndCapture?: import(\"react\").TransitionEventHandler; onTransitionRun?: import(\"react\").TransitionEventHandler; onTransitionRunCapture?: import(\"react\").TransitionEventHandler; onTransitionStart?: import(\"react\").TransitionEventHandler; onTransitionStartCapture?: import(\"react\").TransitionEventHandler; onVolumeChange?: import(\"react\").ReactEventHandler; onVolumeChangeCapture?: import(\"react\").ReactEventHandler; onWaiting?: import(\"react\").ReactEventHandler; onWaitingCapture?: import(\"react\").ReactEventHandler; onWheel?: import(\"react\").WheelEventHandler; onWheelCapture?: import(\"react\").WheelEventHandler; part?: string; popover?: \"\" | \"auto\" | \"manual\" | \"hint\"; popoverTarget?: string; popoverTargetAction?: \"toggle\" | \"show\" | \"hide\"; prefix?: string; property?: string; radioGroup?: string; ref?: Ref; rel?: string; resource?: string; results?: number; rev?: string; role?: import(\"react\").AriaRole; security?: string; size?: ButtonSize; slot?: string; spellCheck?: (boolean | \"true\" | \"false\"); style?: import(\"react\").CSSProperties; suppressContentEditableWarning?: boolean; suppressHydrationWarning?: boolean; tabIndex?: number; testId?: string; title?: string; trailingIcon?: LucideIcon; translate?: \"yes\" | \"no\"; type?: \"submit\" | \"reset\" | \"button\"; typeof?: string; unselectable?: \"off\" | \"on\"; value?: string | number | readonly string[]; variant?: ButtonVariant; vocab?: string;", @@ -106,7 +106,7 @@ "componentSrcMap": { "AccessibleTable": "src/components/AccessibleTable.tsx", "AnswerCard": "src/components/ui/answer-card.tsx", - "AnswerFooter": "src/components/ui/answer-card.tsx", + "AnswerFooter": "src/components/answer/AnswerFooter.tsx", "AsyncButton": "src/components/ui-primitives.tsx", "Breadcrumb": "src/components/ui/page-header.tsx", "Button": "src/components/ui/button.tsx", @@ -118,7 +118,7 @@ "DateDisplay": "src/components/ui/date-display.tsx", "Disclosure": "src/components/ui/disclosure.tsx", "DisclosureGroup": "src/components/ui/disclosure.tsx", - "DoseLine": "src/components/ui/answer-card.tsx", + "DoseLine": "src/components/ui/dose-line.tsx", "DownloadLink": "src/components/ui/link.tsx", "EmptyState": "src/components/ui-primitives.tsx", "ErrorState": "src/components/ui/error-state.tsx", diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index 8f18246b7e..d1bf12d538 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -955,7 +955,7 @@ tests to run. Registered public components: 54 Components with a valid design-sync preview: 54 -Components with product imports: 33 +Components with product imports: 34 This generated snapshot is a local source-derived inventory. It does not assert remote design-project publication. @@ -963,7 +963,7 @@ This generated snapshot is a local source-derived inventory. It does not assert | ------------------------ | -------- | ----- | ------------------ | --------------------- | -------------- | ------------------ | --------------: | | `AccessibleTable` | source | yes | yes | inherited-global-root | yes | no | 4 | | `AnswerCard` | answer | yes | yes | inherited-global-root | yes | no | 1 | -| `AnswerFooter` | answer | yes | yes | no | yes | no | 0 | +| `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 | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 4ac0a7fdc0..47bd8277d9 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -166,11 +166,11 @@ { "name": "AnswerFooter", "family": "answer", - "source": "src/components/ui/answer-card.tsx", + "source": "src/components/answer/AnswerFooter.tsx", "sourceExported": true, "entryExported": true, - "directImportFiles": [], - "productImportFiles": [], + "directImportFiles": ["src/components/ui/answer-card.tsx"], + "productImportFiles": ["src/components/ui/answer-card.tsx"], "designSync": { "listedInSourceMap": true, "listedInDtsProps": true, @@ -193,8 +193,8 @@ }, "built": true, "locallyRegistered": true, - "v2ShellMounted": false, - "v2MountMode": "none", + "v2ShellMounted": true, + "v2MountMode": "inherited-global-root", "proofDeclared": true, "baselineCommitted": false }, @@ -590,7 +590,7 @@ { "name": "DoseLine", "family": "answer", - "source": "src/components/ui/answer-card.tsx", + "source": "src/components/ui/dose-line.tsx", "sourceExported": true, "entryExported": true, "directImportFiles": [], @@ -3443,7 +3443,7 @@ "summary": { "registeredComponentCount": 54, "previewCount": 54, - "productImportedComponentCount": 33, + "productImportedComponentCount": 34, "rootCount": 59, "productionRouteCount": 51, "nextUiEntryCount": 103 diff --git a/src/components/ui/catalogue-toolbar.tsx b/src/components/ui/catalogue-toolbar.tsx index e7877a1934..c56e8e70ec 100644 --- a/src/components/ui/catalogue-toolbar.tsx +++ b/src/components/ui/catalogue-toolbar.tsx @@ -172,7 +172,7 @@ export function CatalogueToolbar({ {(filterTrigger.activeCount ?? 0) > 0 ? ( {filterTrigger.activeCount} From a182995c2c48431a372ae4b150433b7d4487ae49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Aug 2026 02:10:41 +0000 Subject: [PATCH 3/4] Fix catalogue toolbar filter trigger defaults Co-authored-by: BigSimmo <87357024+BigSimmo@users.noreply.github.com> --- src/components/ui/catalogue-toolbar.tsx | 4 ++-- tests/catalogue-toolbar.dom.test.tsx | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/ui/catalogue-toolbar.tsx b/src/components/ui/catalogue-toolbar.tsx index c56e8e70ec..b1dabb57df 100644 --- a/src/components/ui/catalogue-toolbar.tsx +++ b/src/components/ui/catalogue-toolbar.tsx @@ -155,9 +155,9 @@ export function CatalogueToolbar({