diff --git a/src/app/globals.css b/src/app/globals.css
index d0971df7b..b195983b4 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -72,6 +72,7 @@
--animate-sheet-up: sheet-up 250ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-sheet-left: sheet-left 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-pop-in: pop-in 180ms cubic-bezier(0.34, 1.3, 0.64, 1) both;
+ --animate-dialog-rise: dialog-rise 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-action-tray-in: action-tray-in 160ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-shimmer: shimmer 1.4s linear infinite;
}
@@ -1681,6 +1682,17 @@ summary::-webkit-details-marker {
}
}
+@keyframes dialog-rise {
+ from {
+ opacity: 0;
+ transform: translateY(12px) scale(0.98);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
@keyframes action-tray-in {
from {
opacity: 0;
diff --git a/src/app/mockups/answer-evidence-popups/page.tsx b/src/app/mockups/answer-evidence-popups/page.tsx
index 791dff886..831b4050d 100644
--- a/src/app/mockups/answer-evidence-popups/page.tsx
+++ b/src/app/mockups/answer-evidence-popups/page.tsx
@@ -206,6 +206,30 @@ function ButtonText({ children }: { children: ReactNode }) {
return {children};
}
+function DesktopSourcePreviewDemo() {
+ return (
+
+
+ Clozapine monitoring should include FBC/ANC, myocarditis symptoms, metabolic checks, constipation prevention,
+ and shared-care communication.
+
+
+
+ The answer body stays in place. The preview floats above nearby content instead of pushing the support card
+ down.
+
+
+ );
+}
+
function SourcePreviewPopover() {
return (
@@ -576,11 +600,11 @@ function MobileEvidencePanel({ selected }: { selected: string }) {
function DesktopEvidenceModal() {
return (
-
+
@@ -667,18 +691,9 @@ export default function AnswerEvidencePopupsMockupPage() {
-
-
- Clozapine monitoring should include FBC/ANC, myocarditis symptoms, metabolic checks, constipation
- prevention, and shared-care communication.
-
-
-
-
-
-
+
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index 14ae434f9..bdac825b4 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -3186,10 +3186,7 @@ export function ClinicalDashboard({
}
function handleFollowUpQuote(quote: QuoteCard) {
- setQuery(createQuoteFollowUp(quote));
- window.requestAnimationFrame(() => {
- window.setTimeout(() => focusComposerInput(), 120);
- });
+ stageAnswerFollowUpDraft(createQuoteFollowUp(quote));
}
function handlePickFollowUpSuggestion(suggestion: string) {
@@ -3279,18 +3276,10 @@ export function ClinicalDashboard({
return;
}
- const drawer = document.getElementById("answer-evidence-drawer") as HTMLDetailsElement | null;
- if (!drawer) {
- setActionNotice({
- tone: "warning",
- message: "Evidence appears after a source-backed answer is generated.",
- });
- return;
- }
- drawer.scrollIntoView({ block: "start", behavior: "smooth" });
- if (!drawer.open) {
- drawer.querySelector
("summary")?.click();
- }
+ setActionNotice({
+ tone: "warning",
+ message: "Evidence appears after a source-backed answer is generated.",
+ });
}
function navigateMobileSection(href: string, options: { updateHistory?: boolean } = {}) {
@@ -3807,9 +3796,7 @@ export function ClinicalDashboard({
searchMode === "answer"
? compactMobileModeHome
? "mb-0"
- : answer && answerFollowUpSuggestions.length > 0
- ? "mb-[calc(11.5rem+env(safe-area-inset-bottom))] sm:mb-24"
- : "mb-[calc(6.5rem+env(safe-area-inset-bottom))] sm:mb-24"
+ : "mb-[calc(5.25rem+env(safe-area-inset-bottom))] sm:mb-24"
: hasMobileBottomSearch
? bottomSearchScrollHidden
? "mb-0 sm:mb-0"
diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx
index 6c151a097..6145f852e 100644
--- a/src/components/applications-launcher-page.tsx
+++ b/src/components/applications-launcher-page.tsx
@@ -875,11 +875,11 @@ export function ApplicationsLauncherWorkspace({
const query = controlledQuery ?? localQuery;
const normalizedQuery = query.trim().toLowerCase();
const queryDerivedId = useMemo(() => initialToolId(query), [query]);
- const [selection, setSelection] = useState({
- queryKey: normalizedQuery,
- id: initialToolId(query),
- });
- const selectedId = selection.queryKey === normalizedQuery ? selection.id : queryDerivedId;
+ const [selection, setSelection] = useState(() => ({
+ queryKey: (controlledQuery ?? "").trim().toLowerCase(),
+ id: initialToolId(controlledQuery),
+ }));
+ const selectedId = detailOpen || selection.queryKey === normalizedQuery ? selection.id : queryDerivedId;
const filteredApps = useMemo(() => {
return launcherApps.filter((app) => {
diff --git a/src/components/clinical-dashboard/answer-content.tsx b/src/components/clinical-dashboard/answer-content.tsx
index ff148551d..8f7206fb7 100644
--- a/src/components/clinical-dashboard/answer-content.tsx
+++ b/src/components/clinical-dashboard/answer-content.tsx
@@ -36,6 +36,7 @@ import {
sanitizeAnswerDisplayText,
} from "@/components/clinical-dashboard/display-text";
import { useMobilePreviewSheet } from "@/components/clinical-dashboard/use-mobile-preview-sheet";
+import { SourcePreviewPopover } from "@/components/clinical-dashboard/source-preview-popover";
import { clearCachedSignedUrl, getCachedSignedUrl, setCachedSignedUrl } from "@/lib/signed-url-cache";
import { normalizeSourceMetadata, sourceStatusLabel } from "@/lib/source-metadata";
import { clinicalProseUsefulness } from "@/lib/source-text-sanitizer";
@@ -658,10 +659,11 @@ export function NaturalLanguageAnswer({
) : null}
{sourceCapsuleButton}
- {sourcePreviewOpen && canOpenSourcePreview && !usePreviewSheet ? (
- setSourcePreviewOpen(false)}
+ anchorRef={sourceCapsuleRef}
>
-
+
) : null}
@@ -316,8 +317,9 @@ export function StagedAnswerResultSurface({
}
- contentClassName="max-h-[88dvh] bg-[color:var(--surface-raised)] sm:max-h-[min(88dvh,44rem)] sm:max-w-2xl"
+ contentClassName="max-h-[88dvh] bg-[color:var(--surface-raised)] sm:max-h-[min(88dvh,44rem)] sm:max-w-3xl"
bodyClassName="bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3"
+ desktopBackdropClassName="sm:bg-black/50"
returnFocusRef={evidenceTriggerRef}
portal
>
@@ -362,6 +364,7 @@ export function StagedAnswerResultSurface({
closeButtonClassName="inline-flex h-8 w-8 items-center justify-center rounded-full text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
contentClassName="max-h-[88dvh] bg-[color:var(--surface-raised)] sm:max-h-[min(80dvh,36rem)] sm:max-w-lg"
bodyClassName="flex flex-col bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3"
+ desktopBackdropClassName="sm:bg-black/50"
returnFocusRef={safetyTriggerRef}
portal
>
diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx
index 002dcf7db..aee03b76f 100644
--- a/src/components/clinical-dashboard/master-search-header.tsx
+++ b/src/components/clinical-dashboard/master-search-header.tsx
@@ -641,11 +641,20 @@ export function MasterSearchHeader({
}
}
+ const restoreActionMenuFocusRef = useRef(false);
const closeScope = useCallback((restoreFocus = false) => {
+ restoreActionMenuFocusRef.current = restoreFocus;
setScopeOpen(false);
- if (restoreFocus) actionMenuTriggerRef.current?.focus();
}, []);
+ useEffect(() => {
+ if (scopeOpen || !restoreActionMenuFocusRef.current) return;
+ restoreActionMenuFocusRef.current = false;
+ window.requestAnimationFrame(() => {
+ actionMenuTriggerRef.current?.focus({ preventScroll: true });
+ });
+ }, [scopeOpen]);
+
const closeScopeSheet = useCallback(() => {
setScopeSheetOpen(false);
window.requestAnimationFrame(() => actionMenuTriggerRef.current?.focus());
diff --git a/src/components/clinical-dashboard/source-preview-popover.tsx b/src/components/clinical-dashboard/source-preview-popover.tsx
new file mode 100644
index 000000000..f56c8909c
--- /dev/null
+++ b/src/components/clinical-dashboard/source-preview-popover.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import {
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useState,
+ type CSSProperties,
+ type ReactNode,
+ type RefObject,
+} from "react";
+import { createPortal } from "react-dom";
+
+import { useDismissableLayer } from "@/components/use-dismissable-layer";
+import { cn } from "@/components/ui-primitives";
+
+type PopoverPlacement = "below" | "above";
+
+const edgePadding = 12;
+const anchorGap = 8;
+const minPopoverHeight = 220;
+
+function computePopoverLayout(anchor: HTMLElement) {
+ const viewport = window.visualViewport;
+ const viewportTop = viewport?.offsetTop ?? 0;
+ const viewportLeft = viewport?.offsetLeft ?? 0;
+ const viewportWidth = viewport?.width ?? window.innerWidth;
+ const viewportHeight = viewport?.height ?? window.innerHeight;
+ const viewportBottom = viewportTop + viewportHeight;
+ const rect = anchor.getBoundingClientRect();
+ const availableAbove = Math.max(0, rect.top - viewportTop - edgePadding);
+ const availableBelow = Math.max(0, viewportBottom - rect.bottom - edgePadding);
+ const placement: PopoverPlacement =
+ availableBelow >= availableAbove + 40 || availableBelow >= minPopoverHeight ? "below" : "above";
+ const available =
+ placement === "below" ? Math.max(0, availableBelow - anchorGap) : Math.max(0, availableAbove - anchorGap);
+ const maxHeight = Math.max(
+ minPopoverHeight,
+ Math.floor(Math.min(available, viewportHeight - edgePadding * 2, 22 * 16)),
+ );
+ const maxWidth = Math.min(36 * 16, viewportWidth - edgePadding * 2);
+ const preferredLeft = rect.left;
+ const left = Math.max(
+ edgePadding + viewportLeft,
+ Math.min(preferredLeft, viewportLeft + viewportWidth - maxWidth - edgePadding),
+ );
+ const top =
+ placement === "below"
+ ? rect.bottom + anchorGap
+ : Math.max(viewportTop + edgePadding, rect.top - anchorGap - maxHeight);
+
+ return { placement, top, left, maxWidth, maxHeight };
+}
+
+export function SourcePreviewPopover({
+ open,
+ onClose,
+ anchorRef,
+ title = "Sources",
+ children,
+}: {
+ open: boolean;
+ onClose: () => void;
+ anchorRef: RefObject;
+ title?: string;
+ children: ReactNode;
+}) {
+ const surfaceRef = useRef(null);
+ const [layout, setLayout] = useState<{
+ placement: PopoverPlacement;
+ top: number;
+ left: number;
+ maxWidth: number;
+ maxHeight: number;
+ } | null>(null);
+
+ const updateLayout = useCallback(() => {
+ const anchor = anchorRef.current;
+ if (!anchor) return;
+ setLayout(computePopoverLayout(anchor));
+ }, [anchorRef]);
+
+ useLayoutEffect(() => {
+ if (!open) return;
+ updateLayout();
+ }, [open, updateLayout]);
+
+ useEffect(() => {
+ if (!open) return undefined;
+
+ const handleViewportChange = () => updateLayout();
+ window.addEventListener("resize", handleViewportChange);
+ window.visualViewport?.addEventListener("resize", handleViewportChange);
+ window.visualViewport?.addEventListener("scroll", handleViewportChange);
+ window.addEventListener("scroll", handleViewportChange, true);
+
+ return () => {
+ window.removeEventListener("resize", handleViewportChange);
+ window.visualViewport?.removeEventListener("resize", handleViewportChange);
+ window.visualViewport?.removeEventListener("scroll", handleViewportChange);
+ window.removeEventListener("scroll", handleViewportChange, true);
+ };
+ }, [open, updateLayout]);
+
+ useEffect(() => {
+ if (!open) return undefined;
+
+ function handleEscape(event: KeyboardEvent) {
+ if (event.key !== "Escape") return;
+ event.preventDefault();
+ event.stopPropagation();
+ onClose();
+ window.requestAnimationFrame(() => anchorRef.current?.focus({ preventScroll: true }));
+ }
+
+ document.addEventListener("keydown", handleEscape, true);
+ return () => document.removeEventListener("keydown", handleEscape, true);
+ }, [anchorRef, open, onClose]);
+
+ useEffect(() => {
+ if (!open) return;
+ const focusFrame = window.requestAnimationFrame(() => {
+ const focusTarget =
+ surfaceRef.current?.querySelector('[data-sheet-autofocus="true"]') ??
+ surfaceRef.current?.querySelector("a[href], button:not([disabled])") ??
+ surfaceRef.current;
+ focusTarget?.focus({ preventScroll: true });
+ });
+ return () => window.cancelAnimationFrame(focusFrame);
+ }, [open]);
+
+ useDismissableLayer({
+ enabled: open,
+ refs: [anchorRef, surfaceRef],
+ restoreFocusRef: anchorRef,
+ onDismiss: () => onClose(),
+ });
+
+ if (!open || typeof document === "undefined") return null;
+
+ const style: CSSProperties = layout
+ ? {
+ top: layout.top,
+ left: layout.left,
+ width: layout.maxWidth,
+ maxHeight: layout.maxHeight,
+ }
+ : { visibility: "hidden" };
+
+ return createPortal(
+
+ {children}
+
,
+ document.body,
+ );
+}
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
index 361ba5ad2..7f0ce0995 100644
--- a/src/components/ui/sheet.tsx
+++ b/src/components/ui/sheet.tsx
@@ -39,6 +39,7 @@ export function Sheet({
mobilePlacement = "bottom",
mobileSize = "content",
portal = false,
+ desktopBackdropClassName,
}: {
open: boolean;
onClose: () => void;
@@ -64,6 +65,7 @@ export function Sheet({
mobilePlacement?: "bottom" | "top" | "fullscreen";
mobileSize?: SheetMobileSize;
portal?: boolean;
+ desktopBackdropClassName?: string;
}) {
const panelRef = useRef(null);
const closeRef = useRef(null);
@@ -147,6 +149,7 @@ export function Sheet({
restoreFocusRef?.current?.focus({ preventScroll: true }));
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ restoreFocusRef?.current?.focus({ preventScroll: true });
+ });
+ });
}
document.addEventListener("pointerdown", handlePointerDown, true);
diff --git a/tests/supabase-schema.test.ts b/tests/supabase-schema.test.ts
index 32144a386..522d8d115 100644
--- a/tests/supabase-schema.test.ts
+++ b/tests/supabase-schema.test.ts
@@ -792,7 +792,6 @@ describe("Supabase schema Data API grants", () => {
expect(schema).toContain("index_aliases constant jsonb := jsonb_build_object(");
expect(schema).toContain("jsonb_array_elements_text(index_aliases -> index_name)");
});
-
it("mirrors tightened search_document_chunks owner scope in schema and migration", () => {
expect(searchDocumentChunksOwnerScopeMigration).toContain("(p_owner_id is null and d.owner_id is null)");
expect(schema).toContain("create or replace function public.search_document_chunks(");
diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts
index 0593c8ba9..a7a7d0da1 100644
--- a/tests/ui-smoke.spec.ts
+++ b/tests/ui-smoke.spec.ts
@@ -1311,7 +1311,9 @@ test.describe("Clinical KB UI smoke coverage", () => {
expect(popoverMetrics.height).toBeLessThanOrEqual(Math.ceil(popoverMetrics.viewportHeight * 0.72));
await page.keyboard.press("Escape");
await expect(scopePopover).toBeHidden();
- await expect(page.getByRole("button", { name: "Open answer options" })).toBeFocused();
+ await expect(async () => {
+ await expect(page.getByRole("button", { name: "Open answer options" })).toBeFocused();
+ }).toPass({ timeout: 5_000 });
await expectNoPageHorizontalOverflow(page);
});
@@ -1491,20 +1493,19 @@ test.describe("Clinical KB UI smoke coverage", () => {
const sourceCapsule = plainAnswer.getByRole("button", { name: "Open answer sources" });
await expectMinTouchTarget(sourceCapsule);
await sourceCapsule.click();
- const sourceSurface = viewport.sheet
- ? page.getByRole("dialog", { name: "Sources" })
- : page.getByTestId("source-capsule-preview");
+ const sourceSurface = page.getByRole("dialog", { name: "Sources" });
await expect(sourceSurface).toBeVisible();
await expect(sourceSurface.getByTestId("source-capsule-preview-row").first()).toHaveAttribute(
"href",
/\/documents\/.+chunk=/,
);
await expectMinTouchTarget(sourceSurface.getByTestId("source-capsule-preview-row").first());
- if (viewport.sheet) {
- await page.keyboard.press("Escape");
- await expect(sourceSurface).toHaveCount(0);
- await expect(sourceCapsule).toBeFocused();
- } else {
+ await page.keyboard.press("Escape");
+ await expect(sourceSurface).toHaveCount(0);
+ await expect(sourceCapsule).toBeFocused();
+ if (!viewport.sheet) {
+ await sourceCapsule.click();
+ await expect(sourceSurface).toBeVisible();
await sourceCapsule.click();
await expect(sourceSurface).toHaveCount(0);
}