smooth out terminal nav drawer motion

This commit is contained in:
dirtydishes 2026-05-23 20:05:57 -04:00
parent 7ca0e05a2d
commit d42c088432
4 changed files with 642 additions and 11 deletions

View file

@ -19,6 +19,9 @@
--blue: oklch(0.72 0.13 247);
--blue-soft: oklch(0.72 0.13 247 / 0.11);
--drawer-width: min(320px, calc(100vw - 28px));
--drawer-enter-duration: 220ms;
--drawer-exit-duration: 180ms;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--topbar-height: 64px;
}
@ -103,6 +106,24 @@ input {
background: linear-gradient(180deg, oklch(0.16 0.012 250 / 0.98), oklch(0.13 0.011 250 / 0.98));
border-right: 1px solid var(--border);
box-shadow: 0 28px 72px rgba(0, 0, 0, 0.48);
opacity: 0;
transform: translate3d(calc(-1 * min(22px, 6vw)), 0, 0);
transition:
transform var(--drawer-enter-duration) var(--ease-out-expo),
opacity var(--drawer-enter-duration) var(--ease-out-expo),
box-shadow var(--drawer-enter-duration) var(--ease-out-expo);
will-change: transform, opacity;
}
.terminal-nav-drawer[data-state="open"] {
opacity: 1;
transform: translate3d(0, 0, 0);
}
.terminal-nav-drawer[data-state="closing"] {
transform: translate3d(calc(-1 * min(16px, 4vw)), 0, 0);
transition-duration: var(--drawer-exit-duration), var(--drawer-exit-duration), var(--drawer-exit-duration);
pointer-events: none;
}
.terminal-drawer-backdrop {
@ -112,6 +133,17 @@ input {
border: 0;
background: rgba(3, 5, 8, 0.62);
cursor: pointer;
opacity: 0;
transition: opacity var(--drawer-enter-duration) var(--ease-out-expo);
}
.terminal-drawer-backdrop[data-state="open"] {
opacity: 1;
}
.terminal-drawer-backdrop[data-state="closing"] {
transition-duration: var(--drawer-exit-duration);
pointer-events: none;
}
.terminal-drawer-head {
@ -1992,6 +2024,8 @@ h3 {
@media (prefers-reduced-motion: reduce) {
.skip-link,
.terminal-nav-drawer,
.terminal-drawer-backdrop,
.terminal-nav-link,
.terminal-filter-field::before,
.terminal-filter-field::after,

View file

@ -103,6 +103,7 @@ const LIVE_EQUITIES_SILENT_WARNING_MS = parseBoundedInt(
5 * 60 * 1000
);
const LIVE_FLOW_STALE_MS = 30_000;
const NAV_DRAWER_EXIT_MS = 180;
const PINNED_EVIDENCE_TTL_MS = parseBoundedInt(
process.env.NEXT_PUBLIC_PINNED_EVIDENCE_TTL_MS,
20 * 60 * 1000,
@ -118,6 +119,13 @@ const PINNED_EVIDENCE_MAX_ITEMS = parseBoundedInt(
const NBBO_MAX_AGE_MS = Number(process.env.NEXT_PUBLIC_NBBO_MAX_AGE_MS);
const NBBO_MAX_AGE_MS_SAFE =
Number.isFinite(NBBO_MAX_AGE_MS) && NBBO_MAX_AGE_MS > 0 ? NBBO_MAX_AGE_MS : 1000;
type NavDrawerPhase = "closed" | "opening" | "open" | "closing";
const prefersReducedMotion = () =>
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const FLOW_FILTER_PRESET = process.env.NEXT_PUBLIC_FLOW_FILTER_PRESET ?? "smart-money";
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1"]);
const CANDLE_INTERVALS = [
@ -8827,23 +8835,101 @@ function SyntheticControlDock() {
export function TerminalAppShell({ children }: { children: ReactNode }) {
const state = useTerminalState();
const pathname = usePathname();
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerPhase, setDrawerPhase] = useState<NavDrawerPhase>("closed");
const drawerFrameRef = useRef<number | null>(null);
const drawerCloseTimerRef = useRef<number | null>(null);
const tickerFieldId = useId();
const tickerHintId = useId();
const activeNavHref = getTerminalNavCurrentHref(pathname);
const drawerExpanded = drawerPhase === "opening" || drawerPhase === "open";
useEffect(() => {
setDrawerOpen(false);
if (drawerFrameRef.current !== null) {
window.cancelAnimationFrame(drawerFrameRef.current);
drawerFrameRef.current = null;
}
if (drawerCloseTimerRef.current !== null) {
window.clearTimeout(drawerCloseTimerRef.current);
drawerCloseTimerRef.current = null;
}
setDrawerPhase("closed");
}, [pathname]);
useEffect(() => {
if (!drawerOpen) {
return () => {
if (drawerFrameRef.current !== null) {
window.cancelAnimationFrame(drawerFrameRef.current);
}
if (drawerCloseTimerRef.current !== null) {
window.clearTimeout(drawerCloseTimerRef.current);
}
};
}, []);
const openNavDrawer = useCallback(() => {
if (drawerFrameRef.current !== null) {
window.cancelAnimationFrame(drawerFrameRef.current);
drawerFrameRef.current = null;
}
if (drawerCloseTimerRef.current !== null) {
window.clearTimeout(drawerCloseTimerRef.current);
drawerCloseTimerRef.current = null;
}
if (prefersReducedMotion()) {
setDrawerPhase("open");
return;
}
setDrawerPhase("opening");
drawerFrameRef.current = window.requestAnimationFrame(() => {
drawerFrameRef.current = null;
setDrawerPhase("open");
});
}, []);
const closeNavDrawer = useCallback(() => {
if (drawerPhase === "closed" || drawerPhase === "closing") {
return;
}
if (drawerFrameRef.current !== null) {
window.cancelAnimationFrame(drawerFrameRef.current);
drawerFrameRef.current = null;
}
if (drawerCloseTimerRef.current !== null) {
window.clearTimeout(drawerCloseTimerRef.current);
drawerCloseTimerRef.current = null;
}
if (prefersReducedMotion()) {
setDrawerPhase("closed");
return;
}
// Keep the drawer mounted long enough to animate out cleanly.
setDrawerPhase("closing");
drawerCloseTimerRef.current = window.setTimeout(() => {
drawerCloseTimerRef.current = null;
setDrawerPhase("closed");
}, NAV_DRAWER_EXIT_MS);
}, [drawerPhase]);
const toggleNavDrawer = useCallback(() => {
if (drawerExpanded) {
closeNavDrawer();
return;
}
openNavDrawer();
}, [closeNavDrawer, drawerExpanded, openNavDrawer]);
useEffect(() => {
if (!drawerExpanded) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setDrawerOpen(false);
closeNavDrawer();
}
};
@ -8851,7 +8937,7 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [drawerOpen]);
}, [closeNavDrawer, drawerExpanded]);
return (
<TerminalContext.Provider value={state}>
@ -8865,11 +8951,11 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
<div className="terminal-topbar-leading">
<button
aria-controls="terminal-nav-drawer"
aria-expanded={drawerOpen}
aria-label={drawerOpen ? "Close navigation menu" : "Open navigation menu"}
aria-expanded={drawerExpanded}
aria-label={drawerExpanded ? "Close navigation menu" : "Open navigation menu"}
className="terminal-button terminal-menu-trigger"
type="button"
onClick={() => setDrawerOpen((current) => !current)}
onClick={toggleNavDrawer}
>
<span aria-hidden="true" className="terminal-menu-trigger-icon">
<span />
@ -8942,17 +9028,19 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
</main>
</div>
{drawerOpen ? (
{drawerPhase !== "closed" ? (
<>
<button
aria-label="Close navigation drawer"
className="terminal-drawer-backdrop"
data-state={drawerPhase}
type="button"
onClick={() => setDrawerOpen(false)}
onClick={closeNavDrawer}
/>
<aside
aria-label="Primary navigation"
className="terminal-nav-drawer"
data-state={drawerPhase}
id="terminal-nav-drawer"
>
<div className="terminal-drawer-head">
@ -8964,7 +9052,7 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
aria-label="Close navigation drawer"
className="terminal-button terminal-drawer-close"
type="button"
onClick={() => setDrawerOpen(false)}
onClick={closeNavDrawer}
>
Close
</button>