@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
@@ -5,6 +5,7 @@ import { MetricField } from "./propertyPanelPrimitives";
5
5
  import { KeyframeNavigation } from "./KeyframeNavigation";
6
6
  import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
7
7
  import { Transform3DCube, type CubePose } from "./Transform3DCube";
8
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
8
9
 
9
10
  // translateZ only foreshortens under a perspective lens. Rather than hardcode one
10
11
  // (an arbitrary px value reads wrong at different canvas sizes), derive it from the
@@ -71,6 +72,7 @@ function Cube3dControl({
71
72
  onKeyframe?: () => void;
72
73
  keyframed?: boolean;
73
74
  }) {
75
+ const track = useTrackDesignInput();
74
76
  const pose: CubePose = {
75
77
  rotationX: gsapRuntimeValues.rotationX ?? 0,
76
78
  rotationY: gsapRuntimeValues.rotationY ?? 0,
@@ -96,6 +98,7 @@ function Cube3dControl({
96
98
  }
97
99
  const axes = Object.keys(changedProps);
98
100
  if (axes.length === 0) return;
101
+ track("slider", "3D rotation pose");
99
102
  // ONE keyframe for the whole pose change — avoids per-axis commits racing into
100
103
  // adjacent duplicate keyframes.
101
104
  void onCommitAnimatedProperties(element, changedProps);
@@ -111,6 +114,7 @@ function Cube3dControl({
111
114
  scale: 1,
112
115
  transformPerspective: 0,
113
116
  };
117
+ track("button", "Reset 3D transform");
114
118
  void onCommitAnimatedProperties(element, identity);
115
119
  };
116
120
  // Immediate element feedback while dragging — set the live transform without a
@@ -168,6 +172,7 @@ function Cube3dControl({
168
172
  }
169
173
  // One commit for all props so the writes can't race read-modify-write on
170
174
  // the same script (which dropped a prop and reverted after a seek).
175
+ track("slider", "3D depth");
171
176
  void onCommitAnimatedProperties(element, props);
172
177
  }}
173
178
  onRecenter={recenter}
@@ -0,0 +1,28 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { ColorField } from "./propertyPanelColor";
7
+
8
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
9
+
10
+ afterEach(() => {
11
+ document.body.innerHTML = "";
12
+ });
13
+
14
+ describe("ColorField flat trigger", () => {
15
+ it("renders label and value inline with a small swatch, no boxed border", () => {
16
+ const host = document.createElement("div");
17
+ document.body.append(host);
18
+ const root = createRoot(host);
19
+ act(() => {
20
+ root.render(<ColorField flat label="Color" value="rgb(255, 176, 32)" onCommit={vi.fn()} />);
21
+ });
22
+ const trigger = host.querySelector<HTMLButtonElement>('[data-flat-color-trigger="true"]');
23
+ expect(trigger).not.toBeNull();
24
+ expect(trigger?.className).not.toContain("border-neutral-800");
25
+ expect(host.textContent).toContain("Color");
26
+ act(() => root.unmount());
27
+ });
28
+ });
@@ -11,6 +11,7 @@ import {
11
11
  } from "./colorValue";
12
12
  import { resolveFloatingPanelPosition, type FloatingPosition } from "./floatingPanel";
13
13
  import { colorFromCss, FIELD, LABEL } from "./propertyPanelHelpers";
14
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
14
15
 
15
16
  const COLOR_PICKER_SIZE = { width: 292, height: 386 };
16
17
 
@@ -121,13 +122,16 @@ export function ColorField({
121
122
  label,
122
123
  value,
123
124
  disabled,
125
+ flat,
124
126
  onCommit,
125
127
  }: {
126
128
  label: string;
127
129
  value: string;
128
130
  disabled?: boolean;
131
+ flat?: boolean;
129
132
  onCommit: (nextValue: string) => void;
130
133
  }) {
134
+ const track = useTrackDesignInput();
131
135
  const buttonRef = useRef<HTMLButtonElement | null>(null);
132
136
  const panelRef = useRef<HTMLDivElement | null>(null);
133
137
  const [open, setOpen] = useState(false);
@@ -201,7 +205,9 @@ export function ColorField({
201
205
  const commitColor = (nextColor: ParsedColor) => {
202
206
  setDraftColor(nextColor);
203
207
  setHexDraft(toHexColor(nextColor).toUpperCase());
204
- onCommit(formatCssColor(nextColor));
208
+ const nextValue = formatCssColor(nextColor);
209
+ if (nextValue !== value) track("color", label);
210
+ onCommit(nextValue);
205
211
  };
206
212
 
207
213
  const commitHsv = (nextHsv: { hue?: number; saturation?: number; value?: number }) => {
@@ -349,6 +355,30 @@ export function ColorField({
349
355
  }
350
356
  };
351
357
 
358
+ if (flat) {
359
+ return (
360
+ <div className="flex min-h-[30px] items-center justify-between">
361
+ <span className="text-[11px] text-panel-text-2">{label}</span>
362
+ <button
363
+ type="button"
364
+ data-flat-color-trigger="true"
365
+ disabled={disabled}
366
+ aria-label={`Pick ${label.toLowerCase()} color`}
367
+ ref={buttonRef}
368
+ onClick={openPicker}
369
+ className="flex items-center gap-2 disabled:cursor-not-allowed"
370
+ >
371
+ <span
372
+ className="h-4 w-4 flex-shrink-0 rounded-[4px]"
373
+ style={{ backgroundColor: value || "transparent" }}
374
+ />
375
+ <span className="font-mono text-[11px] text-panel-text-0">{value}</span>
376
+ </button>
377
+ {picker}
378
+ </div>
379
+ );
380
+ }
381
+
352
382
  return (
353
383
  <div className="grid min-w-0 gap-1.5">
354
384
  <span className={LABEL}>{label}</span>
@@ -11,6 +11,7 @@ import { ChevronDown, ChevronRight, Plus, X } from "../../icons/SystemIcons";
11
11
  import { LUT_EXT } from "../../utils/mediaTypes";
12
12
  import { LABEL } from "./propertyPanelHelpers";
13
13
  import { ColorGradingSliderControl } from "./propertyPanelColorGradingSlider";
14
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
14
15
 
15
16
  const LUT_UPLOAD_DIR = "assets/luts";
16
17
 
@@ -175,6 +176,7 @@ export function ColorGradingControls({
175
176
  onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
176
177
  onCommitColorGrading: (nextGrading: NormalizedHfColorGrading) => void;
177
178
  }) {
179
+ const track = useTrackDesignInput();
178
180
  const lutInputRef = useRef<HTMLInputElement>(null);
179
181
  const [lutOpen, setLutOpen] = useState(false);
180
182
  const [detailSettings, setDetailSettings] = useState<"vignette" | "grain" | null>(null);
@@ -195,7 +197,10 @@ export function ColorGradingControls({
195
197
 
196
198
  const applyPreset = (preset: string) => {
197
199
  const next = normalizeHfColorGrading({ preset, intensity: 1, lut: grading.lut });
198
- if (next) onCommitColorGrading(next);
200
+ if (next) {
201
+ track("select", "Preset");
202
+ onCommitColorGrading(next);
203
+ }
199
204
  };
200
205
  const updateFilterIntensity = (value: number) => {
201
206
  onCommitColorGrading({
@@ -218,7 +223,10 @@ export function ColorGradingControls({
218
223
  if (!files?.length || !onImportAssets) return;
219
224
  const uploaded = await onImportAssets(files, LUT_UPLOAD_DIR);
220
225
  const firstLut = uploaded.find((asset) => LUT_EXT.test(asset));
221
- if (firstLut) applyLut(firstLut, 1);
226
+ if (firstLut) {
227
+ track("button", "Import LUT");
228
+ applyLut(firstLut, 1);
229
+ }
222
230
  };
223
231
  const commitDetailSlider = (slider: DetailSlider, next: number) => {
224
232
  onCommitColorGrading({
@@ -313,6 +321,7 @@ export function ColorGradingControls({
313
321
  value={selectedLut}
314
322
  onChange={(event) => {
315
323
  const nextSrc = event.target.value;
324
+ track("select", "Custom LUT");
316
325
  applyLut(
317
326
  nextSrc || null,
318
327
  nextSrc && grading.lut?.src === nextSrc ? grading.lut.intensity : 1,
@@ -1,159 +1,15 @@
1
- import {
2
- useCallback,
3
- useEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- type PointerEvent as ReactPointerEvent,
8
- type RefObject,
9
- } from "react";
10
- import {
11
- HF_COLOR_GRADING_ATTR,
12
- isHfColorGradingActive,
13
- normalizeHfColorGrading,
14
- serializeHfColorGrading,
15
- type HfColorGradingTarget,
16
- type NormalizedHfColorGrading,
17
- } from "@hyperframes/core/color-grading";
1
+ import { type PointerEvent as ReactPointerEvent, type RefObject } from "react";
2
+ import { isHfColorGradingActive } from "@hyperframes/core/color-grading";
18
3
  import { Compare, Palette, RotateCcw } from "../../icons/SystemIcons";
19
- import {
20
- addStudioPendingEditFlushListener,
21
- trackStudioPendingEdit,
22
- } from "../../utils/studioPendingEdits";
23
4
  import type { DomEditSelection } from "./domEditing";
24
5
  import { ColorGradingControls } from "./propertyPanelColorGradingControls";
25
- import { stripQueryAndHash } from "./propertyPanelHelpers";
26
6
  import { Section } from "./propertyPanelPrimitives";
27
7
  import {
28
- acceptStudioRuntimeMessage,
29
- postRuntimeControlMessage,
30
- } from "../../player/lib/runtimeProtocol";
31
-
32
- const COLOR_GRADING_DATA_KEY = HF_COLOR_GRADING_ATTR.replace(/^data-/, "");
33
- const RUNTIME_STATUS_REFRESH_DELAYS = [50, 250, 1000, 2500] as const;
34
- const MEDIA_METADATA_CACHE = new Map<string, MediaMetadata | null>();
35
-
36
- interface RuntimeColorGradingStatus {
37
- state: "missing" | "inactive" | "pending" | "active" | "unavailable";
38
- message: string;
39
- }
40
-
41
- interface MediaMetadata {
42
- kind: "video" | "image" | "audio" | "unknown";
43
- color: {
44
- dynamicRange: "hdr" | "sdr" | "unknown";
45
- hdrTransfer: "pq" | "hlg" | "unknown" | null;
46
- label: string;
47
- isHdr: boolean;
48
- codecName?: string;
49
- profile?: string;
50
- pixelFormat?: string;
51
- colorSpace?: string;
52
- colorTransfer?: string;
53
- colorPrimaries?: string;
54
- };
55
- probeError?: string;
56
- }
57
-
58
- interface MediaMetadataResponse {
59
- path: string;
60
- metadata: MediaMetadata;
61
- }
62
-
63
- function stripPreviewAssetPath(src: string, projectId: string): string | null {
64
- let pathname = src;
65
- try {
66
- pathname = new URL(src, window.location.href).pathname;
67
- } catch {
68
- return null;
69
- }
70
- const projectMarker = `/api/projects/${encodeURIComponent(projectId)}/preview/`;
71
- const genericMarker = "/preview/";
72
- const marker = pathname.includes(projectMarker) ? projectMarker : genericMarker;
73
- const index = pathname.indexOf(marker);
74
- if (index < 0) return null;
75
- const assetPath = decodeURIComponent(pathname.slice(index + marker.length)).replace(/^\/+/, "");
76
- if (!assetPath || assetPath.startsWith("comp/")) return null;
77
- return assetPath;
78
- }
79
-
80
- // fallow-ignore-next-line complexity
81
- function resolveProjectAssetPath(
82
- sourceFile: string,
83
- src: string,
84
- projectId: string,
85
- ): string | null {
86
- const trimmed = stripQueryAndHash(src.trim());
87
- if (!trimmed || /^(?:data:|blob:)/i.test(trimmed)) return null;
88
- if (/^https?:\/\//i.test(trimmed)) return stripPreviewAssetPath(trimmed, projectId);
89
- if (trimmed.startsWith("/")) {
90
- return stripPreviewAssetPath(trimmed, projectId);
91
- }
92
-
93
- const sourceDir = sourceFile.includes("/")
94
- ? sourceFile.slice(0, sourceFile.lastIndexOf("/"))
95
- : "";
96
- const parts = `${sourceDir}/${trimmed}`.split("/");
97
- const normalized: string[] = [];
98
- for (const part of parts) {
99
- if (!part || part === ".") continue;
100
- if (part === "..") {
101
- normalized.pop();
102
- continue;
103
- }
104
- normalized.push(part);
105
- }
106
- return normalized.join("/") || null;
107
- }
108
-
109
- function selectedMediaAssetPath(element: DomEditSelection, projectId: string): string | null {
110
- if (element.tagName !== "video" && element.tagName !== "img") return null;
111
- const media = element.element as HTMLImageElement | HTMLVideoElement;
112
- const src = media.getAttribute("src") || media.currentSrc || "";
113
- return resolveProjectAssetPath(element.sourceFile || "index.html", src, projectId);
114
- }
115
-
116
- function defaultColorGrading(): NormalizedHfColorGrading {
117
- const grading = normalizeHfColorGrading("neutral");
118
- if (!grading) throw new Error("Missing neutral color grading preset");
119
- return grading;
120
- }
121
-
122
- function readColorGradingFromElement(element: DomEditSelection): NormalizedHfColorGrading {
123
- return (
124
- normalizeHfColorGrading(element.dataAttributes[COLOR_GRADING_DATA_KEY]) ?? defaultColorGrading()
125
- );
126
- }
127
-
128
- function toBridgeColorGrading(grading: NormalizedHfColorGrading): unknown {
129
- if (!isHfColorGradingActive(grading)) return null;
130
- const { enabled: _enabled, ...bridgeGrading } = grading;
131
- return bridgeGrading;
132
- }
133
-
134
- function readRuntimeColorGradingStatus(
135
- iframe: HTMLIFrameElement | null | undefined,
136
- target: HfColorGradingTarget,
137
- ): RuntimeColorGradingStatus {
138
- try {
139
- const win = iframe?.contentWindow as
140
- | (Window & {
141
- __hf?: {
142
- colorGrading?: {
143
- getStatus?: (
144
- target: HfColorGradingTarget | string | null | undefined,
145
- ) => RuntimeColorGradingStatus;
146
- };
147
- };
148
- })
149
- | null
150
- | undefined;
151
- const status = win?.__hf?.colorGrading?.getStatus?.(target);
152
- return status ?? { state: "pending", message: "Waiting for runtime" };
153
- } catch {
154
- return { state: "unavailable", message: "Preview unavailable" };
155
- }
156
- }
8
+ useColorGradingController,
9
+ type MediaMetadata,
10
+ type RuntimeColorGradingStatus,
11
+ } from "./useColorGradingController";
12
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
157
13
 
158
14
  function StatusPill({ status }: { status: RuntimeColorGradingStatus }) {
159
15
  const dotClass =
@@ -213,10 +69,12 @@ function HoldBeforeButton({
213
69
  disabled: boolean;
214
70
  onHoldChange: (holding: boolean) => void;
215
71
  }) {
72
+ const track = useTrackDesignInput();
216
73
  const startHold = (event: ReactPointerEvent<HTMLButtonElement>) => {
217
74
  if (disabled) return;
218
75
  event.preventDefault();
219
76
  event.stopPropagation();
77
+ track("toggle", "Compare original");
220
78
  onHoldChange(true);
221
79
  const release = () => {
222
80
  onHoldChange(false);
@@ -250,7 +108,10 @@ function HoldBeforeButton({
250
108
  onKeyDown={(event) => {
251
109
  if (disabled || (event.key !== " " && event.key !== "Enter")) return;
252
110
  event.preventDefault();
253
- if (!active) onHoldChange(true);
111
+ if (!active) {
112
+ track("toggle", "Compare original");
113
+ onHoldChange(true);
114
+ }
254
115
  }}
255
116
  onKeyUp={(event) => {
256
117
  if (disabled || (event.key !== " " && event.key !== "Enter")) return;
@@ -283,222 +144,36 @@ export function ColorGradingSection({
283
144
  assets: string[];
284
145
  previewIframeRef?: RefObject<HTMLIFrameElement | null>;
285
146
  onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
286
- onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
147
+ onSetAttributeLive: (
148
+ attr: string,
149
+ value: string | null,
150
+ onSettled?: (ok: boolean) => void,
151
+ ) => void | Promise<void>;
287
152
  onApplyScope?: (
288
153
  scope: "source-file" | "project",
289
154
  value: string | null,
290
155
  ) => Promise<{ changedFiles: number; changedElements: number }>;
291
156
  }) {
292
- const [grading, setGrading] = useState(() => readColorGradingFromElement(element));
293
- const [compareEnabled, setCompareEnabled] = useState(false);
294
- const [applyScope, setApplyScope] = useState<"source-file" | "project">("source-file");
295
- const [applyBusy, setApplyBusy] = useState(false);
296
- const [runtimeStatus, setRuntimeStatus] = useState<RuntimeColorGradingStatus>(() => ({
297
- state: "pending",
298
- message: "Waiting for runtime",
299
- }));
300
- const selectedAssetPath = useMemo(
301
- () => selectedMediaAssetPath(element, projectId),
302
- [element, projectId],
303
- );
304
- const [mediaMetadata, setMediaMetadata] = useState<MediaMetadata | null>(null);
305
- const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
306
- const pendingPersistValueRef = useRef<string | null | undefined>(undefined);
307
- const statusTimersRef = useRef<number[]>([]);
308
- const onSetAttributeLiveRef = useRef(onSetAttributeLive);
309
- const latestGradingRef = useRef(grading);
310
- const compareEnabledRef = useRef(compareEnabled);
311
- onSetAttributeLiveRef.current = onSetAttributeLive;
312
- latestGradingRef.current = grading;
313
- compareEnabledRef.current = compareEnabled;
314
- const target = useMemo(
315
- (): HfColorGradingTarget => ({
316
- id: element.id ?? null,
317
- hfId: element.hfId ?? null,
318
- selector: element.selector ?? null,
319
- selectorIndex: element.selectorIndex ?? null,
320
- }),
321
- [element.hfId, element.id, element.selector, element.selectorIndex],
322
- );
323
-
324
- const refreshRuntimeStatus = useCallback(() => {
325
- setRuntimeStatus(readRuntimeColorGradingStatus(previewIframeRef?.current, target));
326
- }, [previewIframeRef, target]);
327
-
328
- useEffect(() => {
329
- setMediaMetadata(null);
330
- if (!selectedAssetPath) return;
331
- const cacheKey = `${projectId}:${selectedAssetPath}`;
332
- if (MEDIA_METADATA_CACHE.has(cacheKey)) {
333
- setMediaMetadata(MEDIA_METADATA_CACHE.get(cacheKey) ?? null);
334
- return;
335
- }
336
- const controller = new AbortController();
337
- fetch(
338
- `/api/projects/${encodeURIComponent(projectId)}/media/metadata?path=${encodeURIComponent(
339
- selectedAssetPath,
340
- )}`,
341
- { signal: controller.signal },
342
- )
343
- .then((response) => (response.ok ? response.json() : null))
344
- .then((data: MediaMetadataResponse | null) => {
345
- if (controller.signal.aborted) return;
346
- const metadata = data?.metadata ?? null;
347
- MEDIA_METADATA_CACHE.set(cacheKey, metadata);
348
- setMediaMetadata(metadata);
349
- })
350
- .catch(() => {
351
- if (!controller.signal.aborted) MEDIA_METADATA_CACHE.set(cacheKey, null);
352
- });
353
- return () => controller.abort();
354
- }, [projectId, selectedAssetPath]);
355
-
356
- const clearStatusTimers = useCallback(() => {
357
- for (const timer of statusTimersRef.current) clearTimeout(timer);
358
- statusTimersRef.current = [];
359
- }, []);
360
-
361
- const scheduleRuntimeStatusRefresh = useCallback(() => {
362
- clearStatusTimers();
363
- statusTimersRef.current = RUNTIME_STATUS_REFRESH_DELAYS.map((delay) =>
364
- window.setTimeout(refreshRuntimeStatus, delay),
365
- );
366
- }, [clearStatusTimers, refreshRuntimeStatus]);
367
-
368
- useEffect(() => {
369
- refreshRuntimeStatus();
370
- }, [refreshRuntimeStatus]);
371
-
372
- const persistColorGradingValue = useCallback((value: string | null) => {
373
- return trackStudioPendingEdit(
374
- onSetAttributeLiveRef.current(COLOR_GRADING_DATA_KEY, value ?? null),
375
- );
376
- }, []);
377
-
378
- const flushPendingPersist = useCallback(() => {
379
- if (persistTimerRef.current) {
380
- clearTimeout(persistTimerRef.current);
381
- persistTimerRef.current = null;
382
- }
383
- if (pendingPersistValueRef.current === undefined) return undefined;
384
- const value = pendingPersistValueRef.current;
385
- pendingPersistValueRef.current = undefined;
386
- return persistColorGradingValue(value);
387
- }, [persistColorGradingValue]);
388
-
389
- useEffect(() => addStudioPendingEditFlushListener(flushPendingPersist), [flushPendingPersist]);
390
-
391
- useEffect(() => {
392
- return () => {
393
- clearStatusTimers();
394
- void flushPendingPersist();
395
- };
396
- }, [clearStatusTimers, flushPendingPersist]);
397
-
398
- const postColorGrading = useCallback(
399
- (nextGrading: NormalizedHfColorGrading) => {
400
- postRuntimeControlMessage(previewIframeRef?.current?.contentWindow, "set-color-grading", {
401
- target,
402
- grading: toBridgeColorGrading(nextGrading),
403
- });
404
- },
405
- [previewIframeRef, target],
406
- );
407
-
408
- const postCompare = useCallback(
409
- (enabled: boolean) => {
410
- postRuntimeControlMessage(
411
- previewIframeRef?.current?.contentWindow,
412
- "set-color-grading-compare",
413
- {
414
- target,
415
- compare: { enabled, position: 1, lineWidth: 0 },
416
- },
417
- );
418
- },
419
- [previewIframeRef, target],
420
- );
421
-
422
- useEffect(() => {
423
- const iframe = previewIframeRef?.current;
424
- if (!iframe) return;
425
- const refreshAndReplay = () => {
426
- const nextGrading = latestGradingRef.current;
427
- const active = isHfColorGradingActive(nextGrading);
428
- if (active) postColorGrading(nextGrading);
429
- postCompare(compareEnabledRef.current && active);
430
- scheduleRuntimeStatusRefresh();
431
- };
432
- const onMessage = (event: MessageEvent) => {
433
- if (event.source !== iframe.contentWindow) return;
434
- const data = event.data as { source?: unknown; type?: unknown } | null;
435
- if (data?.source !== "hf-preview" || data.type !== "ready") return;
436
- if (!acceptStudioRuntimeMessage(data)) return;
437
- refreshAndReplay();
438
- };
439
- iframe.addEventListener("load", refreshAndReplay);
440
- window.addEventListener("message", onMessage);
441
- const timer = window.setTimeout(refreshAndReplay, 80);
442
- return () => {
443
- iframe.removeEventListener("load", refreshAndReplay);
444
- window.removeEventListener("message", onMessage);
445
- window.clearTimeout(timer);
446
- };
447
- }, [postColorGrading, postCompare, previewIframeRef, scheduleRuntimeStatusRefresh]);
448
-
449
- useEffect(
450
- () => () => {
451
- postCompare(false);
452
- },
453
- [postCompare],
454
- );
455
-
456
- const commitColorGrading = useCallback(
457
- (nextGrading: NormalizedHfColorGrading) => {
458
- setGrading(nextGrading);
459
- setRuntimeStatus({ state: "pending", message: "Updating shader" });
460
- postColorGrading(nextGrading);
461
- const active = isHfColorGradingActive(nextGrading);
462
- if (compareEnabledRef.current) {
463
- postCompare(active);
464
- if (!active) setCompareEnabled(false);
465
- }
466
- scheduleRuntimeStatusRefresh();
467
- if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
468
- pendingPersistValueRef.current = isHfColorGradingActive(nextGrading)
469
- ? serializeHfColorGrading(nextGrading)
470
- : null;
471
- persistTimerRef.current = setTimeout(() => {
472
- const value = pendingPersistValueRef.current;
473
- pendingPersistValueRef.current = undefined;
474
- persistTimerRef.current = null;
475
- void persistColorGradingValue(value ?? null);
476
- }, 350);
477
- },
478
- [persistColorGradingValue, postColorGrading, postCompare, scheduleRuntimeStatusRefresh],
479
- );
480
-
481
- const commitCompare = useCallback(
482
- (enabled: boolean) => {
483
- const nextEnabled = enabled && isHfColorGradingActive(grading);
484
- setCompareEnabled(nextEnabled);
485
- if (nextEnabled) postColorGrading(grading);
486
- postCompare(nextEnabled);
487
- scheduleRuntimeStatusRefresh();
488
- },
489
- [grading, postColorGrading, postCompare, scheduleRuntimeStatusRefresh],
490
- );
491
-
492
- const applyToScope = useCallback(async () => {
493
- if (!onApplyScope || applyBusy) return;
494
- setApplyBusy(true);
495
- try {
496
- const value = isHfColorGradingActive(grading) ? serializeHfColorGrading(grading) : null;
497
- await onApplyScope(applyScope, value);
498
- } finally {
499
- setApplyBusy(false);
500
- }
501
- }, [applyBusy, applyScope, grading, onApplyScope]);
157
+ const track = useTrackDesignInput();
158
+ const {
159
+ grading,
160
+ compareEnabled,
161
+ applyScope,
162
+ applyBusy,
163
+ runtimeStatus,
164
+ mediaMetadata,
165
+ commitColorGrading,
166
+ commitCompare,
167
+ setApplyScope,
168
+ applyToScope,
169
+ resetGrading,
170
+ } = useColorGradingController({
171
+ projectId,
172
+ element,
173
+ previewIframeRef,
174
+ onSetAttributeLive,
175
+ onApplyScope,
176
+ });
502
177
 
503
178
  return (
504
179
  <Section
@@ -516,7 +191,8 @@ export function ColorGradingSection({
516
191
  type="button"
517
192
  onClick={(event) => {
518
193
  event.stopPropagation();
519
- commitColorGrading(defaultColorGrading());
194
+ track("button", "Reset color grading");
195
+ resetGrading();
520
196
  }}
521
197
  className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1"
522
198
  title="Reset color grading"
@@ -537,7 +213,10 @@ export function ColorGradingSection({
537
213
  <div className="mt-4 grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-2">
538
214
  <select
539
215
  value={applyScope}
540
- onChange={(event) => setApplyScope(event.currentTarget.value as typeof applyScope)}
216
+ onChange={(event) => {
217
+ track("select", "Apply scope");
218
+ setApplyScope(event.currentTarget.value as typeof applyScope);
219
+ }}
541
220
  disabled={applyBusy}
542
221
  className="w-full min-w-0 rounded-md bg-panel-input px-3 py-2 text-[11px] font-medium text-panel-text-1 outline-none disabled:cursor-not-allowed disabled:opacity-50"
543
222
  title="Choose where to copy these color grading settings"
@@ -550,6 +229,7 @@ export function ColorGradingSection({
550
229
  disabled={applyBusy}
551
230
  onClick={(event) => {
552
231
  event.stopPropagation();
232
+ track("button", "Apply color grading scope");
553
233
  void applyToScope();
554
234
  }}
555
235
  className="h-8 rounded-md bg-panel-input px-3 text-[11px] font-medium text-panel-text-2 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-50"