Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4aba58a
feat(filters): implement unified clinical filter overhaul
BigSimmo Aug 16, 2026
a64f415
test(filters): cover review regressions
BigSimmo Aug 16, 2026
667f8cc
fix(filters): normalize URL values after trimming
BigSimmo Aug 16, 2026
324ef62
fix(filters): align locality chip labels
BigSimmo Aug 16, 2026
4d31dff
fix(filters): defer and streamline differential projections
BigSimmo Aug 16, 2026
374ba3c
test(filters): cover locality chip casing
BigSimmo Aug 16, 2026
d6da003
test(filters): cover trimmed URL deduplication
BigSimmo Aug 16, 2026
3c9a2cb
fix(filters): defer document scope projections
BigSimmo Aug 16, 2026
0a947f4
Merge main into codex/filter-system-overhaul
BigSimmo Aug 17, 2026
def4df1
fix(filters): restore missing service nav and sheet safe-area types
BigSimmo Aug 17, 2026
89d764e
Merge remote-tracking branch 'refs/remotes/origin/main' into codex/pr…
BigSimmo Aug 17, 2026
df3c4ff
chore(ledger): record PR #1998 review outcome
BigSimmo Aug 17, 2026
7e55c06
fix(filters): address CodeRabbit review findings and test assertions
BigSimmo Aug 17, 2026
c9ac046
chore(ledger): record PR #1998 verified review outcome
BigSimmo Aug 17, 2026
fffe860
Merge remote-tracking branch 'refs/remotes/origin/main' into codex/pr…
BigSimmo Aug 17, 2026
ab426e5
Merge origin/main into codex/filter-system-overhaul (PR #1998 review-…
claude Aug 17, 2026
4cc41de
chore(design-system): regenerate adoption manifest and design-sync co…
claude Aug 17, 2026
e8ed1a0
fix(tools): restore phone footer-dock composer contract in ui-tools t…
claude Aug 17, 2026
d3b3e94
refactor(dashboard): extract document-filters-apply handler to stay u…
claude Aug 17, 2026
97f9055
Merge origin/main into codex/pr-1998-fix (PR #2009 URL-regex robustness)
claude Aug 17, 2026
b6c287f
chore(ledger): record PR #1998 CI-blocker fix review
claude Aug 17, 2026
aa7c3ff
fix(design-system): keep the tools directory changes inside the tap-t…
claude Aug 17, 2026
7e8ac75
chore(ledger): record PR #1998 second CI-blocker fix review
claude Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .design-sync/config.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| 2026-08-17 | codex/pr-1998-fix | 89d764ec9df835c3cb477d4e71859e62b55311cb | pr | PARTIAL-FIX | typecheck, tests(sheets+ui-tools), merge main, docs format |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| 2026-08-17 | pr-1998 | aa7c3ffd93ed3366090c719904dcaae600010775 | filters | fixed-second-ci-blocker | vitest,typecheck,eslint,design-system-contract,design-sync-contract,design-system-adoption,maintainability-budgets,build |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| 2026-08-17 | codex/filter-system-overhaul | 7e55c066e20be34beca8441b2e0f9c40be5fcb95 | pr #1998 clinical filter overhaul | PASS | typecheck, vitest (filters, sheets, search band, dom panels), prettier |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| 2026-08-17 | pr-1998 | 97f9055827ef7712f9b5bb98c4c4e2bfb73f69a0 | filters | fixed-ci-blockers | vitest,typecheck,eslint,design-sync-contract,design-system-adoption,maintainability-budgets,playwright-ui-tools-chromium,build |
4 changes: 2 additions & 2 deletions docs/design-system/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,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 | 12 |
| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 8 |
| `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 |
Expand All @@ -1012,7 +1012,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `TextField` | controls | yes | yes | inherited-global-root | yes | no | 4 |
| `TextLink` | controls | yes | yes | no | yes | no | 0 |
| `ToastRegion` | feedback | yes | yes | no | yes | no | 0 |
| `ToggleSwitch` | controls | yes | yes | inherited-global-root | yes | no | 3 |
| `ToggleSwitch` | controls | yes | yes | inherited-global-root | yes | no | 2 |
| `Tooltip` | feedback | yes | yes | no | yes | no | 0 |
| `VerificationNotice` | answer | yes | yes | inherited-global-root | yes | no | 1 |

Expand Down
16 changes: 3 additions & 13 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@
"tests/accessible-table.dom.test.tsx",
"tests/design-sync-visual-exports.test.ts",
"tests/design-system-adoption.test.ts",
"tests/ui-tools.spec.ts",
"tests/ward-output.test.ts"
],
"baseline": {
Expand Down Expand Up @@ -1399,28 +1398,20 @@
"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/factsheets/factsheets-search-page.tsx",
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/services/services-navigator-page.tsx",
"src/components/specifiers/specifiers-home-page.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
"src/components/therapy-compass/screens/sheets-screen.tsx",
"src/components/tools/tools-search-results-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/factsheets/factsheets-search-page.tsx",
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/services/services-navigator-page.tsx",
"src/components/specifiers/specifiers-home-page.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
"src/components/therapy-compass/screens/sheets-screen.tsx",
"src/components/tools/tools-search-results-page.tsx"
Expand Down Expand Up @@ -1475,6 +1466,7 @@
"tests/answer-progress-ui-smoke.spec.ts",
"tests/design-sync-visual-exports.test.ts",
"tests/eval-process-safety.test.ts",
"tests/search-results-header-band.dom.test.tsx",
"tests/ui-formulation.spec.ts",
"tests/ui-smoke.spec.ts",
"tests/ui-v2-components.dom.test.tsx"
Expand Down Expand Up @@ -1520,9 +1512,9 @@
"src/components/document-viewer/document-clinical-summary.tsx",
"src/components/document-viewer/section-nav.tsx",
"src/components/forms/form-detail-page.tsx",
"src/components/forms/forms-search-results-page.tsx",
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/mode-nav/mode-nav.tsx",
"src/components/services/service-group-nav.tsx",
"src/components/tools/tools-search-results-page.tsx",
"src/components/ui/confirm-dialog.tsx"
],
Expand All @@ -1548,9 +1540,9 @@
"src/components/document-viewer/document-clinical-summary.tsx",
"src/components/document-viewer/section-nav.tsx",
"src/components/forms/form-detail-page.tsx",
"src/components/forms/forms-search-results-page.tsx",
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/mode-nav/mode-nav.tsx",
"src/components/services/service-group-nav.tsx",
"src/components/tools/tools-search-results-page.tsx"
],
"designSync": {
Expand Down Expand Up @@ -1935,12 +1927,10 @@
"entryExported": true,
"directImportFiles": [
"src/components/clinical-dashboard/patient-profile-panel.tsx",
"src/components/forms/forms-search-results-page.tsx",
"src/components/therapy-compass/screens/sheets-screen.tsx"
],
"productImportFiles": [
"src/components/clinical-dashboard/patient-profile-panel.tsx",
"src/components/forms/forms-search-results-page.tsx",
"src/components/therapy-compass/screens/sheets-screen.tsx"
],
"designSync": {
Expand Down
68 changes: 28 additions & 40 deletions docs/search-results-bar-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,24 @@ contract: see [search-chrome-behaviour.md](search-chrome-behaviour.md).
one control sitting flush against the sort group rendered as a different component. It
now uses the band's own control recipe — the same string `Save search` and `Retry` use —
with the active/resting colours as mutually exclusive branches.
- **Every mode's phone filter is now the badged trigger, not a select.** The `w-full`
native select is gone from all seven surfaces that shipped one — differentials, services,
factsheets, prescribing, the tools launcher, and formulation and specifiers, which each
passed _two_ in a two-column grid. Each now passes one `ResultFilterTrigger`
(`result-filter-control.tsx`) with `mobileControlsPlacement="inline"`, so the one-line
band is universal rather than a documents/therapy-compass exception. What the select cost:
a whole second band row; no way to say how many filters were active without spending
label width on it; and, because the iOS anti-zoom rule pins every native select to 16 px
below `sm`, a value rendered at the same size as the query heading above it. Single-choice
dimensions moved into `ResultFilterSheet`, one `role="radiogroup"` per dimension — real
radio semantics, because these are one-of-N and an `aria-pressed` bank asserts otherwise.
Documents keeps its own panel; multi-select facet groups with counts, a find-a-filter
field and collapse-by-default are not radios.
- **Every mode uses one adaptive filter system, not a phone-only substitute.** Each result
surface passes `ResultFilterTrigger` at phone and desktop breakpoints. The trigger opens
`ResultFilterSheet`, which is a bottom sheet on phones and a restrained right drawer from
`sm` up. The old native selects, bespoke desktop dropdowns and competing rails are gone.
Lenses render as roving radio groups; facets render as individually reachable multi-select
rows; `Search in` is a labelled, count-bearing scope selector. Dense catalogues gain search
and collapsible groups from the shared component rather than from route-local panels.
Documents now uses the same panel with staged application and a typed secondary Browse all
sources action. The filter contract, including mode ownership, is in
[filter-contract.md](filter-contract.md).
- **`mobileControlsPlacement` still defaults to `row`.** Nothing relies on that fallback
now that every caller passes `inline`, and it stays anyway: a new mode that forgets the
prop, or one with a genuine reason to hand over something full-width, should degrade to a
second row rather than to an unreadable 58 px line at 320 px. Do not flip the default.
- **Filter at the right edge, Sort inboard.** Sort is set about once a session. Filter is
the only control carrying state and the one returned to repeatedly, and on a phone the
right edge is where the thumb already is. The page filter is therefore the utility
rail's **last** child; `tests/ui-tools.spec.ts` asserts that placement and a matched
phone tap height, rather than the Sort/Filter adjacency it asserted before.
- **Filter is pinned at the right edge; Sort stays inboard.** Sort is set about once a
session. Filter carries narrowing state and is returned to repeatedly, so it is a pinned
sibling after the optional utility rail rather than a child that can scroll away.
`tests/ui-tools.spec.ts` asserts the placement and matched phone tap height.
- **The shelf.** A labelled `Filtered by` row under the bar, one tap to remove each
filter, trailing `Clear` once more than one is applied. The chips scroll in an inner
track; the label and `Clear` are pinned outside it, because the shelf was one
Expand All @@ -98,31 +94,23 @@ contract: see [search-chrome-behaviour.md](search-chrome-behaviour.md).
on every keystroke. Only a fault removes it, because filtering a result set that never
loaded is meaningless.

## The shelf is scoped to two modes, on purpose
## The shelf is shared state, not a mode exception

`documents` and `therapy-compass` only. Both have multi-valued filters hidden behind a
panel, so what is applied is not otherwise visible. Of the other ten results-band modes:
Any mode with hidden, multi-dimensional or non-default scope state passes `appliedFilters`
and `onClearFilters`. The shelf is prop-driven; the shared bar never infers refinement state
from context. `AppliedFilterChip` carries a compact `valueLabel`, an optional desktop
`groupLabel`, a complete `accessibleLabel` when necessary, and `onRemove`.

- Differentials, prescribing, specifiers, formulation, services, and factsheets keep a
single-choice dimension whose desktop control is already on screen, and whose phone
trigger carries a count badge, so a shelf would restate what is visible either way.
- Forms still ships a Filter trigger whose panel is a coming-soon placeholder, not applied
filter state, so there is nothing for a shelf to show.
- Favourites renders its own active-filter chips inside `filterControls` rather than the
shared shelf props.
- DSM filters by category through navigation links, and tools through a single category
dimension — neither passes `appliedFilters`.
The shelf is required for Documents, Services, Forms, Differential workspaces, DSM,
Specifiers, Formulation, Medication, Therapy, Calculators when narrowed, and Favourites. A
compact category lens may omit it only when the active value remains directly visible at that
breakpoint. Selected retrieval and result facets remain visible at zero results so recovery is
always possible.

Two traps met while drawing that line:

- **Count what a control does, not how many there are.** Formulation's "Pattern" and
factsheets' "Category" look like filters and are navigation (`router.push`). Services'
"quick filter" rewrites the query. None of them belong behind a filter surface.
- **A shared component must not read filter state from context.** The original shelf
pulled `commandScopes` from a context that no page populated. It passed a DOM test which
constructed that context by hand, and rendered for nobody in production. The replacement
is prop-driven — the page supplies `appliedFilters` (`id`, `label`, `onRemove`) and an
optional `onClearFilters`. Keep it that way.
Count what narrows results, not how many controls happen to be visible. Scope is counted when
non-default. Sort, display density, grouping, comparison state, suggested searches and
Recently used views are excluded. Query-replacing presets belong near the composer, never in
the shelf or filter badge.

## Deliberately not done

Expand Down
9 changes: 2 additions & 7 deletions src/app/(search-app)/dsm/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from "next";

import { DsmSearchPage } from "@/components/dsm/dsm-search-page";
import { dsmCategories, dsmDiagnoses, listDsmDiagnosisSummaries } from "@/lib/dsm";
import { dsmCategories, listDsmDiagnosisSummaries } from "@/lib/dsm";

export const metadata: Metadata = {
title: "Search DSM diagnoses | Clinical KB",
Expand All @@ -13,7 +13,6 @@ type DsmSearchRouteProps = {
searchParams?: Promise<{
q?: string | string[];
query?: string | string[];
category?: string | string[];
ids?: string | string[];
}>;
};
Expand All @@ -25,8 +24,6 @@ function firstValue(value?: string | string[]) {
export default async function DsmSearchRoute({ searchParams }: DsmSearchRouteProps) {
const params = searchParams ? await searchParams : {};
const query = (firstValue(params.q) ?? firstValue(params.query) ?? "").trim();
const requestedCategory = firstValue(params.category)?.trim();
const category = dsmCategories.some((item) => item.key === requestedCategory) ? requestedCategory : undefined;
const rawIds = firstValue(params.ids) ?? "";
const initialIds = rawIds
? rawIds
Expand All @@ -38,10 +35,8 @@ export default async function DsmSearchRoute({ searchParams }: DsmSearchRoutePro
return (
<DsmSearchPage
query={query}
category={category}
categories={dsmCategories}
results={listDsmDiagnosisSummaries({ query, category })}
totalCount={dsmDiagnoses.length}
results={listDsmDiagnosisSummaries({ query })}
initialIds={initialIds}
/>
);
Expand Down
6 changes: 5 additions & 1 deletion src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
import { useAuthSession } from "@/lib/supabase/client";
import { useEventCallback } from "@/components/clinical-dashboard/use-event-callback";
import { useScopeFilterRelax } from "@/components/clinical-dashboard/use-scope-filter-relax";
import { useApplyFilters } from "@/components/clinical-dashboard/use-apply-filters";
import { AuthPanel } from "@/components/clinical-dashboard/auth-panel";
import { buildMobileSectionFabState, MobileSectionFab, ToolsHub } from "@/components/clinical-dashboard/dashboard-nav";
import * as SidebarDialogs from "@/components/clinical-dashboard/lazy-sidebar-dialogs";
Expand Down Expand Up @@ -2187,7 +2188,7 @@ export function ClinicalDashboard({
if (searchMode === "documents" && trimmedQuery) {
rememberRecentQuery(trimmedQuery);
autoRunSearchSignatureRef.current = searchSubmissionSignature(searchMode, trimmedQuery, navigationContext);
window.history.pushState(
window.history[replaceExistingAnswer ? "replaceState" : "pushState"](
null,
"",
documentsSearchHref({
Expand Down Expand Up @@ -3217,6 +3218,7 @@ export function ClinicalDashboard({
const handleCrossModeSearch = useEventCallback(crossModeSearch);
const handleDocumentTagSearch = useEventCallback(handleTagSearch);
const handleScopeFiltersChange = useScopeFilterRelax(query, queryMode, setScopeFilters, ask);
const handleDocumentFiltersApply = useApplyFilters(query, queryMode, setScopeFilters, setSelectedDocumentIds, askRef);
const handleOpenRecentDocuments = useEventCallback(openRecentDocuments);
const handleOpenSourceLibrary = useEventCallback(openSourceLibrary);
const handleDocumentsDrawerOpenChange = useEventCallback((nextOpen: boolean) => {
Expand Down Expand Up @@ -3762,6 +3764,8 @@ export function ClinicalDashboard({
onTagSearch={handleDocumentTagSearch}
scopeFilters={searchMode === "documents" ? scopeFilters : null}
onScopeFiltersChange={searchMode === "documents" ? handleScopeFiltersChange : undefined}
selectedDocumentIds={searchMode === "documents" ? selectedDocumentIds : []}
onDocumentFiltersApply={searchMode === "documents" ? handleDocumentFiltersApply : undefined}
showHome={searchMode === "documents" && !modeSearchSubmitted}
desktopComposerSlotId={desktopHomeComposerSlotId}
/>
Expand Down
Loading
Loading