mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
* feat(sdk): ws-3 — reorderElements op (batch z-index update) Adds the reorderElements EditOp: each entry sets inline zIndex on one element. Last-write-wins per target so a duplicated target collapses to a single zIndex patch. Positioning is unchanged — z-index only takes effect on non-static elements, so the caller must ensure the target is positioned. Also fixes single-dispatch undo to reverse the inverse patch list (parity with batch()): an op emitting multiple patches whose undo order matters — a duplicated reorderElements target, an aliased multi-target, or a nested parent+child removeElement — must undo in reverse application order, or undo lands on an intermediate value / drops a subtree. validateOp resolves every entry target (E_TARGET_NOT_FOUND for unknown ids; empty entries is a clean no-op). Tests cover set/inverse/validate/duplicate-target. Rebuilt standalone on main (reorderElements only depends on handleSetStyle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sdk): remove unused HTTP persist adapter The HTTP PersistAdapter (createHttpAdapter) was dead weight after the studio cutover went single-writer (ws-4): Studio's writeProjectFile is the sole writer and useSdkSession opens with no persist queue, so the adapter's write/flush/ listVersions/loadFrom were never used — only read() was, to fetch the composition source. Replace those two read() calls with a direct optional fetch (GET /files/<path>?optional=1) and drop the adapter + its export-map entries. Saved for later re-introduction (when a non-Studio SDK host needs server-backed persist) at docs/hyperframes/plans/sdk-http-adapter/ (outside the repo). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(studio): resolver shadow for z-index reorder targets The z-index reorder commit takes the server path (no SDK persist), but the resolver-shadow tripwire is decoupled from cutover — so it should still record whether the SDK resolves each reordered element (reorderElements' targets), the same as timing/delete already do before their cutover gate. This gives wild resolver-parity telemetry on z-index targets before z-index reorder is cut over. Threads an onReorderShadow callback (sdkSession-bound, mirrors onTrySdkDelete) from useDomEditSession → useDomEditCommits → useElementLifecycleOps, called with the reordered elements' hf-ids in handleDomZIndexReorderCommit. Read-only, divergence-only, never throws — same contract as recordResolverParity elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio): guard project-file read path against traversal (CodeQL CSRF) readProjectFileOptional interpolated a user-influenced composition path into the fetch URL, which CodeQL flagged as client-side request forgery. Reject NUL/`..` up front (mirrors the existing guard in timelineEditingHelpers) and encodeURIComponent the projectId too, so both values stay confined to single segments of the same-origin URL. Unsafe path → undefined (graceful for the optional read). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
470 lines
13 KiB
TypeScript
470 lines
13 KiB
TypeScript
import type { TimelineElement } from "../player";
|
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
import type { SidebarTab } from "../components/sidebar/LeftSidebar";
|
|
import type { Composition } from "@hyperframes/sdk";
|
|
import { sdkCutoverPersist, sdkDeletePersist } from "../utils/sdkCutover";
|
|
import { runResolverShadow, recordResolverParity } from "../utils/sdkResolverShadow";
|
|
import { useAskAgentModal } from "./useAskAgentModal";
|
|
import { useDomSelection } from "./useDomSelection";
|
|
import { usePreviewInteraction } from "./usePreviewInteraction";
|
|
import { useDomEditCommits } from "./useDomEditCommits";
|
|
import { useGsapScriptCommits } from "./useGsapScriptCommits";
|
|
import { useGsapCacheVersion } from "./useGsapTweenCache";
|
|
import { useDomEditWiring } from "./useDomEditWiring";
|
|
import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
|
|
|
// ── Types ──
|
|
|
|
interface RecordEditInput {
|
|
label: string;
|
|
kind: EditHistoryKind;
|
|
coalesceKey?: string;
|
|
files: Record<string, { before: string; after: string }>;
|
|
}
|
|
|
|
export interface UseDomEditSessionParams {
|
|
projectId: string | null;
|
|
activeCompPath: string | null;
|
|
isMasterView: boolean;
|
|
compIdToSrc: Map<string, string>;
|
|
captionEditMode: boolean;
|
|
compositionLoading: boolean;
|
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
timelineElements: TimelineElement[];
|
|
setSelectedTimelineElementId: (id: string | null) => void;
|
|
setRightCollapsed: (collapsed: boolean) => void;
|
|
setRightPanelTab: (tab: RightPanelTab) => void;
|
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
refreshPreviewDocumentVersion: () => void;
|
|
queueDomEditSave: (save: () => Promise<void>) => Promise<void>;
|
|
readProjectFile: (path: string) => Promise<string>;
|
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
updateEditingFileContent: (path: string, content: string) => void;
|
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
fileTree: string[];
|
|
importedFontAssetsRef: React.MutableRefObject<ImportedFontAsset[]>;
|
|
projectDir: string | null;
|
|
projectIdRef: React.MutableRefObject<string | null>;
|
|
previewIframe: HTMLIFrameElement | null;
|
|
refreshKey: number;
|
|
rightPanelTab: RightPanelTab;
|
|
applyStudioManualEditsToPreviewRef: React.MutableRefObject<
|
|
(iframe: HTMLIFrameElement) => Promise<void>
|
|
>;
|
|
syncPreviewHistoryHotkey: (iframe: HTMLIFrameElement | null) => void;
|
|
reloadPreview: () => void;
|
|
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
|
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
selectSidebarTab?: (tab: SidebarTab) => void;
|
|
getSidebarTab?: () => SidebarTab;
|
|
sdkSession?: Composition | null;
|
|
forceReloadSdkSession?: () => void;
|
|
}
|
|
|
|
// ── Hook ──
|
|
|
|
export function useDomEditSession({
|
|
projectId,
|
|
activeCompPath,
|
|
isMasterView,
|
|
compIdToSrc,
|
|
captionEditMode,
|
|
compositionLoading,
|
|
previewIframeRef,
|
|
timelineElements,
|
|
setSelectedTimelineElementId,
|
|
setRightCollapsed,
|
|
setRightPanelTab,
|
|
showToast,
|
|
refreshPreviewDocumentVersion,
|
|
queueDomEditSave,
|
|
readProjectFile: _readProjectFile,
|
|
writeProjectFile,
|
|
updateEditingFileContent,
|
|
domEditSaveTimestampRef,
|
|
editHistory,
|
|
fileTree,
|
|
importedFontAssetsRef,
|
|
projectDir,
|
|
projectIdRef,
|
|
previewIframe,
|
|
refreshKey,
|
|
rightPanelTab,
|
|
applyStudioManualEditsToPreviewRef,
|
|
syncPreviewHistoryHotkey,
|
|
reloadPreview,
|
|
setRefreshKey: _setRefreshKey,
|
|
openSourceForSelection,
|
|
selectSidebarTab,
|
|
getSidebarTab,
|
|
sdkSession,
|
|
forceReloadSdkSession,
|
|
}: UseDomEditSessionParams) {
|
|
void _setRefreshKey;
|
|
void _readProjectFile;
|
|
|
|
// ── Selection ──
|
|
|
|
const {
|
|
domEditSelection,
|
|
domEditGroupSelections,
|
|
domEditHoverSelection,
|
|
domEditSelectionRef,
|
|
applyDomSelection,
|
|
clearDomSelection,
|
|
buildDomSelectionFromTarget,
|
|
resolveDomSelectionFromPreviewPoint,
|
|
resolveAllDomSelectionsFromPreviewPoint,
|
|
updateDomEditHoverSelection,
|
|
buildDomSelectionForTimelineElement,
|
|
handleTimelineElementSelect,
|
|
refreshDomEditSelectionFromPreview,
|
|
} = useDomSelection({
|
|
projectId,
|
|
activeCompPath,
|
|
isMasterView,
|
|
compIdToSrc,
|
|
captionEditMode,
|
|
previewIframeRef,
|
|
timelineElements,
|
|
setSelectedTimelineElementId,
|
|
setRightCollapsed,
|
|
setRightPanelTab,
|
|
previewIframe,
|
|
refreshKey,
|
|
rightPanelTab,
|
|
});
|
|
|
|
// ── Agent modal ──
|
|
|
|
const {
|
|
agentModalOpen,
|
|
agentModalAnchorPoint,
|
|
copiedAgentPrompt,
|
|
agentPromptSelectionContext,
|
|
setAgentModalOpen,
|
|
setAgentPromptSelectionContext,
|
|
setAgentModalAnchorPoint,
|
|
handleAskAgent,
|
|
handleAgentModalSubmit,
|
|
} = useAskAgentModal({
|
|
projectId,
|
|
activeCompPath,
|
|
projectDir,
|
|
projectIdRef,
|
|
showToast,
|
|
domEditSelectionRef,
|
|
domEditSelection,
|
|
});
|
|
|
|
// ── GSAP cache (hoisted so both useGsapScriptCommits and useDomEditWiring share the same instance) ──
|
|
|
|
const { version: gsapCacheVersion, bump: bumpGsapCache } = useGsapCacheVersion();
|
|
|
|
// ── GSAP script commits ──
|
|
|
|
const {
|
|
commitMutation: gsapCommitMutation,
|
|
updateGsapProperty,
|
|
updateGsapMeta,
|
|
deleteGsapAnimation,
|
|
deleteAllForSelector,
|
|
addGsapAnimation,
|
|
addGsapProperty,
|
|
removeGsapProperty,
|
|
updateGsapFromProperty,
|
|
addGsapFromProperty,
|
|
removeGsapFromProperty,
|
|
addKeyframe,
|
|
addKeyframeBatch,
|
|
removeKeyframe,
|
|
convertToKeyframes,
|
|
removeAllKeyframes,
|
|
setArcPath,
|
|
updateArcSegment,
|
|
} = useGsapScriptCommits({
|
|
projectIdRef,
|
|
activeCompPath,
|
|
previewIframeRef,
|
|
editHistory,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview,
|
|
onCacheInvalidate: bumpGsapCache,
|
|
onFileContentChanged: updateEditingFileContent,
|
|
showToast,
|
|
sdkSession,
|
|
writeProjectFile,
|
|
forceReloadSdkSession,
|
|
});
|
|
|
|
// ── DOM commit handlers ──
|
|
|
|
const {
|
|
resolveImportedFontAsset,
|
|
handleDomStyleCommit,
|
|
handleDomAttributeCommit,
|
|
handleDomAttributeLiveCommit,
|
|
handleDomHtmlAttributeCommit,
|
|
handleDomTextCommit,
|
|
handleDomTextFieldStyleCommit,
|
|
handleDomAddTextField,
|
|
handleDomRemoveTextField,
|
|
handleDomPathOffsetCommit,
|
|
handleDomGroupPathOffsetCommit,
|
|
handleDomBoxSizeCommit,
|
|
handleDomRotationCommit,
|
|
handleDomManualEditsReset,
|
|
handleDomEditElementDelete,
|
|
handleDomZIndexReorderCommit,
|
|
} = useDomEditCommits({
|
|
activeCompPath,
|
|
previewIframeRef,
|
|
showToast,
|
|
queueDomEditSave,
|
|
writeProjectFile,
|
|
domEditSaveTimestampRef,
|
|
editHistory,
|
|
fileTree,
|
|
importedFontAssetsRef,
|
|
projectId,
|
|
projectIdRef,
|
|
reloadPreview,
|
|
domEditSelection,
|
|
applyDomSelection,
|
|
clearDomSelection,
|
|
refreshDomEditSelectionFromPreview,
|
|
buildDomSelectionFromTarget,
|
|
forceReloadSdkSession,
|
|
onTrySdkPersist: sdkSession
|
|
? (selection, operations, originalContent, targetPath, options) => {
|
|
// Resolver shadow runs regardless of the cutover flag — decoupled tripwire.
|
|
runResolverShadow(sdkSession, selection.hfId, operations);
|
|
return sdkCutoverPersist(
|
|
selection,
|
|
operations,
|
|
originalContent,
|
|
targetPath,
|
|
sdkSession,
|
|
{
|
|
editHistory,
|
|
writeProjectFile,
|
|
reloadPreview,
|
|
domEditSaveTimestampRef,
|
|
compositionPath: activeCompPath,
|
|
},
|
|
options,
|
|
);
|
|
}
|
|
: undefined,
|
|
onTrySdkDelete: sdkSession
|
|
? (hfId, originalContent, targetPath) =>
|
|
sdkDeletePersist(hfId, originalContent, targetPath, sdkSession, {
|
|
editHistory,
|
|
writeProjectFile,
|
|
reloadPreview,
|
|
domEditSaveTimestampRef,
|
|
compositionPath: activeCompPath,
|
|
})
|
|
: undefined,
|
|
// Resolver shadow for the z-index reorder edit: it takes the server path (no
|
|
// SDK persist), but the tripwire is decoupled from cutover — record whether
|
|
// the SDK resolves each reordered element (the reorderElements op's targets).
|
|
onReorderShadow: sdkSession
|
|
? (targets: string[]) => {
|
|
for (const target of targets) recordResolverParity(sdkSession, target, "reorderElements");
|
|
}
|
|
: undefined,
|
|
});
|
|
|
|
// ── Wiring: selection sync, GSAP cache, preview sync, selection handlers ──
|
|
|
|
const {
|
|
onClickToSource,
|
|
selectedGsapAnimations,
|
|
gsapMultipleTimelines,
|
|
gsapUnsupportedTimelinePattern,
|
|
trackGsapInteractionFailure,
|
|
makeFetchFallback,
|
|
handleGsapUpdateProperty,
|
|
handleGsapUpdateMeta,
|
|
handleGsapDeleteAnimation,
|
|
handleGsapDeleteAllForElement,
|
|
handleGsapAddAnimation,
|
|
handleGsapAddProperty,
|
|
handleGsapRemoveProperty,
|
|
handleGsapUpdateFromProperty,
|
|
handleGsapAddFromProperty,
|
|
handleGsapRemoveFromProperty,
|
|
handleGsapAddKeyframe,
|
|
handleGsapAddKeyframeBatch,
|
|
handleGsapRemoveKeyframe,
|
|
handleGsapConvertToKeyframes,
|
|
handleGsapRemoveAllKeyframes,
|
|
handleResetSelectedElementKeyframes,
|
|
} = useDomEditWiring({
|
|
// fallow-ignore-next-line code-duplication
|
|
projectId,
|
|
activeCompPath,
|
|
domEditSelection,
|
|
domEditSelectionRef,
|
|
previewIframeRef,
|
|
previewIframe,
|
|
captionEditMode,
|
|
refreshKey,
|
|
gsapCacheVersion,
|
|
bumpGsapCache,
|
|
showToast,
|
|
refreshPreviewDocumentVersion,
|
|
syncPreviewHistoryHotkey,
|
|
applyStudioManualEditsToPreviewRef,
|
|
applyDomSelection,
|
|
buildDomSelectionFromTarget,
|
|
openSourceForSelection,
|
|
selectSidebarTab,
|
|
getSidebarTab,
|
|
updateGsapProperty,
|
|
updateGsapMeta,
|
|
deleteGsapAnimation,
|
|
deleteAllForSelector,
|
|
addGsapAnimation,
|
|
addGsapProperty,
|
|
removeGsapProperty,
|
|
updateGsapFromProperty,
|
|
addGsapFromProperty,
|
|
removeGsapFromProperty,
|
|
addKeyframe,
|
|
addKeyframeBatch,
|
|
removeKeyframe,
|
|
convertToKeyframes,
|
|
removeAllKeyframes,
|
|
handleDomManualEditsReset,
|
|
});
|
|
|
|
// ── Preview interaction ──
|
|
|
|
const {
|
|
handlePreviewCanvasMouseDown,
|
|
handlePreviewCanvasPointerMove,
|
|
handlePreviewCanvasPointerLeave,
|
|
handleBlockedDomMove,
|
|
handleDomManualDragStart,
|
|
} = usePreviewInteraction({
|
|
captionEditMode,
|
|
compositionLoading,
|
|
previewIframeRef,
|
|
showToast,
|
|
applyDomSelection,
|
|
resolveDomSelectionFromPreviewPoint,
|
|
resolveAllDomSelectionsFromPreviewPoint,
|
|
updateDomEditHoverSelection,
|
|
onClickToSource,
|
|
});
|
|
|
|
// ── GSAP-aware geometry intercepts + animated property commit ──
|
|
|
|
const {
|
|
handleGsapAwarePathOffsetCommit,
|
|
handleGsapAwareBoxSizeCommit,
|
|
handleGsapAwareRotationCommit,
|
|
commitAnimatedProperty,
|
|
handleSetArcPath,
|
|
handleUpdateArcSegment,
|
|
handleUnroll,
|
|
commitMutation,
|
|
} = useGsapAwareEditing({
|
|
domEditSelection,
|
|
selectedGsapAnimations,
|
|
gsapCommitMutation,
|
|
previewIframeRef,
|
|
showToast,
|
|
bumpGsapCache,
|
|
makeFetchFallback,
|
|
trackGsapInteractionFailure,
|
|
handleDomPathOffsetCommit,
|
|
handleDomBoxSizeCommit,
|
|
handleDomRotationCommit,
|
|
addGsapAnimation,
|
|
convertToKeyframes,
|
|
setArcPath,
|
|
updateArcSegment,
|
|
});
|
|
|
|
return {
|
|
// State
|
|
domEditSelection,
|
|
domEditGroupSelections,
|
|
domEditHoverSelection,
|
|
agentModalOpen,
|
|
agentModalAnchorPoint,
|
|
copiedAgentPrompt,
|
|
agentPromptSelectionContext,
|
|
// Refs
|
|
domEditSelectionRef,
|
|
// Callbacks
|
|
handleTimelineElementSelect,
|
|
handlePreviewCanvasMouseDown,
|
|
handlePreviewCanvasPointerMove,
|
|
handlePreviewCanvasPointerLeave,
|
|
applyDomSelection,
|
|
clearDomSelection,
|
|
handleDomStyleCommit,
|
|
handleDomAttributeCommit,
|
|
handleDomAttributeLiveCommit,
|
|
handleDomHtmlAttributeCommit,
|
|
handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
|
|
handleDomGroupPathOffsetCommit,
|
|
handleDomZIndexReorderCommit,
|
|
handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit,
|
|
handleDomRotationCommit: handleGsapAwareRotationCommit,
|
|
handleDomManualEditsReset,
|
|
handleDomTextCommit,
|
|
handleDomTextFieldStyleCommit,
|
|
handleDomAddTextField,
|
|
handleDomRemoveTextField,
|
|
handleAskAgent,
|
|
handleAgentModalSubmit,
|
|
handleBlockedDomMove,
|
|
handleDomManualDragStart,
|
|
handleDomEditElementDelete,
|
|
buildDomSelectionFromTarget,
|
|
buildDomSelectionForTimelineElement,
|
|
updateDomEditHoverSelection,
|
|
resolveImportedFontAsset,
|
|
setAgentModalOpen,
|
|
setAgentPromptSelectionContext,
|
|
setAgentModalAnchorPoint,
|
|
|
|
// GSAP script editing
|
|
selectedGsapAnimations,
|
|
gsapMultipleTimelines,
|
|
gsapUnsupportedTimelinePattern,
|
|
handleGsapUpdateProperty,
|
|
handleGsapUpdateMeta,
|
|
handleGsapDeleteAnimation,
|
|
handleGsapDeleteAllForElement,
|
|
handleGsapAddAnimation,
|
|
handleGsapAddProperty,
|
|
handleGsapRemoveProperty,
|
|
handleGsapUpdateFromProperty,
|
|
handleGsapAddFromProperty,
|
|
handleGsapRemoveFromProperty,
|
|
handleGsapAddKeyframe,
|
|
handleGsapAddKeyframeBatch,
|
|
handleGsapRemoveKeyframe,
|
|
handleGsapConvertToKeyframes,
|
|
handleGsapRemoveAllKeyframes,
|
|
handleResetSelectedElementKeyframes,
|
|
commitAnimatedProperty,
|
|
handleSetArcPath,
|
|
handleUpdateArcSegment,
|
|
handleUnroll,
|
|
invalidateGsapCache: bumpGsapCache,
|
|
previewIframeRef,
|
|
commitMutation,
|
|
};
|
|
}
|