@hyperframes/studio 0.8.49 → 0.8.51

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 (281) hide show
  1. package/dist/assets/{hyperframes-player-CqON2Rzo.js → hyperframes-player-Cn_PpHqP.js} +3 -3
  2. package/dist/assets/index-C05SNrvY.js +360 -0
  3. package/dist/assets/{index-mIgSsKqs.js → index-CPS3Cvj8.js} +1 -1
  4. package/dist/assets/{index-BGovDksE.js → index-Clrb11lj.js} +1 -1
  5. package/dist/assets/index-DUR9qzQM.css +1 -0
  6. package/dist/index.d.ts +29 -16
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7283 -8606
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.js.map +1 -1
  11. package/package.json +11 -12
  12. package/src/App.tsx +132 -143
  13. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  14. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  15. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  16. package/src/captions/components/CaptionTimeline.tsx +1 -1
  17. package/src/captions/components/shared.tsx +2 -2
  18. package/src/components/AskAgentModal.tsx +5 -5
  19. package/src/components/EditorShell.selectionSync.test.tsx +4 -0
  20. package/src/components/EditorShell.tsx +28 -7
  21. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  22. package/src/components/LintModal.tsx +6 -16
  23. package/src/components/MediaPreview.tsx +2 -2
  24. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  25. package/src/components/StudioHeader.tsx +2 -62
  26. package/src/components/StudioLeftSidebar.tsx +7 -4
  27. package/src/components/StudioOverlays.tsx +2 -6
  28. package/src/components/StudioRightPanel.tsx +4 -4
  29. package/src/components/StudioToast.tsx +2 -2
  30. package/src/components/TimelineToolbar.tsx +7 -7
  31. package/src/components/dock/Dock.test.tsx +192 -0
  32. package/src/components/dock/Dock.tsx +264 -0
  33. package/src/components/dock/dock.css +23 -0
  34. package/src/components/dock/dockLayout.ts +58 -0
  35. package/src/components/dock/dockLayoutSchema.ts +66 -0
  36. package/src/components/dock/dockLayoutStore.ts +91 -0
  37. package/src/components/dock/panelRegistry.ts +67 -0
  38. package/src/components/editor/AnimationCard.tsx +4 -4
  39. package/src/components/editor/AnimationCardParts.tsx +6 -6
  40. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  41. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  42. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  43. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  44. package/src/components/editor/DomEditOverlay.tsx +1 -1
  45. package/src/components/editor/EaseCurveSection.tsx +1 -1
  46. package/src/components/editor/EaseParamFields.tsx +5 -5
  47. package/src/components/editor/FileTree.tsx +4 -4
  48. package/src/components/editor/FileTreeIcons.tsx +1 -1
  49. package/src/components/editor/FileTreeNodes.tsx +13 -13
  50. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  51. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  52. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  53. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  54. package/src/components/editor/LayersPanel.tsx +3 -3
  55. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  56. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  57. package/src/components/editor/PreviewGuides.test.tsx +88 -0
  58. package/src/components/editor/PreviewGuides.tsx +125 -0
  59. package/src/components/editor/PromotableControl.tsx +2 -2
  60. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  61. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  62. package/src/components/editor/SnapToolbar.test.tsx +21 -0
  63. package/src/components/editor/SnapToolbar.tsx +27 -3
  64. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  65. package/src/components/editor/TopologyLens.tsx +4 -4
  66. package/src/components/editor/Transform3DCube.tsx +2 -2
  67. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  68. package/src/components/editor/domEditing.test.ts +1 -1
  69. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  70. package/src/components/editor/domEditingAgentPrompt.ts +9 -3
  71. package/src/components/editor/previewGuidesStore.ts +26 -0
  72. package/src/components/editor/propertyPanelColor.tsx +11 -11
  73. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  74. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  75. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  76. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  77. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  78. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  79. package/src/components/editor/propertyPanelFill.tsx +4 -4
  80. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  81. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  82. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  83. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  84. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  85. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  86. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  87. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  88. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  89. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  90. package/src/components/editor/propertyPanelFont.tsx +5 -5
  91. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  92. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  93. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  94. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  95. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  96. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  97. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  98. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  99. package/src/components/editor/propertyPanelSections.tsx +8 -8
  100. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  101. package/src/components/feedback/feedbackTrigger.ts +0 -1
  102. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  103. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  104. package/src/components/nle/NLEPreview.test.ts +13 -0
  105. package/src/components/nle/NLEPreview.tsx +12 -7
  106. package/src/components/nle/PreviewOverlays.tsx +6 -6
  107. package/src/components/nle/PreviewPane.test.tsx +52 -0
  108. package/src/components/nle/PreviewPane.tsx +4 -2
  109. package/src/components/nle/TimelinePane.tsx +17 -3
  110. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  111. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
  112. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -3
  113. package/src/components/panels/SlideshowPanel.tsx +2 -2
  114. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  115. package/src/components/panels/VariablesBindElement.tsx +2 -2
  116. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  117. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  118. package/src/components/panels/VariablesPanel.tsx +3 -3
  119. package/src/components/panels/VariablesValueControls.tsx +2 -2
  120. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  121. package/src/components/renders/RenderQueue.test.tsx +7 -0
  122. package/src/components/renders/RenderQueue.tsx +6 -6
  123. package/src/components/renders/RenderQueueItem.tsx +6 -6
  124. package/src/components/sidebar/AssetCard.tsx +8 -8
  125. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  126. package/src/components/sidebar/AssetsTab.tsx +2 -2
  127. package/src/components/sidebar/AudioRow.tsx +6 -6
  128. package/src/components/sidebar/BlocksTab.tsx +6 -6
  129. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  130. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  131. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  132. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  133. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  134. package/src/components/ui/Button.test.tsx +60 -0
  135. package/src/components/ui/Button.tsx +4 -4
  136. package/src/components/ui/SearchInput.tsx +2 -2
  137. package/src/components/ui/Tooltip.tsx +1 -1
  138. package/src/hooks/appHotkeysDispatch.ts +11 -0
  139. package/src/hooks/timelineAudioGroupCreate.ts +2 -2
  140. package/src/hooks/timelineTrackVisibility.ts +8 -11
  141. package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
  142. package/src/hooks/useAppHotkeys.ts +4 -1
  143. package/src/hooks/useAskAgentModal.ts +2 -0
  144. package/src/hooks/useClipboard.test.ts +234 -0
  145. package/src/hooks/useClipboard.ts +264 -70
  146. package/src/hooks/useGroupCommits.test.ts +0 -1
  147. package/src/hooks/useLintModal.test.tsx +65 -0
  148. package/src/hooks/useLintModal.ts +3 -0
  149. package/src/hooks/useRazorSplit.history.test.tsx +0 -1
  150. package/src/hooks/useRazorSplit.test.ts +0 -2
  151. package/src/hooks/useRazorSplit.test.tsx +0 -1
  152. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  153. package/src/hooks/useSdkSession.ts +43 -10
  154. package/src/hooks/useStudioContextValue.ts +5 -19
  155. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  156. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  157. package/src/hooks/useTimelineEditing.test.tsx +0 -1
  158. package/src/main.tsx +9 -3
  159. package/src/player/components/AudioWaveform.tsx +1 -1
  160. package/src/player/components/AutomationSelectionMenu.tsx +2 -2
  161. package/src/player/components/ClipContextMenu.tsx +90 -10
  162. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  163. package/src/player/components/CompositionThumbnail.tsx +3 -4
  164. package/src/player/components/EditModal.tsx +1 -1
  165. package/src/player/components/ImageThumbnail.tsx +1 -1
  166. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  167. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  168. package/src/player/components/PlayerControls.tsx +1 -1
  169. package/src/player/components/ShortcutsPanel.tsx +9 -9
  170. package/src/player/components/SpeedMenu.tsx +3 -5
  171. package/src/player/components/Timeline.tsx +16 -15
  172. package/src/player/components/TimelineCanvas.tsx +45 -8
  173. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  174. package/src/player/components/TimelineEmptyState.tsx +5 -5
  175. package/src/player/components/TimelineFxButton.tsx +6 -6
  176. package/src/player/components/TimelineGestureOverlay.test.tsx +35 -1
  177. package/src/player/components/TimelineGestureOverlay.tsx +1 -1
  178. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  179. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  180. package/src/player/components/TimelineGroupRow.tsx +1 -1
  181. package/src/player/components/TimelineLanes.test.tsx +54 -1
  182. package/src/player/components/TimelineLanes.tsx +2 -5
  183. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  184. package/src/player/components/TimelineOverlays.tsx +12 -0
  185. package/src/player/components/TimelineRuler.tsx +1 -1
  186. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  187. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  188. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  189. package/src/player/components/TimelineTypes.ts +6 -1
  190. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  191. package/src/player/components/VideoThumbnail.tsx +4 -2
  192. package/src/player/components/VolumeControl.tsx +2 -2
  193. package/src/player/components/timelineAuthoredMoveTarget.ts +1 -2
  194. package/src/player/components/timelineCallbacks.ts +8 -8
  195. package/src/player/components/timelineClipChildren.test.ts +2 -2
  196. package/src/player/components/timelineClipChildren.tsx +2 -1
  197. package/src/player/components/timelineClipDragCommit.ts +2 -2
  198. package/src/player/components/timelineClipDragPreview.ts +51 -27
  199. package/src/player/components/timelineClipDragTypes.ts +3 -0
  200. package/src/player/components/timelineDragDrop.test.tsx +92 -1
  201. package/src/player/components/timelineDragDrop.ts +82 -33
  202. package/src/player/components/timelineEditCapabilities.ts +1 -2
  203. package/src/player/components/timelineEditing.test.ts +0 -16
  204. package/src/player/components/timelineGroupEditing.test.ts +1 -3
  205. package/src/player/components/timelineGroupEditing.ts +0 -1
  206. package/src/player/components/timelineLaneProps.ts +3 -0
  207. package/src/player/components/timelineSnapping.test.ts +27 -0
  208. package/src/player/components/timelineSnapping.ts +14 -1
  209. package/src/player/components/timelineTrackDisplay.ts +1 -1
  210. package/src/player/components/timelineTrackPersistPipeline.test.ts +0 -53
  211. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  212. package/src/player/components/useTimelineClipContextMenu.ts +32 -0
  213. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  214. package/src/player/components/useTimelineClipDrag.ts +30 -3
  215. package/src/player/components/useTimelineTrackDerivations.ts +1 -1
  216. package/src/player/hooks/timelineDomBinding.test.ts +0 -3
  217. package/src/player/hooks/timelineSyncHydration.test.ts +18 -0
  218. package/src/player/hooks/timelineSyncHydration.ts +37 -18
  219. package/src/player/hooks/useTimelinePlayer.test.ts +3 -20
  220. package/src/player/hooks/useTimelineRowElements.test.ts +28 -0
  221. package/src/player/hooks/useTimelineRowElements.ts +17 -0
  222. package/src/player/hooks/useTimelineSyncCallbacks.emptyManifest.test.tsx +41 -0
  223. package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -10
  224. package/src/player/lib/describeClips.test.ts +95 -0
  225. package/src/player/lib/describeClips.ts +52 -0
  226. package/src/player/lib/timelineDOM.test.ts +0 -80
  227. package/src/player/lib/timelineDOM.ts +1 -61
  228. package/src/player/lib/timelineElementHelpers.ts +2 -53
  229. package/src/player/store/playerStore.ts +5 -0
  230. package/src/player/store/timelineElement.ts +1 -3
  231. package/src/shims/canvasBrowserStub.js +50 -0
  232. package/src/styles/iconTokens.ts +20 -0
  233. package/src/styles/palette-reset.css +6 -0
  234. package/src/styles/studio.css +24 -21
  235. package/src/styles/tailwind-preset.shared.js +3 -0
  236. package/src/styles/theme.css +213 -0
  237. package/src/styles/theme.test.ts +196 -0
  238. package/src/utils/clipboardPayload.test.ts +42 -2
  239. package/src/utils/clipboardPayload.ts +57 -32
  240. package/src/utils/previewSafeMargins.test.ts +34 -0
  241. package/src/utils/previewSafeMargins.ts +10 -0
  242. package/src/utils/razorSplitTransaction.test.ts +0 -1
  243. package/src/utils/sdkCanvasComposition.test.ts +77 -0
  244. package/src/utils/studioUiPreferences.test.ts +43 -0
  245. package/src/utils/studioUiPreferences.ts +34 -6
  246. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  247. package/src/utils/timelineDropTrackInsert.ts +83 -0
  248. package/src/utils/timelineElementSplit.test.ts +1 -2
  249. package/src/utils/timelineElementSplit.ts +0 -1
  250. package/src/webmcp/tools/lookTools.test.ts +40 -0
  251. package/src/webmcp/tools/lookTools.ts +32 -1
  252. package/dist/assets/index-B9LfWNyM.css +0 -1
  253. package/dist/assets/index-Bgmx2h7W.js +0 -441
  254. package/src/components/StudioGlobalDragOverlay.tsx +0 -24
  255. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  256. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  257. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  258. package/src/components/storyboard/FramePoster.tsx +0 -86
  259. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  260. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  261. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  262. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  263. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  264. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  265. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  266. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  267. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  268. package/src/components/storyboard/StoryboardView.tsx +0 -169
  269. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  270. package/src/components/storyboard/frameComments.test.ts +0 -95
  271. package/src/components/storyboard/frameComments.ts +0 -115
  272. package/src/components/storyboard/frameStatus.ts +0 -36
  273. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  274. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  275. package/src/components/storyboard/useFrameComments.ts +0 -98
  276. package/src/contexts/ViewModeContext.tsx +0 -142
  277. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  278. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  279. package/src/hooks/useStoryboard.ts +0 -99
  280. package/src/player/hooks/useExpandedTimelineElements.test.ts +0 -774
  281. package/src/player/hooks/useExpandedTimelineElements.ts +0 -460
