Files
astrolabe/src/app/components/PaneSplitHandle.tsx
T
oleh efb5a9bbe0 Data inspector: input/resolved rows below the chart, with a resizable divider
A collapsible Data panel under the Live Preview and Chart Builder charts shows the rows the chart actually uses, switching between Input (parsed source) and Resolved (post-transform) views read from the live Vega view. Collapsed by default; the open-state and a draggable height divider persist.

Rows come through a new RenderHandle.inspectData() accessor, so no component touches the view: core/result-data picks the most-upstream source and most-downstream result from the compiled dataflow, read lazily. The divider reuses the window-splitter pattern (horizontal variant).

Consolidations: a shared DataTable primitive replaces the inspector's and the builder's duplicate read-only tables; useResizeDrag merges the col/row drag-gesture twins.

Docs: spec 04/06 and arch 05/10 updated; the now-shipped exploration memo removed.
2026-06-18 02:22:03 +03:00

110 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Vertical drag handle between the library and preview when the editor is hidden
* (spec §01A). With no editor between them to absorb the change, the two side
* panes share the span: dragging re-splits it — one grows, the other shrinks
* (zero-sum) — each kept above its minimum by the pure `splitLibraryWidth`.
*
* The split is read from the live DOM: the two flanking panes (this handle's
* previous/next siblings) render proportionally (`flex: width 1 0`), so writing
* their measured widths back makes the boundary track the pointer 1:1, at any
* window size. Re-showing the editor later keeps whatever ratio was left here
* (PanesStore → `shownSideWidths`).
*
* Accessibility mirrors the editor-flanking ResizeHandle (WAI-ARIA APG → Window
* Splitter): a focusable `separator` reporting the library's 0100 position via
* `aria-valuenow`, driven by ←/→ to nudge and Home/End to jump to min/max. It
* reuses ResizeHandle's stylesheet so the two handles look identical.
*/
import { useRef } from 'react';
import { useResizeDrag } from '../hooks/useResizeDrag';
import { splitLibraryWidth, splitValue, usePanesStore } from '../stores/PanesStore';
import styles from './ResizeHandle.module.css';
/** Keyboard nudge step (px) per arrow press — matches ResizeHandle. */
const KEY_STEP = 16;
interface PaneSplitHandleProps {
/** Accessible label, e.g. "Resize library and preview". */
label: string;
}
export function PaneSplitHandle({ label }: PaneSplitHandleProps) {
const ref = useRef<HTMLDivElement>(null);
// The reported position recomputes from the stored split as it changes. It reads
// the *stored* ratio (container-independent, per the file header), so unlike
// ResizeHandle aria-valuenow can drift from the rendered position after a window
// enlargement (worst near the extremes) — an accepted simplicity trade-off, the
// contract in arch/10 (splitter). Drag/keyboard read live clientWidth below, so
// resizing itself stays accurate; only the announced value drifts.
const libraryWidth = usePanesStore((s) => s.libraryWidth);
const previewWidth = usePanesStore((s) => s.previewWidth);
const valueNow = splitValue(libraryWidth, previewWidth);
/** The panes this handle sits between: library before it, preview after it. */
const flanks = () => ({
lib: ref.current?.previousElementSibling as HTMLElement | null,
prev: ref.current?.nextElementSibling as HTMLElement | null,
});
/** Apply a desired (rendered) library width, clamped against the live span. */
const applySplit = (desiredLibrary: number) => {
const { lib, prev } = flanks();
if (!lib || !prev) return;
const avail = lib.clientWidth + prev.clientWidth;
const library = splitLibraryWidth(desiredLibrary, avail);
usePanesStore.getState().setSplit(library, avail - library);
};
const onPointerDown = useResizeDrag(
'x',
() => flanks().lib?.clientWidth ?? 0,
(startLib, delta) => applySplit(startLib + delta),
);
// Keyboard per WAI-ARIA APG → Window Splitter: arrows nudge the library side;
// Home/End jump to the library's smallest/largest allowed share of the span.
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const lib = flanks().lib?.clientWidth ?? 0;
switch (e.key) {
case 'ArrowLeft':
applySplit(lib - KEY_STEP);
break;
case 'ArrowRight':
applySplit(lib + KEY_STEP);
break;
case 'Home': // library at its minimum
applySplit(0);
break;
case 'End': // library at its maximum (preview at its minimum)
applySplit(Number.MAX_SAFE_INTEGER);
break;
default:
return; // not ours — let it bubble
}
e.preventDefault();
};
return (
<div
ref={ref}
className={styles.handle}
role="separator"
aria-orientation="vertical"
aria-label={label}
// The split governs both panes it sits between.
aria-controls="pane-library pane-preview"
aria-valuenow={valueNow ?? undefined}
aria-valuemin={valueNow === null ? undefined : 0}
aria-valuemax={valueNow === null ? undefined : 100}
aria-valuetext={valueNow === null ? undefined : `${valueNow}%`}
tabIndex={0}
onPointerDown={onPointerDown}
onKeyDown={onKeyDown}
>
<span className={styles.grip} aria-hidden="true" />
</div>
);
}