From 29b540be5d3204916a35e593794b536fab25fd55 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 14:10:00 +0000 Subject: [PATCH 1/4] feat(filters): adopt the lens contract across the four one-of-N modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Differentials, medication, applications and specifiers each carried a bespoke desktop rail built from aria-pressed toggles while their phone sheet rendered the same dimension as role=radiogroup. The two breakpoints disagreed about whether the dimension was one-of-N; the sheet was right. Each mode now builds ONE option array and hands it to both SegmentedControl and ResultFilterSheet, so the counts cannot drift and the semantics cannot diverge. Also fixed, because both are defects in exactly this surface: - Specifiers' footerNote reported results.length + catalogueMatches.length while its groups narrow only results — the sheet claimed to scope a total its filters could not move. Now reports what it governs, matching the `${list.length} showing` wording every other mode uses. - ResultFilterSheet options with a count announced as "All8": the label and hint spans concatenate and the accessible-name computation normalises the inter-element whitespace away. Same fix SegmentedControl already carries, which is what makes a shared option array announce identically on both surfaces. PR #1848 deferred this to the rollout; this is the rollout. SegmentedControl gains an optional group-level ariaControls so the launcher keeps pointing at #launcher-results-panel — the old rail put aria-controls on each of its six buttons, and converging on the primitive must not drop the association. Dead code removed with its last caller: SpecifierFamilyFilterChips. Its phone-only short label could never render — the band hides filterControls below sm whenever a page also supplies mobileControls, which specifiers does. The scope segment is deliberately NOT included; filter-contract.md section 4 gates it on a catalogue meaningfully larger than the result set, which is services, not these four. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011btGFwWKYFWDs5McQkqz9J --- docs/design-system/COMPONENTS.md | 2 +- docs/design-system/adoption-manifest.json | 18 +++- docs/outstanding-issues.md | 2 +- src/components/applications-launcher-page.tsx | 38 +++---- .../clinical-dashboard/differentials-home.tsx | 98 ++++--------------- .../medication-prescribing-workspace.tsx | 75 +++++--------- .../result-filter-control.tsx | 10 ++ src/components/specifiers/specifier-ui.tsx | 42 +------- .../specifiers/specifiers-home-page.tsx | 26 ++++- src/components/ui/segmented-control.tsx | 10 ++ tests/favourites-auth-gate.dom.test.tsx | 5 +- ...ication-prescribing-workspace.dom.test.tsx | 14 +-- tests/search-results-header-band.dom.test.tsx | 42 ++++++++ tests/ui-v2-components.dom.test.tsx | 35 +++++++ 14 files changed, 207 insertions(+), 210 deletions(-) diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index 09125e6203..ff30133014 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -991,7 +991,7 @@ This generated snapshot is a local source-derived inventory. It does not assert | `RetrievalStateBanner` | answer | yes | yes | inherited-global-root | yes | no | 1 | | `SafeBoldText` | layout | yes | yes | inherited-global-root | yes | no | 8 | | `SearchField` | controls | yes | yes | no | yes | no | 0 | -| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 3 | +| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 7 | | `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 | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 0e8b0a3bd0..2c99bad861 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -1377,14 +1377,22 @@ "sourceExported": true, "entryExported": true, "directImportFiles": [ + "src/components/applications-launcher-page.tsx", + "src/components/clinical-dashboard/differentials-home.tsx", + "src/components/clinical-dashboard/medication-prescribing-workspace.tsx", "src/components/clinical-dashboard/patient-profile-panel.tsx", "src/components/clinical-dashboard/settings-dialog.tsx", - "src/components/in-page-nav/in-page-nav-header.tsx" + "src/components/in-page-nav/in-page-nav-header.tsx", + "src/components/specifiers/specifiers-home-page.tsx" ], "productImportFiles": [ + "src/components/applications-launcher-page.tsx", + "src/components/clinical-dashboard/differentials-home.tsx", + "src/components/clinical-dashboard/medication-prescribing-workspace.tsx", "src/components/clinical-dashboard/patient-profile-panel.tsx", "src/components/clinical-dashboard/settings-dialog.tsx", - "src/components/in-page-nav/in-page-nav-header.tsx" + "src/components/in-page-nav/in-page-nav-header.tsx", + "src/components/specifiers/specifiers-home-page.tsx" ], "designSync": { "listedInSourceMap": true, @@ -1392,7 +1400,11 @@ "preview": ".design-sync/previews/SegmentedControl.tsx", "previewValid": true }, - "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"], + "testFiles": [ + "tests/design-sync-visual-exports.test.ts", + "tests/search-results-header-band.dom.test.tsx", + "tests/ui-v2-components.dom.test.tsx" + ], "baseline": { "targetLayer": "v2", "liveLayer": "v2", diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 3d8c5b7cff..d81215b3ba 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -198,7 +198,7 @@ removed after current-main verification; it is not missing recommended work. | #165 | P2 | task | Adopt a consolidated answer-home notice block — the studies exist, nothing adopts them | **Outcome:** the answer hero states its safety obligation, its scope, and its verification requirement as one block in one voice. **Detail:** `/mockups/warning-consolidation` (PR #1437) diagnoses today's three stacked notices — the APP-5 privacy warning at 11px muted, a bare `/privacy` link, and an accent-blue `ShieldCheck` capability claim at 14px semibold — and shows the hierarchy is inverted: the least important line is the loudest, and two shields with opposite meanings sit ~40px apart. Three consolidations are drawn at 1440px and 390px. Recommended: **02 Safety card** on the hero (obligation on a warning-tinted top row, everything descriptive in one grey voice below) and **01 Assurance bar** on the docked composer — the same content model at two densities, so one component with a `density` prop covers both. **This is a governance change, not just a design one:** `PrivacyInputNotice` is the single site-wide APP-5 line and renders on the answer, documents and calculators composers, so all three move together; `tests/privacy-ui.test.ts`, `tests/ui-accessibility.spec.ts` and the phone-chrome reserve coverage all assert against the current markup and must change in the same commit; and the PR will need a full `## Clinical Governance Preflight` (the mockup PR correctly did not). **Third study (before/after):** `/mockups/answer-home-proposal` draws the concrete D-direction proposal as a full hero before/after rather than an isolated notice. **Second study (words only):** `/mockups/warning-line` answers a narrower brief — no icon, border, tint or background, one line where width allows. Six variants A-F; line counts measured from the rendered DOM, not asserted. Only B (middot clauses), D (obligation + verify) and F (compressed obligation) hold one line at desktop width, and **none fit one line on a 390px phone while the pinned APP-5 sentence stays verbatim** — 46 characters of obligation plus the 27-character link exceeds the ~60 available at 11px. Recommended there: **D**, the only compliant variant that is both one line and keeps weight-only hierarchy, reached by dropping the scope claim (a capability statement already visible on the answer itself). F fits best but rewrites the pinned obligation to \|No patient-identifiable information.\| and so needs the same privacy sign-off as `#166` plus a matching `tests/privacy-ui.test.ts` update. **Status:** PR #1437 was closed unmerged on 2026-07-30 as a deliberate pause during an owner-authorized ordered merge sweep, to be reopened at its queued place; branch `claude/warning-consolidation-mockups-09jyj7` is preserved and merged onto current `main`; these follow-up rows have been renumbered on each sync because `main` kept claiming the next ids while the PR was paused; the superseded numbers are deliberately not listed, since they now belong to unrelated rows. **Next:** decide block (02 + 01) versus line (D) direction, get wording sign-off for `#166`, then implement behind one component and run `verify:phone-chrome` before `verify:ui`. | session 2026-07-30; PR #1437; `/mockups/warning-consolidation`; `/mockups/warning-line` | 2026-07-30 | | #168 | P2 | rec | Sequential issue ids force every concurrent append to conflict | **Outcome:** two sessions can append to this ledger at the same time without conflicting. **Detail:** ids are allocated read-modify-write against the `issues:next-id` marker inside the file being edited, so two branches both read N and both write N. Because duplicate ids are unacceptable, a union merge driver is unsafe — .gitattributes says so explicitly — which is why this file deliberately has no driver and every overlapping append conflicts by hand. Manual resolution is where rows get dropped: PR #1490 was closed during one and took the only record of four snapshots with it (#152), and ids were renumbered under in-flight work three times in one session (#154, #155). The new writer (`scripts/outstanding-issues.mjs`) removes the mechanical errors but explicitly not this one. **Next:** replace the counter with a collision-free id (ULID, timestamp+suffix, or a content hash), keeping a short display number derived at render time if `#151` reads better than 01JQ…; then a union driver becomes safe to reinstate and concurrent appends stop conflicting at all. A larger variant is one row per file under `docs/issues` with the table generated, which the repo already does for `site-map.md`. **Stop:** do not reinstate `merge=union` while ids are sequential — that combination was tried in PR #1416 and removed for duplicating rows and the marker. Renumbered from this PR's original `#159` because `main` already used `#159` for the duplicated test-file-list finding. | session 2026-07-31; .gitattributes; #154/#155; PR #1524 sync | 2026-07-31 | | #169 | P2 | issue | Local branches carry work that exists on no remote | **Outcome:** committed work is not lost when a machine or worktree is reclaimed. **Detail 2026-07-31:** six `claude/*` branches in this checkout have commits and no `origin/` counterpart. Verified real for `claude/clinical-kb-design-system-333a69` — 57 files / +4069, tip `feat(design-system): v2 token layer, 26 components, browser-crash fix` dated 2026-07-31 17:40, whose added `.design-sync/previews/*.tsx` files are absent from main. Others unverified: `design-sync-db0a54`, `fable-implementation-fc937c`, `frosty-mayer-2c6167`, `issues-133-evidence`. **How to check, because the obvious measure lies:** `git rev-list --count origin/main..` and a three-dot diff both report landed work as unmerged, since this repo squash-merges and the original commits never become ancestors — my own merged branch reported 1 commit and +476 by that measure. Test instead whether files the branch adds exist on main (`git ls-tree origin/main `). **Next:** per branch, push it for review or confirm it is superseded and delete it; do not bulk-delete on the commit count. Sibling of #152, which covers uncommitted work in worktrees rather than unpushed commits on branches. NOT VERIFIABLE FROM A CLOUD SESSION (checked 2026-08-12): this row describes machine-local git or process state. Claude Code on the web runs in a container cloned fresh at session start, so it reports 1 worktree, 0 upstream-less branches and no running dev servers no matter what is true on the machine that raised this. Do NOT close this row from a cloud session — that reading is an artifact of the container, not evidence. Verify from the originating machine. | session 2026-07-31; local branch audit | 2026-07-31 | -| #170 | P2 | task | Phone filter sheets exist for documents and therapy; shared-band adoption remains | **Outcome:** phone filtering opens the repo's own bottom sheet instead of competing for width in the utilities rail. **Current state (2026-08-04):** documents already mounts a Filter documents Sheet, and therapy-compass mounts TherapyFilterSheet; both also pass appliedFilters into SearchResultsHeaderBand. The remaining modes still keep filter controls inline (or page-owned chips/nav), and the shared band has not yet adopted a Filter trigger + Sheet that wraps mobileControls for every mode. **Detail:** SearchResultsHeaderBand still renders mobileControls inline below sm for pages that supply them that way. src/components/ui/sheet.tsx already provides the primitive — bottom sheet on mobile via sheet-up, centred dialog at sm+, safe-area aware, focus-trapped, Escape and backdrop dismiss — so shared-band adoption is the remaining work, not new UI. Design settled in the round-7 study: one Filter control at both widths, badge counting applied filters, sheet titled Filter and sort because a phone bar cannot fit a labelled Sort beside Filter at 390 px. **Next:** replace the inline mobileControls render with a Filter trigger + Sheet containing mobileControls ?? filterControls for the modes that still lack a page-owned sheet, without regressing documents/therapy sheets or removing Sort from sheetless Sort consumers (differentials, forms, services). **Stop:** at least four Playwright specs drive those inline controls directly (document-source-type-select, search-query-ribbon-mobile-control-pair); budget for updating them, and do not ship without verify:ui — PR #1523 showed Production UI critical does gate this surface properly. | Round-7 design study; PR #1523 notes; `src/components/ui/sheet.tsx` | 2026-07-31 | +| #170 | P2 | task | Phone filter sheets exist for documents and therapy; shared-band adoption remains | **Outcome:** phone filtering opens the repo's own bottom sheet instead of competing for width in the utilities rail, and both breakpoints agree on what each dimension means. **Current state (2026-08-12):** the shared contract landed (PR #1847, docs/filter-contract.md) discriminating ResultFilterGroup into lens (one-of-N) and facet (many-of-N); SegmentedControl gained an option hint slot (PR #1848); and the four lens modes now adopt it — differentials, medication, applications and specifiers each replaced a bespoke aria-pressed desktop rail with SegmentedControl and build ONE option array shared by rail and sheet. That closes the defect where the same dimension claimed one-of-N in the sheet (role=radiogroup) and many-of-N on the rail (aria-pressed). Specifiers' footerNote also fixed: it reported results.length + catalogueMatches.length while its groups narrow only results. **Remaining:** formulation (derive domains, convert to facet, evict the query-replacing pattern presets), services (facet index + selection state + URL + the scope segment), factsheets (evict category presets to the composer), therapy-compass (converge onto the shared trigger/sheet; its onClearAll still wipes the query, which the contract forbids), documents last (port find-a-filter + collapse up as the >20 tier). **Stop:** the scope segment is deliberately NOT retrofitted to the four lens modes yet — filter-contract.md section 4 gates it on a catalogue meaningfully larger than the result set, and it lands with services where that is true. Do not ship a mode conversion without browser proof at 390px and 320px; these change rendered output, unlike #1847. | Round-7 design study; PR #1523 notes; `src/components/ui/sheet.tsx` | 2026-07-31 | | #175 | P2 | task | Therapy modality is now null on all 205 records and needs curation or removal | VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: public/therapy-compass-data/therapies-index.json holds 205 records and 0 carry a modality value, exactly as described. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. **Outcome:** the Therapy detail and recommend screens either show a curated modality or stop carrying the field at all. **Detail:** the source catalogue derived `modality` from each record's own tag list — all 205 records had one, every value was also present in that record's `tags`, and the whole catalogue collapsed to CBT/ACT/DBT. It mislabelled the treatments it could not describe: ECT and rTMS as "ACT", Psychoanalysis and Psychodynamic Psychotherapy as "CBT", MBT and TFP as "DBT". Pre-existing on main, surfaced by the PR #1489 review. The generator emits it only when the source curates a value that is not already a tag, which today means null for 205/205 on the index projections *and* the full catalogue the detail/recommend screens load (`catalogue: "full"`), so the two chips (`detail-screen.tsx:49`, `recommend-screen.tsx:115`) never render and `select.ts:117` contributes no same-modality point. Removal was provably search-neutral: `src/lib/therapies.ts` scores with boolean `haystack.includes(token)`, not term frequency, and every modality value was already contributed by `tags.join(" ")` in the same haystack. **Next:** one of two — curate real modality values in `public/therapy-compass-data/therapies.json` (clinical work, needs the psychiatrist), or drop the field from `types.ts`, `src/lib/therapies.ts`, the two chips and `select.ts`. **Stop:** do not reinstate the tag-derived value to make the chips reappear; a guess rendered as curated fact is the defect. `tests/therapy-compass-pathways.test.ts` pins the echo invariant on both the index and the full catalogue asset. Renumbered from this PR's original `#169` because `main` claimed `#169`–`#174` while the branch was open. | PR #1489 review remediation; PR #1532; session 2026-07-31 | 2026-07-31 | | #177 | P3 | rec | Therapy catalogue aliases duplicate 2.53 MB of bytes instead of pointing at the hashed file | VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: public/therapy-compass-data/ holds THREE copies of the catalogue (therapies.json plus two hashed assets), which is the grace-generation case this row predicted. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. **Outcome:** the unversioned catalogue aliases stop costing a second copy of every payload in the repo and the image. **Detail:** `public/therapy-compass-data/` ships each catalogue twice — `therapies.json` (2,529,429 bytes) alongside `therapies..json` with identical content, and the same for the index and home projections. Git stores one blob for each identical pair, so history is unaffected, but the working tree and every Docker image carry both. PR #1489's remediation adds a one-deploy grace generation, so a stem whose content changed now holds three copies until the next regeneration. **Next:** serve the alias as a Next rewrite to the current hashed filename instead of writing a duplicate file — the alias would keep its `max-age=0, must-revalidate` header (the hashed target keeps `immutable`, so the rewrite must not inherit it) and the generator would stop copying bytes. Check that a rewrite still satisfies the alias-equality assertions in `check:therapy-data-index` and `tests/therapy-compass-mode-wiring.test.ts`, which currently compare file bytes. **Stop:** do not drop the aliases outright — they are the fallback `useTherapyData` uses when a pre-deploy bundle names a hash that no longer exists. Renumbered from this PR's original `#171`. | PR #1489 review remediation; PR #1532; session 2026-07-31 | 2026-07-31 | | #178 | P2 | rec | pr-policy does not flag operational risk bundled with clinical or UI risk | IN FLIGHT 2026-08-12 in PR #1837 (harden verification & PR policy guards). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. **Outcome:** a PR that mixes operational-risk paths with clinical or UI risk is called out before it merges, because squash-merging that mix destroys per-item revert. **Detail:** `classifyPullRequestFiles` already computes `operationalRisk`, `clinicalRisk`, `ragRanking` and `ui` independently, but nothing reacts to the combination. AGENTS.md's "PR bundling" section forbids bundling anything once `operationalRisk` is true; the classifier is where that could be enforced. PR #1489 is the worked example: 33 files spanning `.github/workflows/ci.yml`, both Dockerfiles, a rewrite of the bundle-budget gate, a phone-chrome scroll change and a therapy data restructure, merged as one squash (945148251). Reverting any single item now means hand-reverting hunks out of the squash commit, because the branch commits are unreachable. The remediation PR for that review repeats the pattern on a smaller scale (clinical data plus a one-line ci.yml timeout), which is why this is a recommendation rather than a hard gate — the right severity is probably a warning that names the mixed classes, not a merge block. **Next:** emit an advisory line from `evaluatePullRequestPolicy` when `operationalRisk` coincides with `clinicalRisk` or `ui`, listing which paths drove each; decide separately whether it ever blocks. Cover it in the `--self-test` block. **Stop:** do not make it a hard failure in the same change that introduces it — land the signal first and see how often it fires. Renumbered from this PR's original `#172`. | PR #1489 review remediation; PR #1532; session 2026-07-31 | 2026-07-31 | diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index 8866d552d1..ef5c727899 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -34,6 +34,7 @@ import { } from "@/components/clinical-dashboard/result-filter-control"; import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context"; import { useFavouritesAccess } from "@/components/clinical-dashboard/use-favourites-access"; +import { SegmentedControl } from "@/components/ui/segmented-control"; import { cn, EmptyState } from "@/components/ui-primitives"; import { Chip, type ChipStatusTone } from "@/components/ui/chip"; import { Sheet } from "@/components/ui/sheet"; @@ -390,31 +391,20 @@ function FilterTabs({ const [filterOpen, setFilterOpen] = useState(false); // `more` is the desktop "All" tab's overflow state, not a category of its own. const resolvedFilter: LauncherFilter = activeFilter === "more" ? "all" : activeFilter; + // One array for the desktop rail and the phone sheet. The categories partition + // the tool list, so this is a lens at both breakpoints — the rail used to say + // many-of-N with `aria-pressed` while the sheet said one-of-N. + const launcherFilterOptions = desktopFilters.map((filter) => ({ value: filter.id, label: filter.label })); return ( <> -
- {desktopFilters.map((filter) => { - const active = filter.id === activeFilter || (filter.id === "all" && activeFilter === "more"); - return ( - - ); - })} +
+
{/* The phone half of the same control. This launcher renders no results band, so the trigger sits inline here rather than in a ribbon slot — @@ -440,7 +430,7 @@ function FilterTabs({ id: "category", label: "Category", value: resolvedFilter, - options: desktopFilters.map((filter) => ({ value: filter.id, label: filter.label })), + options: launcherFilterOptions, onChange: (value) => { setFilterOpen(false); onFilterChange(value); diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index 0f07fb8b07..d7a23e44c3 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -28,8 +28,10 @@ import { SearchResultsHeaderBand } from "@/components/clinical-dashboard/search- import { ResultFilterSheet, ResultFilterTrigger, + type ResultFilterOption, resultFilterGroup, } from "@/components/clinical-dashboard/result-filter-control"; +import { SegmentedControl } from "@/components/ui/segmented-control"; import { UniversalSearchAlsoMatches } from "@/components/clinical-dashboard/universal-search-also-matches"; import { useDifferentialSearch } from "@/components/clinical-dashboard/use-differential-catalog"; import { useResultSort } from "@/components/use-result-sort"; @@ -251,73 +253,6 @@ function StatusBadge({ status, className }: { status: DifferentialRecord["status type KindFilter = "all" | "presentation" | "diagnosis"; -const resultTypeTabFocusRing = - "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; - -function ResultTypeTabs({ - activeFilter, - onFilterChange, - allCount, - presentationCount, - diagnosisCount, -}: { - activeFilter: KindFilter; - onFilterChange: (filter: KindFilter) => void; - allCount: number; - presentationCount: number; - diagnosisCount: number; -}) { - const tabs = [ - { id: "all" as const, label: "All", count: allCount }, - { id: "presentation" as const, label: "Presentations", count: presentationCount }, - { id: "diagnosis" as const, label: "Diagnoses", count: diagnosisCount }, - ]; - - // Single-select filters over one results list — modeled as a toggle group - // (role="group" + aria-pressed), not ARIA tabs (which would need tabpanels, - // aria-controls, and roving tabindex for content that does not exist here). - return ( -
- {tabs.map((tab) => { - const active = activeFilter === tab.id; - return ( - - ); - })} -
- ); -} - function MatchBadge({ label }: { label: string }) { // Match quality is a relevance signal, not a safety one — keep it in the // accent family so red stays reserved for the emergent status badges. @@ -870,6 +805,18 @@ function SearchResultsView({ const presentationCount = results.filter((result) => result.kind === "presentation").length; const diagnosisCount = results.length - presentationCount; + // One array feeds both breakpoints. The counts used to be declared twice — + // as `count` on the desktop rail and again as `hint` in the sheet — which is + // how the two could drift, and how the same dimension came to claim one-of-N + // in the sheet (`role="radiogroup"`) and many-of-N on the rail (`aria-pressed`). + const kindFilterOptions = useMemo>>( + () => [ + { value: "all", label: "All", hint: String(results.length) }, + { value: "presentation", label: "Presentations", hint: String(presentationCount) }, + { value: "diagnosis", label: "Diagnoses", hint: String(diagnosisCount) }, + ], + [diagnosisCount, presentationCount, results.length], + ); const visibleResults = useMemo( () => sortResultItems( @@ -1009,12 +956,11 @@ function SearchResultsView({ /> } filterControls={ - } /> @@ -1031,11 +977,7 @@ function SearchResultsView({ id: "result-type", label: "Show", value: kindFilter, - options: [ - { value: "all", label: "All", hint: String(results.length) }, - { value: "presentation", label: "Presentations", hint: String(presentationCount) }, - { value: "diagnosis", label: "Diagnoses", hint: String(diagnosisCount) }, - ], + options: kindFilterOptions, onChange: setKindFilter, }), ]} diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx index d465a5e6e0..eb6e8d0c27 100644 --- a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx +++ b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx @@ -42,6 +42,7 @@ import { import { medicationIdentityBadges, type MedicationRecord } from "@/lib/medications"; import { SEMANTIC_TONE_META } from "@/lib/semantic-tone"; import { isDeployedClinicalKb } from "@/lib/deployed-app"; +import { SegmentedControl, type SegmentedControlOption } from "@/components/ui/segmented-control"; import { cn, EmptyState, pageContainer } from "@/components/ui-primitives"; type MedicationPrescribingWorkspaceProps = { @@ -262,50 +263,6 @@ function resultMatchesFilter(result: MedicationResult, filter: MedicationResultF ); } -function FilterStrip({ - activeFilter, - counts, - onFilterChange, -}: { - activeFilter: MedicationResultFilter; - counts: Record; - onFilterChange: (filter: MedicationResultFilter) => void; -}) { - return ( -
- {medicationResultFilters.map((filter) => { - const active = activeFilter === filter.id; - const Icon = filter.icon; - return ( - - ); - })} -
- ); -} - function ResultToneIcon({ result, accent }: { result: MedicationResult; accent?: string }) { const tone = result.tone === "teal" ? "teal" : result.tone === "blue" ? "blue" : "slate"; if (accent) { @@ -455,6 +412,18 @@ function MedicationResults({ }; }, [activeFilter, catalog.data, profile, profileEmpty]); const resultCount = rows.length; + // One array feeds the desktop rail and the phone sheet, so the counts cannot + // drift between them and the dimension states one-of-N at both breakpoints. + const filterOptions = useMemo>>( + () => + medicationResultFilters.map((filter) => ({ + value: filter.id, + label: filter.label, + icon: filter.icon, + hint: String(counts[filter.id]), + })), + [counts], + ); // The match-quality badge only earns its slot when it differentiates: hide it on // "Exact clinical fit" rows when every visible row says the same thing. const showMatchBadge = useMemo(() => new Set(rows.map((row) => row.result.match)).size > 1, [rows]); @@ -485,7 +454,17 @@ function MedicationResults({ onToggle={() => setFilterOpen((current) => !current)} /> } - filterControls={} + filterControls={ + + } /> {/* Phone-only by construction: the trigger that opens it lives in the ribbon's `mobileControls` slot, which the band hides from `sm` up. */} @@ -500,11 +479,7 @@ function MedicationResults({ id: "result-filter", label: "Show", value: activeFilter, - options: medicationResultFilters.map((filter) => ({ - value: filter.id, - label: filter.label, - hint: String(counts[filter.id]), - })), + options: filterOptions, onChange: setActiveFilter, }), ]} diff --git a/src/components/clinical-dashboard/result-filter-control.tsx b/src/components/clinical-dashboard/result-filter-control.tsx index bd47b194fd..edca5f8472 100644 --- a/src/components/clinical-dashboard/result-filter-control.tsx +++ b/src/components/clinical-dashboard/result-filter-control.tsx @@ -334,6 +334,13 @@ function FilterRadioGroup({ group, panelId }: { group: ResultFilterLensGroup; pa type="button" role="radio" aria-checked={selected} + // Without this the label and hint spans concatenate to "All8" in + // the accessible name — the name computation normalises the + // inter-element whitespace away, so a text-node separator cannot + // fix it. Same defect and same fix as SegmentedControl, which is + // what lets one shared option array announce identically on the + // desktop rail and in this sheet. + aria-label={option.hint ? `${option.label} (${option.hint})` : undefined} // `aria-disabled` rather than `disabled`: a real disabled // button leaves the tab order, so a keyboard or screen-reader // user loses the option entirely and never learns why. Kept @@ -435,6 +442,9 @@ function FilterFacetGroup({ group, panelId }: { group: ResultFilterFacetGroup; p aria-pressed={selected} aria-disabled={deadEnd || undefined} aria-describedby={deadEnd ? deadEndDescId : undefined} + // Same concatenation defect as the lens chips above: a facet + // count would otherwise be announced as "Crisis12". + aria-label={option.hint ? `${option.label} (${option.hint})` : undefined} onClick={() => { if (deadEnd) return; group.onToggle(option.value); diff --git a/src/components/specifiers/specifier-ui.tsx b/src/components/specifiers/specifier-ui.tsx index aaee82be9e..9610552f8b 100644 --- a/src/components/specifiers/specifier-ui.tsx +++ b/src/components/specifiers/specifier-ui.tsx @@ -4,8 +4,7 @@ import { ArrowRight, CheckCircle2, ChevronsUpDown, Info, Minus, ShieldAlert, Tag import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; import { cn, eyebrowText } from "@/components/ui-primitives"; -import type { SpecifierFamily, SpecifierRecord } from "@/lib/specifiers"; -import { specifierFamilies } from "@/lib/specifiers"; +import type { SpecifierRecord } from "@/lib/specifiers"; import type { SpecifierSourceStatus } from "@/lib/specifiers-search-index"; export const specifierCard = @@ -53,45 +52,6 @@ export function SpecifierWordingPathway() { ); } -const familyChipBase = - "inline-flex min-h-tap shrink-0 items-center rounded-lg border px-3 text-xs font-semibold transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:text-sm"; -const familyChipActive = - "border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"; -const familyChipIdle = - "border-[color:var(--border)] bg-[color:var(--surface-raised)] text-[color:var(--text-muted)] hover:border-[color:var(--border-strong)] hover:text-[color:var(--text-heading)]"; - -export function SpecifierFamilyFilterChips({ - value, - onChange, -}: { - value: "all" | SpecifierFamily; - onChange: (value: "all" | SpecifierFamily) => void; -}) { - return ( -
- {specifierFamilies.map((option) => { - const active = value === option.id; - return ( - - ); - })} -
- ); -} - export function SpecifierDiagnosisFilter({ value, onChange, diff --git a/src/components/specifiers/specifiers-home-page.tsx b/src/components/specifiers/specifiers-home-page.tsx index b38a056eb4..0d2637a840 100644 --- a/src/components/specifiers/specifiers-home-page.tsx +++ b/src/components/specifiers/specifiers-home-page.tsx @@ -16,12 +16,12 @@ import { CategoryTag, ReviewStatusBadge, SpecifierDiagnosisFilter, - SpecifierFamilyFilterChips, SpecifierMatchCard, SpecifierPageShell, SpecifierSafetyNote, specifierCard, } from "@/components/specifiers/specifier-ui"; +import { SegmentedControl } from "@/components/ui/segmented-control"; import { cn } from "@/components/ui-primitives"; import { appModeHomeHref } from "@/lib/app-modes"; import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; @@ -197,6 +197,14 @@ function SpecifierResults({ query }: { query: string }) { // shows "0 matches" with an empty-state banner above real results. const catalogueMatches = useMemo(() => searchSpecifierCatalog(query).slice(0, CATALOGUE_RESULT_LIMIT), [query]); const totalMatches = results.length + catalogueMatches.length; + // One array for the desktop rail and the phone sheet. The full label is safe + // at both: the band hides `filterControls` below `sm` whenever a page also + // supplies `mobileControls`, so the chips' phone-only short label could never + // render and the two breakpoints never showed different text. + const familyOptions = useMemo( + () => specifierFamilies.map((option) => ({ value: option.id, label: option.label })), + [], + ); return ( @@ -221,7 +229,13 @@ function SpecifierResults({ query }: { query: string }) { } filterControls={
- +
} @@ -241,7 +255,7 @@ function SpecifierResults({ query }: { query: string }) { id: "family", label: "Family", value: family, - options: specifierFamilies.map((option) => ({ value: option.id, label: option.shortLabel })), + options: familyOptions, onChange: setFamily, }), resultFilterGroup({ @@ -260,7 +274,11 @@ function SpecifierResults({ query }: { query: string }) { setDiagnosis(""); } } - footerNote={`${totalMatches} showing`} + // `results`, not `totalMatches`. Both groups narrow only the curated + // list; `catalogueMatches` keys on the query alone, so reporting the sum + // claimed the sheet scoped a total its filters could not move — the + // count sat still while a filter visibly changed the list. + footerNote={`${results.length} showing`} /> {totalMatches === 0 ? ( diff --git a/src/components/ui/segmented-control.tsx b/src/components/ui/segmented-control.tsx index 2454f58de1..706da0ed99 100644 --- a/src/components/ui/segmented-control.tsx +++ b/src/components/ui/segmented-control.tsx @@ -31,6 +31,14 @@ export type SegmentedControlProps = AccessibleName & { onChange: (value: T) => void; options: ReadonlyArray>; ariaDescribedBy?: string; + /** + * The region this rail filters. Set it when the control governs a panel + * elsewhere in the page — the launcher's category rail pointed each of its + * buttons at `#launcher-results-panel`, and converging on this primitive must + * not silently drop that association. One group-level reference, because the + * radiogroup is the control; the individual radios are its options. + */ + ariaControls?: string; layout?: "fit" | "equal"; className?: string; }; @@ -42,6 +50,7 @@ export function SegmentedControl({ label, ariaLabelledBy, ariaDescribedBy, + ariaControls, layout = "fit", className, }: SegmentedControlProps) { @@ -91,6 +100,7 @@ export function SegmentedControl({ aria-label={label} aria-labelledby={ariaLabelledBy} aria-describedby={ariaDescribedBy} + aria-controls={ariaControls} onKeyDown={onKeyDown} data-layout={layout} className={cn( diff --git a/tests/favourites-auth-gate.dom.test.tsx b/tests/favourites-auth-gate.dom.test.tsx index e5be94c0ee..0b34deab21 100644 --- a/tests/favourites-auth-gate.dom.test.tsx +++ b/tests/favourites-auth-gate.dom.test.tsx @@ -211,7 +211,8 @@ describe("favourites auth gate DOM", () => { authSession.status = "signed_out"; render(); - expect(screen.queryByRole("button", { name: "Saved" })).toBeNull(); + // The category rail is a lens: its options are radios, not buttons. + expect(screen.queryByRole("radio", { name: "Saved" })).toBeNull(); expect(screen.queryByTestId("tool-shortcut-favourites")).toBeNull(); expect(screen.queryByText("Saved workflows")).toBeNull(); expect(screen.getByTestId("tools-hub")).toBeVisible(); @@ -220,7 +221,7 @@ describe("favourites auth gate DOM", () => { it("keeps Tools Saved workflows available when Favourites access is granted", () => { render(); - expect(screen.getByRole("button", { name: "Saved" })).toBeVisible(); + expect(screen.getByRole("radio", { name: "Saved" })).toBeVisible(); expect(screen.getByTestId("tool-shortcut-favourites")).toBeVisible(); expect( toolCatalogRecordsForSession({ authenticated: true, demoMode: false }).some((t) => t.id === "favourites"), diff --git a/tests/medication-prescribing-workspace.dom.test.tsx b/tests/medication-prescribing-workspace.dom.test.tsx index 66b01ccb2a..0fbb69004c 100644 --- a/tests/medication-prescribing-workspace.dom.test.tsx +++ b/tests/medication-prescribing-workspace.dom.test.tsx @@ -107,8 +107,10 @@ function rowVisible(name: string): boolean { return screen.queryAllByText(name).length > 0; } +// The rail is a one-of-N lens, so its options are radios in a radiogroup, not +// pressed toggles. The accessible name carries the count as "Best (3)". function filterButton(label: string): HTMLElement { - return screen.getByRole("button", { name: new RegExp(`^${label}`, "i") }); + return screen.getByRole("radio", { name: new RegExp(`^${label}`, "i") }); } afterEach(() => { @@ -142,8 +144,8 @@ describe("MedicationPrescribingWorkspace — result filter strip", () => { it("defaults to the Best lens with every result shown", () => { renderWorkspace(); - expect(filterButton("Best")).toHaveAttribute("aria-pressed", "true"); - expect(filterButton("Safety")).toHaveAttribute("aria-pressed", "false"); + expect(filterButton("Best")).toHaveAttribute("aria-checked", "true"); + expect(filterButton("Safety")).toHaveAttribute("aria-checked", "false"); expect(rowVisible("Clozapine")).toBe(true); expect(rowVisible("Lithium")).toBe(true); expect(rowVisible("Sertraline")).toBe(true); @@ -153,8 +155,8 @@ describe("MedicationPrescribingWorkspace — result filter strip", () => { renderWorkspace(); fireEvent.click(filterButton("Indication")); - expect(filterButton("Indication")).toHaveAttribute("aria-pressed", "true"); - expect(filterButton("Best")).toHaveAttribute("aria-pressed", "false"); + expect(filterButton("Indication")).toHaveAttribute("aria-checked", "true"); + expect(filterButton("Best")).toHaveAttribute("aria-checked", "false"); expect(rowVisible("Clozapine")).toBe(true); expect(rowVisible("Lithium")).toBe(true); // Sertraline is a "Related match", so it leaves the Indication lens. @@ -165,7 +167,7 @@ describe("MedicationPrescribingWorkspace — result filter strip", () => { renderWorkspace(); fireEvent.click(filterButton("Monitor")); - expect(filterButton("Monitor")).toHaveAttribute("aria-pressed", "true"); + expect(filterButton("Monitor")).toHaveAttribute("aria-checked", "true"); expect(rowVisible("Lithium")).toBe(true); expect(rowVisible("Clozapine")).toBe(false); expect(rowVisible("Sertraline")).toBe(false); diff --git a/tests/search-results-header-band.dom.test.tsx b/tests/search-results-header-band.dom.test.tsx index 58109eb92d..8041753518 100644 --- a/tests/search-results-header-band.dom.test.tsx +++ b/tests/search-results-header-band.dom.test.tsx @@ -587,6 +587,48 @@ describe("ResultFilterSheet facet groups", () => { expect(onToggle).toHaveBeenCalledWith("Affect"); }); + // The label and hint spans concatenate in the accessible name, and the name + // computation normalises inter-element whitespace away, so a text-node + // separator does not fix it. Both group kinds carry counts, so both need the + // explicit name — and it must match SegmentedControl's, because a mode now + // hands one option array to the desktop rail and this sheet. + it("names a counted option 'label (hint)' rather than concatenating to 'All8'", () => { + render( + (), + options: [{ value: "Crisis", label: "Crisis", hint: "12" }], + onToggle: vi.fn(), + }), + ]} + />, + ); + + expect(screen.getByRole("radio", { name: "All (8)" })).toBeInTheDocument(); + expect(screen.getByRole("radio", { name: "Presentations (1)" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Crisis (12)" })).toBeInTheDocument(); + // A hintless option keeps its plain label — every pre-count call site. + expect(screen.queryByRole("radio", { name: "All8" })).not.toBeInTheDocument(); + }); + // Every toggle must be individually reachable. The lens groups' single tab // stop is correct there because arrowing replaces; here it would strand // options behind a key binding that must not commit anything. diff --git a/tests/ui-v2-components.dom.test.tsx b/tests/ui-v2-components.dom.test.tsx index 1fa5c48afd..bb00fd9fa1 100644 --- a/tests/ui-v2-components.dom.test.tsx +++ b/tests/ui-v2-components.dom.test.tsx @@ -409,6 +409,41 @@ describe("SegmentedControl", () => { expect(screen.getByRole("radio", { name: "All" })).toBeInTheDocument(); }); + it("points the group, not each radio, at the region it filters", () => { + render( + undefined} + options={[ + { value: "all", label: "All" }, + { value: "assess", label: "Assess" }, + ]} + ariaControls="launcher-results-panel" + />, + ); + + expect(screen.getByRole("radiogroup")).toHaveAttribute("aria-controls", "launcher-results-panel"); + // The radios are options of the control, not separate controllers of the + // panel — the launcher's old rail put aria-controls on all six buttons. + for (const radio of screen.getAllByRole("radio")) { + expect(radio).not.toHaveAttribute("aria-controls"); + } + }); + + it("omits aria-controls entirely when the rail governs no separate region", () => { + render( + undefined} + options={[{ value: "all", label: "All" }]} + />, + ); + + expect(screen.getByRole("radiogroup")).not.toHaveAttribute("aria-controls"); + }); + it("keeps a controlled disabled value checked instead of remapping to the first enabled option", () => { render( Date: Wed, 12 Aug 2026 14:14:50 +0000 Subject: [PATCH 2/4] chore(design-sync): regenerate the SegmentedControl props pin for ariaControls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1848 concluded no .design-sync regeneration was needed, and that was correct for that change: `hint` went inside SegmentedControlOption, which dtsPropsFor references BY NAME rather than expanding, so the pinned string was untouched. `ariaControls` is the other case — it sits on SegmentedControlProps itself, which the pin does expand, so the string genuinely changed and both check-design-sync-contract and design-sync-visual-exports went red. Regenerated with design-system:design-sync:update. The diff is one line carrying exactly the new prop; no other component's pin moved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011btGFwWKYFWDs5McQkqz9J --- .design-sync/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.design-sync/config.json b/.design-sync/config.json index fb177362cc..8579ab25cd 100644 --- a/.design-sync/config.json +++ b/.design-sync/config.json @@ -57,7 +57,7 @@ "RetrievalStateBanner": "className?: string; onOpenSource: (sourceId: string, locator?: string) => void; state: DegradedAnswerState;", "SafeBoldText": "text: string;", "SearchField": "about?: string; accept?: string; accessKey?: string; alt?: 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 & {}); autoComplete?: import(\"react\").HTMLInputAutoCompleteAttribute; autoCorrect?: string; autoFocus?: boolean; autoSave?: string; capture?: boolean | \"user\" | \"environment\"; checked?: boolean; children?: import(\"react\").ReactNode; className?: string; clearLabel?: 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\"; error?: string; exportparts?: string; fieldClassName?: string; form?: string; formAction?: string | ((formData: FormData) => void | Promise); formEncType?: string; formMethod?: string; formNoValidate?: boolean; formTarget?: string; height?: string | number; hidden?: boolean; hideLabel?: boolean; hint?: string; 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; label: string; lang?: string; list?: string; max?: string | number; maxLength?: number; min?: string | number; minLength?: number; multiple?: boolean; 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; onClear?: (() => void); 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; pattern?: string; placeholder?: string; popover?: \"\" | \"auto\" | \"manual\" | \"hint\"; popoverTarget?: string; popoverTargetAction?: \"toggle\" | \"show\" | \"hide\"; prefix?: string; property?: string; radioGroup?: string; readOnly?: boolean; ref?: Ref; rel?: string; required?: boolean; resource?: string; results?: number; rev?: string; role?: import(\"react\").AriaRole; security?: string; size?: number; slot?: string; spellCheck?: (boolean | \"true\" | \"false\"); src?: string; step?: string | number; style?: import(\"react\").CSSProperties; suppressContentEditableWarning?: boolean; suppressHydrationWarning?: boolean; tabIndex?: number; title?: string; translate?: \"yes\" | \"no\"; typeof?: string; unselectable?: \"off\" | \"on\"; value?: string | number | readonly string[]; vocab?: string; width?: string | number;", - "SegmentedControl": "ariaDescribedBy?: string; ariaLabelledBy?: string; className?: string; label?: string; layout?: \"fit\" | \"equal\"; onChange: (value: string) => void; options: readonly SegmentedControlOption[]; value: string;", + "SegmentedControl": "ariaControls?: string; ariaDescribedBy?: string; ariaLabelledBy?: string; className?: string; label?: string; layout?: \"fit\" | \"equal\"; onChange: (value: string) => void; options: readonly SegmentedControlOption[]; value: string;", "Select": "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 & {}); autoComplete?: string; autoCorrect?: string; autoFocus?: boolean; autoSave?: string; 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\"; error?: string; exportparts?: string; fieldClassName?: string; form?: string; hidden?: boolean; hideLabel?: boolean; hint?: string; 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; label: string; lang?: string; multiple?: boolean; 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; options: SelectOption[]; part?: string; placeholder?: string; popover?: \"\" | \"auto\" | \"manual\" | \"hint\"; popoverTarget?: string; popoverTargetAction?: \"toggle\" | \"show\" | \"hide\"; prefix?: string; property?: string; radioGroup?: string; ref?: Ref; rel?: string; required?: boolean; resource?: string; results?: number; rev?: string; role?: import(\"react\").AriaRole; security?: string; size?: number; slot?: string; spellCheck?: (boolean | \"true\" | \"false\"); style?: import(\"react\").CSSProperties; suppressContentEditableWarning?: boolean; suppressHydrationWarning?: boolean; tabIndex?: number; title?: string; translate?: \"yes\" | \"no\"; typeof?: string; unselectable?: \"off\" | \"on\"; value?: string | number | readonly string[]; vocab?: string;", "Sheet": "ariaLabel?: string; bodyClassName?: string; children: ReactNode; closeButtonClassName?: string; closeLabel?: string; contentClassName?: string; contentStyle?: CSSProperties; description?: string; descriptionContent?: ReactNode; desktopBackdropClassName?: string; footer?: ReactNode; headerActions?: ReactNode; headerClassName?: string; headerLeading?: ReactNode; id?: string; initialFocusRef?: RefObject; labelledBy?: string; mobilePlacement?: \"bottom\" | \"top\" | \"fullscreen\"; mobileSize?: SheetMobileSize; onClose: () => void; open: boolean; placement?: \"default\" | \"left\"; portal?: boolean; resolveReturnFocusTarget?: (() => HTMLElement | null); returnFocusRef?: RefObject; testId?: string; title?: string; titleAccessory?: ReactNode; titleClassName?: string;", "Skeleton": "about?: string; accessKey?: string; animationDelay?: 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; 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; draggable?: (boolean | \"true\" | \"false\"); enterKeyHint?: \"enter\" | \"done\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\"; exportparts?: string; hidden?: boolean; 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; 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\"; typeof?: string; unselectable?: \"off\" | \"on\"; vocab?: string;", From 68abb52d1abe9cac30777892961de9d88c4a9104 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 14:42:17 +0000 Subject: [PATCH 3/4] chore(ledger): record the PR A lens-adoption review Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011btGFwWKYFWDs5McQkqz9J --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index ee7f4f296b..0fbcc2c1cf 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -910,3 +910,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-12 | 1848 | c65b9d91b760862c2ff9d5001974670219c5da02 | full PR diff and unresolved review feedback | P2 hint contrast and live-count width fixes applied | focused Vitest passed (81); focused ESLint passed; design-system contract passed | | 2026-08-12 | PR #1595 / claude/ds-v2-adopt | 590eb6cfb229c5ae0f7a5025352fa871d8321521 | Supersedes 2026-08-03 PR-J clinical-governance review at f9f73c707d9b6b6226fc04d172fef8e426513055; accepted delta through merged PR head | SUPERSEDES the earlier PR-J clinical-governance row for merge evidence. The final delta added the answer-state projection, the two scoped review fixes, and the clinically approved #228 attribution wording. The user accepted that delta without a second clinical-governance review; this record preserves that explicit limitation rather than implying the earlier review covered the final tree. | Final PR head 590eb6cfb229c5ae0f7a5025352fa871d8321521; squashed to main as f4448f8c1 (historical mapping recorded in #232); no new provider or clinical review performed | | 2026-08-12 | PR-1835 | fe46e5ade8018d21cf15d149711579b1b43c7fb8 | full PR diff and unresolved review feedback | P1/P2 findings fixed during fresh open-PR sweep; rename-only historical review comments dispositioned no-change | focused drift/docs/ledger/migration checks pass; verify:pr-local static+lint+typecheck pass, unrelated Windows unit baseline failures | +| 2026-08-12 | claude/filter-lens-modes | 5cf8871ef122ec9e3d9b25ea65c4643d5a2cf2ba | filter contract PR A: lens adoption across differentials, medication, applications, specifiers | PR #1857 opened; 4 bespoke aria-pressed rails converged onto SegmentedControl with one shared option array per mode; specifiers footerNote fixed (counted results+catalogueMatches while filters govern only results); ResultFilterSheet counted-option accessible name fixed (All8 -> All (8)) on both group kinds; SegmentedControl gained group-level ariaControls so the launcher keeps #launcher-results-panel; dead SpecifierFamilyFilterChips removed; scope segment deliberately deferred to services per filter-contract.md s4 | verify:pr-local all steps green except build, which failed on the /issues #210 dev-types corruption and passed on a clean rebuild; unit suite 6100 passed/4 skipped with one 30s timeout (not an assertion failure) in design-sync-contract under parallel load, passing in isolation 7/7; bundle-budget production 1297.7 KiB and mockups 285.1 KiB both within tolerance on a verified-fresh build; browser proof at 1440/800/390/320px on all four modes, 0px overflow, 48px targets | From cd349d631655b6d061373fb030f5a16d897357d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 15:35:15 +0000 Subject: [PATCH 4/4] fix(test): specifiers family rail is a radiogroup, not a toggle group Production UI shard 3 caught the one assertion this conversion broke: ui-specifiers.spec.ts:82 asserted role="group" on the family rail, which was correct for the aria-pressed chips it replaced and wrong for SegmentedControl. The rail is a lens, so radiogroup is the point of the change rather than an incidental side effect. Found by CI rather than by my own search: I grepped for the component name and the test ids, not for the aria-label string, so a spec that only ever referenced the label slipped through. Shards 1 and 2 passed, so this was the only one. Verified in a browser against the running app: the radiogroup resolves, the old group role returns zero, and the other four assertions in the same test still hold. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011btGFwWKYFWDs5McQkqz9J --- tests/ui-specifiers.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ui-specifiers.spec.ts b/tests/ui-specifiers.spec.ts index f11d41de4a..1d11da3d65 100644 --- a/tests/ui-specifiers.spec.ts +++ b/tests/ui-specifiers.spec.ts @@ -79,7 +79,9 @@ test("searches clinical language without provenance fields and carries a result await expect(queryRibbon.getByRole("group", { name: "Filter specifier results" })).toBeVisible(); await expect(page.getByText(/Results ranked by text relevance/i)).toHaveCount(0); await expect(page.getByText("Top match", { exact: true })).toBeVisible(); - await expect(page.getByRole("group", { name: "Filter by specifier family" })).toBeVisible(); + // A lens, so the rail is a radiogroup rather than a toggle group — the sheet + // already said one-of-N and the desktop rail now agrees with it. + await expect(page.getByRole("radiogroup", { name: "Filter by specifier family" })).toBeVisible(); await expect(page.getByRole("combobox", { name: "Filter by diagnosis" })).toBeVisible(); await expect(page.getByText("Best fit", { exact: true })).toHaveCount(0); await expect(page.getByText(/clinical fit/i)).toHaveCount(0);