@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,52 @@
1
+ /** Plain-field clip facts from the player store; feeds the Ask-agent prompt and `studio_look`. */
2
+ import type { TimelineElement } from "../store/playerStore";
3
+ import { elementAutomationLanes } from "../components/automationLaneData";
4
+ import { byStart, formatClipLine, type ClipFact } from "@hyperframes/core/clip-facts";
5
+ export { byStart } from "@hyperframes/core/clip-facts";
6
+ export type { ClipFact } from "@hyperframes/core/clip-facts";
7
+
8
+ /** The store holds preview URLs; the agent edits project files, so drop the origin and preview prefix. */
9
+ function projectRelativeSrc(src: string): string {
10
+ return src.replace(/^https?:\/\/[^/]+/, "").replace(/^\/api\/projects\/[^/]+\/preview\//, "");
11
+ }
12
+
13
+ export function describeClip(element: TimelineElement): ClipFact {
14
+ return {
15
+ id: element.domId ?? element.id,
16
+ label: element.label ?? null,
17
+ kind: element.kind ?? element.tag.toLowerCase(),
18
+ start: element.start,
19
+ duration: element.duration,
20
+ end: element.start + element.duration,
21
+ trackIndex: element.authoredTrack ?? element.track,
22
+ src: element.src ? projectRelativeSrc(element.src) : null,
23
+ sourceFile: element.sourceFile ?? null,
24
+ volume: element.volume ?? null,
25
+ lanes: elementAutomationLanes(element).map((lane) => ({
26
+ target: lane.target,
27
+ points: lane.points.map(({ t, v }) => ({ t, v })),
28
+ })),
29
+ playbackRate: element.playbackRate === 1 ? null : (element.playbackRate ?? null),
30
+ audioGroup: element.audioGroup ?? null,
31
+ role: element.timelineRole ?? null,
32
+ };
33
+ }
34
+
35
+ export function describeClips(elements: readonly TimelineElement[]): ClipFact[] {
36
+ return elements.map(describeClip).sort(byStart);
37
+ }
38
+
39
+ const PROMPT_CLIP_CAP = 200;
40
+
41
+ /** The Ask-agent prompt's Timeline block; empty when the timeline has no clips. */
42
+ export function formatTimelineBlock(elements: readonly TimelineElement[]): string {
43
+ const clips = describeClips(elements);
44
+ if (clips.length === 0) return "";
45
+ const shown = clips.slice(0, PROMPT_CLIP_CAP).map(formatClipLine);
46
+ const more = clips.length - shown.length;
47
+ return [
48
+ "Timeline (composition seconds; lane points are seconds from the clip start):",
49
+ ...shown,
50
+ ...(more > 0 ? [`(${more} more clips not listed)`] : []),
51
+ ].join("\n");
52
+ }
@@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
3
3
  import {
4
4
  createTimelineElementFromManifestClip,
5
5
  parseTimelineFromDOM,
6
- createImplicitTimelineLayersFromDOM,
7
6
  mergeTimelineElementsPreservingDowngrades,
8
7
  } from "./timelineDOM";
9
8
  import { isTimelineIgnoredElement } from "./timelineElementHelpers";
@@ -302,19 +301,6 @@ describe("createTimelineElementFromManifestClip — source-scoped selector ident
302
301
  // implicit-layer fallback happily gave it a track. Draggable and trimmable, and
303
302
  // writing timing onto a bus means nothing.
304
303
  describe("<hf-audio-group> is not a timeline layer", () => {
305
- it("gets no implicit row of its own", () => {
306
- const doc = makeDoc(`
307
- <div data-composition-id="root">
308
- <audio id="voice-1" data-start="0" data-duration="6" data-audio-group="voiceover"></audio>
309
- <hf-audio-group id="voiceover" data-label="Voiceover"></hf-audio-group>
310
- </div>
311
- `);
312
-
313
- const implicit = createImplicitTimelineLayersFromDOM(doc, 12, []);
314
-
315
- expect(implicit.map((el) => el.domId)).not.toContain("voiceover");
316
- });
317
-
318
304
  it("is excluded by the shared ignore predicate", () => {
319
305
  const doc = makeDoc(`<hf-audio-group id="vo"></hf-audio-group><div id="panel"></div>`);
320
306
  expect(isTimelineIgnoredElement(doc.getElementById("vo") as Element)).toBe(true);
@@ -322,72 +308,6 @@ describe("<hf-audio-group> is not a timeline layer", () => {
322
308
  });
323
309
  });
324
310
 
325
- describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
326
- it("uses the runtime root paint scope for implicit siblings of manifest clips", () => {
327
- const doc = makeDoc(`
328
- <div data-composition-id="root">
329
- <div id="timed" data-start="0" data-duration="5"></div>
330
- <div id="implicit"></div>
331
- </div>
332
- `);
333
- const timedHost = doc.getElementById("timed");
334
- const timed = createTimelineElementFromManifestClip({
335
- clip: {
336
- id: "timed",
337
- label: "Timed",
338
- start: 0,
339
- duration: 5,
340
- track: 0,
341
- stackingContextId: "css:root",
342
- kind: "element",
343
- tagName: "div",
344
- compositionId: null,
345
- parentCompositionId: null,
346
- compositionSrc: null,
347
- assetUrl: null,
348
- },
349
- fallbackIndex: 0,
350
- doc,
351
- hostEl: timedHost,
352
- });
353
- const implicit = createImplicitTimelineLayersFromDOM(doc, 5, [timed])[0];
354
-
355
- expect(implicit?.stackingContextId).toBe("css:root");
356
- expect(implicit?.stackingContextId).toBe(timed.stackingContextId);
357
- });
358
-
359
- it("harvests hfId from an implicit layer child that has data-hf-id", () => {
360
- const doc = makeDoc(`
361
- <div data-composition-id="root">
362
- <div id="layer" class="clip" data-hf-id="hf-xyz789"></div>
363
- </div>
364
- `);
365
-
366
- const layers = createImplicitTimelineLayersFromDOM(doc, 10);
367
- const layer = layers.find((el) => el.domId === "layer");
368
-
369
- expect(layer).toBeDefined();
370
- expect(layer?.hfId).toBe("hf-xyz789");
371
- });
372
-
373
- it("ignores runtime-owned color grading canvases as implicit layers", () => {
374
- const doc = makeDoc(`
375
- <div data-composition-id="root" data-duration="5">
376
- <img id="photo" class="clip" data-start="0" data-duration="5" />
377
- <canvas
378
- class="__hf_color_grading_canvas__"
379
- data-hf-color-grading-canvas="true"
380
- data-hyperframes-ignore
381
- ></canvas>
382
- </div>
383
- `);
384
-
385
- const layers = createImplicitTimelineLayersFromDOM(doc, 5);
386
-
387
- expect(layers).toEqual([]);
388
- });
389
- });
390
-
391
311
  describe("mergeTimelineElementsPreservingDowngrades — genuine removal vs transient downgrade", () => {
392
312
  it("drops a removed TOP-LEVEL element (undo of a split) instead of ghosting it", () => {
393
313
  const current = [el("a"), el("a-split")]; // post-split store: original + clone
@@ -17,8 +17,6 @@ import {
17
17
  resolveMediaElement,
18
18
  applyMediaMetadataFromElement,
19
19
  getTimelineElementDisplayLabel,
20
- getImplicitTimelineLayerLabel,
21
- isImplicitTimelineLayerCandidate,
22
20
  getTimelineElementSelector,
23
21
  getTimelineElementSourceFile,
24
22
  getTimelineElementSelectorIndex,
@@ -203,63 +201,6 @@ export function createTimelineElementFromManifestClip(params: {
203
201
  return entry;
204
202
  }
205
203
 
206
- export function createImplicitTimelineLayersFromDOM(
207
- doc: Document,
208
- rootDuration: number,
209
- existingElements: readonly TimelineElement[] = [],
210
- ): TimelineElement[] {
211
- if (!Number.isFinite(rootDuration) || rootDuration <= 0) return [];
212
- const rootComp = doc.querySelector("[data-composition-id]");
213
- if (!rootComp) return [];
214
-
215
- const existingKeys = new Set(existingElements.map(getTimelineElementIdentity));
216
- const maxTrack = existingElements.reduce(
217
- (max, element) => Math.max(max, Number.isFinite(element.track) ? element.track : 0),
218
- -1,
219
- );
220
- const layers: TimelineElement[] = [];
221
-
222
- for (const child of Array.from(rootComp.children)) {
223
- if (!isImplicitTimelineLayerCandidate(rootComp, child)) continue;
224
-
225
- const selector = getTimelineElementSelector(child);
226
- if (!selector) continue;
227
- const selectorIndex = getTimelineElementSelectorIndex(doc, child, selector);
228
- const sourceFile = getTimelineElementSourceFile(child);
229
- const label = getImplicitTimelineLayerLabel(child);
230
- const identity = buildTimelineElementIdentity({
231
- preferredId: child.id || null,
232
- label,
233
- fallbackIndex: existingElements.length + layers.length,
234
- domId: child.id || undefined,
235
- selector,
236
- selectorIndex,
237
- sourceFile,
238
- });
239
- if (existingKeys.has(identity.key) || existingKeys.has(identity.id)) continue;
240
-
241
- layers.push({
242
- domId: child.id || undefined,
243
- hfId: child.getAttribute("data-hf-id") || undefined,
244
- zIndex: readTimelineElementZIndex(child),
245
- duration: rootDuration,
246
- id: identity.id,
247
- key: identity.key,
248
- label,
249
- selector,
250
- selectorIndex,
251
- sourceFile,
252
- stackingContextId: resolveCssStackingContextId(child),
253
- start: 0,
254
- tag: child.tagName.toLowerCase(),
255
- timingSource: "implicit",
256
- track: maxTrack + 1 + layers.length,
257
- });
258
- }
259
-
260
- return layers;
261
- }
262
-
263
204
  /**
264
205
  * Parse [data-start] elements from a Document into TimelineElement[].
265
206
  * Shared helper — used by onIframeLoad fallback, handleMessage, and enrichMissingCompositions.
@@ -330,7 +271,6 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
330
271
  selectorIndex,
331
272
  sourceFile,
332
273
  stackingContextId: resolveCssStackingContextId(el),
333
- timingSource: "authored",
334
274
  zIndex: readTimelineElementZIndex(el),
335
275
  };
336
276
 
@@ -399,7 +339,7 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
399
339
  els.push(entry);
400
340
  });
401
341
 
402
- return [...els, ...createImplicitTimelineLayersFromDOM(doc, rootDuration, els)];
342
+ return els;
403
343
  }
404
344
 
405
345
  // ---------------------------------------------------------------------------
@@ -2,7 +2,7 @@
2
2
  * Low-level helpers for building and identifying TimelineElement objects.
3
3
  *
4
4
  * Covers: duration reading, media-element metadata extraction, selector/key/
5
- * identity builders, DOM node lookup, and implicit layer detection. These are
5
+ * identity builders, and DOM node lookup. These are
6
6
  * intentionally dependency-free (no store, no hooks) so they can be used in
7
7
  * both the React hook and test environments.
8
8
  */
@@ -82,13 +82,7 @@ function normalizePlaybackRate(raw: number): number {
82
82
  }
83
83
 
84
84
  export function isTimelineIgnoredElement(el: Element): boolean {
85
- // An `<hf-audio-group>` is a mixer bus, not a clip: it carries the group's
86
- // label, fader, mute and FX chain, has no timing of its own, and is drawn as
87
- // a GROUP ROW by the group derivation. Left in, the implicit-layer fallback
88
- // also gave it an ordinary full-duration track — so a grouped composition
89
- // showed "Voiceover • 0.0s – 12.0s" as a phantom clip directly above the real
90
- // group header. Harmless-looking, but that row is draggable and trimmable,
91
- // and writing timing onto the bus is meaningless.
85
+ // An `<hf-audio-group>` is a mixer bus with no timing of its own, drawn as a group row, never a clip.
92
86
  if (el.tagName.toLowerCase() === HF_AUDIO_GROUP_TAG) return true;
93
87
  return Boolean(
94
88
  el.closest(
@@ -223,37 +217,6 @@ export function getTimelineElementDisplayLabel(input: {
223
217
  return tag ? `${tag} clip` : "Timeline clip";
224
218
  }
225
219
 
226
- const IMPLICIT_TIMELINE_LAYER_SKIP_TAGS = new Set([
227
- "base",
228
- "link",
229
- "meta",
230
- "noscript",
231
- "script",
232
- "style",
233
- "template",
234
- ]);
235
-
236
- function humanizeTimelineIdentifier(value: string): string {
237
- return value
238
- .trim()
239
- .replace(/[_-]+/g, " ")
240
- .replace(/\s+/g, " ")
241
- .replace(/\b\w/g, (match) => match.toUpperCase());
242
- }
243
-
244
- export function getImplicitTimelineLayerLabel(el: HTMLElement): string {
245
- const explicitLabel =
246
- el.getAttribute("data-timeline-label") ??
247
- el.getAttribute("data-label") ??
248
- el.getAttribute("aria-label");
249
- if (explicitLabel?.trim()) return explicitLabel.trim();
250
- if (el.id.trim()) return humanizeTimelineIdentifier(el.id);
251
- const classes = el.className.split(/\s+/).filter(Boolean);
252
- const className = classes.find((value) => value !== "clip") ?? classes[0];
253
- if (className) return humanizeTimelineIdentifier(className);
254
- return getTimelineElementDisplayLabel({ tag: el.tagName });
255
- }
256
-
257
220
  // ---------------------------------------------------------------------------
258
221
  // Selector / identity / key builders
259
222
  // ---------------------------------------------------------------------------
@@ -475,17 +438,3 @@ export function createTimelineDomNodeResolver(doc: Document) {
475
438
  return node;
476
439
  };
477
440
  }
478
-
479
- // ---------------------------------------------------------------------------
480
- // Implicit layer detection
481
- // ---------------------------------------------------------------------------
482
-
483
- export function isImplicitTimelineLayerCandidate(root: Element, el: Element): el is HTMLElement {
484
- if (!isHtmlElement(el)) return false;
485
- if (isTimelineIgnoredElement(el)) return false;
486
- if (el.parentElement !== root) return false;
487
- const tagName = el.tagName.toLowerCase();
488
- if (IMPLICIT_TIMELINE_LAYER_SKIP_TAGS.has(tagName)) return false;
489
- if (el.hasAttribute("data-start") || el.hasAttribute("data-track-index")) return false;
490
- return Boolean(getTimelineElementSelector(el));
491
- }
@@ -200,6 +200,8 @@ interface PlayerState extends PlayerStoreSlices {
200
200
  clipManifest: ClipManifestClip[] | null;
201
201
  setClipManifest: (clips: ClipManifestClip[] | null) => void;
202
202
  clipParentMap: Map<string, string>;
203
+ topLevelIds: ReadonlySet<string> | null;
204
+ setTopLevelIds: (ids: ReadonlySet<string> | null) => void;
203
205
  setClipParentMap: (map: Map<string, string>) => void;
204
206
  /**
205
207
  * Sub-composition DOM descendants (groups + their children) that have no
@@ -288,6 +290,7 @@ export function createTimelineResetState() {
288
290
  beatPersist: null,
289
291
  clipManifest: null,
290
292
  clipParentMap: new Map<string, string>(),
293
+ topLevelIds: null,
291
294
  domClipChildren: [],
292
295
  subCompositionHostState: new Map<string, SubCompositionHostState>(),
293
296
  };
@@ -434,6 +437,8 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
434
437
  setClipManifest: (clips) => set({ clipManifest: clips }),
435
438
  clipParentMap: new Map(),
436
439
  setClipParentMap: (map) => set({ clipParentMap: map }),
440
+ topLevelIds: null,
441
+ setTopLevelIds: (ids) => set({ topLevelIds: ids }),
437
442
  domClipChildren: [],
438
443
  setDomClipChildren: (children) => set({ domClipChildren: children }),
439
444
  subCompositionHostState: new Map(),
@@ -59,8 +59,6 @@ export interface TimelineElement {
59
59
  automation?: string;
60
60
  /** Path from data-composition-src — identifies sub-composition elements */
61
61
  compositionSrc?: string;
62
- /** Whether this row came from authored clip timing or Studio's full-duration layer fallback. */
63
- timingSource?: "authored" | "implicit";
64
62
  /** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
65
63
  timelineLocked?: boolean;
66
64
  /** Set by data-hidden on the host element — hides the clip in preview and render. */
@@ -79,7 +77,7 @@ export interface TimelineElement {
79
77
  audioGroupFxChain?: string;
80
78
  audioGroupAutomation?: string;
81
79
  /**
82
- * Set by useExpandedTimelineElements on an inline-expanded sub-composition
80
+ * Set by useTimelineRowElements on an inline-expanded sub-composition
83
81
  * child: the absolute master-timeline start of the sub-comp host the child
84
82
  * lives in. Presence marks the element as expanded; edits subtract it to get
85
83
  * the child's local (sourceFile-relative) time. Works at any nesting depth.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Browser stub for the Node-only `canvas` package.
3
+ *
4
+ * linkedom's `HTMLCanvasElement` constructor calls `createCanvas(300, 150)`
5
+ * (300x150 being the HTML canvas default size). linkedom guards that import in
6
+ * a try/catch: the try requires the native `canvas` package, and the catch
7
+ * falls back to linkedom's own bundled canvas shim.
8
+ *
9
+ * That guard only works with a *runtime* require. Vite resolves the specifier
10
+ * statically, so the bundled form is a plain assignment that cannot throw, the
11
+ * catch never runs, and `createCanvas` lands `undefined`. Parsing any `<canvas>`
12
+ * tag then throws inside `openComposition`, Studio loses its SDK session for the
13
+ * whole composition, and every edit silently falls back to the server path — the
14
+ * resolver shadow does not run either, so it was invisible until
15
+ * `studio:sdk_session_unavailable` shipped in v0.8.47.
16
+ *
17
+ * Production telemetry over the 72h after the flip: 1065 of 1544 session-open
18
+ * failures were this crash, across a large share of 197 users. Canvas is common
19
+ * in compositions (Three.js, shaders, particle effects, charts).
20
+ *
21
+ * Aliasing `canvas` to this file makes the browser build resolve to a
22
+ * measurement-only canvas, which is all linkedom's static parse needs — nothing
23
+ * in the SDK rasterizes through it. It mirrors linkedom's own
24
+ * `commonjs/canvas-shim.cjs`, reimplemented here because linkedom's `exports`
25
+ * map does not expose that file.
26
+ */
27
+
28
+ class BrowserCanvasStub {
29
+ constructor(width, height) {
30
+ this.width = width;
31
+ this.height = height;
32
+ }
33
+ /** linkedom only ever needs the element to exist; nothing draws through it. */
34
+ getContext() {
35
+ return null;
36
+ }
37
+ toDataURL() {
38
+ return "";
39
+ }
40
+ }
41
+
42
+ export function createCanvas(width, height) {
43
+ return new BrowserCanvasStub(width, height);
44
+ }
45
+
46
+ // linkedom consumes this as a DEFAULT import — `import Canvas from
47
+ // '../../commonjs/canvas.cjs'` then `const {createCanvas} = Canvas` — so the
48
+ // default export is the load-bearing one; the named export above is interop
49
+ // belt-and-braces.
50
+ export default { createCanvas };
@@ -0,0 +1,20 @@
1
+ import type { IconWeight } from "@phosphor-icons/react";
2
+
3
+ const WEIGHTS = ["thin", "light", "regular", "bold", "fill", "duotone"] as const;
4
+
5
+ function asWeight(value: string): IconWeight | null {
6
+ return (WEIGHTS as readonly string[]).includes(value) ? (value as IconWeight) : null;
7
+ }
8
+
9
+ /** Studio's Phosphor defaults, read off `theme.css` so size and weight have one
10
+ * owner instead of a CSS variable and a JS constant that drift. A missing or
11
+ * unrecognized value falls back to Phosphor's own default. */
12
+ export function readIconTokens(root: Element = document.documentElement): {
13
+ size: string;
14
+ weight: IconWeight;
15
+ } {
16
+ const styles = getComputedStyle(root);
17
+ const size = styles.getPropertyValue("--icon-size").trim();
18
+ const weight = asWeight(styles.getPropertyValue("--icon-weight").trim());
19
+ return { size: size || "1em", weight: weight ?? "regular" };
20
+ }
@@ -0,0 +1,6 @@
1
+ /* Studio's own app clears Tailwind's stock palette so only Studio tokens are
2
+ * reachable from markup. Kept out of theme.css so a host importing the tokens
3
+ * keeps its own colours. Must be imported before theme.css. */
4
+ @theme static {
5
+ --color-*: initial;
6
+ }
@@ -1,6 +1,19 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
1
+ @import "tailwindcss" source(none);
2
+ @import "./palette-reset.css";
3
+ @import "./theme.css";
4
+
5
+ /* Explicit sources: Studio's own TypeScript and its shell HTML. Automatic
6
+ * detection would walk up to the repo root and scan `data/projects`, gitignored user content. */
7
+ @source "../**/*.{ts,tsx}";
8
+ @source "../../index.html";
9
+
10
+ /* Tailwind v4 preflight no longer gives enabled buttons a pointer cursor. */
11
+ @layer base {
12
+ button:not(:disabled),
13
+ [role="button"]:not(:disabled) {
14
+ cursor: pointer;
15
+ }
16
+ }
4
17
 
5
18
  /*
6
19
  * Studio is a dark-only UI — pin the user-agent color scheme to dark so that
@@ -14,9 +27,9 @@
14
27
  body {
15
28
  margin: 0;
16
29
  padding: 0;
17
- background: #0a0a0a;
18
- color: #e5e5e5;
19
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
30
+ background: var(--color-bg-0);
31
+ color: var(--color-text-body);
32
+ font-family: var(--font-sans);
20
33
  overflow: hidden;
21
34
  }
22
35
 
@@ -290,7 +303,7 @@ body {
290
303
  left: 12px;
291
304
  overflow: hidden;
292
305
  color: rgba(255, 255, 255, 0.34);
293
- font-family: "SF Mono", "Fira Code", monospace;
306
+ font-family: var(--font-mono);
294
307
  font-size: 9px;
295
308
  font-variant-numeric: tabular-nums;
296
309
  line-height: 1;
@@ -328,7 +341,7 @@ body {
328
341
  }
329
342
 
330
343
  .cm-editor .cm-scroller {
331
- font-family: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
344
+ font-family: var(--font-mono);
332
345
  }
333
346
 
334
347
  .cm-editor.cm-focused {
@@ -385,12 +398,7 @@ body {
385
398
  }
386
399
 
387
400
  .hf-loader-title {
388
- font-family:
389
- Inter,
390
- -apple-system,
391
- BlinkMacSystemFont,
392
- "Segoe UI",
393
- sans-serif;
401
+ font-family: var(--font-sans);
394
402
  font-size: 1rem;
395
403
  font-weight: 600;
396
404
  letter-spacing: 0;
@@ -406,12 +414,7 @@ body {
406
414
  min-height: 2.5rem;
407
415
  overflow: hidden;
408
416
  color: var(--hf-text-secondary, rgba(244, 244, 245, 0.68));
409
- font-family:
410
- Inter,
411
- -apple-system,
412
- BlinkMacSystemFont,
413
- "Segoe UI",
414
- sans-serif;
417
+ font-family: var(--font-sans);
415
418
  font-size: 0.82rem;
416
419
  line-height: 1.6;
417
420
  }
@@ -423,7 +426,7 @@ body {
423
426
  text-overflow: ellipsis;
424
427
  white-space: nowrap;
425
428
  color: var(--hf-text-tertiary, rgba(244, 244, 245, 0.46));
426
- font-family: "IBM Plex Mono", "SF Mono", "Fira Code", monospace;
429
+ font-family: var(--font-mono);
427
430
  font-size: 0.75rem;
428
431
  letter-spacing: 0;
429
432
  font-variant-numeric: tabular-nums;
@@ -1,3 +1,6 @@
1
+ /** @deprecated Superseded by `src/styles/theme.css` (published as
2
+ * `@hyperframes/studio/theme.css`); kept one more major for Tailwind v3
3
+ * consumers. Values are byte-identical; theme.test.ts fails if they drift. */
1
4
  const studioPreset = {
2
5
  theme: {
3
6
  extend: {