diff --git a/frontend/src/components/Dashboard/ui/core/controls.ts b/frontend/src/components/Dashboard/ui/core/controls.ts index f7efcb7..2cfa0cd 100644 --- a/frontend/src/components/Dashboard/ui/core/controls.ts +++ b/frontend/src/components/Dashboard/ui/core/controls.ts @@ -5,7 +5,7 @@ * either look: the state, the keyboard and every `aria-` live here, and a * renderer only decides what it looks like while doing it. */ -import { useCallback, useId, useRef, useState } from "react" +import { useCallback, useEffect, useId, useRef, useState } from "react" import { fractionOf } from "./config" @@ -141,6 +141,32 @@ export function tickIntervals(steps: number): number { return [4, 3, 2].find((count) => Number.isInteger(steps / count)) ?? 4 } +/** + * The labels that fit the width the scale was measured in. + * + * The marks are placed in percent, which says nothing about how wide a label + * is: five four-character labels crowd on a tile narrower than the four + * columns a slider is given by default. Keep every Nth instead, with N a + * divisor of the interval count so the first and last — the two that anchor + * the range — are always among those kept. + */ +export function fitMarks( + marks: T[], + width: number, +): T[] { + const intervals = marks.length - 1 + // Not measured yet, or nothing to thin. + if (width === 0 || intervals < 2) return marks + const widest = Math.max(...marks.map((mark) => mark.label.length)) + // A 0.75rem tabular digit runs about 7px, and neighbours need a gap. + const fits = Math.floor(width / (widest * 7 + 12)) + const stride = + [1, 2, 3, 4, 5].find( + (n) => intervals % n === 0 && intervals / n + 1 <= fits, + ) ?? intervals + return marks.filter((_, index) => index % stride === 0) +} + /** * A value set by dragging, published when the handle is let go. * @@ -175,6 +201,20 @@ export function useSliderDrag({ const [draft, setDraft] = useState(null) const current = draft ?? value + // How much room the scale under the track actually has, which is what says + // how many of its labels can be drawn without them running into each other. + const trackRef = useRef(null) + const [width, setWidth] = useState(0) + useEffect(() => { + const el = trackRef.current + if (!el) return + const observer = new ResizeObserver(([entry]) => + setWidth(entry.contentRect.width), + ) + observer.observe(el) + return () => observer.disconnect() + }, []) + const release = () => { if (draft === null) return onCommit(draft) @@ -187,8 +227,23 @@ export function useSliderDrag({ // without a precision setting of its own. const digits = (String(step).split(".")[1] ?? "").length + /** The scale under the track, drawn rather than declared: no browser + * renders `