@hyperframes/studio 0.8.50 → 0.8.52

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 (280) hide show
  1. package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-I5YRzB02.js} +3 -3
  2. package/dist/assets/{index-Dt3qBI-9.js → index-BTAfgesM.js} +1 -1
  3. package/dist/assets/index-CxAu_Okg.css +1 -0
  4. package/dist/assets/index-DocJlW2v.js +376 -0
  5. package/dist/assets/{index-DUAIWxkI.js → index-Du5x7KUR.js} +1 -1
  6. package/dist/index.d.ts +419 -20
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7496 -8288
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.js.map +1 -1
  11. package/package.json +13 -12
  12. package/src/App.tsx +131 -143
  13. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  14. package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
  15. package/src/captions/components/CaptionOverlay.tsx +8 -4
  16. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  17. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  18. package/src/captions/components/CaptionTimeline.tsx +1 -1
  19. package/src/captions/components/shared.tsx +2 -2
  20. package/src/components/AskAgentModal.tsx +5 -5
  21. package/src/components/EditorShell.tsx +8 -7
  22. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  23. package/src/components/LintModal.tsx +6 -16
  24. package/src/components/MediaPreview.tsx +2 -2
  25. package/src/components/RightPanelTabs.test.tsx +151 -0
  26. package/src/components/RightPanelTabs.tsx +59 -0
  27. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  28. package/src/components/StudioHeader.dom.test.tsx +181 -0
  29. package/src/components/StudioHeader.tsx +110 -210
  30. package/src/components/StudioLeftSidebar.tsx +7 -4
  31. package/src/components/StudioOverlays.tsx +1 -1
  32. package/src/components/StudioRightPanel.tsx +51 -39
  33. package/src/components/StudioToast.tsx +2 -2
  34. package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
  35. package/src/components/TimelineHistoryButtons.tsx +54 -0
  36. package/src/components/TimelineToolbar.test.tsx +8 -0
  37. package/src/components/TimelineToolbar.tsx +8 -12
  38. package/src/components/dock/Dock.test.tsx +260 -0
  39. package/src/components/dock/Dock.tsx +281 -0
  40. package/src/components/dock/dock.css +28 -0
  41. package/src/components/dock/dockAccessibility.test.ts +166 -0
  42. package/src/components/dock/dockAccessibility.ts +98 -0
  43. package/src/components/dock/dockLayout.test.ts +134 -0
  44. package/src/components/dock/dockLayout.ts +105 -0
  45. package/src/components/dock/dockLayoutSchema.ts +66 -0
  46. package/src/components/dock/dockLayoutStore.ts +91 -0
  47. package/src/components/dock/dockTestHarness.ts +11 -0
  48. package/src/components/dock/panelRegistry.ts +67 -0
  49. package/src/components/editor/AnimationCard.tsx +4 -4
  50. package/src/components/editor/AnimationCardParts.tsx +6 -6
  51. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  52. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  53. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  54. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  55. package/src/components/editor/DomEditOverlay.tsx +1 -1
  56. package/src/components/editor/EaseCurveSection.tsx +1 -1
  57. package/src/components/editor/EaseParamFields.tsx +5 -5
  58. package/src/components/editor/FileTree.tsx +4 -4
  59. package/src/components/editor/FileTreeIcons.tsx +1 -1
  60. package/src/components/editor/FileTreeNodes.tsx +13 -13
  61. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  62. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  63. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  64. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  65. package/src/components/editor/LayersPanel.test.ts +97 -1
  66. package/src/components/editor/LayersPanel.tsx +7 -5
  67. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  68. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  69. package/src/components/editor/PreviewGuides.tsx +1 -1
  70. package/src/components/editor/PromotableControl.tsx +2 -2
  71. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  72. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  73. package/src/components/editor/SnapToolbar.tsx +2 -2
  74. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  75. package/src/components/editor/TopologyLens.test.tsx +28 -0
  76. package/src/components/editor/TopologyLens.tsx +7 -5
  77. package/src/components/editor/Transform3DCube.tsx +2 -2
  78. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  79. package/src/components/editor/domEditing.test.ts +1 -1
  80. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  81. package/src/components/editor/domEditingAgentPrompt.ts +4 -3
  82. package/src/components/editor/propertyPanelColor.tsx +11 -11
  83. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  84. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  85. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  86. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  87. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  88. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  89. package/src/components/editor/propertyPanelFill.tsx +4 -4
  90. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  91. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  92. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  93. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  94. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  95. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  96. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  97. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  98. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  99. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  100. package/src/components/editor/propertyPanelFont.tsx +5 -5
  101. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  102. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  103. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  104. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  105. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  106. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  107. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  108. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  109. package/src/components/editor/propertyPanelSections.tsx +8 -8
  110. package/src/components/editor/useColorGradingController.test.ts +38 -0
  111. package/src/components/editor/useColorGradingController.ts +9 -1
  112. package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
  113. package/src/components/editor/useColorGradingPreviews.ts +11 -1
  114. package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
  115. package/src/components/editor/usePreviewCompositionRect.ts +21 -0
  116. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  117. package/src/components/feedback/feedbackTrigger.ts +0 -1
  118. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  119. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  120. package/src/components/nle/NLEPreview.tsx +3 -3
  121. package/src/components/nle/PreviewOverlays.tsx +6 -6
  122. package/src/components/nle/PreviewPane.tsx +3 -3
  123. package/src/components/nle/TimelinePane.tsx +3 -3
  124. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  125. package/src/components/panels/SlideshowPanel.tsx +2 -2
  126. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  127. package/src/components/panels/VariablesBindElement.tsx +2 -2
  128. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  129. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  130. package/src/components/panels/VariablesPanel.tsx +3 -3
  131. package/src/components/panels/VariablesValueControls.tsx +2 -2
  132. package/src/components/pointerTargetSize.test.tsx +8 -0
  133. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  134. package/src/components/renders/RenderQueue.test.tsx +7 -0
  135. package/src/components/renders/RenderQueue.tsx +7 -7
  136. package/src/components/renders/RenderQueueItem.tsx +6 -6
  137. package/src/components/sidebar/AssetCard.tsx +8 -8
  138. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  139. package/src/components/sidebar/AssetsTab.tsx +2 -2
  140. package/src/components/sidebar/AudioRow.tsx +6 -6
  141. package/src/components/sidebar/BlocksTab.tsx +6 -6
  142. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  143. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  144. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  145. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  146. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  147. package/src/components/timelineToolbarStyles.ts +5 -0
  148. package/src/components/ui/Button.test.tsx +252 -0
  149. package/src/components/ui/Button.tsx +89 -126
  150. package/src/components/ui/IconButton.tsx +38 -0
  151. package/src/components/ui/Input.tsx +115 -0
  152. package/src/components/ui/Menu.test.tsx +374 -0
  153. package/src/components/ui/Menu.tsx +213 -0
  154. package/src/components/ui/NumberField.tsx +122 -0
  155. package/src/components/ui/Popover.tsx +74 -0
  156. package/src/components/ui/SearchInput.tsx +2 -2
  157. package/src/components/ui/Select.tsx +100 -0
  158. package/src/components/ui/Slider.tsx +153 -0
  159. package/src/components/ui/Tabs.test.tsx +98 -0
  160. package/src/components/ui/Tabs.tsx +71 -0
  161. package/src/components/ui/Toggle.tsx +63 -0
  162. package/src/components/ui/Tooltip.test.tsx +110 -0
  163. package/src/components/ui/Tooltip.tsx +38 -112
  164. package/src/components/ui/cn.test.ts +30 -0
  165. package/src/components/ui/cn.ts +25 -0
  166. package/src/components/ui/index.ts +29 -2
  167. package/src/components/ui/mountHost.testHelpers.ts +24 -0
  168. package/src/components/ui/valueControls.test.tsx +524 -0
  169. package/src/hooks/useAppHotkeys.ts +21 -80
  170. package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
  171. package/src/hooks/useCaptionDetection.ts +3 -1
  172. package/src/hooks/useEditHistoryActions.test.tsx +90 -0
  173. package/src/hooks/useEditHistoryActions.ts +106 -0
  174. package/src/hooks/useLintModal.test.tsx +65 -0
  175. package/src/hooks/useLintModal.ts +3 -0
  176. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  177. package/src/hooks/useSdkSession.ts +43 -10
  178. package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
  179. package/src/hooks/useServerConnection.ts +43 -10
  180. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  181. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  182. package/src/index.ts +51 -0
  183. package/src/main.tsx +9 -3
  184. package/src/player/components/AudioWaveform.tsx +1 -1
  185. package/src/player/components/AutomationSelectionMenu.tsx +3 -3
  186. package/src/player/components/ClipContextMenu.tsx +3 -3
  187. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  188. package/src/player/components/CompositionThumbnail.tsx +3 -4
  189. package/src/player/components/EditModal.tsx +1 -1
  190. package/src/player/components/ImageThumbnail.tsx +1 -1
  191. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  192. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  193. package/src/player/components/PlayerControls.tsx +1 -1
  194. package/src/player/components/ShortcutsPanel.tsx +9 -9
  195. package/src/player/components/SpeedMenu.tsx +3 -5
  196. package/src/player/components/Timeline.tsx +2 -1
  197. package/src/player/components/TimelineCanvas.tsx +21 -14
  198. package/src/player/components/TimelineClip.tsx +0 -1
  199. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  200. package/src/player/components/TimelineEmptyState.tsx +5 -5
  201. package/src/player/components/TimelineFxButton.tsx +6 -6
  202. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  203. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  204. package/src/player/components/TimelineGroupRow.tsx +1 -1
  205. package/src/player/components/TimelineLanes.test.tsx +32 -0
  206. package/src/player/components/TimelineLanes.tsx +2 -5
  207. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  208. package/src/player/components/TimelineRuler.tsx +1 -1
  209. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  210. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  211. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  212. package/src/player/components/TimelineTypes.ts +2 -1
  213. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  214. package/src/player/components/VideoThumbnail.tsx +4 -2
  215. package/src/player/components/VolumeControl.tsx +2 -2
  216. package/src/player/components/timelineCallbacks.ts +8 -8
  217. package/src/player/components/timelineClipDragPreview.ts +51 -27
  218. package/src/player/components/timelineClipDragTypes.ts +3 -0
  219. package/src/player/components/timelineDragDrop.test.tsx +61 -1
  220. package/src/player/components/timelineDragDrop.ts +57 -15
  221. package/src/player/components/timelineLaneProps.ts +3 -0
  222. package/src/player/components/timelineSnapping.test.ts +27 -0
  223. package/src/player/components/timelineSnapping.ts +14 -1
  224. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  225. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  226. package/src/player/components/useTimelineClipDrag.ts +30 -3
  227. package/src/player/lib/describeClips.ts +3 -54
  228. package/src/player/lib/playbackShortcuts.ts +2 -0
  229. package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
  230. package/src/player/store/previewIframeStore.test.tsx +29 -0
  231. package/src/player/store/previewIframeStore.ts +25 -0
  232. package/src/renderQueueExports.test.tsx +58 -0
  233. package/src/styles/classCandidates.test.ts +105 -0
  234. package/src/styles/classCandidates.ts +281 -0
  235. package/src/styles/hex-baseline.json +84 -0
  236. package/src/styles/hexRatchet.test.ts +201 -0
  237. package/src/styles/iconTokens.ts +20 -0
  238. package/src/styles/palette-reset.css +6 -0
  239. package/src/styles/studio.css +24 -21
  240. package/src/styles/styleSources.test.ts +20 -0
  241. package/src/styles/styleSources.ts +47 -0
  242. package/src/styles/tailwind-preset.shared.js +3 -0
  243. package/src/styles/theme.css +243 -0
  244. package/src/styles/theme.test.ts +183 -0
  245. package/src/styles/tokenGate.test.ts +109 -0
  246. package/src/utils/sourcePatcher.ts +1 -1
  247. package/src/utils/studioHelpers.test.ts +18 -0
  248. package/src/utils/studioHelpers.ts +11 -1
  249. package/src/utils/studioUiPreferences.ts +7 -0
  250. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  251. package/src/utils/timelineDropTrackInsert.ts +83 -0
  252. package/src/utils/typingTarget.ts +4 -0
  253. package/dist/assets/index-n2ml4tUn.js +0 -442
  254. package/dist/assets/index-pO6p0H27.css +0 -1
  255. package/src/components/PanelTabButton.tsx +0 -31
  256. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  257. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  258. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  259. package/src/components/storyboard/FramePoster.tsx +0 -86
  260. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  261. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  262. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  263. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  264. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  265. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  266. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  267. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  268. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  269. package/src/components/storyboard/StoryboardView.tsx +0 -169
  270. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  271. package/src/components/storyboard/frameComments.test.ts +0 -95
  272. package/src/components/storyboard/frameComments.ts +0 -115
  273. package/src/components/storyboard/frameStatus.ts +0 -36
  274. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  275. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  276. package/src/components/storyboard/useFrameComments.ts +0 -98
  277. package/src/contexts/ViewModeContext.tsx +0 -142
  278. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  279. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  280. package/src/hooks/useStoryboard.ts +0 -99
