25d9c8c364
In-preview HTML editing with undo/redo sync replaces Agent patch auto-save; Plaza runs on local Mac via Cloudflare Tunnel with embed height and CSP fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
import { useCallback, useRef, useState } from 'react';
|
|
|
|
export type PageDraftSnapshot = {
|
|
title: string;
|
|
summary: string;
|
|
content: string;
|
|
};
|
|
|
|
const MAX_HISTORY = 50;
|
|
const HISTORY_DEBOUNCE_MS = 600;
|
|
|
|
export function usePageDraftHistory(initial: PageDraftSnapshot) {
|
|
const [snapshot, setSnapshot] = useState(initial);
|
|
const stableRef = useRef(initial);
|
|
const pastRef = useRef<PageDraftSnapshot[]>([]);
|
|
const futureRef = useRef<PageDraftSnapshot[]>([]);
|
|
const debounceRef = useRef<number | null>(null);
|
|
const [historyRevision, setHistoryRevision] = useState(0);
|
|
const [canUndo, setCanUndo] = useState(false);
|
|
const [canRedo, setCanRedo] = useState(false);
|
|
|
|
const bump = () => {
|
|
setCanUndo(pastRef.current.length > 0);
|
|
setCanRedo(futureRef.current.length > 0);
|
|
setHistoryRevision((value) => value + 1);
|
|
};
|
|
|
|
const flushDebounced = useCallback(() => {
|
|
if (debounceRef.current != null) {
|
|
window.clearTimeout(debounceRef.current);
|
|
debounceRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
const recordStable = useCallback((next: PageDraftSnapshot) => {
|
|
if (JSON.stringify(stableRef.current) === JSON.stringify(next)) return;
|
|
pastRef.current = [...pastRef.current.slice(-(MAX_HISTORY - 1)), stableRef.current];
|
|
futureRef.current = [];
|
|
stableRef.current = next;
|
|
bump();
|
|
}, []);
|
|
|
|
const update = useCallback(
|
|
(next: PageDraftSnapshot) => {
|
|
setSnapshot(next);
|
|
bump();
|
|
flushDebounced();
|
|
debounceRef.current = window.setTimeout(() => {
|
|
debounceRef.current = null;
|
|
recordStable(next);
|
|
}, HISTORY_DEBOUNCE_MS);
|
|
},
|
|
[flushDebounced, recordStable],
|
|
);
|
|
|
|
const replace = useCallback(
|
|
(next: PageDraftSnapshot, options?: { recordHistory?: boolean }) => {
|
|
flushDebounced();
|
|
if (options?.recordHistory) {
|
|
pastRef.current = [...pastRef.current.slice(-(MAX_HISTORY - 1)), snapshot];
|
|
futureRef.current = [];
|
|
}
|
|
stableRef.current = next;
|
|
setSnapshot(next);
|
|
bump();
|
|
},
|
|
[flushDebounced, snapshot],
|
|
);
|
|
|
|
const undo = useCallback((): PageDraftSnapshot | null => {
|
|
flushDebounced();
|
|
const previous = pastRef.current[pastRef.current.length - 1];
|
|
if (!previous) return null;
|
|
pastRef.current = pastRef.current.slice(0, -1);
|
|
futureRef.current = [snapshot, ...futureRef.current];
|
|
stableRef.current = previous;
|
|
setSnapshot(previous);
|
|
bump();
|
|
return previous;
|
|
}, [flushDebounced, snapshot]);
|
|
|
|
const redo = useCallback((): PageDraftSnapshot | null => {
|
|
flushDebounced();
|
|
const next = futureRef.current[0];
|
|
if (!next) return null;
|
|
futureRef.current = futureRef.current.slice(1);
|
|
pastRef.current = [...pastRef.current, snapshot];
|
|
stableRef.current = next;
|
|
setSnapshot(next);
|
|
bump();
|
|
return next;
|
|
}, [flushDebounced, snapshot]);
|
|
|
|
const reset = useCallback(
|
|
(next: PageDraftSnapshot) => {
|
|
flushDebounced();
|
|
pastRef.current = [];
|
|
futureRef.current = [];
|
|
stableRef.current = next;
|
|
setSnapshot(next);
|
|
bump();
|
|
},
|
|
[flushDebounced],
|
|
);
|
|
|
|
return {
|
|
snapshot,
|
|
update,
|
|
replace,
|
|
undo,
|
|
redo,
|
|
reset,
|
|
canUndo,
|
|
canRedo,
|
|
historyRevision,
|
|
};
|
|
}
|