@hyperframes/studio 0.7.37 → 0.7.39
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-BBrKzTGd.js +459 -0
- package/dist/assets/index-0P10SwC_.css +1 -0
- package/dist/assets/{index-BXYJbIax.js → index-312a3Ceu.js} +1 -1
- package/dist/assets/index-B9YvRJz1.js +396 -0
- package/dist/assets/{index-Ct-X2wlf.js → index-CQHiecE7.js} +1 -1
- package/dist/{chunk-JND3XUJL.js → chunk-RCLGSZ7C.js} +1 -1
- package/dist/chunk-RCLGSZ7C.js.map +1 -0
- package/dist/{domEditingLayers-UIQZJCOA.js → domEditingLayers-S6YOLVRG.js} +2 -2
- package/dist/index.d.ts +48 -10
- package/dist/index.html +2 -2
- package/dist/index.js +6045 -3507
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -24
- package/src/components/AskAgentModal.tsx +23 -6
- package/src/components/LintModal.tsx +40 -10
- package/src/components/MediaPreview.tsx +36 -1
- package/src/components/SaveQueuePausedBanner.tsx +7 -6
- package/src/components/StudioErrorBoundary.tsx +19 -31
- package/src/components/StudioFeedbackBar.tsx +11 -2
- package/src/components/StudioGlobalDragOverlay.tsx +1 -1
- package/src/components/StudioHeader.tsx +184 -106
- package/src/components/StudioLeftSidebar.tsx +24 -3
- package/src/components/StudioOverlays.tsx +34 -8
- package/src/components/StudioPreviewArea.tsx +8 -0
- package/src/components/StudioRightPanel.tsx +160 -9
- package/src/components/StudioSplash.tsx +6 -3
- package/src/components/StudioToast.tsx +12 -11
- package/src/components/TimelineToolbar.test.tsx +9 -3
- package/src/components/TimelineToolbar.tsx +63 -29
- package/src/components/editor/DomEditCropHandles.tsx +238 -0
- package/src/components/editor/DomEditOverlay.tsx +111 -108
- package/src/components/editor/DomEditRotateHandle.tsx +41 -0
- package/src/components/editor/PropertyPanel.test.ts +48 -0
- package/src/components/editor/PropertyPanel.tsx +27 -47
- package/src/components/editor/PropertyPanelEmptyState.tsx +33 -0
- package/src/components/editor/SnapToolbar.tsx +20 -1
- package/src/components/editor/clipPathHelpers.ts +113 -0
- package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
- package/src/components/editor/colorGradingScopePatch.ts +57 -0
- package/src/components/editor/domEditOverlayCrop.test.ts +106 -0
- package/src/components/editor/domEditOverlayCrop.ts +115 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayShape.ts +39 -0
- package/src/components/editor/domEditing.test.ts +2 -0
- package/src/components/editor/domEditingTypes.ts +3 -0
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +0 -6
- package/src/components/editor/marqueeCommit.ts +3 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
- package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
- package/src/components/editor/propertyPanelHelpers.ts +32 -125
- package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
- package/src/components/editor/propertyPanelStyleSections.tsx +75 -1
- package/src/components/editor/propertyPanelTypes.ts +113 -0
- package/src/components/editor/snapTargetCollection.ts +2 -3
- package/src/components/editor/useDomEditCompositionRect.ts +68 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +13 -6
- package/src/components/editor/useDomEditOverlayRects.ts +5 -3
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
- package/src/components/nle/NLELayout.tsx +54 -51
- package/src/components/nle/NLEPreview.tsx +41 -5
- package/src/components/nle/TimelineResizeDivider.tsx +97 -0
- package/src/components/nle/usePreviewBlockDrop.ts +31 -7
- package/src/components/renders/RenderQueue.tsx +125 -24
- package/src/components/renders/RenderQueueItem.tsx +132 -64
- package/src/components/renders/useRenderQueue.ts +136 -24
- package/src/components/sidebar/AssetsTab.test.ts +87 -0
- package/src/components/sidebar/AssetsTab.tsx +113 -15
- package/src/components/sidebar/BlocksTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +91 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
- package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
- package/src/components/storyboard/StoryboardView.tsx +6 -0
- package/src/components/studioColorGradingScope.ts +124 -0
- package/src/components/studioMediaJobs.ts +123 -0
- package/src/components/ui/Button.tsx +20 -11
- package/src/components/ui/HyperframesLoader.tsx +9 -2
- package/src/components/ui/SearchInput.tsx +53 -0
- package/src/components/ui/Tooltip.tsx +52 -6
- package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
- package/src/components/ui/useDialogBehavior.ts +83 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +7 -0
- package/src/hooks/domSelectionTestHarness.ts +1 -0
- package/src/hooks/useCompositionContentLoader.ts +40 -0
- package/src/hooks/useCropMode.ts +91 -0
- package/src/hooks/useDomEditCommits.test.tsx +2 -0
- package/src/hooks/useEditorSave.ts +14 -0
- package/src/hooks/useFileManager.ts +39 -25
- package/src/hooks/useFrameCapture.ts +12 -1
- package/src/hooks/usePanelLayout.ts +1 -0
- package/src/hooks/usePreviewPersistence.ts +24 -15
- package/src/hooks/useStudioContextValue.ts +5 -0
- package/src/hooks/useToast.ts +66 -13
- package/src/player/components/ShortcutsPanel.tsx +9 -0
- package/src/player/store/playerStore.ts +14 -0
- package/src/styles/studio.css +61 -0
- package/src/utils/sdkResolverShadow.test.ts +53 -3
- package/src/utils/sdkResolverShadow.ts +41 -1
- package/src/utils/studioPendingEdits.test.ts +43 -0
- package/src/utils/studioPendingEdits.ts +45 -0
- package/src/utils/studioUiPreferences.ts +4 -0
- package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
- package/dist/assets/index-DmkOvZns.css +0 -1
- package/dist/assets/index-SYSbQyK4.js +0 -396
- package/dist/chunk-JND3XUJL.js.map +0 -1
- /package/dist/{domEditingLayers-UIQZJCOA.js.map → domEditingLayers-S6YOLVRG.js.map} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useRef, type MouseEvent } from "react";
|
|
2
2
|
import { RotateCcw, RotateCw, Camera } from "../icons/SystemIcons";
|
|
3
3
|
import {
|
|
4
4
|
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
@@ -9,12 +9,14 @@ import { useStudioShellContext } from "../contexts/StudioContext";
|
|
|
9
9
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
10
10
|
import { useViewMode, type StudioViewMode } from "../contexts/ViewModeContext";
|
|
11
11
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
12
|
+
import { Tooltip } from "./ui";
|
|
12
13
|
|
|
13
14
|
export interface StudioHeaderProps {
|
|
14
15
|
captureFrameHref: string;
|
|
15
16
|
captureFrameFilename: string;
|
|
16
17
|
handleCaptureFrameClick: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
17
18
|
refreshCaptureFrameTime: () => void;
|
|
19
|
+
capturing?: boolean;
|
|
18
20
|
inspectorButtonActive: boolean;
|
|
19
21
|
inspectorPanelActive: boolean;
|
|
20
22
|
onExport?: () => void;
|
|
@@ -149,26 +151,45 @@ const VIEW_MODE_OPTIONS: Array<{ mode: StudioViewMode; label: string }> = [
|
|
|
149
151
|
/** Segmented control switching the main stage between storyboard and preview. */
|
|
150
152
|
function ViewModeToggle() {
|
|
151
153
|
const { viewMode, setViewMode } = useViewMode();
|
|
154
|
+
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
155
|
+
|
|
156
|
+
const selectMode = (mode: StudioViewMode) => {
|
|
157
|
+
if (mode === viewMode) return;
|
|
158
|
+
trackStudioEvent("view_mode_toggle", { mode });
|
|
159
|
+
setViewMode(mode);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Complete APG tabs pattern: roving tabIndex + arrow-key navigation.
|
|
163
|
+
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
|
|
164
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
const dir = e.key === "ArrowLeft" ? -1 : 1;
|
|
167
|
+
const next = (index + dir + VIEW_MODE_OPTIONS.length) % VIEW_MODE_OPTIONS.length;
|
|
168
|
+
tabRefs.current[next]?.focus();
|
|
169
|
+
selectMode(VIEW_MODE_OPTIONS[next].mode);
|
|
170
|
+
};
|
|
171
|
+
|
|
152
172
|
return (
|
|
153
173
|
<div
|
|
154
174
|
className="flex items-center gap-0.5 rounded-md bg-neutral-800 p-0.5"
|
|
155
175
|
role="tablist"
|
|
156
176
|
aria-label="Studio view"
|
|
157
177
|
>
|
|
158
|
-
{VIEW_MODE_OPTIONS.map(({ mode, label }) => {
|
|
178
|
+
{VIEW_MODE_OPTIONS.map(({ mode, label }, index) => {
|
|
159
179
|
const active = viewMode === mode;
|
|
160
180
|
return (
|
|
161
181
|
<button
|
|
162
182
|
key={mode}
|
|
183
|
+
ref={(el) => {
|
|
184
|
+
tabRefs.current[index] = el;
|
|
185
|
+
}}
|
|
163
186
|
type="button"
|
|
164
187
|
role="tab"
|
|
165
188
|
aria-selected={active}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}}
|
|
171
|
-
className={`rounded px-3 py-1 text-[11px] font-medium transition-colors ${
|
|
189
|
+
tabIndex={active ? 0 : -1}
|
|
190
|
+
onClick={() => selectMode(mode)}
|
|
191
|
+
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
192
|
+
className={`rounded px-3 py-1 text-[11px] font-medium transition-colors active:scale-[0.98] outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-studio-accent ${
|
|
172
193
|
active ? "bg-neutral-200 text-neutral-900" : "text-neutral-400 hover:text-neutral-200"
|
|
173
194
|
}`}
|
|
174
195
|
>
|
|
@@ -186,12 +207,14 @@ export function StudioHeader({
|
|
|
186
207
|
captureFrameFilename,
|
|
187
208
|
handleCaptureFrameClick,
|
|
188
209
|
refreshCaptureFrameTime,
|
|
210
|
+
capturing,
|
|
189
211
|
inspectorButtonActive,
|
|
190
212
|
inspectorPanelActive,
|
|
191
213
|
onExport,
|
|
192
214
|
}: StudioHeaderProps) {
|
|
193
|
-
const { projectId, editHistory, handleUndo, handleRedo } = useStudioShellContext();
|
|
215
|
+
const { projectId, editHistory, handleUndo, handleRedo, renderQueue } = useStudioShellContext();
|
|
194
216
|
const { rightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext();
|
|
217
|
+
const isRendering = renderQueue.isRendering;
|
|
195
218
|
|
|
196
219
|
return (
|
|
197
220
|
<div className="flex items-center justify-between h-10 px-3 bg-neutral-900 border-b border-neutral-800 flex-shrink-0">
|
|
@@ -207,118 +230,173 @@ export function StudioHeader({
|
|
|
207
230
|
<ViewModeToggle />
|
|
208
231
|
{/* Right: toolbar buttons */}
|
|
209
232
|
<div className="flex items-center gap-1.5">
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
onClick={() => {
|
|
213
|
-
trackStudioEvent("toolbar_action", { action: "undo" });
|
|
214
|
-
void handleUndo();
|
|
215
|
-
}}
|
|
216
|
-
disabled={!editHistory.canUndo}
|
|
217
|
-
className={`h-7 w-7 flex items-center justify-center rounded-md transition-colors ${
|
|
218
|
-
editHistory.canUndo
|
|
219
|
-
? "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800"
|
|
220
|
-
: "text-neutral-700 cursor-default"
|
|
221
|
-
}`}
|
|
222
|
-
title={
|
|
233
|
+
<Tooltip
|
|
234
|
+
label={
|
|
223
235
|
editHistory.undoLabel
|
|
224
236
|
? `Undo ${editHistory.undoLabel} (${getHistoryShortcutLabel("undo")})`
|
|
225
237
|
: `Undo (${getHistoryShortcutLabel("undo")})`
|
|
226
238
|
}
|
|
227
|
-
|
|
239
|
+
side="bottom"
|
|
228
240
|
>
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
onClick={() => {
|
|
244
|
+
trackStudioEvent("toolbar_action", { action: "undo" });
|
|
245
|
+
void handleUndo();
|
|
246
|
+
}}
|
|
247
|
+
disabled={!editHistory.canUndo}
|
|
248
|
+
className={`h-7 w-7 flex items-center justify-center rounded-md transition-colors active:scale-[0.98] ${
|
|
249
|
+
editHistory.canUndo
|
|
250
|
+
? "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800"
|
|
251
|
+
: "text-neutral-700 cursor-default"
|
|
252
|
+
}`}
|
|
253
|
+
aria-label="Undo"
|
|
254
|
+
>
|
|
255
|
+
<RotateCcw size={14} />
|
|
256
|
+
</button>
|
|
257
|
+
</Tooltip>
|
|
258
|
+
<Tooltip
|
|
259
|
+
label={
|
|
244
260
|
editHistory.redoLabel
|
|
245
261
|
? `Redo ${editHistory.redoLabel} (${getHistoryShortcutLabel("redo")})`
|
|
246
262
|
: `Redo (${getHistoryShortcutLabel("redo")})`
|
|
247
263
|
}
|
|
248
|
-
|
|
249
|
-
>
|
|
250
|
-
<RotateCw size={14} />
|
|
251
|
-
</button>
|
|
252
|
-
<a
|
|
253
|
-
href={captureFrameHref}
|
|
254
|
-
download={captureFrameFilename}
|
|
255
|
-
onClick={(e) => {
|
|
256
|
-
trackStudioEvent("toolbar_action", { action: "capture_frame" });
|
|
257
|
-
handleCaptureFrameClick(e);
|
|
258
|
-
}}
|
|
259
|
-
onFocus={refreshCaptureFrameTime}
|
|
260
|
-
onPointerDown={refreshCaptureFrameTime}
|
|
261
|
-
className="h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium text-neutral-400 transition-colors hover:text-neutral-200 hover:bg-neutral-800"
|
|
262
|
-
title="Capture current frame"
|
|
263
|
-
aria-label="Capture current frame"
|
|
264
|
+
side="bottom"
|
|
264
265
|
>
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
onClick={() => {
|
|
269
|
+
trackStudioEvent("toolbar_action", { action: "redo" });
|
|
270
|
+
void handleRedo();
|
|
271
|
+
}}
|
|
272
|
+
disabled={!editHistory.canRedo}
|
|
273
|
+
className={`h-7 w-7 flex items-center justify-center rounded-md transition-colors active:scale-[0.98] ${
|
|
274
|
+
editHistory.canRedo
|
|
275
|
+
? "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800"
|
|
276
|
+
: "text-neutral-700 cursor-default"
|
|
277
|
+
}`}
|
|
278
|
+
aria-label="Redo"
|
|
279
|
+
>
|
|
280
|
+
<RotateCw size={14} />
|
|
281
|
+
</button>
|
|
282
|
+
</Tooltip>
|
|
283
|
+
<Tooltip label={capturing ? "Capturing frame…" : "Capture current frame"} side="bottom">
|
|
284
|
+
<a
|
|
285
|
+
href={captureFrameHref}
|
|
286
|
+
download={captureFrameFilename}
|
|
287
|
+
onClick={(e) => {
|
|
288
|
+
if (capturing) {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
trackStudioEvent("toolbar_action", { action: "capture_frame" });
|
|
293
|
+
handleCaptureFrameClick(e);
|
|
294
|
+
}}
|
|
295
|
+
onFocus={refreshCaptureFrameTime}
|
|
296
|
+
onPointerDown={refreshCaptureFrameTime}
|
|
297
|
+
aria-disabled={capturing || undefined}
|
|
298
|
+
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium transition-colors ${
|
|
299
|
+
capturing
|
|
300
|
+
? "text-neutral-600 cursor-default"
|
|
301
|
+
: "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 active:scale-[0.98]"
|
|
302
|
+
}`}
|
|
303
|
+
aria-label={capturing ? "Capturing frame" : "Capture current frame"}
|
|
304
|
+
>
|
|
305
|
+
{capturing ? (
|
|
306
|
+
<svg
|
|
307
|
+
className="animate-spin motion-reduce:animate-none h-3.5 w-3.5"
|
|
308
|
+
viewBox="0 0 24 24"
|
|
309
|
+
fill="none"
|
|
310
|
+
aria-hidden="true"
|
|
311
|
+
>
|
|
312
|
+
<circle
|
|
313
|
+
className="opacity-25"
|
|
314
|
+
cx="12"
|
|
315
|
+
cy="12"
|
|
316
|
+
r="10"
|
|
317
|
+
stroke="currentColor"
|
|
318
|
+
strokeWidth="4"
|
|
319
|
+
/>
|
|
320
|
+
<path
|
|
321
|
+
className="opacity-75"
|
|
322
|
+
fill="currentColor"
|
|
323
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
324
|
+
/>
|
|
325
|
+
</svg>
|
|
326
|
+
) : (
|
|
327
|
+
<Camera size={14} />
|
|
328
|
+
)}
|
|
329
|
+
<span>{capturing ? "Capturing…" : "Capture"}</span>
|
|
330
|
+
</a>
|
|
331
|
+
</Tooltip>
|
|
332
|
+
<Tooltip
|
|
333
|
+
label={
|
|
295
334
|
STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
|
|
296
335
|
}
|
|
336
|
+
side="bottom"
|
|
297
337
|
>
|
|
298
|
-
<
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
onClick={() => {
|
|
341
|
+
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
342
|
+
if (rightCollapsed || !inspectorPanelActive) {
|
|
343
|
+
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
344
|
+
setRightPanelTab("design");
|
|
345
|
+
setRightCollapsed(false);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: true });
|
|
349
|
+
// Keep the current selection when collapsing the Inspector — closing
|
|
350
|
+
// the panel shouldn't deselect the element.
|
|
351
|
+
setRightCollapsed(true);
|
|
352
|
+
}}
|
|
353
|
+
disabled={!STUDIO_INSPECTOR_PANELS_ENABLED}
|
|
354
|
+
aria-pressed={inspectorButtonActive}
|
|
355
|
+
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors active:scale-[0.98] ${
|
|
356
|
+
inspectorButtonActive
|
|
357
|
+
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
358
|
+
: STUDIO_INSPECTOR_PANELS_ENABLED
|
|
359
|
+
? "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
|
|
360
|
+
: "cursor-not-allowed border-transparent text-neutral-700"
|
|
361
|
+
}`}
|
|
362
|
+
aria-label={
|
|
363
|
+
STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
|
|
364
|
+
}
|
|
305
365
|
>
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
366
|
+
<svg
|
|
367
|
+
width="12"
|
|
368
|
+
height="12"
|
|
369
|
+
viewBox="0 0 24 24"
|
|
370
|
+
fill="none"
|
|
371
|
+
stroke="currentColor"
|
|
372
|
+
strokeWidth="2"
|
|
373
|
+
>
|
|
374
|
+
<circle cx="12" cy="12" r="10" />
|
|
375
|
+
<polygon points="10 8 16 12 10 16" fill="currentColor" stroke="none" />
|
|
376
|
+
</svg>
|
|
377
|
+
Inspector
|
|
378
|
+
</button>
|
|
379
|
+
</Tooltip>
|
|
380
|
+
<Tooltip
|
|
381
|
+
label={
|
|
382
|
+
isRendering ? "A render is already in progress" : "Render and export this composition"
|
|
383
|
+
}
|
|
384
|
+
side="bottom"
|
|
319
385
|
>
|
|
320
|
-
|
|
321
|
-
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
disabled={isRendering}
|
|
389
|
+
onClick={() => {
|
|
390
|
+
if (isRendering) return;
|
|
391
|
+
setRightPanelTab("renders");
|
|
392
|
+
setRightCollapsed(false);
|
|
393
|
+
onExport?.();
|
|
394
|
+
}}
|
|
395
|
+
className="h-7 flex items-center gap-1.5 px-3 rounded-md text-[11px] font-semibold bg-studio-accent text-[#09090B] enabled:hover:brightness-110 transition-[filter,transform] enabled:active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
|
|
396
|
+
>
|
|
397
|
+
{isRendering ? "Rendering…" : "Export"}
|
|
398
|
+
</button>
|
|
399
|
+
</Tooltip>
|
|
322
400
|
</div>
|
|
323
401
|
</div>
|
|
324
402
|
);
|
|
@@ -34,6 +34,7 @@ export function StudioLeftSidebar({
|
|
|
34
34
|
const {
|
|
35
35
|
leftCollapsed,
|
|
36
36
|
leftWidth,
|
|
37
|
+
setLeftWidth,
|
|
37
38
|
toggleLeftSidebar,
|
|
38
39
|
handlePanelResizeStart,
|
|
39
40
|
handlePanelResizeMove,
|
|
@@ -114,14 +115,22 @@ export function StudioLeftSidebar({
|
|
|
114
115
|
onRenameFile={handleRenameFile}
|
|
115
116
|
onDuplicateFile={handleDuplicateFile}
|
|
116
117
|
onMoveFile={handleMoveFile}
|
|
117
|
-
onImportFiles={
|
|
118
|
+
onImportFiles={async (files, dir) => {
|
|
119
|
+
await handleImportFiles(files, dir);
|
|
120
|
+
}}
|
|
118
121
|
codeChildren={
|
|
119
122
|
editingFile ? (
|
|
120
123
|
isMediaFile(editingFile.path) ? (
|
|
121
124
|
<MediaPreview projectId={projectId} filePath={editingFile.path} />
|
|
125
|
+
) : editingFile.content == null ? (
|
|
126
|
+
// Never mount the editor on unloaded content: a keystroke would
|
|
127
|
+
// autosave an empty document over the real file.
|
|
128
|
+
<div className="flex h-full items-center justify-center text-[11px] text-neutral-600">
|
|
129
|
+
Loading {editingFile.path}…
|
|
130
|
+
</div>
|
|
122
131
|
) : (
|
|
123
132
|
<SourceEditor
|
|
124
|
-
content={editingFile.content
|
|
133
|
+
content={editingFile.content}
|
|
125
134
|
filePath={editingFile.path}
|
|
126
135
|
onChange={handleContentChange}
|
|
127
136
|
revealOffset={revealSourceOffset}
|
|
@@ -140,11 +149,23 @@ export function StudioLeftSidebar({
|
|
|
140
149
|
onPreviewBlock={onPreviewBlock}
|
|
141
150
|
/>
|
|
142
151
|
<div
|
|
143
|
-
|
|
152
|
+
role="separator"
|
|
153
|
+
aria-label="Resize sidebar"
|
|
154
|
+
aria-orientation="vertical"
|
|
155
|
+
tabIndex={0}
|
|
156
|
+
className="group w-2 flex-shrink-0 cursor-col-resize flex items-center justify-center outline-none focus-visible:bg-studio-accent/20"
|
|
144
157
|
style={{ touchAction: "none" }}
|
|
145
158
|
onPointerDown={(e) => handlePanelResizeStart("left", e)}
|
|
146
159
|
onPointerMove={handlePanelResizeMove}
|
|
147
160
|
onPointerUp={handlePanelResizeEnd}
|
|
161
|
+
onPointerCancel={handlePanelResizeEnd}
|
|
162
|
+
onKeyDown={(e) => {
|
|
163
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
const delta = e.key === "ArrowLeft" ? -16 : 16;
|
|
166
|
+
const maxLeft = Math.floor(window.innerWidth * 0.5);
|
|
167
|
+
setLeftWidth(Math.max(160, Math.min(maxLeft, leftWidth + delta)));
|
|
168
|
+
}}
|
|
148
169
|
>
|
|
149
170
|
<div className="h-[52px] w-px bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
|
|
150
171
|
</div>
|
|
@@ -11,6 +11,7 @@ type LintFindings = ComponentProps<typeof LintModal>["findings"];
|
|
|
11
11
|
|
|
12
12
|
export interface StudioOverlaysProps {
|
|
13
13
|
projectId: string;
|
|
14
|
+
projectDir?: string | null;
|
|
14
15
|
lintModal: LintFindings | null;
|
|
15
16
|
closeLintModal: () => void;
|
|
16
17
|
consoleErrors: LintFindings | null;
|
|
@@ -18,8 +19,8 @@ export interface StudioOverlaysProps {
|
|
|
18
19
|
domEditSession: ReturnType<typeof useDomEditSession>;
|
|
19
20
|
activeCompPath: string | null;
|
|
20
21
|
dragOverlayActive: boolean;
|
|
21
|
-
|
|
22
|
-
dismissToast: () => void;
|
|
22
|
+
toasts: ReturnType<typeof useToast>["toasts"];
|
|
23
|
+
dismissToast: (id: number) => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -30,6 +31,7 @@ export interface StudioOverlaysProps {
|
|
|
30
31
|
// fallow-ignore-next-line complexity
|
|
31
32
|
export function StudioOverlays({
|
|
32
33
|
projectId,
|
|
34
|
+
projectDir,
|
|
33
35
|
lintModal,
|
|
34
36
|
closeLintModal,
|
|
35
37
|
consoleErrors,
|
|
@@ -37,16 +39,30 @@ export function StudioOverlays({
|
|
|
37
39
|
domEditSession,
|
|
38
40
|
activeCompPath,
|
|
39
41
|
dragOverlayActive,
|
|
40
|
-
|
|
42
|
+
toasts,
|
|
41
43
|
dismissToast,
|
|
42
44
|
}: StudioOverlaysProps) {
|
|
43
45
|
return (
|
|
44
46
|
<>
|
|
45
47
|
{lintModal !== null && (
|
|
46
|
-
<LintModal
|
|
48
|
+
<LintModal
|
|
49
|
+
findings={lintModal}
|
|
50
|
+
projectId={projectId}
|
|
51
|
+
projectDir={projectDir}
|
|
52
|
+
onClose={closeLintModal}
|
|
53
|
+
/>
|
|
47
54
|
)}
|
|
48
|
-
{
|
|
49
|
-
|
|
55
|
+
{/* One modal at a time — console errors wait behind an open lint modal
|
|
56
|
+
instead of stacking two full-screen overlays. */}
|
|
57
|
+
{lintModal === null && consoleErrors !== null && consoleErrors.length > 0 && (
|
|
58
|
+
<LintModal
|
|
59
|
+
findings={consoleErrors}
|
|
60
|
+
projectId={projectId}
|
|
61
|
+
projectDir={projectDir}
|
|
62
|
+
title="Console errors in preview"
|
|
63
|
+
promptIntro="Fix these runtime console errors from the composition preview"
|
|
64
|
+
onClose={clearConsoleErrors}
|
|
65
|
+
/>
|
|
50
66
|
)}
|
|
51
67
|
{domEditSession.agentModalOpen && domEditSession.domEditSelection && (
|
|
52
68
|
<AskAgentModal
|
|
@@ -62,8 +78,18 @@ export function StudioOverlays({
|
|
|
62
78
|
/>
|
|
63
79
|
)}
|
|
64
80
|
{dragOverlayActive && <StudioGlobalDragOverlay />}
|
|
65
|
-
{
|
|
66
|
-
<
|
|
81
|
+
{toasts.length > 0 && (
|
|
82
|
+
<div className="absolute bottom-6 right-6 z-[91] flex flex-col items-end gap-2">
|
|
83
|
+
{toasts.map((toast) => (
|
|
84
|
+
<StudioToast
|
|
85
|
+
key={toast.id}
|
|
86
|
+
message={toast.message}
|
|
87
|
+
tone={toast.tone}
|
|
88
|
+
leaving={toast.leaving}
|
|
89
|
+
onDismiss={() => dismissToast(toast.id)}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
67
93
|
)}
|
|
68
94
|
</>
|
|
69
95
|
);
|
|
@@ -69,6 +69,8 @@ export interface StudioPreviewAreaProps {
|
|
|
69
69
|
isGestureRecording?: boolean;
|
|
70
70
|
recordingState?: GestureRecordingState;
|
|
71
71
|
onToggleRecording?: () => void;
|
|
72
|
+
cropMode?: boolean;
|
|
73
|
+
onCropModeChange?: (active: boolean) => void;
|
|
72
74
|
gestureOverlay?: ReactNode;
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -93,6 +95,8 @@ export function StudioPreviewArea({
|
|
|
93
95
|
isGestureRecording,
|
|
94
96
|
recordingState,
|
|
95
97
|
onToggleRecording,
|
|
98
|
+
cropMode,
|
|
99
|
+
onCropModeChange,
|
|
96
100
|
blockPreview,
|
|
97
101
|
gestureOverlay,
|
|
98
102
|
}: StudioPreviewAreaProps) {
|
|
@@ -132,6 +136,7 @@ export function StudioPreviewArea({
|
|
|
132
136
|
handleDomGroupPathOffsetCommit,
|
|
133
137
|
handleDomBoxSizeCommit,
|
|
134
138
|
handleDomRotationCommit,
|
|
139
|
+
handleDomStyleCommit,
|
|
135
140
|
handleGsapRemoveKeyframe,
|
|
136
141
|
handleGsapMoveKeyframeToPlayhead,
|
|
137
142
|
handleGsapMoveKeyframe,
|
|
@@ -375,6 +380,9 @@ export function StudioPreviewArea({
|
|
|
375
380
|
onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
|
|
376
381
|
onBoxSizeCommit={handleDomBoxSizeCommit}
|
|
377
382
|
onRotationCommit={handleDomRotationCommit}
|
|
383
|
+
onStyleCommit={handleDomStyleCommit}
|
|
384
|
+
cropMode={cropMode}
|
|
385
|
+
onCropModeChange={onCropModeChange}
|
|
378
386
|
gridVisible={snapPrefs.gridVisible}
|
|
379
387
|
gridSpacing={snapPrefs.gridSpacing}
|
|
380
388
|
recordingState={recordingState}
|