chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useRef, useState, useSyncExternalStore } from 'react';
import { percent } from '#/lib/languages.js';
import { hasHue, onTheme } from '#/lib/theme.js';
/** Ticks across the whole scale. Each one is a share of the repository. */
const TICKS = 72;
/**
* How solid each language's ticks read, largest first. The gauge has no hue
* to spend, so rank is what tells one run from the next: the language a
* repository is mostly written in stands at full strength and each one after
* it steps back. Past the last step every remaining language reads alike,
* which by then is a tail too thin to tell apart anyway.
*/
const RANKS = [1, 0.78, 0.6, 0.46, 0.36, 0.28, 0.22];
const rank = (index) => RANKS[Math.min(index, RANKS.length - 1)];
/**
* What a repository is written in, as a row of ticks read like a gauge: each
* language holds the run of ticks its share earns, and the one under the
* pointer names itself in a chip below.
*
* A language too small for a whole tick still gets one, so a listed language
* is always visible. The chip is drawn over what follows rather than in the
* flow, so the row keeps its height and nothing moves as the pointer crosses
* it.
*
* The row answers the pointer, not the ticks: which language is under it
* comes from how far across the row it is. Ticks are 2px with air between
* them, so asking each tick would leave the gaps answering for nobody, and
* crossing one run would blink the chip off and on again.
*/
export function LanguageBar({ languages }) {
const [hovered, setHovered] = useState(null);
const row = useRef(null);
// A palette with hue to spend gives each language linguist's own colour.
// One without steps each back by rank instead, which is the only way a
// greyscale gauge can tell them apart. The palette is asked rather than
// consulted about: its own key colour is the honest account of it.
const byRank = useSyncExternalStore(
onTheme,
() => !hasHue(),
() => true,
);
// Hand out the scale, one language at a time, and let the last one take
// whatever rounding left over so the row always ends full. Each tick
// carries its own place in the row, which is what names it to React.
/** @type {{id: number, language: {name: string, color: string}, index: number}[]} */
const ticks = [];
languages.forEach((language, index) => {
const last = index === languages.length - 1;
const count = last
? TICKS - ticks.length
: Math.max(1, Math.round((language.share / 100) * TICKS));
for (let tick = 0; tick < count; tick++) {
ticks.push({ id: ticks.length, language, index });
}
});
const words = languages
.map((language) => `${language.name} ${percent(language.share)}`)
.join(', ');
/** Which language the pointer stands over, by how far across the row it is. */
const track = (event) => {
const box = row.current?.getBoundingClientRect();
if (!box || box.width === 0) return;
const across = (event.clientX - box.left) / box.width;
const at = Math.floor(across * TICKS);
setHovered(
ticks[Math.min(ticks.length - 1, Math.max(0, at))]?.index ?? null,
);
};
return (
<section aria-label="Languages" className="relative">
{/* The ticks stand a few pixels tall, which is a hard thing to keep a
pointer inside. The reach is taller than they are, and pulled back
out of the layout so nothing below it moves. */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: the row inside carries the reading; this only widens the reach for a pointer */}
<div
className="-my-2 py-2"
onMouseMove={track}
onMouseLeave={() => setHovered(null)}
>
{/* The ticks keep their own width and spread across whatever room the
row has, which is what makes the scale read as an instrument
rather than as a filled bar. */}
<div
ref={row}
role="img"
aria-label={words}
className="flex h-3 items-end justify-between"
>
{ticks.map(({ id, language, index }) => (
<span
key={id}
aria-hidden="true"
className="w-[2px] shrink-0 transition-[height]"
style={{
height: hovered === index ? '100%' : '60%',
backgroundColor: byRank ? 'var(--foreground)' : language.color,
opacity:
hovered === null || hovered === index
? byRank
? rank(index)
: 1
: byRank
? rank(index) * 0.3
: 0.3,
}}
/>
))}
</div>
</div>
{hovered !== null && (
<span className="pointer-events-none absolute top-4 left-0 z-10 flex items-center gap-1.5 border border-border bg-card px-2 py-1 text-[12px] shadow-sm">
<span
className="size-2 shrink-0"
style={{
backgroundColor: byRank
? 'var(--foreground)'
: languages[hovered].color,
opacity: byRank ? rank(hovered) : 1,
}}
/>
<span className="whitespace-nowrap font-semibold text-foreground">
{languages[hovered].name}
</span>
<span className="text-faint tabular-nums">
{percent(languages[hovered].share)}
</span>
</span>
)}
</section>
);
}