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"
- />
- setDomain(event.target.value)}
- className="font-semibold"
- options={[
+ ({ value: item, label: item })),
- ]}
- />
-
+ ],
+ }}
+ appliedFilters={
+ domain !== "all"
+ ? [{ id: "domain", groupLabel: "Domain", valueLabel: domain, onRemove: () => setDomain("all") }]
+ : []
+ }
+ onClearFilters={domain !== "all" ? () => setDomain("all") : undefined}
+ matchCount={visibleMechanisms.length}
+ noun="mechanism"
+ />
{visibleMechanisms.map((mechanism) => {
diff --git a/src/components/ui/answer-card.tsx b/src/components/ui/answer-card.tsx
index d9717802f9..ce1c002abf 100644
--- a/src/components/ui/answer-card.tsx
+++ b/src/components/ui/answer-card.tsx
@@ -4,12 +4,9 @@ import type { ReactNode } from "react";
import { cn, type SourceMetadataInput } from "@/components/ui-primitives";
import type { AnswerState, DegradedAnswerState } from "@/components/ui/answer-state";
-import { DateDisplay } from "@/components/ui/date-display";
-import { MissingValue } from "@/components/ui/missing-value";
-import { Quantity } from "@/components/ui/quantity";
import { RetrievalStateBanner } from "@/components/ui/retrieval-state-banner";
-import { StatusMark } from "@/components/ui/status-mark";
import { VerificationNotice, type VerificationNoticeProps } from "@/components/ui/verification-notice";
+import { AnswerFooter, type AnswerFooterProps } from "@/components/answer/AnswerFooter";
import {
answerClipboardCaveatLine,
answerClipboardProvenanceLine,
@@ -241,217 +238,12 @@ function clipboardSourceLine({ title, locator, href }: ClipboardSourceRef) {
return href?.trim() ? `${label} — ${href.trim()}` : label;
}
-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 };
-
-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;
-};
-
-/**
- * `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";
- 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}
-
-
-
- {row.maximum ? (
-
- {"Max "}
-
-
- ) : null}
- {row.source ? (
- onOpenSource(row.source!.sourceId, row.source!.locator)}
- aria-label={`Open ${[row.source.title, row.source.locator].filter(Boolean).join(", ")}`}
- className="mt-1 inline-flex min-h-tap items-center rounded-md text-xs font-semibold underline underline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
- >
- Open source
-
- ) : null}
-
-
- );
- })}
-
-
- );
-}
-
-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;
- className?: string;
-};
+export {
+ DoseLine,
+ type DoseLineProps,
+ type DoseRow,
+ type DoseSourceRef,
+ type DoseQuantity,
+} from "@/components/ui/dose-line";
-/**
- * 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. The previous version
- * dropped unknown segments to avoid a run of "Unknown" filler, but a silently
- * absent publisher and a recorded one look identical that way — and on a
- * provenance strip the absence *is* the governance signal.
- */
-export function AnswerFooter({ publisher, version, reviewDate, generatedAt, className }: AnswerFooterProps) {
- 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}
-
-
- ))}
-
- );
-}
+export { AnswerFooter, type AnswerFooterProps } from "@/components/answer/AnswerFooter";
diff --git a/src/components/ui/catalogue-toolbar.tsx b/src/components/ui/catalogue-toolbar.tsx
new file mode 100644
index 0000000000..e7877a1934
--- /dev/null
+++ b/src/components/ui/catalogue-toolbar.tsx
@@ -0,0 +1,249 @@
+"use client";
+
+import type { ChangeEvent, ReactNode } from "react";
+import { Filter, Search, X } from "lucide-react";
+
+import { cn } from "@/components/ui-primitives";
+import { TextField } from "@/components/ui/text-field";
+import { Select } from "@/components/ui/select";
+import type { AppliedFilterChip } from "@/components/clinical-dashboard/search-results-header-band";
+
+export type CatalogueToolbarSearchProps = {
+ query: string;
+ onQueryChange: (query: string) => void;
+ placeholder?: string;
+ label?: string;
+ hideLabel?: boolean;
+ disabled?: boolean;
+ className?: string;
+};
+
+export type CatalogueToolbarSortProps = {
+ value: string;
+ onChange: (value: string) => void;
+ options: ReadonlyArray<{ value: string; label: string }>;
+ label?: string;
+ hideLabel?: boolean;
+ disabled?: boolean;
+ className?: string;
+};
+
+export type CatalogueToolbarFilterTriggerProps = {
+ open?: boolean;
+ onToggle?: () => void;
+ activeCount?: number;
+ label?: string;
+ panelId?: string;
+ testId?: string;
+ disabled?: boolean;
+};
+
+export type CatalogueToolbarProps = {
+ /** Search control configuration or custom node. */
+ search?: CatalogueToolbarSearchProps | ReactNode;
+ /** Sort select configuration or custom node. */
+ sort?: CatalogueToolbarSortProps | ReactNode;
+ /** Filter trigger configuration or custom trigger. */
+ filterTrigger?: CatalogueToolbarFilterTriggerProps | ReactNode;
+ /** Active filter chips shown below or alongside the toolbar. */
+ appliedFilters?: readonly AppliedFilterChip[];
+ /** Callback when the user clears all applied filters. */
+ onClearFilters?: () => void;
+ /** Match count / results readout. */
+ matchCount?: number;
+ /** Noun for match count (e.g. "mechanism", "specifier", "differential"). */
+ noun?: string;
+ /** Status of the catalogue search. */
+ status?: "ready" | "loading" | "refetching" | "error" | "unauthorized";
+ /** Additional action items (e.g. Compare, View toggle, Create/Build). */
+ actions?: ReactNode;
+ /** Children rendered in the main controls row or below. */
+ children?: ReactNode;
+ className?: string;
+ testId?: string;
+};
+
+function isSearchProps(search: CatalogueToolbarSearchProps | ReactNode): search is CatalogueToolbarSearchProps {
+ return typeof search === "object" && search !== null && "query" in search && "onQueryChange" in search;
+}
+
+function isSortProps(sort: CatalogueToolbarSortProps | ReactNode): sort is CatalogueToolbarSortProps {
+ return typeof sort === "object" && sort !== null && "value" in sort && "onChange" in sort && "options" in sort;
+}
+
+function isFilterTriggerProps(
+ filter: CatalogueToolbarFilterTriggerProps | ReactNode,
+): filter is CatalogueToolbarFilterTriggerProps {
+ return typeof filter === "object" && filter !== null && ("onToggle" in filter || "activeCount" in filter);
+}
+
+/**
+ * Standardized Catalogue Toolbar (`
`) component.
+ *
+ * Consolidates search, sort, filter triggers, applied filter chips, and action
+ * controls into a single coherent, accessible responsive bar for catalogue surfaces
+ * (Differentials, Formulations, Specifiers, Services, Factsheets).
+ */
+export function CatalogueToolbar({
+ search,
+ sort,
+ filterTrigger,
+ appliedFilters = [],
+ onClearFilters,
+ matchCount,
+ noun = "item",
+ status = "ready",
+ actions,
+ children,
+ className,
+ testId = "catalogue-toolbar",
+}: CatalogueToolbarProps) {
+ const hasAppliedFilters = appliedFilters.length > 0;
+ const pluralNoun = noun.endsWith("s") ? noun : `${noun}s`;
+ const singularNoun = noun.endsWith("s") ? noun.slice(0, -1) : noun;
+ const countLabel =
+ typeof matchCount === "number" ? `${matchCount} ${matchCount === 1 ? singularNoun : pluralNoun}` : null;
+
+ return (
+
+ {/* Primary Toolbar Row */}
+
+
+ {/* Search control */}
+ {search ? (
+
+ {isSearchProps(search) ? (
+ ) => search.onQueryChange(e.target.value)}
+ placeholder={search.placeholder ?? `Search ${pluralNoun}...`}
+ disabled={search.disabled}
+ className={cn("font-semibold", search.className)}
+ />
+ ) : (
+ search
+ )}
+
+ ) : null}
+
+ {/* Sort control */}
+ {sort ? (
+
+ {isSortProps(sort) ? (
+ ) => 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) ? (
+ 0
+ ? "border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"
+ : "border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-heading)] hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)]",
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]",
+ )}
+ >
+
+ {filterTrigger.label ?? "Filter"}
+ {(filterTrigger.activeCount ?? 0) > 0 ? (
+
+ {filterTrigger.activeCount}
+
+ ) : null}
+
+ ) : (
+ 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 ? (
+
+ Clear all
+
+ ) : 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 ? (
+ onOpenSource(row.source!.sourceId, row.source!.locator)}
+ aria-label={`Open ${[row.source.title, row.source.locator].filter(Boolean).join(", ")}`}
+ className="mt-1 inline-flex min-h-tap items-center rounded-md text-xs font-semibold underline underline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
+ >
+ Open 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