From 046cb38ad45411c5f539636d4c976b25195f7bbd Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Tue, 28 Jul 2026 15:44:20 +0800
Subject: [PATCH] chore: organize dirty work from
apply-audit-remediation-protocol
---
scripts/check-github-action-pins.mjs | 13 ----
src/app/globals.css | 64 ++++++++++++++++++-
.../calculator-mockups/popup-sheet-mockup.tsx | 4 +-
.../clinical-dashboard/mode-action-popup.tsx | 19 +++---
.../source-preview-popover.tsx | 18 ++++--
src/components/ui/sheet.tsx | 62 +++++++++++-------
src/components/use-overlay-presence.ts | 27 ++++++++
src/lib/motion-tokens.ts | 8 +++
8 files changed, 161 insertions(+), 54 deletions(-)
create mode 100644 src/components/use-overlay-presence.ts
create mode 100644 src/lib/motion-tokens.ts
diff --git a/scripts/check-github-action-pins.mjs b/scripts/check-github-action-pins.mjs
index 539f90a883..9db2b94a1a 100644
--- a/scripts/check-github-action-pins.mjs
+++ b/scripts/check-github-action-pins.mjs
@@ -173,19 +173,6 @@ if (!/^ image: semgrep\/semgrep@sha256:[0-9a-f]{64}\s*$/m.test(semgrepGateJ
// the per-line validation above only covers workflows, a composite skew (e.g.
// setup-node v5 vs v7) was previously invisible. Assert each action name resolves
// to a single SHA everywhere it is used.
-function discoverCompositeActionFiles(workflowRoot) {
- const actionsRoot = path.join(workflowRoot, ".github", "actions");
- if (!existsSync(actionsRoot)) return [];
- const files = [];
- for (const entry of readdirSync(actionsRoot, { withFileTypes: true })) {
- if (!entry.isDirectory()) continue;
- for (const name of ["action.yml", "action.yaml"]) {
- const candidate = path.join(actionsRoot, entry.name, name);
- if (existsSync(candidate)) files.push(candidate);
- }
- }
- return files;
-}
const actionPinPattern = /uses:\s*([^@\s]+)@([0-9a-f]{40})(?:\s*#\s*(\S+))?/;
const shasByAction = new Map();
diff --git a/src/app/globals.css b/src/app/globals.css
index 1db94159d6..631d0ad0d9 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -104,13 +104,19 @@
--ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
--ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1);
+ --ease-exit: cubic-bezier(0.4, 0, 1, 1);
--animate-fade-up: fade-up 200ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-overlay-in: overlay-in 200ms cubic-bezier(0.22, 1, 0.36, 1) both;
+ --animate-overlay-out: overlay-out 140ms cubic-bezier(0.4, 0, 1, 1) both;
--animate-sheet-up: sheet-up 250ms cubic-bezier(0.22, 1, 0.36, 1) both;
+ --animate-sheet-down: sheet-down 140ms cubic-bezier(0.4, 0, 1, 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-dialog-fall: dialog-fall 140ms cubic-bezier(0.4, 0, 1, 1) both;
+ --animate-pop-out: pop-out 140ms cubic-bezier(0.4, 0, 1, 1) both;
+ --animate-sheet-left-out: sheet-left-out 140ms cubic-bezier(0.4, 0, 1, 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;
}
@@ -288,6 +294,7 @@
--duration-fast: 120ms;
--duration-base: 180ms;
--duration-slow: 240ms;
+ --duration-exit: 140ms;
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-emphasized: cubic-bezier(0.2, 0.8, 0.2, 1);
color-scheme: light;
@@ -1252,13 +1259,17 @@ summary::-webkit-details-marker {
}
}
@media (prefers-reduced-motion: no-preference) {
- .mode-action-surface[data-placement="up"] {
+ .mode-action-surface[data-placement="up"]:not([data-state="exiting"]) {
animation: mode-action-fold-up 170ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
- .mode-action-surface[data-placement="down"] {
+ .mode-action-surface[data-placement="down"]:not([data-state="exiting"]) {
animation: mode-action-fold-down 170ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
+
+ .mode-action-surface[data-state="exiting"] {
+ animation: pop-out 140ms cubic-bezier(0.4, 0, 1, 1) both;
+ }
}
.answer-footer-search-input {
@@ -2108,6 +2119,15 @@ html[data-motion="reduced"] .pwa-notice-card {
}
}
+@keyframes overlay-out {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
@keyframes sheet-up {
from {
transform: translateY(100%);
@@ -2117,6 +2137,15 @@ html[data-motion="reduced"] .pwa-notice-card {
}
}
+@keyframes sheet-down {
+ from {
+ transform: translateY(0);
+ }
+ to {
+ transform: translateY(100%);
+ }
+}
+
@keyframes sheet-left {
from {
transform: translateX(-100%);
@@ -2148,6 +2177,37 @@ html[data-motion="reduced"] .pwa-notice-card {
}
}
+@keyframes dialog-fall {
+ from {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(12px) scale(0.98);
+ }
+}
+
+@keyframes pop-out {
+ from {
+ opacity: 1;
+ transform: scale(1);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.97);
+ }
+}
+
+@keyframes sheet-left-out {
+ from {
+ transform: translateX(0);
+ }
+ to {
+ transform: translateX(-100%);
+ }
+}
+
@keyframes action-tray-in {
from {
opacity: 0;
diff --git a/src/components/calculator-mockups/popup-sheet-mockup.tsx b/src/components/calculator-mockups/popup-sheet-mockup.tsx
index 701b70e301..c18a02c1d7 100644
--- a/src/components/calculator-mockups/popup-sheet-mockup.tsx
+++ b/src/components/calculator-mockups/popup-sheet-mockup.tsx
@@ -102,9 +102,9 @@ export function CalculatorSheet({
aria-label="Close calculator"
tabIndex={-1}
onClick={onClose}
- className="absolute inset-0 animate-overlay-in bg-[color:var(--neutral-950)]/55 backdrop-blur-[2px]"
+ className="absolute inset-0 motion-safe:animate-overlay-in bg-[color:var(--neutral-950)]/55 backdrop-blur-[2px] motion-reduce:transition-none"
/>
-
+
diff --git a/src/components/clinical-dashboard/mode-action-popup.tsx b/src/components/clinical-dashboard/mode-action-popup.tsx
index dfb1e872fa..38536ac2de 100644
--- a/src/components/clinical-dashboard/mode-action-popup.tsx
+++ b/src/components/clinical-dashboard/mode-action-popup.tsx
@@ -429,10 +429,12 @@ export function ModeActionPopup({
window.requestAnimationFrame(() => buttonRef.current?.focus());
}, [onOpenChange, setModeSelectorOpen]);
+ const popoverPresence = useOverlayPresence(open && !useSheet);
+
// The sheet owns its own focus trap, Escape, and backdrop dismissal; only the
// anchored popover needs the outside-click dismissable layer.
useDismissableLayer({
- enabled: open && !useSheet,
+ enabled: popoverPresence.isMounted,
refs: [rootRef, surfaceRef, ...(dismissIgnoreRefs ?? [])],
restoreFocusRef: buttonRef,
onDismiss: () => {
@@ -622,9 +624,9 @@ export function ModeActionPopup({
}
useLayoutEffect(() => {
- if (!open || useSheet) return;
+ if (!popoverPresence.isMounted) return;
updatePlacement();
- }, [items.length, open, title, updatePlacement, useSheet]);
+ }, [items.length, popoverPresence.isMounted, title, updatePlacement]);
useEffect(() => {
if (open) return;
@@ -632,12 +634,12 @@ export function ModeActionPopup({
}, [open]);
useEffect(() => {
- if (!open || useSheet) return;
+ if (!popoverPresence.isMounted) return;
onPlacementChange?.(placement);
- }, [onPlacementChange, open, placement, useSheet]);
+ }, [onPlacementChange, popoverPresence.isMounted, placement]);
useEffect(() => {
- if (!open || useSheet) return;
+ if (!popoverPresence.isMounted) return;
let placementFrame: number | null = null;
const schedulePlacementUpdate = () => {
@@ -661,7 +663,7 @@ export function ModeActionPopup({
window.visualViewport?.removeEventListener("resize", schedulePlacementUpdate);
window.visualViewport?.removeEventListener("scroll", schedulePlacementUpdate);
};
- }, [open, updatePlacement, useSheet]);
+ }, [popoverPresence.isMounted, updatePlacement]);
const surfaceStyle = {
"--mode-action-max-height": surfaceMaxHeight ? `${surfaceMaxHeight}px` : undefined,
@@ -869,10 +871,11 @@ export function ModeActionPopup({
}
const actionSurface =
- open && !useSheet ? (
+ popoverPresence.isMounted ? (
(null);
+ const presence = useOverlayPresence(open);
const [layout, setLayout] = useState<{
placement: PopoverPlacement;
top: number;
@@ -77,12 +79,12 @@ export function SourcePreviewPopover({
const updateLayout = useCallback(() => {
const anchor = anchorRef.current;
- if (!anchor) return;
+ if (!anchor || !presence.isMounted) return;
setLayout(computePopoverLayout(anchor));
- }, [anchorRef]);
+ }, [anchorRef, presence.isMounted]);
useLayoutEffect(() => {
- if (!open) return;
+ if (!presence.isMounted) return;
updateLayout();
}, [open, updateLayout]);
@@ -114,7 +116,7 @@ export function SourcePreviewPopover({
window.visualViewport?.removeEventListener("scroll", handleViewportChange);
window.removeEventListener("scroll", handleViewportChange, scrollOptions);
};
- }, [open, updateLayout]);
+ }, [presence.isMounted, updateLayout]);
useEffect(() => {
if (!open) return undefined;
@@ -144,13 +146,13 @@ export function SourcePreviewPopover({
}, [open]);
useDismissableLayer({
- enabled: open,
+ enabled: presence.isMounted,
refs: [anchorRef, surfaceRef],
restoreFocusRef: anchorRef,
onDismiss: () => onClose(),
});
- if (!open || typeof document === "undefined") return null;
+ if (!presence.isMounted || typeof document === "undefined") return null;
const style: CSSProperties = layout
? {
@@ -168,11 +170,13 @@ export function SourcePreviewPopover({
role="dialog"
aria-modal="false"
aria-label={title}
+ aria-hidden={presence.stage === "exiting"}
data-testid="source-capsule-preview"
data-popover-placement={layout?.placement ?? "below"}
style={style}
className={cn(
- "fixed z-[95] min-w-[min(100vw-1.5rem,20rem)] overflow-y-auto overscroll-contain rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-lux)] p-3 shadow-[var(--shadow-elevated)] motion-safe:animate-pop-in motion-reduce:animate-none",
+ "fixed z-[95] min-w-[min(100vw-1.5rem,20rem)] overflow-y-auto overscroll-contain rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-lux)] p-3 shadow-[var(--shadow-elevated)] motion-reduce:animate-none",
+ presence.stage === "exiting" ? "motion-safe:animate-pop-out" : "motion-safe:animate-pop-in"
)}
>
{children}
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
index 685ae1310d..918becc97c 100644
--- a/src/components/ui/sheet.tsx
+++ b/src/components/ui/sheet.tsx
@@ -12,6 +12,7 @@ import {
import { createPortal } from "react-dom";
import { X } from "lucide-react";
import { cn, toolbarButton } from "@/components/ui-primitives";
+import { useOverlayPresence } from "@/components/use-overlay-presence";
export type SheetMobileSize = "content" | "viewport";
@@ -112,7 +113,7 @@ export function Sheet({
const panelRef = useRef
(null);
const closeRef = useRef(null);
const onCloseRef = useRef(onClose);
- const dragRef = useRef<{ startY: number; dragging: boolean }>({ startY: 0, dragging: false });
+ const dragRef = useRef<{ startY: number; startTime: number; dragging: boolean }>({ startY: 0, startTime: 0, dragging: false });
// Backdrop dismiss must require the gesture to *start* on the dimmed area.
// Otherwise a press that begins on the panel and ends on the backdrop would
// synthesize a click on the common ancestor and accidentally close the sheet.
@@ -132,11 +133,12 @@ export function Sheet({
function handleGripPointerDown(event: ReactPointerEvent) {
const panel = panelRef.current;
if (!panel) return;
- dragRef.current = { startY: event.clientY, dragging: true };
+ dragRef.current = { startY: event.clientY, startTime: performance.now(), dragging: true };
panel.style.transition = "none";
// Release the entry animation's `both` fill so the inline drag transform is
// not overridden by the finished keyframes (CSS animations beat inline style).
panel.style.animation = "none";
+ panel.style.willChange = "transform";
event.currentTarget.setPointerCapture(event.pointerId);
}
@@ -149,14 +151,17 @@ export function Sheet({
function handleGripPointerUp(event: ReactPointerEvent) {
if (!dragRef.current.dragging) return;
const delta = Math.max(0, event.clientY - dragRef.current.startY);
- dragRef.current = { startY: 0, dragging: false };
+ const dt = performance.now() - dragRef.current.startTime;
+ const velocity = dt > 0 ? delta / dt : 0;
+ dragRef.current = { startY: 0, startTime: 0, dragging: false };
const panel = panelRef.current;
if (panel) {
// Restore the class-based transition so a non-dismiss snaps back smoothly.
panel.style.transition = "";
panel.style.transform = "";
+ panel.style.willChange = "";
}
- if (delta > 96) onClose();
+ if (delta > 96 || velocity > 0.5) onClose();
}
useEffect(() => {
@@ -165,13 +170,18 @@ export function Sheet({
const explicitReturnElement = returnFocusRef?.current ?? null;
const previousActiveElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
pushSheet(sheetId);
- const focusFrame = window.requestAnimationFrame(() => {
- const focusTarget =
- initialFocusRef?.current ??
- panelRef.current?.querySelector('[data-sheet-autofocus="true"]') ??
- closeRef.current;
- focusTarget?.focus({ preventScroll: true });
- });
+
+ // ISSUE-04: Delay the focus frame so the outline does not translate across the screen
+ let focusFrame: number;
+ const focusTimeout = window.setTimeout(() => {
+ focusFrame = window.requestAnimationFrame(() => {
+ const focusTarget =
+ initialFocusRef?.current ??
+ panelRef.current?.querySelector('[data-sheet-autofocus="true"]') ??
+ closeRef.current;
+ focusTarget?.focus({ preventScroll: true });
+ });
+ }, 250);
function onKeyDown(event: KeyboardEvent) {
// Only the top-most open Sheet reacts, so a stacked overlay (lightbox /
@@ -212,7 +222,8 @@ export function Sheet({
window.addEventListener("keydown", onKeyDown);
return () => {
- window.cancelAnimationFrame(focusFrame);
+ window.clearTimeout(focusTimeout);
+ if (focusFrame) window.cancelAnimationFrame(focusFrame);
window.removeEventListener("keydown", onKeyDown);
popSheet(sheetId);
const restoreTarget = explicitReturnElement ?? previousActiveElement;
@@ -233,7 +244,9 @@ export function Sheet({
};
}, [open, initialFocusRef, returnFocusRef, sheetId]);
- if (!open) return null;
+ const { stage, isMounted } = useOverlayPresence(open);
+
+ if (!isMounted) return null;
const resolvedLabelledBy = labelledBy ?? (title ? titleId : undefined);
const defaultSheetIsFullscreen = placement !== "left" && mobilePlacement === "fullscreen";
@@ -245,7 +258,7 @@ export function Sheet({
className={cn(
"fixed inset-0 z-[100] flex bg-[color:var(--overlay-backdrop)] backdrop-blur-[2px] motion-reduce:animate-none motion-reduce:transition-none",
desktopBackdropClassName,
- placement !== "left" && "motion-safe:animate-overlay-in",
+ stage === "exiting" ? "motion-safe:animate-overlay-out" : "motion-safe:animate-overlay-in",
placement === "left"
? "items-stretch justify-start"
: defaultSheetIsFullscreen
@@ -282,25 +295,30 @@ export function Sheet({
"flex min-w-0 w-full flex-col overflow-hidden border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] text-[color:var(--text)] shadow-[var(--shadow-elevated)] pb-safe",
"transition duration-200 motion-reduce:transition-none sm:duration-150",
placement === "left"
- ? "h-full max-h-full max-w-[min(22rem,calc(100vw-1rem))] rounded-r-2xl border-y-0 border-l-0 pt-safe sm:max-h-dvh sm:max-w-[22rem] sm:rounded-l-none sm:rounded-r-2xl sm:pb-0"
+ ? cn(
+ "h-full max-h-full max-w-[min(22rem,calc(100vw-1rem))] rounded-r-2xl border-y-0 border-l-0 pt-safe sm:max-h-dvh sm:max-w-[22rem] sm:rounded-l-none sm:rounded-r-2xl sm:pb-0",
+ stage === "exiting" ? "motion-safe:animate-sheet-left-out" : "motion-safe:animate-sheet-left"
+ )
: cn(
defaultSheetIsFullscreen
- ? // Fullscreen panels size from the inset-0 backdrop (h-full), not
- // 100dvh: iOS Safari resolves dvh stale across toolbar
- // collapse, which strands a dead band under the sheet.
- "h-full max-h-full rounded-none border-0 motion-safe:animate-pop-in sm:max-w-none sm:rounded-none lg:h-auto lg:max-h-[calc(100dvh-3rem)] lg:rounded-2xl lg:border lg:border-[color:var(--border-lux)] lg:pb-0 lg:motion-safe:animate-dialog-rise"
+ ? cn(
+ "h-full max-h-full rounded-none border-0 sm:max-w-none sm:rounded-none lg:h-auto lg:max-h-[calc(100dvh-3rem)] lg:rounded-2xl lg:border lg:border-[color:var(--border-lux)] lg:pb-0",
+ stage === "exiting" ? "motion-safe:animate-pop-out lg:motion-safe:animate-dialog-fall" : "motion-safe:animate-pop-in lg:motion-safe:animate-dialog-rise"
+ )
: cn(
- "sm:max-w-lg sm:rounded-2xl sm:pb-0 sm:motion-safe:animate-dialog-rise",
+ "sm:max-w-lg sm:rounded-2xl sm:pb-0",
defaultSheetIsTopAligned
? cn(
- "max-h-[calc(100dvh-1.5rem)] rounded-2xl motion-safe:animate-pop-in",
+ "max-h-[calc(100dvh-1.5rem)] rounded-2xl",
defaultSheetUsesViewportSize && "min-h-[calc(100dvh-2rem)] sm:min-h-0",
+ stage === "exiting" ? "motion-safe:animate-pop-out sm:motion-safe:animate-dialog-fall" : "motion-safe:animate-pop-in sm:motion-safe:animate-dialog-rise"
)
: cn(
- "rounded-t-2xl motion-safe:animate-sheet-up",
+ "rounded-t-2xl",
defaultSheetUsesViewportSize
? "min-h-[calc(100dvh-2rem)] max-h-[calc(100dvh-1rem)] sm:min-h-0"
: "max-h-[88dvh]",
+ stage === "exiting" ? "motion-safe:animate-sheet-down sm:motion-safe:animate-dialog-fall" : "motion-safe:animate-sheet-up sm:motion-safe:animate-dialog-rise"
),
),
),
diff --git a/src/components/use-overlay-presence.ts b/src/components/use-overlay-presence.ts
new file mode 100644
index 0000000000..c974df823f
--- /dev/null
+++ b/src/components/use-overlay-presence.ts
@@ -0,0 +1,27 @@
+import { useState, useEffect } from "react";
+
+export type PresenceStage = "unmounted" | "entering" | "mounted" | "exiting";
+
+export function useOverlayPresence(open: boolean, exitDurationMs: number = 140) {
+ const [stage, setStage] = useState(open ? "mounted" : "unmounted");
+
+ useEffect(() => {
+ if (open) {
+ setStage("mounted");
+ } else {
+ setStage((current) => {
+ if (current === "unmounted") return "unmounted";
+ return "exiting";
+ });
+ const timeout = setTimeout(() => {
+ setStage("unmounted");
+ }, exitDurationMs);
+ return () => clearTimeout(timeout);
+ }
+ }, [open, exitDurationMs]);
+
+ return {
+ stage,
+ isMounted: stage !== "unmounted",
+ };
+}
diff --git a/src/lib/motion-tokens.ts b/src/lib/motion-tokens.ts
new file mode 100644
index 0000000000..409b009caa
--- /dev/null
+++ b/src/lib/motion-tokens.ts
@@ -0,0 +1,8 @@
+export const DURATION_FAST = 120;
+export const DURATION_BASE = 180;
+export const DURATION_SLOW = 240;
+export const DURATION_EXIT = 140;
+
+export const EASE_OUT_SOFT = "cubic-bezier(0.22, 1, 0.36, 1)";
+export const EASE_SPRING = "cubic-bezier(0.34, 1.3, 0.64, 1)";
+export const EASE_EXIT = "cubic-bezier(0.4, 0, 1, 1)";