mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Add live-preview busy indicator for slow renders (M6, §04/§10)
This commit is contained in:
@@ -110,11 +110,14 @@ export function LivePreview() {
|
||||
const lastLoadRef = useRef({ bufferEpoch: -1, editorView });
|
||||
const error = usePreviewStore((s) => s.error);
|
||||
const setError = usePreviewStore((s) => s.setError);
|
||||
const busy = usePreviewStore((s) => s.busy);
|
||||
const setBusy = usePreviewStore((s) => s.setBusy);
|
||||
|
||||
// Busy-indication timer ref: if a render exceeds ~1s we surface a non-blocking
|
||||
// overlay (arch §10.2 NN/g: >1s owes a busy indication; <1s shows nothing to
|
||||
// avoid flicker on typical fast renders).
|
||||
const busyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// TODO (council backfill, doc §10.2): a render that exceeds ~1s owes a
|
||||
// non-blocking busy indication (overlay + aria-busy), gated by a threshold so
|
||||
// sub-1s renders show nothing. Deferred — it pairs with the heavier M3 dataset
|
||||
// renders the budget flags; today's inline-data renders are effectively instant.
|
||||
useEffect(() => {
|
||||
const node = hostRef.current;
|
||||
if (!node) return;
|
||||
@@ -147,6 +150,25 @@ export function LivePreview() {
|
||||
}
|
||||
|
||||
const mine = ++generationRef.current;
|
||||
|
||||
// Arm the busy indicator: if the render hasn't settled within ~1s, flip the
|
||||
// flag. Sub-1s renders (the common case) never show the overlay — no flicker
|
||||
// (arch §10.2; NN/g ≤1s = uninterrupted thought, >1s = noticeably waiting).
|
||||
if (busyTimerRef.current !== null) clearTimeout(busyTimerRef.current);
|
||||
busyTimerRef.current = setTimeout(() => {
|
||||
// Only arm for the current generation; a superseded render doesn't own busy.
|
||||
if (mine === generationRef.current) setBusy(true);
|
||||
}, 1000);
|
||||
|
||||
/** Clear busy and the timer unconditionally — called on settle or error. */
|
||||
const clearBusy = () => {
|
||||
if (busyTimerRef.current !== null) {
|
||||
clearTimeout(busyTimerRef.current);
|
||||
busyTimerRef.current = null;
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
try {
|
||||
const prepared = prepareSpecForRender(parsed, { fitMode, datasets });
|
||||
const config = chartConfigFor(uiTheme);
|
||||
@@ -165,8 +187,10 @@ export function LivePreview() {
|
||||
}
|
||||
handleRef.current = handle;
|
||||
setError(null);
|
||||
clearBusy();
|
||||
} catch (e) {
|
||||
if (mine === generationRef.current) {
|
||||
clearBusy();
|
||||
// A missing dataset reference is not a JSON/spec problem, so it gets a
|
||||
// tailored, fixable message instead of the generic syntax hint (council:
|
||||
// GOV.UK error-message + NN/g #9 — name the problem, give the real fix).
|
||||
@@ -187,7 +211,17 @@ export function LivePreview() {
|
||||
}, delay);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [shownText, fitMode, uiTheme, datasets, setError, bufferEpoch, editorView, renderDebounce]);
|
||||
}, [
|
||||
shownText,
|
||||
fitMode,
|
||||
uiTheme,
|
||||
datasets,
|
||||
setError,
|
||||
setBusy,
|
||||
bufferEpoch,
|
||||
editorView,
|
||||
renderDebounce,
|
||||
]);
|
||||
|
||||
// Re-fit the chart when its container resizes (e.g. a pane drag). Vega doesn't
|
||||
// observe the element, so we do: one observer on the stable host node for the
|
||||
@@ -205,13 +239,15 @@ export function LivePreview() {
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
// Finalize the live view on unmount, and clear the shared error so a stale
|
||||
// message never outlives this pane.
|
||||
// Finalize the live view on unmount, and clear the shared error + busy state so
|
||||
// stale transient state never outlives this pane.
|
||||
useEffect(
|
||||
() => () => {
|
||||
handleRef.current?.destroy();
|
||||
handleRef.current = null;
|
||||
if (busyTimerRef.current !== null) clearTimeout(busyTimerRef.current);
|
||||
usePreviewStore.getState().setError(null);
|
||||
usePreviewStore.getState().setBusy(false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -222,7 +258,12 @@ export function LivePreview() {
|
||||
<FitControl />
|
||||
<PreviewSettings />
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{/*
|
||||
* aria-busy on the chart region tells AT the area is being updated (arch §10.2;
|
||||
* spec §04). The overlay is a non-blocking sibling inside the relative-positioned
|
||||
* body; it never covers the header or editor, so editing stays fully interactive.
|
||||
*/}
|
||||
<div className={styles.body} aria-busy={busy || undefined}>
|
||||
{/* Frame is React-owned and carries the fit-sizing class; the inner host
|
||||
is owned by vega-embed (it brands it `.vega-embed` and mutates its
|
||||
classList at runtime), so its className stays static and React never
|
||||
@@ -234,6 +275,18 @@ export function LivePreview() {
|
||||
editor pane's role="alert" (one producer, two subscribers; doc §10.1),
|
||||
so adding one here would double-announce it. */}
|
||||
{error !== null && <pre className={styles.error}>{error}</pre>}
|
||||
{/*
|
||||
* Busy overlay: non-blocking, overlays only the chart body, never the header
|
||||
* or the editor (arch §10.2; spec §04/§10). Shown only after the ~1s threshold
|
||||
* so sub-1s renders produce no flicker. The spinner animation is suppressed
|
||||
* under prefers-reduced-motion (base.css *{animation-duration:0.01ms}).
|
||||
*/}
|
||||
{busy && (
|
||||
<div className={styles.busyOverlay} aria-hidden="true">
|
||||
<span className={styles.busySpinner} />
|
||||
<span className={styles.busyLabel}>Rendering…</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user