diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index aa95abfb3a..644b72a72a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -883,7 +883,7 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-11 | codex/answer-loading-ui-20260811 | 6758f8156f9d1b3e893981dfd7a1f6563aa90da0 | answer creation loading UI | No high-confidence findings | UI 3 passed; unit 8 passed; lint, typecheck, build, design-system and offline RAG passed; full suite 6022 passed with 16 unchanged baseline failures | | 2026-08-11 | claude/codex-m4c-retire-shadow-nliak3 | 448a0d084c4cd2cda6153dd7f03dcb67c43a8df0 | DS Track A2 (#261): retire --shadow-focus; composer focus onto sanctioned outline; contract guard; baseline ratchet; design-system docs + ledger | Approved — PR #1807. Token deleted in both themes; .chat-composer-shell-delta:focus-within uses outline 2px var(--focus) at offset 2px and no longer overrides box-shadow. Reach premise corrected: 0 of 37 production routes render the class (only /mockups/calculators-search). legacyShadowAliases 127->125, globals.css pin 3->1. | check:design-system-contract PASS; design-token-contract.test.ts PASS + mutation-verified both ways; verify:pr-local PASS except pre-existing tests/pr-handoff-stop.test.ts failure baselined on untouched base e8b61d8; build PASS; check:rag:fixtures PASS (36 cases); Chromium look both themes on the mockup route (inspection only, rev 1194 vs pinned 1234 #255); verify:ui/verify:phone-chrome NOT run — delegated to CI | | 2026-08-11 | codex/answer-ecg-animation-20260811 | 12279a8309c225aa957ef1e65afc37545a0ce04c | answer ECG progress variants | No high-confidence findings; physical Safari/PWA remains residual acceptance | design contract, typecheck, focused unit 8/8, trace token 33/33, Chromium 4/4, offline RAG 574/574; full suite baseline/platform failures | -| 2026-08-11 | claude/filter-popup-design-mockups-x6sbjv | 7b64f2559741a9f353adcf939745831e0daff7db | services filter sheet redesign mockups (3 directions, desktop+phone) | PR #1828 opened; design-scratch route only, no production behaviour change | verify:pr-local (1 pre-existing root-uid test failure, reproduced on origin/main 046feb3), build, check:rag:fixtures, check:bundle-budget both baselines within tolerance, 320px 0px overflow | +| 2026-08-11 | codex/chat-services-flow-redesign-20260812 | 6f44b92defb91bcd77509bf10337b428be37619c | Services home, results, shortlist, comparison, and referral detail redesign | No findings; changed-area UI, phone contracts, focused unit, build, and RAG fixtures passed; PR-local Windows baseline limitations documented. | 78 focused tests passed post-merge; 185 changed-browser tests; 129 phone contracts; 7 phone-scroll tests; build and RAG fixtures passed | | 2026-08-11 | work | 6dcd695076d630d16aae594577763e8004361893 | Codex Cloud setup and local parity | P2 fixed: cache-friendly locked Cloud npm install; parity limitations documented | check:codex-cloud; codex-cloud-setup 24/24; full suite 6059 pass, 7 unrelated timeout/state failures | | 2026-08-11 | 1822 | 4fab267f52b72992745e1d2e6975fb4847af447a | review-and-fix | clean | Build pass; Static PR checks pass; Change scope pass; PR mergeability pass; PR policy pass; Safety and config checks pass; Semgrep pass; Semgrep ingestion gate pass; Gitleaks pass; GitGuardian pass; Unit coverage pending; Production UI (1) pass; Production UI (2) pass; Production UI critical pending; Production UI (3) pending; Lighthouse budget pass; PR required pending | | 2026-08-11 | 1822 | 4fab267f52b72992745e1d2e6975fb4847af447a | review-and-fix (supersedes 2026-08-11) | clean | Build pass; Static PR checks pass; Change scope pass; PR mergeability pass; PR policy pass; Safety and config checks pass; Semgrep pass; Semgrep ingestion gate pass; Gitleaks pass; GitGuardian pass; Unit coverage pass; Production UI (1) pass; Production UI (2) pass; Production UI (3) pass; Production UI critical pass; Lighthouse budget pass; PR required pass | @@ -891,33 +891,35 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-11 | 1821 | d76e90547dbdb104256b66a508c15c74302002fc | review-and-fix | dispositioned | PR policy:success; PR mergeability:success; Gitleaks:success; Semgrep:success; Semgrep ingestion gate:success; Safety and config checks:success; Build:success; Production UI critical:success; Production UI (1):success; Production UI (2):success; Production UI (3):failure test not reproduced outside this PR; PR required:failure | | 2026-08-11 | work | 45fd05c8c3947835c0368666ff576c7a38b33ee4 | mobile evidence sheet UX, accessibility, and feedback logic | fixed unexplained claim marker, excess panel reserve, unclear purpose and feedback copy; no remaining high-confidence defects | focused DOM 7/7; Chromium evidence journey 1/1; offline RAG 23 suites/574 tests | | 2026-08-11 | 1820 | 897ff11a4cdb13ae1c01f5eb149007847028f5aa | review-and-fix | fixed | Semgrep:IN_PROGRESS, Gitleaks:IN_PROGRESS, Semgrep ingestion gate:IN_PROGRESS, Static PR checks:QUEUED, Safety and config checks:QUEUED, Unit coverage:QUEUED, Build:QUEUED, Production UI critical:QUEUED, Lighthouse budget:QUEUED | -| 2026-08-12 | claude/filter-popup-design-mockups-x6sbjv | 5e41d164e30e8f5a74b255fbeafd34123385dbb9 | services filter: round-two options study (stop-the-bleed / recommended / presets-evicted) | Pushed to PR #1828; merged babysit fixes to round-one facet semantics; design-scratch only | verify:pr-local (1 pre-existing root-uid failure only), build, check:rag:fixtures, bundle-budget mockups 286.8 KiB within 25% tolerance, counts re-verified vs snapshot, 320px 0px overflow | -| 2026-08-11 | codex/chat-services-flow-redesign-20260812 | 6f44b92defb91bcd77509bf10337b428be37619c | Services home, results, shortlist, comparison, and referral detail redesign | No findings; changed-area UI, phone contracts, focused unit, build, and RAG fixtures passed; PR-local Windows baseline limitations documented. | 78 focused tests passed post-merge; 185 changed-browser tests; 129 phone contracts; 7 phone-scroll tests; build and RAG fixtures passed | | 2026-08-11 | claude/spacing-icon-design-review-rxwh28 | f8701a524f0eb22decc64ce1f626bdafe91751af | mode-home hero spacing rhythm + icon scale (PR #1815) | shipped: group copy reserve banded to measured wrap points, continuous hero medallion, phone composer glyph 1.1rem->icon-lg, surface glyphs onto size-icon-*, privacy link bottom-only negative margin (fixes 8px tap overhang on the APP-5 sentence), composer phone reserve 7.625->6.625rem | test 6043 passed/1 pre-existing root-perm failure; lint; typecheck; build; check:icon-scale; check:type-scale; check:design-system-contract; check:rag:fixtures; check:bundle-budget; prettier --check .; verify:ui NOT run (Playwright r1234 vs image r1194, #255 - delegate to CI Production UI) | | 2026-08-11 | 1815 | be7461ef1f66357999995acefbeecaf95268e481 | unblock | local-build-pass | MergeTreeClean,UnitCoverage,StaticPRChecks,ContainerImages | | 2026-08-11 | claude/spacing-icon-design-review-rxwh28 | 455bc198c077860fb1f830670a5fa9c1de08da52 | pr-1815 heavy review-and-fix | remote already merged main (shadow-tight Switch kept); cherry-picked privacy -mb-4 reclaim + calculators dock cancel; removed duplicate UniversalSearchAlsoMatches; rail-aware section-sheet focus restore; dispositioned CodeRabbit docs/ledger/gates nits and outdated Sentry skeleton gap | verify:cheap PASS prior tip; verify:pr-local PASS prior tip; vitest privacy+in-page-nav 28 passed on cherry-pick; merge-tree clean vs origin/main | | 2026-08-11 | claude/spacing-icon-design-review-rxwh28 | 5b96281ee7da817d5ce7f1102004ebe6f861b920 | pr-1815 heavy review-and-fix | remote already merged main (shadow-tight Switch kept); cherry-picked privacy -mb-4 reclaim + calculators dock cancel; removed duplicate UniversalSearchAlsoMatches; rail-aware section-sheet focus restore; dispositioned CodeRabbit docs/ledger/gates nits and outdated Sentry skeleton gap | verify:cheap PASS prior tip; verify:pr-local PASS prior tip; vitest privacy+in-page-nav 28 passed on cherry-pick; merge-tree clean vs origin/main | | 2026-08-12 | PR #1815 / claude/spacing-icon-design-review-rxwh28 | 9f266210f02081be54d407c70a85f52fed436128 | babysit | no remaining actionable findings; one pre-existing thread resolved as no-change (Dockerfile.worker follow-up needed) | required checks: Gitleaks PR policy PR required (all pass); targeted vitest passed: tests/document-frame-contract.test.ts + tests/in-page-nav-header.dom.test.tsx | | 2026-08-12 | 1815 | 27ce96e1755055ceee2eeae02d6efdf11259fcde | babysit | fixed | Unit coverage: targeted vitest passed: tests/shared-home-empty-state.dom.test.tsx (17 passed). PR required still blocked on pre-existing check failure at old remote head before sync. | -| 2026-08-12 | codex/implement-process-safety-for-multi-agent-workflows | 06144a7fcde8fea5dca63348bcc968fe796b152e | full PR diff and unresolved review feedback | Fixed cached-origin truthfulness, agent-safe approval gates, UI browser proof ordering, Added-date JSON, and foreign-index handoff safety; prior #240/#253 classifier feedback already fixed. | focused Vitest 31/31; outstanding-issues guard pass; gate-manifest pass; git diff --check | +| 2026-08-12 | claude/filter-popup-design-mockups-x6sbjv | 2f69b2346eced0a5dfc120b22ba9391e27367bb3 | filter sheet restyle: 3 styles on formulation + scope segment | PR #1846 opened; design-scratch only; documents 3 source-verifiable defects (0-match domains, unused formulationDomainGroups, preset slice) | verify:pr-local fully green (no failures), clean build, bundle-budget both baselines within tolerance, 320px 0px overflow measured and fixed | +| 2026-08-12 | 1846 | e34a617d493d4ea89de49c089db3095e3f21fe31 | full PR diff and unresolved review feedback | P2 scope premise repaired with a production query that returns the displayed two-result subset | focused Vitest passed (1); focused ESLint passed | | 2026-08-12 | claude/design-issues-triage-wnr7k9 | a6bfc6f2707975d9b9c649843e083965c80afb9c | Tier 1 design-issue re-verification: archive #171/#172/#174/#181/#273/#274/#302, correct #293 | docs-only; six rows verified delivered on main, min-h-tap finding refuted as deliberate sm: step-down | verify:pr-local (10/10 green); check:outstanding-issues 138 open/163 archived | | 2026-08-12 | claude/design-issues-triage-wnr7k9 | 970d39bc7023823d3283c660df090659a2f07aec | Full outstanding-issues ledger sweep: 47 rows individually verified against merged main | 19 archived (delivered or duplicate), 10 re-scoped with re-measured evidence, 1 refuted (#293), 4 machine-local rows annotated do-not-close-from-cloud; 98 rows bucketed by blocker, not individually verified | verify:pr-local 10/10 green; check:outstanding-issues 126 open/175 archived, no ids deleted from base | +| 2026-08-11 | claude/filter-popup-design-mockups-x6sbjv | 7b64f2559741a9f353adcf939745831e0daff7db | services filter sheet redesign mockups (3 directions, desktop+phone) | PR #1828 opened; design-scratch route only, no production behaviour change | verify:pr-local (1 pre-existing root-uid test failure, reproduced on origin/main 046feb3), build, check:rag:fixtures, check:bundle-budget both baselines within tolerance, 320px 0px overflow | +| 2026-08-12 | claude/filter-popup-design-mockups-x6sbjv | 5e41d164e30e8f5a74b255fbeafd34123385dbb9 | services filter: round-two options study (stop-the-bleed / recommended / presets-evicted) | Pushed to PR #1828; merged babysit fixes to round-one facet semantics; design-scratch only | verify:pr-local (1 pre-existing root-uid failure only), build, check:rag:fixtures, bundle-budget mockups 286.8 KiB within 25% tolerance, counts re-verified vs snapshot, 320px 0px overflow | | 2026-08-12 | codex/pr-workflow-safety-230-296 | bc0a491fdf4146775629f9b2b03e2a2cc61bd7cb | pr-1830 unblock | unblocked: merged origin/main (outstanding-issues conflict), PR body RAG impact + governance, resolved Copilot thread; merge-tree clean; required CI in progress | check:outstanding-issues pass; evaluatePullRequestPolicy ok; merge-tree clean; PR policy/mergeability/Change scope in progress | | 2026-08-12 | claude/filter-contract-global | a0add717c2521c7fdeba4da5b094377014383c3e | global filter contract: lens/facet kinds + docs/filter-contract.md (no rendered change) | PR #1847 opened; additive only, zero call sites touched; fixed an accessible-name leak caught by the new DOM tests | verify:pr-local fully green (no failures), 4 new DOM tests, git diff over all 7 mode files empty | | 2026-08-12 | claude/design-issues-triage-wnr7k9 | 2553b64b2342b0ef2ebef0afde152e76c1252496 | Ledger sweep round 2: open-PR cross-check plus clinical/answer-surface verification | 24 rows flagged IN FLIGHT against 7 open PRs (none had said so); 3 answer-surface rows archived (#166 #208 #216); #250 wave plan re-scoped; main-merge conflict resolved preserving both sides, 23 branch changes re-applied via the writer | verify:pr-local 10/10 green; check:outstanding-issues 121 open/180 archived, no ids deleted from base | | 2026-08-12 | claude/rag-canary-test-review-seprbt | bcf357a96fde74d39fc4726ffabb5079a744ef28 | eval-canary review: workflow, compare tooling, snapshot builder, alias tiering, rag-behaviour docs | PR #1843 opened; no retrieval behaviour change; snapshot refresh handed off as /issues #304 | verify:pr-local (green except env-only #296), eval:rag:offline 574/574, focused suites 40/40 | | 2026-08-12 | claude/design-issues-triage-wnr7k9 | 586012639565e4d3306b44361ebc5a3bdb3024ad | Land PR #1838 ledger sweep; close #147 mobile CLS by measurement | Merge resolved as union (main renumbered #302/#303 to #306/#307 — not lost, correcting an earlier claim); #306/#307 archived as already-delivered. #147 archived on two identical offline Lighthouse runs: mobile CLS 0.035/0.000/0.013/0.081/0.000, all under 0.1, cause fixed by PR #1616 not this session. #118 updated (browser drift 141-vs-151, wider than recorded); new #308 for desktop /documents/search CLS 0.119 | verify:pr-local 10/10 green; check:outstanding-issues 121 open/185 archived; verify:lighthouse x2 (gate ungraded on browser drift, measurements valid) | -| 2026-08-12 | codex/implement-process-safety-for-multi-agent-workflows | 6d054c1fa02a988829def3274b32d31c13570851 | full PR diff and unresolved review feedback | Fixed cached-origin truthfulness, agent-safe approval gates, UI browser proof ordering, index handoff safety, and synced main | focused Vitest 31/31; tsc --noEmit pass; git status clean | -| 2026-08-12 | codex/implement-verification-policy-changes-for-multiple-tasks | 8177129497eb95105cdd5bba80dbf72a9f88b066 | pr-review | resolved actionable Codex review findings; updated operational-risk patterns; removed outdated metadata from PR body and title | verify:cheap, pr-policy self-test | +| 2026-08-12 | PR-1836 | eef2e275b7d5944206f1a27e23fdd78d95b383d8 | full PR diff and unresolved review feedback | No P0-P2 findings; prior review feedback already fixed and dispositioned | focused ledger/docs/workflow guards pass after current-main merge | +| 2026-08-12 | claude/segmented-control-count | d5ff3160242986d2fac80ed9a915390b5c69b6e1 | SegmentedControl option hint slot (filter rollout prerequisite) | PR #1848 opened; additive, no call site passes hint yet; fixed a concatenated accessible name (All62 -> All (62)) | lint/typecheck/test green, 80 in ui-v2-components, 56 design-sync with no regen needed, clean build, bundle-budget within tolerance | +| 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-facet-formulation | 9262d89701808c58e3812a60b982596bb3f2b218 | filter contract PR B: formulation facet adoption (derive domains, union counts, evict query-replacing presets) | PR #1858 opened; domain converted from 12 radios to 9 derived facet chips (Biological/Social/Cultural carried by 0 of 12 mechanisms, removed per derive-dont-declare); union counts verified monotonic and non-additive (Affect 9 OR Risk 4 = 10); zero-yield options render as focusable dead ends, never on an already-selected option; Pattern group evicted from the sheet to AnswerSuggestionChips (all 5 presets, old slice(0,4) left one unreachable); ResultFilterFacetChips exported so desktop rail and sheet share one renderer; desktop select of 13 retired. Shares result-filter-control.tsx with PR #1857 - land #1857 first, its accessible-name fix then covers these chips | verify:pr-local failed:(none) not reached:(none), all 15 steps green including build with the server stopped; full unit suite 558/558 files, 6101 passed 4 skipped, zero failures; formulation.test.ts 11 passed with 3 new contract tests; bundle-budget production 1296.1 KiB and mockups 285.1 KiB within tolerance on a freshness-verified build; browser proof 1440/390/320px, 0px overflow, 9 chips not 12, 0 selects, 5 suggestion chips, live dead-end-to-selectable transition on union widening, phone sheet radiogroup count 0 | +| 2026-08-12 | codex/implement-verification-policy-changes-for-multiple-tasks | 8177129497eb95105cdd5bba80dbf72a9f88b066 | pr-review | resolved actionable Codex review findings; updated operational-risk patterns; removed outdated metadata from PR body and title | verify:cheap, pr-policy self-test | | 2026-08-12 | PR-1837 | 401aadc1d24a99067eb0472dc4419d85502e0caa | full PR diff and unresolved review feedback | No P0-P2 findings after current review fixes; existing dispositions verified | focused workflow policy and ledger guards pass after current-main merge | | 2026-08-12 | codex/implement-verification-policy-changes-for-multiple-tasks | 2fed97c601948b959120ff73e88be54af1dc84ba | pr-review | clean | check:pr-policy, verify:pr-local --dry-run | -| 2026-08-12 | PR-1836 | eef2e275b7d5944206f1a27e23fdd78d95b383d8 | full PR diff and unresolved review feedback | No P0-P2 findings; prior review feedback already fixed and dispositioned | focused ledger/docs/workflow guards pass after current-main merge | -| 2026-08-12 | claude/segmented-control-count | d5ff3160242986d2fac80ed9a915390b5c69b6e1 | SegmentedControl option hint slot (filter rollout prerequisite) | PR #1848 opened; additive, no call site passes hint yet; fixed a concatenated accessible name (All62 -> All (62)) | lint/typecheck/test green, 80 in ui-v2-components, 56 design-sync with no regen needed, clean build, bundle-budget within tolerance | -| 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 | 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 | | 2026-08-12 | 1849 | 34e5581c4418ef8a08909dff9ecf91d4a8f622de | full PR diff and unresolved review feedback | P1 setup-only PAT remained accessible through gh credential storage; removed agent-phase PAT persistence and retained safe base/shim changes | check:codex-cloud PASS; docs:check-inventory PASS; focused Vitest blocked by active repository lease | -| 2026-08-12 | claude/filter-facet-formulation | 9262d89701808c58e3812a60b982596bb3f2b218 | filter contract PR B: formulation facet adoption (derive domains, union counts, evict query-replacing presets) | PR #1858 opened; domain converted from 12 radios to 9 derived facet chips (Biological/Social/Cultural carried by 0 of 12 mechanisms, removed per derive-dont-declare); union counts verified monotonic and non-additive (Affect 9 OR Risk 4 = 10); zero-yield options render as focusable dead ends, never on an already-selected option; Pattern group evicted from the sheet to AnswerSuggestionChips (all 5 presets, old slice(0,4) left one unreachable); ResultFilterFacetChips exported so desktop rail and sheet share one renderer; desktop select of 13 retired. Shares result-filter-control.tsx with PR #1857 - land #1857 first, its accessible-name fix then covers these chips | verify:pr-local failed:(none) not reached:(none), all 15 steps green including build with the server stopped; full unit suite 558/558 files, 6101 passed 4 skipped, zero failures; formulation.test.ts 11 passed with 3 new contract tests; bundle-budget production 1296.1 KiB and mockups 285.1 KiB within tolerance on a freshness-verified build; browser proof 1440/390/320px, 0px overflow, 9 chips not 12, 0 selects, 5 suggestion chips, live dead-end-to-selectable transition on union widening, phone sheet radiogroup count 0 | +| 2026-08-12 | codex/implement-process-safety-for-multi-agent-workflows | 06144a7fcde8fea5dca63348bcc968fe796b152e | full PR diff and unresolved review feedback | Fixed cached-origin truthfulness, agent-safe approval gates, UI browser proof ordering, Added-date JSON, and foreign-index handoff safety; prior #240/#253 classifier feedback already fixed. | focused Vitest 31/31; outstanding-issues guard pass; gate-manifest pass; git diff --check | +| 2026-08-12 | codex/implement-process-safety-for-multi-agent-workflows | 6d054c1fa02a988829def3274b32d31c13570851 | full PR diff and unresolved review feedback | Fixed cached-origin truthfulness, agent-safe approval gates, UI browser proof ordering, index handoff safety, and synced main | focused Vitest 31/31; tsc --noEmit pass; git status clean | | 2026-08-13 | PR-1845 | 795ce38e165ce44e167038839a914b2efdb77dae | current-main merge, CI repair, and open-comment review | preserved the current-main ledger; canonicalized whitespace-only q to the non-empty legacy query; replaced the stale clear-filter Playwright locator; no unresolved review threads remained | pending fresh GitHub CI | | 2026-08-12 | PR #1854 / codex/chat-differentials-results-design-differentials-results-design | 86f7d22c0b5d6204708547740fc44228853a9662 | review-and-fix | Fixed the append-only ledger conflict and added a truthful zero-count result-type empty state with reset action; synced current main. | focused Differentials DOM test passed; typecheck passed; fresh hosted CI required on final head | diff --git a/docs/site-map.md b/docs/site-map.md index 8a332e752b..b7cb6e7908 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -1018,6 +1018,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/mockups/favourites-review-console` - Route discovered from app directory Source: `src/app/mockups/favourites-review-console/page.tsx`. - `/mockups/favourites-set-board` - Route discovered from app directory Source: `src/app/mockups/favourites-set-board/page.tsx`. - `/mockups/favourites-set-navigator` - Route discovered from app directory Source: `src/app/mockups/favourites-set-navigator/page.tsx`. +- `/mockups/filter-sheet-restyle` - Route discovered from app directory Source: `src/app/mockups/filter-sheet-restyle/page.tsx`. - `/mockups/medication-prescribing` - Route discovered from app directory Source: `src/app/mockups/medication-prescribing/page.tsx`. - `/mockups/mode-dropdown` - Route discovered from app directory Source: `src/app/mockups/mode-dropdown/page.tsx`. - `/mockups/phone-inpage-navigation` - Route discovered from app directory Source: `src/app/mockups/phone-inpage-navigation/page.tsx`. diff --git a/mockups/README.md b/mockups/README.md index a06679be58..97f7c69e5a 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -127,3 +127,35 @@ has to be unpicked afterwards. The facet engine, chips, band and sheet shell are imported from the round-one study rather than copied — the ~1KB bitmask table would otherwise be duplicated against a finite `mockups` bundle budget, and two studies quoting different numbers for the same catalogue would discredit both. + +### Round three — restyle, and a job for the segment bar (2026-08-12) + +Runnable study at [`/mockups/filter-sheet-restyle`](../src/app/mockups/filter-sheet-restyle/page.tsx). Rounds one and +two settled the information architecture; this one is about craft, drawn on the **formulation** sheet because that is +the specimen that stresses the layout hardest — four domain themes, twelve domains, twelve mechanisms, four presets, thirteen domain chips, and the longest title in the app. + +**The segment bar carries scope, not a verb.** Round two used it for "narrow these / start a new search", which is a +mode set once and rarely changed — a poor use of the most valuable strip in the sheet. Here it is +`These results 2 | All mechanisms 12`, with live counts on both segments. That is a decision the reader makes +constantly and which nothing in the product currently answers: filtering two results by twelve domains is close to +pointless, and today the only way to reach the full set is to clear the query and lose it. It also makes the empty +state recoverable — the commit button becomes "Show N in all mechanisms" instead of a dead end. + +| Style | Shape | Best for | +| -------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| A — Refined clinical | Shipped structure, craft fixed: tinted header, eyebrow/title/live-count, ghost close, uniform counted chips | Cheapest adoption, no new containers | +| B — Themed cards | Each theme a card with icon, label and the library's own description, tinting when it holds a selection | Many options without a word cloud | +| C — Dense list | Full-width rows, proportion bar, right-aligned count column, sticky group headings | Scanning to a known domain | + +Three defects the study documents, all verifiable in source: + +1. **Biological, Social and Cultural match zero of the twelve mechanisms.** They are offered as domain chips and can + never return anything. Counts expose this on sight; without them it is invisible. +2. **`formulationDomainGroups` already exists** in `src/lib/formulation.ts` — four themes, each with a written + description — and the sheet ignores it, rendering one flat ragged wrap of twelve chips. +3. **`formulationSearchPresets.slice(0, 4)` of five** means "If it is not perfect" is unreachable from the filter. + +One deliberate departure from the services study: the per-option count here is the **intrinsic** count (how many +mechanisms carry that domain), not "the total if I added this". Domains are a single OR group, so the union contract +reports the unchanged total for an empty domain — Cultural would read `7`, indistinguishable from a full one. The +commit button remains the thing that predicts the outcome. diff --git a/src/app/mockups/filter-sheet-restyle/page.tsx b/src/app/mockups/filter-sheet-restyle/page.tsx new file mode 100644 index 0000000000..b3cfd2ed3b --- /dev/null +++ b/src/app/mockups/filter-sheet-restyle/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +import { FilterSheetRestyleMockupsPage } from "@/components/filter-sheet-restyle-mockups"; + +export const metadata: Metadata = { + title: "Filter sheet restyle · three styles - Clinical KB", + description: + "Three style directions for the shared filter sheet, drawn on the formulation specimen, with the segmented bar repurposed as a result-scope control.", +}; + +export default function FilterSheetRestyleMockupPage() { + return ; +} diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index 6a2af37cd5..b07c2a9610 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -37,6 +37,10 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { // beside it — that is the concept under judgement, so the shared composer // above it would be a second, real search bar competing with the proposal. const isServicesFilterOptionsMockup = pathname === "/mockups/services-filter-options"; + // Draws its own formulation band and an in-frame filter sheet in every device + // frame; the shared composer would read as a second, real search bar over a + // study about the control that opens from that band. + const isFilterSheetRestyleMockup = pathname === "/mockups/filter-sheet-restyle"; const isPhoneInPageNavigationMockup = pathname === "/mockups/phone-inpage-navigation"; // Draws its own composer in every frame, and the notice under study is the one // the shared composer renders — showing both would put two different privacy @@ -94,6 +98,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isSearchBandDirectionsMockup && !isServicesFilterRefinedMockup && !isServicesFilterOptionsMockup && + !isFilterSheetRestyleMockup && !isTherapyNavigationMockup && !isWarningConsolidationMockup && !isWarningLineMockup && @@ -111,6 +116,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isSearchBandDirectionsMockup && !isServicesFilterRefinedMockup && !isServicesFilterOptionsMockup && + !isFilterSheetRestyleMockup && !isPhoneInPageNavigationMockup && !isTherapyNavigationMockup && !isWarningConsolidationMockup && diff --git a/src/components/AccessibleTable.tsx b/src/components/AccessibleTable.tsx index aea33441b3..0a77faabb4 100644 --- a/src/components/AccessibleTable.tsx +++ b/src/components/AccessibleTable.tsx @@ -328,7 +328,9 @@ function AccessibleTableMarkup({ className={cn( "block min-w-0", renderDensePreview - ? "truncate text-2xs leading-4" + ? cell + ? "truncate text-2xs leading-4" + : "whitespace-normal break-words text-2xs leading-4" : "text-sm leading-6 md:text-inherit md:leading-inherit", )} > diff --git a/src/components/filter-sheet-restyle-mockups.tsx b/src/components/filter-sheet-restyle-mockups.tsx new file mode 100644 index 0000000000..4fc9913a61 --- /dev/null +++ b/src/components/filter-sheet-restyle-mockups.tsx @@ -0,0 +1,1334 @@ +"use client"; + +import { useCallback, useId, useMemo, useState } from "react"; +import { + Brain, + Check, + HeartPulse, + Layers, + ListFilter, + RotateCcw, + Shield, + Sparkles, + Users, + X, + type LucideIcon, +} from "lucide-react"; + +import { cn } from "@/components/ui-primitives"; +import { searchFormulationMechanisms } from "@/lib/formulation"; + +/** + * Restyling the shared filter sheet — three style directions, plus the answer to + * "what is the segmented bar actually for". + * + * The specimen is the FORMULATION sheet rather than services, because that is + * the one whose craft problems are worst and whose content stresses the layout + * hardest: four domain themes, twelve domains, twelve mechanisms, four presets, + * thirteen domain chips, and the longest title in the app + * ("Filter formulation mechanisms" — inside formulation mode, where three of + * those four words are already implied). + * + * Three concrete defects in the shipped sheet drive this, all verifiable: + * + * 1. `formulationDomainGroups` ALREADY EXISTS in `src/lib/formulation.ts` — + * four themed groups, each with a label and a written description — and the + * filter sheet ignores it completely, dumping all twelve domains into one + * flat ragged wrap. The structure was authored and then not used. + * 2. Biological, Social and Cultural are offered as domain chips and match + * ZERO of the twelve mechanisms. Three controls that can never return + * anything, indistinguishable from the nine that can. Counts expose this on + * sight; without them it is invisible. + * 3. The sheet renders `formulationSearchPresets.slice(0, 4)` of five, so + * "If it is not perfect" is silently unreachable from the filter. + * + * THE SEGMENT BAR. The previous study used it to pick a verb — narrow vs. start + * a new search. That was the wrong job for it: a verb switch is a mode you set + * once and rarely change, so a permanent bar spends the most valuable strip of + * the sheet on a rare decision. Here it carries SCOPE instead — "these 2" vs + * "all 12" — which is the decision a reader actually makes repeatedly, and which + * nothing in the product currently answers: + * + * - Filtering two results by twelve domains is close to pointless; the reader + * nearly always wants the full set. Today the only route there is to clear + * the search box and start again, losing the query. + * - It makes the empty state RECOVERABLE. "No mechanisms match" becomes "no + * mechanisms match in these 2 — see 7 in all 12", one tap away, instead of a + * dead end that forces a reset. + * - Both segments carry live counts, so the control explains itself without a + * label and the reader can see the cost of switching before switching. + * + * Every count here is real, computed live from the twelve mechanisms in + * `src/data/formulation-content.json` and their `domains` arrays. + * + * Sizing note: these render a 390px phone frame inside a wide page, so viewport + * `sm:` variants would resolve against the page rather than the frame. Every + * component takes an explicit `compact` flag instead. + */ + +/* ------------------------------------------------------------------------- */ +/* Real data — src/data/formulation-content.json */ +/* ------------------------------------------------------------------------- */ + +const MECHANISMS: ReadonlyArray<{ name: string; domains: ReadonlyArray }> = [ + { name: "Avoidance", domains: ["Behaviour", "Cognition", "Trauma", "Risk"] }, + { name: "Splitting", domains: ["Defence", "Interpersonal", "Affect", "Attachment"] }, + { name: "Shame", domains: ["Affect", "Trauma", "Attachment", "Interpersonal"] }, + { name: "Emotional dysregulation", domains: ["Affect", "Behaviour", "Interpersonal", "Risk"] }, + { name: "Reassurance seeking", domains: ["Interpersonal", "Attachment", "Cognition", "Behaviour"] }, + { name: "Attachment avoidance", domains: ["Attachment", "Interpersonal", "Developmental", "Defence"] }, + { name: "Negative core beliefs", domains: ["Cognition", "Developmental", "Attachment", "Affect"] }, + { name: "Projection", domains: ["Defence", "Interpersonal", "Affect", "Cognition"] }, + { name: "Rumination", domains: ["Cognition", "Affect", "Behaviour"] }, + { name: "Worry", domains: ["Cognition", "Affect", "Behaviour", "Risk"] }, + { name: "Dissociation", domains: ["Trauma", "Affect", "Behaviour", "Risk"] }, + { name: "Perfectionism", domains: ["Cognition", "Behaviour", "Affect", "Developmental"] }, +]; + +/** The query in the screenshot's band, resolved to the mechanisms it returns. */ +export const filterSheetRestyleCurrentQuery = "Worry"; +export const filterSheetRestyleCurrentSubset = searchFormulationMechanisms(filterSheetRestyleCurrentQuery).map( + (result) => result.mechanism.name, +); + +/** + * `formulationDomainGroups`, quoted verbatim from `src/lib/formulation.ts`. + * The descriptions are the library's own — the sheet has simply never shown them. + */ +const DOMAIN_GROUPS: ReadonlyArray<{ + id: string; + label: string; + description: string; + icon: LucideIcon; + domains: ReadonlyArray; +}> = [ + { + id: "meaning", + label: "Meaning and belief", + description: "How experience is interpreted and organised.", + icon: Brain, + domains: ["Cognition", "Developmental", "Cultural"], + }, + { + id: "emotion", + label: "Emotion and threat", + description: "Affect, trauma responses, and risk-relevant escalation.", + icon: HeartPulse, + domains: ["Affect", "Trauma", "Risk", "Biological"], + }, + { + id: "response", + label: "Coping and action", + description: "What the person does to manage distress or uncertainty.", + icon: Sparkles, + domains: ["Behaviour", "Social"], + }, + { + id: "relationship", + label: "Relationship and protection", + description: "Attachment strategies, interpersonal patterns, and defences.", + icon: Users, + domains: ["Attachment", "Interpersonal", "Defence"], + }, +]; + +type Scope = "current" | "all"; + +function scopeMechanisms(scope: Scope) { + return scope === "all" ? MECHANISMS : MECHANISMS.filter((m) => filterSheetRestyleCurrentSubset.includes(m.name)); +} + +/** Domains OR together: picking two domains widens, exactly like the services facets. */ +function matching(scope: Scope, selected: ReadonlySet) { + const pool = scopeMechanisms(scope); + if (selected.size === 0) return pool; + return pool.filter((m) => m.domains.some((d) => selected.has(d))); +} + +function useDomainCounts(scope: Scope, selected: ReadonlySet) { + return useMemo(() => { + const pool = scopeMechanisms(scope); + const total = matching(scope, selected).length; + const perDomain: Record = {}; + for (const group of DOMAIN_GROUPS) { + for (const domain of group.domains) { + // INTRINSIC count — how many mechanisms in this scope carry this domain — + // deliberately NOT the "total if I added this" contract the services study + // settled on. Domains are a single OR group, so adding one can only widen: + // with Cognition selected, "total if added" reports the unchanged union + // total (7) for Cultural, which has no mechanisms at all. That semantic + // makes an empty domain indistinguishable from a full one, which would + // hide the single most useful thing these counts have to say. The commit + // button remains the thing that predicts the outcome. + perDomain[domain] = pool.filter((m) => m.domains.includes(domain)).length; + } + } + return { total, perDomain, scopeSize: pool.length }; + }, [scope, selected]); +} + +/** Counts for the other scope, so the segment can advertise what switching buys. */ +function useScopeCounts(selected: ReadonlySet) { + return useMemo( + () => ({ + current: matching("current", selected).length, + all: matching("all", selected).length, + }), + [selected], + ); +} + +/* ------------------------------------------------------------------------- */ +/* Shared craft primitives */ +/* ------------------------------------------------------------------------- */ + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +/** + * The header the study is really about. + * + * Shipped: one heavy 24px string ("Filter formulation mechanisms") beside a + * boxed ✕ of almost equal visual weight, on flat white, with no indication of + * what is being filtered or how much of it is left. + * + * Here: an accent eyebrow carries the mode so the title need not repeat it, the + * title drops to the two words that are actually load-bearing, a live line + * underneath states the result of the current selection, and the close control + * becomes a quiet ghost circle — present, reachable, no longer competing with + * the title for first read. + */ +function SheetHeader({ + compact, + total, + scopeTotal, + onClear, + clearable, + tone = "tinted", +}: { + compact: boolean; + total: number; + scopeTotal: number; + onClear: () => void; + clearable: boolean; + tone?: "tinted" | "plain" | "accent"; +}) { + return ( +
+
+ + Formulation + +

+ Filter mechanisms +

+

+ {total} of{" "} + {scopeTotal} match +

+
+ {clearable ? ( + + ) : null} + +
+ ); +} + +/** + * The segmented bar, carrying SCOPE. + * + * Each segment states its own count, so the control needs no label and the + * reader can price the switch before making it. When the active scope has been + * filtered to nothing and the other has matches, the inactive segment is given + * the accent tint — the sheet points at its own way out. + */ +function ScopeSwitch({ + value, + onChange, + compact, + counts, +}: { + value: Scope; + onChange: (next: Scope) => void; + compact: boolean; + counts: { current: number; all: number }; +}) { + const segments: ReadonlyArray<{ value: Scope; label: string; count: number; icon: LucideIcon }> = [ + { value: "current", label: "These results", count: counts.current, icon: ListFilter }, + { value: "all", label: "All mechanisms", count: counts.all, icon: Layers }, + ]; + const rescue = counts.current === 0 && counts.all > 0; + return ( +
+ {segments.map((segment) => { + const active = value === segment.value; + const points = rescue && segment.value === "all"; + return ( + + ); + })} +
+ ); +} + +/** Uniform chip: checkbox, label, count. Zero-count reads as a different kind of thing. */ +function DomainChip({ + domain, + count, + selected, + compact, + onToggle, +}: { + domain: string; + count: number; + selected: boolean; + compact: boolean; + onToggle: () => void; +}) { + const empty = count === 0 && !selected; + const emptyId = `${useId()}-empty`; + return ( + + ); +} + +export function filterSheetRestyleDomainProportionPercent(count: number, scopeSize: number) { + if (scopeSize <= 0) return 0; + return Math.round((count / scopeSize) * 100); +} + +/** Full-width row variant for the dense direction. */ +function DomainRow({ + domain, + count, + scopeSize, + selected, + compact, + onToggle, +}: { + domain: string; + count: number; + scopeSize: number; + selected: boolean; + compact: boolean; + onToggle: () => void; +}) { + const empty = count === 0 && !selected; + return ( + + ); +} + +/** Live commit. Carries the escape hatch when the active scope is empty. */ +function CommitBar({ + compact, + total, + scope, + otherCount, + onSwitchScope, +}: { + compact: boolean; + total: number; + scope: Scope; + otherCount: number; + onSwitchScope: () => void; +}) { + if (total === 0 && otherCount > 0) { + return ( +
+

+ Nothing matches in {scope === "current" ? "these results" : "all mechanisms"}. +

+ +
+ ); + } + return ( + + ); +} + +/** In-frame phone sheet shell. */ +function SheetShell({ + header, + children, + footer, + maxHeight = "max-h-[88%]", +}: { + header: React.ReactNode; + children: React.ReactNode; + footer: React.ReactNode; + maxHeight?: string; +}) { + return ( +
+
+ +
+ {header} +
{children}
+
{footer}
+
+ ); +} + +/** The band the sheet opens from, for context in every frame. */ +function Band({ compact, count, active }: { compact: boolean; count: number; active: number }) { + return ( +
+
+ + {count}{" "} + mechanisms + · {filterSheetRestyleCurrentQuery} + + 0 + ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" + : "border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)]", + )} + > + + Filter + {active > 0 ? ( + + {active} + + ) : null} + +
+
+ ); +} + +/* ------------------------------------------------------------------------- */ +/* Style A — Refined clinical */ +/* ------------------------------------------------------------------------- */ + +function StyleABody({ + selected, + counts, + onToggle, + compact, +}: { + selected: ReadonlySet; + counts: { perDomain: Record }; + onToggle: (domain: string) => void; + compact: boolean; +}) { + return ( +
+ {DOMAIN_GROUPS.map((group) => { + const chosen = group.domains.filter((d) => selected.has(d)).length; + return ( +
+
+ +
+ {group.label} +
+ {chosen > 0 ? ( + + {chosen} + + ) : null} +
+
+ {group.domains.map((domain) => ( + onToggle(domain)} + /> + ))} +
+
+ ); + })} +
+ ); +} + +/* ------------------------------------------------------------------------- */ +/* Style B — Themed cards */ +/* ------------------------------------------------------------------------- */ + +function StyleBBody({ + selected, + counts, + onToggle, + compact, +}: { + selected: ReadonlySet; + counts: { perDomain: Record }; + onToggle: (domain: string) => void; + compact: boolean; +}) { + return ( +
+ {DOMAIN_GROUPS.map((group) => { + const chosen = group.domains.filter((d) => selected.has(d)).length; + return ( +
0 + ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)]/25" + : "border-[color:var(--border)] bg-[color:var(--surface)]", + )} + > +
+ 0 + ? "bg-[color:var(--clinical-accent)] text-white" + : "bg-[color:var(--surface-subtle)] text-[color:var(--decoration-soft)]", + )} + > + + + + + {group.label} + + {/* The library already wrote these descriptions; the shipped + sheet has simply never shown them. */} + + {group.description} + + + {chosen > 0 ? ( + + {chosen} + + ) : null} +
+
+ {group.domains.map((domain) => ( + onToggle(domain)} + /> + ))} +
+
+ ); + })} +
+ ); +} + +/* ------------------------------------------------------------------------- */ +/* Style C — Dense list */ +/* ------------------------------------------------------------------------- */ + +function StyleCBody({ + selected, + counts, + onToggle, + compact, +}: { + selected: ReadonlySet; + counts: { perDomain: Record; scopeSize: number }; + onToggle: (domain: string) => void; + compact: boolean; +}) { + return ( +
+ {DOMAIN_GROUPS.map((group) => ( +
+ {/* Sticky sub-head: with twelve domain rows the group label must survive the + scroll, or a reader loses which dimension they are in. */} +
+ + {group.label} +
+
+ {group.domains.map((domain) => ( + onToggle(domain)} + /> + ))} +
+
+ ))} +
+ ); +} + +/* ------------------------------------------------------------------------- */ +/* Today — the shipped sheet, reproduced */ +/* ------------------------------------------------------------------------- */ + +const TODAY_DOMAINS = [ + "All domains", + "Attachment", + "Trauma", + "Cognition", + "Affect", + "Behaviour", + "Interpersonal", + "Defence", + "Developmental", + "Biological", + "Social", + "Cultural", + "Risk", +]; + +function TodaySheet() { + return ( +
+ +
+
+ +
+
+

+ Filter formulation mechanisms +

+ + + +
+
+
+ Pattern +
+
+ {[ + "Current search", + "I keep going over it", + "What if something goes wrong?", + "Zero to one hundred", + "I do not need anyone", + ].map((label, index) => ( + + {index === 0 ? : null} + {label} + + ))} +
+
+ Domain +
+
+ {TODAY_DOMAINS.map((label, index) => ( + + {index === 0 ? : null} + {label} + + ))} +
+
+
+ 2 showing + + Done + +
+
+
+ ); +} + +const TODAY_DEFECTS: ReadonlyArray<{ label: string; detail: string }> = [ + { + label: "Three chips can never match anything", + detail: + "Biological, Social and Cultural are offered as domains and match 0 of the 12 mechanisms. Nothing distinguishes them from the nine that work. Counts expose this on sight.", + }, + { + label: "An authored grouping sits unused", + detail: + "formulationDomainGroups in src/lib/formulation.ts already sorts the twelve domains into four themes, each with a written description. The sheet ignores it and renders one flat ragged wrap.", + }, + { + label: "The title is the loudest thing on screen", + detail: + "“Filter formulation mechanisms” inside formulation mode — three of four words are implied. It is set at the same weight as the page H1 and paired with a boxed ✕ of near-equal weight.", + }, + { + label: "One preset is unreachable", + detail: + "The sheet renders formulationSearchPresets.slice(0, 4) of five, so “If it is not perfect” cannot be selected here at all.", + }, + { + label: "No counts, no scope, no commit", + detail: + "Every choice is blind, there is no way to filter beyond the 2 current results, and “2 showing” is a passive label beside a low-emphasis “Done”.", + }, +]; + +/* ------------------------------------------------------------------------- */ +/* Study frame */ +/* ------------------------------------------------------------------------- */ + +type StyleId = "a" | "b" | "c"; + +function StyleSheet({ + style, + compact, + scope, + onScope, + selected, + onToggle, + onClear, +}: { + style: StyleId; + compact: boolean; + scope: Scope; + onScope: (next: Scope) => void; + selected: ReadonlySet; + onToggle: (domain: string) => void; + onClear: () => void; +}) { + const counts = useDomainCounts(scope, selected); + const scopeCounts = useScopeCounts(selected); + const scopeTotal = scopeMechanisms(scope).length; + const otherCount = scope === "current" ? scopeCounts.all : scopeCounts.current; + + const header = ( + <> + 0} + tone={style === "b" ? "accent" : "tinted"} + /> +
+ +
+ + ); + + const body = + style === "a" ? ( + + ) : style === "b" ? ( + + ) : ( + + ); + + const footer = ( + onScope(scope === "current" ? "all" : "current")} + /> + ); + + if (compact) { + return ( +
+ + + {body} + +
+ ); + } + + return ( +
+
+ +
+ {matching(scope, selected) + .slice(0, 4) + .map((mechanism) => ( +
+ + {mechanism.name} + + {mechanism.domains.join(" · ")} + + +
+ ))} + {counts.total === 0 ? ( + + No mechanisms match this filter set. + + ) : counts.total > 4 ? ( + + {counts.total - 4} more + ) : null} +
+
+
+ {header} +
{body}
+
+ {footer} +
+
+
+ ); +} + +function PreviewFrame({ + label, + phone = false, + children, +}: { + label: string; + phone?: boolean; + children: React.ReactNode; +}) { + return ( +
+
+ + {label} + + {phone ? "390 px" : "1440 px"} +
+
+ {/* The desktop specimen's min-width is gated at `sm`. Unconditional, it + propagates out of the scroll box into the grid track and scrolls the + whole page ~45px at 320px — the blocking narrow breakpoint — which was + measured, not assumed. Below `sm` the specimen simply reflows; a + 1440px comp rendered at 320px carries no information anyway. */} +
{children}
+
+
+ ); +} + +function StyleSection({ + id, + eyebrow, + title, + lede, + strengths, + style, + shared, +}: { + id: string; + eyebrow: string; + title: string; + lede: string; + strengths: ReadonlyArray; + style: StyleId; + shared: { + scope: Scope; + onScope: (next: Scope) => void; + selected: ReadonlySet; + onToggle: (domain: string) => void; + onClear: () => void; + }; +}) { + return ( +
+
+
+ + {eyebrow} + +

+ {title} +

+

{lede}

+
+
+ {strengths.map((strength) => ( + + {strength} + + ))} +
+
+
+ + + + + + +
+
+ ); +} + +const craft: ReadonlyArray<{ term: string; detail: string }> = [ + { + term: "The title stops shouting the obvious", + detail: + "“Filter formulation mechanisms” becomes an accent eyebrow (Formulation) over a two-word title (Filter mechanisms) over a live result line. Three ranks of information in less vertical space than the single heavy string it replaces.", + }, + { + term: "Close is a ghost, not a box", + detail: + "The shipped ✕ is a bordered square at nearly the title's visual weight, so the eye lands on the dismiss control first. A borderless circle keeps the same 48px target and stops competing.", + }, + { + term: "The segment carries scope, not a verb", + detail: + "“These results 2 / All mechanisms 12”. A verb switch is set once; scope is chosen constantly, and until now the only way to widen past the current search was to clear the query and lose it.", + }, + { + term: "Zero-yield options are visibly different", + detail: + "Biological, Social and Cultural match nothing. Dashed border plus muted pair rather than opacity, so the distinction survives forced-colors, where border-style is kept and opacity is not.", + }, + { + term: "Groups come from the library, not the layout", + detail: + "The four themes and their descriptions are formulationDomainGroups, already written and already shipped. Using them costs nothing and replaces a twelve-chip ragged wrap with four labelled sets.", + }, + { + term: "The empty state has an exit", + detail: + "When a filter set empties the current results, the commit button becomes “Show N in all mechanisms”. The dead end becomes one tap, and the query is never discarded to reach it.", + }, +]; + +export function FilterSheetRestyleMockupsPage() { + const [scope, setScope] = useState("current"); + const [selected, setSelected] = useState>(() => new Set(["Cognition"])); + + const toggle = useCallback((domain: string) => { + setSelected((current) => { + const next = new Set(current); + if (next.has(domain)) next.delete(domain); + else next.add(domain); + return next; + }); + }, []); + + const clear = useCallback(() => setSelected(new Set()), []); + const shared = { scope, onScope: setScope, selected, onToggle: toggle, onClear: clear }; + const scopeCounts = useScopeCounts(selected); + + return ( +
+
+
+
+ + Shared filter sheet · restyle + +

+ Three styles, and a real job for the segment bar +

+

+ Drawn on the formulation sheet, which stresses the layout hardest: four domain themes, twelve domains, + twelve mechanisms, four presets, thirteen domain chips and the longest title in the app. The three + directions share one set of craft fixes and differ only in how the options are arranged — flat, carded, or + listed. +

+

+ Counts are live, from the 12 mechanisms in{" "} + src/data/formulation-content.json. All three frames share one + selection and one scope, so a domain chosen anywhere updates the whole page. +

+
+
+ + + Scope{" "} + + {scope === "all" ? "all mechanisms" : "these results"} + + {" · "} + {selected.size === 0 + ? "no domains selected" + : `${selected.size} domain${selected.size === 1 ? "" : "s"}`}{" "} + — {scope === "all" ? scopeCounts.all : scopeCounts.current} match + + {selected.size > 0 ? ( + + ) : null} +
+
+
+ +
+
+
+ Today +

+ The shipped formulation sheet +

+

+ Reproduced from the live component. The problems are craft, but two of them are also correctness. +

+
+
+ + + +
    + {TODAY_DEFECTS.map((defect) => ( +
  • +

    {defect.label}

    +

    {defect.detail}

    +
  • + ))} +
+
+
+ + + + + + + +
+
+

+ The craft fixes, shared by all three +

+

+ These are the parts that are not a matter of taste between the directions. +

+
+
+ {craft.map((entry, index) => ( +
0 && "border-t", + index % 2 === 1 && "sm:border-l", + index === 1 && "sm:border-t-0", + )} + > +
{entry.term}
+
{entry.detail}
+
+ ))} +
+
+ +
+