@@ -93,7 +93,7 @@ export function EaseBezierField({
93
93
  }
94
94
  }}
95
95
  onBlur={(event) => commit(event.currentTarget.value)}
96
- className={`w-full rounded border bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-none ${
96
+ className={`w-full rounded border bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden ${
97
97
  error
98
98
  ? "border-red-500/70 focus:border-red-400"
99
99
  : "border-white/10 focus:border-panel-accent/50"
@@ -183,7 +183,7 @@ export function SpringBounceField({
183
183
  const bounce = parseSpringBounce(`spring(${value})`);
184
184
  if (bounce !== null) onCommit(`spring(${round2(bounce)})`);
185
185
  }}
186
- className="w-16 rounded border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-none focus:border-panel-accent/50"
186
+ className="w-16 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
187
187
  />
188
188
  </div>
189
189
  );
@@ -207,7 +207,7 @@ export function WiggleField({
207
207
  min={1}
208
208
  step={1}
209
209
  onCommit={(value) => commitWiggle(onCommit, value, config.type, amplitude)}
210
- className="w-14 rounded border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-none focus:border-panel-accent/50"
210
+ className="w-14 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
211
211
  />
212
212
  </div>
213
213
  <div className="flex items-center gap-1">
@@ -220,7 +220,7 @@ export function WiggleField({
220
220
  commitWiggle(onCommit, config.wiggles, event.currentTarget.value, amplitude);
221
221
  }
222
222
  }}
223
- className="rounded border border-white/10 bg-black/20 px-1.5 py-1 text-[10px] text-neutral-300 outline-none focus:border-panel-accent/50"
223
+ className="rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
224
224
  >
225
225
  {WIGGLE_TYPES.map((type) => (
226
226
  <option key={type} value={type}>
@@ -238,7 +238,7 @@ export function WiggleField({
238
238
  max={1}
239
239
  step={0.01}
240
240
  onCommit={(value) => commitWiggle(onCommit, config.wiggles, config.type, value)}
241
- className="w-16 rounded border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-none focus:border-panel-accent/50"
241
+ className="w-16 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
242
242
  />
243
243
  </div>
244
244
  </div>
@@ -235,14 +235,14 @@ export const FileTree = memo(function FileTree({
235
235
  <div className="flex flex-col h-full min-h-0">
236
236
  {/* FILES header with action buttons */}
237
237
  {hasFileOps && (
238
- <div className="flex items-center justify-between px-2.5 py-1.5 border-b border-neutral-800/50 flex-shrink-0">
238
+ <div className="flex items-center justify-between px-2.5 py-1.5 border-b border-neutral-800/50 shrink-0">
239
239
  <span className="text-[10px] font-semibold tracking-wider text-neutral-600 uppercase">
240
240
  Files
241
241
  </span>
242
242
  <div className="flex items-center gap-0.5">
243
243
  <button
244
244
  onClick={() => handleNewFile("")}
245
- className="p-1.5 rounded hover:bg-neutral-800 text-neutral-600 hover:text-neutral-400 active:scale-[0.97] transition-colors"
245
+ className="p-1.5 rounded-sm hover:bg-neutral-800 text-neutral-600 hover:text-neutral-400 active:scale-[0.97] transition-colors"
246
246
  title="New File"
247
247
  aria-label="New File"
248
248
  >
@@ -250,7 +250,7 @@ export const FileTree = memo(function FileTree({
250
250
  </button>
251
251
  <button
252
252
  onClick={() => handleNewFolder("")}
253
- className="p-1.5 rounded hover:bg-neutral-800 text-neutral-600 hover:text-neutral-400 active:scale-[0.97] transition-colors"
253
+ className="p-1.5 rounded-sm hover:bg-neutral-800 text-neutral-600 hover:text-neutral-400 active:scale-[0.97] transition-colors"
254
254
  title="New Folder"
255
255
  aria-label="New Folder"
256
256
  >
@@ -263,7 +263,7 @@ export const FileTree = memo(function FileTree({
263
263
  <div
264
264
  className={`flex-1 overflow-y-auto py-1 transition-colors ${
265
265
  dragOverFolder === ""
266
- ? "bg-[#3CE6AC]/5 outline outline-1 outline-[#3CE6AC]/30 -outline-offset-1"
266
+ ? "bg-[#3CE6AC]/5 outline-solid outline-1 outline-[#3CE6AC]/30 -outline-offset-1"
267
267
  : ""
268
268
  }`}
269
269
  onContextMenu={handleRootContextMenu}
@@ -23,7 +23,7 @@ const W = "duotone" as const;
23
23
 
24
24
  export function FileIcon({ path }: { path: string }) {
25
25
  const ext = path.split(".").pop()?.toLowerCase() ?? "";
26
- const c = "flex-shrink-0";
26
+ const c = "shrink-0";
27
27
  if (ext === "html") return <FileHtml size={SZ} weight={W} color="#E44D26" className={c} />;
28
28
  if (ext === "css") return <FileCss size={SZ} weight={W} color="#264DE4" className={c} />;
29
29
  if (ext === "js" || ext === "mjs" || ext === "cjs")
@@ -107,7 +107,7 @@ export function ContextMenu({
107
107
  <>
108
108
  <button
109
109
  role="menuitem"
110
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-none cursor-pointer text-left"
110
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-hidden cursor-pointer text-left"
111
111
  onClick={() => {
112
112
  onNewFile(state.targetPath);
113
113
  onClose();
@@ -118,7 +118,7 @@ export function ContextMenu({
118
118
  </button>
119
119
  <button
120
120
  role="menuitem"
121
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-none cursor-pointer text-left"
121
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-hidden cursor-pointer text-left"
122
122
  onClick={() => {
123
123
  onNewFolder(state.targetPath);
124
124
  onClose();
@@ -134,7 +134,7 @@ export function ContextMenu({
134
134
  <>
135
135
  <button
136
136
  role="menuitem"
137
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-none cursor-pointer text-left"
137
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-hidden cursor-pointer text-left"
138
138
  onClick={() => {
139
139
  onNewFile(parentPath);
140
140
  onClose();
@@ -148,7 +148,7 @@ export function ContextMenu({
148
148
  )}
149
149
  <button
150
150
  role="menuitem"
151
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-none cursor-pointer text-left"
151
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-hidden cursor-pointer text-left"
152
152
  onClick={() => {
153
153
  onRename(state.targetPath);
154
154
  onClose();
@@ -160,7 +160,7 @@ export function ContextMenu({
160
160
  {!state.targetIsFolder && (
161
161
  <button
162
162
  role="menuitem"
163
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-none cursor-pointer text-left"
163
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 active:bg-neutral-700 outline-hidden cursor-pointer text-left"
164
164
  onClick={() => {
165
165
  onDuplicate(state.targetPath);
166
166
  onClose();
@@ -173,7 +173,7 @@ export function ContextMenu({
173
173
  <div className="border-t border-neutral-700 my-1" />
174
174
  <button
175
175
  role="menuitem"
176
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-900/30 focus-visible:bg-red-900/30 active:bg-red-900/50 outline-none cursor-pointer text-left"
176
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-900/30 focus-visible:bg-red-900/30 active:bg-red-900/50 outline-hidden cursor-pointer text-left"
177
177
  onClick={() => {
178
178
  onDelete(state.targetPath);
179
179
  onClose();
@@ -263,7 +263,7 @@ export function InlineInput({
263
263
  style={{ paddingLeft: `${8 + depth * 12 + (isFolder ? 0 : 14)}px` }}
264
264
  >
265
265
  {isFolder ? (
266
- <FolderSimple size={SZ_ICON} weight="duotone" color="#6B7280" className="flex-shrink-0" />
266
+ <FolderSimple size={SZ_ICON} weight="duotone" color="#6B7280" className="shrink-0" />
267
267
  ) : (
268
268
  <FileIcon path={value} />
269
269
  )}
@@ -278,7 +278,7 @@ export function InlineInput({
278
278
  onKeyDown={handleKeyDown}
279
279
  onBlur={handleBlur}
280
280
  aria-invalid={error ? true : undefined}
281
- className={`w-full min-w-0 bg-neutral-800 text-neutral-200 text-xs px-1.5 py-0.5 rounded border outline-none ${
281
+ className={`w-full min-w-0 bg-neutral-800 text-neutral-200 text-xs px-1.5 py-0.5 rounded border outline-hidden ${
282
282
  error ? "border-red-500/70" : "border-neutral-600 focus:border-[#3CE6AC]"
283
283
  }`}
284
284
  spellCheck={false}
@@ -344,13 +344,13 @@ export function DeleteConfirm({
344
344
  <div className="flex gap-1.5">
345
345
  <button
346
346
  onClick={onCancel}
347
- className="flex-1 px-2 py-1 rounded bg-neutral-700 text-neutral-300 hover:bg-neutral-600 transition-colors"
347
+ className="flex-1 px-2 py-1 rounded-sm bg-neutral-700 text-neutral-300 hover:bg-neutral-600 transition-colors"
348
348
  >
349
349
  Cancel
350
350
  </button>
351
351
  <button
352
352
  onClick={onConfirm}
353
- className="flex-1 px-2 py-1 rounded bg-red-900/60 text-red-300 hover:bg-red-800/60 transition-colors"
353
+ className="flex-1 px-2 py-1 rounded-sm bg-red-900/60 text-red-300 hover:bg-red-800/60 transition-colors"
354
354
  >
355
355
  Delete
356
356
  </button>
@@ -419,7 +419,7 @@ export const TreeFile = memo(function TreeFile({
419
419
  <span className="truncate flex-1">{node.name}</span>
420
420
  {lintInfo && lintInfo.count > 0 && (
421
421
  <span
422
- className="flex-shrink-0 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400 text-center mr-1"
422
+ className="shrink-0 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400 text-center mr-1"
423
423
  title={lintInfo.messages.join("\n")}
424
424
  >
425
425
  {lintInfo.count}
@@ -505,11 +505,11 @@ export const TreeFolder = memo(function TreeFolder({
505
505
  }}
506
506
  onDragLeave={onDragLeave}
507
507
  className={`w-full flex items-center gap-1.5 px-2.5 py-1 min-h-7 text-left text-xs text-neutral-400 hover:bg-neutral-800/30 hover:text-neutral-300 transition-colors ${
508
- isDragOver ? "bg-[#3CE6AC]/10 outline outline-1 outline-[#3CE6AC]/40" : ""
508
+ isDragOver ? "bg-[#3CE6AC]/10 outline-solid outline-1 outline-[#3CE6AC]/40" : ""
509
509
  }`}
510
510
  style={{ paddingLeft: `${8 + depth * 12}px` }}
511
511
  >
512
- <Chevron size={10} className="flex-shrink-0 text-neutral-600" />
512
+ <Chevron size={10} className="shrink-0 text-neutral-600" />
513
513
  <span className="truncate font-medium">{node.name}</span>
514
514
  </button>
515
515
  {isOpen && (
@@ -77,7 +77,7 @@ export function InlineTextToolbar({
77
77
  data-inline-text-toolbar="true"
78
78
  role="toolbar"
79
79
  aria-label="Text formatting"
80
- className="pointer-events-auto fixed z-[200] flex items-center gap-1 rounded-lg border border-white/10 bg-[#15171c] p-1 shadow-[0_8px_24px_rgba(0,0,0,0.55)]"
80
+ className="pointer-events-auto fixed z-200 flex items-center gap-1 rounded-lg border border-white/10 bg-[#15171c] p-1 shadow-[0_8px_24px_rgba(0,0,0,0.55)]"
81
81
  style={{
82
82
  left: placement.left,
83
83
  top: placement.top,
@@ -37,7 +37,7 @@ export function InspectorHeaderActions({
37
37
  track("toggle", "Element visibility");
38
38
  void onToggleHidden(selectedElementId, !selectedElementHidden);
39
39
  }}
40
- className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
40
+ className="flex h-6 w-6 items-center justify-center rounded-sm text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
41
41
  >
42
42
  {selectedElementHidden ? (
43
43
  <EyeSlash size={13} weight="bold" aria-hidden="true" />
@@ -54,7 +54,7 @@ export function InspectorHeaderActions({
54
54
  onUngroup();
55
55
  }}
56
56
  title="Ungroup (⌘⇧G)"
57
- className="flex h-6 items-center rounded px-2 text-[11px] font-medium text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-neutral-200"
57
+ className="flex h-6 items-center rounded-sm px-2 text-[11px] font-medium text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-neutral-200"
58
58
  >
59
59
  Ungroup
60
60
  </button>
@@ -91,7 +91,7 @@ export function InspectorHeaderActions({
91
91
  track("button", "Clear selection");
92
92
  onClear();
93
93
  }}
94
- className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
94
+ className="flex h-6 w-6 items-center justify-center rounded-sm text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
95
95
  >
96
96
  <X size={13} />
97
97
  </button>
@@ -29,7 +29,7 @@ export const KeyframeDiamond = memo(function KeyframeDiamond({
29
29
  e.stopPropagation();
30
30
  onClick();
31
31
  }}
32
- className="relative flex-shrink-0 p-0.5 transition-opacity hover:opacity-100 before:absolute before:-inset-1.5 before:content-['']"
32
+ className="relative shrink-0 p-0.5 transition-opacity hover:opacity-100 before:absolute before:-inset-1.5 before:content-['']"
33
33
  style={{ color, opacity }}
34
34
  title={title}
35
35
  aria-label={title}
@@ -74,7 +74,7 @@ export function KeyframeEaseList({
74
74
  const next = e.target.value;
75
75
  if (next) onApplyAll(next);
76
76
  }}
77
- className="ml-auto cursor-pointer rounded bg-neutral-800 px-1.5 py-0.5 text-[9px] text-neutral-300 outline-none hover:bg-neutral-700 focus:ring-1 focus:ring-panel-accent/40"
77
+ className="ml-auto cursor-pointer rounded-sm bg-neutral-800 px-1.5 py-0.5 text-[9px] text-neutral-300 outline-hidden hover:bg-neutral-700 focus:ring-1 focus:ring-panel-accent/40"
78
78
  >
79
79
  <option value="" disabled>
80
80
  Set all…
@@ -1,12 +1,55 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
+ import { createElement, act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
3
5
  import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
4
6
  import { Window } from "happy-dom";
7
+ import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
5
8
  import type { DomEditLayerItem } from "./domEditingTypes";
6
- import { createRafThrottle, sortLayersByZIndex } from "./LayersPanel";
9
+ import { createRafThrottle, LayersPanel, sortLayersByZIndex } from "./LayersPanel";
7
10
  import { isLayerDraggable } from "./useLayerDrag";
8
11
  import { liveTime } from "../../player";
9
12
 
13
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
14
+
15
+ const mocks = vi.hoisted(() => ({
16
+ previewIframeRef: { current: null } as { current: unknown },
17
+ collect: vi.fn(),
18
+ setActiveGroupElement: vi.fn(),
19
+ showToast: vi.fn(),
20
+ }));
21
+
22
+ vi.mock("../../contexts/StudioContext", () => ({
23
+ useStudioShellContext: () => ({
24
+ previewIframeRef: mocks.previewIframeRef,
25
+ activeCompPath: "index.html",
26
+ showToast: mocks.showToast,
27
+ }),
28
+ useStudioPlaybackContext: () => ({
29
+ refreshKey: 0,
30
+ compositionLoading: true,
31
+ timelineElements: [],
32
+ isPlaying: false,
33
+ }),
34
+ }));
35
+ vi.mock("../../contexts/DomEditContext", () => ({
36
+ useDomEditContext: () => ({
37
+ domEditSelection: null,
38
+ activeGroupElement: null,
39
+ applyDomSelection: vi.fn(),
40
+ updateDomEditHoverSelection: vi.fn(),
41
+ handleDomZIndexReorderCommit: vi.fn(),
42
+ setActiveGroupElement: mocks.setActiveGroupElement,
43
+ }),
44
+ }));
45
+ vi.mock("../nle/useCanvasZOrderTimelineMirror", () => ({
46
+ useLayerReorderTimelineMirror: () => vi.fn(),
47
+ }));
48
+ vi.mock("./domEditing", async (importOriginal) => ({
49
+ ...(await importOriginal<typeof import("./domEditing")>()),
50
+ collectDomEditLayerItems: mocks.collect,
51
+ }));
52
+
10
53
  function makeLayer(
11
54
  overrides: Partial<DomEditLayerItem> & { zIndex?: string; locked?: boolean },
12
55
  ): DomEditLayerItem {
@@ -197,3 +240,56 @@ describe("liveTime subscribe / unsubscribe (LayersPanel scrub contract)", () =>
197
240
  unsubscribe();
198
241
  });
199
242
  });
243
+
244
+ describe("LayersPanel preview promotion", () => {
245
+ let root: Root;
246
+ let host: HTMLDivElement;
247
+
248
+ function makeIframe(compId: string): HTMLIFrameElement {
249
+ const iframe = document.createElement("iframe");
250
+ const doc = new Window().document;
251
+ doc.body.innerHTML = `<div data-composition-id="${compId}"></div>`;
252
+ Object.defineProperty(iframe, "contentDocument", { value: doc });
253
+ return iframe;
254
+ }
255
+
256
+ const collectedRootIds = () =>
257
+ mocks.collect.mock.calls.map((call) => (call[0] as HTMLElement).dataset.compositionId);
258
+
259
+ beforeEach(() => {
260
+ mocks.collect.mockReset().mockReturnValue([]);
261
+ host = document.createElement("div");
262
+ document.body.append(host);
263
+ root = createRoot(host);
264
+ });
265
+
266
+ afterEach(() => {
267
+ act(() => root.unmount());
268
+ host.remove();
269
+ usePreviewIframeStore.setState({ iframe: null });
270
+ });
271
+
272
+ it("recollects layers from the promoted document's load, not the replaced one", () => {
273
+ const a = makeIframe("a");
274
+ const b = makeIframe("b");
275
+ mocks.previewIframeRef.current = a;
276
+ usePreviewIframeStore.setState({ iframe: a });
277
+ act(() => root.render(createElement(LayersPanel)));
278
+
279
+ act(() => {
280
+ mocks.previewIframeRef.current = b;
281
+ usePreviewIframeStore.getState().setIframe(b);
282
+ });
283
+ mocks.collect.mockClear();
284
+
285
+ act(() => {
286
+ a.dispatchEvent(new Event("load"));
287
+ });
288
+ expect(mocks.collect).not.toHaveBeenCalled();
289
+
290
+ act(() => {
291
+ b.dispatchEvent(new Event("load"));
292
+ });
293
+ expect(collectedRootIds()).toEqual(["b"]);
294
+ });
295
+ });
@@ -1,3 +1,4 @@
1
+ import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
1
2
  import { memo, useState, useCallback, useEffect, useRef } from "react";
2
3
  import {
3
4
  collectDomEditLayerItems,
@@ -122,6 +123,7 @@ export const LayersPanel = memo(function LayersPanel() {
122
123
 
123
124
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
124
125
 
126
+ const livePreviewIframe = useLivePreviewIframe();
125
127
  const collectLayers = useCallback(() => {
126
128
  const iframe = previewIframeRef.current;
127
129
  if (!iframe) return;
@@ -152,7 +154,7 @@ export const LayersPanel = memo(function LayersPanel() {
152
154
 
153
155
  useEffect(() => {
154
156
  collectLayers();
155
- }, [collectLayers, refreshKey, zEditVersion]);
157
+ }, [collectLayers, refreshKey, zEditVersion, livePreviewIframe]);
156
158
 
157
159
  useEffect(() => {
158
160
  const iframe = previewIframeRef.current;
@@ -163,7 +165,7 @@ export const LayersPanel = memo(function LayersPanel() {
163
165
  };
164
166
  iframe.addEventListener("load", handleLoad);
165
167
  return () => iframe.removeEventListener("load", handleLoad);
166
- }, [previewIframeRef, collectLayers]);
168
+ }, [previewIframeRef, livePreviewIframe, collectLayers]);
167
169
 
168
170
  useEffect(() => {
169
171
  if (!compositionLoading) {
@@ -485,7 +487,7 @@ export const LayersPanel = memo(function LayersPanel() {
485
487
  onClick={(e) => toggleCollapse(layer.key, e)}
486
488
  aria-expanded={!isCollapsed}
487
489
  aria-label={isCollapsed ? "Expand children" : "Collapse children"}
488
- className="relative flex h-4 w-4 flex-shrink-0 items-center justify-center rounded text-neutral-500 hover:text-neutral-300 before:absolute before:-inset-1.5 before:content-['']"
490
+ className="relative flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-300 before:absolute before:-inset-1.5 before:content-['']"
489
491
  >
490
492
  <svg
491
493
  width="8"
@@ -498,10 +500,10 @@ export const LayersPanel = memo(function LayersPanel() {
498
500
  </svg>
499
501
  </button>
500
502
  ) : (
501
- <span className="w-4 flex-shrink-0" />
503
+ <span className="w-4 shrink-0" />
502
504
  )}
503
505
  <span
504
- className={`flex h-5 w-5 flex-shrink-0 items-center justify-center rounded text-[8px] font-bold uppercase ${
506
+ className={`flex h-5 w-5 shrink-0 items-center justify-center rounded text-[8px] font-bold uppercase ${
505
507
  selected
506
508
  ? "bg-panel-accent/18 text-panel-accent"
507
509
  : isCompHost
@@ -19,7 +19,7 @@ export function MarqueeOverlay({ candidateRects, marqueeRect }: MarqueeOverlayPr
19
19
  <div
20
20
  key={i}
21
21
  aria-hidden="true"
22
- className="pointer-events-none absolute rounded-sm border border-studio-accent bg-studio-accent/5"
22
+ className="pointer-events-none absolute rounded-xs border border-studio-accent bg-studio-accent/5"
23
23
  style={{ left: r.left, top: r.top, width: r.width, height: r.height }}
24
24
  />
25
25
  ))}
@@ -167,7 +167,7 @@ export function ChildRectOutlines({
167
167
  {rects.map((rect, index) => (
168
168
  <div
169
169
  key={index}
170
- className="pointer-events-none absolute border border-dashed border-white/20 rounded-sm"
170
+ className="pointer-events-none absolute border border-dashed border-white/20 rounded-xs"
171
171
  style={{
172
172
  left: rect.left,
173
173
  top: rect.top,
@@ -8,7 +8,7 @@ interface PreviewGuidesProps {
8
8
  }
9
9
 
10
10
  const TICKS = Array.from({ length: 11 }, (_, i) => i * 10);
11
- const INK = "rgba(255,255,255,0.7)";
11
+ const INK = "color-mix(in srgb, white 70%, transparent)";
12
12
 
13
13
  /** Ruler and safe-margin boxes drawn over the preview pane, never inside the composition. */
14
14
  export const PreviewGuides = memo(function PreviewGuides({ iframeRef }: PreviewGuidesProps) {
@@ -83,7 +83,7 @@ export function PromotableControl({
83
83
  <span
84
84
  // Sits above the row (not on top of top-0) so it clears a value that
85
85
  // renders flush to the row's right edge, e.g. flat Font/Color rows.
86
- className="pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
86
+ className="pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded-sm bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
87
87
  title={`Bound to variable "${promote.boundId}"`}
88
88
  >
89
89
  ◆ {promote.boundId}
@@ -97,7 +97,7 @@ export function PromotableControl({
97
97
  e.stopPropagation();
98
98
  promote.promote();
99
99
  }}
100
- className="absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
100
+ className="absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded-sm bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
101
101
  >
102
102
  ◇ var
103
103
  </button>
@@ -18,7 +18,7 @@ function FlatEmptyState() {
18
18
  </svg>
19
19
  </span>
20
20
  <div className="text-[13px] font-semibold text-panel-text-0">Nothing selected</div>
21
- <div className="max-w-[250px] text-[11px] leading-[1.5] text-panel-text-3">
21
+ <div className="max-w-[250px] text-[11px] leading-normal text-panel-text-3">
22
22
  Click any element on the canvas to edit it, or drag to select several.
23
23
  </div>
24
24
  <div className="mt-2 flex w-full flex-col gap-1.5">
@@ -27,7 +27,7 @@ function FlatEmptyState() {
27
27
  <span className="text-panel-danger">●</span>
28
28
  Record a gesture
29
29
  </span>
30
- <span className="rounded border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
30
+ <span className="rounded-sm border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
31
31
  R
32
32
  </span>
33
33
  </span>
@@ -36,7 +36,7 @@ function FlatEmptyState() {
36
36
  <span className="text-panel-accent">✦</span>
37
37
  Describe a change to the agent
38
38
  </span>
39
- <span className="rounded border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
39
+ <span className="rounded-sm border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
40
40
  ⌘K
41
41
  </span>
42
42
  </span>
@@ -74,7 +74,7 @@ function FlatMultiSelectState({
74
74
  return (
75
75
  <div className="flex flex-col gap-3 px-4 py-3">
76
76
  <div className="flex items-center gap-3 rounded-xl border border-panel-border bg-panel-surface p-3">
77
- <span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-panel-accent/10 text-panel-accent">
77
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-panel-accent/10 text-panel-accent">
78
78
  <Layers size={16} />
79
79
  </span>
80
80
  <div className="min-w-0 flex-1">
@@ -90,7 +90,7 @@ function FlatMultiSelectState({
90
90
  data-flat-multiselect-clear="true"
91
91
  aria-label="Clear selection"
92
92
  onClick={onClearSelection}
93
- className="flex h-[26px] w-[26px] flex-shrink-0 items-center justify-center text-panel-text-3"
93
+ className="flex h-[26px] w-[26px] shrink-0 items-center justify-center text-panel-text-3"
94
94
  >
95
95
  <svg
96
96
  width="13"
@@ -113,14 +113,14 @@ function FlatMultiSelectState({
113
113
  className="flex items-center gap-2 rounded-lg border border-panel-border bg-panel-bg px-2.5 py-[7px]"
114
114
  >
115
115
  <span
116
- className={`flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded text-[9px] font-bold ${className}`}
116
+ className={`flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-sm text-[9px] font-bold ${className}`}
117
117
  >
118
118
  {glyph}
119
119
  </span>
120
120
  <span className="min-w-0 flex-1 truncate text-[11px] text-panel-text-1">
121
121
  {element.label}
122
122
  </span>
123
- <span className="flex-shrink-0 font-mono text-[9px] text-panel-text-4">
123
+ <span className="shrink-0 font-mono text-[9px] text-panel-text-4">
124
124
  {element.id ? `#${element.id}` : element.selector}
125
125
  </span>
126
126
  </span>
@@ -41,13 +41,13 @@ export function PropertyPanelFlatHeader({
41
41
  <Icon
42
42
  size={15}
43
43
  data-flat-header-icon="true"
44
- className={`flex-shrink-0 ${ICON_COLOR_BY_KIND[elementKind]}`}
44
+ className={`shrink-0 ${ICON_COLOR_BY_KIND[elementKind]}`}
45
45
  />
46
46
  <div className="flex min-w-0 flex-1 items-baseline gap-2">
47
47
  <span className="truncate text-[13px] font-semibold text-panel-text-0">{name}</span>
48
48
  <span className="truncate font-mono text-[10px] text-panel-text-4">{meta}</span>
49
49
  </div>
50
- <div className="flex flex-shrink-0 items-center gap-2.5 text-panel-text-3">
50
+ <div className="flex shrink-0 items-center gap-2.5 text-panel-text-3">
51
51
  {showUngroup && (
52
52
  <button
53
53
  type="button"
@@ -179,7 +179,7 @@ export const SnapToolbar = memo(function SnapToolbar({ onSnapChange }: SnapToolb
179
179
  </button>
180
180
  <button
181
181
  type="button"
182
- className="absolute -right-0.5 -bottom-0.5 rounded p-0.5 text-white/50 hover:text-white/90 bg-black/50"
182
+ className="absolute -right-0.5 -bottom-0.5 rounded-sm p-0.5 text-white/50 hover:text-white/90 bg-black/50"
183
183
  onClick={() => setGridPopoverOpen((v) => !v)}
184
184
  title="Grid options"
185
185
  aria-label="Grid options"
@@ -209,7 +209,7 @@ export const SnapToolbar = memo(function SnapToolbar({ onSnapChange }: SnapToolb
209
209
  updatePrefs({ gridSpacing: val });
210
210
  }
211
211
  }}
212
- className="w-16 rounded bg-neutral-900 border border-neutral-600 px-1.5 py-0.5 text-xs text-white text-right tabular-nums outline-none focus:border-studio-accent"
212
+ className="w-16 rounded-sm bg-neutral-900 border border-neutral-600 px-1.5 py-0.5 text-xs text-white text-right tabular-nums outline-hidden focus:border-studio-accent"
213
213
  />
214
214
  </label>
215
215
  <label className="flex items-center gap-2 text-xs text-white/80 cursor-pointer">
@@ -139,7 +139,7 @@ export function TimelineFxPopover({
139
139
  ref={rootRef}
140
140
  role="dialog"
141
141
  aria-label="Effects"
142
- className="z-[200] flex flex-col overflow-hidden rounded-md border border-white/10 bg-[#1b1b1f] p-2 shadow-xl"
142
+ className="z-200 flex flex-col overflow-hidden rounded-md border border-white/10 bg-[#1b1b1f] p-2 shadow-xl"
143
143
  style={clampedStyle(anchorRect)}
144
144
  onKeyDown={onKeyDown}
145
145
  onPointerDown={(event) => event.stopPropagation()}
@@ -4,6 +4,7 @@ import React, { act } from "react";
4
4
  import { createRoot, type Root } from "react-dom/client";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { studioEditLifecycle, type StudioWriteResult } from "../../webmcp/writeCoordinator";
7
+ import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
7
8
  import { TopologyLens } from "./TopologyLens";
8
9
 
9
10
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
@@ -291,4 +292,31 @@ describe("TopologyLens", () => {
291
292
  expect(vi.getTimerCount()).toBe(0);
292
293
  expect(studioEditLifecycle.getSnapshot()).toEqual({ phase: "idle" });
293
294
  });
295
+
296
+ it("dismisses on the promoted iframe's load, not the replaced one", () => {
297
+ const a = document.createElement("iframe");
298
+ const b = document.createElement("iframe");
299
+ document.body.append(a, b);
300
+ const ref = { current: a as HTMLIFrameElement | null };
301
+ usePreviewIframeStore.setState({ iframe: a });
302
+ window.matchMedia = matchMedia(false);
303
+ host = document.createElement("div");
304
+ document.body.append(host);
305
+ root = createRoot(host);
306
+ act(() => {
307
+ root?.render(<TopologyLens iframeRef={ref} activeCompositionPath="index.html" />);
308
+ });
309
+ begin();
310
+
311
+ act(() => {
312
+ ref.current = b;
313
+ usePreviewIframeStore.getState().setIframe(b);
314
+ });
315
+ act(() => a.dispatchEvent(new Event("load")));
316
+ expect(host.querySelector('[data-topology-lens="hidden"]')).toBeNull();
317
+
318
+ act(() => b.dispatchEvent(new Event("load")));
319
+ expect(host.querySelector('[data-topology-lens="hidden"]')).not.toBeNull();
320
+ usePreviewIframeStore.setState({ iframe: null });
321
+ });
294
322
  });