@@ -121,7 +121,7 @@ function FlatTextFieldEditor({
121
121
  track("select", "Weight");
122
122
  onSetTextFieldStyle(field.key, "font-weight", e.target.value);
123
123
  }}
124
- className={`appearance-none bg-transparent text-right font-mono text-[11px] outline-none ${
124
+ className={`appearance-none bg-transparent text-right font-mono text-[11px] outline-hidden ${
125
125
  VALUE_TIER_VALUE_CLASS[resolveValueTier(field.inlineStyles["font-weight"], "400")]
126
126
  }`}
127
127
  >
@@ -138,7 +138,7 @@ function FlatTextFieldEditor({
138
138
  height="10"
139
139
  viewBox="0 0 10 10"
140
140
  fill="currentColor"
141
- className="flex-shrink-0 text-panel-text-5"
141
+ className="shrink-0 text-panel-text-5"
142
142
  >
143
143
  <path d="M2 3l3 4 3-4z" />
144
144
  </svg>
@@ -387,13 +387,13 @@ export function FlatTextLayerList({
387
387
  }`}
388
388
  >
389
389
  <span
390
- className="h-3 w-3 flex-shrink-0 rounded-sm"
390
+ className="h-3 w-3 shrink-0 rounded-xs"
391
391
  style={{ backgroundColor: getTextFieldColor(field, styles) }}
