This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/hooks/usePreview.ts
T
Miguel Palhas ea8c42bfb2 feat(maestro): port preview mode to React (#251)
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).
2026-09-09 01:39:05 +01:00

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