@hyperframes/studio 0.7.5 → 0.7.7
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/{index-BwFzbjZQ.js → index-B2YXvFxf.js} +1 -1
- package/dist/assets/index-BSkUuN8g.css +1 -0
- package/dist/assets/index-BeRh2hMe.js +375 -0
- package/dist/assets/{index-C5NAfiPa.js → index-BoASKOeE.js} +1 -1
- package/dist/chunk-KZXYQYIU.js +876 -0
- package/dist/chunk-KZXYQYIU.js.map +1 -0
- package/dist/domEditingLayers-SSXQZHHQ.js +41 -0
- package/dist/domEditingLayers-SSXQZHHQ.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.html +2 -2
- package/dist/index.js +3326 -2939
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/captions/hooks/useCaptionSync.ts +6 -1
- package/src/components/StudioPreviewArea.tsx +10 -3
- package/src/components/StudioRightPanel.tsx +4 -0
- package/src/components/editor/AnimationCard.tsx +77 -250
- package/src/components/editor/AnimationCardParts.tsx +220 -0
- package/src/components/editor/BlockParamsPanel.tsx +4 -8
- package/src/components/editor/DomEditOverlay.tsx +132 -48
- package/src/components/editor/EaseCurveSection.tsx +177 -101
- package/src/components/editor/GsapAnimationSection.tsx +4 -0
- package/src/components/editor/KeyframeEaseList.tsx +127 -0
- package/src/components/editor/OffCanvasIndicators.tsx +111 -0
- package/src/components/editor/PropertyPanel.tsx +63 -56
- package/src/components/editor/gsapAnimationCallbacks.ts +3 -0
- package/src/components/editor/gsapAnimationConstants.ts +11 -32
- package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
- package/src/components/editor/manualOffsetDrag.ts +8 -0
- package/src/components/editor/marqueeCommit.ts +168 -0
- package/src/components/editor/propertyPanelHelpers.ts +7 -0
- package/src/components/editor/propertyPanelTimingSection.tsx +35 -2
- package/src/components/editor/snapTargetCollection.ts +0 -5
- package/src/components/editor/useMotionPathData.ts +2 -1
- package/src/components/panels/SlideshowPanel.tsx +0 -1
- package/src/components/sidebar/AssetContextMenu.tsx +97 -0
- package/src/components/sidebar/AssetsTab.tsx +542 -254
- package/src/components/sidebar/assetHelpers.ts +40 -0
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/hooks/gsapDragCommit.ts +101 -0
- package/src/hooks/gsapDragPositionCommit.ts +1 -2
- package/src/hooks/gsapRuntimeBridge.ts +28 -2
- package/src/hooks/gsapRuntimeKeyframes.test.ts +47 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +13 -0
- package/src/hooks/gsapRuntimeReaders.ts +1 -6
- package/src/hooks/useDomEditCommits.ts +0 -4
- package/src/hooks/useDomEditPreviewSync.ts +5 -0
- package/src/hooks/useDomEditSession.ts +43 -0
- package/src/hooks/useDomEditTextCommits.ts +3 -12
- package/src/hooks/useDomEditWiring.ts +3 -0
- package/src/hooks/useDomSelection.ts +46 -0
- package/src/hooks/useGestureCommit.ts +26 -7
- package/src/hooks/useGestureRecording.ts +2 -16
- package/src/hooks/useGsapAnimationOps.ts +2 -2
- package/src/hooks/useGsapAwareEditing.ts +0 -4
- package/src/hooks/useGsapScriptCommits.ts +0 -12
- package/src/hooks/useGsapTweenCache.test.ts +45 -1
- package/src/hooks/useGsapTweenCache.ts +151 -42
- package/src/hooks/useMusicBeatAnalysis.ts +36 -21
- package/src/hooks/useRazorSplit.ts +0 -3
- package/src/player/components/Player.tsx +0 -5
- package/src/player/components/timelineIcons.tsx +2 -1
- package/src/player/hooks/useTimelinePlayer.ts +4 -14
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -9
- package/src/player/lib/timelineIframeHelpers.ts +2 -6
- package/src/telemetry/client.ts +2 -0
- package/src/utils/gestureSmoother.test.ts +48 -0
- package/src/utils/gestureSmoother.ts +46 -0
- package/src/utils/marqueeGeometry.test.ts +123 -0
- package/src/utils/marqueeGeometry.ts +172 -0
- package/src/utils/optimisticUpdate.ts +1 -2
- package/src/utils/sourcePatcher.ts +0 -10
- package/src/utils/velocityEaseFitter.test.ts +58 -0
- package/src/utils/velocityEaseFitter.ts +121 -0
- package/dist/assets/index-D_JGXmfx.js +0 -374
- package/dist/assets/index-DzWIinxk.css +0 -1
- package/src/utils/editDebugLog.ts +0 -16
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { MetricField } from "./propertyPanelPrimitives";
|
|
2
|
+
import {
|
|
3
|
+
PERCENT_PROPS,
|
|
4
|
+
PROP_CONSTRAINTS,
|
|
5
|
+
PROP_LABELS,
|
|
6
|
+
PROP_TOOLTIPS,
|
|
7
|
+
PROP_UNITS,
|
|
8
|
+
clampPropertyValue,
|
|
9
|
+
} from "./gsapAnimationConstants";
|
|
10
|
+
import { P } from "./panelTokens";
|
|
11
|
+
|
|
12
|
+
export const BOOLEAN_PROPS = new Set(["visibility"]);
|
|
13
|
+
const STRING_PROPS = new Set(["filter", "clipPath"]);
|
|
14
|
+
const FILTER_PRESETS = [
|
|
15
|
+
{ label: "Blur", value: "blur(4px)" },
|
|
16
|
+
{ label: "Bright", value: "brightness(1.5)" },
|
|
17
|
+
{ label: "Gray", value: "grayscale(1)" },
|
|
18
|
+
{ label: "None", value: "none" },
|
|
19
|
+
];
|
|
20
|
+
const CLIP_PATH_PRESETS = [
|
|
21
|
+
{ label: "Circle", value: "circle(50% at 50% 50%)" },
|
|
22
|
+
{ label: "Inset", value: "inset(10%)" },
|
|
23
|
+
{ label: "None", value: "none" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function isPercentProp(prop: string): boolean {
|
|
27
|
+
return PERCENT_PROPS.has(prop);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function displayValue(prop: string, val: number | string): string {
|
|
31
|
+
if (isPercentProp(prop)) return String(Math.round(Math.max(0, Math.min(1, Number(val))) * 100));
|
|
32
|
+
return String(val);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function adjustedValue(prop: string, raw: string): string {
|
|
36
|
+
if (isPercentProp(prop)) return String(clampPropertyValue(prop, Number(raw) / 100));
|
|
37
|
+
const num = Number(raw);
|
|
38
|
+
if (!Number.isNaN(num) && PROP_CONSTRAINTS[prop]) {
|
|
39
|
+
return String(clampPropertyValue(prop, num));
|
|
40
|
+
}
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function RemoveButton({ onClick, title }: { onClick: () => void; title: string }) {
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={onClick}
|
|
49
|
+
className="flex-shrink-0 rounded p-0.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400"
|
|
50
|
+
title={title}
|
|
51
|
+
>
|
|
52
|
+
<svg
|
|
53
|
+
width="12"
|
|
54
|
+
height="12"
|
|
55
|
+
viewBox="0 0 12 12"
|
|
56
|
+
fill="none"
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
strokeWidth="1.5"
|
|
59
|
+
>
|
|
60
|
+
<path d="M3 3l6 6M9 3l-6 6" />
|
|
61
|
+
</svg>
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// fallow-ignore-next-line complexity
|
|
67
|
+
export function PropertyRow({
|
|
68
|
+
prop,
|
|
69
|
+
val,
|
|
70
|
+
onCommit,
|
|
71
|
+
onRemove,
|
|
72
|
+
removeTitle,
|
|
73
|
+
}: {
|
|
74
|
+
prop: string;
|
|
75
|
+
val: number | string;
|
|
76
|
+
onCommit: (adjusted: string) => void;
|
|
77
|
+
onRemove: () => void;
|
|
78
|
+
removeTitle: string;
|
|
79
|
+
}) {
|
|
80
|
+
if (BOOLEAN_PROPS.has(prop)) {
|
|
81
|
+
const isVisible = val === "visible" || val === 1;
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex items-center gap-1">
|
|
84
|
+
<div className="min-w-0 flex-1 flex items-center gap-2 px-2 py-1 rounded-lg bg-neutral-900 border border-neutral-800">
|
|
85
|
+
<span className="flex-1 text-[11px] font-medium text-neutral-500">
|
|
86
|
+
{PROP_LABELS[prop] ?? prop}
|
|
87
|
+
</span>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={() => onCommit(isVisible ? "hidden" : "visible")}
|
|
91
|
+
className="flex-shrink-0 rounded-full transition-all duration-150 relative"
|
|
92
|
+
style={{ width: 28, height: 16, background: isVisible ? P.accent : P.borderInput }}
|
|
93
|
+
title={isVisible ? "Visible — click to hide" : "Hidden — click to show"}
|
|
94
|
+
>
|
|
95
|
+
<span
|
|
96
|
+
className="absolute top-[2px] left-0 rounded-full transition-transform duration-150"
|
|
97
|
+
style={{
|
|
98
|
+
width: 12,
|
|
99
|
+
height: 12,
|
|
100
|
+
background: isVisible ? P.white : P.textMuted,
|
|
101
|
+
transform: isVisible ? "translateX(14px)" : "translateX(2px)",
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
<RemoveButton onClick={onRemove} title={removeTitle} />
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (STRING_PROPS.has(prop)) {
|
|
111
|
+
const presets =
|
|
112
|
+
prop === "filter" ? FILTER_PRESETS : prop === "clipPath" ? CLIP_PATH_PRESETS : [];
|
|
113
|
+
return (
|
|
114
|
+
<div className="flex flex-col gap-1">
|
|
115
|
+
<div className="flex items-center gap-1">
|
|
116
|
+
<div className="min-w-0 flex-1 flex items-center gap-2 px-2 py-1 rounded-lg bg-neutral-900 border border-neutral-800">
|
|
117
|
+
<span className="flex-shrink-0 text-[11px] font-medium text-neutral-500">
|
|
118
|
+
{PROP_LABELS[prop] ?? prop}
|
|
119
|
+
</span>
|
|
120
|
+
<input
|
|
121
|
+
type="text"
|
|
122
|
+
defaultValue={String(val)}
|
|
123
|
+
className="flex-1 bg-transparent text-[11px] text-neutral-200 outline-none"
|
|
124
|
+
onBlur={(e) => onCommit(e.currentTarget.value)}
|
|
125
|
+
onKeyDown={(e) => {
|
|
126
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
<RemoveButton onClick={onRemove} title={removeTitle} />
|
|
131
|
+
</div>
|
|
132
|
+
{presets.length > 0 && (
|
|
133
|
+
<div className="flex gap-1 pl-1">
|
|
134
|
+
{presets.map((p) => (
|
|
135
|
+
<button
|
|
136
|
+
key={p.value}
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={() => onCommit(p.value)}
|
|
139
|
+
className="px-1.5 py-0.5 rounded text-[9px] font-medium text-neutral-500 bg-neutral-800/50 hover:bg-neutral-800 hover:text-neutral-300 transition-colors"
|
|
140
|
+
>
|
|
141
|
+
{p.label}
|
|
142
|
+
</button>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return (
|
|
150
|
+
<div className="flex items-center gap-1">
|
|
151
|
+
<div className="min-w-0 flex-1">
|
|
152
|
+
<MetricField
|
|
153
|
+
label={PROP_LABELS[prop] ?? prop}
|
|
154
|
+
value={displayValue(prop, val)}
|
|
155
|
+
suffix={PROP_UNITS[prop]}
|
|
156
|
+
tooltip={PROP_TOOLTIPS[prop]}
|
|
157
|
+
scrub
|
|
158
|
+
liveCommit
|
|
159
|
+
onCommit={(raw) => onCommit(adjustedValue(prop, raw))}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
<RemoveButton onClick={onRemove} title={removeTitle} />
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function AddPropertyTrigger({
|
|
168
|
+
adding,
|
|
169
|
+
available,
|
|
170
|
+
addLabel,
|
|
171
|
+
addTitle,
|
|
172
|
+
onAdd,
|
|
173
|
+
onOpen,
|
|
174
|
+
onClose,
|
|
175
|
+
buttonClassName,
|
|
176
|
+
}: {
|
|
177
|
+
adding: boolean;
|
|
178
|
+
available: string[];
|
|
179
|
+
addLabel: string;
|
|
180
|
+
addTitle: string;
|
|
181
|
+
onAdd: (prop: string) => void;
|
|
182
|
+
onOpen: () => void;
|
|
183
|
+
onClose: () => void;
|
|
184
|
+
buttonClassName: string;
|
|
185
|
+
}) {
|
|
186
|
+
if (adding && available.length > 0) {
|
|
187
|
+
return (
|
|
188
|
+
<select
|
|
189
|
+
autoFocus
|
|
190
|
+
className="min-w-0 rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-[11px] text-neutral-100 outline-none"
|
|
191
|
+
defaultValue=""
|
|
192
|
+
onChange={(e) => {
|
|
193
|
+
if (e.target.value) onAdd(e.target.value);
|
|
194
|
+
onClose();
|
|
195
|
+
}}
|
|
196
|
+
onBlur={onClose}
|
|
197
|
+
>
|
|
198
|
+
<option value="" disabled>
|
|
199
|
+
Choose property…
|
|
200
|
+
</option>
|
|
201
|
+
{available.map((p) => (
|
|
202
|
+
<option key={p} value={p}>
|
|
203
|
+
{PROP_LABELS[p] ?? p}
|
|
204
|
+
</option>
|
|
205
|
+
))}
|
|
206
|
+
</select>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (available.length === 0) return null;
|
|
210
|
+
return (
|
|
211
|
+
<button type="button" onClick={onOpen} className={buttonClassName} title={addTitle}>
|
|
212
|
+
{addLabel}
|
|
213
|
+
</button>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function parseNumericOrString(raw: string): number | string {
|
|
218
|
+
const num = Number(raw);
|
|
219
|
+
return Number.isFinite(num) ? num : raw;
|
|
220
|
+
}
|
|
@@ -12,7 +12,7 @@ interface BlockParamsPanelProps {
|
|
|
12
12
|
export const BlockParamsPanel = memo(function BlockParamsPanel({
|
|
13
13
|
blockTitle,
|
|
14
14
|
params,
|
|
15
|
-
compositionPath,
|
|
15
|
+
compositionPath: _compositionPath,
|
|
16
16
|
onClose,
|
|
17
17
|
}: BlockParamsPanelProps) {
|
|
18
18
|
const [values, setValues] = useState<Record<string, string>>(() => {
|
|
@@ -23,13 +23,9 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
|
|
|
23
23
|
return initial;
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
const handleChange = useCallback(
|
|
27
|
-
(
|
|
28
|
-
|
|
29
|
-
console.log(`[BlockParams] ${compositionPath} ${key}: ${value}`);
|
|
30
|
-
},
|
|
31
|
-
[compositionPath],
|
|
32
|
-
);
|
|
26
|
+
const handleChange = useCallback((key: string, value: string) => {
|
|
27
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
28
|
+
}, []);
|
|
33
29
|
|
|
34
30
|
return (
|
|
35
31
|
<div className="flex flex-col h-full">
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { memo, useMemo, useRef, useState, type RefObject } from "react";
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
2
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
3
|
import { type DomEditSelection } from "./domEditing";
|
|
4
|
-
import {
|
|
4
|
+
import { useMarqueeGestures } from "./marqueeCommit";
|
|
5
|
+
import { resolveDomEditGroupOverlayRect, toOverlayRect } from "./domEditOverlayGeometry";
|
|
6
|
+
import { collectDomEditLayerItems } from "./domEditingLayers";
|
|
7
|
+
import { isElementComputedVisible } from "./domEditingElement";
|
|
5
8
|
import {
|
|
6
9
|
type BlockedMoveState,
|
|
7
10
|
type DomEditGroupPathOffsetCommit,
|
|
@@ -11,6 +14,7 @@ import {
|
|
|
11
14
|
focusDomEditOverlayElement,
|
|
12
15
|
} from "./domEditOverlayGestures";
|
|
13
16
|
import { useDomEditOverlayRects } from "./useDomEditOverlayRects";
|
|
17
|
+
import { OffCanvasIndicators, type OffCanvasRect } from "./OffCanvasIndicators";
|
|
14
18
|
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
15
19
|
import { SnapGuideOverlay, type SnapGuidesState } from "./SnapGuideOverlay";
|
|
16
20
|
import { GridOverlay } from "./GridOverlay";
|
|
@@ -67,8 +71,10 @@ interface DomEditOverlayProps {
|
|
|
67
71
|
gridSpacing?: number;
|
|
68
72
|
recordingState?: GestureRecordingState;
|
|
69
73
|
onToggleRecording?: () => void;
|
|
74
|
+
onMarqueeSelect?: (selections: DomEditSelection[], additive: boolean) => void;
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
// fallow-ignore-next-line complexity
|
|
72
78
|
export const DomEditOverlay = memo(function DomEditOverlay({
|
|
73
79
|
iframeRef,
|
|
74
80
|
activeCompositionPath,
|
|
@@ -88,13 +94,16 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
88
94
|
onGroupPathOffsetCommit,
|
|
89
95
|
onBoxSizeCommit,
|
|
90
96
|
onRotationCommit,
|
|
97
|
+
onMarqueeSelect,
|
|
91
98
|
}: DomEditOverlayProps) {
|
|
92
99
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
|
93
100
|
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
101
|
+
const onMarqueeSelectRef = useRef(onMarqueeSelect);
|
|
102
|
+
onMarqueeSelectRef.current = onMarqueeSelect;
|
|
94
103
|
|
|
95
104
|
const selectionShapeStyles = (() => {
|
|
96
105
|
const fallback = {
|
|
97
|
-
borderRadius:
|
|
106
|
+
borderRadius: 8 as string | number,
|
|
98
107
|
clipPath: undefined as string | undefined,
|
|
99
108
|
};
|
|
100
109
|
if (!selection?.element) return fallback;
|
|
@@ -213,6 +222,50 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
213
222
|
return () => cancelAnimationFrame(frame);
|
|
214
223
|
});
|
|
215
224
|
|
|
225
|
+
// Off-canvas element indicators — dashed outlines for elements positioned
|
|
226
|
+
// outside the composition bounds so users can find them.
|
|
227
|
+
const offCanvasElementsRef = useRef<Map<string, HTMLElement>>(new Map());
|
|
228
|
+
const [offCanvasRects, setOffCanvasRects] = useState<OffCanvasRect[]>([]);
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
const iframe = iframeRef.current;
|
|
231
|
+
const overlay = overlayRef.current;
|
|
232
|
+
if (!iframe || !overlay || compRect.width <= 0) {
|
|
233
|
+
setOffCanvasRects([]);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const doc = iframe.contentDocument;
|
|
237
|
+
if (!doc) return;
|
|
238
|
+
const root = doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.body;
|
|
239
|
+
const acp = activeCompositionPath ?? "index.html";
|
|
240
|
+
const items = collectDomEditLayerItems(root, {
|
|
241
|
+
activeCompositionPath: acp,
|
|
242
|
+
isMasterView: !acp || acp === "index.html",
|
|
243
|
+
});
|
|
244
|
+
const rects: typeof offCanvasRects = [];
|
|
245
|
+
const elMap = new Map<string, HTMLElement>();
|
|
246
|
+
for (const item of items) {
|
|
247
|
+
if (!isElementComputedVisible(item.element)) continue;
|
|
248
|
+
const r = toOverlayRect(overlay, iframe, item.element);
|
|
249
|
+
if (!r) continue;
|
|
250
|
+
// Any edge crossing the composition border → gray-zone indicator (the
|
|
251
|
+
// in-canvas portion is clipped away below, so only the sliver shows).
|
|
252
|
+
const extendsOutsideComp =
|
|
253
|
+
r.left < compRect.left ||
|
|
254
|
+
r.left + r.width > compRect.left + compRect.width ||
|
|
255
|
+
r.top < compRect.top ||
|
|
256
|
+
r.top + r.height > compRect.top + compRect.height;
|
|
257
|
+
if (extendsOutsideComp) {
|
|
258
|
+
rects.push({ key: item.key, left: r.left, top: r.top, width: r.width, height: r.height });
|
|
259
|
+
elMap.set(item.key, item.element);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
offCanvasElementsRef.current = elMap;
|
|
263
|
+
setOffCanvasRects(rects);
|
|
264
|
+
// Positions depend on layout, not selection — the selected-element
|
|
265
|
+
// suppression is a render-time filter, so selection/groupSelections stay
|
|
266
|
+
// out of the deps to avoid re-walking geometry on each selection change.
|
|
267
|
+
}, [iframeRef, compRect, activeCompositionPath]);
|
|
268
|
+
|
|
216
269
|
const gestures = createDomEditOverlayGestureHandlers({
|
|
217
270
|
overlayRef,
|
|
218
271
|
iframeRef,
|
|
@@ -238,6 +291,15 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
238
291
|
snapGuidesRef,
|
|
239
292
|
});
|
|
240
293
|
|
|
294
|
+
const marquee = useMarqueeGestures({
|
|
295
|
+
iframeRef,
|
|
296
|
+
overlayRef,
|
|
297
|
+
activeCompositionPathRef,
|
|
298
|
+
onMarqueeSelectRef,
|
|
299
|
+
selectionRef,
|
|
300
|
+
gestures,
|
|
301
|
+
});
|
|
302
|
+
|
|
241
303
|
const selectionKey = useMemo(() => {
|
|
242
304
|
if (!selection) return "none";
|
|
243
305
|
return `${selection.sourceFile}:${selection.id ?? selection.selector ?? selection.label}:${selection.selectorIndex ?? 0}`;
|
|
@@ -265,22 +327,9 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
265
327
|
}
|
|
266
328
|
const target = event.target as HTMLElement | null;
|
|
267
329
|
if (target?.closest('[data-dom-edit-selection-box="true"]')) return;
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
const overlayEl = overlayRef.current;
|
|
272
|
-
if (overlayEl) {
|
|
273
|
-
const overlayRect = overlayEl.getBoundingClientRect();
|
|
274
|
-
const clickX = event.clientX - overlayRect.left;
|
|
275
|
-
const clickY = event.clientY - overlayRect.top;
|
|
276
|
-
const outsideComp =
|
|
277
|
-
clickX < compRect.left ||
|
|
278
|
-
clickX > compRect.left + compRect.width ||
|
|
279
|
-
clickY < compRect.top ||
|
|
280
|
-
clickY > compRect.top + compRect.height;
|
|
281
|
-
if (outsideComp) return;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
330
|
+
// Allow clicks anywhere on the overlay — GSAP-translated elements can
|
|
331
|
+
// extend beyond the composition rect into the gray zone, and users need
|
|
332
|
+
// to select/deselect them by clicking there.
|
|
284
333
|
onCanvasMouseDown(event, { preferClipAncestor: false });
|
|
285
334
|
if (event.shiftKey) {
|
|
286
335
|
suppressNextBoxMouseDownRef.current = true;
|
|
@@ -306,6 +355,36 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
306
355
|
|
|
307
356
|
const target = event.target as HTMLElement | null;
|
|
308
357
|
if (target?.closest('[data-dom-edit-selection-box="true"]')) return;
|
|
358
|
+
|
|
359
|
+
// Start marquee if clicking on empty canvas (no element under pointer)
|
|
360
|
+
if (!hoverSelectionRef.current && onMarqueeSelectRef.current && compRect.width > 0) {
|
|
361
|
+
const overlayEl = overlayRef.current;
|
|
362
|
+
if (overlayEl) {
|
|
363
|
+
const oRect = overlayEl.getBoundingClientRect();
|
|
364
|
+
const cx = event.clientX - oRect.left;
|
|
365
|
+
const cy = event.clientY - oRect.top;
|
|
366
|
+
const inComp =
|
|
367
|
+
cx >= compRect.left &&
|
|
368
|
+
cx <= compRect.left + compRect.width &&
|
|
369
|
+
cy >= compRect.top &&
|
|
370
|
+
cy <= compRect.top + compRect.height;
|
|
371
|
+
if (inComp) {
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
event.stopPropagation();
|
|
374
|
+
suppressNextOverlayMouseDownRef.current = true;
|
|
375
|
+
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
|
376
|
+
marquee.marqueeRef.current = {
|
|
377
|
+
startX: cx,
|
|
378
|
+
startY: cy,
|
|
379
|
+
currentX: cx,
|
|
380
|
+
currentY: cy,
|
|
381
|
+
pointerId: event.pointerId,
|
|
382
|
+
pastThreshold: false,
|
|
383
|
+
};
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
309
388
|
};
|
|
310
389
|
|
|
311
390
|
const handleBoxClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
@@ -332,44 +411,29 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
332
411
|
className="absolute inset-0 z-10 pointer-events-auto outline-none"
|
|
333
412
|
tabIndex={-1}
|
|
334
413
|
aria-label="Composition canvas"
|
|
414
|
+
// Cursor follows marquee rect *state* (re-renders), not the mutable ref.
|
|
415
|
+
style={marquee.marqueeRect ? { cursor: "crosshair" } : undefined}
|
|
335
416
|
onPointerDownCapture={(event) =>
|
|
336
417
|
focusDomEditOverlayElement(event.currentTarget as FocusableDomEditOverlay)
|
|
337
418
|
}
|
|
338
419
|
onPointerDown={handleOverlayPointerDown}
|
|
339
420
|
onMouseDown={handleOverlayMouseDown}
|
|
340
|
-
onPointerMove={
|
|
421
|
+
onPointerMove={marquee.onPointerMove}
|
|
341
422
|
onPointerLeave={() => onCanvasPointerLeaveRef.current()}
|
|
342
|
-
onPointerUp={
|
|
343
|
-
onPointerCancel={
|
|
423
|
+
onPointerUp={marquee.onPointerUp}
|
|
424
|
+
onPointerCancel={marquee.onPointerCancel}
|
|
344
425
|
>
|
|
345
426
|
{hoverSelection && hoverRect && compRect.width > 0 && (
|
|
346
427
|
<div
|
|
347
428
|
aria-hidden="true"
|
|
348
429
|
data-dom-edit-hover-box="true"
|
|
349
|
-
className="pointer-events-none absolute border border-studio-accent/80 bg-studio-accent/5 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
|
350
|
-
style={
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (tag !== "svg" && tag !== "img" && tag !== "video" && tag !== "canvas") {
|
|
357
|
-
const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
|
|
358
|
-
if (cs?.borderRadius && cs.borderRadius !== "0px") br = cs.borderRadius;
|
|
359
|
-
if (cs?.clipPath && cs.clipPath !== "none") cp = cs.clipPath;
|
|
360
|
-
}
|
|
361
|
-
} catch {
|
|
362
|
-
/* cross-origin guard */
|
|
363
|
-
}
|
|
364
|
-
return {
|
|
365
|
-
left: hoverRect.left,
|
|
366
|
-
top: hoverRect.top,
|
|
367
|
-
width: hoverRect.width,
|
|
368
|
-
height: hoverRect.height,
|
|
369
|
-
borderRadius: br,
|
|
370
|
-
clipPath: cp,
|
|
371
|
-
};
|
|
372
|
-
})()}
|
|
430
|
+
className="pointer-events-none absolute rounded-md border border-studio-accent/80 bg-studio-accent/5 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
|
431
|
+
style={{
|
|
432
|
+
left: hoverRect.left,
|
|
433
|
+
top: hoverRect.top,
|
|
434
|
+
width: hoverRect.width,
|
|
435
|
+
height: hoverRect.height,
|
|
436
|
+
}}
|
|
373
437
|
/>
|
|
374
438
|
)}
|
|
375
439
|
{hasGroupSelection && groupOverlayItems.length > 1 && groupBounds && compRect.width > 0 && (
|
|
@@ -437,13 +501,12 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
437
501
|
key={selectionKey}
|
|
438
502
|
ref={boxRef}
|
|
439
503
|
data-dom-edit-selection-box="true"
|
|
440
|
-
className={`pointer-events-auto absolute ${selectionShapeStyles.clipPath ? "shadow-[inset_0_0_0_2px_rgba(60,230,172,0.6)]" : "border border-studio-accent/80 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"} bg-studio-accent/5`}
|
|
504
|
+
className={`pointer-events-auto absolute rounded-md ${selectionShapeStyles.clipPath ? "shadow-[inset_0_0_0_2px_rgba(60,230,172,0.6)]" : "border border-studio-accent/80 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"} bg-studio-accent/5`}
|
|
441
505
|
style={{
|
|
442
506
|
left: overlayRect.left,
|
|
443
507
|
top: overlayRect.top,
|
|
444
508
|
width: overlayRect.width,
|
|
445
509
|
height: overlayRect.height,
|
|
446
|
-
borderRadius: selectionShapeStyles.borderRadius,
|
|
447
510
|
clipPath: selectionShapeStyles.clipPath,
|
|
448
511
|
cursor:
|
|
449
512
|
allowCanvasMovement && selection.capabilities.canApplyManualOffset
|
|
@@ -496,6 +559,27 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
496
559
|
}}
|
|
497
560
|
/>
|
|
498
561
|
))}
|
|
562
|
+
<OffCanvasIndicators
|
|
563
|
+
rects={offCanvasRects}
|
|
564
|
+
elements={offCanvasElementsRef}
|
|
565
|
+
compRect={compRect}
|
|
566
|
+
selection={selection}
|
|
567
|
+
groupSelections={groupSelections}
|
|
568
|
+
activeCompositionPathRef={activeCompositionPathRef}
|
|
569
|
+
onSelectionChangeRef={onSelectionChangeRef}
|
|
570
|
+
/>
|
|
571
|
+
{marquee.marqueeRect && (
|
|
572
|
+
<div
|
|
573
|
+
aria-hidden="true"
|
|
574
|
+
className="pointer-events-none absolute border border-dashed border-studio-accent bg-studio-accent/10"
|
|
575
|
+
style={{
|
|
576
|
+
left: marquee.marqueeRect.left,
|
|
577
|
+
top: marquee.marqueeRect.top,
|
|
578
|
+
width: marquee.marqueeRect.width,
|
|
579
|
+
height: marquee.marqueeRect.height,
|
|
580
|
+
}}
|
|
581
|
+
/>
|
|
582
|
+
)}
|
|
499
583
|
<GridOverlay
|
|
500
584
|
visible={gridVisible}
|
|
501
585
|
spacing={gridSpacing}
|