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
|
- **Columns** — `split_by_depth`, driven by the `Columns` buttons via
|
||||||
`applySplitDepth()`
|
`applySplitDepth()`
|
||||||
|
|
||||||
Both sets are drawn by `components/DepthButtons.jsx` from the axis's own field
|
Both axes are drawn by `components/DepthButtons.jsx` from the axis's own field
|
||||||
list (`groupFull` / `splitFull`), so each button is named after the level it
|
list (`groupFull` / `splitFull`), as a **stepper**: `◂ partgroup ▸`, with the
|
||||||
reveals. The row axis used to be a fixed `0 1 2 3`, which named nothing and
|
middle button opening the full level list for a jump rather than a step.
|
||||||
offered levels a two-field `group_by` does not have.
|
|
||||||
|
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
|
Both are **1-based**: they count the levels to show, where the imperative
|
||||||
`view.set_depth()` counts the boundary below them. `server.cpp` does
|
`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
|
import { useEffect, useRef, useState } from 'react'
|
||||||
// ViewConfig, counting the levels to show — so they are drawn by one component
|
|
||||||
// rather than two that happen to look alike.
|
// One axis's collapse control, as a stepper.
|
||||||
//
|
//
|
||||||
// The buttons are named after the level they reveal. The row axis used to be a
|
// Rows and columns are the same thing — a depth in ViewConfig, counting the
|
||||||
// fixed 0 1 2 3: a number means nothing without already knowing what group_by
|
// levels to show — so one component draws both.
|
||||||
// holds, and a fixed range offers levels that do not exist while hiding ones
|
//
|
||||||
// that do.
|
// 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' }) {
|
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 (
|
return (
|
||||||
<div className="flex items-center gap-1.5">
|
<div ref={boxRef} className="relative flex items-center gap-1">
|
||||||
<span className="pf-section-label">{label}</span>
|
<span className="pf-section-label mr-0.5">{label}</span>
|
||||||
{Array.from({ length: levels.length + 1 }, (_, n) => (
|
|
||||||
<button key={n} onClick={() => onPick(n)}
|
<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
|
title={n === 0
|
||||||
? `Collapse ${label.toLowerCase()} to a single total`
|
? `Collapse ${label.toLowerCase()} to a single total`
|
||||||
: `Show ${label.toLowerCase()} down to ${levels.slice(0, n).join(' › ')}`}
|
: `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
|
className={`block w-full text-left px-3 py-1 truncate
|
||||||
${depth === n ? 'border-blue-300 text-blue-600 bg-blue-50'
|
${depth === n ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'}`}>
|
||||||
: 'border-gray-200 text-gray-500 hover:border-gray-400'}`}>
|
|
||||||
{n === 0 ? totalLabel : levels[n - 1]}
|
{n === 0 ? totalLabel : levels[n - 1]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user