feat(ferratas): keep loaded via ferratas in a tile cache on server and device, group segments, own look and count badge

This commit is contained in:
Maurice
2026-10-14 16:21:30 +02:00
parent a9882a6415
commit 160bc89e78
126 changed files with 5003 additions and 1153 deletions
@@ -1,38 +1,26 @@
import { useEffect, useState } from 'react';
import { ferrataPaint } from './ferrataPaint';
import { type FerrataPaint, ferrataPaint, sameFerrataPaint } from './ferrataPaint';
/** The attributes `applyAppearance()` writes on `<html>` that change what the status tokens resolve to. */
/** The attributes `applyAppearance()` writes on `<html>` that change what the tokens resolve to. */
const THEME_ATTRIBUTES = ['class', 'style', 'data-scheme'];
type Paint = ReturnType<typeof ferrataPaint>;
function samePaint(a: Paint, b: Paint): boolean {
return (
a.casing === b.casing &&
a.tone.easy === b.tone.easy &&
a.tone.medium === b.tone.medium &&
a.tone.hard === b.tone.hard &&
a.tone.unknown === b.tone.unknown
);
}
/**
* The line colours, resolved again whenever the light/dark class, the colour
* scheme or a custom accent on `<html>` changes. Map paint holds resolved
* strings rather than `var(--token)`, so without this a theme toggle would leave
* the lines in the old scheme's colours until the next refetch. Same observer
* approach as `useIsDark`, which only watches the class. The reference only
* changes when a colour does, so a memo keyed on it does not churn.
* the lines in the old scheme's colours. Same observer approach as `useIsDark`,
* which only watches the class. The reference only changes when a colour does,
* so a memo keyed on it does not churn.
*/
export function useFerrataPaint(): Paint {
export function useFerrataPaint(): FerrataPaint {
const [paint, setPaint] = useState(ferrataPaint);
useEffect(() => {
if (typeof document === 'undefined') return;
const observer = new MutationObserver(() => {
const next = ferrataPaint();
setPaint((current) => (samePaint(current, next) ? current : next));
setPaint((current) => (sameFerrataPaint(current, next) ? current : next));
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: THEME_ATTRIBUTES });
return () => observer.disconnect();