Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions docs/design-system/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1080,7 +1080,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `IconButton` | controls | yes | yes | inherited-global-root | yes | no | 3 |
| `InlineNotice` | feedback | yes | yes | inherited-global-root | yes | no | 33 |
| `LinkAction` | controls | yes | yes | no | yes | no | 0 |
| `LoadingPanel` | feedback | yes | yes | inherited-global-root | yes | no | 11 |
| `LoadingPanel` | feedback | yes | yes | inherited-global-root | yes | no | 10 |
| `MissingValue` | feedback | yes | yes | inherited-global-root | yes | no | 11 |
| `OverlayRoot` | layout | yes | yes | inherited-global-root | yes | no | 1 |
| `PageHeader` | layout | yes | yes | inherited-global-root | yes | no | 16 |
Expand All @@ -1095,7 +1095,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 33 |
| `Select` | controls | yes | yes | inherited-global-root | yes | no | 16 |
| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 136 |
| `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 8 |
| `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 9 |
| `SourceDesignationBadge` | source | yes | yes | inherited-global-root | yes | no | 4 |
| `SourceProvenance` | source | yes | yes | inherited-global-root | yes | no | 1 |
| `SourceStatusBadge` | source | yes | yes | inherited-global-root | yes | no | 4 |
Expand Down
4 changes: 2 additions & 2 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1563,7 +1563,6 @@
"src/app/(search-app)/dictionary/topics/page.tsx",
"src/components/clinical-dashboard/clinical-dashboard-lazy.tsx",
"src/components/clinical-dashboard/dashboard-nav.tsx",
"src/components/clinical-dashboard/document-search-results.tsx",
"src/components/clinical-dashboard/global-search-shell.tsx",
"src/components/clinical-dashboard/medication-record-page.tsx",
"src/components/document-viewer/document-visuals-panel.tsx",
Expand All @@ -1576,7 +1575,6 @@
"src/app/(search-app)/dictionary/topics/page.tsx",
"src/components/clinical-dashboard/clinical-dashboard-lazy.tsx",
"src/components/clinical-dashboard/dashboard-nav.tsx",
"src/components/clinical-dashboard/document-search-results.tsx",
"src/components/clinical-dashboard/global-search-shell.tsx",
"src/components/clinical-dashboard/medication-record-page.tsx",
"src/components/document-viewer/document-visuals-panel.tsx",
Expand Down Expand Up @@ -2613,6 +2611,7 @@
"src/app/(search-app)/open-shifts/loading.tsx",
"src/app/(search-app)/roster/loading.tsx",
"src/components/clinical-dashboard/document-search-results.tsx",
"src/components/clinical-dashboard/result-rows-placeholder.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/mode-home-page-skeleton.tsx"
],
Expand All @@ -2623,6 +2622,7 @@
"src/app/(search-app)/open-shifts/loading.tsx",
"src/app/(search-app)/roster/loading.tsx",
"src/components/clinical-dashboard/document-search-results.tsx",
"src/components/clinical-dashboard/result-rows-placeholder.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/mode-home-page-skeleton.tsx"
],
Expand Down
4 changes: 4 additions & 0 deletions src/components/clinical-dashboard/answer-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "@/components/clinical-dashboard/answer-evidence-preview";
import type { VerifiedEvidencePreviewUnit } from "@/lib/answer-stream-contract";
import { useAppPreferences } from "@/components/clinical-dashboard/use-app-preferences";
import { useModeResultsWarmup } from "@/components/clinical-dashboard/use-mode-results-warmup";
import { ModeHomeTemplate } from "@/components/mode-home-template";
import { LazyMyDayHomeCard } from "@/components/my-day/my-day-home-card-lazy";
import { ShowAllChip } from "@/components/show-all-chip";
Expand Down Expand Up @@ -67,6 +68,9 @@ export function SharedHomeEmptyState({
: [];
const presentation: SharedHomePresentation = sharedHomePresentation[modeId];
const catalogueChip = sharedHomeCatalogueChips[modeId];
// Load what this mode's results page needs while the home is open, so the first
// search does not wait for it. Local chunks and catalogue only; never a query.
useModeResultsWarmup(modeId);

return (
<ModeHomeTemplate
Expand Down
14 changes: 4 additions & 10 deletions src/components/clinical-dashboard/document-search-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,10 @@ import {
DocumentBadge,
documentActionClass,
} from "@/components/clinical-dashboard/document-ui";
import { ResultRowsPlaceholder } from "@/components/clinical-dashboard/result-rows-placeholder";
import { useBatchSignedImageUrls } from "@/components/clinical-dashboard/use-batch-signed-urls";
import { useSignedImageUrl } from "@/components/clinical-dashboard/use-signed-image-url";
import {
cn,
floatingControl,
LoadingPanel,
metadataPillDensity,
Skeleton,
sourceCard,
textMuted,
} from "@/components/ui-primitives";
import { cn, floatingControl, metadataPillDensity, Skeleton, sourceCard, textMuted } from "@/components/ui-primitives";
import { copyTextToClipboard } from "@/lib/copy-to-clipboard";
import { compactBestUseTitle } from "@/lib/compact-best-use-title";
import { withCatalogueDegradedNotice } from "@/lib/site-content/catalogue-degraded-notice";
Expand Down Expand Up @@ -1545,7 +1538,8 @@ function DocumentSearchResultsPanelImpl({
) : null}

{loading ? (
<LoadingPanel label="Finding matching documents" />
// Rows shaped like the document results on their way, not a spinner over an empty page.
<ResultRowsPlaceholder label="Finding matching documents" rows={4} testId="document-results-placeholder" />
) : matches.length === 0 ? (
// A services or forms search that matched records but no documents.
// This branch used to render `null`, which stranded the reader: moving
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
resultFilterGroup,
} from "@/components/clinical-dashboard/result-filter-control";
import { UniversalSearchAlsoMatches } from "@/components/clinical-dashboard/universal-search-also-matches";
import { ResultRowsPlaceholder } from "@/components/clinical-dashboard/result-rows-placeholder";
import {
medicationVerdictRingClass,
PatientVerdictSignal,
Expand Down Expand Up @@ -880,11 +881,11 @@ function MedicationResults({
<PatientDetailsDockAction />

{/* The error branch moved into the band's fault panel, which carries the
same message and announces it once. Loading copy stays here. */}
same message and announces it once. While the first response is on its way the
list shows rows shaped like the results it is waiting for, not a line of text
over an empty page. */}
{initialCatalogLoading ? (
<div className="medication-results-inset">
<p className="text-sm text-[color:var(--text-muted)]">Loading medication catalogue…</p>
</div>
<ResultRowsPlaceholder label="Loading medication results" rows={4} testId="medication-results-placeholder" />
) : null}

{!initialCatalogLoading && !catalog.error && resultCount === 0 ? (
Expand Down Expand Up @@ -1117,7 +1118,11 @@ function MedicationResults({
})}
</div>

<UniversalSearchAlsoMatches modeId="prescribing" query={query} />
{/* Mounted (so its lookup runs in parallel) but kept out of view until the medication
list itself has arrived: it fills in underneath the results, never ahead of them. */}
<div hidden={initialCatalogLoading}>
<UniversalSearchAlsoMatches modeId="prescribing" query={query} />
</div>

<StatusNotice
realDataReady={realDataReady}
Expand Down
54 changes: 54 additions & 0 deletions src/components/clinical-dashboard/result-rows-placeholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Skeleton, cn } from "@/components/ui-primitives";

/**
* Placeholder rows shaped like the result cards they stand in for.
*
* A results page used to show a spinner or one muted sentence ("Loading medication
* catalogue…") over an otherwise empty page until the first response landed, and then
* snap to a full list. These rows hold the list's real geometry instead, an icon tile
* beside a title, a subtitle and a short detail line, so the page reads as "results
* arriving" and nothing below it jumps when they do.
*
* Accessibility follows `LoadingPanel`'s skeleton variant (COMPONENTS.md §9.29): one
* `role="status"` node named by `label`, an `sr-only` twin of the label, every shimmer
* bar `aria-hidden`, and no `aria-live` on visible content (§9.2). Callers do not mark a
* parent `aria-busy`: that would hold back this status until the rows had gone.
*/
// The inset tone, not Skeleton's default subtle one: on a raised card the subtle tone is
// nearly the card's own colour in light mode, so the rows read as empty boxes.
const bar = "bg-[color:var(--surface-inset)]";

export function ResultRowsPlaceholder({
label,
rows = 4,
className,
testId,
}: {
label: string;
rows?: number;
className?: string;
testId?: string;
}) {
return (
<div role="status" aria-label={label} data-testid={testId} className={cn("grid gap-2", className)}>
{Array.from({ length: rows }).map((_, index) => {
const delay = `${index * 60}ms`;
return (
<div
key={index}
aria-hidden
className="grid min-w-0 grid-cols-[2.25rem_minmax(0,1fr)] items-start gap-2.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] p-2 shadow-[var(--shadow-inset)]"
>
<Skeleton className={cn("size-9 rounded-lg", bar)} animationDelay={delay} />
<div className="grid min-w-0 gap-2 py-0.5">
<Skeleton className={cn("h-4", bar, index % 2 === 0 ? "w-1/2" : "w-2/5")} animationDelay={delay} />
<Skeleton className={cn("h-3 w-4/5", bar)} animationDelay={delay} />
<Skeleton className={cn("h-3", bar, index % 2 === 0 ? "w-3/5" : "w-2/3")} animationDelay={delay} />
</div>
</div>
);
})}
<span className="sr-only">{label}</span>
</div>
);
}
Loading
Loading