@hyperframes/studio 0.7.57 → 0.7.59

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 (247) hide show
  1. package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
  2. package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
  3. package/dist/assets/index-CrkAdJkb.js +426 -0
  4. package/dist/assets/index-Dj5p8U_A.css +1 -0
  5. package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
  6. package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
  7. package/dist/chunk-OBAG3GWK.js.map +1 -0
  8. package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
  9. package/dist/index.d.ts +27 -7
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +18029 -11563
  12. package/dist/index.js.map +1 -1
  13. package/dist/styles/tailwind-preset.d.ts +5 -0
  14. package/dist/styles/tailwind-preset.js +8 -1
  15. package/dist/styles/tailwind-preset.js.map +1 -1
  16. package/package.json +7 -7
  17. package/src/App.tsx +6 -6
  18. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  19. package/src/components/StudioFeedbackBar.tsx +2 -2
  20. package/src/components/StudioRightPanel.tsx +29 -51
  21. package/src/components/editor/AnimationCard.test.tsx +134 -0
  22. package/src/components/editor/AnimationCard.tsx +19 -4
  23. package/src/components/editor/ArcPathControls.tsx +1 -0
  24. package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
  25. package/src/components/editor/CanvasContextMenu.tsx +71 -6
  26. package/src/components/editor/DomEditOverlay.tsx +5 -2
  27. package/src/components/editor/GestureRecordControl.tsx +7 -1
  28. package/src/components/editor/GsapAnimationSection.tsx +109 -14
  29. package/src/components/editor/InspectorHeaderActions.tsx +42 -4
  30. package/src/components/editor/LayersPanel.tsx +110 -99
  31. package/src/components/editor/PropertyPanel.test.tsx +948 -0
  32. package/src/components/editor/PropertyPanel.tsx +144 -148
  33. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  34. package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
  35. package/src/components/editor/PropertyPanelFlat.tsx +580 -0
  36. package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
  37. package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
  38. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
  39. package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
  40. package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
  41. package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
  42. package/src/components/editor/domEditingDom.ts +5 -6
  43. package/src/components/editor/domEditingElement.ts +15 -32
  44. package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
  45. package/src/components/editor/gsapLivePreview.ts +34 -0
  46. package/src/components/editor/layersPanelSort.ts +80 -0
  47. package/src/components/editor/manualEditingAvailability.test.ts +10 -0
  48. package/src/components/editor/manualEditingAvailability.ts +9 -0
  49. package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
  50. package/src/components/editor/propertyPanelColor.test.tsx +28 -0
  51. package/src/components/editor/propertyPanelColor.tsx +31 -1
  52. package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
  53. package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
  54. package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
  55. package/src/components/editor/propertyPanelFill.tsx +22 -5
  56. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
  57. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
  58. package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
  59. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
  60. package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
  61. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
  62. package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
  63. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
  64. package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
  65. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
  66. package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
  67. package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
  68. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
  69. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
  70. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
  71. package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
  72. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
  73. package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
  74. package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
  75. package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
  76. package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
  77. package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
  78. package/src/components/editor/propertyPanelFont.test.tsx +30 -0
  79. package/src/components/editor/propertyPanelFont.tsx +136 -95
  80. package/src/components/editor/propertyPanelHelpers.ts +73 -0
  81. package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
  82. package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
  83. package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
  84. package/src/components/editor/propertyPanelSections.test.tsx +161 -0
  85. package/src/components/editor/propertyPanelSections.tsx +154 -90
  86. package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
  87. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  88. package/src/components/editor/propertyPanelTypes.ts +14 -1
  89. package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
  90. package/src/components/editor/propertyPanelValueTier.ts +28 -0
  91. package/src/components/editor/useColorGradingController.test.ts +418 -0
  92. package/src/components/editor/useColorGradingController.ts +596 -0
  93. package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
  94. package/src/components/editor/useLayerRevealOverride.ts +389 -0
  95. package/src/components/nle/PreviewOverlays.tsx +27 -2
  96. package/src/components/nle/TimelinePane.test.ts +3 -0
  97. package/src/components/nle/TimelinePane.tsx +11 -1
  98. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
  99. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
  100. package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
  101. package/src/components/nle/zLaneGesture.test.ts +176 -0
  102. package/src/components/nle/zLaneGesture.ts +77 -0
  103. package/src/components/panels/VariablesPanel.tsx +4 -0
  104. package/src/components/sidebar/CompositionsTab.test.ts +28 -2
  105. package/src/components/sidebar/CompositionsTab.tsx +3 -1
  106. package/src/components/storyboard/FramePoster.tsx +24 -4
  107. package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
  108. package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
  109. package/src/components/storyboard/StoryboardGrid.tsx +23 -1
  110. package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
  111. package/src/components/storyboard/StoryboardView.tsx +9 -2
  112. package/src/components/storyboard/frameComments.test.ts +95 -0
  113. package/src/components/storyboard/frameComments.ts +115 -0
  114. package/src/components/storyboard/useFrameComments.ts +82 -0
  115. package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
  116. package/src/contexts/DesignPanelInputContext.tsx +48 -0
  117. package/src/contexts/DomEditContext.tsx +4 -0
  118. package/src/contexts/VariablePromoteContext.test.tsx +67 -0
  119. package/src/contexts/VariablePromoteContext.tsx +10 -5
  120. package/src/hooks/domEditCommitRunner.ts +11 -0
  121. package/src/hooks/domEditCommitTypes.ts +17 -1
  122. package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
  123. package/src/hooks/fetchStubTestUtils.ts +16 -0
  124. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  125. package/src/hooks/timelineEditingHelpers.test.ts +92 -1
  126. package/src/hooks/timelineEditingHelpers.ts +32 -15
  127. package/src/hooks/timelineMoveAdapter.ts +2 -1
  128. package/src/hooks/timelineTimingSync.test.ts +703 -33
  129. package/src/hooks/timelineTimingSync.ts +282 -97
  130. package/src/hooks/timelineTrackVisibility.test.ts +55 -1
  131. package/src/hooks/timelineTrackVisibility.ts +31 -13
  132. package/src/hooks/useAppHotkeys.ts +9 -0
  133. package/src/hooks/useClipboard.ts +19 -17
  134. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  135. package/src/hooks/useDomEditCommits.test.tsx +316 -38
  136. package/src/hooks/useDomEditCommits.ts +81 -162
  137. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  138. package/src/hooks/useDomEditSession.test.tsx +3 -1
  139. package/src/hooks/useDomEditSession.ts +18 -6
  140. package/src/hooks/useDomEditTextCommits.ts +14 -9
  141. package/src/hooks/useDomEditWiring.ts +10 -9
  142. package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
  143. package/src/hooks/useElementLifecycleOps.ts +182 -84
  144. package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
  145. package/src/hooks/useFileManager.ts +82 -60
  146. package/src/hooks/useGsapAnimationOps.ts +7 -6
  147. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  148. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  149. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  150. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  151. package/src/hooks/useGsapScriptCommits.ts +150 -50
  152. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  153. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  154. package/src/hooks/useInspectorSplitResize.ts +51 -0
  155. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  156. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  157. package/src/hooks/usePersistentEditHistory.ts +71 -31
  158. package/src/hooks/usePreviewPersistence.ts +2 -2
  159. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  160. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  161. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  162. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  163. package/src/hooks/useSdkSession.ts +155 -14
  164. package/src/hooks/useSlideshowPersist.ts +7 -1
  165. package/src/hooks/useStoryboard.ts +23 -4
  166. package/src/hooks/useTimelineEditing.test.tsx +306 -155
  167. package/src/hooks/useTimelineEditing.ts +111 -101
  168. package/src/hooks/useTimelineEditingTypes.ts +4 -1
  169. package/src/hooks/useTimelineGroupEditing.ts +51 -5
  170. package/src/hooks/useVariablesPersist.ts +9 -7
  171. package/src/icons/SystemIcons.tsx +2 -0
  172. package/src/player/components/Timeline.test.ts +31 -21
  173. package/src/player/components/Timeline.tsx +62 -91
  174. package/src/player/components/TimelineCanvas.tsx +36 -5
  175. package/src/player/components/TimelineLanes.tsx +32 -10
  176. package/src/player/components/TimelineOverlays.tsx +36 -0
  177. package/src/player/components/TimelineRuler.tsx +18 -5
  178. package/src/player/components/TrackGapContextMenu.tsx +116 -0
  179. package/src/player/components/timelineCallbacks.ts +4 -1
  180. package/src/player/components/timelineClipDragCommit.test.ts +279 -3
  181. package/src/player/components/timelineClipDragCommit.ts +173 -71
  182. package/src/player/components/timelineGapCommit.test.ts +163 -0
  183. package/src/player/components/timelineGapCommit.ts +103 -0
  184. package/src/player/components/timelineGaps.test.ts +221 -0
  185. package/src/player/components/timelineGaps.ts +182 -0
  186. package/src/player/components/timelineLayout.test.ts +2 -1
  187. package/src/player/components/timelineLayout.ts +22 -6
  188. package/src/player/components/timelineMarquee.test.ts +20 -10
  189. package/src/player/components/timelineMarquee.ts +9 -2
  190. package/src/player/components/timelineStackingSync.test.ts +42 -1
  191. package/src/player/components/timelineStackingSync.ts +31 -16
  192. package/src/player/components/timelineZMirror.test.ts +667 -0
  193. package/src/player/components/timelineZMirror.ts +393 -0
  194. package/src/player/components/timelineZoom.test.ts +2 -2
  195. package/src/player/components/timelineZoom.ts +7 -3
  196. package/src/player/components/useTimelineClipDrag.ts +1 -1
  197. package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
  198. package/src/player/components/useTimelineGapHighlights.ts +110 -0
  199. package/src/player/components/useTimelinePlayhead.ts +4 -3
  200. package/src/player/components/useTimelineRangeSelection.ts +6 -4
  201. package/src/player/components/useTimelineScrollViewport.ts +73 -0
  202. package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
  203. package/src/player/components/useTimelineStackingSync.ts +5 -3
  204. package/src/player/components/useTimelineTrackDerivations.ts +38 -0
  205. package/src/player/components/useTrackGapMenu.ts +155 -0
  206. package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
  207. package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
  208. package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
  209. package/src/player/lib/layerOrdering.test.ts +1 -15
  210. package/src/player/lib/layerOrdering.ts +5 -28
  211. package/src/player/lib/timelineDOM.test.ts +72 -0
  212. package/src/player/lib/timelineDOM.ts +3 -0
  213. package/src/player/lib/timelineElementHelpers.ts +35 -11
  214. package/src/player/store/playerStore.ts +12 -1
  215. package/src/styles/studio.css +22 -0
  216. package/src/styles/tailwind-preset.shared.js +7 -0
  217. package/src/telemetry/events.test.ts +10 -0
  218. package/src/telemetry/events.ts +1 -0
  219. package/src/utils/designInputTracking.test.ts +97 -0
  220. package/src/utils/designInputTracking.ts +80 -0
  221. package/src/utils/domEditSaveQueue.ts +4 -3
  222. package/src/utils/editHistory.test.ts +82 -0
  223. package/src/utils/editHistory.ts +14 -2
  224. package/src/utils/gsapSoftReload.test.ts +54 -115
  225. package/src/utils/gsapSoftReload.ts +42 -152
  226. package/src/utils/gsapUndoRestore.test.ts +269 -0
  227. package/src/utils/gsapUndoRestore.ts +269 -0
  228. package/src/utils/sdkCutover.gate.test.ts +7 -7
  229. package/src/utils/sdkCutover.test.ts +490 -57
  230. package/src/utils/sdkCutover.ts +181 -219
  231. package/src/utils/sdkEditTransaction.ts +292 -0
  232. package/src/utils/sdkResolverAttempts.ts +94 -0
  233. package/src/utils/sdkResolverShadow.test.ts +148 -0
  234. package/src/utils/sdkResolverShadow.ts +139 -106
  235. package/src/utils/setSlideshowManifest.test.ts +0 -9
  236. package/src/utils/setSlideshowManifest.ts +20 -27
  237. package/src/utils/sourceScopedSelectorIndex.ts +29 -0
  238. package/src/utils/studioFileHistory.test.ts +41 -0
  239. package/src/utils/studioFileHistory.ts +30 -27
  240. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  241. package/src/utils/studioHelpers.test.ts +45 -0
  242. package/src/utils/studioHelpers.ts +45 -14
  243. package/src/utils/studioSaveDiagnostics.ts +17 -0
  244. package/dist/assets/index-DfmYkU44.js +0 -423
  245. package/dist/assets/index-_pqzyxB1.css +0 -1
  246. package/dist/chunk-5QSIMBEJ.js.map +0 -1
  247. /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
