// 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 | 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 }; }