@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
@@ -1,460 +0,0 @@
1
- import { useMemo } from "react";
2
- import {
3
- usePlayerStore,
4
- type TimelineElement,
5
- type DomClipChild,
6
- type SubCompositionHostState,
7
- } from "../store/playerStore";
8
- import type { ClipManifestClip } from "../lib/playbackTypes";
9
- import { createTimelineElementFromManifestClip } from "../lib/timelineDOM";
10
- import { buildTimelineElementKey, splitTimelineElementKey } from "../lib/timelineElementHelpers";
11
-
12
- function findTopLevelAncestor(id: string, parentMap: Map<string, string>): string | null {
13
- let current = parentMap.get(id);
14
- if (!current) return null;
15
- const visited = new Set<string>();
16
- visited.add(id);
17
- while (parentMap.has(current)) {
18
- if (visited.has(current)) return current;
19
- visited.add(current);
20
- const parent = parentMap.get(current);
21
- if (!parent) return current;
22
- current = parent;
23
- }
24
- return current;
25
- }
26
-
27
- function resolveRawId(
28
- selectedId: string | null,
29
- manifest: ClipManifestClip[],
30
- parentMap: Map<string, string>,
31
- ): string | null {
32
- if (!selectedId) return null;
33
- const rawId = splitTimelineElementKey(selectedId).domId;
34
- if (parentMap.has(rawId)) return rawId;
35
- if (parentMap.has(selectedId)) return selectedId;
36
- const clip = manifest.find((c) => c.label === selectedId || c.label === rawId);
37
- if (clip?.id && parentMap.has(clip.id)) return clip.id;
38
- return null;
39
- }
40
-
41
- interface TimelineExpansionRawIdInput {
42
- selectedElementId: string | null;
43
- currentTime: number;
44
- manifest: ClipManifestClip[];
45
- parentMap: Map<string, string>;
46
- }
47
-
48
- function clipContainsTime(clip: ClipManifestClip, time: number): boolean {
49
- return Number.isFinite(time) && time >= clip.start && time < clip.start + clip.duration;
50
- }
51
-
52
- /** Half-open containment plus the closing boundary — see the fallback below. */
53
- function clipTouchesTime(clip: ClipManifestClip, time: number): boolean {
54
- return Number.isFinite(time) && time >= clip.start && time <= clip.start + clip.duration;
55
- }
56
-
57
- function getActiveParentDepth(id: string, parentMap: Map<string, string>, activeIds: Set<string>) {
58
- let depth = 0;
59
- let parent = parentMap.get(id);
60
- const visited = new Set<string>();
61
- visited.add(id);
62
- while (parent) {
63
- if (visited.has(parent)) return depth;
64
- visited.add(parent);
65
- if (activeIds.has(parent)) depth += 1;
66
- parent = parentMap.get(parent);
67
- }
68
- return depth;
69
- }
70
-
71
- function findActiveExpandableCompositionId(
72
- currentTime: number,
73
- manifest: ClipManifestClip[],
74
- parentMap: Map<string, string>,
75
- ): string | null {
76
- const parentIds = new Set(parentMap.values());
77
- const collect = (matches: (clip: ClipManifestClip, time: number) => boolean) => {
78
- const ids = new Set<string>();
79
- for (const clip of manifest) {
80
- if (clip.id && parentIds.has(clip.id) && matches(clip, currentTime)) ids.add(clip.id);
81
- }
82
- return ids;
83
- };
84
- // Clip windows are half-open, so a playhead parked exactly on a clip's end is
85
- // inside nothing — at the end of the timeline that collapsed every expanded
86
- // sub-composition row and its keyframe lanes. Only when the strict pass finds
87
- // nothing do we accept the closing boundary, so a playhead landing on the seam
88
- // between two adjacent clips still expands the one that is starting.
89
- const strict = collect(clipContainsTime);
90
- const activeIds = strict.size > 0 ? strict : collect(clipTouchesTime);
91
- let bestId: string | null = null;
92
- let bestDepth = -1;
93
- for (const id of activeIds) {
94
- const depth = getActiveParentDepth(id, parentMap, activeIds);
95
- if (depth <= bestDepth) continue;
96
- bestId = id;
97
- bestDepth = depth;
98
- }
99
- return bestId;
100
- }
101
-
102
- export function resolveTimelineExpansionRawId({
103
- selectedElementId,
104
- currentTime,
105
- manifest,
106
- parentMap,
107
- }: TimelineExpansionRawIdInput): string | null {
108
- const selectedRawId = resolveRawId(selectedElementId, manifest, parentMap);
109
- if (selectedRawId) return selectedRawId;
110
- return findActiveExpandableCompositionId(currentTime, manifest, parentMap);
111
- }
112
-
113
- function filterToTopLevel(
114
- elements: TimelineElement[],
115
- parentMap: Map<string, string>,
116
- ): TimelineElement[] {
117
- if (parentMap.size === 0) return elements;
118
- return elements.filter((el) => !parentMap.has(el.domId ?? el.id));
119
- }
120
-
121
- function clampChildToParent(
122
- child: ClipManifestClip,
123
- parentStart: number,
124
- parentEnd: number,
125
- ): { start: number; duration: number } | null {
126
- const childEnd = child.start + child.duration;
127
- if (child.start >= parentEnd || childEnd <= parentStart) return null;
128
- const clampedStart = Math.max(child.start, parentStart);
129
- const clampedDuration = Math.min(childEnd, parentEnd) - clampedStart;
130
- return clampedDuration > 0 ? { start: clampedStart, duration: clampedDuration } : null;
131
- }
132
-
133
- interface DisplayBounds {
134
- start: number;
135
- end: number;
136
- track: number;
137
- }
138
-
139
- /**
140
- * Audio-group membership for an expanded child, from whichever source has it.
141
- *
142
- * The flat store twin when there is one; otherwise the `DomClipChild` record,
143
- * which carried it off the live element during the DOM walk. That fallback is
144
- * the ONLY source for a sub-composition that declares both a group and its
145
- * members: those members never enter the flat store, so "inherit from the flat
146
- * twin" silently produced no membership and therefore no group row — for
147
- * exactly the case group support was extended to cover.
148
- */
149
- function childGroupState(
150
- flat: TimelineElement | undefined,
151
- domChild: DomClipChild | undefined,
152
- ): Partial<TimelineElement> {
153
- const source = flat?.audioGroup ? flat : domChild?.audioGroup ? domChild : null;
154
- if (!source) return {};
155
- return {
156
- audioGroup: source.audioGroup,
157
- audioGroupLabel: source.audioGroupLabel,
158
- audioGroupVolume: source.audioGroupVolume,
159
- audioGroupHidden: source.audioGroupHidden,
160
- audioGroupFxChain: source.audioGroupFxChain,
161
- audioGroupAutomation: source.audioGroupAutomation,
162
- };
163
- }
164
-
165
- /**
166
- * State that lives on the live host element, not in the clip manifest:
167
- * `data-hidden`, `data-timeline-locked`, `data-timeline-role`, `data-fx-chain`,
168
- * `data-automation`. A child row is built from a manifest clip with no hostEl to
169
- * read, so createTimelineElementFromManifestClip cannot see any of it.
170
- *
171
- * `live` is the reading taken off the element itself and is authoritative. For a
172
- * child of a REAL sub-composition it is also the only source: such a clip is
173
- * filtered out of the manifest before the flat store is built, so no twin exists
174
- * to inherit from. The flat twin still covers the phantom-wrapper case, where
175
- * the child does keep a store entry of its own.
176
- *
177
- * Without a reading of the element, the eye on an expanded child always reported
178
- * the row visible, so clicking it wrote data-hidden again instead of removing
179
- * it, and a hidden child could never be shown again (not even after a reload,
180
- * since the attribute is in the source). Missing fxChain and automation, an
181
- * audio child inside a sub-composition reserved no automation height and drew no
182
- * lanes, while the property panel, which reads the live DOM selection rather
183
- * than this row, still showed the chain and its toggles.
184
- */
185
- function hostElementState(
186
- flat: TimelineElement | undefined,
187
- live: SubCompositionHostState | undefined,
188
- ): Partial<TimelineElement> {
189
- if (!flat) return { ...live };
190
- const { hidden, timelineLocked, timelineRole, fxChain, automation } = flat;
191
- // `live` last: it is the reading off the element, so it wins wherever it has one.
192
- return { hidden, timelineLocked, timelineRole, fxChain, automation, ...live };
193
- }
194
-
195
- // `display` bounds come from the top-level scene clip (where the expanded row is
196
- // drawn). `editBasis` comes from the child's immediate sub-comp host: its absolute
197
- // start anchors local-time edits and its compositionSrc is the file edits write to.
198
- // They differ only for sub-comp-inside-sub-comp nesting.
199
- function buildChildElements(
200
- siblings: ClipManifestClip[],
201
- display: DisplayBounds,
202
- editBasis: { start: number; sourceFile: string | undefined },
203
- expandedHostKey: string,
204
- elements: readonly TimelineElement[],
205
- domChildrenById: ReadonlyMap<string, DomClipChild>,
206
- hostStateById: ReadonlyMap<string, SubCompositionHostState>,
207
- ): TimelineElement[] {
208
- const result: TimelineElement[] = [];
209
- for (const child of siblings) {
210
- const clamped = clampChildToParent(child, display.start, display.end);
211
- if (!clamped) continue;
212
- const base = createTimelineElementFromManifestClip({
213
- clip: child,
214
- fallbackIndex: result.length,
215
- });
216
- const domId = child.id ?? undefined;
217
- const selector = child.id ? `#${child.id}` : undefined;
218
- // `base.key` was built without a hostEl, so it fell back to the colon form
219
- // (`index.html:<id>:<idx>`) even though we set domId below. Recompute it from
220
- // the same inputs the store uses (`<sourceFile>#<domId>`) so an expanded
221
- // child shares one identity with its flat store element — otherwise selecting
222
- // it sets `selectedElementId` to the store's hash key while the rendered row
223
- // is keyed by the colon form, and `isSelected` never matches (no highlight).
224
- const key = buildTimelineElementKey({
225
- id: base.id,
226
- fallbackIndex: result.length,
227
- domId,
228
- selector,
229
- selectorIndex: base.selectorIndex,
230
- sourceFile: editBasis.sourceFile,
231
- });
232
- result.push({
233
- ...base,
234
- ...hostElementState(
235
- elements.find((element) => element.key === key),
236
- domId ? hostStateById.get(domId) : undefined,
237
- ),
238
- ...childGroupState(
239
- elements.find((element) => element.key === key),
240
- domId ? domChildrenById.get(domId) : undefined,
241
- ),
242
- key,
243
- start: clamped.start,
244
- duration: clamped.duration,
245
- // `track` becomes a synthetic display row under the expanded host, but the
246
- // factory-set `authoredTrack` (the child's data-track-index in ITS OWN
247
- // file's coordinate space) and the runtime-computed `stackingContextId`
248
- // must survive verbatim — lane persists and z-sync read them, they are
249
- // never reconstructed from display lanes.
250
- //
251
- // COLLISION-FREE synthetic rows: the old `display.track + index` scheme
252
- // could equal a REAL clip's integer lane (host on track 0 with two
253
- // children puts child #2 on track 1 — where an unrelated top-level clip
254
- // may live). Lane grouping merges purely by track number, so that
255
- // collision fused clips from DIFFERENT source files into one display
256
- // lane, and lane-scoped actions (gap close) then batch-persisted foreign
257
- // clips. Fractions strictly between the host's lane and the next integer
258
- // can never equal a normalized (integer) lane, while still rendering the
259
- // children as their own ordered rows directly under the host.
260
- //
261
- // Confined to the LOWER half of that gap, because a GROUP row anchors at
262
- // exactly `firstMemberTrack - 0.5` (`useTimelineTrackDerivations`) — and
263
- // the old `k / (n + 2)` hit 0.5 dead on for a host with two children
264
- // (2/4), producing a duplicate row key and a duplicated group header. This
265
- // scheme's maximum is `0.5 * n / (n + 1)`, strictly under 0.5 for every n.
266
- track: display.track + (0.5 * (result.length + 1)) / (siblings.length + 1),
267
- authoredTrack: base.authoredTrack,
268
- stackingContextId: base.stackingContextId,
269
- expandedParentStart: editBasis.start,
270
- expandedHostKey,
271
- domId,
272
- selector,
273
- sourceFile: editBasis.sourceFile,
274
- timingSource: "authored",
275
- });
276
- }
277
- return result;
278
- }
279
-
280
- // Sub-comp DOM children (groups/pills) aren't manifest clips and have no timing
281
- // of their own — they're "always on" within their sub-comp host, so synthesize
282
- // clips spanning the host's full bounds. The host element supplies start/duration
283
- // and the composition file edits write to.
284
- function domSiblingClips(
285
- domClipChildren: DomClipChild[],
286
- siblingParentId: string,
287
- host: {
288
- id: string | null;
289
- start: number;
290
- duration: number;
291
- track: number;
292
- compositionSrc?: string | null;
293
- },
294
- ): ClipManifestClip[] {
295
- return domClipChildren
296
- .filter((c) => c.parentId === siblingParentId)
297
- .map(
298
- (c): ClipManifestClip => ({
299
- id: c.id,
300
- label: c.label,
301
- start: host.start,
302
- duration: host.duration,
303
- track: host.track,
304
- kind: "element",
305
- tagName: null,
306
- compositionId: null,
307
- parentCompositionId: host.id ?? null,
308
- compositionSrc: host.compositionSrc ?? null,
309
- assetUrl: null,
310
- stackingContextId: c.stackingContextId,
311
- }),
312
- );
313
- }
314
-
315
- // Exported for tests.
316
- export function buildExpandedElements(
317
- elements: TimelineElement[],
318
- manifest: ClipManifestClip[],
319
- parentMap: Map<string, string>,
320
- topLevelId: string,
321
- siblingParentId: string,
322
- domClipChildren: DomClipChild[] = [],
323
- subCompositionHostState: ReadonlyMap<string, SubCompositionHostState> = new Map(),
324
- ): TimelineElement[] {
325
- const topLevelElement = elements.find((el) => el.id === topLevelId || el.domId === topLevelId);
326
- if (!topLevelElement) return filterToTopLevel(elements, parentMap);
327
-
328
- // The sub-comp host the children actually live in: top-level host for 1-level
329
- // nesting, a nested host for deeper nesting. Its start/file anchor edits.
330
- const parentHost = manifest.find((c) => c.id === siblingParentId);
331
-
332
- // Prefer real manifest children; fall back to DOM-only sub-comp children
333
- // (groups/pills) that have no data-start and thus never enter the manifest.
334
- // Those are synthesized against the host they actually live in, not the
335
- // top-level element, or every child row reads the whole top-level window.
336
- const siblings = (() => {
337
- const fromManifest = manifest.filter(
338
- (c) => c.id != null && parentMap.get(c.id) === siblingParentId,
339
- );
340
- if (fromManifest.length > 0) return fromManifest;
341
- return domSiblingClips(domClipChildren, siblingParentId, parentHost ?? topLevelElement);
342
- })();
343
- if (siblings.length === 0) return filterToTopLevel(elements, parentMap);
344
-
345
- const editBasis = {
346
- start: parentHost?.start ?? topLevelElement.start,
347
- sourceFile: parentHost?.compositionSrc ?? topLevelElement.compositionSrc ?? undefined,
348
- };
349
-
350
- const parentKey = topLevelElement.key ?? topLevelElement.id;
351
- const domChildrenById = new Map(domClipChildren.map((child) => [child.id, child]));
352
- const expanded = buildChildElements(
353
- siblings,
354
- {
355
- start: topLevelElement.start,
356
- end: topLevelElement.start + topLevelElement.duration,
357
- track: topLevelElement.track,
358
- },
359
- editBasis,
360
- parentKey,
361
- elements,
362
- domChildrenById,
363
- subCompositionHostState,
364
- );
365
- if (expanded.length === 0) return filterToTopLevel(elements, parentMap);
366
-
367
- // Every host between the drilled one and the top level owns a row, so the
368
- // drill has to spare all of them, not just the top. A middle host is still a
369
- // host: dropping its row drops its keyframe lane with it.
370
- const drillPath = new Set<string>();
371
- for (let cursor: string | undefined = siblingParentId; cursor; ) {
372
- if (drillPath.has(cursor)) break;
373
- drillPath.add(cursor);
374
- if (cursor === topLevelId) break;
375
- cursor = parentMap.get(cursor);
376
- }
377
- // Children hang under the DEEPEST host on that path, so anchor them there
378
- // when it has a row of its own and fall back to the top-level row when it
379
- // does not (a host that lives only in the manifest never had one).
380
- const anchorsChildren = (el: TimelineElement): boolean =>
381
- drillPath.has(siblingParentId) && elements.some((e) => (e.domId ?? e.id) === siblingParentId)
382
- ? (el.domId ?? el.id) === siblingParentId
383
- : (el.key ?? el.id) === parentKey;
384
-
385
- // ADDITIVE drill-in: the host row stays and its children are appended under
386
- // it. Expansion is also triggered by the playhead alone (active auto-expand),
387
- // so substituting the host row made it vanish on an ordinary seek, and with
388
- // it the host's keyframe lane, since diamonds render per row from
389
- // `keyframeCache.get(elementKey)`. The synthetic fractional lanes above sit
390
- // strictly between the host's lane and the next integer, so the children have
391
- // their own rows without the host having to give up its own.
392
- return elements
393
- .filter(
394
- (el) =>
395
- (el.key ?? el.id) === parentKey ||
396
- drillPath.has(el.domId ?? el.id) ||
397
- !parentMap.has(el.domId ?? el.id),
398
- )
399
- .flatMap((el) => (anchorsChildren(el) ? [el, ...expanded] : [el]));
400
- }
401
-
402
- export function useExpandedTimelineElements(): TimelineElement[] {
403
- const elements = usePlayerStore((s) => s.elements);
404
- const clipManifest = usePlayerStore((s) => s.clipManifest);
405
- const clipParentMap = usePlayerStore((s) => s.clipParentMap);
406
- const domClipChildren = usePlayerStore((s) => s.domClipChildren);
407
- const subCompositionHostState = usePlayerStore((s) => s.subCompositionHostState);
408
- const selectedElementId = usePlayerStore((s) => s.selectedElementId);
409
- const currentTime = usePlayerStore((s) => s.currentTime);
410
-
411
- // Resolve which raw clip drives expansion from the store's committed playhead
412
- // time. The RAF loop keeps live time out of React and Zustand during playback,
413
- // so this target deliberately stays stable until the loop commits or a seek /
414
- // pause updates the store. The scan re-runs on scrub ticks, but its RESULT only
415
- // changes when the playhead crosses a composition boundary. Keying the expensive
416
- // build below on these ids (not raw currentTime) avoids re-allocating
417
- // expandedElements — and cascading TimelineClip re-renders — on every tick.
418
- const { rawId, selectedRawId } = useMemo(() => {
419
- if (!clipManifest || clipManifest.length === 0 || clipParentMap.size === 0) {
420
- return { rawId: null as string | null, selectedRawId: null as string | null };
421
- }
422
- return {
423
- rawId: resolveTimelineExpansionRawId({
424
- selectedElementId,
425
- currentTime,
426
- manifest: clipManifest,
427
- parentMap: clipParentMap,
428
- }),
429
- selectedRawId: resolveRawId(selectedElementId, clipManifest, clipParentMap),
430
- };
431
- }, [clipManifest, clipParentMap, selectedElementId, currentTime]);
432
-
433
- return useMemo(() => {
434
- if (!clipManifest || clipManifest.length === 0 || clipParentMap.size === 0) {
435
- return elements;
436
- }
437
- if (!rawId) return filterToTopLevel(elements, clipParentMap);
438
-
439
- const immediateParent = selectedRawId ? clipParentMap.get(rawId) : rawId;
440
- if (!immediateParent) return filterToTopLevel(elements, clipParentMap);
441
- const topLevel = findTopLevelAncestor(rawId, clipParentMap) ?? immediateParent;
442
- return buildExpandedElements(
443
- elements,
444
- clipManifest,
445
- clipParentMap,
446
- topLevel,
447
- immediateParent,
448
- domClipChildren,
449
- subCompositionHostState,
450
- );
451
- }, [
452
- elements,
453
- clipManifest,
454
- clipParentMap,
455
- domClipChildren,
456
- subCompositionHostState,
457
- rawId,
458
- selectedRawId,
459
- ]);
460
- }