@hyperframes/studio 0.8.55 → 0.8.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/hyperframes-player-c23wnIfc.js +464 -0
- package/dist/assets/{index-DrHQAuGO.css → index-DKJpHYYd.css} +1 -1
- package/dist/assets/{index-BEIRytvV.js → index-DgkAzx3n.js} +1 -1
- package/dist/assets/{index-DaNpNFJC.js → index-IDNr3X1H.js} +1 -1
- package/dist/assets/{index-TcBZyzNA.js → index-gbOCDCuZ.js} +202 -202
- package/dist/index.d.ts +111 -17
- package/dist/index.html +2 -2
- package/dist/index.js +21534 -20683
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/App.tsx +18 -1
- package/src/components/EditorShell.readOnlyHostMount.test.tsx +116 -0
- package/src/components/EditorShell.selectionSync.test.tsx +57 -67
- package/src/components/EditorShell.tsx +62 -51
- package/src/components/EditorShellTestMocks.ts +46 -0
- package/src/components/ProjectUnreachableBanner.test.tsx +113 -0
- package/src/components/ProjectUnreachableBanner.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +19 -55
- package/src/components/editor/DomEditOverlayHover.test.tsx +1 -15
- package/src/components/editor/DomEditSelectionChrome.tsx +11 -9
- package/src/components/editor/GridOverlay.tsx +12 -23
- package/src/components/editor/MotionPathOverlay.test.tsx +66 -0
- package/src/components/editor/MotionPathOverlay.tsx +3 -1
- package/src/components/editor/PreviewGuides.test.tsx +6 -1
- package/src/components/editor/PreviewGuides.tsx +8 -10
- package/src/components/editor/PreviewOverlayProvider.test.tsx +90 -0
- package/src/components/editor/PreviewOverlayProvider.tsx +86 -0
- package/src/components/editor/PropertyPanel.deselect.test.tsx +71 -0
- package/src/components/editor/PropertyPanel.tsx +4 -3
- package/src/components/editor/SnapToolbar.test.tsx +26 -12
- package/src/components/editor/SnapToolbar.tsx +30 -59
- package/src/components/editor/domEditOverlayTestMocks.ts +16 -0
- package/src/components/editor/previewReadOnly.overlay.test.tsx +260 -0
- package/src/components/editor/previewReadOnlyContext.tsx +59 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +9 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +46 -12
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -1
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +3 -13
- package/src/components/editor/useDomEditNudge.ts +9 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +4 -0
- package/src/components/editor/useOffCanvasIndicators.ts +50 -0
- package/src/components/editor/usePreviewCompositionRect.test.tsx +19 -13
- package/src/components/editor/usePreviewCompositionRect.ts +12 -12
- package/src/components/editor/useVolumeAutomation.test.tsx +68 -0
- package/src/components/editor/useVolumeAutomation.ts +57 -16
- package/src/components/nle/NLEContext.tsx +40 -4
- package/src/components/nle/NLEPreview.test.ts +66 -1
- package/src/components/nle/NLEPreview.tsx +88 -44
- package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +4 -35
- package/src/components/nle/PreviewOverlays.tsx +19 -24
- package/src/components/nle/PreviewOverlaysTestMocks.ts +40 -0
- package/src/components/nle/PreviewPane.tsx +17 -2
- package/src/components/nle/TimelinePane.tsx +5 -1
- package/src/components/nle/previewReadOnly.test.tsx +129 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -6
- package/src/components/nle/usePreviewBlockDrop.ts +3 -1
- package/src/hooks/appHotkeysDispatch.ts +16 -1
- package/src/hooks/newTweenTargetHooks.test.tsx +1 -0
- package/src/hooks/timelineAudioGroupCreate.ts +3 -4
- package/src/hooks/timelineTrackVisibility.ts +7 -8
- package/src/hooks/useAppHotkeys.test.ts +71 -3
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
- package/src/hooks/useAppHotkeys.ts +4 -0
- package/src/hooks/useCompositionDimensions.test.tsx +38 -0
- package/src/hooks/useCompositionDimensions.ts +17 -1
- package/src/hooks/useDomEditAttributeCommits.test.ts +23 -0
- package/src/hooks/useDomEditAttributeCommits.ts +14 -3
- package/src/hooks/useDomEditCommits.test.tsx +1 -0
- package/src/hooks/useDomEditCommits.ts +3 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -0
- package/src/hooks/useDomEditSession.ts +7 -4
- package/src/hooks/useDomEditWiring.ts +40 -40
- package/src/hooks/useDomGeometryCommits.test.tsx +85 -2
- package/src/hooks/useDomGeometryCommits.ts +26 -20
- package/src/hooks/useFileTree.test.tsx +56 -0
- package/src/hooks/useFileTree.ts +8 -1
- package/src/hooks/useGestureCommit.test.tsx +29 -0
- package/src/hooks/useGestureCommit.ts +23 -0
- package/src/hooks/useInlineTextEdit.test.tsx +23 -2
- package/src/hooks/useInlineTextEdit.ts +11 -2
- package/src/hooks/useSdkSession.lifecycle.test.tsx +80 -0
- package/src/hooks/useSdkSession.ts +46 -10
- package/src/hooks/useServerConnection.hashProject.test.tsx +303 -0
- package/src/hooks/useServerConnection.ts +81 -8
- package/src/hooks/useTimelineAssetDropOps.test.tsx +3 -4
- package/src/hooks/useTimelineAssetDropOps.ts +1 -6
- package/src/hooks/useTimelineEditing.test.tsx +10 -7
- package/src/hooks/useTimelineEditing.ts +2 -7
- package/src/index.ts +12 -0
- package/src/player/components/Player.test.ts +141 -13
- package/src/player/components/Player.tsx +58 -2
- package/src/player/components/ShortcutsPanel.test.tsx +4 -10
- package/src/player/components/Timeline.tsx +23 -573
- package/src/player/components/TimelineCanvas.tsx +9 -30
- package/src/player/components/TimelineClip.test.tsx +52 -0
- package/src/player/components/TimelineClip.tsx +13 -2
- package/src/player/components/TimelineOverlays.test.ts +67 -32
- package/src/player/components/TimelineOverlays.tsx +46 -107
- package/src/player/components/TimelineProvider.tsx +191 -0
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/testRootHarness.ts +18 -0
- package/src/player/components/timelineMotionStyles.test.ts +3 -3
- package/src/player/components/timelineProviderStateBuilders.test.ts +109 -0
- package/src/player/components/timelineProviderStateBuilders.ts +83 -0
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +34 -30
- package/src/player/components/timelineTrackDisplay.ts +1 -4
- package/src/player/components/useTimelineClipContextMenu.ts +1 -1
- package/src/player/components/useTimelineOverlaysState.ts +100 -0
- package/src/player/components/useTimelineProviderState.tsx +583 -0
- package/src/player/components/useTimelineTrackDerivations.ts +2 -2
- package/src/player/hooks/playbackAdapterResolution.ts +98 -0
- package/src/player/hooks/timelinePlayerTestHarness.ts +120 -0
- package/src/player/hooks/timelineSyncHydration.test.ts +0 -18
- package/src/player/hooks/timelineSyncHydration.ts +0 -23
- package/src/player/hooks/usePreviewFirstFrameTelemetry.ts +51 -0
- package/src/player/hooks/useShadowPreviewReload.ts +183 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +8 -88
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +457 -0
- package/src/player/hooks/useTimelinePlayer.ts +65 -99
- package/src/player/hooks/useTimelineSyncCallbacks.ts +159 -91
- package/src/player/store/playerStore.ts +0 -4
- package/src/player/store/timelineElement.ts +2 -4
- package/src/player/store/timelineResetState.ts +0 -1
- package/src/styles/hex-baseline.json +3 -4
- package/src/styles/studio.css +7 -7
- package/src/styles/theme.css +29 -0
- package/src/telemetry/events.test.ts +18 -0
- package/src/telemetry/events.ts +10 -0
- package/src/telemetry/system.test.ts +49 -0
- package/src/telemetry/system.ts +6 -0
- package/src/telemetry/tabId.test.ts +43 -0
- package/src/telemetry/tabId.ts +44 -0
- package/src/utils/previewCompositionSize.ts +26 -0
- package/src/utils/reloadDebug.ts +2 -4
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioTelemetry.test.ts +14 -0
- package/src/utils/studioTelemetry.ts +4 -0
- package/dist/assets/hyperframes-player-BjAGc2ID.js +0 -464
- package/src/player/hooks/useTimelineRowElements.test.ts +0 -28
- package/src/player/hooks/useTimelineRowElements.ts +0 -17
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { ProjectUnreachableBanner } from "./ProjectUnreachableBanner";
|
|
6
|
+
|
|
7
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
8
|
+
|
|
9
|
+
let root: Root | null = null;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
if (root) act(() => root?.unmount());
|
|
13
|
+
root = null;
|
|
14
|
+
document.body.replaceChildren();
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
vi.unstubAllGlobals();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function stubProjects(projects: unknown): void {
|
|
20
|
+
vi.stubGlobal(
|
|
21
|
+
"fetch",
|
|
22
|
+
vi.fn(async () => new Response(JSON.stringify({ projects }), { status: 200 })),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function renderBanner(projectId: string): Promise<string> {
|
|
27
|
+
const host = document.createElement("div");
|
|
28
|
+
document.body.append(host);
|
|
29
|
+
root = createRoot(host);
|
|
30
|
+
await act(async () => {
|
|
31
|
+
root?.render(<ProjectUnreachableBanner projectId={projectId} />);
|
|
32
|
+
});
|
|
33
|
+
await act(async () => {
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
35
|
+
});
|
|
36
|
+
return host.textContent ?? "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("ProjectUnreachableBanner", () => {
|
|
40
|
+
// The production case: the CLI host serves exactly one project, and it is not
|
|
41
|
+
// this tab's. Naming both sides is the whole point — the user needs to know
|
|
42
|
+
// their project still exists and which one this Studio actually opened.
|
|
43
|
+
it("names both projects when this Studio serves a different one", async () => {
|
|
44
|
+
stubProjects([{ id: "out-video1", title: "out-video1" }]);
|
|
45
|
+
const text = await renderBanner("out-fb1");
|
|
46
|
+
|
|
47
|
+
expect(text).toContain("out-video1");
|
|
48
|
+
expect(text).toContain("out-fb1");
|
|
49
|
+
expect(text).toContain("This Studio is serving");
|
|
50
|
+
expect(text).not.toContain("renamed");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// With several projects a 404 cannot be told apart from a rename or a
|
|
54
|
+
// deletion, so the banner must not claim a cause. "Could be gone" is the
|
|
55
|
+
// honest ceiling, and it is a weaker claim than "is gone".
|
|
56
|
+
it("does not claim a cause when several projects are served", async () => {
|
|
57
|
+
stubProjects([
|
|
58
|
+
{ id: "a", title: "a" },
|
|
59
|
+
{ id: "b", title: "b" },
|
|
60
|
+
]);
|
|
61
|
+
const text = await renderBanner("out-fb1");
|
|
62
|
+
|
|
63
|
+
expect(text).toContain("renamed, moved, or deleted");
|
|
64
|
+
expect(text).not.toContain("This Studio is serving");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// A server that answers with this tab's own project while the read 404s is
|
|
68
|
+
// not the stale-tab case, so the single-project wording would be false.
|
|
69
|
+
it("falls back to the general wording when the served project is this one", async () => {
|
|
70
|
+
stubProjects([{ id: "out-fb1", title: "out-fb1" }]);
|
|
71
|
+
const text = await renderBanner("out-fb1");
|
|
72
|
+
|
|
73
|
+
expect(text).toContain("renamed, moved, or deleted");
|
|
74
|
+
expect(text).not.toContain("This Studio is serving");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("says nothing it cannot back up when the project list cannot be read", async () => {
|
|
78
|
+
vi.stubGlobal(
|
|
79
|
+
"fetch",
|
|
80
|
+
vi.fn(async () => new Response("boom", { status: 500 })),
|
|
81
|
+
);
|
|
82
|
+
const text = await renderBanner("out-fb1");
|
|
83
|
+
|
|
84
|
+
expect(text).toContain("renamed, moved, or deleted");
|
|
85
|
+
expect(text).not.toContain("This Studio is serving");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("renders nothing until the project list resolves", async () => {
|
|
89
|
+
let resolveList: ((value: Response) => void) | undefined;
|
|
90
|
+
vi.stubGlobal(
|
|
91
|
+
"fetch",
|
|
92
|
+
vi.fn(
|
|
93
|
+
() =>
|
|
94
|
+
new Promise<Response>((resolve) => {
|
|
95
|
+
resolveList = resolve;
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
const host = document.createElement("div");
|
|
100
|
+
document.body.append(host);
|
|
101
|
+
root = createRoot(host);
|
|
102
|
+
await act(async () => {
|
|
103
|
+
root?.render(<ProjectUnreachableBanner projectId="out-fb1" />);
|
|
104
|
+
});
|
|
105
|
+
expect(host.textContent).toBe("");
|
|
106
|
+
|
|
107
|
+
await act(async () => {
|
|
108
|
+
resolveList?.(new Response(JSON.stringify({ projects: [{ id: "other" }] }), { status: 200 }));
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
110
|
+
});
|
|
111
|
+
expect(host.textContent).toContain("This Studio is serving");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface ServedProject {
|
|
4
|
+
id: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ProjectUnreachableBannerProps {
|
|
9
|
+
/** The project this tab was opened for and the server cannot resolve. */
|
|
10
|
+
projectId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shown when the running Studio cannot resolve this tab's project id.
|
|
15
|
+
*
|
|
16
|
+
* Why this exists: a tab whose project the server does not serve fails every
|
|
17
|
+
* read with a 404, so every edit silently takes the server path and nothing
|
|
18
|
+
* lands. The user sees a normal-looking editor that does not save. In the
|
|
19
|
+
* CLI-embedded host — the only host that reports telemetry, since
|
|
20
|
+
* `telemetry/policy.ts` suppresses Vite dev — the cause is almost always that
|
|
21
|
+
* this Studio is serving a *different* project: `hyperframes preview` reuses
|
|
22
|
+
* port 3002, so starting it on another folder takes the port from under an
|
|
23
|
+
* open tab, and the project it was pointed at is untouched on disk.
|
|
24
|
+
*
|
|
25
|
+
* The wording is deliberately narrower than "this project is gone". We can see
|
|
26
|
+
* which project this server serves; we cannot see why it does not serve this
|
|
27
|
+
* one, and nothing here is lost. See
|
|
28
|
+
* `docs/hyperframes/plans/sdk/2026-09-20-stale-project-id-remaining-doors-plan.md` §C.
|
|
29
|
+
*/
|
|
30
|
+
export function ProjectUnreachableBanner({ projectId }: ProjectUnreachableBannerProps) {
|
|
31
|
+
const [served, setServed] = useState<ServedProject[] | null>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
// A list we could not read is not evidence of anything, so a failed fetch
|
|
36
|
+
// leaves `served` empty and the banner falls back to the vague wording
|
|
37
|
+
// rather than claiming this Studio serves nothing.
|
|
38
|
+
fetch("/api/projects")
|
|
39
|
+
.then((res) => (res.ok ? res.json() : { projects: [] }))
|
|
40
|
+
.then((data: { projects?: ServedProject[] }) => {
|
|
41
|
+
if (!cancelled) setServed(data.projects ?? []);
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
if (!cancelled) setServed([]);
|
|
45
|
+
});
|
|
46
|
+
return () => {
|
|
47
|
+
cancelled = true;
|
|
48
|
+
};
|
|
49
|
+
}, [projectId]);
|
|
50
|
+
|
|
51
|
+
// Withheld until the list arrives: which wording applies is a claim about
|
|
52
|
+
// what this server serves, and guessing it would flash the wrong one.
|
|
53
|
+
if (served === null) return null;
|
|
54
|
+
|
|
55
|
+
// Exactly one project served, and it is not this tab's. Under the CLI host
|
|
56
|
+
// that is the whole truth, and it names both sides. With several projects we
|
|
57
|
+
// cannot tell a rename from a deletion, so we do not pretend to.
|
|
58
|
+
const soleProject = served.length === 1 && served[0].id !== projectId ? served[0] : null;
|
|
59
|
+
const servedLabel = soleProject?.title ?? soleProject?.id;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
role="alert"
|
|
64
|
+
className="hf-backdrop-in absolute left-1/2 top-14 z-92 flex max-w-[calc(100vw-32px)] -translate-x-1/2 items-center gap-3 rounded-md border border-amber-500/30 bg-amber-950/85 px-4 py-2 text-[12px] font-medium text-amber-100 shadow-lg shadow-black/30"
|
|
65
|
+
>
|
|
66
|
+
{soleProject ? (
|
|
67
|
+
<>
|
|
68
|
+
<span>
|
|
69
|
+
This Studio is serving <strong>{servedLabel}</strong>. This tab was opened for{" "}
|
|
70
|
+
<strong>{projectId}</strong>.
|
|
71
|
+
</span>
|
|
72
|
+
{/* Reloading re-runs the mount-time hash check, which rewrites the hash
|
|
73
|
+
to the project this server actually serves. That is the same
|
|
74
|
+
fallback a fresh load performs, so the button promises nothing
|
|
75
|
+
beyond what a reload already does. */}
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => window.location.reload()}
|
|
79
|
+
className="shrink-0 rounded-sm border border-amber-300/20 px-2 py-1 text-[11px] text-amber-100 transition-colors hover:bg-amber-400/10 active:scale-[0.98]"
|
|
80
|
+
>
|
|
81
|
+
Open {servedLabel}
|
|
82
|
+
</button>
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
<span>Couldn't open this project — it may have been renamed, moved, or deleted.</span>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useEffect, useMemo, useRef,
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, type RefObject } from "react";
|
|
2
2
|
import { type DomEditSelection } from "./domEditing";
|
|
3
3
|
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
|
|
4
4
|
import { useMarqueeGestures } from "./marqueeCommit";
|
|
@@ -16,24 +16,23 @@ import {
|
|
|
16
16
|
resolveShiftClickCandidate,
|
|
17
17
|
} from "./domEditOverlayGestures";
|
|
18
18
|
import { useDomEditOverlayRects } from "./useDomEditOverlayRects";
|
|
19
|
-
import { ChildRectOutlines, OffCanvasIndicators
|
|
19
|
+
import { ChildRectOutlines, OffCanvasIndicators } from "./OffCanvasIndicators";
|
|
20
20
|
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
21
21
|
import { useDomEditNudge } from "./useDomEditNudge";
|
|
22
22
|
import { SnapGuideOverlay, type SnapGuidesState } from "./SnapGuideOverlay";
|
|
23
|
-
import { GridOverlay } from "./GridOverlay";
|
|
24
23
|
import type { GestureRecordingState } from "./GestureRecordControl";
|
|
25
24
|
import { DomEditGroupChrome, DomEditSelectionChrome } from "./DomEditSelectionChrome";
|
|
26
25
|
import { hugRectForElement } from "./domEditOverlayCrop";
|
|
27
26
|
import { useCropOverlay } from "../../hooks/useCropOverlay";
|
|
28
27
|
import { readDomEditSelectionShapeStyles, resolveBoxChromeClass } from "./domEditOverlayShape";
|
|
29
28
|
import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
|
|
30
|
-
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
31
|
-
import { startOffCanvasIndicatorRefresh } from "./offCanvasIndicatorRefresh";
|
|
32
29
|
import { CanvasContextMenu } from "./CanvasContextMenu";
|
|
33
30
|
import { useInlineTextEditing } from "./useInlineTextEditing";
|
|
31
|
+
import { usePreviewReadOnly } from "./previewReadOnlyContext";
|
|
34
32
|
import type { ZOrderAction, ZOrderPatch } from "./canvasContextMenuZOrder";
|
|
35
33
|
import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
|
|
36
34
|
import { logSelect } from "../../utils/selectDebug";
|
|
35
|
+
import { useOffCanvasIndicators } from "./useOffCanvasIndicators";
|
|
37
36
|
|
|
38
37
|
// Re-exports for external consumers — preserving existing import paths.
|
|
39
38
|
export {
|
|
@@ -84,8 +83,6 @@ interface DomEditOverlayProps {
|
|
|
84
83
|
) => Promise<void> | void;
|
|
85
84
|
onRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void> | void;
|
|
86
85
|
onStyleCommit?: (property: string, value: string) => Promise<unknown> | void;
|
|
87
|
-
gridVisible?: boolean;
|
|
88
|
-
gridSpacing?: number;
|
|
89
86
|
recordingState?: GestureRecordingState;
|
|
90
87
|
onToggleRecording?: () => void;
|
|
91
88
|
onMarqueeSelect?: (selections: DomEditSelection[], additive: boolean) => void;
|
|
@@ -125,8 +122,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
125
122
|
onCanvasPointerLeave,
|
|
126
123
|
onSelectionChange,
|
|
127
124
|
onBlockedMove,
|
|
128
|
-
gridVisible = false,
|
|
129
|
-
gridSpacing = 50,
|
|
130
125
|
onManualDragStart,
|
|
131
126
|
onPathOffsetCommit,
|
|
132
127
|
onGroupPathOffsetCommit,
|
|
@@ -137,6 +132,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
137
132
|
onDeleteSelection,
|
|
138
133
|
onApplyZIndex,
|
|
139
134
|
}: DomEditOverlayProps) {
|
|
135
|
+
const readOnly = usePreviewReadOnly();
|
|
140
136
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
|
141
137
|
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
142
138
|
const onMarqueeSelectRef = useRef(onMarqueeSelect);
|
|
@@ -220,39 +216,13 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
220
216
|
const boxClipPath = hasCropInsets ? undefined : selectionShapeStyles.clipPath;
|
|
221
217
|
const boxChromeClass = resolveBoxChromeClass(Boolean(cropOutlineInsetPx), boxClipPath);
|
|
222
218
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const offCanvasObservedDocRef = useRef<Document | null>(null);
|
|
231
|
-
|
|
232
|
-
// Positions depend on live iframe layout, not selection — the selected-element
|
|
233
|
-
// suppression is a render-time filter, so selection/groupSelections stay out
|
|
234
|
-
// of the geometry walk.
|
|
235
|
-
useMountEffect(() =>
|
|
236
|
-
startOffCanvasIndicatorRefresh({
|
|
237
|
-
iframeRef,
|
|
238
|
-
overlayRef,
|
|
239
|
-
compRectRef,
|
|
240
|
-
activeCompositionPathRef,
|
|
241
|
-
dirtyRef: offCanvasDirtyRef,
|
|
242
|
-
sigRef: offCanvasSigRef,
|
|
243
|
-
observerRef: offCanvasObserverRef,
|
|
244
|
-
observedDocRef: offCanvasObservedDocRef,
|
|
245
|
-
elementsRef: offCanvasElementsRef,
|
|
246
|
-
setRects: setOffCanvasRects,
|
|
247
|
-
}),
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
// Switching compositions may not swap the iframe document (so the observer's
|
|
251
|
-
// doc-swap detection wouldn't fire) yet changes which elements are off-canvas.
|
|
252
|
-
// Force a recompute explicitly on comp change.
|
|
253
|
-
useEffect(() => {
|
|
254
|
-
offCanvasDirtyRef.current = true;
|
|
255
|
-
}, [activeCompositionPath]);
|
|
219
|
+
const { offCanvasRects, offCanvasElementsRef } = useOffCanvasIndicators({
|
|
220
|
+
iframeRef,
|
|
221
|
+
overlayRef,
|
|
222
|
+
compRectRef,
|
|
223
|
+
activeCompositionPathRef,
|
|
224
|
+
activeCompositionPath,
|
|
225
|
+
});
|
|
256
226
|
|
|
257
227
|
const gestures = createDomEditOverlayGestureHandlers({
|
|
258
228
|
overlayRef,
|
|
@@ -280,12 +250,16 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
280
250
|
snapGuidesRef,
|
|
281
251
|
});
|
|
282
252
|
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (readOnly) gestures.clearPointerState(selectionRef);
|
|
255
|
+
}, [gestures, readOnly, selectionRef]);
|
|
256
|
+
|
|
283
257
|
// Arrow-key nudge (1px, Shift = 10px) — commits through the same
|
|
284
258
|
// path-offset callbacks as a drag, one undo entry per key burst.
|
|
285
259
|
const { flushNudge } = useDomEditNudge({
|
|
286
260
|
selection,
|
|
287
261
|
groupSelections,
|
|
288
|
-
allowCanvasMovement,
|
|
262
|
+
allowCanvasMovement: allowCanvasMovement && !readOnly,
|
|
289
263
|
selectionRef,
|
|
290
264
|
overlayRectRef,
|
|
291
265
|
groupOverlayItemsRef,
|
|
@@ -558,7 +532,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
558
532
|
selection={contextMenu.sel}
|
|
559
533
|
onClose={closeContextMenu}
|
|
560
534
|
onDelete={
|
|
561
|
-
onDeleteSelection
|
|
535
|
+
onDeleteSelection && !readOnly
|
|
562
536
|
? (sel) => {
|
|
563
537
|
closeContextMenu();
|
|
564
538
|
onDeleteSelection(sel);
|
|
@@ -566,7 +540,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
566
540
|
: undefined
|
|
567
541
|
}
|
|
568
542
|
onApplyZIndex={
|
|
569
|
-
onApplyZIndex
|
|
543
|
+
onApplyZIndex && !readOnly
|
|
570
544
|
? (patches, action, crossed) => {
|
|
571
545
|
onApplyZIndex(contextMenu.sel, patches, action, crossed);
|
|
572
546
|
}
|
|
@@ -576,16 +550,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
576
550
|
/>
|
|
577
551
|
)}
|
|
578
552
|
<ZOrderCrossedFlash rect={zOrderFlashRect} />
|
|
579
|
-
<GridOverlay
|
|
580
|
-
visible={gridVisible}
|
|
581
|
-
spacing={gridSpacing}
|
|
582
|
-
scaleX={compRect.scaleX}
|
|
583
|
-
scaleY={compRect.scaleY}
|
|
584
|
-
compositionLeft={compRect.left}
|
|
585
|
-
compositionTop={compRect.top}
|
|
586
|
-
compositionWidth={compRect.width}
|
|
587
|
-
compositionHeight={compRect.height}
|
|
588
|
-
/>
|
|
589
553
|
<SnapGuideOverlay
|
|
590
554
|
snapGuidesRef={snapGuidesRef}
|
|
591
555
|
compositionLeft={compRect.left}
|
|
@@ -4,6 +4,7 @@ import React, { act } from "react";
|
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { expect, it, vi } from "vitest";
|
|
6
6
|
import type { DomEditSelection } from "./domEditing";
|
|
7
|
+
import "./domEditOverlayTestMocks";
|
|
7
8
|
import { DomEditOverlay } from "./DomEditOverlay";
|
|
8
9
|
|
|
9
10
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -33,21 +34,6 @@ vi.mock("./useDomEditOverlayRects", () => ({
|
|
|
33
34
|
}),
|
|
34
35
|
}));
|
|
35
36
|
|
|
36
|
-
vi.mock("./useDomEditCompositionRect", () => ({
|
|
37
|
-
useDomEditCompositionRect: () => ({
|
|
38
|
-
left: 0,
|
|
39
|
-
top: 0,
|
|
40
|
-
width: 800,
|
|
41
|
-
height: 450,
|
|
42
|
-
scaleX: 1,
|
|
43
|
-
scaleY: 1,
|
|
44
|
-
}),
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
vi.mock("./offCanvasIndicatorRefresh", () => ({
|
|
48
|
-
startOffCanvasIndicatorRefresh: () => () => undefined,
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
37
|
function renderHover(angle: number): string {
|
|
52
38
|
hoverState.angle = angle;
|
|
53
39
|
const host = document.createElement("div");
|
|
@@ -6,6 +6,7 @@ import type { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGes
|
|
|
6
6
|
import { DomEditCropHandles } from "./DomEditCropHandles";
|
|
7
7
|
import { DomEditRotateHandle } from "./DomEditRotateHandle";
|
|
8
8
|
import { resolveRotatedResizeCursor } from "./domEditResizeLocal";
|
|
9
|
+
import { usePreviewReadOnly } from "./previewReadOnlyContext";
|
|
9
10
|
|
|
10
11
|
// Corner resize handles, Canva-style: one per corner, diagonal cursors.
|
|
11
12
|
// Corners scale about the element center; the translate keeps the center
|
|
@@ -75,6 +76,7 @@ export function DomEditGroupChrome({
|
|
|
75
76
|
onBoxMouseDown,
|
|
76
77
|
onBoxClick,
|
|
77
78
|
}: DomEditGroupChromeProps) {
|
|
79
|
+
const canManipulate = allowCanvasMovement && !usePreviewReadOnly();
|
|
78
80
|
return (
|
|
79
81
|
<>
|
|
80
82
|
{groupOverlayItems.map((item) => (
|
|
@@ -98,10 +100,10 @@ export function DomEditGroupChrome({
|
|
|
98
100
|
top: groupBounds.top,
|
|
99
101
|
width: groupBounds.width,
|
|
100
102
|
height: groupBounds.height,
|
|
101
|
-
cursor:
|
|
103
|
+
cursor: canManipulate && groupCanMove ? "move" : "default",
|
|
102
104
|
}}
|
|
103
105
|
onPointerDown={(e) => {
|
|
104
|
-
if (!
|
|
106
|
+
if (!canManipulate || !groupCanMove || e.shiftKey) return;
|
|
105
107
|
gestures.startGroupDrag(e);
|
|
106
108
|
}}
|
|
107
109
|
onMouseDown={onBoxMouseDown}
|
|
@@ -164,6 +166,8 @@ export function DomEditSelectionChrome({
|
|
|
164
166
|
// into: left interactive, it swallows every press, so the caret can never be
|
|
165
167
|
// moved and characters can never be selected by dragging.
|
|
166
168
|
const editing = inlineText?.editing ?? false;
|
|
169
|
+
const readOnly = usePreviewReadOnly();
|
|
170
|
+
const canManipulate = allowCanvasMovement && !readOnly;
|
|
167
171
|
|
|
168
172
|
return (
|
|
169
173
|
<>
|
|
@@ -174,7 +178,7 @@ export function DomEditSelectionChrome({
|
|
|
174
178
|
transform: overlayRect.angle ? `rotate(${overlayRect.angle}deg)` : undefined,
|
|
175
179
|
}}
|
|
176
180
|
>
|
|
177
|
-
{
|
|
181
|
+
{canManipulate && !editing && selection.capabilities.canApplyManualRotation && (
|
|
178
182
|
<DomEditRotateHandle
|
|
179
183
|
overlayRect={overlayRect}
|
|
180
184
|
cropOutlineInsetPx={cropOutlineInsetPx}
|
|
@@ -196,9 +200,7 @@ export function DomEditSelectionChrome({
|
|
|
196
200
|
height: overlayRect.height,
|
|
197
201
|
clipPath: boxClipPath,
|
|
198
202
|
cursor:
|
|
199
|
-
|
|
200
|
-
? "move"
|
|
201
|
-
: "default",
|
|
203
|
+
canManipulate && selection.capabilities.canApplyManualOffset ? "move" : "default",
|
|
202
204
|
}}
|
|
203
205
|
onPointerDown={(e) => {
|
|
204
206
|
// A second press opens the element's text for editing, and must be
|
|
@@ -211,7 +213,7 @@ export function DomEditSelectionChrome({
|
|
|
211
213
|
e.stopPropagation();
|
|
212
214
|
return;
|
|
213
215
|
}
|
|
214
|
-
if (!
|
|
216
|
+
if (!canManipulate || e.shiftKey) return;
|
|
215
217
|
if (selection.capabilities.canApplyManualOffset) {
|
|
216
218
|
gestures.startGesture("drag", e);
|
|
217
219
|
return;
|
|
@@ -245,7 +247,7 @@ export function DomEditSelectionChrome({
|
|
|
245
247
|
children, so they paint strictly above the box border. Each handle
|
|
246
248
|
is positioned relative to the overlay container using the
|
|
247
249
|
overlayRect origin, matching the old child-relative offsets. */}
|
|
248
|
-
{
|
|
250
|
+
{canManipulate &&
|
|
249
251
|
!editing &&
|
|
250
252
|
selection.capabilities.canApplyManualSize &&
|
|
251
253
|
RESIZE_HANDLE_DEFS.map((def) =>
|
|
@@ -271,7 +273,7 @@ export function DomEditSelectionChrome({
|
|
|
271
273
|
</div>
|
|
272
274
|
{/* Crop owns its element-local oriented frame. Keep it outside the chrome's
|
|
273
275
|
rotated plane or a rotated selection applies the angle twice. */}
|
|
274
|
-
{selection.capabilities.canCrop && !editing && groupSelectionCount <= 1 && (
|
|
276
|
+
{selection.capabilities.canCrop && !editing && !readOnly && groupSelectionCount <= 1 && (
|
|
275
277
|
<DomEditCropHandles
|
|
276
278
|
selection={selection}
|
|
277
279
|
overlayRect={overlayRect}
|
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
// fallow-ignore-file unused-file
|
|
2
1
|
import { memo } from "react";
|
|
2
|
+
import { usePreviewOverlayContext } from "./PreviewOverlayProvider";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// fallow-ignore-next-line complexity
|
|
16
|
-
export const GridOverlay = memo(function GridOverlay({
|
|
17
|
-
visible,
|
|
18
|
-
spacing,
|
|
19
|
-
scaleX,
|
|
20
|
-
scaleY,
|
|
21
|
-
compositionLeft,
|
|
22
|
-
compositionTop,
|
|
23
|
-
compositionWidth,
|
|
24
|
-
compositionHeight,
|
|
25
|
-
}: GridOverlayProps) {
|
|
4
|
+
export const GridOverlay = memo(function GridOverlay() {
|
|
5
|
+
const { state } = usePreviewOverlayContext();
|
|
6
|
+
const { gridVisible: visible, gridSpacing: spacing } = state.snapPrefs;
|
|
7
|
+
const {
|
|
8
|
+
scaleX,
|
|
9
|
+
scaleY,
|
|
10
|
+
left: compositionLeft,
|
|
11
|
+
top: compositionTop,
|
|
12
|
+
width: compositionWidth,
|
|
13
|
+
height: compositionHeight,
|
|
14
|
+
} = state.compositionRect;
|
|
26
15
|
if (!visible || spacing <= 0) return null;
|
|
27
16
|
|
|
28
17
|
const overlaySpacingX = spacing * scaleX;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { expect, it, vi } from "vitest";
|
|
5
|
+
import { MotionPathOverlay } from "./MotionPathOverlay";
|
|
6
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
7
|
+
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
8
|
+
import type { DomEditSelection } from "./domEditing";
|
|
9
|
+
|
|
10
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
11
|
+
|
|
12
|
+
vi.mock("../../contexts/DomEditContext", () => ({
|
|
13
|
+
useDomEditContext: () => ({ selectedGsapAnimations: [] }),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("./motionPathSelection", () => ({
|
|
16
|
+
selectorFor: () => "#box",
|
|
17
|
+
editableAnimationId: () => null,
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("./useMotionPathData", async (importOriginal) => ({
|
|
20
|
+
...(await importOriginal<typeof import("./useMotionPathData")>()),
|
|
21
|
+
useMotionPathData: () => ({
|
|
22
|
+
rect: null,
|
|
23
|
+
geometry: null,
|
|
24
|
+
geometryResolved: true,
|
|
25
|
+
visibleInPreview: true,
|
|
26
|
+
home: null,
|
|
27
|
+
pScale: 1,
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
it("re-evaluates set-destination availability when a reload swaps the live iframe", () => {
|
|
32
|
+
const host = document.createElement("div");
|
|
33
|
+
const before = document.createElement("iframe");
|
|
34
|
+
const after = document.createElement("iframe");
|
|
35
|
+
document.body.append(host, before, after);
|
|
36
|
+
Reflect.set(after.contentWindow as object, "MotionPathPlugin", {});
|
|
37
|
+
const ref = { current: before };
|
|
38
|
+
const root = createRoot(host);
|
|
39
|
+
const selection = { element: document.createElement("div") } as unknown as DomEditSelection;
|
|
40
|
+
try {
|
|
41
|
+
act(() => usePreviewIframeStore.getState().setIframe(before));
|
|
42
|
+
act(() =>
|
|
43
|
+
root.render(
|
|
44
|
+
<MotionPathOverlay
|
|
45
|
+
iframeRef={ref}
|
|
46
|
+
selection={selection}
|
|
47
|
+
compositionSize={{ width: 1920, height: 1080 }}
|
|
48
|
+
isPlaying={false}
|
|
49
|
+
/>,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
expect(usePlayerStore.getState().motionPathCreateAvailable).toBe(false);
|
|
53
|
+
|
|
54
|
+
act(() => {
|
|
55
|
+
ref.current = after;
|
|
56
|
+
usePreviewIframeStore.getState().setIframe(after);
|
|
57
|
+
});
|
|
58
|
+
expect(usePlayerStore.getState().motionPathCreateAvailable).toBe(true);
|
|
59
|
+
} finally {
|
|
60
|
+
act(() => root.unmount());
|
|
61
|
+
usePreviewIframeStore.setState({ iframe: null });
|
|
62
|
+
host.remove();
|
|
63
|
+
before.remove();
|
|
64
|
+
after.remove();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -3,6 +3,7 @@ import { memo, useEffect, useRef, useState, type RefObject } from "react";
|
|
|
3
3
|
import type { DomEditSelection } from "./domEditing";
|
|
4
4
|
import { useDomEditContext } from "../../contexts/DomEditContext";
|
|
5
5
|
import { usePlayerStore } from "../../player/store/playerStore";
|
|
6
|
+
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
6
7
|
import { parkPlayheadOnKeyframe } from "../../hooks/gsapDragCommit";
|
|
7
8
|
import { commitWholePropertyOffset } from "../../hooks/gsapWholePropertyOffsetCommit";
|
|
8
9
|
import { nearestPointOnPath, type MotionNodeRef } from "./motionPathGeometry";
|
|
@@ -141,7 +142,8 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
|
|
|
141
142
|
// No one-element selector means the path could only be authored onto the
|
|
142
143
|
// element's class siblings, so the toolbar toggle stays hidden instead of
|
|
143
144
|
// arming a press that the effect below would silently drop.
|
|
144
|
-
const
|
|
145
|
+
const livePreviewIframe = useLivePreviewIframe();
|
|
146
|
+
const canCreate = createMode && !!createSelector && hasMotionPathPlugin(livePreviewIframe);
|
|
145
147
|
|
|
146
148
|
// Publish whether the selected element can take a path so the preview toolbar
|
|
147
149
|
// shows its "set destination" toggle. Drops to false when this overlay unmounts
|
|
@@ -4,6 +4,7 @@ import React, { act } from "react";
|
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { PreviewGuides } from "./PreviewGuides";
|
|
7
|
+
import { PreviewOverlayProvider } from "./PreviewOverlayProvider";
|
|
7
8
|
import { usePreviewGuidesStore } from "./previewGuidesStore";
|
|
8
9
|
|
|
9
10
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -26,7 +27,11 @@ function render(rulerVisible: boolean, safeMarginsVisible: boolean) {
|
|
|
26
27
|
const host = document.createElement("div");
|
|
27
28
|
document.body.append(host);
|
|
28
29
|
act(() => {
|
|
29
|
-
createRoot(host).render(
|
|
30
|
+
createRoot(host).render(
|
|
31
|
+
<PreviewOverlayProvider>
|
|
32
|
+
<PreviewGuides />
|
|
33
|
+
</PreviewOverlayProvider>,
|
|
34
|
+
);
|
|
30
35
|
});
|
|
31
36
|
return host;
|
|
32
37
|
}
|
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
import { memo, useRef, type RefObject } from "react";
|
|
2
2
|
import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
|
|
3
|
-
import { RULER_GUTTER_PX
|
|
3
|
+
import { RULER_GUTTER_PX } from "./previewGuidesStore";
|
|
4
4
|
import { SAFE_BOX_PERCENTS, safeBoxInsetPercent } from "../../utils/previewSafeMargins";
|
|
5
|
-
|
|
6
|
-
interface PreviewGuidesProps {
|
|
7
|
-
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
8
|
-
}
|
|
5
|
+
import { usePreviewOverlayContext } from "./PreviewOverlayProvider";
|
|
9
6
|
|
|
10
7
|
const TICKS = Array.from({ length: 11 }, (_, i) => i * 10);
|
|
11
8
|
const INK = "color-mix(in srgb, white 70%, transparent)";
|
|
12
9
|
|
|
13
10
|
/** Ruler and safe-margin boxes drawn over the preview pane, never inside the composition. */
|
|
14
|
-
export const PreviewGuides = memo(function PreviewGuides(
|
|
15
|
-
const
|
|
16
|
-
const safeMarginsVisible =
|
|
11
|
+
export const PreviewGuides = memo(function PreviewGuides() {
|
|
12
|
+
const { state } = usePreviewOverlayContext();
|
|
13
|
+
const { rulerVisible, safeMarginsVisible } = state;
|
|
17
14
|
if (!rulerVisible && !safeMarginsVisible) return null;
|
|
18
15
|
return (
|
|
19
16
|
<ActiveGuides
|
|
20
|
-
iframeRef={iframeRef}
|
|
17
|
+
iframeRef={state.iframeRef}
|
|
21
18
|
rulerVisible={rulerVisible}
|
|
22
19
|
safeMarginsVisible={safeMarginsVisible}
|
|
23
20
|
/>
|
|
24
21
|
);
|
|
25
22
|
});
|
|
26
23
|
|
|
27
|
-
interface ActiveGuidesProps
|
|
24
|
+
interface ActiveGuidesProps {
|
|
25
|
+
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
28
26
|
rulerVisible: boolean;
|
|
29
27
|
safeMarginsVisible: boolean;
|
|
30
28
|
}
|