diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 5004057b58..339778e1a3 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -752,13 +752,10 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-08 | claude/ds-tap-and-linkaction | 6916c80526603514d91bd29d224959dd420af59c | M5 LinkAction tone refusal plus re-measured corrections to outstanding-issues #270, #118 and #269 — final reviewed head, adds the tone?: never fix, its type-contract test and both regenerated manifests | PR #1720, superseding the 824c1b74a record. Codex found the Omit form still accepted tone through a spread; verified with a focused tsc probe before changing anything (Omit accepted the spread with no diagnostic, tone?: never rejected it with TS2345), because excess-property checking only fires on object literals. Fixed with tone?: never plus a type-level contract test that stops compiling if the prop widens back. CodeRabbit's future-dated finding fixed in ff307cc5b. CodeRabbit's ledger-scope finding does not apply: that row records a different ref and head and was accurate as written, but a superseding row for the final #1719 head was appended anyway since its scope grew after the review pass | tsc -p tsconfig.typecheck.json --noEmit exit 0 zero diagnostics; lint exit 0; check:design-system-contract exit 0 (676 production files, legacy shadow aliases 228 confirming the #262 re-measure, adoption 53 components 55 roots, design-sync 53 components and 7 guidelines); check-icon-scale.mjs --strict exit 0; vitest threads pool 3 files 164 tests passed; check:outstanding-issues pass; check:branch-review-ledger pass; prettier --check . pass whole-tree; main merged in with merge-tree proven clean first and an id-set proof over both merge parents showing 274 ids each side, none lost, none invented | | 2026-08-08 | claude/ds-close-276 (PR #1724) | 75c89993f3ea23b70a250f605b21437b4ea9aac8 | PR #1724 review-and-fix | fixed Codex P2 wrong #118 Lighthouse cause (150 overwrite vs 151 pin); dispositioned CodeRabbit #276 archive claim as false (issues:done move); merge-tree clean; required CI was green on prior tip 8ae8c48f; no Bugbot findings | check:outstanding-issues pass; prettier --check docs/outstanding-issues.md pass; no provider-backed checks | | 2026-08-08 | claude/ds-close-276 (PR #1724) | 4baa9a1b42fa05731a6f983b3e0d0ebbd37f5271 | PR #1724 review-and-fix | synced origin/main (#1725 conflict on outstanding-issues resolved by preferring main queue then re-applying #276 done + corrected #118 diagnosis); Codex P2 fixed; CodeRabbit #276 archive claim dispositioned false; merge-tree clean after sync | check:outstanding-issues pass; prettier --check docs/outstanding-issues.md pass; merge-tree clean vs origin/main; no provider-backed checks | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 32474bcd20d5fa39097a3f75b85d3af81b404320 | form-detail Confirm checklist polish | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 3cf0ed99a1a90f46cb7c6aff7e8b7f7bfd6212b8 | form-detail Confirm checklist polish | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 3cf0ed99a1a90f46cb7c6aff7e8b7f7bfd6212b8 | form-detail Confirm checklist polish (supersedes 2026-08-08) | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 96c4d3a3a1a46efedfa5b43c4bf1de227c1d19a6 | PR #1734 confirm checklist | clean; no P0/P1/P2 in ConfirmCalloutText/confirmCheckParts/Avoid row | diff vs main; form-1a catalog wiring; vitest form-confirm-callout.dom.test.tsx PASS | +| 2026-08-08 | cursor/forms-info-disclosure-68d6 | f5dd1dea495e8d6e9bd5106dcf0a4d062ee02292 | forms-info-disclosure | fixed Form information tick rows to expand via DisclosureGroup | verify:pr-local; forms-information-disclosure.dom.test; check:design-system-adoption | +| 2026-08-08 | cursor/forms-info-disclosure-68d6 | 8f25e6c482d8e4cd879098d7cfd73b7f8603e478 | forms-info-disclosure | fixed Form information tick rows to expand via DisclosureGroup | verify:pr-local; forms-information-disclosure.dom.test; check:design-system-adoption | | 2026-08-08 | claude/document-image-mobile-view-30xzw8 | 2394d903a6ca1ba7a84e380c9ed5cada038fa5c0 | document-viewer phone image layout + lightbox geometry (PR #1737) | implemented: capped rail/body grid tracks, removed aspect-ratio min-height transfer, rebuilt phone image viewer (legible open scale, rotation re-fit, clamped pan, double-tap, footer controls) | lint, typecheck, test (5647 pass / 1 pre-existing fail), build, eval:rag:offline, check:bundle-budget, all verify:pr-local static steps by hand; browser gates blocked by #255 | | 2026-08-08 | claude/document-image-mobile-view-30xzw8 | d257df7e11913db1d367535171fac726f47e7f1c | PR #1737 document-viewer phone image review-and-fix | fixed P1 expand fixture/threshold + P2 double-tap stage coords/pointer-up + resize re-clamp; Production UI timeout root cause cleared; merge-tree clean | verify:pr-local PASS (525 files/5653 tests); lint; typecheck; focused vitest 64/64; Production UI delegated to CI | -| 2026-08-08 | cursor/confirm-checklist-polish-195c | 89cc8711dd0536c32818cbbd493edff860763a61 | PR #1734 unblock | synced origin/main (behind-but-clean DIRTY; merge-tree clean); no product conflict; advisory lighthouse ignored | merge-tree clean vs origin/main; ledger:dedupe none | | 2026-08-08 | PR #1740 / claude/inpage-nav-info-pages-v8rhnd | b67f33f65e00529eb0dd1682d6925e708243ee93 | Extract InPageNavHeader (default in-page nav template) + convert differentials detail; PR 1 of 3 | HANDOFF. Template extracted from the duplicated DocumentViewer/differential-detail markup into src/components/in-page-nav/ (InPageNavHeader, PageSection/toDocumentSections, usePageSectionWeights); differential-detail-page converted (-207 lines), behaviour-neutral. section-index.ts untouched so document tests unaffected. DocumentViewer deliberately NOT converged (owns h1, edge-glass-header, visual baselines) - follow-up. Anchor-offset hook generalisation deferred to PR 2 where it is consumed. 3 source-scanning contracts + addon-slot guard updated to follow the markup and additionally assert adoption; addon-slot scan widened to InPageNavHeader or it would go silent for every future adopter. Single failing test (pr-handoff-stop) is a root-uid artifact: chmod 0555 does not block root, reproduced with work stashed on clean tree. | verify:cheap 5618 passed/1 failed (root artifact); verify:pr-local same, short-circuits at test so build not reached; build run separately - Compiled successfully in 53s + client bundle secret check passed; verify:phone-chrome EXIT=0 (stage1 119 passed, stage2 7 passed 23.5s, full UI policy auto not selected); lint/typecheck/prettier --check . clean. No provider-backed gates. Deps installed with engine check relaxed (user-approved; Node 24.13.0 vs jsdom floor 24.15) - lockfile untouched. | | 2026-08-08 | claude/mode-routing-search-pages-jabe17 | 3a0bdd62466080ad713873cdd690ae600635a979 | mode routing: one shared home page at /, mode pill retargets the composer, /documents + /medications mode homes | handoff — PR #1744 opened; 2 pre-existing failures verified at base bc33d41 | test:e2e:pr 406 passed/2 failed (both fail at base); vitest 5608 passed/1 failed (pre-existing); lint clean; tsc clean; sitemap:check, docs:check-index, docs:check-inventory, check:design-system-contract, check:outstanding-issues pass; verify:pr-local and verify:ui blocked by pre-existing installed-lock-parity (playwright 1.62.0 vs locked 1.62.1) | | 2026-08-08 | claude/mode-routing-search-pages-jabe17 | 468cc3fce85726a66098af0600d2b5d5951e3213 | bug-hunt | findings: P1 documents home autoRun on keystroke; P2 stale PWA /?mode=prescribing; P2 landing vs lastAppMode race; P2 /medications?q&run deep-link lost | vitest app-modes+search-route-ownership 36 pass; static ownership/ask-routing proof; no browser/UI/provider | @@ -770,6 +767,12 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-08 | claude/planning-build-intelligence-9ot0nm | 1ebc84bb288b516bb322c09cde2889e981d302a4 | AGENTS.md reasoning-effort calibration section (docs-only) | Authored and handed off as PR #1730; docs-only, pr-policy classifier returns clinicalRisk/operationalRisk/ragRanking false | prettier --check . (repo-wide, pass); docs:check-links (1665 refs resolve, pass); pr-policy classifyPullRequestFiles(AGENTS.md) | | 2026-08-08 | claude/planning-build-intelligence-9ot0nm | 2b0ad7d41d841c13515f10de7c41e449470dfa78 | pr-1730 review-and-fix | Deep review + Bugbot: no P0/P1; fixed 2 scoped P2 clarity risks (version-bump under-planning; live-state vs provider boundary). Residual: OPENAI_*_REASONING_EFFORT vocab overlap. Merge-tree clean; required CI was green pre-push. | prettier --check AGENTS.md; docs:check-links (1667); verify:pr-local (docs route pass); verify:cheap (524 files / 5607 tests pass); pr-policy classify clinical/operational/rag false; Bugbot no P0-P2 | | 2026-08-08 | dependabot/npm_and_yarn/js-yaml-4.3.1 | a79943df33e653d2a65d4db2f192ee77c22ab75a | PR #1668 unblock | late-synced main after CI green on f04a96c3; merge-tree clean (GitHub DIRTY was stale); js-yaml 4.3.1 + nanoid 3.3.18 preserved; no unresolved threads; CI re-run after push | pre-late-sync: PR required pass on f04a96c3; Production UI skipped; post-sync pending | +| 2026-08-08 | cursor/forms-info-disclosure-68d6 (PR #1735) | 9e1390d73ebbae0bbfc0f81bf3b3921dadf24577 | heavy review-and-fix | CONFLICT merge-tree on docs/design-system/adoption-manifest.json resolved by regenerating (DisclosureGroup form-detail import + main documents/medications routes); product forms DisclosureGroup intent preserved; 0 unresolved threads; no ambiguous clinical/auth conflicts | check:design-system-adoption PASS (53 components, 57 roots); vitest forms-information-disclosure.dom 2/2 PASS; no provider-backed checks | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 32474bcd20d5fa39097a3f75b85d3af81b404320 | form-detail Confirm checklist polish | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 3cf0ed99a1a90f46cb7c6aff7e8b7f7bfd6212b8 | form-detail Confirm checklist polish | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 3cf0ed99a1a90f46cb7c6aff7e8b7f7bfd6212b8 | form-detail Confirm checklist polish (supersedes 2026-08-08) | shipped spacing/typography polish + DOM guard | vitest form-confirm-callout.dom; visual Form 1A Confirm | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 96c4d3a3a1a46efedfa5b43c4bf1de227c1d19a6 | PR #1734 confirm checklist | clean; no P0/P1/P2 in ConfirmCalloutText/confirmCheckParts/Avoid row | diff vs main; form-1a catalog wiring; vitest form-confirm-callout.dom.test.tsx PASS | +| 2026-08-08 | cursor/confirm-checklist-polish-195c | 89cc8711dd0536c32818cbbd493edff860763a61 | PR #1734 unblock | synced origin/main (behind-but-clean DIRTY; merge-tree clean); no product conflict; advisory lighthouse ignored | merge-tree clean vs origin/main; ledger:dedupe none | | 2026-08-08 | cursor/safety-plan-copy-timer-a650 | cf57b34a36b768e150cd776f7e19acfd984245f7 | PR #1717 unblock | fixed missing it() closer from Copilot autofix; merged origin/main after #1668; merge-tree clean; no unresolved threads | local: vitest patient-safety-plan.dom.test.tsx (8/8); format ok; pending hosted CI after push | | 2026-08-08 | cursor/safety-plan-copy-timer-a650 | 3142eb9a93275ce2c2435523560b4ed6624d8f53 | PR #1717 unblock | fixed parse + no-explicit-any from Copilot autofix; merged origin/main after #1668; merge-tree clean; 0 threads | local: vitest 8/8; eslint file clean; format ok; pending hosted CI | | 2026-08-08 | cursor/compact-services-result-text-9b7d (PR #1731) | f07828041199458bd756090d04fb5105f41e3ca4 | PR #1731 unblock | before: MERGEABLE/BEHIND(1) merge-tree CLEAN tip 14fd8aa9; required CI green (PR required + Production UI 1/2/3 + critical); 0 threads; autoMerge SQUASH armed. after: late-synced origin/main (aa6cf68c from #1668/#1717) via worktree merge (update-branch 403); merge-tree clean; 0 behind; CI will re-run on sync tip; autoMerge left armed; no product code change | gh pr checks --watch: PR required SUCCESS; Production UI (1)(2)(3)+critical SUCCESS; merge-tree clean; ledger:dedupe none; no provider gates | diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index cfa9aff691..fa1d86e6ee 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -947,7 +947,7 @@ tests to run. Registered public components: 53 Components with a valid design-sync preview: 53 -Components with product imports: 30 +Components with product imports: 31 This generated snapshot is a local source-derived inventory. It does not assert remote design-project publication. @@ -966,7 +966,7 @@ This generated snapshot is a local source-derived inventory. It does not assert | `ConfirmDialog` | layout | yes | yes | no | yes | no | 0 | | `DateDisplay` | source | yes | yes | inherited-global-root | yes | no | 3 | | `Disclosure` | layout | yes | yes | no | yes | no | 0 | -| `DisclosureGroup` | layout | yes | yes | no | yes | no | 0 | +| `DisclosureGroup` | layout | yes | yes | inherited-global-root | yes | no | 1 | | `DoseLine` | answer | yes | yes | no | yes | no | 0 | | `DownloadLink` | controls | yes | yes | no | yes | no | 0 | | `EmptyState` | feedback | yes | yes | inherited-global-root | yes | no | 12 | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index ea3a3b82d5..7434319c33 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -544,8 +544,8 @@ "source": "src/components/ui/disclosure.tsx", "sourceExported": true, "entryExported": true, - "directImportFiles": [], - "productImportFiles": [], + "directImportFiles": ["src/components/forms/form-detail-page.tsx"], + "productImportFiles": ["src/components/forms/form-detail-page.tsx"], "designSync": { "listedInSourceMap": true, "listedInDtsProps": true, @@ -561,8 +561,8 @@ }, "built": true, "locallyRegistered": true, - "v2ShellMounted": false, - "v2MountMode": "none", + "v2ShellMounted": true, + "v2MountMode": "inherited-global-root", "proofDeclared": true, "baselineCommitted": false }, @@ -3281,7 +3281,7 @@ "summary": { "registeredComponentCount": 53, "previewCount": 53, - "productImportedComponentCount": 30, + "productImportedComponentCount": 31, "rootCount": 57, "productionRouteCount": 49, "nextUiEntryCount": 101 diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index 0bece582ed..c3dadbca48 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -43,6 +43,7 @@ import { } from "@/components/ui-primitives"; import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; import { FormCodeBadge, splitFormCode } from "@/components/forms/form-code-badge"; +import { DisclosureGroup } from "@/components/ui/disclosure"; import { appModeHomeHref } from "@/lib/app-modes"; import { formCatalogDetails, formTitleForCode, type FormRecord } from "@/lib/form-catalog"; import type { ServiceChipTone, ServiceContact, ServiceCriterion, ServiceSummaryCard } from "@/lib/service-ranker"; @@ -623,21 +624,34 @@ function RailCard({ icon: Icon, title, children }: { icon: LucideIcon; title: st ); } -function InfoRow({ label, value, icon: Icon }: { label: string; value: string | null | undefined; icon: LucideIcon }) { - return ( -
- - - -
-

