diff --git a/CLAUDE.md b/CLAUDE.md index 899fb7c..98dd643 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -238,10 +238,18 @@ Both axes collapse the same way: a **depth in `ViewConfig`**, set through - **Columns** — `split_by_depth`, driven by the `Columns` buttons via `applySplitDepth()` -Both sets are drawn by `components/DepthButtons.jsx` from the axis's own field -list (`groupFull` / `splitFull`), so each button is named after the level it -reveals. The row axis used to be a fixed `0 1 2 3`, which named nothing and -offered levels a two-field `group_by` does not have. +Both axes are drawn by `components/DepthButtons.jsx` from the axis's own field +list (`groupFull` / `splitFull`), as a **stepper**: `◂ partgroup ▸`, with the +middle button opening the full level list for a jump rather than a step. + +Three controls per axis whatever the depth. It was one button per level, which +grows with the pivot — a 5-by-3 layout rendered ten of them — and the current +level was a blue button among the greys, so reading the state meant scanning. +Before that the row axis was a fixed `0 1 2 3`, which named nothing and offered +levels a two-field `group_by` does not have. + +`depth` is null until the first layout has been read back; the stepper shows an +em dash rather than guessing a level. Both are **1-based**: they count the levels to show, where the imperative `view.set_depth()` counts the boundary below them. `server.cpp` does diff --git a/ui/src/components/DepthButtons.jsx b/ui/src/components/DepthButtons.jsx index b4cb4f5..b2bdb00 100644 --- a/ui/src/components/DepthButtons.jsx +++ b/ui/src/components/DepthButtons.jsx @@ -1,26 +1,79 @@ -// One axis's collapse control. Rows and columns are the same thing — a depth in -// ViewConfig, counting the levels to show — so they are drawn by one component -// rather than two that happen to look alike. +import { useEffect, useRef, useState } from 'react' + +// One axis's collapse control, as a stepper. // -// The buttons are named after the level they reveal. The row axis used to be a -// fixed 0 1 2 3: a number means nothing without already knowing what group_by -// holds, and a fixed range offers levels that do not exist while hiding ones -// that do. +// Rows and columns are the same thing — a depth in ViewConfig, counting the +// levels to show — so one component draws both. +// +// It used to be one button per level, which is N+1 controls that grow with the +// pivot: a 5-by-3 layout rendered ten of them, and the active level was a blue +// button somewhere among the greys, so reading the current state meant scanning. +// A stepper is three controls whatever the depth, and says where you are rather +// than making you find it. The full list is still one click away, for jumping +// rather than stepping. export default function DepthButtons({ label, levels, depth, onPick, totalLabel = 'Total' }) { + const [open, setOpen] = useState(false) + const boxRef = useRef(null) + + useEffect(() => { + if (!open) return + const onDown = (e) => { if (!boxRef.current?.contains(e.target)) setOpen(false) } + const onKey = (e) => { if (e.key === 'Escape') setOpen(false) } + // Capture phase: the pivot lives in a shadow root and retargets its events, + // so a bubbled listener sees the host rather than the real target. + document.addEventListener('mousedown', onDown, true) + window.addEventListener('keydown', onKey) + return () => { + document.removeEventListener('mousedown', onDown, true) + window.removeEventListener('keydown', onKey) + } + }, [open]) + + const max = levels.length + // depth is null until the first layout has been read back. Showing the last + // level would be a guess; an em dash is honest and the arrows still work. + const known = depth != null + const current = !known ? '—' : (depth === 0 ? totalLabel : levels[depth - 1]) + const step = (d) => onPick(Math.min(max, Math.max(0, d))) + + const arrow = 'border border-gray-200 rounded px-1 py-0.5 text-gray-500 leading-none ' + + 'hover:border-gray-400 disabled:opacity-30 disabled:hover:border-gray-200 transition-colors' + return ( -
- {label} - {Array.from({ length: levels.length + 1 }, (_, n) => ( - - ))} +
+ {label} + + + + + + + + {open && ( +
+ {Array.from({ length: max + 1 }, (_, n) => ( + + ))} +
+ )}
) }