@hyperframes/studio 0.7.37 → 0.7.38

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.
Files changed (75) hide show
  1. package/dist/assets/hyperframes-player-BBrKzTGd.js +459 -0
  2. package/dist/assets/index-BLRTwY5l.js +396 -0
  3. package/dist/assets/{index-BXYJbIax.js → index-ClUipc8i.js} +1 -1
  4. package/dist/assets/index-DJaiR8T2.css +1 -0
  5. package/dist/assets/{index-Ct-X2wlf.js → index-Ykq7ihge.js} +1 -1
  6. package/dist/index.d.ts +35 -10
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +5146 -3321
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +19 -18
  12. package/src/components/AskAgentModal.tsx +23 -6
  13. package/src/components/LintModal.tsx +40 -10
  14. package/src/components/MediaPreview.tsx +36 -1
  15. package/src/components/SaveQueuePausedBanner.tsx +7 -6
  16. package/src/components/StudioErrorBoundary.tsx +19 -31
  17. package/src/components/StudioFeedbackBar.tsx +11 -2
  18. package/src/components/StudioGlobalDragOverlay.tsx +1 -1
  19. package/src/components/StudioHeader.tsx +184 -106
  20. package/src/components/StudioLeftSidebar.tsx +24 -3
  21. package/src/components/StudioOverlays.tsx +34 -8
  22. package/src/components/StudioRightPanel.tsx +154 -9
  23. package/src/components/StudioSplash.tsx +6 -3
  24. package/src/components/StudioToast.tsx +12 -11
  25. package/src/components/TimelineToolbar.test.tsx +9 -3
  26. package/src/components/TimelineToolbar.tsx +63 -29
  27. package/src/components/editor/PropertyPanel.tsx +18 -17
  28. package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
  29. package/src/components/editor/colorGradingScopePatch.ts +57 -0
  30. package/src/components/editor/manualEditingAvailability.test.ts +0 -12
  31. package/src/components/editor/manualEditingAvailability.ts +0 -6
  32. package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
  33. package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
  34. package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
  35. package/src/components/editor/propertyPanelHelpers.ts +22 -91
  36. package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
  37. package/src/components/editor/propertyPanelTypes.ts +111 -0
  38. package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
  39. package/src/components/nle/NLELayout.tsx +54 -51
  40. package/src/components/nle/NLEPreview.tsx +41 -5
  41. package/src/components/nle/TimelineResizeDivider.tsx +97 -0
  42. package/src/components/nle/usePreviewBlockDrop.ts +31 -7
  43. package/src/components/renders/RenderQueue.tsx +125 -24
  44. package/src/components/renders/RenderQueueItem.tsx +132 -64
  45. package/src/components/renders/useRenderQueue.ts +136 -24
  46. package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
  47. package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
  48. package/src/components/storyboard/StoryboardView.tsx +6 -0
  49. package/src/components/studioColorGradingScope.ts +124 -0
  50. package/src/components/studioMediaJobs.ts +123 -0
  51. package/src/components/ui/Button.tsx +20 -11
  52. package/src/components/ui/HyperframesLoader.tsx +9 -2
  53. package/src/components/ui/SearchInput.tsx +53 -0
  54. package/src/components/ui/Tooltip.tsx +52 -6
  55. package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
  56. package/src/components/ui/useDialogBehavior.ts +83 -0
  57. package/src/contexts/PanelLayoutContext.tsx +3 -0
  58. package/src/contexts/StudioContext.tsx +7 -0
  59. package/src/hooks/useCompositionContentLoader.ts +40 -0
  60. package/src/hooks/useEditorSave.ts +14 -0
  61. package/src/hooks/useFileManager.ts +39 -25
  62. package/src/hooks/useFrameCapture.ts +12 -1
  63. package/src/hooks/usePanelLayout.ts +1 -0
  64. package/src/hooks/usePreviewPersistence.ts +24 -15
  65. package/src/hooks/useStudioContextValue.ts +5 -0
  66. package/src/hooks/useToast.ts +66 -13
  67. package/src/styles/studio.css +61 -0
  68. package/src/utils/sdkResolverShadow.test.ts +53 -3
  69. package/src/utils/sdkResolverShadow.ts +41 -1
  70. package/src/utils/studioPendingEdits.test.ts +43 -0
  71. package/src/utils/studioPendingEdits.ts +45 -0
  72. package/src/utils/studioUiPreferences.ts +4 -0
  73. package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
  74. package/dist/assets/index-DmkOvZns.css +0 -1
  75. package/dist/assets/index-SYSbQyK4.js +0 -396
@@ -1,4 +1,4 @@
1
- import type { MouseEvent } from "react";
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
- onClick={() => {
167
- if (active) return;
168
- trackStudioEvent("view_mode_toggle", { mode });
169
- setViewMode(mode);
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
- <button
211
- type="button"
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
- aria-label="Undo"
239
+ side="bottom"
228
240
  >
229
- <RotateCcw size={14} />
230
- </button>
231
- <button
232
- type="button"
233
- onClick={() => {
234
- trackStudioEvent("toolbar_action", { action: "redo" });
235
- void handleRedo();
236
- }}
237
- disabled={!editHistory.canRedo}
238
- className={`h-7 w-7 flex items-center justify-center rounded-md transition-colors ${
239
- editHistory.canRedo
240
- ? "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800"
241
- : "text-neutral-700 cursor-default"
242
- }`}
243
- title={
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
- aria-label="Redo"
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
- <Camera size={14} />
266
- <span>Capture</span>
267
- </a>
268
- <button
269
- type="button"
270
- onClick={() => {
271
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
272
- if (rightCollapsed || !inspectorPanelActive) {
273
- trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
274
- setRightPanelTab("design");
275
- setRightCollapsed(false);
276
- return;
277
- }
278
- trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: true });
279
- // Keep the current selection when collapsing the Inspector — closing
280
- // the panel shouldn't deselect the element.
281
- setRightCollapsed(true);
282
- }}
283
- disabled={!STUDIO_INSPECTOR_PANELS_ENABLED}
284
- className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors ${
285
- inspectorButtonActive
286
- ? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
287
- : STUDIO_INSPECTOR_PANELS_ENABLED
288
- ? "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
289
- : "cursor-not-allowed border-transparent text-neutral-700"
290
- }`}
291
- title={
292
- STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
293
- }
294
- aria-label={
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
- <svg
299
- width="12"
300
- height="12"
301
- viewBox="0 0 24 24"
302
- fill="none"
303
- stroke="currentColor"
304
- strokeWidth="2"
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
- <circle cx="12" cy="12" r="10" />
307
- <polygon points="10 8 16 12 10 16" fill="currentColor" stroke="none" />
308
- </svg>
309
- Inspector
310
- </button>
311
- <button
312
- type="button"
313
- onClick={() => {
314
- setRightPanelTab("renders");
315
- setRightCollapsed(false);
316
- onExport?.();
317
- }}
318
- className="h-7 flex items-center gap-1.5 px-3 rounded-md text-[11px] font-semibold bg-studio-accent text-[#09090B] hover:brightness-110 transition-colors"
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
- Export
321
- </button>
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={handleImportFiles}
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
- className="group w-2 flex-shrink-0 cursor-col-resize flex items-center justify-center"
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
- appToast: ReturnType<typeof useToast>["appToast"];
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
- appToast,
42
+ toasts,
41
43
  dismissToast,
42
44
  }: StudioOverlaysProps) {
43
45
  return (
44
46
  <>
45
47
  {lintModal !== null && (
46
- <LintModal findings={lintModal} projectId={projectId} onClose={closeLintModal} />
48
+ <LintModal
49
+ findings={lintModal}
50
+ projectId={projectId}
51
+ projectDir={projectDir}
52
+ onClose={closeLintModal}
53
+ />
47
54
  )}
48
- {consoleErrors !== null && consoleErrors.length > 0 && (
49
- <LintModal findings={consoleErrors} projectId={projectId} onClose={clearConsoleErrors} />
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
- {appToast && (
66
- <StudioToast message={appToast.message} tone={appToast.tone} onDismiss={dismissToast} />
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
  );