From 0a7a69b3158b4ccbb5ac0135c78c1f380c14e1f3 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 15 Aug 2023 02:10:10 -0700 Subject: [PATCH] Improve website book sidebar and nav ripple --- .../volunteer/guide/getting-started/_index.md | 4 +- website/sass/base.scss | 41 ++++--- website/sass/blog.scss | 1 + website/sass/book.scss | 107 +++++++++--------- website/sass/index.scss | 10 -- website/static/js/book.js | 23 +++- website/static/js/navbar.js | 75 ++++++------ website/templates/base.html | 4 +- website/templates/book.html | 14 ++- 9 files changed, 152 insertions(+), 127 deletions(-) diff --git a/website/content/volunteer/guide/getting-started/_index.md b/website/content/volunteer/guide/getting-started/_index.md index 0d57706e49..eb0d0e114b 100644 --- a/website/content/volunteer/guide/getting-started/_index.md +++ b/website/content/volunteer/guide/getting-started/_index.md @@ -26,12 +26,12 @@ You only need to explicitly install Node.js dependencies. Rust's cargo dependenc One tool in the Rust ecosystem does need to be installed: -``` +```sh cargo install cargo-watch ``` That's it! Now, to run the project while developing, just execute: -``` +```sh npm start ``` diff --git a/website/sass/base.scss b/website/sass/base.scss index b579d5e3ba..bdae0775d8 100644 --- a/website/sass/base.scss +++ b/website/sass/base.scss @@ -148,6 +148,7 @@ h6 { p { margin: 0; + -webkit-hyphens: auto; hyphens: auto; text-align: justify; } @@ -212,6 +213,7 @@ code { color: var(--color-black); padding: 0 4px; overflow-wrap: anywhere; + -webkit-hyphens: none; hyphens: none; } @@ -1006,18 +1008,8 @@ body > .page { header { padding: 0 var(--page-edge-padding); color: var(--color-walnut); - - @media screen and (max-width: 1824px) { - .ripple { - width: calc(100% + (var(--page-edge-padding) * 2)); - margin-left: calc(-1 * var(--page-edge-padding)); - margin-right: calc(-1 * var(--page-edge-padding)); - } - - hr { - display: none; - } - } + position: relative; + z-index: 1000; nav { margin: auto; @@ -1026,14 +1018,20 @@ header { .row { display: flex; justify-content: space-between; - padding: 30px 0; + --nav-padding-above-below: 30px; + padding-top: var(--nav-padding-above-below); + padding-bottom: calc(var(--nav-padding-above-below) - 16px); + margin-bottom: calc(var(--nav-padding-above-below) - 16px); + // Covers up content that extends up underneath the header + background: white; @media screen and (max-width: 760px) { - padding: 20px 0; + --nav-padding-above-below: 24px; } .left, .right { + z-index: 1; display: flex; align-items: center; gap: 40px; @@ -1156,7 +1154,8 @@ header { .ripple { display: block; background: none; - fill: none; + // Covers up content that extends up underneath the header + fill: white; stroke: currentColor; --ripple-height: 16px; height: var(--ripple-height); @@ -1173,6 +1172,18 @@ header { hr { background: none; } + + @media screen and (max-width: 1824px) { + .ripple { + width: calc(100% + (var(--page-edge-padding) * 2)); + margin-left: calc(-1 * var(--page-edge-padding)); + margin-right: calc(-1 * var(--page-edge-padding)); + } + + hr { + display: none; + } + } } main { diff --git a/website/sass/blog.scss b/website/sass/blog.scss index 8ecbb5e4c8..3630589df1 100644 --- a/website/sass/blog.scss +++ b/website/sass/blog.scss @@ -81,6 +81,7 @@ flex-direction: column; gap: 20px; text-align: justify; + -webkit-hyphens: auto; hyphens: auto; overflow: hidden; text-overflow: ellipsis; diff --git a/website/sass/book.scss b/website/sass/book.scss index 0637949490..feeb01b2dd 100644 --- a/website/sass/book.scss +++ b/website/sass/book.scss @@ -2,16 +2,35 @@ position: relative; display: flex; gap: 40px; + // Creates a stacking context for the left popout sidebar's sticky positioning transform: translate(0); + .close-chapter-selection, + .open-chapter-selection { + display: none; + fill: var(--color-navy); + background: none; + border: none; + box-sizing: content-box; + padding: 6px; + font-size: 0; + cursor: pointer; + + svg { + width: 36px; + height: 36px; + } + } + .reading-material { width: 800px; - flex: 0 0 auto; - + flex: 1 2 100%; + .prev-next { display: flex; justify-content: space-between; width: 100%; + gap: 20px; a { display: flex; @@ -25,40 +44,35 @@ } } - @media screen and (max-width: 1320px) { + // Page contents right sidebar is removed on smaller screens + @media screen and (max-width: 1200px) { .contents { display: none; } - - .reading-material.reading-material { - margin-right: 0; - } - } - - .hamburger-menu-button { - display: none; - border: none; - cursor: pointer; - width: 30px; - height: 30px; } - // Overlaid fold-out menu - @media screen and (max-width: 1080px) { + // Overlaid fold-out menu for chapter selection + @media screen and (max-width: 1000px) { gap: 0; .chapters { position: sticky; - height: 100vh; width: 0; margin-top: calc(-120 * var(--variable-px)); overflow: visible; z-index: 10; + height: 0; + transition: height 0.25s ease-in-out 0.25s; - &.open .wrapper-outer { - left: 0; + &.open { + height: 100vh; + transition: height 0s; + + .wrapper-outer { + left: 0; + } } - + .wrapper-outer { position: absolute; background: white; @@ -66,13 +80,15 @@ bottom: 0; padding-left: var(--page-edge-padding); margin-left: calc(-1 * var(--page-edge-padding)); - padding-bottom: 120px; - margin-bottom: -120px; + padding-bottom: calc(120 * var(--variable-px)); + margin-bottom: calc(-120 * var(--variable-px)); + padding-top: 16px; + margin-top: -16px; border-right: var(--border-thickness) solid var(--color-walnut); box-sizing: border-box; + transition: left 0.25s ease-in-out; + left: calc(-1 * (300px + 10px)); width: 300px; - transition: left 0.2s ease-in-out; - left: -310px; &::after { content: ""; @@ -89,35 +105,17 @@ overflow-y: auto; height: 100%; padding-right: var(--page-edge-padding); - padding-bottom: 120px; + padding-bottom: calc(-120 * var(--variable-px)); ul:first-of-type { margin-top: calc(120 * var(--variable-px)); } - .hamburger-menu-button.close { - display: inline-block; - background: none; - overflow: hidden; + .close-chapter-selection { + display: block; position: absolute; - top: 20px; - right: 20px; - - &::before, - &::after { - content: ""; - position: absolute; - background: var(--color-walnut); - width: 36px; - height: 4px; - top: 50%; - left: 50%; - transform: translate(-18px, -2px) rotate(45deg); - } - - &::after { - transform: translate(-18px, -2px) rotate(-45deg); - } + top: calc(20px - 6px); + right: calc(20px - 6px); } } } @@ -131,15 +129,15 @@ .article-title { display: flex; white-space: nowrap; - gap: 20px; - .hamburger-menu-button.open { - flex: 0 0 auto; - position: relative; + .open-chapter-selection { display: inline-block; vertical-align: top; - top: calc(var(--font-size-heading-h1) * 0.25); - background: linear-gradient(to bottom, transparent calc(25% / 3), var(--color-walnut) calc(25% / 3), var(--color-walnut) calc(75% / 3), transparent calc(75% / 3), transparent calc(125% / 3), var(--color-walnut) calc(125% / 3), var(--color-walnut) calc(175% / 3), transparent calc(175% / 3), transparent calc(225% / 3), var(--color-walnut) calc(225% / 3), var(--color-walnut) calc(275% / 3), transparent calc(275% / 3)); + height: calc(var(--font-size-heading-h1) * 1.25); + padding-top: 0; + padding-bottom: 0; + margin-left: -6px; + margin-right: calc(20px - 6px); } h1 { @@ -157,6 +155,7 @@ width: 300px; max-height: 100vh; margin-top: -40px; + flex: 0 1 auto; li { margin-top: 0.5em; diff --git a/website/sass/index.scss b/website/sass/index.scss index d9f4b0160a..37a2850a1f 100644 --- a/website/sass/index.scss +++ b/website/sass/index.scss @@ -142,16 +142,6 @@ div { min-height: auto; } - - @media screen and (max-width: 1200px) { - flex-direction: column; - flex: 1 1 100%; - - &.name, - &.email { - flex: 1 1 100%; - } - } } .column { diff --git a/website/static/js/book.js b/website/static/js/book.js index 6419ac72a3..8425eb810d 100644 --- a/website/static/js/book.js +++ b/website/static/js/book.js @@ -1,5 +1,5 @@ addEventListener("DOMContentLoaded", trackScrollHeadingInTOC); -addEventListener("DOMContentLoaded", listenForClickToOpenTOC); +addEventListener("DOMContentLoaded", listenForClickToOpenOrCloseTOC); // Listen for scroll events and update the active section in the table of contents to match the visible content's heading function trackScrollHeadingInTOC() { @@ -52,12 +52,25 @@ function trackScrollHeadingInTOC() { updateVisibleHeading(); } -function listenForClickToOpenTOC() { - document.querySelector("[data-hamburger-menu-button-open]")?.addEventListener("click", () => { - document.querySelector("[data-chapters]")?.classList.add("open"); +function listenForClickToOpenOrCloseTOC() { + // Open the chapter selection if the user clicks the open button + document.querySelector("[data-open-chapter-selection]")?.addEventListener("click", () => { + // Wait until after the click-outside-the-panel event has been handled before opening the panel so it doesn't immediately get closed in the same call stack + setTimeout(() => { + document.querySelector("[data-chapters]")?.classList.add("open"); + }); }); - document.querySelector("[data-hamburger-menu-button-close]")?.addEventListener("click", () => { + // Close the chapter selection if the user clicks the close button + document.querySelector("[data-close-chapter-selection]")?.addEventListener("click", () => { document.querySelector("[data-chapters]")?.classList.remove("open"); }); + + // Close the chapter selection if the user clicks outside of it + document.querySelector("main")?.addEventListener("click", (e) => { + const chapters = document.querySelector("[data-chapters]"); + if (chapters?.classList.contains("open") && !e.target.closest("[data-chapters]")) { + chapters.classList.remove("open"); + } + }); } diff --git a/website/static/js/navbar.js b/website/static/js/navbar.js index 08d4139784..198d5a1943 100644 --- a/website/static/js/navbar.js +++ b/website/static/js/navbar.js @@ -1,9 +1,8 @@ -const NAV_BUTTON_INITIAL_FONT_SIZE = 28; // Keep up to date with `--nav-font-size` in base.scss +const NAV_BUTTON_INITIAL_FONT_SIZE = 28; // Keep up to date with the initial `--nav-font-size` in base.scss const RIPPLE_ANIMATION_MILLISECONDS = 100; const RIPPLE_WIDTH = 100; const HANDLE_STRETCH = 0.4; -let ripplesInitialized; let navButtons; let rippleSvg; let ripplePath; @@ -12,21 +11,20 @@ let ripples; let activeRippleIndex; window.addEventListener("DOMContentLoaded", initializeRipples); -window.addEventListener("resize", () => animate(true)); function initializeRipples() { - ripplesInitialized = true; + window.addEventListener("resize", () => animate(true)); navButtons = document.querySelectorAll("header nav a"); rippleSvg = document.querySelector("header .ripple"); ripplePath = rippleSvg.querySelector("path"); - fullRippleHeight = Number.parseInt(window.getComputedStyle(rippleSvg).height, 10) - 4; + fullRippleHeight = Number.parseInt(window.getComputedStyle(rippleSvg).height, 10); ripples = Array.from(navButtons).map((button) => ({ element: button, + goingUp: false, animationStartTime: 0, animationEndTime: 0, - goingUp: false, })); activeRippleIndex = ripples.findIndex((ripple) => { @@ -51,11 +49,12 @@ function initializeRipples() { const elapsed = now - start; const remaining = stop - now; + ripple.goingUp = goingUp; + // Encode the potential reversing of direction via the animation start and end times ripple.animationStartTime = now < stop ? now - remaining : now; ripple.animationEndTime = now < stop ? now + elapsed : now + RIPPLE_ANIMATION_MILLISECONDS; - ripple.goingUp = goingUp; - animate(false); + animate(); }; ripple.element.addEventListener("pointerenter", () => updateTimings(true)); @@ -64,64 +63,66 @@ function initializeRipples() { if (activeRippleIndex >= 0) ripples[activeRippleIndex] = { ...ripples[activeRippleIndex], + goingUp: true, + // Set to non-zero, but very old times (1ms after epoch), so the math works out as if the animation has already completed animationStartTime: 1, animationEndTime: 1 + RIPPLE_ANIMATION_MILLISECONDS, - goingUp: true, }; setRipples(); } -function animate(forceRefresh) { - if (!ripplesInitialized) return; - - const animateThisFrame = ripples.some((ripple) => ripple.animationStartTime && ripple.animationEndTime && Date.now() <= ripple.animationEndTime); +function animate(forceRefresh = false) { + const FUZZ_MILLISECONDS = 100; + const animateThisFrame = ripples.some((ripple) => ripple.animationStartTime > 0 && ripple.animationEndTime > 0 && Date.now() <= ripple.animationEndTime + FUZZ_MILLISECONDS); if (animateThisFrame || forceRefresh) { setRipples(); - window.requestAnimationFrame(() => animate(false)); + window.requestAnimationFrame(() => animate()); } } function setRipples() { + const lerp = (a, b, t) => a + (b - a) * t; + const ease = (x) => 1 - (1 - x) * (1 - x); + const clamp01 = (x) => Math.min(Math.max(x, 0), 1); + + const rippleSvgRect = rippleSvg.getBoundingClientRect(); + const rippleStrokeWidth = Number.parseInt(window.getComputedStyle(ripplePath).getPropertyValue("--border-thickness"), 10); const navButtonFontSize = Number.parseInt(window.getComputedStyle(navButtons[0]).fontSize, 10) || NAV_BUTTON_INITIAL_FONT_SIZE; const mediaQueryScaleFactor = navButtonFontSize / NAV_BUTTON_INITIAL_FONT_SIZE; - const rippleHeight = Math.round(fullRippleHeight * mediaQueryScaleFactor) + (rippleStrokeWidth === 2 ? 0 : 0.5); - const rippleSvgRect = rippleSvg.getBoundingClientRect(); - const rippleSvgLeft = rippleSvgRect.left; - const rippleSvgWidth = rippleSvgRect.width; + // Position of bottom centerline to top centerline + const rippleBaselineCenterline = fullRippleHeight - rippleStrokeWidth / 2; + const rippleToplineCenterline = rippleStrokeWidth / 2; - let path = `M 0,${rippleHeight + 3} `; + let path = `M -16,${rippleBaselineCenterline - 16} L 0,${rippleBaselineCenterline} `; ripples.forEach((ripple) => { - if (!ripple.animationStartTime || !ripple.animationEndTime) return; + if (ripple.animationStartTime === 0 || ripple.animationEndTime === 0) return; - const t = Math.min((Date.now() - ripple.animationStartTime) / (ripple.animationEndTime - ripple.animationStartTime), 1); - const height = rippleHeight * (ripple.goingUp ? ease(t) : 1 - ease(t)); + const elapsed = Date.now() - ripple.animationStartTime; + const duration = ripple.animationEndTime - ripple.animationStartTime; + const t = ease(clamp01(elapsed / duration)); + + const bumpCrestRaiseFactor = (ripple.goingUp ? t : 1 - t) * mediaQueryScaleFactor; + const bumpCrest = lerp(rippleToplineCenterline, rippleBaselineCenterline, bumpCrestRaiseFactor); + const bumpCrestDelta = bumpCrest - rippleStrokeWidth / 2; const buttonRect = ripple.element.getBoundingClientRect(); - const buttonCenter = buttonRect.width / 2; - const rippleCenter = (RIPPLE_WIDTH / 2) * mediaQueryScaleFactor; + const rippleCenter = RIPPLE_WIDTH / 2 * mediaQueryScaleFactor; const rippleOffset = rippleCenter - buttonCenter; + const rippleStartX = buttonRect.left - rippleSvgRect.left - rippleOffset; + const handleRadius = rippleCenter * HANDLE_STRETCH; - const rippleStartX = buttonRect.left - rippleSvgLeft - rippleOffset; - - const rippleRadius = (RIPPLE_WIDTH / 2) * mediaQueryScaleFactor; - const handleRadius = rippleRadius * HANDLE_STRETCH; - - path += `L ${rippleStartX},${rippleHeight + 3} `; - path += `c ${handleRadius},0 ${rippleRadius - handleRadius},${-height} ${rippleRadius},${-height} `; - path += `s ${rippleRadius - handleRadius},${height} ${rippleRadius},${height} `; + path += `L ${rippleStartX},${rippleBaselineCenterline} `; + path += `c ${handleRadius},0 ${rippleCenter - handleRadius},${-bumpCrestDelta} ${rippleCenter},${-bumpCrestDelta} `; + path += `s ${rippleCenter - handleRadius},${bumpCrestDelta} ${rippleCenter},${bumpCrestDelta} `; }); - path += `l ${rippleSvgWidth},0`; + path += `L ${rippleSvgRect.width + 16},${rippleBaselineCenterline} L${rippleSvgRect.width + 16},${rippleBaselineCenterline - 16}`; ripplePath.setAttribute("d", path); } - -function ease(x) { - return 1 - (1 - x) * (1 - x); -} diff --git a/website/templates/base.html b/website/templates/base.html index e2082dfe48..b094eefbd2 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -82,14 +82,14 @@ diff --git a/website/templates/book.html b/website/templates/book.html index c3ca39e0ca..5d5ec2a488 100644 --- a/website/templates/book.html +++ b/website/templates/book.html @@ -38,7 +38,11 @@