Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7510216
fix(ui): mark search empty-state tone by stroke and restore its deskt…
BigSimmo Aug 18, 2026
9171ae4
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
fb03f01
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
ed22af7
docs(ledger): record the search recovery rail review (PR #2147)
BigSimmo Aug 18, 2026
81dcda2
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
b15fbc8
docs(issues): queue hoisting filtered-zero out of the documents resul…
BigSimmo Aug 18, 2026
3265a50
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
ab0e658
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
6308eb7
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
1c6bf7b
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
ccca759
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
5101196
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
b249ee9
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
d5ee17a
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
13c0e65
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
416c741
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
1efd897
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 2026
6651799
Merge branch 'main' into claude/search-recovery-rail
BigSimmo Aug 18, 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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-18 | claude/search-recovery-rail | 7510216c672f293d26b8fdfe04a9ed3fd286b7f6 | SearchResultsEmptyState rail restyle + desktop tap floor restore (PR #2147) | approved — presentation-only; no copy, testid, heading-level, live-region or handler change; band untouched | typecheck 0 errors; eslint+prettier+format:changed clean; 10 DOM files/168 tests passed; chromium ui-accessibility 16 passed; full verify:ui not completed (lock contention + host exit) |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "714e714b-dd1d-4a10-8f95-8552fa2c5296",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P2",
"type": "task",
"summary": "Hoist the filtered-zero empty state out of the documents results grid so it can sit flush under the results band",
"detail": "SearchResultsEmptyState renders the filtered-to-zero case nested two divs inside the results grid in document-search-results.tsx (grid gap-3 wrapper, with a conditional 'N results after filters' pill above it), not adjacent to SearchResultsHeaderBand. That nesting blocks the inline treatment evaluated for PR #2147: recovery cannot share the band's bottom edge, the band's data-tone lead is not adjacent so the panel has no state mark, and the filter chips are not near enough for the panel to point at them instead of duplicating them. PR #2147 therefore shipped the self-contained rail panel, which does not depend on adjacency. Hoisting the state to a sibling of the band would let the panel drop its own rail, its named-removal row and its eyebrow — roughly a third less height on phones. Only the documents nesting was verified; the other five consumers (favourites, calculators, forms, services, therapy-compass) were not checked and may nest the same way.",
"source": "PR #2147 design review; document-search-results.tsx:1558",
"issueUlid": "01M0B36TGQGBBYTAN7G28RJHWW"
}
}
77 changes: 61 additions & 16 deletions src/components/clinical-dashboard/search-results-header-band.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -962,24 +962,65 @@ export function SearchResultsEmptyState({
].filter(Boolean);

return (
<div className="rounded-lg border border-dashed border-[color:var(--border-strong)] bg-[color:var(--surface-inset)] p-5 text-center shadow-[var(--shadow-inset)]">
<div className="relative overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] p-5 text-left shadow-[var(--shadow-inset)]">
{/* The state mark, carrying the same vocabulary as the band's own lead:
the stroke *count* — not the hue — is what separates a degraded search
from a healthy one, because `--clinical-accent` resolves to LinkText
under forced colors while `--warning` resolves to CanvasText, and hue
alone would render "filtered" and "could not complete" identically.
Utilities rather than an unlayered class: `.search-band-lead` is
unlayered precisely so it can beat Tailwind's utilities layer, and this
mark has no utility to beat — adding a class here would enlarge the
inventory `style-contract-registry` polices for nothing. `box-content`
+ `w-0` makes the border the entire width. Exactly one width class per
branch: two would both emit `border-left-width` and the winner would be
stylesheet order rather than the branch. */}
<span
aria-hidden
className={cn(
"absolute inset-y-0 left-0 box-content w-0",
degraded
? "border-l-[6px] border-double border-l-[color:var(--warning)]"
: filtered
? "border-l-[3px] border-solid border-l-[color:var(--clinical-accent)]"
: "border-l-[3px] border-solid border-l-[color:var(--border-strong)]",
"forced-colors:border-l-[color:CanvasText]",
)}
/>
{/* Visible copy is never inside the live region: a region that mounts
already populated is silent in most screen readers, and wrapping the
visible tree would flash empty for a frame. The sr-only live region
below is populated after mount (query-only) or omitted (filtered).
NOT `role="status"`: the band already owns that role on every search
route and a second one makes singular `getByRole("status")` ambiguous. */}
<span className="mx-auto grid h-tap w-tap place-items-center rounded-full bg-[color:var(--surface)] text-[color:var(--text-muted)]">
{degraded ? (
<CircleAlert className="h-5 w-5" aria-hidden />
) : filtered ? (
<Funnel className="h-5 w-5" aria-hidden />
) : (
<Search className="h-5 w-5" aria-hidden />
)}
</span>
<Title className="mt-3 text-sm font-semibold text-[color:var(--text-heading)]">{emptyTitle}</Title>
<p className="mt-1 text-xs font-medium text-[color:var(--text-muted)]">{emptyBody}</p>
{/* The glyph sits beside the heading rather than inside it: a 48px tinted
disc above centred copy is a container the icon vocabulary does not
have, and putting the SVG inside `Title` would put a node inside the
accessible name that every heading assertion resolves. */}
<div className="flex items-start gap-2.5">
<span
className={cn(
"mt-px shrink-0",
degraded
? "text-[color:var(--warning)]"
: filtered
? "text-[color:var(--clinical-accent)]"
: "text-[color:var(--text-muted)]",
)}
>
{degraded ? (
<CircleAlert className="h-4 w-4" aria-hidden />
) : filtered ? (
<Funnel className="h-4 w-4" aria-hidden />
) : (
<Search className="h-4 w-4" aria-hidden />
)}
</span>
<div className="min-w-0">
<Title className="text-sm font-semibold text-[color:var(--text-heading)]">{emptyTitle}</Title>
<p className="mt-1 text-xs font-medium text-[color:var(--text-muted)]">{emptyBody}</p>
</div>
</div>
{!filtered ? (
<div aria-live="polite" className="sr-only">
{liveMessage}
Expand All@@ -991,15 +1032,19 @@ export function SearchResultsEmptyState({
own action, which is also the fix for the single button that said
"Clear filters" and called `clearSearch`. */}
{filtered ? (
<div className="mt-3 flex flex-wrap items-center justify-center gap-1.5">
<div className="mt-3.5 flex flex-wrap items-center gap-1.5">
{lastFilter ? (
<button
key={lastFilter.id}
type="button"
onClick={lastFilter.onRemove}
data-testid="search-results-empty-remove-filter"
className={cn(
"inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-3 text-xs font-semibold text-[color:var(--clinical-accent)] hover:border-[color:var(--clinical-accent)] sm:min-h-9",
// `sm:min-h-10`, not `sm:min-h-9`: the desktop floor in this file
// is 40px — `emptyStateAction` above and every shelf control use
// it — and this is the control the filtered path leads with. The
// phone floor was already correct.
"inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-3 text-xs font-semibold text-[color:var(--clinical-accent)] hover:border-[color:var(--clinical-accent)] sm:min-h-10",
focusRing,
)}
>
Expand All@@ -1022,10 +1067,10 @@ export function SearchResultsEmptyState({
{secondary.length > 0 ? (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-1.5",
"flex flex-wrap items-center gap-1.5",
// Demoted below a rule once relaxing is on offer: an example query is
// a different search, and the reader has not finished this one.
filtered ? "mt-3.5 border-t border-[color:var(--border)] pt-3.5" : "mt-3",
filtered ? "mt-3.5 border-t border-[color:var(--border)] pt-3.5" : "mt-3.5",
)}
>
{secondary}
Expand Down
Loading