From ff2c04a10090757450bba6c2cf768174ba8878a0 Mon Sep 17 00:00:00 2001 From: nym21 Date: Fri, 5 Jun 2026 16:03:04 +0200 Subject: [PATCH] website: redesign part 7 --- crates/brk_client/examples/basic.rs | 2 +- website_next/header/style.css | 2 +- website_next/index.html | 30 +++++++- website_next/learn/contents/index.js | 50 +++++++++++++ website_next/learn/contents/style.css | 60 +++++++++++++++ website_next/learn/index.js | 96 ++++++++++-------------- website_next/learn/style.css | 102 +++++--------------------- website_next/main.js | 35 +++++++-- website_next/styles/main.css | 23 +++--- website_next/styles/variables.css | 3 + website_next/utils/timing.js | 15 ++++ 11 files changed, 254 insertions(+), 164 deletions(-) create mode 100644 website_next/learn/contents/index.js create mode 100644 website_next/learn/contents/style.css create mode 100644 website_next/utils/timing.js diff --git a/crates/brk_client/examples/basic.rs b/crates/brk_client/examples/basic.rs index eb584bc99..c5eea27be 100644 --- a/crates/brk_client/examples/basic.rs +++ b/crates/brk_client/examples/basic.rs @@ -17,7 +17,7 @@ fn main() -> brk_client::Result<()> { // day1() returns DateMetricEndpointBuilder, so fetch() returns DateMetricData let price_close = client .series() - .prices + .price .split .close .usd diff --git a/website_next/header/style.css b/website_next/header/style.css index f88bbb3ab..bad412d1e 100644 --- a/website_next/header/style.css +++ b/website_next/header/style.css @@ -2,7 +2,7 @@ body { > header { position: fixed; inset: 1.5rem 2rem auto; - z-index: 10; + z-index: var(--layer-header); display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; diff --git a/website_next/index.html b/website_next/index.html index 61b84f9df..f8dd7a51c 100644 --- a/website_next/index.html +++ b/website_next/index.html @@ -20,21 +20,44 @@ background: var(--black); } + html { + --layer-transition: 8; + --layer-loading: 100; + --transition-duration: 150ms; + --reveal-duration: 500ms; + --overlay-layer: var(--layer-transition); + --overlay-duration: var(--transition-duration); + } + body::before { content: ""; position: fixed; inset: 0; - z-index: 100; + z-index: var(--overlay-layer); background: var(--black); opacity: 0; pointer-events: none; - transition: opacity 150ms ease; + transition: opacity var(--overlay-duration) ease; } - html[data-loading] body::before { + html[data-loading] body::before, + html[data-transition] body::before { opacity: 1; } + html[data-loading], + html[data-revealing] { + --overlay-layer: var(--layer-loading); + } + + html[data-revealing] { + --overlay-duration: var(--reveal-duration); + } + + html[data-transition] { + --overlay-layer: var(--layer-transition); + } + @media (prefers-reduced-motion: reduce) { body::before { transition: none; @@ -76,6 +99,7 @@ + diff --git a/website_next/learn/contents/index.js b/website_next/learn/contents/index.js new file mode 100644 index 000000000..e67a26bb4 --- /dev/null +++ b/website_next/learn/contents/index.js @@ -0,0 +1,50 @@ +/** + * @param {{ title: string, children: Section[] }} section + */ +function createSectionId(section) { + return section.title.toLowerCase().replaceAll(" ", "-"); +} + +/** + * @param {{ title: string, children: Section[] }} section + * @param {number[]} indexes + */ +function createContentsItem(section, indexes) { + const item = document.createElement("li"); + const anchor = document.createElement("a"); + anchor.href = `#${createSectionId(section)}`; + anchor.append(section.title); + + if (section.children.length) { + const list = document.createElement("ol"); + + for (const [index, child] of section.children.entries()) { + list.append(createContentsItem(child, indexes.concat(index + 1))); + } + item.append(list); + } + + item.prepend(anchor); + return item; +} + +/** @param {Section[]} sections */ +export function createContents(sections) { + const nav = document.createElement("nav"); + const list = document.createElement("ol"); + + nav.setAttribute("aria-label", "Learn contents"); + + for (const [index, section] of sections.entries()) { + list.append(createContentsItem(section, [index + 1])); + } + + nav.append(list); + return nav; +} + +/** + * @typedef {Object} Section + * @property {string} title + * @property {Section[]} children + */ diff --git a/website_next/learn/contents/style.css b/website_next/learn/contents/style.css new file mode 100644 index 000000000..6757c72fd --- /dev/null +++ b/website_next/learn/contents/style.css @@ -0,0 +1,60 @@ +main.learn { + > nav { + position: sticky; + top: 0; + padding-top: var(--top-offset); + padding-bottom: calc(var(--top-offset) / 2); + max-height: 100dvh; + overflow: auto; + scrollbar-width: thin; + font-size: var(--font-size-xs); + line-height: var(--line-height-xs); + text-transform: uppercase; + + ol { + list-style: none; + margin: 0; + padding: 0; + } + + > ol > li { + counter-increment: content-theme; + counter-reset: content-topic; + } + + ol ol { + margin-top: 0.5rem; + margin-left: 1rem; + color: var(--gray); + } + + ol ol > li { + counter-increment: content-topic; + } + + li + li { + margin-top: 0.5rem; + } + + a { + color: inherit; + text-decoration: none; + + &::before { + opacity: 0.5; + } + + &:hover { + color: var(--orange); + } + } + + > ol > li > a::before { + content: counter(content-theme, upper-roman) ". "; + } + + ol ol > li > a::before { + content: counter(content-topic) ". "; + } + } +} diff --git a/website_next/learn/index.js b/website_next/learn/index.js index 27867e702..96e2a383a 100644 --- a/website_next/learn/index.js +++ b/website_next/learn/index.js @@ -1,3 +1,4 @@ +import { createContents } from "./contents/index.js"; import { sections } from "./data.js"; /** @param {string} label */ @@ -31,7 +32,7 @@ function createSection(section, indexes) { const description = document.createElement("p"); const id = createSectionId(section.title); - title.id = id; + element.id = id; anchor.href = `#${id}`; anchor.append(section.title); title.append(anchor); @@ -45,49 +46,10 @@ function createSection(section, indexes) { return element; } -/** - * @param {{ title: string, children: Section[] }} section - * @param {number[]} indexes - */ -function createContentsItem(section, indexes) { - const item = document.createElement("li"); - const anchor = document.createElement("a"); - anchor.href = `#${createSectionId(section.title)}`; - anchor.append(section.title); - item.append(anchor); - - if (section.children.length) { - const list = document.createElement("ol"); - for (const [index, child] of section.children.entries()) { - list.append(createContentsItem(child, indexes.concat(index + 1))); - } - item.append(list); - } - - return item; -} - -function createContents() { - const nav = document.createElement("nav"); - const list = document.createElement("ol"); - - nav.setAttribute("aria-label", "Learn contents"); - - for (const [index, section] of sections.entries()) { - list.append(createContentsItem(section, [index + 1])); - } - - nav.append(list); - return nav; -} - -/** - * @param {HTMLElement} main - * @param {HTMLElement} article - */ -function initScrollSpy(main, article) { - const titles = [...article.querySelectorAll("h1[id], h2[id]")]; - const visible = new Set(); +/** @param {HTMLElement} main */ +function initScrollSpy(main) { + const headings = [...main.querySelectorAll("article h1, article h2")]; + const visibleHeadings = new Set(); const links = new Map( [...main.querySelectorAll('nav a[href^="#"]')].map((link) => [ link.getAttribute("href"), @@ -98,36 +60,54 @@ function initScrollSpy(main, article) { /** @type {string | null} */ let current = null; - function update() { - const title = titles.find((title) => visible.has(title.id)); - if (!title) return; + /** @param {Element} heading */ + function getHash(heading) { + const section = /** @type {HTMLElement} */ ( + heading.closest("section[id]") + ); + return `#${section.id}`; + } - const hash = `#${title.id}`; + /** @param {string} hash */ + function getLink(hash) { + return /** @type {HTMLAnchorElement} */ (links.get(hash)); + } + + /** @param {string} hash */ + function setCurrent(hash) { if (hash === current) return; - links.get(current)?.removeAttribute("aria-current"); - links.get(hash)?.setAttribute("aria-current", "location"); + if (current) getLink(current).removeAttribute("aria-current"); + getLink(hash).setAttribute("aria-current", "location"); history.replaceState(null, "", hash); current = hash; } + function update() { + if (main.hidden) return; + + const heading = headings.findLast((heading) => + visibleHeadings.has(heading), + ); + if (heading) setCurrent(getHash(heading)); + } + const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { - visible.add(entry.target.id); + visibleHeadings.add(entry.target); } else { - visible.delete(entry.target.id); + visibleHeadings.delete(entry.target); } } + update(); }, - { root: article }, + { rootMargin: "0px 0px -80% 0px" }, ); - for (const title of titles) { - observer.observe(title); - } + for (const heading of headings) observer.observe(heading); } export function createLearnPage() { @@ -139,8 +119,8 @@ export function createLearnPage() { article.append(createSection(section, [index + 1])); } - main.append(article, createContents()); - initScrollSpy(main, article); + main.append(createContents(sections), article); + initScrollSpy(main); return main; } diff --git a/website_next/learn/style.css b/website_next/learn/style.css index 11e572970..a77600439 100644 --- a/website_next/learn/style.css +++ b/website_next/learn/style.css @@ -1,19 +1,16 @@ main.learn { - --sticky-h: 5rem; - --sidebar-top: 6rem; + --top-offset: 6rem; --sidebar-bottom: 1rem; - display: flex; - overflow: hidden; + display: grid; + grid-template-columns: 14rem minmax(0, 1fr); + gap: 4rem; + padding: 0 2rem; article { - position: relative; counter-reset: theme; - min-width: 0; - min-height: 0; - overflow: auto; - scroll-behavior: smooth; - scrollbar-gutter: stable; + padding-top: var(--top-offset); + padding-bottom: calc(var(--top-offset) / 2); &::before { content: ""; @@ -22,9 +19,10 @@ main.learn { z-index: 2; display: block; width: min(100%, 52rem); - height: var(--sticky-h); + height: var(--top-offset); + margin-top: calc(-1 * var(--top-offset)); margin-inline: auto; - margin-bottom: calc(-1 * var(--sticky-h)); + margin-bottom: calc(-1 * var(--top-offset)); background: var(--black); pointer-events: none; } @@ -34,6 +32,12 @@ main.learn { counter-reset: topic; width: min(100%, 52rem); margin-inline: auto; + scroll-margin-top: var(--top-offset); + } + + > section:first-of-type { + margin-top: calc(-1 * var(--top-offset)); + padding-top: var(--top-offset); } > section + section { @@ -42,7 +46,7 @@ main.learn { section section { counter-increment: topic; - margin-top: 4rem; + scroll-margin-top: var(--top-offset); } } @@ -86,8 +90,7 @@ main.learn { h1 { z-index: 3; - top: var(--sticky-h); - scroll-margin-top: var(--sticky-h); + top: var(--top-offset); padding-bottom: 0.5rem; border-bottom: 1px solid var(--dark-gray); font-size: 2.75rem; @@ -99,9 +102,8 @@ main.learn { h2 { z-index: 1; - top: var(--sticky-h); - scroll-margin-top: var(--sticky-h); - padding-top: 3.5rem; + top: var(--top-offset); + padding-top: 4.5rem; padding-bottom: 0.5rem; border-bottom: 1px dashed var(--dark-gray); font-size: 1.5rem; @@ -134,68 +136,4 @@ main.learn { margin-top: 0.75rem; } } - - > nav { - counter-reset: content-theme; - min-height: 0; - overflow: auto; - scrollbar-gutter: stable; - padding-top: var(--sidebar-top); - padding-bottom: var(--sidebar-bottom); - font-size: var(--font-size-xs); - line-height: var(--line-height-sm); - text-transform: uppercase; - - ol { - list-style: none; - margin: 0; - padding: 0; - } - - > ol > li { - counter-increment: content-theme; - counter-reset: content-topic; - } - - ol ol { - margin-top: 0.5rem; - margin-left: 1rem; - color: var(--gray); - } - - ol ol > li { - counter-increment: content-topic; - } - - li + li { - margin-top: 0.5rem; - } - - a { - color: inherit; - text-decoration: none; - - &::before { - opacity: 0.5; - } - - &:hover { - color: var(--orange); - } - } - - > ol > li > a::before { - content: counter(content-theme, upper-roman) ". "; - } - - ol ol > li > a::before { - content: counter(content-topic) ". "; - } - } -} - -@media (prefers-reduced-motion: reduce) { - main.learn article { - scroll-behavior: auto; - } } diff --git a/website_next/main.js b/website_next/main.js index 5987b96df..d163db183 100644 --- a/website_next/main.js +++ b/website_next/main.js @@ -3,6 +3,7 @@ import { createBuildPage } from "./build/index.js"; import { createExplorePage } from "./explore/index.js"; import { createHomePage } from "./home/index.js"; import { createLearnPage } from "./learn/index.js"; +import { readCssDuration, wait } from "./utils/timing.js"; /** @type {Record HTMLElement>} */ const routes = { @@ -18,6 +19,14 @@ let currentPage; /** @type {Map} */ const pageByPath = new Map(); +function waitForTransition() { + return wait(readCssDuration("--transition-duration")); +} + +function waitForReveal() { + return wait(readCssDuration("--reveal-duration")); +} + /** @param {string} pathname */ function normalizePath(pathname) { return pathname in routes ? pathname : "/"; @@ -45,6 +54,7 @@ function getPage(pathname) { if (!page) { page = routes[pathname](); + page.hidden = true; page.inert = true; pageByPath.set(pathname, page); document.body.append(page); @@ -56,12 +66,12 @@ function getPage(pathname) { /** @param {HTMLElement} page */ function activatePage(page) { if (currentPage) { + currentPage.hidden = true; currentPage.inert = true; - delete currentPage.dataset.active; } + page.hidden = false; page.inert = false; - page.dataset.active = ""; currentPage = page; } @@ -73,11 +83,18 @@ function renderPage() { /** @param {string} pathname */ function navigate(pathname) { - if (pathname !== window.location.pathname) { - history.pushState(null, "", pathname); - } + if (pathname === window.location.pathname) return; + history.pushState(null, "", pathname); + transitionPage(); +} +async function transitionPage() { + document.documentElement.dataset.transition = ""; + await waitForTransition(); renderPage(); + requestAnimationFrame(() => { + delete document.documentElement.dataset.transition; + }); } document.addEventListener("click", (event) => { @@ -105,7 +122,11 @@ window.addEventListener("popstate", renderPage); renderPage(); requestAnimationFrame(() => { - setTimeout(() => { + waitForTransition().then(() => { delete document.documentElement.dataset.loading; - }, 150); + document.documentElement.dataset.revealing = ""; + waitForReveal().then(() => { + delete document.documentElement.dataset.revealing; + }); + }); }); diff --git a/website_next/styles/main.css b/website_next/styles/main.css index 856b971f0..617914a11 100644 --- a/website_next/styles/main.css +++ b/website_next/styles/main.css @@ -7,27 +7,26 @@ body { background: var(--black); } +html { + scroll-behavior: smooth; +} + body { > main { - position: fixed; - inset: 0; - overflow: auto; color: white; - opacity: 0; - pointer-events: none; - scroll-behavior: smooth; - transition: opacity 150ms ease; } +} - > main[data-active] { - opacity: 1; - pointer-events: auto; - } +main { + min-height: 100dvh; } @media (prefers-reduced-motion: reduce) { - body > main { + html { scroll-behavior: auto; + } + + body::before { transition: none; } } diff --git a/website_next/styles/variables.css b/website_next/styles/variables.css index 6132d5bda..b15a50256 100644 --- a/website_next/styles/variables.css +++ b/website_next/styles/variables.css @@ -48,4 +48,7 @@ --negative-main-padding: calc(-1 * var(--main-padding)); --font-weight-base: 400; --max-main-width: 70dvw; + --layer-transition: 8; + --layer-header: 10; + --layer-loading: 100; } diff --git a/website_next/utils/timing.js b/website_next/utils/timing.js new file mode 100644 index 000000000..fd7f3d62b --- /dev/null +++ b/website_next/utils/timing.js @@ -0,0 +1,15 @@ +/** @param {number} ms */ +export function wait(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +/** @param {string} name */ +export function readCssDuration(name) { + const value = getComputedStyle(document.documentElement) + .getPropertyValue(name) + .trim(); + + return Number.parseFloat(value) * (value.endsWith("ms") ? 1 : 1000); +}