Extends layoutStore with previewSession/commitPreview, mirroring multiview.ts's preview controller: a throwaway tab for a session no pane holds, byte-for-byte restore on end, and every real layout-changing action drops an active preview first. Never schedules a save while a preview is up. Adds usePreview, wrapping the store actions with previewSchedule.ts's 120ms leading-edge throttle, plus a touch guard and an immediate `end()` for real changes. MultiView/TabBar render the preview hue on the pane titlebar, canvas ring/chip, and the throwaway tab (dashed, unclickable).
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
// React port of previewSchedule.ts + multiview.ts's preview controller
|
|
// (issue #251). Owns the throttled schedule, hands the layout store the
|
|
// throttled result, and exposes the three ways a preview ends for real —
|
|
// see docs/PREVIEW.md, the contract this mirrors.
|
|
|
|
import { useCallback, useEffect, useRef } from "react";
|
|
import { useStore } from "zustand";
|
|
|
|
import { createPreviewSchedule, type PreviewScheduleDeps } from "../previewSchedule.js";
|
|
import type { LayoutStore, LayoutStoreApi } from "../stores/layoutStore.js";
|
|
import type { Uuid } from "../types.js";
|
|
|
|
export interface UsePreviewOptions {
|
|
store: LayoutStoreApi;
|
|
/** Test seam for the schedule's clock/timers. */
|
|
scheduleDeps?: PreviewScheduleDeps;
|
|
}
|
|
|
|
export interface UsePreviewResult {
|
|
/** The session currently shown as a preview, or null. */
|
|
previewedSessionId: Uuid | null;
|
|
/** Throttled: show `sessionId` in the content pane, or `null` to put back
|
|
* whatever was on screen (a highlight move, or the highlight leaving
|
|
* every session). Never delayed on the leading edge. */
|
|
preview(sessionId: Uuid | null): void;
|
|
/** Pointer-hover entry point: touch has no hover, so a touch pointer never
|
|
* previews (docs/PREVIEW.md). */
|
|
previewFromPointer(sessionId: Uuid | null, pointerType: string): void;
|
|
/** Choosing the previewed session — click/Enter on its row, click into its
|
|
* pane, or handing it the keyboard (Tab). Keeps exactly what is on
|
|
* screen: a throwaway tab becomes real, a focus move inside an existing
|
|
* tab stands. Nothing moves a second time. */
|
|
commit(sessionId: Uuid): void;
|
|
/** Any other real change — opening the command bar, a dialog, navigating
|
|
* away — ends a pending or active preview immediately, bypassing the
|
|
* throttle. */
|
|
end(): void;
|
|
}
|
|
|
|
/** Preview state management (docs/PREVIEW.md): wraps the layout store's
|
|
* `previewSession`/`commitPreview` with the 120ms pane-swap throttle so the
|
|
* UI never has to schedule it by hand. */
|
|
export function usePreview({ store, scheduleDeps }: UsePreviewOptions): UsePreviewResult {
|
|
const previewedSessionId = useStore(store, (s: LayoutStore) => s.preview?.sessionId ?? null);
|
|
|
|
const scheduleRef = useRef<ReturnType<typeof createPreviewSchedule> | null>(null);
|
|
if (!scheduleRef.current) {
|
|
scheduleRef.current = createPreviewSchedule((id) => store.getState().previewSession(id), scheduleDeps);
|
|
}
|
|
useEffect(() => {
|
|
const schedule = scheduleRef.current;
|
|
return () => schedule?.cancel();
|
|
}, []);
|
|
|
|
const preview = useCallback((sessionId: Uuid | null) => {
|
|
scheduleRef.current?.(sessionId);
|
|
}, []);
|
|
|
|
const previewFromPointer = useCallback(
|
|
(sessionId: Uuid | null, pointerType: string) => {
|
|
if (pointerType === "touch") return;
|
|
preview(sessionId);
|
|
},
|
|
[preview],
|
|
);
|
|
|
|
const commit = useCallback(
|
|
(sessionId: Uuid) => {
|
|
scheduleRef.current?.cancel();
|
|
store.getState().openSession(sessionId);
|
|
},
|
|
[store],
|
|
);
|
|
|
|
const end = useCallback(() => {
|
|
scheduleRef.current?.cancel();
|
|
store.getState().previewSession(null);
|
|
}, [store]);
|
|
|
|
return { previewedSessionId, preview, previewFromPointer, commit, end };
|
|
}
|