mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-10 15:03:32 -07:00
website: redesign part 15
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
main.home {
|
||||
--home-offset: 6rem;
|
||||
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
padding: var(--home-offset) var(--page-x);
|
||||
padding: 6rem var(--page-x);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* @param {ChartSeries[]} series
|
||||
* @returns {ChartSeries[]}
|
||||
*/
|
||||
export function createSeries(series) {
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ChartSeries} series
|
||||
* @returns {ChartSeries}
|
||||
*/
|
||||
export function referenceLine(series) {
|
||||
return { ...series, role: "line" };
|
||||
}
|
||||
|
||||
/** @typedef {import("./index.js").ChartSeries} ChartSeries */
|
||||
@@ -40,13 +40,14 @@ export function createSeriesHighlight(items) {
|
||||
}
|
||||
|
||||
/** @param {number} index */
|
||||
function activateItem(index) {
|
||||
setActive(items[index], true);
|
||||
function previewItem(index) {
|
||||
scrollToItem(index);
|
||||
items[index].dataset.preview = "";
|
||||
}
|
||||
|
||||
/** @param {number} index */
|
||||
function clearItem(index) {
|
||||
clearState(items[index]);
|
||||
function clearPreview(index) {
|
||||
delete items[index].dataset.preview;
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
@@ -62,11 +63,6 @@ export function createSeriesHighlight(items) {
|
||||
*/
|
||||
function add(node, index) {
|
||||
seriesNodes[index].push(node);
|
||||
node.addEventListener("pointerenter", () => {
|
||||
scrollToItem(index);
|
||||
activateItem(index);
|
||||
});
|
||||
node.addEventListener("pointerleave", () => clearItem(index));
|
||||
}
|
||||
|
||||
function clearNodes() {
|
||||
@@ -79,7 +75,9 @@ export function createSeriesHighlight(items) {
|
||||
|
||||
return {
|
||||
add,
|
||||
clearPreview,
|
||||
clearNodes,
|
||||
preview: previewItem,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,6 +99,7 @@ function setActive(element, active) {
|
||||
function clearState(element) {
|
||||
delete element.dataset.active;
|
||||
delete element.dataset.muted;
|
||||
delete element.dataset.preview;
|
||||
}
|
||||
|
||||
/** @typedef {(SVGPathElement | SVGCircleElement)[]} SeriesNode */
|
||||
@@ -108,5 +107,7 @@ function clearState(element) {
|
||||
/**
|
||||
* @typedef {Object} SeriesHighlight
|
||||
* @property {(node: SVGPathElement | SVGCircleElement, index: number) => void} add
|
||||
* @property {(index: number) => void} clearPreview
|
||||
* @property {() => void} clearNodes
|
||||
* @property {(index: number) => void} preview
|
||||
*/
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { brk } from "../../utils/client.js";
|
||||
import { renderBarPlot } from "./bar/index.js";
|
||||
import { createFullscreenButton } from "./fullscreen.js";
|
||||
import { createSeriesHighlight } from "./highlight.js";
|
||||
import { onChartVisibility } from "./intersection.js";
|
||||
import { createLegend } from "./legend.js";
|
||||
import { renderLinePlot } from "./line/index.js";
|
||||
import { createChartRenderer } from "./renderer.js";
|
||||
import {
|
||||
createScaleControl,
|
||||
getDefaultScale,
|
||||
saveScale,
|
||||
} from "./scale.js";
|
||||
import { createScrubber } from "./scrubber.js";
|
||||
import { renderDotsPlot } from "./dots/index.js";
|
||||
import { createSvgElement } from "./svg.js";
|
||||
import { renderStackedPlot } from "./stacked/index.js";
|
||||
import {
|
||||
createTimeframeControl,
|
||||
fetchTimeframe,
|
||||
getDefaultTimeframe,
|
||||
saveTimeframe,
|
||||
} from "./timeframes.js";
|
||||
@@ -25,203 +18,7 @@ import {
|
||||
getDefaultView,
|
||||
saveView,
|
||||
} from "./views.js";
|
||||
import {
|
||||
FALLBACK_VIEWBOX_HEIGHT,
|
||||
getViewBoxHeight,
|
||||
VIEWBOX_WIDTH,
|
||||
} from "./viewbox.js";
|
||||
|
||||
/** @typedef {import("./legend.js").Readout} Readout */
|
||||
/** @typedef {import("./scale.js").ChartScale} ChartScale */
|
||||
/** @typedef {import("./timeframes.js").TimeframeValue} TimeframeValue */
|
||||
/** @typedef {import("./views.js").ChartView} ChartView */
|
||||
|
||||
/**
|
||||
* @param {ChartResult} result
|
||||
* @returns {{ date: Date, value: number }[]}
|
||||
*/
|
||||
function createEntries(result) {
|
||||
/** @type {{ date: Date, value: number }[]} */
|
||||
const entries = [];
|
||||
/** @type {number | undefined} */
|
||||
let lastValue;
|
||||
|
||||
for (const [date, value] of result.dateEntries()) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) lastValue = value;
|
||||
if (lastValue !== undefined) entries.push({ date, value: lastValue });
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Chart} chart
|
||||
* @param {TimeframeValue} timeframe
|
||||
* @returns {Promise<LoadedSeries[]>}
|
||||
*/
|
||||
async function loadSeries(chart, timeframe) {
|
||||
return Promise.all(
|
||||
chart.series.map(async (item) => ({
|
||||
series: item,
|
||||
color: item.color(),
|
||||
entries: createEntries(await fetchTimeframe(item.metric(brk), timeframe)),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {Chart} chart */
|
||||
function createLoadedSeriesCache(chart) {
|
||||
/** @type {TimeframeValue | undefined} */
|
||||
let cachedTimeframe;
|
||||
/** @type {Promise<LoadedSeries[]> | undefined} */
|
||||
let cachedPromise;
|
||||
|
||||
/** @param {TimeframeValue} timeframe */
|
||||
return function getLoadedSeries(timeframe) {
|
||||
if (timeframe !== cachedTimeframe || !cachedPromise) {
|
||||
cachedTimeframe = timeframe;
|
||||
cachedPromise = loadSeries(chart, timeframe).catch((error) => {
|
||||
if (timeframe === cachedTimeframe) cachedPromise = undefined;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return cachedPromise;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ChartView} view
|
||||
* @param {SVGGElement} group
|
||||
* @param {LoadedSeries[]} loadedSeries
|
||||
* @param {number} height
|
||||
* @param {SeriesHighlight} highlight
|
||||
* @param {ChartScale} scale
|
||||
*/
|
||||
function renderPlot(view, group, loadedSeries, height, highlight, scale) {
|
||||
switch (view) {
|
||||
case "line":
|
||||
return renderLinePlot(group, loadedSeries, height, highlight, scale);
|
||||
case "bar":
|
||||
case "bar-reversed":
|
||||
return renderBarPlot(
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
{ reversed: view === "bar-reversed" },
|
||||
scale,
|
||||
);
|
||||
case "dots":
|
||||
return renderDotsPlot(group, loadedSeries, height, highlight, scale);
|
||||
default:
|
||||
return renderStackedPlot(
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
{ reversed: view === "stacked-reversed" },
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGSVGElement} svg
|
||||
* @param {Readout} readout
|
||||
* @param {HTMLElement[]} items
|
||||
* @param {HTMLElement} status
|
||||
* @param {Chart} chart
|
||||
* @param {() => ChartView} getView
|
||||
* @param {() => ChartScale} getScale
|
||||
* @param {() => TimeframeValue} getTimeframe
|
||||
*/
|
||||
function createChartRenderer(
|
||||
svg,
|
||||
readout,
|
||||
items,
|
||||
status,
|
||||
chart,
|
||||
getView,
|
||||
getScale,
|
||||
getTimeframe,
|
||||
) {
|
||||
const group = createSvgElement("g");
|
||||
const highlight = createSeriesHighlight(items);
|
||||
const getLoadedSeries = createLoadedSeriesCache(chart);
|
||||
/** @type {LoadedSeries[]} */
|
||||
let loadedSeries = [];
|
||||
/** @type {ReturnType<typeof createScrubber> | undefined} */
|
||||
let scrubber;
|
||||
const resizeObserver = new ResizeObserver(renderCurrent);
|
||||
let active = false;
|
||||
let loadId = 0;
|
||||
|
||||
svg.append(group);
|
||||
|
||||
function renderCurrent() {
|
||||
if (!active || !loadedSeries.length) return;
|
||||
|
||||
const height = getViewBoxHeight(svg);
|
||||
|
||||
svg.setAttribute("viewBox", `0 0 ${VIEWBOX_WIDTH} ${height}`);
|
||||
group.replaceChildren();
|
||||
highlight.clearNodes();
|
||||
scrubber ??= createScrubber(svg, readout, highlight);
|
||||
scrubber.setSeries(
|
||||
renderPlot(getView(), group, loadedSeries, height, highlight, getScale()),
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadCurrent() {
|
||||
const id = (loadId += 1);
|
||||
svg.setAttribute("aria-busy", "true");
|
||||
|
||||
try {
|
||||
const nextSeries = await getLoadedSeries(getTimeframe());
|
||||
|
||||
if (id !== loadId || !active) return;
|
||||
|
||||
loadedSeries = nextSeries;
|
||||
renderCurrent();
|
||||
status.textContent = "";
|
||||
} catch (error) {
|
||||
if (id !== loadId) return;
|
||||
console.error(error);
|
||||
status.textContent = "Chart unavailable";
|
||||
} finally {
|
||||
if (id === loadId) svg.removeAttribute("aria-busy");
|
||||
}
|
||||
}
|
||||
|
||||
function resume() {
|
||||
if (active) return;
|
||||
|
||||
active = true;
|
||||
resizeObserver.observe(svg);
|
||||
void loadCurrent();
|
||||
}
|
||||
|
||||
function suspend() {
|
||||
if (!active) return;
|
||||
|
||||
active = false;
|
||||
loadId += 1;
|
||||
resizeObserver.disconnect();
|
||||
group.replaceChildren();
|
||||
highlight.clearNodes();
|
||||
scrubber?.clear();
|
||||
svg.removeAttribute("aria-busy");
|
||||
}
|
||||
|
||||
return {
|
||||
loadCurrent,
|
||||
renderCurrent,
|
||||
resume,
|
||||
suspend,
|
||||
};
|
||||
}
|
||||
import { FALLBACK_VIEWBOX_HEIGHT, VIEWBOX_WIDTH } from "./viewbox.js";
|
||||
|
||||
/** @param {Chart} chart */
|
||||
export function createChart(chart) {
|
||||
@@ -251,16 +48,16 @@ export function createChart(chart) {
|
||||
status.setAttribute("aria-live", "polite");
|
||||
status.setAttribute("role", "status");
|
||||
|
||||
const renderer = createChartRenderer(
|
||||
const renderer = createChartRenderer({
|
||||
svg,
|
||||
readout,
|
||||
items,
|
||||
status,
|
||||
chart,
|
||||
() => currentView,
|
||||
() => currentScale,
|
||||
() => currentTimeframe,
|
||||
);
|
||||
getView: () => currentView,
|
||||
getScale: () => currentScale,
|
||||
getTimeframe: () => currentTimeframe,
|
||||
});
|
||||
const viewControl = createViewControl(currentView, (view) => {
|
||||
currentView = view;
|
||||
saveView(chartKey, view);
|
||||
@@ -305,7 +102,7 @@ export function createChart(chart) {
|
||||
* @property {string} label
|
||||
* @property {() => string} color
|
||||
* @property {"line"} [role]
|
||||
* @property {(client: typeof brk) => import("./timeframes.js").TimeframeMetric} metric
|
||||
* @property {(client: typeof import("../../utils/client.js").brk) => import("./timeframes.js").TimeframeMetric} metric
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -319,5 +116,3 @@ export function createChart(chart) {
|
||||
* @property {string} color
|
||||
* @property {{ date: Date, value: number }[]} entries
|
||||
*/
|
||||
|
||||
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { brk } from "../../utils/client.js";
|
||||
import { fetchTimeframe } from "./timeframes.js";
|
||||
|
||||
/**
|
||||
* @param {ChartResult} result
|
||||
* @returns {{ date: Date, value: number }[]}
|
||||
*/
|
||||
function createEntries(result) {
|
||||
/** @type {{ date: Date, value: number }[]} */
|
||||
const entries = [];
|
||||
/** @type {number | undefined} */
|
||||
let lastValue;
|
||||
|
||||
for (const [date, value] of result.dateEntries()) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) lastValue = value;
|
||||
if (lastValue !== undefined) entries.push({ date, value: lastValue });
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Chart} chart
|
||||
* @param {TimeframeValue} timeframe
|
||||
*/
|
||||
function loadSeries(chart, timeframe) {
|
||||
return Promise.all(
|
||||
chart.series.map(async (item) => ({
|
||||
series: item,
|
||||
color: item.color(),
|
||||
entries: createEntries(await fetchTimeframe(item.metric(brk), timeframe)),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {Chart} chart */
|
||||
export function createSeriesLoader(chart) {
|
||||
/** @type {TimeframeValue | undefined} */
|
||||
let cachedTimeframe;
|
||||
/** @type {Promise<LoadedSeries[]> | undefined} */
|
||||
let cachedPromise;
|
||||
|
||||
/** @param {TimeframeValue} timeframe */
|
||||
return function loadTimeframe(timeframe) {
|
||||
if (timeframe !== cachedTimeframe || !cachedPromise) {
|
||||
cachedTimeframe = timeframe;
|
||||
cachedPromise = loadSeries(chart, timeframe).catch((error) => {
|
||||
if (timeframe === cachedTimeframe) cachedPromise = undefined;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return cachedPromise;
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef {import("./index.js").Chart} Chart */
|
||||
/** @typedef {import("./index.js").ChartResult} ChartResult */
|
||||
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
|
||||
/** @typedef {import("./timeframes.js").TimeframeValue} TimeframeValue */
|
||||
@@ -0,0 +1,159 @@
|
||||
import { renderBarPlot } from "./bar/index.js";
|
||||
import { createSeriesHighlight } from "./highlight.js";
|
||||
import { createSeriesLoader } from "./loader.js";
|
||||
import { renderLinePlot } from "./line/index.js";
|
||||
import { createScrubber } from "./scrubber.js";
|
||||
import { renderDotsPlot } from "./dots/index.js";
|
||||
import { createSvgElement } from "./svg.js";
|
||||
import { renderStackedPlot } from "./stacked/index.js";
|
||||
import { getViewBoxHeight, VIEWBOX_WIDTH } from "./viewbox.js";
|
||||
|
||||
/**
|
||||
* @param {ChartView} view
|
||||
* @param {SVGGElement} group
|
||||
* @param {LoadedSeries[]} loadedSeries
|
||||
* @param {number} height
|
||||
* @param {SeriesHighlight} highlight
|
||||
* @param {ChartScale} scale
|
||||
*/
|
||||
function renderPlot(view, group, loadedSeries, height, highlight, scale) {
|
||||
switch (view) {
|
||||
case "line":
|
||||
return renderLinePlot(group, loadedSeries, height, highlight, scale);
|
||||
case "bar":
|
||||
case "bar-reversed":
|
||||
return renderBarPlot(
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
{ reversed: view === "bar-reversed" },
|
||||
scale,
|
||||
);
|
||||
case "dots":
|
||||
return renderDotsPlot(group, loadedSeries, height, highlight, scale);
|
||||
default:
|
||||
return renderStackedPlot(
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
{ reversed: view === "stacked-reversed" },
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {SVGSVGElement} args.svg
|
||||
* @param {Readout} args.readout
|
||||
* @param {HTMLElement[]} args.items
|
||||
* @param {HTMLElement} args.status
|
||||
* @param {Chart} args.chart
|
||||
* @param {() => ChartView} args.getView
|
||||
* @param {() => ChartScale} args.getScale
|
||||
* @param {() => TimeframeValue} args.getTimeframe
|
||||
*/
|
||||
export function createChartRenderer({
|
||||
svg,
|
||||
readout,
|
||||
items,
|
||||
status,
|
||||
chart,
|
||||
getView,
|
||||
getScale,
|
||||
getTimeframe,
|
||||
}) {
|
||||
const group = createSvgElement("g");
|
||||
const highlight = createSeriesHighlight(items);
|
||||
const loadSeries = createSeriesLoader(chart);
|
||||
/** @type {LoadedSeries[]} */
|
||||
let loadedSeries = [];
|
||||
/** @type {ReturnType<typeof createScrubber> | undefined} */
|
||||
let scrubber;
|
||||
const resizeObserver = new ResizeObserver(renderCurrent);
|
||||
let active = false;
|
||||
let loadId = 0;
|
||||
|
||||
svg.append(group);
|
||||
|
||||
function renderCurrent() {
|
||||
if (!active || !loadedSeries.length) return;
|
||||
|
||||
const height = getViewBoxHeight(svg);
|
||||
|
||||
svg.setAttribute("viewBox", `0 0 ${VIEWBOX_WIDTH} ${height}`);
|
||||
group.replaceChildren();
|
||||
highlight.clearNodes();
|
||||
scrubber ??= createScrubber(svg, readout, highlight);
|
||||
scrubber.setSeries(
|
||||
renderPlot(
|
||||
getView(),
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
getScale(),
|
||||
),
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadCurrent() {
|
||||
const id = (loadId += 1);
|
||||
svg.setAttribute("aria-busy", "true");
|
||||
|
||||
try {
|
||||
const nextSeries = await loadSeries(getTimeframe());
|
||||
|
||||
if (id !== loadId || !active) return;
|
||||
|
||||
loadedSeries = nextSeries;
|
||||
renderCurrent();
|
||||
status.textContent = "";
|
||||
} catch (error) {
|
||||
if (id !== loadId) return;
|
||||
console.error(error);
|
||||
status.textContent = "Chart unavailable";
|
||||
} finally {
|
||||
if (id === loadId) svg.removeAttribute("aria-busy");
|
||||
}
|
||||
}
|
||||
|
||||
function resume() {
|
||||
if (active) return;
|
||||
|
||||
active = true;
|
||||
resizeObserver.observe(svg);
|
||||
void loadCurrent();
|
||||
}
|
||||
|
||||
function suspend() {
|
||||
if (!active) return;
|
||||
|
||||
active = false;
|
||||
loadedSeries = [];
|
||||
loadId += 1;
|
||||
resizeObserver.disconnect();
|
||||
group.replaceChildren();
|
||||
highlight.clearNodes();
|
||||
scrubber?.clear();
|
||||
svg.removeAttribute("aria-busy");
|
||||
}
|
||||
|
||||
return {
|
||||
loadCurrent,
|
||||
renderCurrent,
|
||||
resume,
|
||||
suspend,
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef {import("./index.js").Chart} Chart */
|
||||
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
|
||||
/** @typedef {import("./legend.js").Readout} Readout */
|
||||
/** @typedef {import("./scale.js").ChartScale} ChartScale */
|
||||
/** @typedef {import("./timeframes.js").TimeframeValue} TimeframeValue */
|
||||
/** @typedef {import("./views.js").ChartView} ChartView */
|
||||
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
|
||||
@@ -63,6 +63,8 @@ export function createScrubber(svg, readout, highlight) {
|
||||
let markers = [];
|
||||
let height = 0;
|
||||
let stepCount = 0;
|
||||
/** @type {number | undefined} */
|
||||
let previewIndex;
|
||||
|
||||
group.dataset.scrubber = "root";
|
||||
guide.dataset.scrubber = "guide";
|
||||
@@ -108,11 +110,28 @@ export function createScrubber(svg, readout, highlight) {
|
||||
function clear() {
|
||||
series = [];
|
||||
markers = [];
|
||||
clearPreview();
|
||||
group.replaceChildren(guide);
|
||||
delete svg.dataset.index;
|
||||
delete svg.dataset.scrubbing;
|
||||
}
|
||||
|
||||
/** @param {number} index */
|
||||
function preview(index) {
|
||||
if (index === previewIndex) return;
|
||||
|
||||
if (previewIndex !== undefined) highlight.clearPreview(previewIndex);
|
||||
highlight.preview(index);
|
||||
previewIndex = index;
|
||||
}
|
||||
|
||||
function clearPreview() {
|
||||
if (previewIndex === undefined) return;
|
||||
|
||||
highlight.clearPreview(previewIndex);
|
||||
previewIndex = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ScrubberSeries[]} nextSeries
|
||||
* @param {number} nextHeight
|
||||
@@ -141,14 +160,25 @@ export function createScrubber(svg, readout, highlight) {
|
||||
function updateFromPointer(event) {
|
||||
const { left, width } = svg.getBoundingClientRect();
|
||||
const x = ((event.clientX - left) / width) * VIEWBOX_WIDTH;
|
||||
const index = Number(
|
||||
/** @type {SVGElement} */ (event.target).dataset.series,
|
||||
);
|
||||
|
||||
if (Number.isInteger(index)) preview(index);
|
||||
else clearPreview();
|
||||
update(x / VIEWBOX_WIDTH);
|
||||
}
|
||||
|
||||
svg.addEventListener("pointermove", updateFromPointer);
|
||||
svg.addEventListener("pointerleave", hide);
|
||||
svg.addEventListener("pointerleave", () => {
|
||||
clearPreview();
|
||||
hide();
|
||||
});
|
||||
svg.addEventListener("focus", () => update(1));
|
||||
svg.addEventListener("blur", hide);
|
||||
svg.addEventListener("blur", () => {
|
||||
clearPreview();
|
||||
hide();
|
||||
});
|
||||
svg.addEventListener("keydown", (event) => {
|
||||
const current = Number(svg.dataset.index || stepCount);
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ main.learn {
|
||||
text-transform: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:is(:hover, :focus-visible, [data-active]) {
|
||||
&:is(:hover, :focus-visible, [data-active], [data-preview]) {
|
||||
color: var(--black);
|
||||
background: var(--color);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createSeries } from "./charts/config.js";
|
||||
import { colors } from "../utils/colors.js";
|
||||
|
||||
const palette = [
|
||||
@@ -29,13 +28,11 @@ function colorAt(index) {
|
||||
|
||||
/** @param {readonly { label: string, color?: ChartColor, metric: Metric }[]} items */
|
||||
export function createCohortSeries(items) {
|
||||
return createSeries(
|
||||
items.map(({ label, color, metric }, index) => ({
|
||||
label,
|
||||
color: color ?? colorAt(index),
|
||||
metric,
|
||||
})),
|
||||
);
|
||||
return items.map(({ label, color, metric }, index) => ({
|
||||
label,
|
||||
color: color ?? colorAt(index),
|
||||
metric,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+6
-10
@@ -3,17 +3,13 @@ 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", createPage: createExplorePage },
|
||||
{ pathname: "/learn", createPage: createLearnPage },
|
||||
{ pathname: "/build", createPage: createBuildPage },
|
||||
];
|
||||
|
||||
/** @type {Record<string, () => HTMLElement>} */
|
||||
const routes = Object.fromEntries(
|
||||
pages.map(({ pathname, createPage }) => [pathname, createPage]),
|
||||
);
|
||||
const routes = {
|
||||
"/": createHomePage,
|
||||
"/explore": createExplorePage,
|
||||
"/learn": createLearnPage,
|
||||
"/build": createBuildPage,
|
||||
};
|
||||
|
||||
/** @param {string} pathname */
|
||||
export function isRoute(pathname) {
|
||||
|
||||
Reference in New Issue
Block a user