+ Before this becomes production +

+
    +
  1. + + Biological, Social and Cultural should be fixed, not just labelled. + {" "} + Counts make three dead chips visible, which is an improvement — but the real answer is either to tag + mechanisms into those domains or to stop offering them. Shipping a permanent “0” is a worse admission than + shipping nothing. +
  2. +
  3. + Scope needs a home in the URL. If it is + not round-tripped, a shared link silently reverts to the current-results scope and the recipient sees a + different set from the sender. +
  4. +
  5. + The pattern group is still a navigation.{" "} + These comps show only the domain dimension restyled; patterns replace the query, so they need the + separated, arrow-marked treatment from the earlier study rather than a checkbox. +
  6. +
  7. + + Style C’s sticky headings need a scroll-owner check. + {" "} + A sticky element inside the sheet body is a second scroll context on phones — read{" "} + docs/search-chrome-behaviour.md before adopting it. +
  8. +
  9. + Mockups skip two gates, not all of them.{" "} + Every control here is at min-h-tap (48px) on phone; do not + relax to 44px for generic a11y guidance, which reintroduces a known{" "} + ui-smoke flake. +
  10. +
+
+
+
+ ); +} diff --git a/tests/accessible-table.dom.test.tsx b/tests/accessible-table.dom.test.tsx index 547fefef64..6611cc5f28 100644 --- a/tests/accessible-table.dom.test.tsx +++ b/tests/accessible-table.dom.test.tsx @@ -107,6 +107,18 @@ describe("AccessibleTable (jsdom)", () => { expect(screen.queryByText("-")).not.toBeInTheDocument(); }); + it("keeps the full missing-value phrase readable in the dense 320px preview", () => { + render(); + + const missingValue = screen.getByTestId("missing-value"); + const valueWrapper = missingValue.parentElement; + + expect(missingValue).toHaveTextContent("Not recorded"); + expect(valueWrapper).not.toBeNull(); + expect(valueWrapper!).toHaveClass("whitespace-normal", "break-words"); + expect(valueWrapper!).not.toHaveClass("truncate"); + }); + it("opens the full-screen dialog when the mobile expand control is clicked", async () => { setMatchMedia(true); // emulate the mobile/coarse-pointer breakpoint that enables expansion const user = userEvent.setup(); diff --git a/tests/design-token-contract.test.ts b/tests/design-token-contract.test.ts index 013f90d7ae..30289c2842 100644 --- a/tests/design-token-contract.test.ts +++ b/tests/design-token-contract.test.ts @@ -158,6 +158,33 @@ describe("elevation ladder", () => { expect(tokens.get("--e0")).toBe("none"); }); + // `--shadow-focus` is retired (`#261`). It was not an elevation alias at all: + // it packed a 3px accent halo in FRONT of `--shadow-soft`, so its one consumer + // — `.chat-composer-shell-delta:focus-within` — painted a companion ring on + // top of the accent border swap, which is the second focus affordance the + // shared `:focus-visible` treatment is written to prevent. The composer now + // uses the sanctioned `outline: 2px solid var(--focus)`. + // + // Unlike the `--shadow-tight` assertion above this is not a raw substring + // check: the stylesheet comment at the composer rule names the retired token + // on purpose, so that the next author reaching for a focus halo finds the + // reason it is gone rather than re-deriving it. The two spellings below are + // the only ways the token can actually come back to life — a declaration and + // a `var()` consumer — so they are what the gate rejects. + it("keeps the retired --shadow-focus token deleted in every scope", () => { + for (const [name, stylesheet] of [ + ["globals.css", globals], + ["ckb-v2-tokens.css", v2Stylesheet], + ] as const) { + expect(stylesheet, `${name} redeclares --shadow-focus; focus is an outline, not a ring`).not.toContain( + "--shadow-focus:", + ); + expect(stylesheet, `${name} consumes --shadow-focus; focus is an outline, not a ring`).not.toContain( + "var(--shadow-focus)", + ); + } + }); + it("flattens the ladder itself under forced colors, not only the role aliases", () => { const forced = globals.slice(globals.indexOf("@media (forced-colors: active)")); for (const tier of ["--e1", "--e2", "--e3", "--e4"]) { @@ -167,6 +194,11 @@ describe("elevation ladder", () => { }); describe("status colour ranking", () => { + it.each(themes)("keeps warning body text AA-safe on the default surface in $name", ({ tokens, name }) => { + const ratio = contrastRatio(colourOf(tokens, "--warning"), colourOf(tokens, "--surface")); + expect(ratio, `${name} --warning body text on --surface`).toBeGreaterThanOrEqual(4.5); + }); + // Every status pair used to sit in a 4.6–5.2:1 band, so nothing read as more // urgent than anything else. it("clears 5.5:1 for every light-mode status pair", () => { diff --git a/tests/filter-sheet-restyle-mockups.test.ts b/tests/filter-sheet-restyle-mockups.test.ts new file mode 100644 index 0000000000..e443e12278 --- /dev/null +++ b/tests/filter-sheet-restyle-mockups.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { + filterSheetRestyleCurrentQuery, + filterSheetRestyleCurrentSubset, + filterSheetRestyleDomainProportionPercent, +} from "@/components/filter-sheet-restyle-mockups"; +import { formulationMechanisms, searchFormulationMechanisms } from "@/lib/formulation"; + +describe("filter sheet restyle mockup scope", () => { + it("grounds the two-result scope comparison in the production search", () => { + const productionSubset = searchFormulationMechanisms(filterSheetRestyleCurrentQuery).map( + (result) => result.mechanism.name, + ); + + expect([...filterSheetRestyleCurrentSubset].sort()).toEqual(["Reassurance seeking", "Worry"]); + expect([...productionSubset].sort()).toEqual([...filterSheetRestyleCurrentSubset].sort()); + expect(filterSheetRestyleCurrentSubset).toHaveLength(2); + expect(filterSheetRestyleCurrentSubset.length).toBeLessThan(formulationMechanisms.length); + }); + + it("scales dense-list proportions against the active scope", () => { + expect(filterSheetRestyleDomainProportionPercent(2, 2)).toBe(100); + expect(filterSheetRestyleDomainProportionPercent(1, 2)).toBe(50); + expect(filterSheetRestyleDomainProportionPercent(0, 0)).toBe(0); + }); +});