@@ -50,8 +50,17 @@ interface CanvasContextMenuProps {
50
50
  * affected siblings). The menu does NOT touch the live DOM — wire to
51
51
  * handleDomZIndexReorderCommit, which applies the live styles itself
52
52
  * (see module-level wiring comment).
53
+ *
54
+ * `crossed` is the sibling a forward/backward step moved past, resolved from
55
+ * the SAME pre-mutation render order as the patches (null for front/back or
56
+ * when there is no neighbor). The host uses it to mirror the z action into a
57
+ * timeline lane move (resolveZMirrorLaneMove's crossedKey).
53
58
  */
54
- onApplyZIndex?: (patches: ZOrderPatch[], action: ZOrderAction) => void;
59
+ onApplyZIndex?: (
60
+ patches: ZOrderPatch[],
61
+ action: ZOrderAction,
62
+ crossed: HTMLElement | null,
63
+ ) => void;
55
64
  /**
56
65
  * Called after a successful bring-forward / send-backward with the sibling
57
66
  * the target stepped over (resolved from the SAME pre-mutation state as the
@@ -70,10 +79,61 @@ interface CanvasContextMenuProps {
70
79
 
71
80
  type ZAction = "bring-forward" | "send-backward" | "bring-to-front" | "send-to-back";
72
81
 
82
+ // Stacked-layer + arrow glyphs, one per z action (16px, stroke, currentColor —
83
+ // matches the studio's inline-SVG conventions: fill="none", 1.2 stroke, round
84
+ // caps/joins). Single actions show ONE layer diamond with the arrow stepping
85
+ // one way; front/back show a TWO-diamond stack with the arrow piercing through
86
+ // and beyond it. `paths` are the d attributes, drawn in order.
87
+ const Z_ACTION_ICONS: Record<ZAction, string[]> = {
88
+ "bring-forward": [
89
+ "M3 11 L8 8.5 L13 11 L8 13.5 Z", // layer diamond (bottom)
90
+ "M8 8.5 L8 2", // arrow shaft up
91
+ "M5.5 4.5 L8 2 L10.5 4.5", // arrow head
92
+ ],
93
+ "send-backward": [
94
+ "M3 5 L8 2.5 L13 5 L8 7.5 Z", // layer diamond (top)
95
+ "M8 7.5 L8 14", // arrow shaft down
96
+ "M5.5 11.5 L8 14 L10.5 11.5", // arrow head
97
+ ],
98
+ "bring-to-front": [
99
+ "M3 9.5 L8 7 L13 9.5 L8 12 Z", // upper layer of the stack
100
+ "M3 12.5 L8 10 L13 12.5 L8 15 Z", // lower layer of the stack
101
+ "M8 12.5 L8 2", // arrow piercing up through/above the stack
102
+ "M5.5 4.5 L8 2 L10.5 4.5", // arrow head
103
+ ],
104
+ "send-to-back": [
105
+ "M3 4 L8 1.5 L13 4 L8 6.5 Z", // upper layer of the stack
106
+ "M3 7 L8 4.5 L13 7 L8 9.5 Z", // lower layer of the stack
107
+ "M8 3.5 L8 14", // arrow piercing down through/below the stack
108
+ "M5.5 11.5 L8 14 L10.5 11.5", // arrow head
109
+ ],
110
+ };
111
+
112
+ function ZActionIcon({ action }: { action: ZAction }) {
113
+ return (
114
+ <svg
115
+ width="16"
116
+ height="16"
117
+ viewBox="0 0 16 16"
118
+ fill="none"
119
+ stroke="currentColor"
120
+ strokeWidth="1.2"
121
+ strokeLinecap="round"
122
+ strokeLinejoin="round"
123
+ className="mr-2 shrink-0"
124
+ aria-hidden="true"
125
+ >
126
+ {Z_ACTION_ICONS[action].map((d) => (
127
+ <path key={d} d={d} />
128
+ ))}
129
+ </svg>
130
+ );
131
+ }
132
+
73
133
  const Z_ACTIONS: Array<{ action: ZAction; label: string }> = [
134
+ { action: "bring-to-front", label: "Bring to front" },
74
135
  { action: "bring-forward", label: "Bring forward" },
75
136
  { action: "send-backward", label: "Send backward" },
76
- { action: "bring-to-front", label: "Bring to front" },
77
137
  { action: "send-to-back", label: "Send to back" },
78
138
  ];
79
139
 
@@ -113,14 +173,16 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
113
173
  const patches = resolveZOrderChange(el, action);
114
174
  if (patches === null) return;
115
175
  // Resolve the crossed neighbor BEFORE the commit path mutates live styles —
116
- // both resolvers must read the same pre-change render order.
117
- const crossed = onZOrderCrossed ? resolveCrossedNeighbor(el, action) : null;
176
+ // both resolvers must read the same pre-change render order. Always resolved
177
+ // (not only for the flash): onApplyZIndex forwards it so the host can mirror
178
+ // the z step into a timeline lane move.
179
+ const crossed = resolveCrossedNeighbor(el, action);
118
180
  // Do NOT pre-apply styles here: handleDomZIndexReorderCommit writes the
119
181
  // live z-index (and injects position:relative for static elements) in the
120
182
  // same synchronous flow, so feedback is still instant — and it must read
121
183
  // the PRE-change styles itself, both to capture true rollback values and
122
184
  // to detect a static position that needs persisting.
123
- onApplyZIndex(patches, action);
185
+ onApplyZIndex(patches, action, crossed);
124
186
  if (crossed && onZOrderCrossed) onZOrderCrossed(crossed, action);
125
187
  onClose();
126
188
  }
@@ -185,7 +247,10 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
185
247
  if (enabled) handleZAction(action);
186
248
  }}
187
249
  >
188
- {label}
250
+ {/* Icon inherits the item's text color via currentColor, so the
251
+ disabled muted tone applies to both icon and label. */}
252
+ <ZActionIcon action={action} />
253
+ <span>{label}</span>
189
254
  </button>