{label}

-

- {displayText(value)} -

-
- -
- ); +function formInformationIcon(label: string): LucideIcon { + const normalized = label.toLowerCase(); + if (normalized.includes("only")) return Route; + if (normalized.includes("sign")) return Clipboard; + if (normalized.includes("clinical")) return Info; + if (normalized.includes("source")) return FileText; + if (normalized.includes("pathway")) return Navigation; + return CircleCheck; +} + +function formInformationItems(rows: Array<{ label: string; value?: string | null }>) { + return rows.map((row, index) => { + const Icon = formInformationIcon(row.label); + const value = displayText(row.value); + return { + id: `form-info-${index}-${row.label}`, + title: ( + + + + + {row.label} + + ), + description: value, + content:

{value}

, + }; + }); } export function FormDetailPage({ form }: { form: FormRecord }) { @@ -878,21 +892,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) {
- {detailRows.map((row) => { - const label = row.label.toLowerCase(); - const Icon = label.includes("only") - ? Route - : label.includes("sign") - ? Clipboard - : label.includes("clinical") - ? Info - : label.includes("source") - ? FileText - : label.includes("pathway") - ? Navigation - : CircleCheck; - return ; - })} +
{/* The `-mobile`/`-desktop` id pairs below are the section anchors diff --git a/src/components/ui/disclosure.tsx b/src/components/ui/disclosure.tsx index a19da858fa..3d343d1087 100644 --- a/src/components/ui/disclosure.tsx +++ b/src/components/ui/disclosure.tsx @@ -97,7 +97,18 @@ export function Disclosure({ /> {title} - {description ? {description} : null} + {/* Visual preview only: keep it out of the accessible name so the + trigger stays label-sized. Full copy lives in the panel for SR + once expanded. Wrap from sm+ so desktop scanners are not forced + through a tap the way phone truncation requires. */} + {description ? ( + + ) : null} {meta ? {meta} : null} diff --git a/tests/forms-information-disclosure.dom.test.tsx b/tests/forms-information-disclosure.dom.test.tsx new file mode 100644 index 0000000000..a0cb9473ed --- /dev/null +++ b/tests/forms-information-disclosure.dom.test.tsx @@ -0,0 +1,91 @@ +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { FormDetailPage } from "@/components/forms/form-detail-page"; +import { getFormRecord } from "@/lib/forms"; + +vi.mock("@/components/account-data-provider", () => ({ + useAccountData: () => ({ + isSaved: () => false, + setFavourite: vi.fn(async () => true), + }), +})); + +describe("Form information disclosures", () => { + it("expands a tick row to reveal the full information content", async () => { + const user = userEvent.setup(); + const form = getFormRecord("form-1a"); + expect(form).toBeTruthy(); + if (!form) return; + + const doesNotAuthorise = form.referralInfo?.find((row) => row.label === "Does not authorise"); + const fullText = doesNotAuthorise?.value; + expect(fullText).toEqual(expect.any(String)); + if (!fullText) return; + + render(); + + const section = screen.getByRole("region", { name: "Form information" }); + const trigger = within(section).getByRole("button", { name: "Does not authorise" }); + expect(trigger).toHaveAccessibleName("Does not authorise"); + expect(trigger).toHaveAttribute("aria-expanded", "false"); + + const panelId = trigger.getAttribute("aria-controls"); + expect(panelId).toEqual(expect.any(String)); + if (!panelId) return; + const panel = document.getElementById(panelId); + expect(panel).toBeTruthy(); + if (!panel) return; + expect(panel).toHaveAttribute("hidden"); + + await user.click(trigger); + + expect(trigger).toHaveAttribute("aria-expanded", "true"); + expect(panel).not.toHaveAttribute("hidden"); + expect(within(panel).getByText(fullText)).toBeVisible(); + }); + + it("keeps duplicate labels on independent disclosure triggers", async () => { + const user = userEvent.setup(); + const base = getFormRecord("form-1a"); + expect(base).toBeTruthy(); + if (!base) return; + + const form = { + ...base, + referralInfo: [ + { label: "Clinical note", value: "First clinical detail" }, + { label: "Clinical note", value: "Second clinical detail" }, + ], + }; + + render(); + + const section = screen.getByRole("region", { name: "Form information" }); + const triggers = within(section).getAllByRole("button", { name: "Clinical note" }); + expect(triggers).toHaveLength(2); + + const panelIds = triggers.map((trigger) => trigger.getAttribute("aria-controls")); + expect(panelIds.every((id) => Boolean(id))).toBe(true); + expect(new Set(panelIds).size).toBe(2); + + await user.click(triggers[0]); + expect(triggers[0]).toHaveAttribute("aria-expanded", "true"); + expect(triggers[1]).toHaveAttribute("aria-expanded", "false"); + + const firstPanel = document.getElementById(panelIds[0]!); + expect(firstPanel).toBeTruthy(); + if (!firstPanel) return; + expect(firstPanel).not.toHaveAttribute("hidden"); + expect(within(firstPanel).getByText("First clinical detail")).toBeVisible(); + + await user.click(triggers[1]); + expect(triggers[0]).toHaveAttribute("aria-expanded", "true"); + expect(triggers[1]).toHaveAttribute("aria-expanded", "true"); + const secondPanel = document.getElementById(panelIds[1]!); + expect(secondPanel).toBeTruthy(); + if (!secondPanel) return; + expect(within(secondPanel).getByText("Second clinical detail")).toBeVisible(); + }); +}); diff --git a/tests/ui-v2-components.dom.test.tsx b/tests/ui-v2-components.dom.test.tsx index e4274488a1..dd08ff2db0 100644 --- a/tests/ui-v2-components.dom.test.tsx +++ b/tests/ui-v2-components.dom.test.tsx @@ -281,6 +281,37 @@ describe("Disclosure / Progress", () => { expect(screen.getByRole("heading", { level: 4, name: "Monitoring" })).toBeVisible(); }); + it("keeps description as a visual preview outside the accessible name", async () => { + const user = userEvent.setup(); + render( + + Treatment, detention, transport, restraint, seclusion or force by itself. + , + ); + + const description = "Treatment, detention, transport, restraint, seclusion or force by itself."; + + const trigger = screen.getByRole("button", { name: "Does not authorise" }); + expect(trigger).toHaveAccessibleName("Does not authorise"); + expect(within(trigger).getByText(description)).toBeVisible(); + expect(trigger).toHaveAttribute("aria-expanded", "false"); + await user.click(trigger); + expect(trigger).toHaveAttribute("aria-expanded", "true"); + const panelId = trigger.getAttribute("aria-controls"); + expect(panelId).toEqual(expect.any(String)); + if (!panelId) return; + const panel = document.getElementById(panelId); + expect(panel).toBeTruthy(); + if (!panel) return; + expect( + within(panel).getByText("Treatment, detention, transport, restraint, seclusion or force by itself."), + ).toBeVisible(); + }); + it("animates determinate progress with scaleX rather than width", () => { render(); const fill = screen.getByTestId("progress-fill");