File size: 2,732 Bytes
33962ff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | "use client";
import { useEffect, useState } from "react";
import {getJSON, type Stats} from "@/lib/api";
import { DIMS, type View } from "@/lib/api";
import { VIEW_INFO, VIEW_ORDER } from "@/lib/views";
export function Segmented<T extends string | number>({
value,
options,
onChange,
render,
label,
ariaLabel,
}: {
value: T;
options: readonly T[];
onChange: (v: T) => void;
render?: (v: T) => React.ReactNode;
label: string;
ariaLabel?: string;
}) {
return (
<div className="flex items-center gap-2">
{label && <span className="text-xs text-muted">{label}</span>}
<div role="radiogroup" aria-label={ariaLabel ?? label} className="flex flex-wrap rounded-lg border border-line bg-surface p-0.5">
{options.map((o) => (
<button
key={String(o)}
role="radio"
aria-checked={o === value}
onClick={() => onChange(o)}
className={`rounded-md px-2.5 py-1 text-sm transition-colors ${
o === value ? "bg-surface-2 text-ink" : "text-ink-2 hover:text-ink"
}`}
>
{render ? render(o) : String(o)}
</button>
))}
</div>
</div>
);
}
export function ViewSwatch({ view }: { view: View }) {
return <span className="inline-block h-2 w-2 rounded-full" style={{ background: VIEW_INFO[view].color }} />;
}
export function ViewPicker({ value, onChange, views = VIEW_ORDER }: { value: View; onChange: (v: View) => void; views?: View[] }) {
const [available,setAvailable]=useState<View[]>(views);
useEffect(()=>{void getJSON<Stats>("/api/stats").then(s=>{setAvailable(s.views);if(s.views.length&&!s.views.includes(value))onChange(s.views[0]);}).catch(()=>{});
// Initial capabilities are a server snapshot; manual selection remains local.
// eslint-disable-next-line react-hooks/exhaustive-deps
},[]);
return (
<Segmented
label="View"
value={value}
options={available.filter(v=>views.includes(v))}
onChange={onChange}
render={(v) => (
<span className="flex items-center gap-1.5" title={VIEW_INFO[v].blurb}>
<ViewSwatch view={v} />
{VIEW_INFO[v].label}
</span>
)}
/>
);
}
export function DimPicker({ value, onChange }: { value: number; onChange: (d: number) => void }) {
const [dims,setDims]=useState<readonly number[]>(DIMS);
useEffect(()=>{void getJSON<Stats>("/api/stats").then(s=>{setDims(s.dims);if(s.dims.length&&!s.dims.includes(value))onChange(s.dims[0]);}).catch(()=>{});
// eslint-disable-next-line react-hooks/exhaustive-deps
},[]);
return <Segmented label="Dims" value={value} options={dims} onChange={onChange} render={(d) => `${d}d`} />;
}
|