190
255
  );
191
256
  })}
@@ -103,6 +103,9 @@ interface DomEditOverlayProps {
103
103
  selection: DomEditSelection,
104
104
  patches: ZOrderPatch[],
105
105
  action: ZOrderAction,
106
+ /** Sibling a forward/backward step moved past (pre-mutation render order);
107
+ * null for front/back. Feeds the timeline z-mirror's crossedKey. */
108
+ crossed: HTMLElement | null,
106
109
  ) => void;
107
110
  }
108
111
 
@@ -529,8 +532,8 @@ export const DomEditOverlay = memo(function DomEditOverlay({
529
532
  }
530
533
  onApplyZIndex={
531
534
  onApplyZIndex
532
- ? (patches, action) => {
533
- onApplyZIndex(contextMenu.sel, patches, action);
535
+ ? (patches, action, crossed) => {
536
+ onApplyZIndex(contextMenu.sel, patches, action, crossed);
534
537
  }
535
538
  : undefined
536
539
  }
@@ -1,3 +1,5 @@
1
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
2
+
1
3
  export type GestureRecordingState = "idle" | "recording" | "preview";
2
4
 
3
5
  interface GestureRecordIconProps {
@@ -28,13 +30,17 @@ export function GestureRecordPanelButton({
28
30
  onToggleRecording,
29
31
  }: GestureRecordPanelButtonProps) {
30
32
  const recording = recordingState === "recording";
33
+ const track = useTrackDesignInput();
31
34
 
32
35
  return (
33
36
  <div className="px-4 pb-3">
34
37
  <button
35
38
  type="button"
36
39
  onMouseDown={(e) => e.preventDefault()}
37
- onClick={onToggleRecording}
40
+ onClick={() => {
41
+ track("button", "Gesture recording");
42
+ onToggleRecording();
43
+ }}
38
44
  className={`w-full flex items-center justify-center gap-2 rounded-lg py-2 text-[11px] font-medium transition-colors ${
39
45
  recording
40
46
  ? "bg-red-500/15 text-red-400 border border-red-500/30 animate-pulse"
@@ -4,7 +4,11 @@ import { Film } from "../../icons/SystemIcons";
4
4
  import { Section } from "./propertyPanelPrimitives";
5
5
  import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";
6
6
  import { AnimationCard } from "./AnimationCard";
7
- import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
7
+ import {
8
+ trackAnimationMetaUpdate,
9
+ type GsapAnimationEditCallbacks,
10
+ } from "./gsapAnimationCallbacks";
11
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
8
12
 
9
13
  interface GsapAnimationSectionProps extends GsapAnimationEditCallbacks {
10
14
  animations: GsapAnimation[];
@@ -34,7 +38,24 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
34
38
  onSetAllKeyframeEases,
35
39
  onUnroll,
36
40
  }: GsapAnimationSectionProps) {
41
+ const track = useTrackDesignInput();
37
42
  const [addMenuOpen, setAddMenuOpen] = useState(false);
43
+ const trackProperty = (property: string) => {
44
+ const control =
45
+ property === "visibility"
46
+ ? "toggle"
47
+ : property === "filter" || property === "clipPath"
48
+ ? "text"
49
+ : "metric";
50
+ track(control, property);
51
+ };
52
+ const updateMeta = (
53
+ animationId: string,
54
+ updates: { duration?: number; ease?: string; position?: number },
55
+ ) => {
56
+ trackAnimationMetaUpdate(track, updates);
57
+ onUpdateMeta(animationId, updates);
58
+ };
38
59
 
39
60
  return (
40
61
  <Section title="Animation" icon={<Film size={15} />}>
@@ -58,21 +79,94 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
58
79
  key={anim.id}
59
80
  animation={anim}
60
81
  defaultExpanded={index === 0}
61
- onUpdateProperty={onUpdateProperty}
62
- onUpdateMeta={onUpdateMeta}
63
- onDeleteAnimation={onDeleteAnimation}
64
- onAddProperty={onAddProperty}
65
- onRemoveProperty={onRemoveProperty}
66
- onUpdateFromProperty={onUpdateFromProperty}
67
- onAddFromProperty={onAddFromProperty}
68
- onRemoveFromProperty={onRemoveFromProperty}
82
+ onUpdateProperty={(animationId, property, value) => {
83
+ trackProperty(property);
84
+ onUpdateProperty(animationId, property, value);
85
+ }}
86
+ onUpdateMeta={updateMeta}
87
+ onDeleteAnimation={(animationId) => {
88
+ track("button", "Remove animation");
89
+ onDeleteAnimation(animationId);
90
+ }}
91
+ onAddProperty={(animationId, property) => {
92
+ track("select", "Add effect property");
93
+ onAddProperty(animationId, property);
94
+ }}
95
+ onRemoveProperty={(animationId, property) => {
96
+ track("button", `Remove ${property}`);
97
+ onRemoveProperty(animationId, property);
98
+ }}
99
+ onUpdateFromProperty={
100
+ onUpdateFromProperty
101
+ ? (animationId, property, value) => {
102
+ trackProperty(property);
103
+ onUpdateFromProperty(animationId, property, value);
104
+ }
105
+ : undefined
106
+ }
107
+ onAddFromProperty={
108
+ onAddFromProperty
109
+ ? (animationId, property) => {
110
+ track("select", "Add from property");
111
+ onAddFromProperty(animationId, property);
112
+ }
113
+ : undefined
114
+ }
115
+ onRemoveFromProperty={
116
+ onRemoveFromProperty
117
+ ? (animationId, property) => {
118
+ track("button", `Remove from ${property}`);
119
+ onRemoveFromProperty(animationId, property);
120
+ }
121
+ : undefined
122
+ }
69
123
  onLivePreview={onLivePreview}
70
124
  onLivePreviewEnd={onLivePreviewEnd}
71
- onSetArcPath={onSetArcPath}
72
- onUpdateArcSegment={onUpdateArcSegment}
73
- onUpdateKeyframeEase={onUpdateKeyframeEase}
74
- onSetAllKeyframeEases={onSetAllKeyframeEases}
75
- onUnroll={onUnroll}
125
+ onSetArcPath={
126
+ onSetArcPath
127
+ ? (animationId, config) => {
128
+ track(
129
+ "toggle",
130
+ config.autoRotate !== undefined ? "Auto rotate" : "Arc motion",
131
+ );
132
+ onSetArcPath(animationId, config);
133
+ }
134
+ : undefined
135
+ }
136
+ onUpdateArcSegment={
137
+ onUpdateArcSegment
138
+ ? (animationId, segmentIndex, update) => {
139
+ if (update.curviness === undefined) {
140
+ track("button", `Reset arc segment ${segmentIndex + 1}`);
141
+ }
142
+ onUpdateArcSegment(animationId, segmentIndex, update);
143
+ }
144
+ : undefined
145
+ }
146
+ onUpdateKeyframeEase={
147
+ onUpdateKeyframeEase
148
+ ? (animationId, percentage, ease) => {
149
+ track("select", "Keyframe ease");
150
+ onUpdateKeyframeEase(animationId, percentage, ease);
151
+ }
152
+ : undefined
153
+ }
154
+ onSetAllKeyframeEases={
155
+ onSetAllKeyframeEases
156
+ ? (animationId, ease) => {
157
+ track("select", "All keyframe eases");
158
+ onSetAllKeyframeEases(animationId, ease);
159
+ }
160
+ : undefined
161
+ }
162
+ onUnroll={
163
+ onUnroll
164
+ ? (animationId) => {
165
+ track("button", "Unroll animation");
166
+ onUnroll(animationId);
167
+ }
168
+ : undefined
169
+ }
76
170
  />
77
171
  ))}
78
172
 
@@ -85,6 +179,7 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
85
179
  type="button"
86
180
  title={METHOD_TOOLTIPS[method]}
87
181
  onClick={() => {
182
+ track("button", `Add ${method} animation`);
88
183
  onAddAnimation(method);
89
184
  setAddMenuOpen(false);
90
185
  }}
@@ -1,26 +1,58 @@
1
+ import { Eye, EyeSlash } from "@phosphor-icons/react";
1
2
  import { X } from "../../icons/SystemIcons";
3
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
2
4
  import type { DomEditSelection } from "./domEditingTypes";
3
5
 
4
- /** The action buttons in the inspector header: Ungroup (groups only), copy, clear. */
6
+ /** The action buttons in the inspector header: visibility, Ungroup (groups only), copy, clear. */
5
7
  export function InspectorHeaderActions({
6
8
  element,
7
9
  copied,
8
10
  onCopy,
9
11
  onClear,
10
12
  onUngroup,
13
+ selectedElementId,
14
+ selectedElementHidden,
15
+ visibilityLabel,
16
+ onToggleHidden,
11
17
  }: {
12
18
  element: DomEditSelection;
13
19
  copied: boolean;
14
20
  onCopy: () => void;
15
21
  onClear: () => void;
16
22
  onUngroup?: () => void;
23
+ selectedElementId?: string | null;
24
+ selectedElementHidden?: boolean;
25
+ visibilityLabel?: string;
26
+ onToggleHidden?: (id: string, hidden: boolean) => void;
17
27
  }) {
28
+ const track = useTrackDesignInput();
18
29
  return (
19
30
  <div className="flex items-center gap-1">
31
+ {selectedElementId && onToggleHidden && (
32
+ <button
33
+ type="button"
34
+ aria-label={visibilityLabel}
35
+ title={visibilityLabel}
36
+ onClick={() => {
37
+ track("toggle", "Element visibility");
38
+ void onToggleHidden(selectedElementId, !selectedElementHidden);
39
+ }}
40
+ className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
41
+ >
42
+ {selectedElementHidden ? (
43
+ <EyeSlash size={13} weight="bold" aria-hidden="true" />
44
+ ) : (
45
+ <Eye size={13} weight="bold" aria-hidden="true" />
46
+ )}
47
+ </button>
48
+ )}
20
49
  {onUngroup && element.dataAttributes["hf-group"] != null && (
21
50
  <button
22
51
  type="button"
23
- onClick={onUngroup}
52
+ onClick={() => {
53
+ track("button", "Ungroup");
54
+ onUngroup();
55
+ }}
24
56
  title="Ungroup (⌘⇧G)"
25
57
  className="flex h-6 items-center rounded px-2 text-[11px] font-medium text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-neutral-200"
26
58
  >
@@ -29,7 +61,10 @@ export function InspectorHeaderActions({
29
61
  )}
30
62
  <button
31
63
  type="button"
32
- onClick={onCopy}
64
+ onClick={() => {
65
+ track("button", "Copy element info");
66
+ onCopy();
67
+ }}
33
68
  className={`flex h-6 w-6 items-center justify-center rounded transition-colors ${
34
69
  copied
35
70
  ? "text-studio-accent"
@@ -52,7 +87,10 @@ export function InspectorHeaderActions({
52
87
  <button
53
88
  type="button"
54
89
  aria-label="Clear selection"
55
- onClick={onClear}
90
+ onClick={() => {
91
+ track("button", "Clear selection");
92
+ onClear();
93
+ }}
56
94
  className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
57
95
  >
58
96
  <X size={13} />
@@ -14,8 +14,14 @@ import {
14
14
  } from "../../utils/studioHelpers";
15
15
  import { Layers } from "../../icons/SystemIcons";
16
16
  import { useLayerDrag, isLayerDraggable, type LayerReorderEvent } from "./useLayerDrag";
17
- import { computeReorderZValues, getElementZIndex } from "../../player/lib/layerOrdering";
17
+ import { getVisibleLayers, sortLayersByZIndex } from "./layersPanelSort";
18
18
  import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
19
+ import { resolveZOrderReposition } from "./canvasContextMenuZOrder";
20
+ import { buildStableSelector } from "./domEditingDom";
21
+ import { zReorderCoalesceKey } from "../../hooks/useElementLifecycleOps";
22
+ import { useLayerReorderTimelineMirror } from "../nle/useCanvasZOrderTimelineMirror";
23
+ import { runZLaneGesture } from "../nle/zLaneGesture";
24
+ import { useLayerRevealOverride } from "./useLayerRevealOverride";
19
25
 
20
26
  const TAG_ICONS: Record<string, string> = {
21
27
  video: "Vi",
@@ -85,8 +91,13 @@ interface CollapsedState {
85
91
  // fallow-ignore-next-line complexity
86
92
  export const LayersPanel = memo(function LayersPanel() {
87
93
  const { previewIframeRef, activeCompPath, showToast } = useStudioShellContext();
88
- const { refreshKey, compositionLoading, timelineElements } = useStudioPlaybackContext();
94
+ const { refreshKey, compositionLoading, timelineElements, isPlaying } =
95
+ useStudioPlaybackContext();
89
96
  const currentTime = usePlayerStore((s) => s.currentTime);
97
+ // Flashless z commits (canvas menu, timeline lane-drag z-sync) mutate iframe
98
+ // z-indexes with no reload and no refreshKey bump — while paused, nothing
99
+ // else re-collects, so the panel's z-sorted order would go stale.
100
+ const zEditVersion = usePlayerStore((s) => s.zEditVersion);
90
101
  const {
91
102
  domEditSelection,
92
103
  activeGroupElement,
@@ -100,6 +111,11 @@ export const LayersPanel = memo(function LayersPanel() {
100
111
  const [collapsed, setCollapsed] = useState<CollapsedState>({});
101
112
  const prevDocVersionRef = useRef(0);
102
113
  const scrollContainerRef = useRef<HTMLDivElement>(null);
114
+ const mirrorLayerReorderToTimeline = useLayerReorderTimelineMirror();
115
+ const { scheduleReveal } = useLayerRevealOverride({
116
+ isPlaying,
117
+ selectedElement: domEditSelection?.element ?? null,
118
+ });
103
119
 
104
120
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
105
121
 
@@ -131,7 +147,7 @@ export const LayersPanel = memo(function LayersPanel() {
131
147
 
132
148
  useEffect(() => {
133
149
  collectLayers();
134
- }, [collectLayers, refreshKey]);
150
+ }, [collectLayers, refreshKey, zEditVersion]);
135
151
 
136
152
  useEffect(() => {
137
153
  const iframe = previewIframeRef.current;
@@ -230,8 +246,13 @@ export const LayersPanel = memo(function LayersPanel() {
230
246
  if (!selection) return;
231
247
  applyDomSelection(selection);
232
248
  await seekToLayer(layer);
249
+ // Force-reveal AFTER the seek's runtime visibility sync has had a beat:
250
+ // a clip made active by the seek shows naturally and needs no override,
251
+ // so the reveal only touches nodes that REMAIN hidden (animation-parked
252
+ // opacity, non-clip display/visibility hides, hidden ancestors).
253
+ scheduleReveal(selection.element, 150);
233
254
  },
234
- [resolveSelection, applyDomSelection, seekToLayer],
255
+ [resolveSelection, applyDomSelection, seekToLayer, scheduleReveal],
235
256
  );
236
257
 
237
258
  // Double-click a group row → drill into it; any other row → select it.
@@ -264,6 +285,7 @@ export const LayersPanel = memo(function LayersPanel() {
264
285
  setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }));
265
286
  }, []);
266
287
 
288
+ // fallow-ignore-next-line complexity
267
289
  const handleReorder = useCallback(
268
290
  (event: LayerReorderEvent) => {
269
291
  const { siblingLayers, fromIndex, toIndex } = event;
@@ -271,29 +293,88 @@ export const LayersPanel = memo(function LayersPanel() {
271
293
  const [moved] = reordered.splice(fromIndex, 1);
272
294
  reordered.splice(toIndex, 0, moved);
273
295
 
274
- const existingValues = siblingLayers.map((l) => getElementZIndex(l.element));
275
- const zValues = computeReorderZValues(existingValues, fromIndex, toIndex);
276
-
277
- const entries = reordered.map((layer, i) => ({
278
- element: layer.element,
279
- zIndex: zValues[i],
280
- id: layer.id,
281
- selector: layer.selector,
282
- selectorIndex: layer.selectorIndex,
283
- sourceFile: layer.sourceFile,
284
- key: deriveTimelineStoreKey({
285
- domId: layer.id,
286
- selector: layer.selector,
287
- selectorIndex: layer.selectorIndex,
288
- sourceFile: layer.sourceFile,
289
- }),
290
- }));
291
-
292
- // "layer-drag" keeps consecutive drops of the same sibling set coalescing
293
- // into one undo step, without merging with a context-menu z action.
294
- handleDomZIndexReorderCommit(entries, undefined, "layer-drag");
296
+ // Panel order is top-first (sortLayersByZIndex: z desc, later-DOM-first),
297
+ // so the desired RENDER order (bottom→top) is the reverse. The minimal
298
+ // resolver (shared with the canvas z-menu) then writes one between-z
299
+ // value when a strict gap exists, band-safe renumber otherwise — instead
300
+ // of the old computeReorderZValues stamp of every sibling.
301
+ const desiredBottomToTop = [...reordered].reverse();
302
+ const patches = resolveZOrderReposition(
303
+ moved.element,
304
+ desiredBottomToTop.map((l) => l.element),
305
+ );
306
+ if (!patches || patches.length === 0) return; // paint order unchanged
307
+
308
+ const layerByElement = new Map(siblingLayers.map((l) => [l.element, l]));
309
+ const entries: Array<{
310
+ element: HTMLElement;
311
+ zIndex: number;
312
+ id?: string;
313
+ selector?: string;
314
+ selectorIndex?: number;
315
+ sourceFile: string;
316
+ key?: string;
317
+ }> = [];
318
+ for (const patch of patches) {
319
+ // The renumber fallback can patch a painting sibling the panel didn't
320
+ // list (non-collected family member): derive its identity from the DOM
321
+ // node, exactly like the canvas menu's siblingZIndexEntry. Un-targetable
322
+ // nodes get a live-only style write (reverts on reload).
323
+ const layer = layerByElement.get(patch.element);
324
+ const id = layer?.id ?? (patch.element.id || undefined);
325
+ const selector = layer?.selector ?? buildStableSelector(patch.element);
326
+ if (!id && !selector) {
327
+ patch.element.style.zIndex = String(patch.zIndex);
328
+ continue;
329
+ }
330
+ const sourceFile = layer?.sourceFile ?? moved.sourceFile;
331
+ entries.push({
332
+ element: patch.element,
333
+ zIndex: patch.zIndex,
334
+ id,
335
+ selector,
336
+ selectorIndex: layer?.selectorIndex,
337
+ sourceFile,
338
+ key: deriveTimelineStoreKey({
339
+ domId: id,
340
+ selector,
341
+ selectorIndex: layer?.selectorIndex,
342
+ sourceFile,
343
+ }),
344
+ });
345
+ }
346
+ if (entries.length === 0) return;
347
+
348
+ // ONE undo entry for the whole gesture: the z persist and the timeline
349
+ // lane mirror below share this per-gesture-unique key (same contract as
350
+ // the canvas menu's wiring in PreviewOverlays).
351
+ const coalesceKey = zReorderCoalesceKey(entries, "layer-drag");
352
+ const desiredOrderKeys = desiredBottomToTop.map(
353
+ (l) =>
354
+ deriveTimelineStoreKey({
355
+ domId: l.id,
356
+ selector: l.selector,
357
+ selectorIndex: l.selectorIndex,
358
+ sourceFile: l.sourceFile,
359
+ }) ?? null,
360
+ );
361
+ const movedKey = deriveTimelineStoreKey({
362
+ domId: moved.id,
363
+ selector: moved.selector,
364
+ selectorIndex: moved.selectorIndex,
365
+ sourceFile: moved.sourceFile,
366
+ });
367
+ // One serialized z→lane transaction (shared queue with the canvas
368
+ // z-order menu): the mirror runs only after a DURABLE z persist, and
369
+ // rapid successive gestures cannot interleave phases — see
370
+ // runZLaneGesture.
371
+ runZLaneGesture({
372
+ commitZ: () => handleDomZIndexReorderCommit(entries, coalesceKey, "layer-drag"),
373
+ mirror: () =>
374
+ mirrorLayerReorderToTimeline({ selectionKey: movedKey, desiredOrderKeys, coalesceKey }),
375
+ }).catch(() => undefined);
295
376
  },
296
- [handleDomZIndexReorderCommit],
377
+ [handleDomZIndexReorderCommit, mirrorLayerReorderToTimeline],
297
378
  );
298
379
 
299
380
  const selectedKey = domEditSelection ? getDomEditLayerKey(domEditSelection) : null;
@@ -436,76 +517,6 @@ export const LayersPanel = memo(function LayersPanel() {
436
517
  );
437
518
  });
438
519
 
439
- // ── Pure helpers ──────────────────────────────────────────────────────
440
-
441
- // fallow-ignore-next-line complexity
442
- export function sortLayersByZIndex(layers: DomEditLayerItem[]): DomEditLayerItem[] {
443
- if (layers.length <= 1) return layers;
444
-
445
- const minDepth = layers[0].depth;
446
- for (let i = 1; i < layers.length; i++) {
447
- if (layers[i].depth < minDepth) return layers;
448
- }
449
-
450
- const chunks: Array<{ root: DomEditLayerItem; children: DomEditLayerItem[]; domIndex: number }> =
451
- [];
452
-
453
- for (let i = 0; i < layers.length; i++) {
454
- if (layers[i].depth === minDepth) {
455
- const children: DomEditLayerItem[] = [];
456
- let j = i + 1;
457
- while (j < layers.length && layers[j].depth > minDepth) {
458
- children.push(layers[j]);
459
- j++;
460
- }
461
- chunks.push({ root: layers[i], children, domIndex: chunks.length });
462
- }
463
- }
464
-
465
- if (chunks.length <= 1) {
466
- if (chunks.length === 1 && chunks[0].children.length > 0) {
467
- const sorted = sortLayersByZIndex(chunks[0].children);
468
- return [chunks[0].root, ...sorted];
469
- }
470
- return layers;
471
- }
472
-
473
- chunks.sort((a, b) => {
474
- const zA = getElementZIndex(a.root.element);
475
- const zB = getElementZIndex(b.root.element);
476
- if (zA !== zB) return zB - zA;
477
- return b.domIndex - a.domIndex;
478
- });
479
-
480
- const result: DomEditLayerItem[] = [];
481
- for (const chunk of chunks) {
482
- result.push(chunk.root);
483
- if (chunk.children.length > 0) {
484
- result.push(...sortLayersByZIndex(chunk.children));
485
- }
486
- }
487
- return result;
488
- }
489
-
490
- function getVisibleLayers(
491
- layers: DomEditLayerItem[],
492
- collapsed: CollapsedState,
493
- ): DomEditLayerItem[] {
494
- if (Object.keys(collapsed).length === 0) return layers;
495
-
496
- const result: DomEditLayerItem[] = [];
497
- let skipDepth = -1;
498
-
499
- for (const layer of layers) {
500
- if (skipDepth >= 0 && layer.depth > skipDepth) continue;
501
- skipDepth = -1;
502
-
503
- result.push(layer);
504
-
505
- if (collapsed[layer.key] && layer.childCount > 0) {
506
- skipDepth = layer.depth;
507
- }
508
- }
509
-
510
- return result;
511
- }
520
+ // The sort helper lives in layersPanelSort.ts (600-line studio cap);
521
+ // re-exported so existing imports from "./LayersPanel" still resolve.
522
+ export { sortLayersByZIndex } from "./layersPanelSort";