diff --git a/docs/filter-contract.md b/docs/filter-contract.md
index 52d89db67c..ec26f06e2b 100644
--- a/docs/filter-contract.md
+++ b/docs/filter-contract.md
@@ -116,17 +116,17 @@ corpus of that size, and it stays.
## 5. Density is a function of option count
-Facet groups only. Two states, not three: the shared renderer uses the same threshold for its
-find-a-filter field and collapse-by-default disclosures.
+Facet groups only. Density scales with option and group volume across three tiers:
-| Options | Renderer |
-| --------------------------- | ---------------------------------------------------------------------------------------- |
-| ≤ 3 groups and ≤ 20 options | chips, single row where they fit (unchanged from before this section) |
-| > 3 groups, or > 20 options | chips plus find-a-filter and collapse-by-default, every group behind a disclosure header |
+| Options / Groups | Renderer |
+| --------------------------- | -------------------------------------------------------------------------------------- |
+| ≤ 5 options | chips, single row / wrapping chips |
+| 6–20 options | dense full-width vertical list with right-aligned count column and group headings |
+| > 3 groups, or > 20 options | list/chips plus find-a-filter and collapse-by-default, every group behind a disclosure |
-`ResultFilterSheet` computes the threshold once across all facet groups. The option-count limb
-catches a small number of very large groups, while the group-count limb covers services' six
-facet groups. Below the threshold every group renders as before.
+`ResultFilterSheet` computes the threshold across facet groups. Facet groups containing 6–20 options
+render as compact full-width rows with a right-aligned count column for fast scanning. When a sheet
+exceeds 3 groups or 20 total options, it additionally adds find-a-filter and collapse-by-default chrome.
Collapse rules, when they apply: groups start collapsed; a group holding a selection opens
itself; an explicit user collapse beats that; an active needle forces every matched group open
diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx
index bafb30038e..459da9790b 100644
--- a/src/components/DocumentViewer.tsx
+++ b/src/components/DocumentViewer.tsx
@@ -1404,7 +1404,10 @@ export function DocumentViewer({
a phone reader sees the clinical priorities digest before scrolling
past the PDF. */}
{readyDocument ? (
-
+
)}
- {/* No privacy link here: the composer's PrivacyInputNotice is the
- single site-wide notice, so the hero footer must not repeat it. */}
- {/* Pre-query copy must describe what the search does, not assert that
- every indexed source is verified/current (PT-06): validation status
- varies per document and is surfaced on the results themselves. */}
+ {modeId === "answer" ? (
+
+ ) : null}
}
/>
diff --git a/src/components/clinical-dashboard/favourites-command-library-page.tsx b/src/components/clinical-dashboard/favourites-command-library-page.tsx
index 5b1a72668a..0fa8256ba6 100644
--- a/src/components/clinical-dashboard/favourites-command-library-page.tsx
+++ b/src/components/clinical-dashboard/favourites-command-library-page.tsx
@@ -35,6 +35,14 @@ import {
type FavouriteItem as PrototypeFavouriteItem,
} from "@/components/clinical-dashboard/favourites-prototype-data";
import { useSavedRegistryFavourites } from "@/components/clinical-dashboard/use-saved-registry-favourites";
+import {
+ formatLastOpened,
+ lastOpenedScore,
+ loadFavouriteLastOpened,
+ loadFavouritePinnedIds,
+ recordFavouriteOpened,
+ subscribeFavouritesStorage,
+} from "@/components/favourites/favourites-storage";
import {
SearchResultsEmptyState,
SearchResultsHeaderBand,
@@ -112,8 +120,6 @@ const lastUsedByItemId: Record
= {
"qt-prolongation-quote": "Mon 11:03",
};
-const pinnedItemIds = new Set(["acamprosate-renal-screen", "lithium-monitoring-guideline"]);
-
const typeByPrototypeType: Record = {
medications: "Medication",
documents: "Document",
@@ -186,7 +192,11 @@ async function copyFavouriteCitation(item: FavouriteItem): Promise {
}
}
-function toCommandItem(item: PrototypeFavouriteItem): FavouriteItem {
+function toCommandItem(
+ item: PrototypeFavouriteItem,
+ lastOpenedMap: Record,
+ pinnedIds: ReadonlySet,
+): FavouriteItem {
const type =
item.type === "sources" && item.primaryAction === "Run"
? "Saved search"
@@ -199,11 +209,14 @@ function toCommandItem(item: PrototypeFavouriteItem): FavouriteItem {
tabId: item.type,
set: item.set || (item.type === "services" ? "Saved services" : item.type === "forms" ? "Saved forms" : "Unsorted"),
evidence: item.sourceMeta,
- lastUsed: lastUsedByItemId[item.id] ?? "Saved",
+ lastUsed:
+ lastOpenedMap[item.id] !== undefined
+ ? formatLastOpened(lastOpenedMap[item.id])
+ : (lastUsedByItemId[item.id] ?? "Saved"),
action: item.primaryAction,
href: item.href,
icon: item.icon ?? fallbackIconByType[item.type],
- pinned: pinnedItemIds.has(item.id),
+ pinned: pinnedIds.has(item.id),
};
}
@@ -1106,9 +1119,18 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?:
status: favouritesHookStatus,
refetch: refetchFavouritesRegistry,
} = useSavedRegistryFavourites();
+ const lastOpenedMap = useSyncExternalStore(
+ subscribeFavouritesStorage,
+ loadFavouriteLastOpened,
+ () => ({}) as Record,
+ );
+ const pinnedIds = useSyncExternalStore(subscribeFavouritesStorage, loadFavouritePinnedIds, () => new Set());
const items = useMemo(
- () => [...(demoMode ? prototypeFavouriteItems : []), ...savedRegistryFavourites].map(toCommandItem),
- [demoMode, savedRegistryFavourites],
+ () =>
+ [...(demoMode ? prototypeFavouriteItems : []), ...savedRegistryFavourites].map((item) =>
+ toCommandItem(item, lastOpenedMap, pinnedIds),
+ ),
+ [demoMode, savedRegistryFavourites, lastOpenedMap, pinnedIds],
);
// Demo prototypes live outside the hook. If they are the only items while a
// registry/account read failed, keep their honest nonzero count but mark it
@@ -1162,7 +1184,7 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?:
const recentItems = useMemo(
() =>
[...items]
- .sort((first, second) => lastUsedScore(second.lastUsed) - lastUsedScore(first.lastUsed))
+ .sort((first, second) => lastOpenedScore(second.lastUsed) - lastOpenedScore(first.lastUsed))
.slice(0, recentPreviewLimit),
[items],
);
@@ -1517,7 +1539,10 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?:
sortMode={sortMode}
selectedItemId={selectedItemId}
onSortModeChange={setSortMode}
- onSelectItem={setSelectedItemId}
+ onSelectItem={(id) => {
+ if (id) recordFavouriteOpened(id);
+ setSelectedItemId(id);
+ }}
/>
)}
@@ -1548,6 +1573,7 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?:
recordFavouriteOpened(item.id)}
aria-label={`Open ${item.title}`}
className={cn(
"inline-flex min-h-tap shrink-0 items-center rounded-lg border border-[color:var(--border)] px-2.5 text-xs font-bold text-[color:var(--text)] hover:bg-[color:var(--surface-subtle)] sm:min-h-9",
diff --git a/src/components/clinical-dashboard/result-filter-control.tsx b/src/components/clinical-dashboard/result-filter-control.tsx
index 71fbd46246..e9ede86aeb 100644
--- a/src/components/clinical-dashboard/result-filter-control.tsx
+++ b/src/components/clinical-dashboard/result-filter-control.tsx
@@ -479,7 +479,12 @@ export function ResultFilterFacetChips({
const panelId = idPrefix;
const groupLabelId = `${panelId}-${group.id}-label`;
const visibleOptions = options ?? group.options;
- const renderOptions = (items: ReadonlyArray>) =>
+ const isDenseList =
+ !group.optionSections &&
+ ((group.options.length >= 6 && group.options.length <= 20) ||
+ (visibleOptions.length >= 6 && visibleOptions.length <= 20));
+
+ const renderOptions = (items: ReadonlyArray>, isDense: boolean = isDenseList) =>
items.map((option) => {
const selected = group.selected.has(option.value);
const deadEnd = Boolean(option.disabled) && !selected;
@@ -497,7 +502,9 @@ export function ResultFilterFacetChips({
group.onToggle(option.value);
}}
className={cn(
- "inline-flex min-h-tap max-w-full items-center gap-1.5 rounded-md border px-2.5 text-2xs font-semibold shadow-[var(--shadow-inset)] transition motion-reduce:transition-none sm:min-h-10 sm:gap-1 sm:px-2",
+ isDense
+ ? "flex min-h-tap w-full min-w-0 items-center justify-between gap-2.5 rounded-lg border px-3 py-2 text-left text-xs font-semibold shadow-[var(--shadow-inset)] transition motion-reduce:transition-none sm:min-h-9 sm:py-1.5"
+ : "inline-flex min-h-tap max-w-full items-center gap-1.5 rounded-md border px-2.5 text-2xs font-semibold shadow-[var(--shadow-inset)] transition motion-reduce:transition-none sm:min-h-10 sm:gap-1 sm:px-2",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]",
selected
? "border-[color:var(--clinical-accent)]/35 bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"
@@ -506,19 +513,25 @@ export function ResultFilterFacetChips({
: "border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] text-[color:var(--text-muted)] hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)]",
)}
>
-
- {selected ? : null}
-
- {option.label}
- {option.hint ? {option.hint} : null}
+
+
+ {selected ? : null}
+
+ {option.label}
+
+ {option.hint ? (
+
+ {option.hint}
+
+ ) : null}
{deadEnd ? (
No matches with your current filters.
@@ -583,12 +596,18 @@ export function ResultFilterFacetChips({
hidden={disclosure ? !disclosure.open : false}
role="group"
aria-labelledby={groupLabelId}
- className={cn("pb-2.5", group.optionSections ? "grid gap-3" : "flex flex-wrap gap-2 sm:gap-1.5")}
+ className={cn(
+ "pb-2.5",
+ group.optionSections ? "grid gap-3" : isDenseList ? "grid gap-1" : "flex flex-wrap gap-2 sm:gap-1.5",
+ )}
>
{group.optionSections
? group.optionSections.map((section) => {
const sectionOptions = visibleOptions.filter((option) => section.optionValues.includes(option.value));
if (sectionOptions.length === 0) return null;
+ const sectionDense =
+ (section.optionValues.length >= 6 && section.optionValues.length <= 20) ||
+ (sectionOptions.length >= 6 && sectionOptions.length <= 20);
return (
@@ -599,11 +618,13 @@ export function ResultFilterFacetChips({
) : null}
- {renderOptions(sectionOptions)}
+
+ {renderOptions(sectionOptions, sectionDense)}
+
);
})
- : renderOptions(visibleOptions)}
+ : renderOptions(visibleOptions, isDenseList)}
);
diff --git a/src/components/document-viewer/document-rail-panels.tsx b/src/components/document-viewer/document-rail-panels.tsx
index 3918810d69..58dec45dd1 100644
--- a/src/components/document-viewer/document-rail-panels.tsx
+++ b/src/components/document-viewer/document-rail-panels.tsx
@@ -138,7 +138,7 @@ export function DocumentViewerRail({
data-testid="high-yield-summary"
className={cn(
panel,
- "group min-w-0 scroll-mt-[var(--document-anchor-offset,6rem)] source-print md:col-span-2 lg:col-span-1",
+ "group min-w-0 max-sm:hidden print:block scroll-mt-[var(--document-anchor-offset,6rem)] source-print md:col-span-2 lg:col-span-1",
)}
>
= {
"source-evidence": ["source-evidence-rail"],
+ // The rail's document-profile disclosure owns the canonical anchor (the spy
+ // treats it as an exclusive-accordion member), while the in-flow clinical
+ // summary card above the PDF is its phone/tablet copy.
+ "source-summary": ["source-summary-card"],
};
/**
diff --git a/src/components/favourites/favourites-storage.ts b/src/components/favourites/favourites-storage.ts
new file mode 100644
index 0000000000..a658faba01
--- /dev/null
+++ b/src/components/favourites/favourites-storage.ts
@@ -0,0 +1,197 @@
+"use client";
+
+export const DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY = "database:favourites:last-opened-v1";
+export const DATABASE_FAVOURITES_PINNED_STORAGE_KEY = "database:favourites:pinned-v1";
+
+const DEFAULT_PINNED_ITEM_IDS = ["acamprosate-renal-screen", "lithium-monitoring-guideline"];
+
+// Initial baseline offsets for demo prototype items before user interactions
+function getDefaultInitialTimestamps(): Record {
+ const now = Date.now();
+ const dayMs = 24 * 60 * 60 * 1000;
+ return {
+ "acamprosate-renal-screen": now - 15 * 60 * 1000, // 15 mins ago (today)
+ "lithium-monitoring-guideline": now - 35 * 60 * 1000, // 35 mins ago (today)
+ "renal-dose-search": now - 55 * 60 * 1000, // 55 mins ago (today)
+ "clozapine-monitoring-table": now - dayMs - 2 * 60 * 60 * 1000, // yesterday
+ "qt-prolongation-quote": now - 3 * dayMs, // earlier this week
+ };
+}
+
+let inMemoryLastOpened: Record | null = null;
+let inMemoryPinned: Set | null = null;
+const listeners = new Set<() => void>();
+
+function notifyListeners() {
+ for (const listener of listeners) {
+ try {
+ listener();
+ } catch {
+ // Ignore listener errors
+ }
+ }
+}
+
+// Single shared storage handler at module level to avoid O(N²) callbacks when
+// multiple subscribers are active (each per-subscriber handler would call
+// notifyListeners(), firing all listeners N times per storage event).
+let sharedStorageListenerAttached = false;
+function ensureSharedStorageListener() {
+ if (sharedStorageListenerAttached || typeof window === "undefined") return;
+ sharedStorageListenerAttached = true;
+ window.addEventListener("storage", (event: StorageEvent) => {
+ if (
+ event.key === DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY ||
+ event.key === DATABASE_FAVOURITES_PINNED_STORAGE_KEY
+ ) {
+ inMemoryLastOpened = null;
+ inMemoryPinned = null;
+ notifyListeners();
+ }
+ });
+}
+
+export function subscribeFavouritesStorage(listener: () => void): () => void {
+ ensureSharedStorageListener();
+ listeners.add(listener);
+ return () => {
+ listeners.delete(listener);
+ };
+}
+
+export function loadFavouriteLastOpened(): Record {
+ if (typeof window === "undefined") {
+ return getDefaultInitialTimestamps();
+ }
+ if (inMemoryLastOpened) return inMemoryLastOpened;
+
+ try {
+ const raw = localStorage.getItem(DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ if (typeof parsed === "object" && parsed !== null) {
+ const result: Record = { ...getDefaultInitialTimestamps(), ...parsed };
+ inMemoryLastOpened = result;
+ return result;
+ }
+ }
+ } catch {
+ // Fallback on JSON parse / storage errors
+ }
+
+ const fallback = getDefaultInitialTimestamps();
+ inMemoryLastOpened = fallback;
+ return fallback;
+}
+
+export function recordFavouriteOpened(itemId: string, timestamp: number = Date.now()): Record {
+ const current: Record = { ...loadFavouriteLastOpened(), [itemId]: timestamp };
+ inMemoryLastOpened = current;
+ if (typeof window !== "undefined") {
+ try {
+ localStorage.setItem(DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY, JSON.stringify(current));
+ } catch {
+ // Ignore storage write errors (e.g. quota)
+ }
+ }
+ notifyListeners();
+ return current;
+}
+
+export function loadFavouritePinnedIds(): Set {
+ if (typeof window === "undefined") {
+ return new Set(DEFAULT_PINNED_ITEM_IDS);
+ }
+ if (inMemoryPinned) return inMemoryPinned;
+
+ try {
+ const raw = localStorage.getItem(DATABASE_FAVOURITES_PINNED_STORAGE_KEY);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ if (Array.isArray(parsed)) {
+ const result = new Set(parsed.filter((id): id is string => typeof id === "string"));
+ inMemoryPinned = result;
+ return result;
+ }
+ }
+ } catch {
+ // Fallback on JSON parse / storage errors
+ }
+
+ const fallback = new Set(DEFAULT_PINNED_ITEM_IDS);
+ inMemoryPinned = fallback;
+ return fallback;
+}
+
+export function toggleFavouritePinnedId(itemId: string): Set {
+ const current = new Set(loadFavouritePinnedIds());
+ if (current.has(itemId)) {
+ current.delete(itemId);
+ } else {
+ current.add(itemId);
+ }
+ inMemoryPinned = current;
+ if (typeof window !== "undefined") {
+ try {
+ localStorage.setItem(DATABASE_FAVOURITES_PINNED_STORAGE_KEY, JSON.stringify(Array.from(current)));
+ } catch {
+ // Ignore storage write errors
+ }
+ }
+ notifyListeners();
+ return current;
+}
+
+const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+export function formatLastOpened(timestampOrLabel: number | string | undefined): string {
+ if (timestampOrLabel === undefined || timestampOrLabel === null) {
+ return "Saved";
+ }
+ if (typeof timestampOrLabel === "string") {
+ return timestampOrLabel;
+ }
+
+ const date = new Date(timestampOrLabel);
+ if (Number.isNaN(date.getTime())) {
+ return "Saved";
+ }
+
+ const now = new Date();
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const itemDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+ const diffDays = Math.round((today.getTime() - itemDate.getTime()) / (24 * 60 * 60 * 1000));
+
+ const hours = String(date.getHours()).padStart(2, "0");
+ const mins = String(date.getMinutes()).padStart(2, "0");
+ const timeStr = `${hours}:${mins}`;
+
+ if (diffDays === 0) {
+ return `Today ${timeStr}`;
+ }
+ if (diffDays === 1) {
+ return `Yesterday ${timeStr}`;
+ }
+ if (diffDays >= 2 && diffDays <= 6) {
+ return `${dayNames[date.getDay()]} ${timeStr}`;
+ }
+ return `${date.getDate()} ${monthNames[date.getMonth()]}`;
+}
+
+export function lastOpenedScore(lastUsed: string | number | undefined): number {
+ if (typeof lastUsed === "number") {
+ return lastUsed;
+ }
+ if (!lastUsed) return 0;
+
+ const lower = lastUsed.toLowerCase();
+ if (lower.startsWith("today")) {
+ const timeMatch = lastUsed.match(/(\d{1,2}):(\d{2})/);
+ if (timeMatch) return 1_000_000_000_000 + Number(timeMatch[1]) * 60 + Number(timeMatch[2]);
+ return 1_000_000_000_000;
+ }
+ if (lower.startsWith("yesterday")) return 500_000_000_000;
+ if (dayNames.some((d) => lower.startsWith(d.toLowerCase()))) return 100_000_000_000;
+ return 1_000;
+}
diff --git a/src/components/search/ResultFilterSheet.tsx b/src/components/search/ResultFilterSheet.tsx
new file mode 100644
index 0000000000..56d3025c1c
--- /dev/null
+++ b/src/components/search/ResultFilterSheet.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+export {
+ ResultFilterSheet,
+ ResultFilterTrigger,
+ ResultFilterFacetChips,
+ ResultFilterScopeSelector,
+ resultFilterGroup,
+ resultFilterFacetGroup,
+ isFacetGroup,
+ type ResultFilterOption,
+ type ResultFilterOptionSection,
+ type ResultFilterGroupKind,
+ type ResultFilterLensGroup,
+ type ResultFilterFacetGroup,
+ type ResultFilterGroup,
+ type ResultFilterScopeOption,
+ type ResultFilterScopeConfig,
+ type ResultFilterSummary,
+ type ResultFilterSecondaryAction,
+} from "@/components/clinical-dashboard/result-filter-control";
diff --git a/tests/favourites.test.ts b/tests/favourites.test.ts
new file mode 100644
index 0000000000..f69622aaa3
--- /dev/null
+++ b/tests/favourites.test.ts
@@ -0,0 +1,79 @@
+/** @vitest-environment jsdom */
+
+import { beforeEach, describe, expect, it } from "vitest";
+
+import {
+ DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY,
+ DATABASE_FAVOURITES_PINNED_STORAGE_KEY,
+ formatLastOpened,
+ lastOpenedScore,
+ loadFavouriteLastOpened,
+ loadFavouritePinnedIds,
+ recordFavouriteOpened,
+ toggleFavouritePinnedId,
+} from "@/components/favourites/favourites-storage";
+
+describe("favourites storage, timestamps and pinning", () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it("loads default seed timestamps and records real timestamp when item is opened", () => {
+ const initial = loadFavouriteLastOpened();
+ expect(initial["acamprosate-renal-screen"]).toBeDefined();
+ expect(typeof initial["acamprosate-renal-screen"]).toBe("number");
+
+ const customTime = Date.now() + 5000;
+ recordFavouriteOpened("test-item-1", customTime);
+
+ const updated = loadFavouriteLastOpened();
+ expect(updated["test-item-1"]).toBe(customTime);
+
+ const storedRaw = localStorage.getItem(DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY);
+ expect(storedRaw).not.toBeNull();
+ const parsed = JSON.parse(storedRaw!);
+ expect(parsed["test-item-1"]).toBe(customTime);
+ });
+
+ it("loads default pinned IDs and allows toggling pinning state with localStorage persistence", () => {
+ const initialPinned = loadFavouritePinnedIds();
+ expect(initialPinned.has("acamprosate-renal-screen")).toBe(true);
+ expect(initialPinned.has("custom-unpinned-id")).toBe(false);
+
+ toggleFavouritePinnedId("custom-unpinned-id");
+ const updated = loadFavouritePinnedIds();
+ expect(updated.has("custom-unpinned-id")).toBe(true);
+
+ const storedRaw = localStorage.getItem(DATABASE_FAVOURITES_PINNED_STORAGE_KEY);
+ expect(storedRaw).not.toBeNull();
+ const parsed = JSON.parse(storedRaw!);
+ expect(parsed).toContain("custom-unpinned-id");
+
+ toggleFavouritePinnedId("custom-unpinned-id");
+ const reverted = loadFavouritePinnedIds();
+ expect(reverted.has("custom-unpinned-id")).toBe(false);
+ });
+
+ it("formats timestamps into human-readable relative strings", () => {
+ const now = Date.now();
+ const formattedNow = formatLastOpened(now);
+ expect(formattedNow).toMatch(/^Today \d{2}:\d{2}$/);
+
+ const yesterday = now - 24 * 60 * 60 * 1000;
+ const formattedYesterday = formatLastOpened(yesterday);
+ expect(formattedYesterday).toMatch(/^Yesterday \d{2}:\d{2}$/);
+
+ expect(formatLastOpened(undefined)).toBe("Saved");
+ expect(formatLastOpened("Today 08:44")).toBe("Today 08:44");
+ });
+
+ it("computes sort scores correctly prioritizing recent timestamps", () => {
+ const t1 = Date.now();
+ const t2 = t1 - 10000;
+
+ expect(lastOpenedScore(t1)).toBeGreaterThan(lastOpenedScore(t2));
+ expect(lastOpenedScore("Today 10:00")).toBeGreaterThan(lastOpenedScore("Yesterday 10:00"));
+ expect(lastOpenedScore("Yesterday 10:00")).toBeGreaterThan(lastOpenedScore("Mon 10:00"));
+ expect(lastOpenedScore("Saved")).toBe(1000);
+ });
+});
diff --git a/tests/filter-contract.dom.test.tsx b/tests/filter-contract.dom.test.tsx
new file mode 100644
index 0000000000..d8f58fae4b
--- /dev/null
+++ b/tests/filter-contract.dom.test.tsx
@@ -0,0 +1,163 @@
+import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import {
+ ResultFilterSheet,
+ resultFilterFacetGroup,
+ resultFilterGroup,
+ type ResultFilterOption,
+} from "@/components/search/ResultFilterSheet";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("filter contract and density rendering", () => {
+ it("renders facet groups with <= 5 options as compact wrapping chips", () => {
+ const onToggle = vi.fn();
+ const group = resultFilterFacetGroup({
+ id: "small-facet",
+ label: "Category",
+ selected: new Set(["a"]),
+ options: [
+ { value: "a", label: "Option A", hint: "3" },
+ { value: "b", label: "Option B", hint: "5" },
+ { value: "c", label: "Option C", hint: "2" },
+ ],
+ onToggle,
+ });
+
+ render(
+ ,
+ );
+
+ const buttonA = screen.getByRole("button", { name: /^Option A/ });
+ expect(buttonA).toHaveAttribute("aria-pressed", "true");
+ expect(buttonA.className).toContain("inline-flex");
+ expect(buttonA.className.split(/\s+/)).not.toContain("w-full");
+ expect(buttonA.className).toContain("min-h-tap");
+ });
+
+ it("renders facet groups with 6–20 options as dense full-width vertical list with right-aligned counts", async () => {
+ const user = userEvent.setup();
+ const onToggle = vi.fn();
+ const options: ResultFilterOption[] = Array.from({ length: 9 }, (_, i) => ({
+ value: `option-${i + 1}`,
+ label: `Domain ${i + 1}`,
+ hint: `${(i + 1) * 2}`,
+ }));
+
+ const group = resultFilterFacetGroup({
+ id: "dense-domains",
+ label: "Domains",
+ selected: new Set(["option-2"]),
+ options,
+ onToggle,
+ });
+
+ render(
+ ,
+ );
+
+ const groupEl = screen.getByRole("group", { name: "Domains" });
+ expect(groupEl).toBeInTheDocument();
+ expect(groupEl.className).toContain("grid");
+
+ const buttons = within(groupEl).getAllByRole("button");
+ expect(buttons).toHaveLength(9);
+
+ const firstButton = buttons[0];
+ expect(firstButton.className).toContain("w-full");
+ expect(firstButton.className).toContain("justify-between");
+ expect(firstButton.className).toContain("min-h-tap");
+ expect(firstButton).toHaveAttribute("aria-pressed", "false");
+
+ const secondButton = buttons[1];
+ expect(secondButton).toHaveAttribute("aria-pressed", "true");
+
+ await user.click(firstButton);
+ expect(onToggle).toHaveBeenCalledWith("option-1");
+ });
+
+ it("adds find-a-filter and disclosure header when total facet options exceed 20 or facet groups exceed 3", () => {
+ const onToggle = vi.fn();
+ const groups = Array.from({ length: 4 }, (_, groupIndex) =>
+ resultFilterFacetGroup({
+ id: `facet-group-${groupIndex + 1}`,
+ label: `Group ${groupIndex + 1}`,
+ selected: new Set(),
+ options: [
+ { value: `g${groupIndex}-1`, label: `Item 1`, hint: "1" },
+ { value: `g${groupIndex}-2`, label: `Item 2`, hint: "2" },
+ ],
+ onToggle,
+ }),
+ );
+
+ render(
+ ,
+ );
+
+ expect(screen.getByTestId("super-dense-panel-find")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /Group 1/ })).toHaveAttribute("aria-expanded", "false");
+ });
+
+ it("handles roving radio selection for lens groups", () => {
+ const onChange = vi.fn();
+ const lensGroup = resultFilterGroup({
+ id: "view-lens",
+ label: "View",
+ value: "all",
+ options: [
+ { value: "all", label: "All items" },
+ { value: "presentations", label: "Presentations" },
+ { value: "diagnoses", label: "Diagnoses" },
+ ],
+ onChange,
+ });
+
+ render(
+ ,
+ );
+
+ const radioAll = screen.getByRole("radio", { name: "All items" });
+ expect(radioAll).toHaveAttribute("aria-checked", "true");
+ expect(radioAll).toHaveAttribute("tabindex", "0");
+
+ const radioPres = screen.getByRole("radio", { name: "Presentations" });
+ expect(radioPres).toHaveAttribute("aria-checked", "false");
+ expect(radioPres).toHaveAttribute("tabindex", "-1");
+
+ fireEvent.click(radioPres);
+ expect(onChange).toHaveBeenCalledWith("presentations");
+ });
+});
diff --git a/tests/filter-contract.test.ts b/tests/filter-contract.test.ts
new file mode 100644
index 0000000000..05368331a6
--- /dev/null
+++ b/tests/filter-contract.test.ts
@@ -0,0 +1,31 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { describe, expect, it } from "vitest";
+
+function source(path: string) {
+ return readFileSync(resolve(process.cwd(), path), "utf8");
+}
+
+describe("filter contract structural verification", () => {
+ it("exports ResultFilterSheet from src/components/search/ResultFilterSheet.tsx", () => {
+ const filterExport = source("src/components/search/ResultFilterSheet.tsx");
+ expect(filterExport).toContain("ResultFilterSheet");
+ expect(filterExport).toContain("ResultFilterTrigger");
+ expect(filterExport).toContain("ResultFilterFacetChips");
+ });
+
+ it("implements density tier detection in result-filter-control.tsx", () => {
+ const control = source("src/components/clinical-dashboard/result-filter-control.tsx");
+ expect(control).toContain("isDenseList");
+ expect(control).toContain("group.options.length >= 6");
+ expect(control).toContain("group.options.length <= 20");
+ expect(control).toContain("min-h-tap");
+ });
+
+ it("documents density tiers in docs/filter-contract.md", () => {
+ const doc = source("docs/filter-contract.md");
+ expect(doc).toContain("6–20 options");
+ expect(doc).toContain("dense full-width vertical list");
+ expect(doc).toContain("≤ 5 options");
+ });
+});
diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts
index 65682065d5..645269dd67 100644
--- a/tests/ui-smoke.spec.ts
+++ b/tests/ui-smoke.spec.ts
@@ -4886,7 +4886,12 @@ test.describe("Clinical KB UI smoke coverage", () => {
await expect(passages.nth(0)).toHaveJSProperty("open", false);
await clickSectionNav(/High-yield summary/);
- await expect(summary).toHaveJSProperty("open", true);
+ // At this 390px viewport the rail's high-yield-summary disclosure is
+ // hidden (superseded by the in-flow DocumentClinicalSummary card), so
+ // there is nothing for the exclusive accordion to open here —
+ // jumpToDocumentSection scrolls to the visible copy instead.
+ await expect(page.locator("#source-summary-card")).toBeInViewport();
+ await expect(summary).toHaveJSProperty("open", false);
await expect(indexedText).toHaveJSProperty("open", false);
await openImagesDisclosure();