mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
fix(studio): break all 7 circular dependency cycles and fix rules-of-hooks violation (#1422)
This commit is contained in:
@@ -267,7 +267,7 @@ jobs:
|
|||||||
- run: bun run --cwd packages/core build:hyperframes-runtime
|
- run: bun run --cwd packages/core build:hyperframes-runtime
|
||||||
- name: Start studio and check for runtime errors
|
- name: Start studio and check for runtime errors
|
||||||
run: |
|
run: |
|
||||||
# Start the studio dev server in the background
|
# Start the studio Vite dev server (fast — no bundle step)
|
||||||
bun run --filter '@hyperframes/studio' dev -- --port 5199 &
|
bun run --filter '@hyperframes/studio' dev -- --port 5199 &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
@@ -283,8 +283,8 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Load the studio in headless Chrome and capture console errors
|
# Load the studio in headless Chrome with API mocking to trigger
|
||||||
# puppeteer is a dependency of @hyperframes/producer; resolve from there
|
# the full splash→main transition (catches hooks-after-early-return bugs)
|
||||||
cd packages/producer
|
cd packages/producer
|
||||||
node --input-type=module <<'SMOKE_EOF'
|
node --input-type=module <<'SMOKE_EOF'
|
||||||
import puppeteer from "puppeteer";
|
import puppeteer from "puppeteer";
|
||||||
@@ -298,18 +298,68 @@ jobs:
|
|||||||
page.on("console", (msg) => {
|
page.on("console", (msg) => {
|
||||||
if (msg.type() === "error") errors.push(msg.text());
|
if (msg.type() === "error") errors.push(msg.text());
|
||||||
});
|
});
|
||||||
await page.goto("http://localhost:5199/", { waitUntil: "networkidle0", timeout: 30000 });
|
|
||||||
|
// Mock the project API so the studio transitions past the splash screen.
|
||||||
|
// Without this, useServerConnection stays in "waiting" and the full React
|
||||||
|
// tree (with all hooks) never renders — missing hooks-order violations.
|
||||||
|
const COMP_HTML = '<div data-composition-id="root" data-width="1920" data-height="1080" data-duration="1" data-start="0"><div class="clip" data-start="0" data-duration="1">Test</div></div>';
|
||||||
|
await page.setRequestInterception(true);
|
||||||
|
page.on("request", (req) => {
|
||||||
|
const url = req.url();
|
||||||
|
if (url.includes("/api/projects") && !url.includes("/files") && !url.includes("/preview") && !url.includes("/gsap")) {
|
||||||
|
req.respond({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ projects: [{ id: "smoke-test" }] }),
|
||||||
|
});
|
||||||
|
} else if (url.includes("/api/") && url.includes("/files")) {
|
||||||
|
req.respond({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ files: [{ path: "index.html", type: "file" }] }),
|
||||||
|
});
|
||||||
|
} else if (url.includes("/api/") && url.includes("/preview")) {
|
||||||
|
req.respond({ status: 200, contentType: "text/html", body: COMP_HTML });
|
||||||
|
} else if (url.includes("/api/")) {
|
||||||
|
req.respond({ status: 200, contentType: "application/json", body: JSON.stringify({}) });
|
||||||
|
} else {
|
||||||
|
req.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("http://localhost:5199/#project=smoke-test", {
|
||||||
|
waitUntil: "networkidle0",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
// Wait for React to render past splash into the full studio UI
|
||||||
await new Promise((r) => setTimeout(r, 3000));
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
|
||||||
|
// Check for React error boundary (catches hooks violations, render crashes)
|
||||||
|
const errorBoundary = await page.evaluate(() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
if (text.includes("Something went wrong")) return text;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (errorBoundary) {
|
||||||
|
errors.push("React error boundary triggered: " + errorBoundary);
|
||||||
|
}
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
// Filter expected noise from mock endpoints
|
||||||
const fatal = errors.filter(
|
const fatal = errors.filter(
|
||||||
(e) => !e.includes("favicon") && !e.includes("ERR_CONNECTION_REFUSED"),
|
(e) =>
|
||||||
|
!e.includes("favicon") &&
|
||||||
|
!e.includes("ERR_CONNECTION_REFUSED") &&
|
||||||
|
!e.includes("Failed to fetch") &&
|
||||||
|
!e.includes("is not iterable") &&
|
||||||
|
!e.includes("Cannot read properties of undefined") &&
|
||||||
|
!e.includes("Cannot read properties of null"),
|
||||||
);
|
);
|
||||||
if (fatal.length > 0) {
|
if (fatal.length > 0) {
|
||||||
console.error("FAIL: studio had runtime errors on load:");
|
console.error("FAIL: studio had runtime errors:");
|
||||||
for (const e of fatal) console.error(" •", e);
|
for (const e of fatal) console.error(" •", e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log("PASS: studio loaded without runtime errors");
|
console.log("PASS: studio loaded and transitioned without runtime errors");
|
||||||
SMOKE_EOF
|
SMOKE_EOF
|
||||||
|
|
||||||
kill $SERVER_PID 2>/dev/null || true
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
|||||||
Binary file not shown.
@@ -452,8 +452,6 @@ export function StudioApp() {
|
|||||||
timelineVisible,
|
timelineVisible,
|
||||||
toggleTimelineVisibility,
|
toggleTimelineVisibility,
|
||||||
});
|
});
|
||||||
if (resolving || waitingForServer || !projectId)
|
|
||||||
return <StudioSplash waiting={waitingForServer} />;
|
|
||||||
const timelineToolbar = useMemo(
|
const timelineToolbar = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TimelineToolbar
|
<TimelineToolbar
|
||||||
@@ -464,6 +462,8 @@ export function StudioApp() {
|
|||||||
),
|
),
|
||||||
[toggleTimelineVisibility, domEditSession, timelineEditing.handleTimelineElementSplit],
|
[toggleTimelineVisibility, domEditSession, timelineEditing.handleTimelineElementSplit],
|
||||||
);
|
);
|
||||||
|
if (resolving || waitingForServer || !projectId)
|
||||||
|
return <StudioSplash waiting={waitingForServer} />;
|
||||||
return (
|
return (
|
||||||
<StudioShellProvider value={studioCtxValue}>
|
<StudioShellProvider value={studioCtxValue}>
|
||||||
<StudioPlaybackProvider value={studioCtxValue}>
|
<StudioPlaybackProvider value={studioCtxValue}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { type DomEditSelection } from "./domEditing";
|
|||||||
import { resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry";
|
import { resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry";
|
||||||
import {
|
import {
|
||||||
type BlockedMoveState,
|
type BlockedMoveState,
|
||||||
|
type DomEditGroupPathOffsetCommit,
|
||||||
type FocusableDomEditOverlay,
|
type FocusableDomEditOverlay,
|
||||||
type GestureState,
|
type GestureState,
|
||||||
type GroupGestureState,
|
type GroupGestureState,
|
||||||
@@ -27,11 +28,7 @@ export {
|
|||||||
resolveDomEditResizeGesture,
|
resolveDomEditResizeGesture,
|
||||||
resolveDomEditRotationGesture,
|
resolveDomEditRotationGesture,
|
||||||
} from "./domEditOverlayGestures";
|
} from "./domEditOverlayGestures";
|
||||||
|
export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
||||||
export interface DomEditGroupPathOffsetCommit {
|
|
||||||
selection: DomEditSelection;
|
|
||||||
next: { x: number; y: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DomEditOverlayProps {
|
interface DomEditOverlayProps {
|
||||||
iframeRef: RefObject<HTMLIFrameElement | null>;
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { RefObject } from "react";
|
||||||
import type { DomEditSelection } from "./domEditing";
|
import type { DomEditSelection } from "./domEditing";
|
||||||
import type {
|
import type {
|
||||||
StudioBoxSizeSnapshot,
|
StudioBoxSizeSnapshot,
|
||||||
@@ -5,8 +6,9 @@ import type {
|
|||||||
StudioRotationSnapshot,
|
StudioRotationSnapshot,
|
||||||
} from "./manualEdits";
|
} from "./manualEdits";
|
||||||
import type { ManualOffsetDragMember } from "./manualOffsetDrag";
|
import type { ManualOffsetDragMember } from "./manualOffsetDrag";
|
||||||
import type { GroupOverlayItem } from "./domEditOverlayGeometry";
|
import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry";
|
||||||
import type { SnapContext } from "./snapTargetCollection";
|
import type { SnapContext } from "./snapTargetCollection";
|
||||||
|
import type { SnapGuidesState } from "./SnapGuideOverlay";
|
||||||
|
|
||||||
export type GestureKind = "drag" | "resize" | "rotate";
|
export type GestureKind = "drag" | "resize" | "rotate";
|
||||||
|
|
||||||
@@ -143,3 +145,54 @@ export function resolveDomEditRotationGesture(input: {
|
|||||||
export function hasDomEditRotationChanged(initialAngle: number, nextAngle: number): boolean {
|
export function hasDomEditRotationChanged(initialAngle: number, nextAngle: number): boolean {
|
||||||
return Math.abs(nextAngle - initialAngle) >= ROTATION_COMMIT_EPSILON_DEGREES;
|
return Math.abs(nextAngle - initialAngle) >= ROTATION_COMMIT_EPSILON_DEGREES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Shared types for DomEditOverlay gesture wiring ──
|
||||||
|
// These live here (rather than in DomEditOverlay.tsx or useDomEditOverlayGestures.ts)
|
||||||
|
// to break circular imports between those files.
|
||||||
|
|
||||||
|
export interface DomEditGroupPathOffsetCommit {
|
||||||
|
selection: DomEditSelection;
|
||||||
|
next: { x: number; y: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refs are stable across renders; values are read via .current.
|
||||||
|
export type UseDomEditOverlayGesturesOptions = {
|
||||||
|
overlayRef: RefObject<HTMLDivElement | null>;
|
||||||
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||||
|
boxRef: RefObject<HTMLDivElement | null>;
|
||||||
|
selectionRef: RefObject<DomEditSelection | null>;
|
||||||
|
overlayRectRef: RefObject<OverlayRect | null>;
|
||||||
|
groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
|
||||||
|
gestureRef: RefObject<GestureState | null>;
|
||||||
|
groupGestureRef: RefObject<GroupGestureState | null>;
|
||||||
|
blockedMoveRef: RefObject<BlockedMoveState | null>;
|
||||||
|
rafPausedRef: RefObject<boolean>;
|
||||||
|
suppressNextBoxClickRef: RefObject<boolean>;
|
||||||
|
setOverlayRect: (next: OverlayRect | null) => void;
|
||||||
|
setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
|
||||||
|
onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>;
|
||||||
|
onManualDragStartRef: RefObject<(() => void) | undefined>;
|
||||||
|
onPathOffsetCommitRef: RefObject<
|
||||||
|
(s: DomEditSelection, n: { x: number; y: number }) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
onGroupPathOffsetCommitRef: RefObject<
|
||||||
|
(updates: DomEditGroupPathOffsetCommit[]) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
onBoxSizeCommitRef: RefObject<
|
||||||
|
(s: DomEditSelection, n: { width: number; height: number }) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
onRotationCommitRef: RefObject<
|
||||||
|
(s: DomEditSelection, n: { angle: number }) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
onCanvasPointerMoveRef: RefObject<
|
||||||
|
(
|
||||||
|
e: React.PointerEvent<HTMLDivElement>,
|
||||||
|
o?: { preferClipAncestor?: boolean },
|
||||||
|
) => Promise<DomEditSelection | null>
|
||||||
|
>;
|
||||||
|
onCanvasMouseDown: (
|
||||||
|
e: React.MouseEvent<HTMLDivElement>,
|
||||||
|
o?: { preferClipAncestor?: boolean },
|
||||||
|
) => void;
|
||||||
|
snapGuidesRef: RefObject<SnapGuidesState | null>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ import {
|
|||||||
filterNestedDomEditGroupItems,
|
filterNestedDomEditGroupItems,
|
||||||
selectionCacheKey,
|
selectionCacheKey,
|
||||||
} from "./domEditOverlayGeometry";
|
} from "./domEditOverlayGeometry";
|
||||||
import { type GestureKind, type GestureState } from "./domEditOverlayGestures";
|
import {
|
||||||
import type { UseDomEditOverlayGesturesOptions } from "./useDomEditOverlayGestures";
|
type GestureKind,
|
||||||
|
type GestureState,
|
||||||
|
type UseDomEditOverlayGesturesOptions,
|
||||||
|
} from "./domEditOverlayGestures";
|
||||||
import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
|
import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
|
||||||
|
|
||||||
export function startGroupDrag(
|
export function startGroupDrag(
|
||||||
|
|||||||
@@ -33,15 +33,14 @@ import {
|
|||||||
} from "./domEditOverlayGeometry";
|
} from "./domEditOverlayGeometry";
|
||||||
import {
|
import {
|
||||||
BLOCKED_MOVE_THRESHOLD_PX,
|
BLOCKED_MOVE_THRESHOLD_PX,
|
||||||
type BlockedMoveState,
|
|
||||||
type GestureKind,
|
type GestureKind,
|
||||||
type GestureState,
|
type GestureState,
|
||||||
type GroupGestureState,
|
type GroupGestureState,
|
||||||
|
type UseDomEditOverlayGesturesOptions,
|
||||||
hasDomEditRotationChanged,
|
hasDomEditRotationChanged,
|
||||||
resolveDomEditResizeGesture,
|
resolveDomEditResizeGesture,
|
||||||
resolveDomEditRotationGesture,
|
resolveDomEditRotationGesture,
|
||||||
} from "./domEditOverlayGestures";
|
} from "./domEditOverlayGestures";
|
||||||
import type { DomEditGroupPathOffsetCommit } from "./DomEditOverlay";
|
|
||||||
import {
|
import {
|
||||||
startGesture as _startGesture,
|
startGesture as _startGesture,
|
||||||
startGroupDrag as _startGroupDrag,
|
startGroupDrag as _startGroupDrag,
|
||||||
@@ -52,50 +51,6 @@ import {
|
|||||||
resolveEquidistanceGuides,
|
resolveEquidistanceGuides,
|
||||||
SNAP_THRESHOLD_PX,
|
SNAP_THRESHOLD_PX,
|
||||||
} from "./snapEngine";
|
} from "./snapEngine";
|
||||||
import type { SnapGuidesState } from "./SnapGuideOverlay";
|
|
||||||
|
|
||||||
// Refs are stable across renders; values are read via .current.
|
|
||||||
export type UseDomEditOverlayGesturesOptions = {
|
|
||||||
overlayRef: RefObject<HTMLDivElement | null>;
|
|
||||||
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
||||||
boxRef: RefObject<HTMLDivElement | null>;
|
|
||||||
selectionRef: RefObject<DomEditSelection | null>;
|
|
||||||
overlayRectRef: RefObject<OverlayRect | null>;
|
|
||||||
groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
|
|
||||||
gestureRef: RefObject<GestureState | null>;
|
|
||||||
groupGestureRef: RefObject<GroupGestureState | null>;
|
|
||||||
blockedMoveRef: RefObject<BlockedMoveState | null>;
|
|
||||||
rafPausedRef: RefObject<boolean>;
|
|
||||||
suppressNextBoxClickRef: RefObject<boolean>;
|
|
||||||
setOverlayRect: (next: OverlayRect | null) => void;
|
|
||||||
setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
|
|
||||||
onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>;
|
|
||||||
onManualDragStartRef: RefObject<(() => void) | undefined>;
|
|
||||||
onPathOffsetCommitRef: RefObject<
|
|
||||||
(s: DomEditSelection, n: { x: number; y: number }) => Promise<void> | void
|
|
||||||
>;
|
|
||||||
onGroupPathOffsetCommitRef: RefObject<
|
|
||||||
(updates: DomEditGroupPathOffsetCommit[]) => Promise<void> | void
|
|
||||||
>;
|
|
||||||
onBoxSizeCommitRef: RefObject<
|
|
||||||
(s: DomEditSelection, n: { width: number; height: number }) => Promise<void> | void
|
|
||||||
>;
|
|
||||||
onRotationCommitRef: RefObject<
|
|
||||||
(s: DomEditSelection, n: { angle: number }) => Promise<void> | void
|
|
||||||
>;
|
|
||||||
onCanvasPointerMoveRef: RefObject<
|
|
||||||
(
|
|
||||||
e: React.PointerEvent<HTMLDivElement>,
|
|
||||||
o?: { preferClipAncestor?: boolean },
|
|
||||||
) => Promise<DomEditSelection | null>
|
|
||||||
>;
|
|
||||||
onCanvasMouseDown: (
|
|
||||||
e: React.MouseEvent<HTMLDivElement>,
|
|
||||||
o?: { preferClipAncestor?: boolean },
|
|
||||||
) => void;
|
|
||||||
snapGuidesRef: RefObject<SnapGuidesState | null>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
|
export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
|
||||||
const setDraftOverlayRect = (next: OverlayRect) => {
|
const setDraftOverlayRect = (next: OverlayRect) => {
|
||||||
opts.setOverlayRect(next);
|
opts.setOverlayRect(next);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
||||||
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
||||||
|
|
||||||
|
export type PersistDomEditOperations = (
|
||||||
|
selection: DomEditSelection,
|
||||||
|
operations: PatchOperation[],
|
||||||
|
options?: {
|
||||||
|
label?: string;
|
||||||
|
coalesceKey?: string;
|
||||||
|
skipRefresh?: boolean;
|
||||||
|
prepareContent?: (html: string, sourceFile: string) => string;
|
||||||
|
shouldSave?: () => boolean;
|
||||||
|
},
|
||||||
|
) => Promise<void>;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TimelineElement } from "../player";
|
import type { TimelineElement } from "../player/store/playerStore";
|
||||||
import { applyPatchByTarget, readAttributeByTarget } from "../utils/sourcePatcher";
|
import { applyPatchByTarget, readAttributeByTarget } from "../utils/sourcePatcher";
|
||||||
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
||||||
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
||||||
@@ -6,7 +6,7 @@ import type { EditHistoryKind } from "../utils/editHistory";
|
|||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
|
|
||||||
interface RecordEditInput {
|
export interface RecordEditInput {
|
||||||
label: string;
|
label: string;
|
||||||
kind: EditHistoryKind;
|
kind: EditHistoryKind;
|
||||||
coalesceKey?: string;
|
coalesceKey?: string;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation";
|
import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation";
|
||||||
import { FONT_EXT } from "../utils/mediaTypes";
|
import { FONT_EXT } from "../utils/mediaTypes";
|
||||||
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
||||||
import { trackStudioEvent } from "../utils/studioTelemetry";
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
||||||
import { primaryFontFamilyValue } from "../utils/studioFontHelpers";
|
import { primaryFontFamilyValue } from "../utils/studioFontHelpers";
|
||||||
import { createStudioSaveHttpError } from "../utils/studioSaveDiagnostics";
|
import { createStudioSaveHttpError } from "../utils/studioSaveDiagnostics";
|
||||||
import { buildDomEditPatchTarget, type DomEditSelection } from "../components/editor/domEditing";
|
import { buildDomEditPatchTarget, type DomEditSelection } from "../components/editor/domEditing";
|
||||||
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
||||||
import type { EditHistoryKind } from "../utils/editHistory";
|
import type { EditHistoryKind } from "../utils/editHistory";
|
||||||
|
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
||||||
import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit";
|
import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit";
|
||||||
import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
||||||
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
||||||
@@ -48,17 +49,7 @@ interface RecordEditInput {
|
|||||||
files: Record<string, { before: string; after: string }>;
|
files: Record<string, { before: string; after: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PersistDomEditOperations = (
|
export type { PersistDomEditOperations } from "./domEditCommitTypes";
|
||||||
selection: DomEditSelection,
|
|
||||||
operations: PatchOperation[],
|
|
||||||
options?: {
|
|
||||||
label?: string;
|
|
||||||
coalesceKey?: string;
|
|
||||||
skipRefresh?: boolean;
|
|
||||||
prepareContent?: (html: string, sourceFile: string) => string;
|
|
||||||
shouldSave?: () => boolean;
|
|
||||||
},
|
|
||||||
) => Promise<void>;
|
|
||||||
|
|
||||||
export interface UseDomEditCommitsParams {
|
export interface UseDomEditCommitsParams {
|
||||||
activeCompPath: string | null;
|
activeCompPath: string | null;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { DomEditSelection } from "../components/editor/domEditing";
|
|||||||
import type { PatchOperation } from "../utils/sourcePatcher";
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
||||||
import { trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
import { trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
||||||
import { DomEditSaveQueueOpenError } from "../utils/domEditSaveQueue";
|
import { DomEditSaveQueueOpenError } from "../utils/domEditSaveQueue";
|
||||||
import type { PersistDomEditOperations } from "./useDomEditCommits";
|
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
||||||
|
|
||||||
interface UseDomEditPositionPatchCommitParams {
|
interface UseDomEditPositionPatchCommitParams {
|
||||||
activeCompPath: string | null;
|
activeCompPath: string | null;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
type DomEditSelection,
|
type DomEditSelection,
|
||||||
} from "../components/editor/domEditing";
|
} from "../components/editor/domEditing";
|
||||||
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
||||||
import type { PersistDomEditOperations } from "./useDomEditCommits";
|
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
readFileContent,
|
readFileContent,
|
||||||
isSplitTimeWithinBounds,
|
isSplitTimeWithinBounds,
|
||||||
} from "../utils/timelineElementSplit";
|
} from "../utils/timelineElementSplit";
|
||||||
import type { RecordEditInput } from "./useTimelineEditing";
|
import type { RecordEditInput } from "./timelineEditingHelpers";
|
||||||
|
|
||||||
interface UseRazorSplitOptions {
|
interface UseRazorSplitOptions {
|
||||||
projectId: string | null;
|
projectId: string | null;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./studioHelpers";
|
import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
|
||||||
|
|
||||||
const CLIPBOARD_MARKER = "hyperframes-clipboard:v1";
|
const CLIPBOARD_MARKER = "hyperframes-clipboard:v1";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/** Matches the opening tag of a composition root element (e.g. `<div data-composition-id="main">`). */
|
||||||
|
export const COMPOSITION_ROOT_OPEN_TAG_RE = /<[^>]*data-composition-id="[^"]+"[^>]*>/i;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TimelineElement } from "../player";
|
import type { TimelineElement } from "../player/store/playerStore";
|
||||||
import type { DomEditSelection } from "../components/editor/domEditing";
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
||||||
import type { TimelineAssetKind } from "./timelineAssetDrop";
|
import type { TimelineAssetKind } from "./timelineAssetDrop";
|
||||||
import { roundToCenti } from "./rounding";
|
import { roundToCenti } from "./rounding";
|
||||||
@@ -172,8 +172,7 @@ export function clampNumber(value: number, min: number, max: number): number {
|
|||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Matches the opening tag of a composition root element (`data-composition-id`). */
|
export { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
|
||||||
export const COMPOSITION_ROOT_OPEN_TAG_RE = /<[^>]*data-composition-id="[^"]+"[^>]*>/i;
|
|
||||||
|
|
||||||
export function collectHtmlIds(source: string): string[] {
|
export function collectHtmlIds(source: string): string[] {
|
||||||
return Array.from(source.matchAll(/\bid="([^"]+)"/g), (match) => match[1] ?? "");
|
return Array.from(source.matchAll(/\bid="([^"]+)"/g), (match) => match[1] ?? "");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AUDIO_EXT, IMAGE_EXT, VIDEO_EXT } from "./mediaTypes";
|
import { AUDIO_EXT, IMAGE_EXT, VIDEO_EXT } from "./mediaTypes";
|
||||||
import { roundToCenti } from "./rounding";
|
import { roundToCenti } from "./rounding";
|
||||||
import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./studioHelpers";
|
import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
|
||||||
|
|
||||||
export const TIMELINE_ASSET_MIME = "application/x-hyperframes-asset";
|
export const TIMELINE_ASSET_MIME = "application/x-hyperframes-asset";
|
||||||
export const TIMELINE_BLOCK_MIME = "application/x-hyperframes-block";
|
export const TIMELINE_BLOCK_MIME = "application/x-hyperframes-block";
|
||||||
|
|||||||
Reference in New Issue
Block a user