smooth out terminal nav drawer motion
This commit is contained in:
parent
7ca0e05a2d
commit
d42c088432
4 changed files with 642 additions and 11 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue