@hyperframes/studio 0.8.51 → 0.8.53-desktop.0

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 (160) hide show
  1. package/dist/assets/{hyperframes-player-Cn_PpHqP.js → hyperframes-player-PTEnAHhg.js} +1 -1
  2. package/dist/assets/{index-CPS3Cvj8.js → index--myXp8ui.js} +1 -1
  3. package/dist/assets/{index-Clrb11lj.js → index-BjslOS9g.js} +1 -1
  4. package/dist/assets/index-BnXZuIJD.css +1 -0
  5. package/dist/assets/index-CNyviK_l.js +390 -0
  6. package/dist/index.css +29 -0
  7. package/dist/index.css.map +1 -0
  8. package/dist/index.d.ts +808 -12
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +8109 -7583
  11. package/dist/index.js.map +1 -1
  12. package/package.json +9 -7
  13. package/src/App.tsx +45 -50
  14. package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
  15. package/src/captions/components/CaptionOverlay.tsx +8 -4
  16. package/src/components/EditorShell.selectionSync.test.tsx +9 -4
  17. package/src/components/EditorShell.tsx +56 -56
  18. package/src/components/RightPanelTabs.test.tsx +151 -0
  19. package/src/components/RightPanelTabs.tsx +59 -0
  20. package/src/components/StudioHeader.dom.test.tsx +181 -0
  21. package/src/components/StudioHeader.test.ts +0 -12
  22. package/src/components/StudioHeader.tsx +117 -160
  23. package/src/components/StudioLeftPanels.tsx +202 -0
  24. package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
  25. package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
  26. package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
  27. package/src/components/TimelineHistoryButtons.tsx +54 -0
  28. package/src/components/TimelineToolbar.test.tsx +8 -0
  29. package/src/components/TimelineToolbar.tsx +3 -7
  30. package/src/components/dock/Dock.test.tsx +100 -18
  31. package/src/components/dock/Dock.tsx +32 -5
  32. package/src/components/dock/dock.css +5 -0
  33. package/src/components/dock/dockAccessibility.test.ts +166 -0
  34. package/src/components/dock/dockAccessibility.ts +98 -0
  35. package/src/components/dock/dockLayout.test.ts +134 -0
  36. package/src/components/dock/dockLayout.ts +53 -6
  37. package/src/components/dock/dockLayoutStore.test.ts +28 -0
  38. package/src/components/dock/dockLayoutStore.ts +21 -4
  39. package/src/components/dock/dockTestHarness.ts +11 -0
  40. package/src/components/editor/LayersPanel.test.ts +97 -1
  41. package/src/components/editor/LayersPanel.tsx +4 -2
  42. package/src/components/editor/PreviewGuides.tsx +1 -1
  43. package/src/components/editor/SnapToolbar.test.tsx +0 -3
  44. package/src/components/editor/TopologyLens.test.tsx +28 -0
  45. package/src/components/editor/TopologyLens.tsx +3 -1
  46. package/src/components/editor/useColorGradingController.test.ts +38 -0
  47. package/src/components/editor/useColorGradingController.ts +9 -1
  48. package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
  49. package/src/components/editor/useColorGradingPreviews.ts +11 -1
  50. package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
  51. package/src/components/editor/usePreviewCompositionRect.ts +21 -0
  52. package/src/components/nle/NLEContext.tsx +0 -43
  53. package/src/components/nle/NLEPreview.tsx +1 -1
  54. package/src/components/nle/PreviewPane.tsx +1 -1
  55. package/src/components/nle/TimelinePane.tsx +53 -67
  56. package/src/components/panels/SlideshowPanel.tsx +1 -1
  57. package/src/components/panels/VariablesPanel.tsx +1 -1
  58. package/src/components/pointerTargetSize.test.tsx +8 -0
  59. package/src/components/renders/RenderQueue.tsx +1 -1
  60. package/src/components/renders/RenderQueuePanel.tsx +1 -1
  61. package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
  62. package/src/components/sidebar/CompositionsPanel.tsx +15 -0
  63. package/src/components/sidebar/{LeftSidebar.lintBadge.test.tsx → SidebarLintButton.test.tsx} +6 -11
  64. package/src/components/sidebar/SidebarLintButton.tsx +50 -0
  65. package/src/components/timelineToolbarStyles.ts +5 -0
  66. package/src/components/ui/Button.test.tsx +222 -30
  67. package/src/components/ui/Button.tsx +88 -125
  68. package/src/components/ui/IconButton.tsx +38 -0
  69. package/src/components/ui/Input.tsx +115 -0
  70. package/src/components/ui/Menu.test.tsx +374 -0
  71. package/src/components/ui/Menu.tsx +213 -0
  72. package/src/components/ui/NumberField.tsx +122 -0
  73. package/src/components/ui/Popover.tsx +74 -0
  74. package/src/components/ui/Select.tsx +100 -0
  75. package/src/components/ui/Slider.tsx +153 -0
  76. package/src/components/ui/Tabs.test.tsx +98 -0
  77. package/src/components/ui/Tabs.tsx +71 -0
  78. package/src/components/ui/Toggle.tsx +63 -0
  79. package/src/components/ui/Tooltip.test.tsx +110 -0
  80. package/src/components/ui/Tooltip.tsx +38 -112
  81. package/src/components/ui/cn.test.ts +30 -0
  82. package/src/components/ui/cn.ts +25 -0
  83. package/src/components/ui/index.ts +29 -2
  84. package/src/components/ui/mountHost.testHelpers.ts +24 -0
  85. package/src/components/ui/valueControls.test.tsx +524 -0
  86. package/src/contexts/PanelLayoutContext.tsx +3 -57
  87. package/src/hooks/appHotkeysDispatch.ts +3 -4
  88. package/src/hooks/timelineEditPermission.ts +31 -0
  89. package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
  90. package/src/hooks/useAppHotkeys.test.ts +0 -1
  91. package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
  92. package/src/hooks/useAppHotkeys.ts +20 -83
  93. package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
  94. package/src/hooks/useCaptionDetection.ts +3 -1
  95. package/src/hooks/useDomEditPreviewSync.ts +4 -6
  96. package/src/hooks/useDomEditSession.ts +0 -7
  97. package/src/hooks/useDomEditWiring.ts +4 -9
  98. package/src/hooks/useEditHistoryActions.test.tsx +90 -0
  99. package/src/hooks/useEditHistoryActions.ts +106 -0
  100. package/src/hooks/useFileManager.ts +21 -123
  101. package/src/hooks/usePanelLayout.test.ts +108 -296
  102. package/src/hooks/usePanelLayout.ts +50 -255
  103. package/src/hooks/usePersistentEditHistory.ts +1 -1
  104. package/src/hooks/useProjectFileWriter.ts +150 -0
  105. package/src/hooks/useRightPanelIntents.test.ts +181 -0
  106. package/src/hooks/useRightPanelIntents.ts +63 -0
  107. package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
  108. package/src/hooks/useServerConnection.ts +43 -10
  109. package/src/hooks/useSlideshowTabState.test.ts +9 -31
  110. package/src/hooks/useSlideshowTabState.ts +6 -19
  111. package/src/hooks/useStudioContextValue.test.ts +6 -18
  112. package/src/hooks/useStudioContextValue.ts +5 -26
  113. package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
  114. package/src/hooks/useTimelineEditing.test.tsx +322 -3
  115. package/src/hooks/useTimelineEditing.ts +103 -16
  116. package/src/hooks/useTimelineEditingTypes.ts +3 -0
  117. package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
  118. package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
  119. package/src/hostMount.test.tsx +222 -0
  120. package/src/index.ts +87 -0
  121. package/src/player/components/AutomationSelectionMenu.tsx +1 -1
  122. package/src/player/components/Timeline.tsx +1 -1
  123. package/src/player/components/TimelineCanvas.tsx +4 -4
  124. package/src/player/components/TimelineClip.tsx +0 -1
  125. package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
  126. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  127. package/src/player/components/timelineTheme.test.ts +4 -4
  128. package/src/player/components/timelineTheme.ts +30 -30
  129. package/src/player/lib/playbackShortcuts.ts +2 -0
  130. package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
  131. package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
  132. package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
  133. package/src/player/store/previewIframeStore.test.tsx +29 -0
  134. package/src/player/store/previewIframeStore.ts +25 -0
  135. package/src/renderQueueExports.test.tsx +58 -0
  136. package/src/styles/classCandidates.test.ts +105 -0
  137. package/src/styles/classCandidates.ts +281 -0
  138. package/src/styles/hex-baseline.json +84 -0
  139. package/src/styles/hexRatchet.test.ts +201 -0
  140. package/src/styles/styleSources.test.ts +20 -0
  141. package/src/styles/styleSources.ts +47 -0
  142. package/src/styles/theme.css +57 -0
  143. package/src/styles/theme.test.ts +2 -15
  144. package/src/styles/tokenGate.test.ts +109 -0
  145. package/src/utils/sourcePatcher.ts +1 -1
  146. package/src/utils/studioHelpers.test.ts +18 -0
  147. package/src/utils/studioHelpers.ts +11 -8
  148. package/src/utils/studioUiPreferences.test.ts +10 -16
  149. package/src/utils/studioUiPreferences.ts +0 -16
  150. package/src/utils/typingTarget.ts +4 -0
  151. package/dist/assets/index-C05SNrvY.js +0 -360
  152. package/dist/assets/index-DUR9qzQM.css +0 -1
  153. package/src/components/PanelTabButton.tsx +0 -31
  154. package/src/components/StudioLeftSidebar.tsx +0 -207
  155. package/src/components/nle/TimelineResizeDivider.tsx +0 -101
  156. package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
  157. package/src/components/sidebar/LeftSidebar.tsx +0 -348
  158. package/src/hooks/useInspectorSplitResize.ts +0 -51
  159. package/src/utils/fitPanels.test.ts +0 -164
  160. package/src/utils/fitPanels.ts +0 -151
@@ -1,7 +1,6 @@
1
1
  import { useCallback } from "react";
2
- import type { StudioRightPanelProps } from "./StudioRightPanel.types";
2
+ import type { StudioRightPanelsProps } from "./StudioRightPanels.types";
3
3
 
4
- export type { StudioRightPanelProps };
5
4
  import { PropertyPanel } from "./editor/PropertyPanel";
6
5
  import { LayersPanel } from "./editor/LayersPanel";
7
6
  import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
@@ -9,14 +8,18 @@ import { BlockParamsPanel } from "./editor/BlockParamsPanel";
9
8
  import { RenderQueuePanel } from "./renders/RenderQueuePanel";
10
9
  import { SlideshowPanel } from "./panels/SlideshowPanel";
11
10
  import { VariablesPanel } from "./panels/VariablesPanel";
12
- import { PanelTabButton } from "./PanelTabButton";
11
+ import { Dock } from "./dock/Dock";
12
+ import { useDockLayoutStore } from "./dock/dockLayoutStore";
13
13
  import type { RenderJob } from "./renders/useRenderQueue";
14
- import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
15
14
  import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
16
15
  import { useSlideshowTabState } from "../hooks/useSlideshowTabState";
16
+ import {
17
+ useBlockParamsDismissal,
18
+ useCaptionDesignFocus,
19
+ useSlideshowDockPanel,
20
+ } from "../hooks/useRightPanelIntents";
17
21
  import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
18
22
  import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
19
- import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
20
23
  import { useFileManagerContext } from "../contexts/FileManagerContext";
21
24
  import { useDomEditContext } from "../contexts/DomEditContext";
22
25
  import { usePlayerStore } from "../player";
@@ -27,14 +30,13 @@ import {
27
30
  } from "./studioColorGradingScope";
28
31
  import { timelineKeysForSelections } from "../utils/studioHelpers";
29
32
  import { canHideSelections } from "../utils/timelineInspector";
30
- import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
31
33
  import { useRemoveBackground } from "../hooks/useRemoveBackground";
32
34
 
33
35
  // fallow-ignore-next-line complexity
34
- export function StudioRightPanel({
35
- designPanelActive,
36
+ export function StudioRightPanels({
36
37
  activeBlockParams,
37
38
  onCloseBlockParams,
39
+ onDismissBlockParams,
38
40
  recordingState,
39
41
  recordingDuration,
40
42
  onToggleRecording,
@@ -46,20 +48,7 @@ export function StudioRightPanel({
46
48
  onToggleElementHidden,
47
49
  onAutoGroupCarveSources,
48
50
  onAddMediaOverlay,
49
- }: StudioRightPanelProps) {
50
- const {
51
- rightWidth,
52
- adjustPanelWidth,
53
- rightPanelTab,
54
- setRightPanelTab,
55
- rightInspectorPanes,
56
- toggleRightInspectorPane,
57
- setExclusiveRightInspectorPane,
58
- handlePanelResizeStart,
59
- handlePanelResizeMove,
60
- handlePanelResizeEnd,
61
- } = usePanelLayoutContext();
62
-
51
+ }: StudioRightPanelsProps) {
63
52
  const {
64
53
  previewIframeRef,
65
54
  projectId,
@@ -154,41 +143,20 @@ export function StudioRightPanel({
154
143
  coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
155
144
  });
156
145
 
157
- const {
158
- layersPanePercent,
159
- splitContainerRef,
160
- handleInspectorSplitResizeStart,
161
- handleInspectorSplitResizeMove,
162
- handleInspectorSplitResizeEnd,
163
- } = useInspectorSplitResize();
164
-
165
146
  const renderJobs = renderQueue.jobs as RenderJob[];
166
- const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
167
-
147
+ const slideshowVisible = useDockLayoutStore((state) => state.visiblePanels.has("slideshow"));
168
148
  const { isSlideshowComposition, slideshowScenes } = useSlideshowTabState({
169
149
  editingFileContent: editingFile?.content,
170
150
  previewIframeRef,
171
151
  refreshKey,
172
- rightPanelTab,
173
- setRightPanelTab,
152
+ slideshowVisible,
174
153
  });
175
- const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
176
- const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers;
177
-
178
- const handleInspectorPaneButtonClick = (pane: "design" | "layers") => {
179
- if (!inspectorTabActive) {
180
- setRightPanelTab(pane);
181
- return;
182
- }
183
- // Flat inspector: Layers always renders full-height by itself (see the
184
- // render branch below), so the two panes are mutually exclusive here —
185
- // otherwise both tabs could show "active" while only one actually shows.
186
- if (STUDIO_FLAT_INSPECTOR_ENABLED) {
187
- setExclusiveRightInspectorPane(pane);
188
- return;
189
- }
190
- toggleRightInspectorPane(pane);
191
- };
154
+ useSlideshowDockPanel(isSlideshowComposition);
155
+ useBlockParamsDismissal({
156
+ hasBlockParams: activeBlockParams != null,
157
+ onDismiss: onDismissBlockParams,
158
+ });
159
+ useCaptionDesignFocus(captionEditMode);
192
160
 
193
161
  const handleApplyColorGradingScope = useCallback(
194
162
  async (scope: ColorGradingScope, value: string | null) =>
@@ -337,153 +305,48 @@ export function StudioRightPanel({
337
305
  </DesignPanelPromoteProvider>
338
306
  );
339
307
 
340
- const renderQueuePanel = <RenderQueuePanel />;
308
+ let designBody = propertyPanel;
309
+ if (captionEditMode) {
310
+ designBody = <CaptionPropertyPanel iframeRef={previewIframeRef} />;
311
+ } else if (activeBlockParams) {
312
+ designBody = (
313
+ <BlockParamsPanel
314
+ blockName={activeBlockParams.blockName}
315
+ blockTitle={activeBlockParams.blockTitle}
316
+ params={activeBlockParams.params}
317
+ compositionPath={activeBlockParams.compositionPath}
318
+ onClose={onCloseBlockParams ?? (() => {})}
319
+ />
320
+ );
321
+ }
341
322
 
342
323
  return (
343
324
  <>
344
- {/* Vertical resize divider: 3px visible seam, 13px hit zone via the inner div. */}
345
- <div
346
- role="separator"
347
- aria-label="Resize inspector panel"
348
- aria-orientation="vertical"
349
- tabIndex={0}
350
- className="group relative w-[3px] shrink-0 cursor-col-resize outline-hidden focus-visible:bg-studio-accent/20"
351
- style={{ touchAction: "none" }}
352
- onPointerDown={(e) => handlePanelResizeStart("right", e)}
353
- onPointerMove={handlePanelResizeMove}
354
- onPointerUp={handlePanelResizeEnd}
355
- onPointerCancel={handlePanelResizeEnd}
356
- onKeyDown={(e) => {
357
- if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
358
- e.preventDefault();
359
- // Panel is right-anchored: ArrowLeft grows it, ArrowRight shrinks it.
360
- const delta = e.key === "ArrowLeft" ? 16 : -16;
361
- adjustPanelWidth("right", delta);
362
- }}
363
- >
364
- {/* Asymmetric hit zone: 8px into the preview's p-2 gutter (the only dead
365
- space), the 3px seam, 2px into the card. Stops short of the 24px WCAG
366
- 2.5.8 target because the next pixel each way is live. */}
367
- <div className="absolute inset-y-0 left-[-8px] w-[13px]" />
368
- {/* Visible hairline */}
369
- <div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
370
- </div>
371
- <div
372
- className="flex min-w-0 shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-800 bg-neutral-950"
373
- style={{ width: rightWidth }}
325
+ <Dock.Panel id="design">{designBody}</Dock.Panel>
326
+ <Dock.Panel id="layers">
327
+ <LayersPanel />
328
+ </Dock.Panel>
329
+ <Dock.Panel
330
+ id="renders"
331
+ title={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : undefined}
374
332
  >
375
- {captionEditMode ? (
376
- <CaptionPropertyPanel iframeRef={previewIframeRef} />
377
- ) : (
378
- <>
379
- <div className="flex min-w-0 items-center gap-1 overflow-hidden border-b border-neutral-800 px-3 py-2">
380
- <PanelTabButton
381
- label="Design"
382
- tooltip="Element styles and properties"
383
- active={designPaneOpen}
384
- onClick={() => handleInspectorPaneButtonClick("design")}
385
- />
386
- <PanelTabButton
387
- label="Layers"
388
- tooltip="Composition layer stack"
389
- active={layersPaneOpen}
390
- onClick={() => handleInspectorPaneButtonClick("layers")}
391
- />
392
- <PanelTabButton
393
- label={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
394
- tooltip="Render queue and exports"
395
- active={rightPanelTab === "renders"}
396
- onClick={() => setRightPanelTab("renders")}
397
- />
398
- {isSlideshowComposition && (
399
- <PanelTabButton
400
- label="Slideshow"
401
- tooltip="Slideshow branching editor"
402
- active={rightPanelTab === "slideshow"}
403
- onClick={() => setRightPanelTab("slideshow")}
404
- />
405
- )}
406
- <PanelTabButton
407
- label="Variables"
408
- tooltip="Template variables — declare, preview with values"
409
- active={rightPanelTab === "variables"}
410
- onClick={() => setRightPanelTab("variables")}
411
- />
412
- </div>
413
- <div className="min-h-0 min-w-0 flex-1 overflow-hidden">
414
- {rightPanelTab === "block-params" && activeBlockParams ? (
415
- <BlockParamsPanel
416
- blockName={activeBlockParams.blockName}
417
- blockTitle={activeBlockParams.blockTitle}
418
- params={activeBlockParams.params}
419
- compositionPath={activeBlockParams.compositionPath}
420
- onClose={onCloseBlockParams ?? (() => {})}
421
- />
422
- ) : rightPanelTab === "slideshow" && isSlideshowComposition ? (
423
- <SlideshowPanel
424
- scenes={slideshowScenes}
425
- onPersist={onPersistSlideshow}
426
- onPersistNotes={onPersistSlideshowNotes}
427
- />
428
- ) : rightPanelTab === "variables" ? (
429
- <VariablesPanel
430
- sdkSession={sdkSession}
431
- publishSdkSession={publishSdkSession}
432
- reloadPreview={reloadPreview}
433
- recordEdit={recordEdit}
434
- />
435
- ) : layersPaneOpen && designPaneOpen && !STUDIO_FLAT_INSPECTOR_ENABLED ? (
436
- <div ref={splitContainerRef} className="flex h-full min-h-0 min-w-0 flex-col">
437
- <div
438
- className="min-h-[120px] overflow-hidden"
439
- style={{ flexBasis: `${layersPanePercent}%`, flexShrink: 0 }}
440
- >
441
- <LayersPanel />
442
- </div>
443
- <div
444
- role="separator"
445
- aria-label="Resize Layers and Design panes"
446
- aria-orientation="horizontal"
447
- className="group flex h-2 shrink-0 cursor-row-resize items-center justify-center border-y border-neutral-800 bg-neutral-900"
448
- style={{ touchAction: "none" }}
449
- onPointerDown={handleInspectorSplitResizeStart}
450
- onPointerMove={handleInspectorSplitResizeMove}
451
- onPointerUp={handleInspectorSplitResizeEnd}
452
- onPointerCancel={handleInspectorSplitResizeEnd}
453
- >
454
- <div className="h-px w-10 rounded-full bg-white/12 transition-colors group-hover:bg-white/24 group-active:bg-studio-accent/70" />
455
- </div>
456
- <div className="min-h-0 flex-1 overflow-hidden">{propertyPanel}</div>
457
- </div>
458
- ) : layersPaneOpen ? (
459
- <LayersPanel />
460
- ) : designPaneOpen ? (
461
- propertyPanel
462
- ) : inspectorTabActive ? (
463
- // Inspector tab selected but no pane can render (panes toggled
464
- // off, or inspector inactive during playback/recording): show an
465
- // explanation instead of silently rendering the render queue
466
- // under a highlighted inspector tab.
467
- <div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
468
- <p className="text-xs text-neutral-500">
469
- Inspector is unavailable right now — select the Design or Layers pane above, or
470
- pause playback/recording to inspect elements.
471
- </p>
472
- <button
473
- type="button"
474
- onClick={() => setRightPanelTab("renders")}
475
- className="h-7 rounded-md border border-neutral-800 px-3 text-[11px] font-medium text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200 active:scale-[0.98]"
476
- >
477
- Show Renders
478
- </button>
479
- </div>
480
- ) : (
481
- renderQueuePanel
482
- )}
483
- </div>
484
- </>
485
- )}
486
- </div>
333
+ <RenderQueuePanel />
334
+ </Dock.Panel>
335
+ <Dock.Panel id="variables">
336
+ <VariablesPanel
337
+ sdkSession={sdkSession}
338
+ publishSdkSession={publishSdkSession}
339
+ reloadPreview={reloadPreview}
340
+ recordEdit={recordEdit}
341
+ />
342
+ </Dock.Panel>
343
+ <Dock.Panel id="slideshow">
344
+ <SlideshowPanel
345
+ scenes={slideshowScenes}
346
+ onPersist={onPersistSlideshow}
347
+ onPersistNotes={onPersistSlideshowNotes}
348
+ />
349
+ </Dock.Panel>
487
350
  </>
488
351
  );
489
352
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Props for StudioRightPanel.
2
+ * Props for StudioRightPanels.
3
3
  *
4
4
  * Kept beside the component rather than inside it: the panel is at the file-size
5
5
  * cap, and this block is the part that changes least, so moving it keeps the
@@ -14,8 +14,7 @@ import type { UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
14
14
  import type { AddMediaOverlayHandler } from "./editor/propertyPanelTypes";
15
15
  import type { ToggleHiddenHandler } from "../utils/studioHelpers";
16
16
 
17
- export interface StudioRightPanelProps extends StudioEditPersistenceProps {
18
- designPanelActive: boolean;
17
+ export interface StudioRightPanelsProps extends StudioEditPersistenceProps {
19
18
  activeBlockParams?: {
20
19
  blockName: string;
21
20
  blockTitle: string;
@@ -23,6 +22,8 @@ export interface StudioRightPanelProps extends StudioEditPersistenceProps {
23
22
  compositionPath: string;
24
23
  } | null;
25
24
  onCloseBlockParams?: () => void;
25
+ /** Drops the block-params view without touching the dock (selection or leaving Design). */
26
+ onDismissBlockParams: () => void;
26
27
  recordingState?: "idle" | "recording" | "preview";
27
28
  recordingDuration?: number;
28
29
  onToggleRecording?: () => void;
@@ -0,0 +1,117 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
5
+ import { cleanupMounted, mountHost } from "./ui/mountHost.testHelpers";
6
+ import { isTypingTarget } from "../utils/typingTarget";
7
+ import { shouldIgnorePlaybackShortcutTarget } from "../player/lib/playbackShortcuts";
8
+
9
+ const editHistory = {
10
+ canUndo: false,
11
+ canRedo: false,
12
+ undoLabel: undefined as string | undefined,
13
+ redoLabel: undefined as string | undefined,
14
+ };
15
+ const handleUndo = vi.fn();
16
+ const handleRedo = vi.fn();
17
+ const trackStudioEvent = vi.fn();
18
+
19
+ vi.mock("../contexts/StudioContext", () => ({
20
+ useStudioShellContext: () => ({ editHistory, handleUndo, handleRedo }),
21
+ }));
22
+ vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent }));
23
+
24
+ const { TimelineHistoryButtons } = await import("./TimelineHistoryButtons");
25
+ const { TimelineToolbar } = await import("./TimelineToolbar");
26
+
27
+ beforeEach(() => {
28
+ Object.assign(editHistory, {
29
+ canUndo: false,
30
+ canRedo: false,
31
+ undoLabel: undefined,
32
+ redoLabel: undefined,
33
+ });
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ afterEach(cleanupMounted);
38
+
39
+ const mount = mountHost;
40
+
41
+ function button(host: HTMLElement, label: string): HTMLButtonElement {
42
+ const el = host.querySelector<HTMLButtonElement>(`button[aria-label="${label}"]`);
43
+ if (!el) throw new Error(`not rendered: ${label}`);
44
+ return el;
45
+ }
46
+
47
+ function click(el: HTMLElement): void {
48
+ act(() => {
49
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
50
+ });
51
+ }
52
+
53
+ it("calls handleUndo once and tracks the event when Undo is enabled", () => {
54
+ editHistory.canUndo = true;
55
+ const host = mount(<TimelineHistoryButtons />);
56
+
57
+ click(button(host, "Undo"));
58
+
59
+ expect(handleUndo).toHaveBeenCalledTimes(1);
60
+ expect(handleRedo).not.toHaveBeenCalled();
61
+ expect(trackStudioEvent).toHaveBeenCalledWith("toolbar_action", { action: "undo" });
62
+ });
63
+
64
+ it("calls handleRedo once and tracks the event when Redo is enabled", () => {
65
+ editHistory.canRedo = true;
66
+ const host = mount(<TimelineHistoryButtons />);
67
+
68
+ click(button(host, "Redo"));
69
+
70
+ expect(handleRedo).toHaveBeenCalledTimes(1);
71
+ expect(trackStudioEvent).toHaveBeenCalledWith("toolbar_action", { action: "redo" });
72
+ });
73
+
74
+ it("disables both buttons and ignores clicks on an empty history", () => {
75
+ const host = mount(<TimelineHistoryButtons />);
76
+
77
+ for (const label of ["Undo", "Redo"]) {
78
+ const el = button(host, label);
79
+ expect(el.disabled).toBe(true);
80
+ expect(el.className).toContain("cursor-not-allowed");
81
+ click(el);
82
+ }
83
+ expect(handleUndo).not.toHaveBeenCalled();
84
+ expect(handleRedo).not.toHaveBeenCalled();
85
+ expect(trackStudioEvent).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it("leaves enabled buttons without the disabled attribute", () => {
89
+ editHistory.canUndo = true;
90
+ const el = button(mount(<TimelineHistoryButtons />), "Undo");
91
+
92
+ expect(el.disabled).toBe(false);
93
+ expect(el.className).not.toContain("cursor-not-allowed");
94
+ });
95
+
96
+ it("orders the toolbar Undo, Redo, Select, Razor", () => {
97
+ const host = mount(<TimelineToolbar />);
98
+ const labels = Array.from(host.querySelectorAll("button"))
99
+ .map((b) => b.getAttribute("aria-label"))
100
+ .filter((l) => l !== null)
101
+ .slice(0, 4);
102
+
103
+ expect(labels).toEqual(["Undo", "Redo", "Selection tool", "Razor tool"]);
104
+ });
105
+
106
+ it("classifies Undo and Redo for the hotkey filters at their new location (KTD13)", () => {
107
+ // The header's own version of this check dropped Undo/Redo when they moved
108
+ // here; a <button> is never a typing target and is always claimed by the
109
+ // playback filter, same as every other toolbar tool.
110
+ const host = mount(<TimelineHistoryButtons />);
111
+
112
+ for (const label of ["Undo", "Redo"]) {
113
+ const el = button(host, label);
114
+ expect(isTypingTarget(el), label).toBe(false);
115
+ expect(shouldIgnorePlaybackShortcutTarget(el), label).toBe(true);
116
+ }
117
+ });
@@ -0,0 +1,54 @@
1
+ import { useStudioShellContext } from "../contexts/StudioContext";
2
+ import { RotateCcw, RotateCw } from "../icons/SystemIcons";
3
+ import { trackStudioEvent } from "../utils/studioTelemetry";
4
+ import { historyTooltipLabel } from "../utils/studioHelpers";
5
+ import { flatDisabled, flatIdle } from "./timelineToolbarStyles";
6
+ import { Tooltip } from "./ui";
7
+
8
+ interface HistoryButtonProps {
9
+ action: "undo" | "redo";
10
+ enabled: boolean;
11
+ label: string | undefined;
12
+ onClick: () => Promise<void> | void;
13
+ }
14
+
15
+ function HistoryButton({ action, enabled, label, onClick }: HistoryButtonProps) {
16
+ const Icon = action === "undo" ? RotateCcw : RotateCw;
17
+ return (
18
+ <Tooltip label={historyTooltipLabel(action, label)}>
19
+ <button
20
+ type="button"
21
+ aria-label={action === "undo" ? "Undo" : "Redo"}
22
+ disabled={!enabled}
23
+ className={enabled ? flatIdle : flatDisabled}
24
+ onClick={() => {
25
+ trackStudioEvent("toolbar_action", { action });
26
+ void onClick();
27
+ }}
28
+ >
29
+ <Icon size={16} />
30
+ </button>
31
+ </Tooltip>
32
+ );
33
+ }
34
+
35
+ /** Undo and Redo, on the one edit-history path the shell already owns. */
36
+ export function TimelineHistoryButtons() {
37
+ const { editHistory, handleUndo, handleRedo } = useStudioShellContext();
38
+ return (
39
+ <>
40
+ <HistoryButton
41
+ action="undo"
42
+ enabled={editHistory.canUndo}
43
+ label={editHistory.undoLabel}
44
+ onClick={handleUndo}
45
+ />
46
+ <HistoryButton
47
+ action="redo"
48
+ enabled={editHistory.canRedo}
49
+ label={editHistory.redoLabel}
50
+ onClick={handleRedo}
51
+ />
52
+ </>
53
+ );
54
+ }
@@ -8,6 +8,14 @@ import { usePlayerStore } from "../player/store/playerStore";
8
8
  import { makeSelection } from "../hooks/domSelectionTestHarness";
9
9
  import { TimelineToolbar } from "./TimelineToolbar";
10
10
 
11
+ vi.mock("../contexts/StudioContext", () => ({
12
+ useStudioShellContext: () => ({
13
+ editHistory: { canUndo: false, canRedo: false },
14
+ handleUndo: vi.fn(),
15
+ handleRedo: vi.fn(),
16
+ }),
17
+ }));
18
+
11
19
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
12
20
 
13
21
  afterEach(() => {
@@ -22,6 +22,8 @@ import {
22
22
  import { useTimelineZoom } from "../player/components/useTimelineZoom";
23
23
  import { usePlayerStore, type TimelineElement } from "../player";
24
24
  import { Tooltip } from "./ui";
25
+ import { flatActive, flatBtn, flatDisabled, flatIdle } from "./timelineToolbarStyles";
26
+ import { TimelineHistoryButtons } from "./TimelineHistoryButtons";
25
27
  import { Scissors } from "../icons/SystemIcons";
26
28
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
27
29
  import type { DomEditSelection } from "./editor/domEditingTypes";
@@ -186,19 +188,13 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
186
188
  return () => window.removeEventListener("keydown", onKeyDown);
187
189
  }, []);
188
190
 
189
- // CapCut-flat icon buttons: no per-button border/box chrome — a transparent
190
- // 28px hit area with a subtle rounded hover wash, consistent 16px glyphs.
191
- const flatBtn = "flex h-7 w-7 items-center justify-center rounded-md transition-colors";
192
- const flatIdle = `${flatBtn} text-neutral-400 hover:bg-white/6 hover:text-neutral-200 active:scale-[0.98]`;
193
- const flatActive = `${flatBtn} bg-white/8 text-neutral-100 active:scale-[0.98]`;
194
- const flatDisabled = `${flatBtn} text-neutral-700 cursor-not-allowed`;
195
-
196
191
  return (
197
192
  // The "TIMELINE" label is dropped for CapCut-like density — the pane's
198
193
  // position (tracks right below) makes it self-evident.
199
194
  <div className="border-b border-neutral-800/60">
200
195
  <div className="flex items-center justify-between px-2 py-0.5">
201
196
  <div className="flex items-center gap-0.5">
197
+ <TimelineHistoryButtons />
202
198
  <Tooltip label="Selection tool (V)">
203
199
  <button
204
200
  type="button"