392
392
  />
393
393
  <span className="min-w-0 flex-1 truncate text-[11px] text-panel-text-1">
394
394
  {formatTextFieldPreview(field.value) || `Text ${index + 1}`}
395
395
  </span>
396
- <span className="flex-shrink-0 font-mono text-[9px] text-panel-text-4">
396
+ <span className="shrink-0 font-mono text-[9px] text-panel-text-4">
397
397
  {field.tagName}
398
398
  </span>
399
399
  {fields.length > 1 && (
@@ -406,7 +406,7 @@ export function FlatTextLayerList({
406
406
  track("button", "Remove text field");
407
407
  onRemove(field.key);
408
408
  }}
409
- className="flex-shrink-0 text-panel-text-4 hover:text-panel-text-1"
409
+ className="shrink-0 text-panel-text-4 hover:text-panel-text-1"
410
410
  >
411
411
  <X size={10} />
412
412
  </button>
@@ -37,7 +37,7 @@ export function FlatToggle({
37
37
  track("toggle", label);
38
38
  onChange(!checked);
39
39
  }}
40
- className={`relative h-[14px] w-6 flex-shrink-0 rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
40
+ className={`relative h-[14px] w-6 shrink-0 rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
41
41
  checked ? "bg-panel-accent/35" : "bg-panel-hover"
42
42
  }`}
43
43
  >
@@ -425,7 +425,7 @@ export function FontFamilyField({
425
425
  role="combobox"
426
426
  aria-expanded={open}
427
427
  aria-autocomplete="list"
428
- className="min-w-0 rounded-lg border border-neutral-800 bg-neutral-900 px-2.5 py-2 text-[11px] font-medium text-neutral-100 outline-none placeholder:text-neutral-600 focus:border-neutral-600"
428
+ className="min-w-0 rounded-lg border border-neutral-800 bg-neutral-900 px-2.5 py-2 text-[11px] font-medium text-neutral-100 outline-hidden placeholder:text-neutral-600 focus:border-neutral-600"
429
429
  />
430
430
  {canQueryLocalFonts && (
431
431
  <button
@@ -487,12 +487,12 @@ export function FontFamilyField({
487
487
  {option.family}
488
488
  </span>
489
489
  {renderAliasFor(option.family) && (
490
- <span className="flex-shrink-0 text-[9px] text-neutral-500">
490
+ <span className="shrink-0 text-[9px] text-neutral-500">
491
491
  → {renderAliasFor(option.family)}
492
492
  </span>
493
493
  )}
494
494
  </span>
495
- <span className="flex-shrink-0 text-[9px] uppercase tracking-[0.14em] text-neutral-600">
495
+ <span className="shrink-0 text-[9px] uppercase tracking-[0.14em] text-neutral-600">
496
496
  {option.source}
497
497
  </span>
498
498
  </button>
@@ -524,7 +524,7 @@ export function FontFamilyField({
524
524
  height="10"
525
525
  viewBox="0 0 10 10"
526
526
  fill="currentColor"
527
- className="flex-shrink-0 text-panel-text-5"
527
+ className="shrink-0 text-panel-text-5"
528
528
  >
529
529
  <path d="M2 3l3 4 3-4z" />
530
530
  </svg>
@@ -549,7 +549,7 @@ export function FontFamilyField({
549
549
  >
550
550
  {currentFamily}
551
551
  </span>
552
- <span className="flex-shrink-0 text-[10px] uppercase tracking-[0.14em] text-neutral-600">
552
+ <span className="shrink-0 text-[10px] uppercase tracking-[0.14em] text-neutral-600">
553
553
  Font
554
554
  </span>
555
555
  </button>
@@ -338,7 +338,7 @@ function CarveSourceRow({
338
338
  <div className="hf-fx-row flex min-h-6 items-center gap-2">
339
339
  {/* Wraps like every other name in this column (see FxParamRow) — one
340
340
  truncating row beside wrapping ones reads as a rendering bug. */}
341
- <span className="hf-fx-label w-[86px] flex-shrink-0 break-words text-[10px] leading-tight text-panel-text-2">
341
+ <span className="hf-fx-label w-[86px] shrink-0 wrap-break-word text-[10px] leading-tight text-panel-text-2">
342
342
  Listen to
343
343
  </span>
344
344
  {soleVoice ? (
@@ -141,7 +141,7 @@ describe("FxParamRow label tooltip", () => {
141
141
  // The whole name is present and allowed to wrap. No `title`: a tooltip
142
142
  // answers one row on hover, wrapping answers the column at rest.
143
143
  expect(label?.textContent).toBe("Gap between repeats");
144
- expect(label?.className).toContain("break-words");
144
+ expect(label?.className).toContain("wrap-break-word");
145
145
  expect(label?.className).not.toContain("truncate");
146
146
  expect(label?.getAttribute("title")).toBeNull();
147
147
  // The hint stays where it was: the two are different questions, and the
@@ -88,7 +88,7 @@ export function AutomationToggle({
88
88
  <Tooltip label={automated ? "Automated" : "Automate"}>
89
89
  <button
90
90
  type="button"
91
- className={`hf-fx-automate w-[16px] flex-shrink-0 rounded-[3px] border font-mono text-[9px] leading-none ${
91
+ className={`hf-fx-automate w-[16px] shrink-0 rounded-[3px] border font-mono text-[9px] leading-none ${
92
92
  automated
93
93
  ? "border-panel-accent text-panel-accent"
94
94
  : "border-panel-border-input text-panel-text-2 hover:text-panel-text-0"
@@ -192,10 +192,10 @@ export function FxParamRow({
192
192
  big the space is" — so 86px of truncation left "How big the sp…", and
193
193
  three rows of that read as the same word four times. A title only
194
194
  answers it on hover, one row at a time; wrapping answers it for the
195
- whole column at rest. `break-words` so a long single token breaks
195
+ whole column at rest. `wrap-break-word` so a long single token breaks
196
196
  instead of widening the column. The row keeps `title={param.hint}`:
197
197
  the name and the explanation are different questions. */}
198
- <span className="hf-fx-label w-[86px] flex-shrink-0 break-words text-[10px] leading-tight text-panel-text-2">
198
+ <span className="hf-fx-label w-[86px] shrink-0 wrap-break-word text-[10px] leading-tight text-panel-text-2">
199
199
  {param.label}
200
200
  </span>
201
201
  <select
@@ -236,7 +236,7 @@ export function FxParamRow({
236
236
  >
237
237
  {/* See the enum row above for why the name wraps instead of truncating. */}
238
238
  <span
239
- className={`hf-fx-label w-[86px] flex-shrink-0 break-words text-[10px] leading-tight ${
239
+ className={`hf-fx-label w-[86px] shrink-0 wrap-break-word text-[10px] leading-tight ${
240
240
  automated ? "text-panel-accent" : "text-panel-text-2"
241
241
  }`}
242
242
  >
@@ -258,7 +258,7 @@ export function FxParamRow({
258
258
  onBlur={commit}
259
259
  />
260
260
  <input
261
- className="hf-fx-number w-[54px] flex-shrink-0 rounded-[3px] bg-panel-surface px-1 py-0.5 text-right font-mono text-[10px] text-panel-text-0"
261
+ className="hf-fx-number w-[54px] shrink-0 rounded-[3px] bg-panel-surface px-1 py-0.5 text-right font-mono text-[10px] text-panel-text-0"
262
262
  type="number"
263
263
  min={param.min}
264
264
  max={param.max}
@@ -287,7 +287,7 @@ export function FxParamRow({
287
287
  }}
288
288
  />
289
289
  {param.unit ? (
290
- <span className="hf-fx-unit w-[22px] flex-shrink-0 font-mono text-[9px] text-panel-text-2">
290
+ <span className="hf-fx-unit w-[22px] shrink-0 font-mono text-[9px] text-panel-text-2">
291
291
  {param.unit}
292
292
  </span>
293
293
  ) : null}
@@ -87,7 +87,7 @@ function Fader({
87
87
  <div className="relative h-[74px] w-full">
88
88
  <span className="pointer-events-none absolute inset-x-0 top-1/2 h-px bg-panel-border-input" />
89
89
  <span
90
- className="pointer-events-none absolute left-1/2 w-[3px] -translate-x-1/2 rounded-sm bg-panel-accent"
90
+ className="pointer-events-none absolute left-1/2 w-[3px] -translate-x-1/2 rounded-xs bg-panel-accent"
91
91
  style={
92
92
  // `value`, not `band.gain`: mid-drag across zero the fill would
93
93
  // otherwise keep pointing the way it started.
@@ -95,7 +95,7 @@ function Fader({
95
95
  }
96
96
  />
97
97
  <input
98
- className="hf-fx-eq-fader absolute left-1/2 h-[19px] w-[74px] -translate-x-1/2 -translate-y-1/2 rotate-[-90deg] cursor-ns-resize appearance-none bg-transparent"
98
+ className="hf-fx-eq-fader absolute left-1/2 h-[19px] w-[74px] -translate-x-1/2 -translate-y-1/2 -rotate-90 cursor-ns-resize appearance-none bg-transparent"
99
99
  style={{ top: "50%" }}
100
100
  type="range"
101
101
  min={-HF_AUDIO_EQ_RANGE_DB}
@@ -146,7 +146,7 @@ export const FX_PRESET_STYLE: Record<string, FxPresetStyle> = {
146
146
  },
147
147
  // The name is a joke and the type is in on it.
148
148
  "doofus-worble": {
149
- type: "text-[16px] tracking-[0.1em]",
149
+ type: "text-[16px] tracking-widest",
150
150
  color: "hsl(288, 85%, 72%)",
151
151
  family: FACE.theatrical,
152
152
  },
@@ -164,7 +164,7 @@ export const FX_PRESET_STYLE: Record<string, FxPresetStyle> = {
164
164
  },
165
165
  // Rough and too close — heavy industrial caps, growled rather than shouted.
166
166
  monster: {
167
- type: "text-[15px] font-black uppercase tracking-[0.05em]",
167
+ type: "text-[15px] font-black uppercase tracking-wider",
168
168
  color: "hsl(350, 78%, 64%)",
169
169
  family: FACE.condensed,
170
170
  },
@@ -109,7 +109,7 @@ export function GradingNumberField({
109
109
  event.currentTarget.blur();
110
110
  }
111
111
  }}
112
- className={`border-b border-panel-border-input/50 bg-transparent py-0.5 text-right font-mono text-[9px] text-panel-text-2 outline-none focus:border-panel-accent disabled:opacity-40 ${inputClassName}`}
112
+ className={`border-b border-panel-border-input/50 bg-transparent py-0.5 text-right font-mono text-[9px] text-panel-text-2 outline-hidden focus:border-panel-accent disabled:opacity-40 ${inputClassName}`}
113
113
  />
114
114
  </label>
115
115
  );
@@ -182,7 +182,7 @@ export function MediaSection({
182
182
  event.stopPropagation();
183
183
  void runBackgroundRemoval();
184
184
  }}
185
- className="flex h-8 flex-shrink-0 items-center gap-1.5 rounded-md bg-panel-input px-2.5 text-[11px] font-medium text-panel-text-2 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-50"
185
+ className="flex h-8 shrink-0 items-center gap-1.5 rounded-md bg-panel-input px-2.5 text-[11px] font-medium text-panel-text-2 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-50"
186
186
  title={
187
187
  canRemoveBackground
188
188
  ? "Remove background and save a transparent asset"
@@ -70,21 +70,21 @@ export function MetricField({
70
70
  scrub && !disabled
71
71
  ? ({
72
72
  className:
73
- "flex-shrink-0 text-[11px] font-medium text-neutral-500 cursor-ew-resize select-none",
73
+ "shrink-0 text-[11px] font-medium text-neutral-500 cursor-ew-resize select-none",
74
74
  onPointerDown: handleScrubPointerDown,
75
75
  onPointerMove: handleScrubPointerMove,
76
76
  onPointerUp: handleScrubPointerUp,
77
77
  onPointerCancel: handleScrubPointerUp,
78
78
  onLostPointerCapture: handleScrubPointerUp,
79
79
  } as const)
80
- : ({ className: "flex-shrink-0 text-[11px] font-medium text-neutral-500" } as const);
80
+ : ({ className: "shrink-0 text-[11px] font-medium text-neutral-500" } as const);
81
81
 
82
82
  return (
83
83
  <div className={FIELD} title={tooltip}>
84
84
  <div className="flex min-w-0 items-center gap-3">
85
85
  <span {...scrubProps}>{label}</span>
86
86
  <CommitField value={value} disabled={disabled} liveCommit={liveCommit} onCommit={commit} />
87
- {suffix && <span className="flex-shrink-0 text-[10px] text-neutral-600">{suffix}</span>}
87
+ {suffix && <span className="shrink-0 text-[10px] text-neutral-600">{suffix}</span>}
88
88
  </div>
89
89
  </div>
90
90
  );
@@ -262,7 +262,7 @@ export function SelectField({
262
262
  const renderedOptions = value && !options.includes(value) ? [value, ...options] : options;
263
263
  return (
264
264
  <label className={`${FIELD} flex items-center gap-3`}>
265
- <span className="flex-shrink-0 text-[11px] font-medium text-neutral-500">{label}</span>
265
+ <span className="shrink-0 text-[11px] font-medium text-neutral-500">{label}</span>
266
266
  <select
267
267
  value={value}
268
268
  disabled={disabled}
@@ -270,7 +270,7 @@ export function SelectField({
270
270
  track("select", label);
271
271
  onChange(e.target.value);
272
272
  }}
273
- className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
273
+ className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-hidden disabled:cursor-not-allowed disabled:text-neutral-600"
274
274
  >
275
275
  {renderedOptions.map((option) => (
276
276
  <option key={option} value={option}>
@@ -302,7 +302,7 @@ export function Section({
302
302
  height="10"
303
303
  viewBox="0 0 10 10"
304
304
  fill="currentColor"
305
- className={`flex-shrink-0 text-panel-text-5 transition-transform duration-150 ${
305
+ className={`shrink-0 text-panel-text-5 transition-transform duration-150 ${
306
306
  collapsed ? "-rotate-90" : ""
307
307
  }`}
308
308
  >
@@ -324,7 +324,7 @@ export function Section({
324
324
  <h3 className="text-[12px] font-semibold text-panel-text-1">{title}</h3>
325
325
  {collapseIcon}
326
326
  </button>
327
- {accessory && <div className="flex flex-shrink-0 items-center">{accessory}</div>}
327
+ {accessory && <div className="flex shrink-0 items-center">{accessory}</div>}
328
328
  </div>
329
329
  {!collapsed && <div className="px-4 pb-3">{children}</div>}
330
330
  </section>
@@ -147,7 +147,7 @@ export function TextAreaField({
147
147
  onFocus={handleFocus}
148
148
  onChange={handleChange}
149
149
  onBlur={handleBlur}
150
- className="[field-sizing:content] max-h-[40vh] min-h-12 w-full resize-y overflow-x-hidden overflow-y-auto bg-transparent font-mono text-[11px] leading-normal text-panel-text-0 outline-none disabled:cursor-not-allowed disabled:text-panel-text-4"
150
+ className="field-sizing-content max-h-[40vh] min-h-12 w-full resize-y overflow-x-hidden overflow-y-auto bg-transparent font-mono text-[11px] leading-normal text-panel-text-0 outline-hidden disabled:cursor-not-allowed disabled:text-panel-text-4"
151
151
  />
152
152
  </div>
153
153
  );
@@ -165,7 +165,7 @@ export function TextAreaField({
165
165
  onFocus={handleFocus}
166
166
  onChange={handleChange}
167
167
  onBlur={handleBlur}
168
- className="[field-sizing:content] max-h-[40vh] min-h-20 w-full resize-y overflow-x-hidden overflow-y-auto bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
168
+ className="field-sizing-content max-h-[40vh] min-h-20 w-full resize-y overflow-x-hidden overflow-y-auto bg-transparent text-[11px] font-medium text-neutral-100 outline-hidden disabled:cursor-not-allowed disabled:text-neutral-600"
169
169
  />
170
170
  </div>
171
171
  </label>
@@ -189,7 +189,7 @@ function FontWeightField({
189
189
  return (
190
190
  <div className={FIELD}>
191
191
  <div className="flex min-w-0 items-center gap-3">
192
- <span className="flex-shrink-0 text-[11px] font-medium text-neutral-500">Weight</span>
192
+ <span className="shrink-0 text-[11px] font-medium text-neutral-500">Weight</span>
193
193
  <select
194
194
  value={value}
195
195
  disabled={disabled}
@@ -197,7 +197,7 @@ function FontWeightField({
197
197
  track("select", "Weight");
198
198
  onCommit(e.target.value);
199
199
  }}
200
- className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
200
+ className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-hidden disabled:cursor-not-allowed disabled:text-neutral-600"
201
201
  >
202
202
  {displayOptions.map((o) => (
203
203
  <option key={o} value={o}>
@@ -323,7 +323,7 @@ function TextFieldEditor({
323
323
  track("button", "Remove text field");
324
324
  onRemoveTextField(field.key);
325
325
  }}
326
- className="inline-flex h-7 flex-shrink-0 items-center rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
326
+ className="inline-flex h-7 shrink-0 items-center rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
327
327
  >
328
328
  Remove
329
329
  </button>
@@ -476,7 +476,7 @@ export function TextSection({
476
476
  }}
477
477
  className="inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
478
478
  >
479
- <Plus size={12} className="flex-shrink-0" />
479
+ <Plus size={12} className="shrink-0" />
480
480
  <span className="truncate">Add text</span>
481
481
  </button>
482
482
  </div>
@@ -497,14 +497,14 @@ export function TextSection({
497
497
  <div className="flex min-w-0 items-center justify-between gap-2">
498
498
  <div className="flex min-w-0 items-center gap-2">
499
499
  <span
500
- className="h-4 w-4 flex-shrink-0 rounded border border-neutral-700 bg-neutral-950"
500
+ className="h-4 w-4 shrink-0 rounded-sm border border-neutral-700 bg-neutral-950"
501
501
  style={{ backgroundColor: getTextFieldColor(field, styles) }}
502
502
  />
503
503
  <span className="min-w-0 truncate text-[11px] font-medium text-neutral-100">
504
504
  {formatTextFieldPreview(field.value) || `Text ${index + 1}`}
505
505
  </span>
506
506
  </div>
507
- <span className="flex-shrink-0 rounded-md border border-neutral-700 bg-neutral-950 px-1.5 py-0.5 text-[10px] text-neutral-500">
507
+ <span className="shrink-0 rounded-md border border-neutral-700 bg-neutral-950 px-1.5 py-0.5 text-[10px] text-neutral-500">
508
508
  {field.tagName}
509
509
  </span>
510
510
  </div>
@@ -243,7 +243,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
243
243
  if (req) trackStudioFeedbackInterviewClick({ reason: req.reason });
244
244
  close("sent");
245
245
  }}
246
- className="h-6 flex-shrink-0 whitespace-nowrap rounded-full border border-white/20 bg-white/[0.08] px-2.5 text-[11px] leading-6 text-neutral-100 transition-[background-color,border-color,transform] duration-150 ease-out hover:border-white/35 hover:bg-white/[0.16] active:scale-[0.97] motion-reduce:transition-none"
246
+ className="h-6 shrink-0 whitespace-nowrap rounded-full border border-white/20 bg-white/8 px-2.5 text-[11px] leading-6 text-neutral-100 transition-[background-color,border-color,transform] duration-150 ease-out hover:border-white/35 hover:bg-white/16 active:scale-[0.97] motion-reduce:transition-none"
247
247
  >
248
248
  Talk to us, 30 min
249
249
  </a>
@@ -251,7 +251,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
251
251
  <button
252
252
  type="button"
253
253
  onClick={() => close("close")}
254
- className="-mr-1 -mt-0.5 flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors duration-150 hover:bg-white/10 hover:text-neutral-200"
254
+ className="-mr-1 -mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors duration-150 hover:bg-white/10 hover:text-neutral-200"
255
255
  aria-label="Dismiss"
256
256
  >
257
257
  <svg
@@ -283,7 +283,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
283
283
  key={n}
284
284
  // A resting fill, so the row reads as eleven controls rather
285
285
  // than as a line of text that happens to be clickable.
286
- className="flex h-7 cursor-pointer items-center justify-center rounded-md bg-white/[0.04] text-[11px] tabular-nums text-neutral-400 transition-[background-color,color,transform] duration-150 ease-out hover:bg-white/[0.14] hover:text-neutral-100 active:scale-[0.97] has-[:checked]:bg-white/90 has-[:checked]:text-neutral-900 has-[:focus-visible]:ring-1 has-[:focus-visible]:ring-white/50 motion-reduce:transition-none"
286
+ className="flex h-7 cursor-pointer items-center justify-center rounded-md bg-white/4 text-[11px] tabular-nums text-neutral-400 transition-[background-color,color,transform] duration-150 ease-out hover:bg-white/[0.14] hover:text-neutral-100 active:scale-[0.97] has-checked:bg-white/90 has-checked:text-neutral-900 has-focus-visible:ring-1 has-focus-visible:ring-white/50 motion-reduce:transition-none"
287
287
  >
288
288
  <input
289
289
  type="radio"
@@ -328,8 +328,8 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
328
328
  // attached to nothing.
329
329
  className={`h-6 rounded-full border px-2.5 text-[11px] transition-[background-color,border-color,color,transform] duration-150 ease-out active:scale-[0.97] motion-reduce:transition-none ${
330
330
  failed
331
- ? "border-red-300/20 bg-red-100/[0.06] text-red-100/80 hover:border-red-200/70 hover:bg-red-100/25 hover:text-white"
332
- : "border-white/10 bg-white/[0.04] text-neutral-300 hover:border-white/60 hover:bg-white/25 hover:text-white"
331
+ ? "border-red-300/20 bg-red-100/6 text-red-100/80 hover:border-red-200/70 hover:bg-red-100/25 hover:text-white"
332
+ : "border-white/10 bg-white/4 text-neutral-300 hover:border-white/60 hover:bg-white/25 hover:text-white"
333
333
  }`}
334
334
  >
335
335
  {preset.label}
@@ -355,12 +355,12 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
355
355
  placeholder={followUp.placeholder}
356
356
  aria-label={followUp.prompt}
357
357
  maxLength={500}
358
- className="h-7 min-w-0 flex-1 rounded-md border border-white/10 bg-black/20 px-2 text-[11px] text-neutral-100 outline-none transition-colors duration-150 placeholder:text-neutral-500 focus:border-white/25"
358
+ className="h-7 min-w-0 flex-1 rounded-md border border-white/10 bg-black/20 px-2 text-[11px] text-neutral-100 outline-hidden transition-colors duration-150 placeholder:text-neutral-500 focus:border-white/25"
359
359
  />
360
360
  <button
361
361
  type="button"
362
362
  onClick={() => submit()}
363
- className="h-7 flex-shrink-0 rounded-md bg-white/10 px-2.5 text-[11px] text-neutral-100 transition-[background-color,transform] duration-150 ease-out hover:bg-white/[0.16] active:scale-[0.97] motion-reduce:transition-none"
363
+ className="h-7 shrink-0 rounded-md bg-white/10 px-2.5 text-[11px] text-neutral-100 transition-[background-color,transform] duration-150 ease-out hover:bg-white/16 active:scale-[0.97] motion-reduce:transition-none"
364
364
  >
365
365
  Send
366
366
  </button>
@@ -76,7 +76,6 @@ const FOLLOW_UPS: readonly FollowUpQuestion[] = [
76
76
  { label: "Layers panel", hint: "The layer tree on the right" },
77
77
  { label: "Variables panel", hint: "Composition variables on the right" },
78
78
  { label: "Blocks browser", hint: "The block library in the sidebar" },
79
- { label: "Storyboard mode", hint: "The storyboard view next to Preview" },
80
79
  { label: "Caption editing", hint: "Editing caption words and presets in Studio" },
81
80
  { label: "Render history", hint: "The list of past exports" },
82
81
  { label: "Nothing", hint: "It all earns its place" },
@@ -42,7 +42,11 @@ function AssetPreviewMedia({
42
42
  }) {
43
43
  if (kind === "image") {
44
44
  return (
45
- <img src={serveUrl} alt={name} className="max-w-full max-h-[40vh] rounded object-contain" />
45
+ <img
46
+ src={serveUrl}
47
+ alt={name}
48
+ className="max-w-full max-h-[40vh] rounded-sm object-contain"
49
+ />
46
50
  );
47
51
  }
48
52
  if (kind === "video") {
@@ -53,7 +57,7 @@ function AssetPreviewMedia({
53
57
  autoPlay
54
58
  muted
55
59
  playsInline
56
- className="max-w-full max-h-[40vh] rounded"
60
+ className="max-w-full max-h-[40vh] rounded-sm"
57
61
  />
58
62
  );
59
63
  }
@@ -21,7 +21,7 @@ export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcru
21
21
  return (
22
22
  <nav
23
23
  aria-label="Composition navigation"
24
- className="flex items-center gap-1 px-2 h-8 border-b border-neutral-800/50 bg-neutral-900/50 flex-shrink-0"
24
+ className="flex items-center gap-1 px-2 h-8 border-b border-neutral-800/50 bg-neutral-900/50 shrink-0"
25
25
  >
26
26
  {/* Back button — always goes to parent */}
27
27
  <button
@@ -33,7 +33,7 @@ export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcru
33
33
  });
34
34
  onNavigate(stack.length - 2);
35
35
  }}
36
- className="flex items-center gap-1 px-1.5 py-0.5 rounded text-xs text-neutral-400 hover:text-white hover:bg-neutral-800 active:scale-[0.98] transition-colors"
36
+ className="flex items-center gap-1 px-1.5 py-0.5 rounded-sm text-xs text-neutral-400 hover:text-white hover:bg-neutral-800 active:scale-[0.98] transition-colors"
37
37
  title="Back (Esc, or double-click empty timeline)"
38
38
  aria-label="Back to parent composition"
39
39
  >
@@ -45,7 +45,7 @@ export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcru
45
45
  const isLast = i === stack.length - 1;
46
46
  return (
47
47
  <span key={level.id} className="flex items-center gap-1">
48
- {i > 0 && <CaretRight size={10} className="text-neutral-600 flex-shrink-0" />}
48
+ {i > 0 && <CaretRight size={10} className="text-neutral-600 shrink-0" />}
49
49
  {isLast ? (
50
50
  <span className="text-xs text-neutral-200 font-medium">{level.label}</span>
51
51
  ) : (
@@ -134,6 +134,19 @@ describe("getPreviewPlayerKey", () => {
134
134
  });
135
135
 
136
136
  describe("resolvePreviewStageSize", () => {
137
+ it("reserves the ruler gutter on both sides of both axes", () => {
138
+ const wide = { width: 1920, height: 1080 };
139
+ const tall = { width: 1080, height: 1920 };
140
+ expect(resolvePreviewStageSize(512, 402, wide, undefined, 16)).toEqual({
141
+ width: 464,
142
+ height: 261,
143
+ });
144
+ expect(resolvePreviewStageSize(512, 402, tall, undefined, 16)).toEqual({
145
+ width: 199.125,
146
+ height: 354,
147
+ });
148
+ });
149
+
137
150
  it("fits portrait composition dimensions by height in a narrow viewport", () => {
138
151
  expect(resolvePreviewStageSize(512, 402, { width: 1080, height: 1920 }, undefined)).toEqual({
139
152
  width: 217.125,
@@ -11,6 +11,7 @@ import {
11
11
  toDomPrecision,
12
12
  type PreviewZoomState,
13
13
  } from "./previewZoom";
14
+ import { RULER_GUTTER_PX, usePreviewGuidesStore } from "../editor/previewGuidesStore";
14
15
  import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
15
16
  interface NLEPreviewProps {
16
17
  projectId: string;
@@ -89,9 +90,10 @@ export function resolvePreviewStageSize(
89
90
  viewportHeight: number,
90
91
  compositionSize: PreviewCompositionSize | null,
91
92
  portrait: boolean | undefined,
93
+ gutterPx = 0,
92
94
  ): { width: number; height: number } {
93
- const availableWidth = Math.max(0, viewportWidth - PREVIEW_STAGE_INSET_PX);
94
- const availableHeight = Math.max(0, viewportHeight - PREVIEW_STAGE_INSET_PX);
95
+ const availableWidth = Math.max(0, viewportWidth - PREVIEW_STAGE_INSET_PX - 2 * gutterPx);
96
+ const availableHeight = Math.max(0, viewportHeight - PREVIEW_STAGE_INSET_PX - 2 * gutterPx);
95
97
  const aspectRatio =
96
98
  compositionSize && compositionSize.width > 0 && compositionSize.height > 0
97
99
  ? compositionSize.width / compositionSize.height
@@ -135,6 +137,7 @@ export const NLEPreview = memo(function NLEPreview({
135
137
  onStageRef?.(stageRef);
136
138
  }, [onStageRef]);
137
139
  const [compositionSize, setCompositionSize] = useState<PreviewCompositionSize | null>(null);
140
+ const gutterPx = usePreviewGuidesStore((s) => (s.rulerVisible ? RULER_GUTTER_PX : 0));
138
141
  const [stageSize, setStageSize] = useState(() => resolvePreviewStageSize(0, 0, null, portrait));
139
142
 
140
143
  const zoomRef = useRef<PreviewZoomState>(loadInitialZoom());
@@ -164,14 +167,16 @@ export const NLEPreview = memo(function NLEPreview({
164
167
 
165
168
  const updateStageSize = () => {
166
169
  const rect = viewport.getBoundingClientRect();
167
- setStageSize(resolvePreviewStageSize(rect.width, rect.height, compositionSize, portrait));
170
+ setStageSize(
171
+ resolvePreviewStageSize(rect.width, rect.height, compositionSize, portrait, gutterPx),
172
+ );
168
173
  };
169
174
 
170
175
  updateStageSize();
171
176
  const observer = new ResizeObserver(updateStageSize);
172
177
  observer.observe(viewport);
173
178
  return () => observer.disconnect();
174
- }, [compositionSize, portrait]);
179
+ }, [compositionSize, portrait, gutterPx]);
175
180
 
176
181
  const onCompositionSizeChangeRef = useRef(onCompositionSizeChange);
177
182
  onCompositionSizeChangeRef.current = onCompositionSizeChange;
@@ -437,7 +442,7 @@ export const NLEPreview = memo(function NLEPreview({
437
442
  <div className="flex flex-col h-full min-h-0">
438
443
  <div
439
444
  ref={viewportRef}
440
- className="relative flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0 outline-none focus:ring-1 focus:ring-studio-accent/40 bg-neutral-950"
445
+ className="relative flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0 outline-hidden focus:ring-1 focus:ring-studio-accent/40 bg-neutral-950"
441
446
  tabIndex={0}
442
447
  aria-label="Composition preview"
443
448
  >
@@ -487,14 +492,14 @@ export const NLEPreview = memo(function NLEPreview({
487
492
  </div>
488
493
  <div
489
494
  ref={hudRef}
490
- className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 rounded-lg px-4 py-2 text-sm font-mono tabular-nums text-white/90 bg-black/60 backdrop-blur-sm shadow-lg"
495
+ className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 rounded-lg px-4 py-2 text-sm font-mono tabular-nums text-white/90 bg-black/60 backdrop-blur-xs shadow-lg"
491
496
  style={{ opacity: 0, transition: "opacity 200ms ease-in" }}
492
497
  aria-live="polite"
493
498
  />
494
499
  {!isPreviewAtFit(settledZoom) && (
495
500
  <button
496
501
  type="button"
497
- className="absolute bottom-3 right-3 z-50 rounded-md px-2.5 py-1 text-xs font-medium text-white/80 bg-black/50 backdrop-blur-sm hover:bg-black/70 hover:text-white transition-colors"
502
+ className="absolute bottom-3 right-3 z-50 rounded-md px-2.5 py-1 text-xs font-medium text-white/80 bg-black/50 backdrop-blur-xs hover:bg-black/70 hover:text-white transition-colors"
498
503
  onClick={() => applyZoom(DEFAULT_PREVIEW_ZOOM)}
499
504
  aria-label="Reset zoom to fit"
500
505
  data-testid="preview-reset-zoom"
@@ -222,13 +222,13 @@ export function PreviewOverlays({
222
222
  <TopologyLens iframeRef={previewIframeRef} activeCompositionPath={activeCompPath} />
223
223
  <CaptionOverlay iframeRef={previewIframeRef} />
224
224
  {/* Mode indicator + explicit exit */}
225
- <div className="absolute top-2 left-1/2 -translate-x-1/2 z-[60] flex items-center gap-2 rounded-full border border-studio-accent/40 bg-black/70 px-2.5 py-1">
225
+ <div className="absolute top-2 left-1/2 -translate-x-1/2 z-60 flex items-center gap-2 rounded-full border border-studio-accent/40 bg-black/70 px-2.5 py-1">
226
226
  <span className="h-1.5 w-1.5 rounded-full bg-studio-accent" aria-hidden="true" />
227
227
  <span className="text-2xs text-neutral-200">Editing captions</span>
228
228
  <button
229
229
  type="button"
230
230
  onClick={exitCaptionMode}
231
- className="rounded text-2xs text-neutral-400 underline underline-offset-2 hover:text-neutral-100 focus-visible:outline focus-visible:outline-1 focus-visible:outline-studio-accent"
231
+ className="rounded-sm text-2xs text-neutral-400 underline underline-offset-2 hover:text-neutral-100 focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-studio-accent"
232
232
  >
233
233
  Exit
234
234
  </button>
@@ -236,13 +236,13 @@ export function PreviewOverlays({
236
236
  {captionSyncError && (
237
237
  <div
238
238
  role="alert"
239
- className="absolute top-10 left-1/2 -translate-x-1/2 z-[60] flex items-center gap-2 rounded-full border border-red-500/50 bg-red-950/90 px-2.5 py-1"
239
+ className="absolute top-10 left-1/2 -translate-x-1/2 z-60 flex items-center gap-2 rounded-full border border-red-500/50 bg-red-950/90 px-2.5 py-1"
240
240
  >
241
241
  <span className="text-2xs text-red-200">{captionSyncError}</span>
242
242
  <button
243
243
  type="button"
244
244
  onClick={() => useCaptionStore.getState().retrySave?.()}
245
- className="rounded text-2xs text-red-100 underline underline-offset-2 hover:text-white"
245
+ className="rounded-sm text-2xs text-red-100 underline underline-offset-2 hover:text-white"
246
246
  >
247
247
  Retry
248
248
  </button>
@@ -250,7 +250,7 @@ export function PreviewOverlays({
250
250
  type="button"
251
251
  onClick={() => useCaptionStore.getState().setSyncError(null)}
252
252
  aria-label="Dismiss"
253
- className="rounded px-0.5 text-2xs text-red-300/70 hover:text-red-100"
253
+ className="rounded-sm px-0.5 text-2xs text-red-300/70 hover:text-red-100"
254
254
  >
255
255
  ✕
256
256
  </button>
@@ -338,7 +338,7 @@ export function PreviewOverlays({
338
338
  <button
339
339
  type="button"
340
340
  onClick={enterCaptionMode}
341
- className="absolute top-2 left-1/2 -translate-x-1/2 z-[60] rounded-full border border-neutral-700 bg-black/60 px-2.5 py-1 text-2xs text-neutral-300 transition-colors hover:border-studio-accent/50 hover:text-studio-accent focus-visible:outline focus-visible:outline-1 focus-visible:outline-studio-accent"
341
+ className="absolute top-2 left-1/2 -translate-x-1/2 z-60 rounded-full border border-neutral-700 bg-black/60 px-2.5 py-1 text-2xs text-neutral-300 transition-colors hover:border-studio-accent/50 hover:text-studio-accent focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-studio-accent"
342
342
  >
343
343
  Edit captions
344
344
  </button>