Step through pivot depth instead of listing it
One button per level grows with the pivot: the saved layouts here run 3-5 group_by and 2-3 split_by, so "Customer by Program" rendered ten depth buttons, and with the layout menu, Save and Fit that is thirteen controls in the view zone. The active level was a blue button among the greys, so reading the current state meant scanning for it. A stepper is three controls whatever the depth and names where you are. The full list is still there behind the middle button, for jumping rather than stepping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05631817ae
commit
2fdf113636
16
CLAUDE.md
16
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
|
||||
|
||||
@ -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 (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="pf-section-label">{label}</span>
|
||||
{Array.from({ length: levels.length + 1 }, (_, n) => (
|
||||
<button key={n} onClick={() => onPick(n)}
|
||||
<div ref={boxRef} className="relative flex items-center gap-1">
|
||||
<span className="pf-section-label mr-0.5">{label}</span>
|
||||
|
||||
<button className={arrow} disabled={!known || depth <= 0}
|
||||
title={`Collapse ${label.toLowerCase()} by one level`}
|
||||
onClick={() => step((depth ?? 0) - 1)}>◂</button>
|
||||
|
||||
<button onClick={() => setOpen(o => !o)}
|
||||
title={`${label}: showing ${current}`}
|
||||
className={`border rounded px-2 py-0.5 max-w-[10rem] truncate transition-colors
|
||||
${open ? 'border-blue-300 text-blue-700 bg-blue-50'
|
||||
: 'border-gray-200 text-gray-600 hover:border-gray-400'}`}>
|
||||
{current}
|
||||
</button>
|
||||
|
||||
<button className={arrow} disabled={!known || depth >= max}
|
||||
title={`Expand ${label.toLowerCase()} by one level`}
|
||||
onClick={() => step((depth ?? 0) + 1)}>▸</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 min-w-[10rem] bg-white border
|
||||
border-gray-200 rounded shadow-lg py-1 text-xs">
|
||||
{Array.from({ length: max + 1 }, (_, n) => (
|
||||
<button key={n} onClick={() => { onPick(n); setOpen(false) }}
|
||||
title={n === 0
|
||||
? `Collapse ${label.toLowerCase()} to a single total`
|
||||
: `Show ${label.toLowerCase()} down to ${levels.slice(0, n).join(' › ')}`}
|
||||
className={`border rounded px-1.5 py-0.5 transition-colors max-w-[9rem] truncate
|
||||
${depth === n ? 'border-blue-300 text-blue-600 bg-blue-50'
|
||||
: 'border-gray-200 text-gray-500 hover:border-gray-400'}`}>
|
||||
className={`block w-full text-left px-3 py-1 truncate
|
||||
${depth === n ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'}`}>
|
||||
{n === 0 ? totalLabel : levels[n - 1]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user