mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-10 06:53:33 -07:00
website: redesign part 7
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+27
-3
@@ -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 @@
|
||||
<link rel="stylesheet" href="/home/style.css" />
|
||||
<link rel="stylesheet" href="/explore/style.css" />
|
||||
<link rel="stylesheet" href="/learn/style.css" />
|
||||
<link rel="stylesheet" href="/learn/contents/style.css" />
|
||||
<link rel="stylesheet" href="/build/style.css" />
|
||||
<!-- /IMPORTMAP -->
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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) ". ";
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-58
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+28
-7
@@ -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<string, () => HTMLElement>} */
|
||||
const routes = {
|
||||
@@ -18,6 +19,14 @@ let currentPage;
|
||||
/** @type {Map<string, HTMLElement>} */
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user