@hyperframes/studio 0.6.1 → 0.6.3
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-C_Hp2qSf.js +117 -0
- package/dist/assets/index-DMJCfYoN.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +14 -18
- package/src/components/StudioRightPanel.tsx +15 -1
- package/src/components/editor/DomEditOverlay.test.ts +165 -1
- package/src/components/editor/DomEditOverlay.tsx +0 -1
- package/src/components/editor/LayersPanel.tsx +302 -0
- package/src/components/editor/SourceEditor.tsx +20 -3
- package/src/components/editor/propertyPanelPrimitives.tsx +24 -5
- package/src/components/editor/propertyPanelStyleSections.tsx +6 -6
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -10
- package/src/components/nle/NLELayout.tsx +4 -2
- package/src/components/nle/NLEPreview.tsx +250 -30
- package/src/components/nle/previewZoom.test.ts +118 -0
- package/src/components/nle/previewZoom.ts +84 -0
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/contexts/DomEditContext.tsx +3 -0
- package/src/hooks/useConsoleErrorCapture.ts +3 -3
- package/src/hooks/useDomEditSession.ts +1 -0
- package/src/hooks/useDomSelection.ts +27 -6
- package/src/hooks/usePanelLayout.ts +8 -2
- package/src/player/hooks/useTimelinePlayer.ts +11 -1
- package/src/player/store/playerStore.ts +18 -2
- package/src/utils/studioHelpers.ts +1 -1
- package/src/utils/studioUiPreferences.test.ts +49 -0
- package/src/utils/studioUiPreferences.ts +84 -0
- package/dist/assets/index-D1JDq7Gg.css +0 -1
- package/dist/assets/index-hYc4aP7M.js +0 -117
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { memo, useState, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
collectDomEditLayerItems,
|
|
4
|
+
getDomEditLayerKey,
|
|
5
|
+
resolveDomEditSelection,
|
|
6
|
+
type DomEditLayerItem,
|
|
7
|
+
} from "./domEditing";
|
|
8
|
+
import { useStudioContext } from "../../contexts/StudioContext";
|
|
9
|
+
import { useDomEditContext } from "../../contexts/DomEditContext";
|
|
10
|
+
import { usePlayerStore } from "../../player";
|
|
11
|
+
import { findMatchingTimelineElementId } from "../../utils/studioHelpers";
|
|
12
|
+
import { Layers } from "../../icons/SystemIcons";
|
|
13
|
+
|
|
14
|
+
const TAG_ICONS: Record<string, string> = {
|
|
15
|
+
video: "Vi",
|
|
16
|
+
audio: "Au",
|
|
17
|
+
img: "Im",
|
|
18
|
+
svg: "Sv",
|
|
19
|
+
canvas: "Cn",
|
|
20
|
+
div: "Di",
|
|
21
|
+
section: "Se",
|
|
22
|
+
span: "Sp",
|
|
23
|
+
p: "P",
|
|
24
|
+
h1: "H1",
|
|
25
|
+
h2: "H2",
|
|
26
|
+
h3: "H3",
|
|
27
|
+
h4: "H4",
|
|
28
|
+
h5: "H5",
|
|
29
|
+
h6: "H6",
|
|
30
|
+
a: "A",
|
|
31
|
+
button: "Bt",
|
|
32
|
+
ul: "Ul",
|
|
33
|
+
ol: "Ol",
|
|
34
|
+
li: "Li",
|
|
35
|
+
style: "St",
|
|
36
|
+
template: "Te",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function getTagBadge(tagName: string): string {
|
|
40
|
+
return TAG_ICONS[tagName] ?? tagName.slice(0, 2).toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isCompositionHost(el: HTMLElement): boolean {
|
|
44
|
+
return el.hasAttribute("data-composition-src") || el.hasAttribute("data-composition-file");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface CollapsedState {
|
|
48
|
+
[key: string]: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const LayersPanel = memo(function LayersPanel() {
|
|
52
|
+
const { previewIframeRef, activeCompPath, refreshKey, compositionLoading, timelineElements } =
|
|
53
|
+
useStudioContext();
|
|
54
|
+
const { domEditSelection, applyDomSelection, updateDomEditHoverSelection } = useDomEditContext();
|
|
55
|
+
|
|
56
|
+
const [layers, setLayers] = useState<DomEditLayerItem[]>([]);
|
|
57
|
+
const [collapsed, setCollapsed] = useState<CollapsedState>({});
|
|
58
|
+
const prevDocVersionRef = useRef(0);
|
|
59
|
+
|
|
60
|
+
const isMasterView = !activeCompPath || activeCompPath === "index.html";
|
|
61
|
+
|
|
62
|
+
const collectLayers = useCallback(() => {
|
|
63
|
+
const iframe = previewIframeRef.current;
|
|
64
|
+
if (!iframe) return;
|
|
65
|
+
let doc: Document | null = null;
|
|
66
|
+
try {
|
|
67
|
+
doc = iframe.contentDocument;
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!doc) return;
|
|
72
|
+
|
|
73
|
+
const root =
|
|
74
|
+
doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.documentElement ?? null;
|
|
75
|
+
if (!root) return;
|
|
76
|
+
|
|
77
|
+
const items = collectDomEditLayerItems(root, {
|
|
78
|
+
activeCompositionPath: activeCompPath,
|
|
79
|
+
isMasterView,
|
|
80
|
+
});
|
|
81
|
+
setLayers(items);
|
|
82
|
+
}, [previewIframeRef, activeCompPath, isMasterView]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
collectLayers();
|
|
86
|
+
}, [collectLayers, refreshKey]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const iframe = previewIframeRef.current;
|
|
90
|
+
if (!iframe) return;
|
|
91
|
+
const handleLoad = () => {
|
|
92
|
+
prevDocVersionRef.current += 1;
|
|
93
|
+
collectLayers();
|
|
94
|
+
};
|
|
95
|
+
iframe.addEventListener("load", handleLoad);
|
|
96
|
+
return () => iframe.removeEventListener("load", handleLoad);
|
|
97
|
+
}, [previewIframeRef, collectLayers]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!compositionLoading) {
|
|
101
|
+
const timer = setTimeout(collectLayers, 100);
|
|
102
|
+
return () => clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}, [compositionLoading, collectLayers]);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const ref = hoverSeekTimerRef;
|
|
108
|
+
return () => {
|
|
109
|
+
if (ref.current) clearTimeout(ref.current);
|
|
110
|
+
};
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const resolveSelection = useCallback(
|
|
114
|
+
(layer: DomEditLayerItem) =>
|
|
115
|
+
resolveDomEditSelection(layer.element, {
|
|
116
|
+
activeCompositionPath: activeCompPath,
|
|
117
|
+
isMasterView,
|
|
118
|
+
preferClipAncestor: false,
|
|
119
|
+
}),
|
|
120
|
+
[activeCompPath, isMasterView],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const hoverSeekTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
124
|
+
|
|
125
|
+
const seekToLayer = useCallback(
|
|
126
|
+
(layer: DomEditLayerItem) => {
|
|
127
|
+
const selection = resolveSelection(layer);
|
|
128
|
+
if (!selection) return;
|
|
129
|
+
|
|
130
|
+
let matchedId = findMatchingTimelineElementId(selection, timelineElements);
|
|
131
|
+
|
|
132
|
+
// No direct match — walk up DOM ancestors to find the nearest element
|
|
133
|
+
// that has a timeline entry (e.g. a child of scene1 seeks to scene1.start)
|
|
134
|
+
if (!matchedId) {
|
|
135
|
+
const sourceFile = selection.sourceFile ?? "index.html";
|
|
136
|
+
let ancestor = layer.element.parentElement;
|
|
137
|
+
while (ancestor && !matchedId) {
|
|
138
|
+
const elId = ancestor.id;
|
|
139
|
+
if (elId) {
|
|
140
|
+
const found = timelineElements.find(
|
|
141
|
+
(e) => e.domId === elId && (e.sourceFile ?? "index.html") === sourceFile,
|
|
142
|
+
);
|
|
143
|
+
if (found) matchedId = found.key ?? found.id;
|
|
144
|
+
}
|
|
145
|
+
ancestor = ancestor.parentElement;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (matchedId) {
|
|
150
|
+
const el = timelineElements.find((e) => (e.key ?? e.id) === matchedId);
|
|
151
|
+
if (el) {
|
|
152
|
+
usePlayerStore.getState().requestSeek(el.start + el.duration / 2);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[resolveSelection, timelineElements],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const handleSelectLayer = useCallback(
|
|
160
|
+
(layer: DomEditLayerItem) => {
|
|
161
|
+
const selection = resolveSelection(layer);
|
|
162
|
+
if (!selection) return;
|
|
163
|
+
applyDomSelection(selection);
|
|
164
|
+
seekToLayer(layer);
|
|
165
|
+
},
|
|
166
|
+
[resolveSelection, applyDomSelection, seekToLayer],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const handleLayerHover = useCallback(
|
|
170
|
+
(layer: DomEditLayerItem | null) => {
|
|
171
|
+
if (!layer) {
|
|
172
|
+
if (hoverSeekTimerRef.current) clearTimeout(hoverSeekTimerRef.current);
|
|
173
|
+
updateDomEditHoverSelection(null);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const selection = resolveSelection(layer);
|
|
177
|
+
updateDomEditHoverSelection(selection);
|
|
178
|
+
// Debounce hover seeks so brushing past items doesn't thrash the player
|
|
179
|
+
if (hoverSeekTimerRef.current) clearTimeout(hoverSeekTimerRef.current);
|
|
180
|
+
hoverSeekTimerRef.current = setTimeout(() => seekToLayer(layer), 300);
|
|
181
|
+
},
|
|
182
|
+
[resolveSelection, updateDomEditHoverSelection, seekToLayer],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const toggleCollapse = useCallback((key: string, e: React.MouseEvent) => {
|
|
186
|
+
e.stopPropagation();
|
|
187
|
+
setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
const selectedKey = domEditSelection ? getDomEditLayerKey(domEditSelection) : null;
|
|
191
|
+
|
|
192
|
+
const visibleLayers = getVisibleLayers(layers, collapsed);
|
|
193
|
+
|
|
194
|
+
if (layers.length === 0) {
|
|
195
|
+
return (
|
|
196
|
+
<div className="flex h-full flex-col items-center justify-center bg-neutral-900 px-6 text-center">
|
|
197
|
+
<Layers size={18} className="mb-3 text-neutral-600" />
|
|
198
|
+
<p className="text-sm font-medium text-neutral-200">No layers</p>
|
|
199
|
+
<p className="mt-1 text-xs text-neutral-500">Load a composition to see its element tree</p>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
className="flex h-full min-h-0 flex-col overflow-hidden bg-neutral-900"
|
|
207
|
+
onPointerLeave={() => handleLayerHover(null)}
|
|
208
|
+
>
|
|
209
|
+
<div className="border-b border-white/10 px-3 py-2 text-[11px] text-neutral-500">
|
|
210
|
+
{layers.length} layer{layers.length === 1 ? "" : "s"}
|
|
211
|
+
</div>
|
|
212
|
+
<div className="min-h-0 flex-1 overflow-y-auto py-1">
|
|
213
|
+
{visibleLayers.map((layer) => {
|
|
214
|
+
const selected = layer.key === selectedKey;
|
|
215
|
+
const isCollapsed = collapsed[layer.key] ?? false;
|
|
216
|
+
const hasChildren = layer.childCount > 0;
|
|
217
|
+
const isCompHost = isCompositionHost(layer.element);
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
key={layer.key}
|
|
222
|
+
role="button"
|
|
223
|
+
tabIndex={0}
|
|
224
|
+
onClick={() => handleSelectLayer(layer)}
|
|
225
|
+
onPointerEnter={() => handleLayerHover(layer)}
|
|
226
|
+
onKeyDown={(e) => {
|
|
227
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
handleSelectLayer(layer);
|
|
230
|
+
}
|
|
231
|
+
}}
|
|
232
|
+
className={`group flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left transition-colors ${
|
|
233
|
+
selected
|
|
234
|
+
? "bg-studio-accent/14 text-studio-accent"
|
|
235
|
+
: "text-neutral-300 hover:bg-white/[0.04] hover:text-neutral-100"
|
|
236
|
+
}`}
|
|
237
|
+
style={{ paddingLeft: 8 + layer.depth * 16 }}
|
|
238
|
+
>
|
|
239
|
+
{hasChildren ? (
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
onClick={(e) => toggleCollapse(layer.key, e)}
|
|
243
|
+
className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded text-neutral-500 hover:text-neutral-300"
|
|
244
|
+
>
|
|
245
|
+
<svg
|
|
246
|
+
width="8"
|
|
247
|
+
height="8"
|
|
248
|
+
viewBox="0 0 8 8"
|
|
249
|
+
fill="currentColor"
|
|
250
|
+
className={`transition-transform ${isCollapsed ? "" : "rotate-90"}`}
|
|
251
|
+
>
|
|
252
|
+
<path d="M2 1l4 3-4 3z" />
|
|
253
|
+
</svg>
|
|
254
|
+
</button>
|
|
255
|
+
) : (
|
|
256
|
+
<span className="w-4 flex-shrink-0" />
|
|
257
|
+
)}
|
|
258
|
+
<span
|
|
259
|
+
className={`flex h-5 w-5 flex-shrink-0 items-center justify-center rounded text-[8px] font-bold uppercase ${
|
|
260
|
+
selected
|
|
261
|
+
? "bg-studio-accent/18 text-studio-accent"
|
|
262
|
+
: isCompHost
|
|
263
|
+
? "bg-blue-900/40 text-blue-400"
|
|
264
|
+
: "bg-neutral-800 text-neutral-500"
|
|
265
|
+
}`}
|
|
266
|
+
>
|
|
267
|
+
{getTagBadge(layer.tagName)}
|
|
268
|
+
</span>
|
|
269
|
+
<span className="min-w-0 flex-1 truncate text-[11px]">{layer.label}</span>
|
|
270
|
+
{hasChildren && (
|
|
271
|
+
<span className="text-[9px] tabular-nums text-neutral-600">{layer.childCount}</span>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
})}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
function getVisibleLayers(
|
|
282
|
+
layers: DomEditLayerItem[],
|
|
283
|
+
collapsed: CollapsedState,
|
|
284
|
+
): DomEditLayerItem[] {
|
|
285
|
+
if (Object.keys(collapsed).length === 0) return layers;
|
|
286
|
+
|
|
287
|
+
const result: DomEditLayerItem[] = [];
|
|
288
|
+
let skipDepth = -1;
|
|
289
|
+
|
|
290
|
+
for (const layer of layers) {
|
|
291
|
+
if (skipDepth >= 0 && layer.depth > skipDepth) continue;
|
|
292
|
+
skipDepth = -1;
|
|
293
|
+
|
|
294
|
+
result.push(layer);
|
|
295
|
+
|
|
296
|
+
if (collapsed[layer.key] && layer.childCount > 0) {
|
|
297
|
+
skipDepth = layer.depth;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useCallback, memo } from "react";
|
|
1
|
+
import { useRef, useCallback, useEffect, memo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
EditorView,
|
|
4
4
|
keymap,
|
|
@@ -69,6 +69,9 @@ export const SourceEditor = memo(function SourceEditor({
|
|
|
69
69
|
const onChangeRef = useRef(onChange);
|
|
70
70
|
onChangeRef.current = onChange;
|
|
71
71
|
|
|
72
|
+
const contentRef = useRef(content);
|
|
73
|
+
contentRef.current = content;
|
|
74
|
+
|
|
72
75
|
const mountEditor = useCallback(
|
|
73
76
|
(node: HTMLDivElement | null) => {
|
|
74
77
|
if (editorRef.current) {
|
|
@@ -87,7 +90,7 @@ export const SourceEditor = memo(function SourceEditor({
|
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
const state = EditorState.create({
|
|
90
|
-
doc:
|
|
93
|
+
doc: contentRef.current,
|
|
91
94
|
extensions: [
|
|
92
95
|
lineNumbers(),
|
|
93
96
|
highlightActiveLine(),
|
|
@@ -112,8 +115,22 @@ export const SourceEditor = memo(function SourceEditor({
|
|
|
112
115
|
|
|
113
116
|
editorRef.current = new EditorView({ state, parent: node });
|
|
114
117
|
},
|
|
115
|
-
[
|
|
118
|
+
[filePath, language, readOnly],
|
|
116
119
|
);
|
|
117
120
|
|
|
121
|
+
// Sync external content changes into the editor without recreating it.
|
|
122
|
+
// Only applies when the new content differs from the current document
|
|
123
|
+
// (e.g. file switch or server refresh), not on every keystroke.
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const view = editorRef.current;
|
|
126
|
+
if (!view) return;
|
|
127
|
+
const current = view.state.doc.toString();
|
|
128
|
+
if (current !== content) {
|
|
129
|
+
view.dispatch({
|
|
130
|
+
changes: { from: 0, to: current.length, insert: content },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}, [content]);
|
|
134
|
+
|
|
118
135
|
return <div ref={mountEditor} className="h-full w-full overflow-hidden" />;
|
|
119
136
|
});
|
|
@@ -334,24 +334,43 @@ export function Section({
|
|
|
334
334
|
icon,
|
|
335
335
|
children,
|
|
336
336
|
accessory,
|
|
337
|
+
defaultCollapsed = false,
|
|
337
338
|
}: {
|
|
338
339
|
title: string;
|
|
339
340
|
icon: ReactNode;
|
|
340
341
|
children: ReactNode;
|
|
341
342
|
accessory?: ReactNode;
|
|
343
|
+
defaultCollapsed?: boolean;
|
|
342
344
|
}) {
|
|
345
|
+
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
346
|
+
|
|
343
347
|
return (
|
|
344
|
-
<section className="min-w-0 border-t border-neutral-800/80
|
|
345
|
-
<
|
|
348
|
+
<section className="min-w-0 border-t border-neutral-800/80">
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
onClick={() => setCollapsed((v) => !v)}
|
|
352
|
+
className="flex w-full items-center justify-between gap-2 px-4 py-3"
|
|
353
|
+
>
|
|
346
354
|
<div className="flex min-w-0 items-center gap-2.5">
|
|
347
355
|
<span className="flex-shrink-0 text-neutral-500">{icon}</span>
|
|
348
356
|
<h3 className="text-[11px] font-semibold uppercase tracking-[0.12em] text-neutral-300">
|
|
349
357
|
{title}
|
|
350
358
|
</h3>
|
|
351
359
|
</div>
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
<div className="flex items-center gap-2">
|
|
361
|
+
{accessory}
|
|
362
|
+
<svg
|
|
363
|
+
width="10"
|
|
364
|
+
height="10"
|
|
365
|
+
viewBox="0 0 10 10"
|
|
366
|
+
fill="currentColor"
|
|
367
|
+
className={`flex-shrink-0 text-neutral-500 transition-transform ${collapsed ? "-rotate-90" : ""}`}
|
|
368
|
+
>
|
|
369
|
+
<path d="M2 3l3 4 3-4z" />
|
|
370
|
+
</svg>
|
|
371
|
+
</div>
|
|
372
|
+
</button>
|
|
373
|
+
{!collapsed && <div className="px-4 pb-4">{children}</div>}
|
|
355
374
|
</section>
|
|
356
375
|
);
|
|
357
376
|
}
|
|
@@ -109,7 +109,7 @@ export function StyleSections({
|
|
|
109
109
|
return (
|
|
110
110
|
<>
|
|
111
111
|
{isFlex && (
|
|
112
|
-
<Section title="Flex" icon={<Layers size={15} />}>
|
|
112
|
+
<Section title="Flex" icon={<Layers size={15} />} defaultCollapsed>
|
|
113
113
|
<div className="space-y-4">
|
|
114
114
|
<SegmentedControl
|
|
115
115
|
disabled={styleEditingDisabled}
|
|
@@ -154,7 +154,7 @@ export function StyleSections({
|
|
|
154
154
|
)}
|
|
155
155
|
|
|
156
156
|
{hasVisualBackground && (
|
|
157
|
-
<Section title="Radius" icon={<Settings size={15} />}>
|
|
157
|
+
<Section title="Radius" icon={<Settings size={15} />} defaultCollapsed>
|
|
158
158
|
<SliderControl
|
|
159
159
|
value={radiusValue}
|
|
160
160
|
min={0}
|
|
@@ -168,7 +168,7 @@ export function StyleSections({
|
|
|
168
168
|
</Section>
|
|
169
169
|
)}
|
|
170
170
|
|
|
171
|
-
<Section title="Stroke" icon={<Square size={15} />}>
|
|
171
|
+
<Section title="Stroke" icon={<Square size={15} />} defaultCollapsed>
|
|
172
172
|
<div className="space-y-4">
|
|
173
173
|
<div className={RESPONSIVE_GRID}>
|
|
174
174
|
<MetricField
|
|
@@ -226,7 +226,7 @@ export function StyleSections({
|
|
|
226
226
|
</div>
|
|
227
227
|
</Section>
|
|
228
228
|
|
|
229
|
-
<Section title="Effects" icon={<Zap size={15} />}>
|
|
229
|
+
<Section title="Effects" icon={<Zap size={15} />} defaultCollapsed>
|
|
230
230
|
<div className="space-y-4">
|
|
231
231
|
<SelectField
|
|
232
232
|
label="Shadow"
|
|
@@ -279,7 +279,7 @@ export function StyleSections({
|
|
|
279
279
|
</div>
|
|
280
280
|
</Section>
|
|
281
281
|
|
|
282
|
-
<Section title="Clip" icon={<Layers size={15} />}>
|
|
282
|
+
<Section title="Clip" icon={<Layers size={15} />} defaultCollapsed>
|
|
283
283
|
<div className="space-y-4">
|
|
284
284
|
<div className={RESPONSIVE_GRID}>
|
|
285
285
|
<SelectField
|
|
@@ -325,7 +325,7 @@ export function StyleSections({
|
|
|
325
325
|
</div>
|
|
326
326
|
</Section>
|
|
327
327
|
|
|
328
|
-
<Section title="Transparency" icon={<Eye size={15} />}>
|
|
328
|
+
<Section title="Transparency" icon={<Eye size={15} />} defaultCollapsed>
|
|
329
329
|
<div className="space-y-4">
|
|
330
330
|
<SliderControl
|
|
331
331
|
value={opacityValue}
|
|
@@ -23,12 +23,7 @@ import {
|
|
|
23
23
|
restoreStudioPathOffset,
|
|
24
24
|
restoreStudioRotation,
|
|
25
25
|
} from "./manualEdits";
|
|
26
|
-
import {
|
|
27
|
-
type GroupOverlayItem,
|
|
28
|
-
type OverlayRect,
|
|
29
|
-
groupOverlayItemsEqual,
|
|
30
|
-
rectsEqual,
|
|
31
|
-
} from "./domEditOverlayGeometry";
|
|
26
|
+
import { type GroupOverlayItem, type OverlayRect } from "./domEditOverlayGeometry";
|
|
32
27
|
import {
|
|
33
28
|
BLOCKED_MOVE_THRESHOLD_PX,
|
|
34
29
|
type BlockedMoveState,
|
|
@@ -87,8 +82,6 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
87
82
|
|
|
88
83
|
export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
|
|
89
84
|
const setDraftOverlayRect = (next: OverlayRect) => {
|
|
90
|
-
if (rectsEqual(opts.overlayRectRef.current, next)) return;
|
|
91
|
-
opts.overlayRectRef.current = next;
|
|
92
85
|
opts.setOverlayRect(next);
|
|
93
86
|
};
|
|
94
87
|
const restoreGestureOverlayRect = (g: GestureState) => {
|
|
@@ -102,8 +95,6 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
102
95
|
});
|
|
103
96
|
};
|
|
104
97
|
const setDraftGroupOverlayItems = (next: GroupOverlayItem[]) => {
|
|
105
|
-
if (groupOverlayItemsEqual(opts.groupOverlayItemsRef.current, next)) return;
|
|
106
|
-
opts.groupOverlayItemsRef.current = next;
|
|
107
98
|
opts.setGroupOverlayItems(next);
|
|
108
99
|
};
|
|
109
100
|
|
|
@@ -247,9 +247,11 @@ export const NLELayout = memo(function NLELayout({
|
|
|
247
247
|
const currentLevel = compositionStack[compositionStack.length - 1];
|
|
248
248
|
const directUrl = compositionStack.length > 1 ? currentLevel.previewUrl : undefined;
|
|
249
249
|
|
|
250
|
+
const onIframeRefStable = useRef(onIframeRef);
|
|
251
|
+
onIframeRefStable.current = onIframeRef;
|
|
250
252
|
useEffect(() => {
|
|
251
|
-
|
|
252
|
-
}, [compositionStack.length,
|
|
253
|
+
onIframeRefStable.current?.(iframeRef.current);
|
|
254
|
+
}, [compositionStack.length, refreshKey, iframeRef]);
|
|
253
255
|
|
|
254
256
|
// Resize divider handlers
|
|
255
257
|
const handleDividerPointerDown = useCallback(
|