@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
@@ -0,0 +1,213 @@
1
+ /* Studio's design tokens, the one place a design value is decided. Tailwind emits
2
+ * each as a utility and a CSS custom property; `@theme static` keeps unused
3
+ * tokens in `:root` so plain CSS and canvas code can still read them. */
4
+
5
+ @theme static {
6
+ /* ---------------------------------------------------------------- colour */
7
+
8
+ /* Background levels, darkest (app shell) to lightest (inset panel body). */
9
+ --color-bg-0: #0a0a0a;
10
+ --color-bg-1: #0c0c0e;
11
+ --color-bg-2: #121214;
12
+
13
+ /* Raised surfaces. */
14
+ --color-surface: #18181b;
15
+ --color-surface-alt: #141414;
16
+ --color-input: #161618;
17
+ --color-hover: #27272a;
18
+
19
+ /* Separators, weakest to strongest. */
20
+ --color-hairline: #1a1a1c;
21
+ --color-border: #1e1e1e;
22
+ --color-border-input: #27272a;
23
+ --color-border-strong: #262626;
24
+
25
+ /* Text, brightest (0) to dimmest (5). */
26
+ --color-text-0: #fafafa;
27
+ --color-text-1: #e4e4e7;
28
+ --color-text-2: #a1a1aa;
29
+ --color-text-3: #71717a;
30
+ --color-text-4: #52525b;
31
+ --color-text-5: #3f3f46;
32
+ --color-text-body: #e5e5e5;
33
+ --color-text-muted: #737373;
34
+
35
+ /* Roles. Selection and playhead are reserved and differ from the accent so a
36
+ * selected clip, the playhead and an accented control stay tellable apart. */
37
+ --color-accent: #3ce6ac;
38
+ --color-selection: #4ba3d2;
39
+ --color-playhead: #ffffff;
40
+ --color-danger: #ef4444;
41
+ --color-media: #00e3ff;
42
+ --color-container: #f5a623;
43
+
44
+ /* -------------------------------------------------------------- geometry */
45
+
46
+ --radius-sm: 0.25rem;
47
+ --radius-md: 0.375rem;
48
+ --radius-lg: 0.5rem;
49
+
50
+ /* Control heights: 24, 28 and 32 px. `--spacing-*` gives `h-ctl`, `min-h-ctl`
51
+ * and friends. */
52
+ --spacing-ctl-sm: 24px;
53
+ --spacing-ctl: 28px;
54
+ --spacing-ctl-lg: 32px;
55
+
56
+ /* ------------------------------------------------------------------ type */
57
+
58
+ /* Dense UI type sizes named by pixel value. `step` avoids a clash with
59
+ * Tailwind's own `--text-*` scale, whose `xs`, `sm` and `lg` are unchanged. */
60
+ --text-step-7: 7px;
61
+ --text-step-8: 8px;
62
+ --text-step-9: 9px;
63
+ --text-step-10: 10px;
64
+ --text-step-11: 11px;
65
+ --text-step-12: 12px;
66
+ --text-step-13: 13px;
67
+ --text-step-14: 14px;
68
+ --text-step-15: 15px;
69
+ --text-step-16: 16px;
70
+ --text-step-17: 17px;
71
+ --text-step-18: 18px;
72
+
73
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
74
+ --font-mono: "JetBrains Mono", "IBM Plex Mono", "SF Mono", "Fira Code", monospace;
75
+
76
+ /* ---------------------------------------------------------------- shadow */
77
+
78
+ /* Floating chrome over a near-black canvas: the hairline ring does the edge
79
+ * definition that a light-mode drop shadow cannot. */
80
+ --shadow-menu: 0 8px 24px rgb(0 0 0 / 0.45), 0 0 0 1px rgb(0 0 0 / 0.4);
81
+ --shadow-popover: 0 16px 40px rgb(0 0 0 / 0.55), 0 0 0 1px rgb(0 0 0 / 0.45);
82
+
83
+ /* ---------------------------------------------------------------- motion */
84
+
85
+ --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
86
+ --ease-out-expo: cubic-bezier(0.23, 1, 0.32, 1);
87
+ --ease-in-out-circ: cubic-bezier(0.77, 0, 0.175, 1);
88
+
89
+ /* Durations have no Tailwind namespace, so these are plain variables paired
90
+ * with the `@utility` rules at the bottom of this file. All are at or under
91
+ * 200 ms. */
92
+ --duration-focus: 100ms;
93
+ --duration-press: 120ms;
94
+ --duration-hover: 150ms;
95
+ --duration-expand: 160ms;
96
+ --duration-open: 180ms;
97
+
98
+ /* ------------------------------------------------------------------ icon */
99
+
100
+ /* Read by `readIconTokens` and handed to Phosphor's `IconContext`. */
101
+ --icon-size: 16px;
102
+ --icon-weight: regular;
103
+
104
+ /* ------------------------------------------------- deprecated: old names */
105
+
106
+ /* The `studio.*` and `panel.*` scales, aliases of the tokens above so existing
107
+ * markup keeps working during the sweep. U12 removes them from Studio's
108
+ * source; the published JS preset keeps them one more major. */
109
+ --color-studio-bg: var(--color-bg-0);
110
+ --color-studio-surface: var(--color-surface-alt);
111
+ --color-studio-border: var(--color-border-strong);
112
+ --color-studio-text: var(--color-text-body);
113
+ --color-studio-muted: var(--color-text-muted);
114
+ --color-studio-accent: var(--color-accent);
115
+ --color-panel-bg: var(--color-bg-1);
116
+ --color-panel-bg-inset: var(--color-bg-2);
117
+ --color-panel-input: var(--color-input);
118
+ --color-panel-surface: var(--color-surface);
119
+ --color-panel-hover: var(--color-hover);
120
+ --color-panel-border: var(--color-border);
121
+ --color-panel-border-input: var(--color-border-input);
122
+ --color-panel-hairline: var(--color-hairline);
123
+ --color-panel-text-0: var(--color-text-0);
124
+ --color-panel-text-1: var(--color-text-1);
125
+ --color-panel-text-2: var(--color-text-2);
126
+ --color-panel-text-3: var(--color-text-3);
127
+ --color-panel-text-4: var(--color-text-4);
128
+ --color-panel-text-5: var(--color-text-5);
129
+ --color-panel-accent: var(--color-accent);
130
+ --color-panel-danger: var(--color-danger);
131
+ --color-panel-media: var(--color-media);
132
+ --color-panel-container: var(--color-container);
133
+
134
+ /* ------------------------------------------- deprecated: Tailwind palette */
135
+
136
+ /* Verbatim Tailwind 4.3.3 defaults for stock colours markup still names, so
137
+ * `--color-*: initial` does not blank them. U12 deletes this block;
138
+ * `theme.test.ts` fails if a value drifts from upstream. */
139
+ --color-amber-50: oklch(98.7% 0.022 95.277);
140
+ --color-amber-100: oklch(96.2% 0.059 95.617);
141
+ --color-amber-200: oklch(92.4% 0.12 95.746);
142
+ --color-amber-300: oklch(87.9% 0.169 91.605);
143
+ --color-amber-400: oklch(82.8% 0.189 84.429);
144
+ --color-amber-500: oklch(76.9% 0.188 70.08);
145
+ --color-amber-900: oklch(41.4% 0.112 45.904);
146
+ --color-amber-950: oklch(27.9% 0.077 45.635);
147
+ --color-black: #000;
148
+ --color-blue-400: oklch(70.7% 0.165 254.624);
149
+ --color-blue-500: oklch(62.3% 0.214 259.815);
150
+ --color-cyan-400: oklch(78.9% 0.154 211.53);
151
+ --color-cyan-500: oklch(71.5% 0.143 215.221);
152
+ --color-emerald-300: oklch(84.5% 0.143 164.978);
153
+ --color-emerald-400: oklch(76.5% 0.177 163.223);
154
+ --color-emerald-500: oklch(69.6% 0.17 162.48);
155
+ --color-emerald-600: oklch(59.6% 0.145 163.225);
156
+ --color-emerald-700: oklch(50.8% 0.118 165.612);
157
+ --color-green-400: oklch(79.2% 0.209 151.711);
158
+ --color-green-500: oklch(72.3% 0.219 149.579);
159
+ --color-green-600: oklch(62.7% 0.194 149.214);
160
+ --color-neutral-50: oklch(98.5% 0 none);
161
+ --color-neutral-100: oklch(97% 0 none);
162
+ --color-neutral-200: oklch(92.2% 0 none);
163
+ --color-neutral-300: oklch(87% 0 none);
164
+ --color-neutral-400: oklch(70.8% 0 none);
165
+ --color-neutral-500: oklch(55.6% 0 none);
166
+ --color-neutral-600: oklch(43.9% 0 none);
167
+ --color-neutral-700: oklch(37.1% 0 none);
168
+ --color-neutral-800: oklch(26.9% 0 none);
169
+ --color-neutral-900: oklch(20.5% 0 none);
170
+ --color-neutral-950: oklch(14.5% 0 none);
171
+ --color-orange-300: oklch(83.7% 0.128 66.29);
172
+ --color-orange-400: oklch(75% 0.183 55.934);
173
+ --color-pink-400: oklch(71.8% 0.202 349.761);
174
+ --color-pink-500: oklch(65.6% 0.241 354.308);
175
+ --color-purple-200: oklch(90.2% 0.063 306.703);
176
+ --color-purple-300: oklch(82.7% 0.119 306.383);
177
+ --color-purple-400: oklch(71.4% 0.203 305.504);
178
+ --color-purple-500: oklch(62.7% 0.265 303.9);
179
+ --color-purple-900: oklch(38.1% 0.176 304.987);
180
+ --color-red-100: oklch(93.6% 0.032 17.717);
181
+ --color-red-200: oklch(88.5% 0.062 18.334);
182
+ --color-red-300: oklch(80.8% 0.114 19.571);
183
+ --color-red-400: oklch(70.4% 0.191 22.216);
184
+ --color-red-500: oklch(63.7% 0.237 25.331);
185
+ --color-red-600: oklch(57.7% 0.245 27.325);
186
+ --color-red-700: oklch(50.5% 0.213 27.518);
187
+ --color-red-800: oklch(44.4% 0.177 26.899);
188
+ --color-red-900: oklch(39.6% 0.141 25.723);
189
+ --color-red-950: oklch(25.8% 0.092 26.042);
190
+ --color-rose-400: oklch(71.2% 0.194 13.428);
191
+ --color-rose-500: oklch(64.5% 0.246 16.439);
192
+ --color-sky-200: oklch(90.1% 0.058 230.902);
193
+ --color-sky-300: oklch(82.8% 0.111 230.318);
194
+ --color-sky-400: oklch(74.6% 0.16 232.661);
195
+ --color-sky-500: oklch(68.5% 0.169 237.323);
196
+ --color-sky-700: oklch(50% 0.134 242.749);
197
+ --color-sky-900: oklch(39.1% 0.09 240.876);
198
+ --color-sky-950: oklch(29.3% 0.066 243.157);
199
+ --color-violet-400: oklch(70.2% 0.183 293.541);
200
+ --color-violet-500: oklch(60.6% 0.25 292.717);
201
+ --color-white: #fff;
202
+ }
203
+
204
+ /* Motion durations as utilities, since Tailwind has no `--duration-*`
205
+ * namespace. Each one zeroes itself under `prefers-reduced-motion`, so a
206
+ * caller cannot forget the reduced-motion case. */
207
+
208
+ @utility duration-* {
209
+ transition-duration: --value(--duration-*);
210
+ @media (prefers-reduced-motion: reduce) {
211
+ transition-duration: 0ms;
212
+ }
213
+ }
@@ -0,0 +1,196 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /** Guards the ways `theme.css` can silently break: a dropped CSS custom
4
+ * property, a utility that stops compiling, the stock Tailwind palette
5
+ * leaking back in, or the JS preset / legacy colours drifting from it. */
6
+
7
+ import { createRequire } from "node:module";
8
+ import { readFileSync } from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import React, { act } from "react";
12
+ import { createRoot } from "react-dom/client";
13
+ import { Gear, IconContext } from "@phosphor-icons/react";
14
+ import { compile } from "tailwindcss";
15
+ import { afterEach, describe, expect, it } from "vitest";
16
+ import studioPreset from "./tailwind-preset.shared.js";
17
+ import { readIconTokens } from "./iconTokens";
18
+
19
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
20
+
21
+ const STYLES_DIR = path.dirname(fileURLToPath(import.meta.url));
22
+ const require = createRequire(import.meta.url);
23
+ const TAILWIND_DIR = path.dirname(require.resolve("tailwindcss/package.json"));
24
+
25
+ function loadStylesheet(id: string, base: string) {
26
+ const file = id === "tailwindcss" ? path.join(TAILWIND_DIR, "index.css") : path.resolve(base, id);
27
+ return { path: file, base: path.dirname(file), content: readFileSync(file, "utf8") };
28
+ }
29
+
30
+ async function buildSource(source: string, candidates: string[]): Promise<string> {
31
+ const compiled = await compile(source, {
32
+ base: STYLES_DIR,
33
+ loadStylesheet,
34
+ });
35
+ return compiled.build(candidates);
36
+ }
37
+
38
+ const build = (entry: string, candidates: string[]) =>
39
+ buildSource(readFileSync(path.join(STYLES_DIR, entry), "utf8"), candidates);
40
+
41
+ function rootVariables(css: string): Map<string, string> {
42
+ const block = css.match(/:root, :host \{([\s\S]*?)\n {2}\}/);
43
+ const vars = new Map<string, string>();
44
+ for (const line of (block?.[1] ?? "").split(";")) {
45
+ const match = line.match(/(--[a-z0-9-]+):([\s\S]+)/i);
46
+ if (match) vars.set(match[1], match[2].trim().replace(/\s+/g, " "));
47
+ }
48
+ return vars;
49
+ }
50
+
51
+ const themeSource = readFileSync(path.join(STYLES_DIR, "theme.css"), "utf8");
52
+
53
+ function declaredValue(name: string): string {
54
+ const match = themeSource.match(new RegExp(`\\n\\s*${name}:\\s*([^;]+);`));
55
+ if (!match) throw new Error(`theme.css does not declare ${name}`);
56
+ return match[1].trim();
57
+ }
58
+
59
+ afterEach(() => {
60
+ document.body.innerHTML = "";
61
+ document.documentElement.removeAttribute("style");
62
+ });
63
+
64
+ describe("studio theme", () => {
65
+ it("emits the semantic palette as custom properties and as utilities", async () => {
66
+ const css = await build("studio.css", ["bg-accent", "text-text-2", "border-border"]);
67
+
68
+ expect(rootVariables(css).get("--color-accent")).toBe("#3ce6ac");
69
+ expect(css).toContain(".bg-accent {");
70
+ expect(css).toContain("background-color: var(--color-accent)");
71
+ });
72
+
73
+ it("resolves an alpha modifier on a plain hex token through color-mix", async () => {
74
+ const css = await build("studio.css", ["bg-accent/30"]);
75
+
76
+ expect(css).toMatch(/\.bg-accent\\\/30 \{[\s\S]*?color-mix\(/);
77
+ });
78
+
79
+ it("drops every Tailwind default color Studio does not use", async () => {
80
+ // `palette-reset.css` clears the stock palette. The entries Studio's
81
+ // markup still references are re-declared as deprecated aliases (U12
82
+ // removes them); anything outside that set must no longer resolve.
83
+ const css = await build("studio.css", ["bg-neutral-750", "text-teal-500", "bg-neutral-800"]);
84
+
85
+ expect(css).not.toContain("bg-neutral-750");
86
+ expect(css).not.toContain("text-teal-500");
87
+ expect(css).toContain(".bg-neutral-800 {");
88
+ });
89
+
90
+ it("leaves a host's stock Tailwind colors alone when only theme.css is imported", async () => {
91
+ const css = await buildSource('@import "tailwindcss";\n@import "./theme.css";', [
92
+ "bg-teal-500",
93
+ "text-accent",
94
+ ]);
95
+
96
+ expect(css).toContain(".bg-teal-500 {");
97
+ expect(css).toContain(".text-accent {");
98
+ });
99
+
100
+ it("keeps selection, playhead and accent as three distinct colors", () => {
101
+ const accent = declaredValue("--color-accent");
102
+ const selection = declaredValue("--color-selection");
103
+ const playhead = declaredValue("--color-playhead");
104
+
105
+ expect(new Set([accent, selection, playhead]).size).toBe(3);
106
+ });
107
+
108
+ it("names every micro type step in use and leaves the Tailwind sizes alone", async () => {
109
+ const steps = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
110
+ const css = await build(
111
+ "studio.css",
112
+ steps.map((step) => `text-step-${step}`).concat(["text-xs", "text-sm", "text-lg"]),
113
+ );
114
+ const vars = rootVariables(css);
115
+
116
+ for (const step of steps) {
117
+ expect(vars.get(`--text-step-${step}`)).toBe(`${step}px`);
118
+ expect(css).toContain(`.text-step-${step} {`);
119
+ }
120
+ expect(vars.get("--text-xs")).toBe("0.75rem");
121
+ expect(vars.get("--text-sm")).toBe("0.875rem");
122
+ expect(vars.get("--text-lg")).toBe("1.125rem");
123
+ });
124
+
125
+ it("compiles a motion-duration utility with a reduced-motion variant", async () => {
126
+ const css = await build("studio.css", ["duration-press", "duration-open"]);
127
+
128
+ expect(css).toMatch(
129
+ /\.duration-press \{[\s\S]*?transition-duration: var\(--duration-press\)[\s\S]*?prefers-reduced-motion: reduce[\s\S]*?transition-duration: 0ms/,
130
+ );
131
+ expect(css).toContain(".duration-open {");
132
+ });
133
+
134
+ it("keeps every legacy Tailwind palette entry at its upstream default value", async () => {
135
+ // These are copies of Tailwind's own values, kept only so existing markup
136
+ // renders unchanged. A Tailwind upgrade that moves one of them would
137
+ // otherwise silently change Studio's colors.
138
+ const upstream = new Map(
139
+ [
140
+ ...readFileSync(path.join(TAILWIND_DIR, "theme.css"), "utf8").matchAll(
141
+ /\n\s*(--color-[a-z]+-(?:50|\d00|950)):\s*([^;]+);/g,
142
+ ),
143
+ ].map(([, name, value]) => [name, value.trim()]),
144
+ );
145
+ const legacy = [...themeSource.matchAll(/\n\s*(--color-[a-z]+-(?:50|\d00|950)):\s*([^;]+);/g)];
146
+
147
+ expect(legacy.length).toBeGreaterThan(0);
148
+ for (const [, name, value] of legacy) {
149
+ expect(upstream.has(name), `${name} is not a Tailwind default`).toBe(true);
150
+ expect(`${name}: ${value.trim()}`).toBe(`${name}: ${upstream.get(name)}`);
151
+ }
152
+ });
153
+
154
+ it("keeps the deprecated JS preset in step with the CSS it shadows", () => {
155
+ const colors = studioPreset.theme.extend.colors;
156
+
157
+ expect(colors.studio.accent.toLowerCase()).toBe(declaredValue("--color-accent"));
158
+ expect(colors.panel.surface.toLowerCase()).toBe(declaredValue("--color-surface"));
159
+ });
160
+
161
+ it("reads the icon defaults off the theme rather than hard-coding them", () => {
162
+ document.documentElement.style.setProperty("--icon-size", "14px");
163
+ document.documentElement.style.setProperty("--icon-weight", "bold");
164
+
165
+ expect(readIconTokens()).toEqual({ size: "14px", weight: "bold" });
166
+ });
167
+
168
+ it("gives a child icon the token size and weight when it sets neither", () => {
169
+ document.documentElement.style.setProperty("--icon-size", "14px");
170
+ document.documentElement.style.setProperty("--icon-weight", "bold");
171
+
172
+ const host = document.createElement("div");
173
+ document.body.append(host);
174
+ const root = createRoot(host);
175
+ act(() => {
176
+ root.render(
177
+ React.createElement(
178
+ IconContext.Provider,
179
+ { value: readIconTokens() },
180
+ React.createElement(Gear, null),
181
+ ),
182
+ );
183
+ });
184
+ const inherited = host.querySelector("svg");
185
+ const inheritedShape = inherited?.innerHTML;
186
+ act(() => {
187
+ root.render(React.createElement(Gear, { size: 14, weight: "bold" }));
188
+ });
189
+ const explicitShape = host.querySelector("svg")?.innerHTML;
190
+ act(() => root.unmount());
191
+
192
+ expect(inherited?.getAttribute("width")).toBe("14px");
193
+ expect(inherited?.getAttribute("height")).toBe("14px");
194
+ expect(inheritedShape).toBe(explicitShape);
195
+ });
196
+ });
@@ -33,10 +33,41 @@ describe("deduplicateIds", () => {
33
33
  });
34
34
 
35
35
  describe("serializeClipboardPayload / deserializeClipboardPayload", () => {
36
- it("round-trips a timeline clip payload", () => {
36
+ it("round-trips a single-clip timeline payload", () => {
37
37
  const payload: ClipboardPayload = {
38
38
  kind: "timeline-clip",
39
- html: '<img id="photo" src="a.png" data-start="1" data-duration="3" />',
39
+ clips: [
40
+ {
41
+ html: '<img id="photo" src="a.png" data-start="1" data-duration="3" />',
42
+ start: 1,
43
+ duration: 3,
44
+ track: 0,
45
+ },
46
+ ],
47
+ sourceFile: "index.html",
48
+ };
49
+ const json = serializeClipboardPayload(payload);
50
+ const parsed = deserializeClipboardPayload(json);
51
+ expect(parsed).toEqual(payload);
52
+ });
53
+
54
+ it("round-trips a group timeline-clip payload", () => {
55
+ const payload: ClipboardPayload = {
56
+ kind: "timeline-clip",
57
+ clips: [
58
+ {
59
+ html: '<img id="a" data-start="1" data-duration="0.5" />',
60
+ start: 1,
61
+ duration: 0.5,
62
+ track: 0,
63
+ },
64
+ {
65
+ html: '<img id="b" data-start="2" data-duration="0.5" />',
66
+ start: 2,
67
+ duration: 0.5,
68
+ track: 1,
69
+ },
70
+ ],
40
71
  sourceFile: "index.html",
41
72
  };
42
73
  const json = serializeClipboardPayload(payload);
@@ -59,4 +90,13 @@ describe("serializeClipboardPayload / deserializeClipboardPayload", () => {
59
90
  expect(deserializeClipboardPayload("not json")).toBeNull();
60
91
  expect(deserializeClipboardPayload('{"kind":"unknown"}')).toBeNull();
61
92
  });
93
+
94
+ it("returns null for a timeline-clip payload with no clips", () => {
95
+ const json = serializeClipboardPayload({
96
+ kind: "timeline-clip",
97
+ clips: [],
98
+ sourceFile: "index.html",
99
+ });
100
+ expect(deserializeClipboardPayload(json)).toBeNull();
101
+ });
62
102
  });
@@ -2,35 +2,35 @@ import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
2
2
 
3
3
  const CLIPBOARD_MARKER = "hyperframes-clipboard:v1";
4
4
 
5
- export interface ClipboardPayload {
6
- kind: "timeline-clip" | "dom-element";
5
+ /** One clip's copied markup plus the original placement needed to preserve
6
+ * relative offsets and tracks across a group paste. */
7
+ export interface TimelineClipboardClip {
7
8
  html: string;
8
- sourceFile: string;
9
- originSelector?: string;
10
- originSelectorIndex?: number;
9
+ start: number;
10
+ duration: number;
11
+ track: number;
11
12
  }
12
13
 
13
- interface SerializedPayload {
14
- _marker: string;
15
- kind: "timeline-clip" | "dom-element";
16
- html: string;
17
- sourceFile: string;
18
- originSelector?: string;
19
- originSelectorIndex?: number;
20
- }
14
+ export type ClipboardPayload =
15
+ | { kind: "timeline-clip"; clips: TimelineClipboardClip[]; sourceFile: string }
16
+ | {
17
+ kind: "dom-element";
18
+ html: string;
19
+ sourceFile: string;
20
+ originSelector?: string;
21
+ originSelectorIndex?: number;
22
+ };
23
+
24
+ type SerializedPayload = { _marker: string } & ClipboardPayload;
21
25
 
22
26
  export function serializeClipboardPayload(payload: ClipboardPayload): string {
23
- const data: SerializedPayload = {
24
- _marker: CLIPBOARD_MARKER,
25
- kind: payload.kind,
26
- html: payload.html,
27
- sourceFile: payload.sourceFile,
28
- originSelector: payload.originSelector,
29
- originSelectorIndex: payload.originSelectorIndex,
30
- };
27
+ const data: SerializedPayload = { _marker: CLIPBOARD_MARKER, ...payload };
31
28
  return JSON.stringify(data);
32
29
  }
33
30
 
31
+ // Each branch validates one wire shape at the trust boundary; splitting further
32
+ // would fragment one parse into partial validators with no independent reuse.
33
+ // fallow-ignore-next-line complexity
34
34
  export function deserializeClipboardPayload(json: string): ClipboardPayload | null {
35
35
  let parsed: unknown;
36
36
  try {
@@ -41,16 +41,35 @@ export function deserializeClipboardPayload(json: string): ClipboardPayload | nu
41
41
  if (!parsed || typeof parsed !== "object") return null;
42
42
  const obj = parsed as Record<string, unknown>;
43
43
  if (obj._marker !== CLIPBOARD_MARKER) return null;
44
- if (obj.kind !== "timeline-clip" && obj.kind !== "dom-element") return null;
45
- if (typeof obj.html !== "string" || typeof obj.sourceFile !== "string") return null;
46
- return {
47
- kind: obj.kind,
48
- html: obj.html,
49
- sourceFile: obj.sourceFile,
50
- originSelector: typeof obj.originSelector === "string" ? obj.originSelector : undefined,
51
- originSelectorIndex:
52
- typeof obj.originSelectorIndex === "number" ? obj.originSelectorIndex : undefined,
53
- };
44
+ if (typeof obj.sourceFile !== "string") return null;
45
+ if (obj.kind === "timeline-clip") {
46
+ if (!Array.isArray(obj.clips)) return null;
47
+ const clips = obj.clips.filter((c): c is TimelineClipboardClip => {
48
+ const clip = c as Partial<TimelineClipboardClip> | null;
49
+ return (
50
+ !!clip &&
51
+ typeof clip === "object" &&
52
+ typeof clip.html === "string" &&
53
+ typeof clip.start === "number" &&
54
+ typeof clip.duration === "number" &&
55
+ typeof clip.track === "number"
56
+ );
57
+ });
58
+ if (clips.length === 0) return null;
59
+ return { kind: "timeline-clip", clips, sourceFile: obj.sourceFile };
60
+ }
61
+ if (obj.kind === "dom-element") {
62
+ if (typeof obj.html !== "string") return null;
63
+ return {
64
+ kind: "dom-element",
65
+ html: obj.html,
66
+ sourceFile: obj.sourceFile,
67
+ originSelector: typeof obj.originSelector === "string" ? obj.originSelector : undefined,
68
+ originSelectorIndex:
69
+ typeof obj.originSelectorIndex === "number" ? obj.originSelectorIndex : undefined,
70
+ };
71
+ }
72
+ return null;
54
73
  }
55
74
 
56
75
  /**
@@ -59,6 +78,7 @@ export function deserializeClipboardPayload(json: string): ClipboardPayload | nu
59
78
  * the composition root if the selector doesn't match — so paste never silently
60
79
  * drops the content.
61
80
  */
81
+ // fallow-ignore-next-line complexity
62
82
  export function insertAsSibling(
63
83
  source: string,
64
84
  newHtml: string,
@@ -110,6 +130,7 @@ export function insertAsSibling(
110
130
  return source + newHtml;
111
131
  }
112
132
 
133
+ // fallow-ignore-next-line complexity
113
134
  function findClosingTagPosition(html: string, openTagStart: number): number {
114
135
  // Find the end of the opening tag
115
136
  const openTagEnd = html.indexOf(">", openTagStart);
@@ -156,9 +177,13 @@ function findClosingTagPosition(html: string, openTagStart: number): number {
156
177
  return -1;
157
178
  }
158
179
 
180
+ /** An `id="..."` attribute, not `data-id="..."` or similar — only matches
181
+ * when preceded by whitespace, the way every generated attribute is. */
182
+ export const ID_ATTR_RE = /(?<=\s)id="([^"]+)"/;
183
+
159
184
  export function deduplicateIds(html: string, existingIds: string[]): string {
160
185
  const existingSet = new Set(existingIds);
161
- return html.replace(/(?<=\s)id="([^"]+)"/g, (full, id: string) => {
186
+ return html.replace(new RegExp(ID_ATTR_RE.source, "g"), (full, id: string) => {
162
187
  if (!existingSet.has(id)) return full;
163
188
  let counter = 2;
164
189
  while (existingSet.has(`${id}-${counter}`)) counter++;
@@ -0,0 +1,34 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ ACTION_SAFE_PERCENT,
6
+ SAFE_BOX_PERCENTS,
7
+ TITLE_SAFE_PERCENT,
8
+ safeBoxInsetPercent,
9
+ } from "./previewSafeMargins";
10
+
11
+ const SKILL_PATH = join(import.meta.dirname, "../../../../skills/hyperframes-studio/SKILL.md");
12
+
13
+ describe("safe boxes", () => {
14
+ it("are the 90% and 80% boxes, inset 5% and 10% from every edge", () => {
15
+ expect(SAFE_BOX_PERCENTS).toEqual([90, 80]);
16
+ expect(SAFE_BOX_PERCENTS.map(safeBoxInsetPercent)).toEqual([5, 10]);
17
+ });
18
+ });
19
+
20
+ // Skipped until the hyperframes-studio skill exists on this branch.
21
+ describe.runIf(existsSync(SKILL_PATH))("hyperframes-studio skill matches the constants", () => {
22
+ it("has a safe-margins line naming both percentages", () => {
23
+ const skill = readFileSync(SKILL_PATH, "utf8");
24
+ const line = skill
25
+ .split("\n")
26
+ .find(
27
+ (l) =>
28
+ /\bsafe margins?\b/i.test(l) &&
29
+ l.includes(`${ACTION_SAFE_PERCENT}%`) &&
30
+ l.includes(`${TITLE_SAFE_PERCENT}%`),
31
+ );
32
+ expect(line).toBeDefined();
33
+ });
34
+ });
@@ -0,0 +1,10 @@
1
+ // Adobe Premiere default: action-safe and title-safe margins are 10% and 20% total (5% and 10% per edge).
2
+ export const ACTION_SAFE_PERCENT = 90;
3
+ export const TITLE_SAFE_PERCENT = 80;
4
+
5
+ export const SAFE_BOX_PERCENTS = [ACTION_SAFE_PERCENT, TITLE_SAFE_PERCENT] as const;
6
+
7
+ /** Inset from each frame edge, as a percent of the frame. */
8
+ export function safeBoxInsetPercent(boxPercent: number): number {
9
+ return (100 - boxPercent) / 2;
10
+ }
@@ -10,7 +10,6 @@ const element = (over: Partial<TimelineElement> = {}): TimelineElement => ({
10
10
  start: 0,
11
11
  duration: 4,
12
12
  track: 0,
13
- timingSource: "authored",
14
13
  sourceFile: "index.html",
15
14
  ...over,
16
15
  });