diff --git a/public/marketing/marketing.css b/public/marketing/marketing.css index 6d4b7cd31..e0a415be1 100644 --- a/public/marketing/marketing.css +++ b/public/marketing/marketing.css @@ -672,3 +672,67 @@ ::view-transition-old(sidebar-dot):only-child { animation: sidebar-dot-out 0.2s ease both; } + +/* Marketing sidebar sub-nav unfurl (see Sidebar.svelte). Each nav section is a + view-transition group. As one section grows/shrinks, the sections below tween + their boxes to the new positions (smooth reflow, no jump), and clipping the + growing/shrinking box turns the height change into a top-to-bottom unfurl. + These always run, but on a same-section navigation the boxes don't change + size so it's a no-op. */ +::view-transition-group(install-section), +::view-transition-group(learn-section), +::view-transition-group(sidebar-external) { + animation-duration: 0.25s; + animation-timing-function: ease-out; +} + +::view-transition-group(install-section), +::view-transition-group(learn-section) { + overflow: clip; +} + +/* Anchor the section snapshots to the top at natural height, so a growing/ + shrinking group reveals/hides content by clipping rather than scaling it. */ +::view-transition-old(install-section), +::view-transition-new(install-section), +::view-transition-old(learn-section), +::view-transition-new(learn-section) { + height: auto; + inset-block-start: 0; +} + +/* Animatable soft-edge distance for the reveal, registered so it can tween. */ +@property --subnav-fade { + syntax: ""; + inherits: false; + initial-value: 0px; +} + +@keyframes subnav-fade-edge { + /* Hold the soft edge at full through the 0.25s reveal (0-78% of this 0.32s), + then dissolve it over the short tail so the last item keeps its soft edge + while revealing and the gradient is gone before the pseudo is removed (no + pop, no hard cut). */ + 0%, + 78% { + --subnav-fade: 2rem; + } + 100% { + --subnav-fade: 0rem; + } +} + +/* The soft-edge gradient only applies when a section actually opens or closes: + the sidebar adds `.sidebar-unfurling` to the document element for exactly + those navigations (see Sidebar.svelte), so moving *within* a section shows no + gradient. `max()` floors the opaque region to the header height so a + collapsing section never fades its own header text. */ +:root.sidebar-unfurling::view-transition-image-pair(install-section), +:root.sidebar-unfurling::view-transition-image-pair(learn-section) { + mask-image: linear-gradient( + to bottom, + #000 max(1.75rem, calc(100% - var(--subnav-fade))), + transparent + ); + animation: subnav-fade-edge 0.32s ease-out both; +} diff --git a/src/marketing/Sidebar.svelte b/src/marketing/Sidebar.svelte index d44db1e26..c7b81ae0d 100644 --- a/src/marketing/Sidebar.svelte +++ b/src/marketing/Sidebar.svelte @@ -34,6 +34,27 @@ ["/learn", "/principles", "/faq", "/glossary"].includes(pathname) || pathname.startsWith("/guides"); + // Flag the document element only when a section actually opens or closes, so + // the sub-nav unfurl gradient (see marketing.css) fires on expand/collapse + // but not when navigating within an already-open section. + let sidebarStateInitialised = false; + let unfurlTimeout: ReturnType; + $: flagSectionChange(installOpen, learnOpen); + function flagSectionChange(_installOpen: boolean, _learnOpen: boolean) { + if (!sidebarStateInitialised) { + sidebarStateInitialised = true; + return; + } + if (typeof document === "undefined") { + return; + } + document.documentElement.classList.add("sidebar-unfurling"); + clearTimeout(unfurlTimeout); + unfurlTimeout = setTimeout(() => { + document.documentElement.classList.remove("sidebar-unfurling"); + }, 500); + } + function onNav(event: MouseEvent, route: Route) { if (useDefaultNavigation(event)) { return; @@ -150,7 +171,7 @@