mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-25 17:58:11 -07:00
website: redesign part 8
This commit is contained in:
@@ -14,15 +14,17 @@ npx --package typescript tsc --noEmit --pretty false | grep -v "modules/"
|
|||||||
|
|
||||||
ALWAYS
|
ALWAYS
|
||||||
|
|
||||||
- fast
|
- very fast
|
||||||
|
- light (memory)
|
||||||
- KISS
|
- KISS
|
||||||
- DRY
|
- DRY
|
||||||
- very well organized
|
- very well organized
|
||||||
- contained
|
- contained
|
||||||
- colocated
|
- colocated
|
||||||
|
- composed
|
||||||
- prefer one concept per file
|
- prefer one concept per file
|
||||||
- prefer more files and folders than big files
|
- prefer more files and folders than big files
|
||||||
- reads like english
|
- reads like english
|
||||||
- very easy to understand
|
- easy to understand
|
||||||
- very easy to maintain
|
- maintainability
|
||||||
- avoid defensive checks when the code itself guarantees correctness
|
- avoid defensive checks when the code itself guarantees correctness
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
import { createCube } from "../cube/index.js";
|
import { createCube } from "../cube/index.js";
|
||||||
|
import { primaryRoutes } from "../routes.js";
|
||||||
|
|
||||||
const header = document.createElement("header");
|
export function createHeader() {
|
||||||
|
const header = document.createElement("header");
|
||||||
|
|
||||||
const home = document.createElement("a");
|
const home = document.createElement("a");
|
||||||
const cube = document.createElement("span");
|
const cube = document.createElement("span");
|
||||||
|
|
||||||
home.href = "/";
|
home.href = "/";
|
||||||
home.ariaLabel = "bitview home";
|
home.ariaLabel = "bitview home";
|
||||||
cube.append(createCube());
|
cube.append(createCube());
|
||||||
home.append(cube, "bitview");
|
home.append(cube, "bitview");
|
||||||
|
|
||||||
const nav = document.createElement("nav");
|
const nav = document.createElement("nav");
|
||||||
nav.setAttribute("aria-label", "Primary");
|
const list = document.createElement("ul");
|
||||||
nav.innerHTML = `
|
nav.setAttribute("aria-label", "Primary");
|
||||||
<ul>
|
|
||||||
<li><a href="/explore" aria-current="page">Explore</a></li>
|
|
||||||
<li><a href="/learn">Learn</a></li>
|
|
||||||
<li><a href="/build">Build</a></li>
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
|
|
||||||
header.append(home, nav);
|
for (const { pathname, label } of primaryRoutes) {
|
||||||
document.body.append(header);
|
const item = document.createElement("li");
|
||||||
|
const anchor = document.createElement("a");
|
||||||
|
|
||||||
|
anchor.href = pathname;
|
||||||
|
anchor.append(label);
|
||||||
|
item.append(anchor);
|
||||||
|
list.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.append(list);
|
||||||
|
header.append(home, nav);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
body {
|
body {
|
||||||
> header {
|
> header {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 1.5rem 2rem auto;
|
inset: 1.5rem var(--page-x) auto;
|
||||||
z-index: var(--layer-header);
|
z-index: var(--layer-header);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
|||||||
@@ -89,9 +89,9 @@
|
|||||||
|
|
||||||
<!-- IMPORTMAP -->
|
<!-- IMPORTMAP -->
|
||||||
<link rel="stylesheet" href="/styles/reset.css" />
|
<link rel="stylesheet" href="/styles/reset.css" />
|
||||||
<link rel="stylesheet" href="/styles/main.css" />
|
|
||||||
<link rel="stylesheet" href="/styles/fonts.css" />
|
|
||||||
<link rel="stylesheet" href="/styles/variables.css" />
|
<link rel="stylesheet" href="/styles/variables.css" />
|
||||||
|
<link rel="stylesheet" href="/styles/fonts.css" />
|
||||||
|
<link rel="stylesheet" href="/styles/main.css" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/cube/style.css" />
|
<link rel="stylesheet" href="/cube/style.css" />
|
||||||
<link rel="stylesheet" href="/header/style.css" />
|
<link rel="stylesheet" href="/header/style.css" />
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
/**
|
import { createId } from "../../utils/id.js";
|
||||||
* @param {{ title: string, children: Section[] }} section
|
|
||||||
*/
|
|
||||||
function createSectionId(section) {
|
|
||||||
return section.title.toLowerCase().replaceAll(" ", "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ title: string, children: Section[] }} section
|
* @param {{ title: string, children: Section[] }} section
|
||||||
* @param {number[]} indexes
|
|
||||||
*/
|
*/
|
||||||
function createContentsItem(section, indexes) {
|
function createContentsItem(section) {
|
||||||
const item = document.createElement("li");
|
const item = document.createElement("li");
|
||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
anchor.href = `#${createSectionId(section)}`;
|
anchor.href = `#${createId(section.title)}`;
|
||||||
anchor.append(section.title);
|
anchor.append(section.title);
|
||||||
|
|
||||||
if (section.children.length) {
|
if (section.children.length) {
|
||||||
const list = document.createElement("ol");
|
const list = document.createElement("ol");
|
||||||
|
|
||||||
for (const [index, child] of section.children.entries()) {
|
for (const child of section.children) {
|
||||||
list.append(createContentsItem(child, indexes.concat(index + 1)));
|
list.append(createContentsItem(child));
|
||||||
}
|
}
|
||||||
item.append(list);
|
item.append(list);
|
||||||
}
|
}
|
||||||
@@ -35,8 +29,8 @@ export function createContents(sections) {
|
|||||||
|
|
||||||
nav.setAttribute("aria-label", "Learn contents");
|
nav.setAttribute("aria-label", "Learn contents");
|
||||||
|
|
||||||
for (const [index, section] of sections.entries()) {
|
for (const section of sections) {
|
||||||
list.append(createContentsItem(section, [index + 1]));
|
list.append(createContentsItem(section));
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.append(list);
|
nav.append(list);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
main.learn {
|
main.learn {
|
||||||
> nav {
|
> nav {
|
||||||
|
counter-reset: content-theme;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
padding-top: var(--top-offset);
|
padding-top: var(--top-offset);
|
||||||
@@ -44,7 +45,7 @@ main.learn {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:is(:hover, [aria-current="location"]) {
|
||||||
color: var(--orange);
|
color: var(--orange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-79
@@ -1,5 +1,7 @@
|
|||||||
import { createContents } from "./contents/index.js";
|
import { createContents } from "./contents/index.js";
|
||||||
import { sections } from "./data.js";
|
import { sections } from "./data.js";
|
||||||
|
import { initScrollSpy } from "./scroll-spy.js";
|
||||||
|
import { createId } from "../utils/id.js";
|
||||||
|
|
||||||
/** @param {string} label */
|
/** @param {string} label */
|
||||||
function createChart(label) {
|
function createChart(label) {
|
||||||
@@ -14,23 +16,16 @@ function createChart(label) {
|
|||||||
return figure;
|
return figure;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} title
|
|
||||||
*/
|
|
||||||
function createSectionId(title) {
|
|
||||||
return title.toLowerCase().replaceAll(" ", "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Section} section
|
* @param {Section} section
|
||||||
* @param {number[]} indexes
|
* @param {number} [level]
|
||||||
*/
|
*/
|
||||||
function createSection(section, indexes) {
|
function createSection(section, level = 1) {
|
||||||
const element = document.createElement("section");
|
const element = document.createElement("section");
|
||||||
const title = document.createElement(indexes.length === 1 ? "h1" : "h2");
|
const title = document.createElement(level === 1 ? "h1" : "h2");
|
||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
const description = document.createElement("p");
|
const description = document.createElement("p");
|
||||||
const id = createSectionId(section.title);
|
const id = createId(section.title);
|
||||||
|
|
||||||
element.id = id;
|
element.id = id;
|
||||||
anchor.href = `#${id}`;
|
anchor.href = `#${id}`;
|
||||||
@@ -39,84 +34,20 @@ function createSection(section, indexes) {
|
|||||||
description.append(section.description);
|
description.append(section.description);
|
||||||
element.append(title, description, createChart(section.chart));
|
element.append(title, description, createChart(section.chart));
|
||||||
|
|
||||||
for (const [index, child] of section.children.entries()) {
|
for (const child of section.children) {
|
||||||
element.append(createSection(child, indexes.concat(index + 1)));
|
element.append(createSection(child, level + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @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"),
|
|
||||||
link,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
/** @type {string | null} */
|
|
||||||
let current = null;
|
|
||||||
|
|
||||||
/** @param {Element} heading */
|
|
||||||
function getHash(heading) {
|
|
||||||
const section = /** @type {HTMLElement} */ (
|
|
||||||
heading.closest("section[id]")
|
|
||||||
);
|
|
||||||
return `#${section.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} hash */
|
|
||||||
function getLink(hash) {
|
|
||||||
return /** @type {HTMLAnchorElement} */ (links.get(hash));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} hash */
|
|
||||||
function setCurrent(hash) {
|
|
||||||
if (hash === current) return;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
visibleHeadings.add(entry.target);
|
|
||||||
} else {
|
|
||||||
visibleHeadings.delete(entry.target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update();
|
|
||||||
},
|
|
||||||
{ rootMargin: "0px 0px -80% 0px" },
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const heading of headings) observer.observe(heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createLearnPage() {
|
export function createLearnPage() {
|
||||||
const main = document.createElement("main");
|
const main = document.createElement("main");
|
||||||
main.className = "learn";
|
main.className = "learn";
|
||||||
const article = document.createElement("article");
|
const article = document.createElement("article");
|
||||||
|
|
||||||
for (const [index, section] of sections.entries()) {
|
for (const section of sections) {
|
||||||
article.append(createSection(section, [index + 1]));
|
article.append(createSection(section));
|
||||||
}
|
}
|
||||||
|
|
||||||
main.append(createContents(sections), article);
|
main.append(createContents(sections), article);
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/** @param {HTMLElement} main */
|
||||||
|
export 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"),
|
||||||
|
link,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @type {string | null} */
|
||||||
|
let current = null;
|
||||||
|
|
||||||
|
/** @param {Element} heading */
|
||||||
|
function getHash(heading) {
|
||||||
|
const section = /** @type {HTMLElement} */ (
|
||||||
|
heading.closest("section[id]")
|
||||||
|
);
|
||||||
|
return `#${section.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} hash */
|
||||||
|
function getLink(hash) {
|
||||||
|
return /** @type {HTMLAnchorElement} */ (links.get(hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} hash */
|
||||||
|
function setCurrent(hash) {
|
||||||
|
if (hash === current) return;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
visibleHeadings.add(entry.target);
|
||||||
|
} else {
|
||||||
|
visibleHeadings.delete(entry.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
{ rootMargin: "0px 0px -80% 0px" },
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const heading of headings) observer.observe(heading);
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
main.learn {
|
main.learn {
|
||||||
--top-offset: 6rem;
|
--top-offset: 6rem;
|
||||||
--sidebar-bottom: 1rem;
|
--content-width: 52rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 14rem minmax(0, 1fr);
|
grid-template-columns: 14rem minmax(0, 1fr);
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
padding: 0 2rem;
|
padding: 0 var(--page-x);
|
||||||
|
|
||||||
article {
|
article {
|
||||||
counter-reset: theme;
|
counter-reset: theme;
|
||||||
@@ -18,7 +18,6 @@ main.learn {
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: block;
|
display: block;
|
||||||
width: min(100%, 52rem);
|
|
||||||
height: var(--top-offset);
|
height: var(--top-offset);
|
||||||
margin-top: calc(-1 * var(--top-offset));
|
margin-top: calc(-1 * var(--top-offset));
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
@@ -30,7 +29,7 @@ main.learn {
|
|||||||
> section {
|
> section {
|
||||||
counter-increment: theme;
|
counter-increment: theme;
|
||||||
counter-reset: topic;
|
counter-reset: topic;
|
||||||
width: min(100%, 52rem);
|
width: min(100%, var(--content-width));
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
scroll-margin-top: var(--top-offset);
|
scroll-margin-top: var(--top-offset);
|
||||||
}
|
}
|
||||||
@@ -53,13 +52,15 @@ main.learn {
|
|||||||
h1,
|
h1,
|
||||||
h2 {
|
h2 {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
top: var(--top-offset);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
background: var(--black);
|
background: var(--black);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: white;
|
color: var(--white);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@@ -90,8 +91,6 @@ main.learn {
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
top: var(--top-offset);
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom: 1px solid var(--dark-gray);
|
border-bottom: 1px solid var(--dark-gray);
|
||||||
font-size: 2.75rem;
|
font-size: 2.75rem;
|
||||||
|
|
||||||
@@ -102,9 +101,7 @@ main.learn {
|
|||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: var(--top-offset);
|
|
||||||
padding-top: 4.5rem;
|
padding-top: 4.5rem;
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom: 1px dashed var(--dark-gray);
|
border-bottom: 1px dashed var(--dark-gray);
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
|||||||
+13
-48
@@ -1,17 +1,7 @@
|
|||||||
import "./header/index.js";
|
import { createHeader } from "./header/index.js";
|
||||||
import { createBuildPage } from "./build/index.js";
|
import { createRoutePage, isRoute, normalizePath } from "./routes.js";
|
||||||
import { createExplorePage } from "./explore/index.js";
|
import { getEventAnchor } from "./utils/event.js";
|
||||||
import { createHomePage } from "./home/index.js";
|
import { revealPage, transitionPage } from "./utils/transition.js";
|
||||||
import { createLearnPage } from "./learn/index.js";
|
|
||||||
import { readCssDuration, wait } from "./utils/timing.js";
|
|
||||||
|
|
||||||
/** @type {Record<string, () => HTMLElement>} */
|
|
||||||
const routes = {
|
|
||||||
"/": createHomePage,
|
|
||||||
"/build": createBuildPage,
|
|
||||||
"/explore": createExplorePage,
|
|
||||||
"/learn": createLearnPage,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {HTMLElement | undefined} */
|
/** @type {HTMLElement | undefined} */
|
||||||
let currentPage;
|
let currentPage;
|
||||||
@@ -19,24 +9,16 @@ let currentPage;
|
|||||||
/** @type {Map<string, HTMLElement>} */
|
/** @type {Map<string, HTMLElement>} */
|
||||||
const pageByPath = new Map();
|
const pageByPath = new Map();
|
||||||
|
|
||||||
function waitForTransition() {
|
const header = createHeader();
|
||||||
return wait(readCssDuration("--transition-duration"));
|
document.body.append(header);
|
||||||
}
|
|
||||||
|
|
||||||
function waitForReveal() {
|
const navLinks = [...header.querySelectorAll("nav a")];
|
||||||
return wait(readCssDuration("--reveal-duration"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} pathname */
|
|
||||||
function normalizePath(pathname) {
|
|
||||||
return pathname in routes ? pathname : "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} pathname */
|
/** @param {string} pathname */
|
||||||
function updateCurrentLink(pathname) {
|
function updateCurrentLink(pathname) {
|
||||||
const currentPath = normalizePath(pathname);
|
const currentPath = normalizePath(pathname);
|
||||||
|
|
||||||
for (const link of document.querySelectorAll("body > header > nav a")) {
|
for (const link of navLinks) {
|
||||||
const linkPath = new URL(/** @type {HTMLAnchorElement} */ (link).href)
|
const linkPath = new URL(/** @type {HTMLAnchorElement} */ (link).href)
|
||||||
.pathname;
|
.pathname;
|
||||||
|
|
||||||
@@ -53,7 +35,7 @@ function getPage(pathname) {
|
|||||||
let page = pageByPath.get(pathname);
|
let page = pageByPath.get(pathname);
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
page = routes[pathname]();
|
page = createRoutePage(pathname);
|
||||||
page.hidden = true;
|
page.hidden = true;
|
||||||
page.inert = true;
|
page.inert = true;
|
||||||
pageByPath.set(pathname, page);
|
pageByPath.set(pathname, page);
|
||||||
@@ -85,16 +67,7 @@ function renderPage() {
|
|||||||
function navigate(pathname) {
|
function navigate(pathname) {
|
||||||
if (pathname === window.location.pathname) return;
|
if (pathname === window.location.pathname) return;
|
||||||
history.pushState(null, "", pathname);
|
history.pushState(null, "", pathname);
|
||||||
transitionPage();
|
void transitionPage(renderPage);
|
||||||
}
|
|
||||||
|
|
||||||
async function transitionPage() {
|
|
||||||
document.documentElement.dataset.transition = "";
|
|
||||||
await waitForTransition();
|
|
||||||
renderPage();
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
delete document.documentElement.dataset.transition;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
@@ -102,16 +75,14 @@ document.addEventListener("click", (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchor = /** @type {HTMLAnchorElement | null} */ (
|
const anchor = getEventAnchor(event);
|
||||||
/** @type {HTMLElement} */ (event.target).closest("a[href]")
|
|
||||||
);
|
|
||||||
if (!anchor) return;
|
if (!anchor) return;
|
||||||
|
|
||||||
const url = new URL(anchor.href);
|
const url = new URL(anchor.href);
|
||||||
if (url.origin !== window.location.origin) return;
|
if (url.origin !== window.location.origin) return;
|
||||||
if (url.pathname === window.location.pathname && url.hash) return;
|
if (url.pathname === window.location.pathname && url.hash) return;
|
||||||
|
|
||||||
if (!(url.pathname in routes)) return;
|
if (!isRoute(url.pathname)) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
navigate(url.pathname);
|
navigate(url.pathname);
|
||||||
@@ -122,11 +93,5 @@ window.addEventListener("popstate", renderPage);
|
|||||||
renderPage();
|
renderPage();
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
waitForTransition().then(() => {
|
void revealPage();
|
||||||
delete document.documentElement.dataset.loading;
|
|
||||||
document.documentElement.dataset.revealing = "";
|
|
||||||
waitForReveal().then(() => {
|
|
||||||
delete document.documentElement.dataset.revealing;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { createBuildPage } from "./build/index.js";
|
||||||
|
import { createExplorePage } from "./explore/index.js";
|
||||||
|
import { createHomePage } from "./home/index.js";
|
||||||
|
import { createLearnPage } from "./learn/index.js";
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
{ pathname: "/", createPage: createHomePage },
|
||||||
|
{ pathname: "/explore", label: "Explore", createPage: createExplorePage },
|
||||||
|
{ pathname: "/learn", label: "Learn", createPage: createLearnPage },
|
||||||
|
{ pathname: "/build", label: "Build", createPage: createBuildPage },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @type {Record<string, () => HTMLElement>} */
|
||||||
|
const routes = Object.fromEntries(
|
||||||
|
pages.map(({ pathname, createPage }) => [pathname, createPage]),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const primaryRoutes = pages.flatMap(({ pathname, label }) =>
|
||||||
|
label ? [{ pathname, label }] : [],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @param {string} pathname */
|
||||||
|
export function isRoute(pathname) {
|
||||||
|
return pathname in routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} pathname */
|
||||||
|
export function normalizePath(pathname) {
|
||||||
|
return isRoute(pathname) ? pathname : "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} pathname */
|
||||||
|
export function createRoutePage(pathname) {
|
||||||
|
return routes[pathname]();
|
||||||
|
}
|
||||||
@@ -13,20 +13,13 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
> main {
|
> main {
|
||||||
color: white;
|
min-height: 100dvh;
|
||||||
|
color: var(--white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
|
||||||
min-height: 100dvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html {
|
html {
|
||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::before {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
--white: oklch(95% 0 0);
|
--white: oklch(95% 0 0);
|
||||||
--dark-white: oklch(92.5% 0 0);
|
--dark-white: oklch(92.5% 0 0);
|
||||||
@@ -44,11 +44,10 @@
|
|||||||
--font-size-xl: 1.25rem;
|
--font-size-xl: 1.25rem;
|
||||||
--line-height-xl: calc(1.75 / 1.25);
|
--line-height-xl: calc(1.75 / 1.25);
|
||||||
|
|
||||||
|
--page-x: 2rem;
|
||||||
--main-padding: 2rem;
|
--main-padding: 2rem;
|
||||||
--negative-main-padding: calc(-1 * var(--main-padding));
|
--negative-main-padding: calc(-1 * var(--main-padding));
|
||||||
--font-weight-base: 400;
|
--font-weight-base: 400;
|
||||||
--max-main-width: 70dvw;
|
--max-main-width: 70dvw;
|
||||||
--layer-transition: 8;
|
|
||||||
--layer-header: 10;
|
--layer-header: 10;
|
||||||
--layer-loading: 100;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* @param {Event} event
|
||||||
|
* @param {string} selector
|
||||||
|
*/
|
||||||
|
export function getEventTarget(event, selector) {
|
||||||
|
return /** @type {HTMLElement | null} */ (
|
||||||
|
/** @type {HTMLElement} */ (event.target).closest(selector)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} event */
|
||||||
|
export function getEventAnchor(event) {
|
||||||
|
return /** @type {HTMLAnchorElement | null} */ (
|
||||||
|
getEventTarget(event, "a[href]")
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/** @param {string} value */
|
||||||
|
export function createId(value) {
|
||||||
|
return value.toLowerCase().replaceAll(" ", "-");
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/** @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);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
let transitionId = 0;
|
||||||
|
|
||||||
|
/** @param {number} ms */
|
||||||
|
function wait(ms) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} name */
|
||||||
|
function readCssDuration(name) {
|
||||||
|
const value = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue(name)
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return Number.parseFloat(value) * (value.endsWith("ms") ? 1 : 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForTransition() {
|
||||||
|
return wait(readCssDuration("--transition-duration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForReveal() {
|
||||||
|
return wait(readCssDuration("--reveal-duration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {() => void} render */
|
||||||
|
export async function transitionPage(render) {
|
||||||
|
const id = ++transitionId;
|
||||||
|
document.documentElement.dataset.transition = "";
|
||||||
|
await waitForTransition();
|
||||||
|
if (id !== transitionId) return;
|
||||||
|
|
||||||
|
render();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (id === transitionId) delete document.documentElement.dataset.transition;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function revealPage() {
|
||||||
|
await waitForTransition();
|
||||||
|
delete document.documentElement.dataset.loading;
|
||||||
|
document.documentElement.dataset.revealing = "";
|
||||||
|
await waitForReveal();
|
||||||
|
delete document.documentElement.dataset.revealing;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user