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);
+}