@hyperframes/studio 0.7.58 → 0.7.60

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 (173) hide show
  1. package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
  2. package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
  3. package/dist/assets/index-DXbu6IPT.css +1 -0
  4. package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
  5. package/dist/assets/index-cH6NfVV_.js +426 -0
  6. package/dist/index.d.ts +12 -3
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +13755 -8627
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.d.ts +5 -0
  11. package/dist/styles/tailwind-preset.js +8 -1
  12. package/dist/styles/tailwind-preset.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +4 -4
  15. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  16. package/src/components/StudioRightPanel.tsx +42 -53
  17. package/src/components/editor/AnimationCard.test.tsx +134 -0
  18. package/src/components/editor/AnimationCard.tsx +19 -4
  19. package/src/components/editor/ArcPathControls.tsx +1 -0
  20. package/src/components/editor/GestureRecordControl.tsx +7 -1
  21. package/src/components/editor/GsapAnimationSection.tsx +109 -14
  22. package/src/components/editor/InspectorHeaderActions.tsx +42 -4
  23. package/src/components/editor/PropertyPanel.test.tsx +948 -0
  24. package/src/components/editor/PropertyPanel.tsx +144 -148
  25. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  26. package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
  27. package/src/components/editor/PropertyPanelFlat.tsx +580 -0
  28. package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
  29. package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
  30. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
  31. package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
  32. package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
  33. package/src/components/editor/gsapLivePreview.ts +34 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +10 -0
  35. package/src/components/editor/manualEditingAvailability.ts +9 -0
  36. package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
  37. package/src/components/editor/propertyPanelColor.test.tsx +28 -0
  38. package/src/components/editor/propertyPanelColor.tsx +31 -1
  39. package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
  40. package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
  41. package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
  42. package/src/components/editor/propertyPanelFill.tsx +22 -5
  43. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
  44. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
  45. package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
  46. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
  47. package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
  48. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
  49. package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
  50. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
  51. package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
  52. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
  53. package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
  54. package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
  55. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
  56. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
  57. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
  58. package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
  59. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
  60. package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
  61. package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
  62. package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
  63. package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
  64. package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
  65. package/src/components/editor/propertyPanelFont.test.tsx +30 -0
  66. package/src/components/editor/propertyPanelFont.tsx +136 -95
  67. package/src/components/editor/propertyPanelHelpers.ts +73 -0
  68. package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
  69. package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
  70. package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
  71. package/src/components/editor/propertyPanelSections.test.tsx +161 -0
  72. package/src/components/editor/propertyPanelSections.tsx +154 -90
  73. package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
  74. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  75. package/src/components/editor/propertyPanelTypes.ts +14 -1
  76. package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
  77. package/src/components/editor/propertyPanelValueTier.ts +28 -0
  78. package/src/components/editor/useColorGradingController.test.ts +418 -0
  79. package/src/components/editor/useColorGradingController.ts +596 -0
  80. package/src/components/panels/VariablesPanel.tsx +4 -0
  81. package/src/components/sidebar/CompositionsTab.test.ts +28 -2
  82. package/src/components/sidebar/CompositionsTab.tsx +3 -1
  83. package/src/components/storyboard/FramePoster.tsx +24 -4
  84. package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
  85. package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
  86. package/src/components/storyboard/StoryboardGrid.tsx +23 -1
  87. package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
  88. package/src/components/storyboard/StoryboardView.tsx +9 -2
  89. package/src/components/storyboard/frameComments.test.ts +95 -0
  90. package/src/components/storyboard/frameComments.ts +115 -0
  91. package/src/components/storyboard/useFrameComments.ts +82 -0
  92. package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
  93. package/src/contexts/DesignPanelInputContext.tsx +48 -0
  94. package/src/contexts/DomEditContext.tsx +4 -0
  95. package/src/contexts/FileManagerContext.tsx +3 -0
  96. package/src/contexts/PanelLayoutContext.tsx +3 -0
  97. package/src/contexts/VariablePromoteContext.test.tsx +67 -0
  98. package/src/contexts/VariablePromoteContext.tsx +10 -5
  99. package/src/hooks/domEditCommitRunner.ts +11 -0
  100. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  101. package/src/hooks/timelineEditingHelpers.ts +2 -2
  102. package/src/hooks/timelineTrackVisibility.test.ts +52 -0
  103. package/src/hooks/timelineTrackVisibility.ts +27 -10
  104. package/src/hooks/useAppHotkeys.ts +9 -0
  105. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  106. package/src/hooks/useDomEditCommits.test.tsx +41 -1
  107. package/src/hooks/useDomEditCommits.ts +31 -177
  108. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  109. package/src/hooks/useDomEditSession.ts +18 -6
  110. package/src/hooks/useDomEditTextCommits.ts +14 -9
  111. package/src/hooks/useDomEditWiring.ts +10 -9
  112. package/src/hooks/useEditorSave.ts +1 -1
  113. package/src/hooks/useElementLifecycleOps.ts +8 -3
  114. package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
  115. package/src/hooks/useFileManager.ts +151 -65
  116. package/src/hooks/useGsapAnimationOps.ts +7 -6
  117. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  118. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  119. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  120. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  121. package/src/hooks/useGsapScriptCommits.ts +150 -50
  122. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  123. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  124. package/src/hooks/useInspectorSplitResize.ts +51 -0
  125. package/src/hooks/usePanelLayout.test.ts +118 -0
  126. package/src/hooks/usePanelLayout.ts +24 -1
  127. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  128. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  129. package/src/hooks/usePersistentEditHistory.ts +71 -31
  130. package/src/hooks/usePreviewPersistence.ts +4 -1
  131. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  132. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  133. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  134. package/src/hooks/useRazorSplit.history.test.tsx +12 -4
  135. package/src/hooks/useRazorSplit.test.tsx +88 -0
  136. package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
  137. package/src/hooks/useRazorSplit.ts +50 -16
  138. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  139. package/src/hooks/useSdkSession.ts +155 -14
  140. package/src/hooks/useSlideshowPersist.ts +7 -1
  141. package/src/hooks/useStoryboard.ts +23 -4
  142. package/src/hooks/useTimelineEditing.test.tsx +4 -1
  143. package/src/hooks/useTimelineEditing.ts +18 -10
  144. package/src/hooks/useTimelineEditingTypes.ts +5 -1
  145. package/src/hooks/useTimelineGroupEditing.ts +17 -3
  146. package/src/hooks/useVariablesPersist.ts +9 -7
  147. package/src/icons/SystemIcons.tsx +2 -0
  148. package/src/styles/studio.css +22 -0
  149. package/src/styles/tailwind-preset.shared.js +7 -0
  150. package/src/utils/designInputTracking.test.ts +97 -0
  151. package/src/utils/designInputTracking.ts +80 -0
  152. package/src/utils/domEditSaveQueue.test.ts +19 -0
  153. package/src/utils/domEditSaveQueue.ts +2 -1
  154. package/src/utils/sdkCutover.gate.test.ts +7 -7
  155. package/src/utils/sdkCutover.test.ts +511 -62
  156. package/src/utils/sdkCutover.ts +181 -219
  157. package/src/utils/sdkEditTransaction.ts +293 -0
  158. package/src/utils/sdkResolverAttempts.ts +94 -0
  159. package/src/utils/sdkResolverShadow.test.ts +148 -0
  160. package/src/utils/sdkResolverShadow.ts +139 -106
  161. package/src/utils/setSlideshowManifest.test.ts +0 -9
  162. package/src/utils/setSlideshowManifest.ts +20 -27
  163. package/src/utils/studioFileHistory.test.ts +41 -0
  164. package/src/utils/studioFileHistory.ts +34 -29
  165. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  166. package/src/utils/studioFileVersion.test.ts +36 -0
  167. package/src/utils/studioFileVersion.ts +23 -0
  168. package/src/utils/studioHelpers.test.ts +45 -0
  169. package/src/utils/studioHelpers.ts +45 -14
  170. package/src/utils/studioSaveDiagnostics.test.ts +18 -0
  171. package/src/utils/studioSaveDiagnostics.ts +38 -0
  172. package/dist/assets/index-B_UvTX3E.js +0 -423
  173. package/dist/assets/index-uahwWkgw.css +0 -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"
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { Minus, Plus, RotateCcw, Settings } from "../../icons/SystemIcons";
3
3
  import { LABEL } from "./propertyPanelHelpers";
4
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
4
5
 
5
6
  const SLIDER_THUMB_SIZE = 10;
6
7
  const SLIDER_THUMB_RADIUS = SLIDER_THUMB_SIZE / 2;
@@ -59,9 +60,11 @@ export function ColorGradingSliderControl({
59
60
  onClick: () => void;
60
61
  };
61
62
  }) {
63
+ const track = useTrackDesignInput();
62
64
  const [draftState, setDraftState] = useState<{ value: number; source: number } | null>(null);
63
65
  const [inputDraft, setInputDraft] = useState<{ value: string; source: number } | null>(null);
64
66
  const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
67
+ const interactionChangedRef = useRef(false);
65
68
  const valueRef = useRef(value);
66
69
  valueRef.current = value;
67
70
 
@@ -92,9 +95,13 @@ export function ColorGradingSliderControl({
92
95
  (nextValue: number) => {
93
96
  const clamped = setLocalDraft(nextValue);
94
97
  if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
98
+ if (interactionChangedRef.current) {
99
+ interactionChangedRef.current = false;
100
+ track("slider", label);
101
+ }
95
102
  if (clamped !== valueRef.current) onCommit(clamped);
96
103
  },
97
- [onCommit, setLocalDraft],
104
+ [label, onCommit, setLocalDraft, track],
98
105
  );
99
106
 
100
107
  const scheduleCommit = useCallback(
@@ -115,6 +122,7 @@ export function ColorGradingSliderControl({
115
122
  const commitInputDraft = useCallback(() => {
116
123
  const parsed = parseNumericInput(inputValue, scale);
117
124
  if (parsed === null) {
125
+ interactionChangedRef.current = false;
118
126
  setInputDraft(null);
119
127
  return;
120
128
  }
@@ -123,6 +131,7 @@ export function ColorGradingSliderControl({
123
131
 
124
132
  const nudge = useCallback(
125
133
  (direction: -1 | 1) => {
134
+ interactionChangedRef.current = true;
126
135
  commitDraft(draft + step * direction);
127
136
  },
128
137
  [commitDraft, draft, step],
@@ -166,6 +175,7 @@ export function ColorGradingSliderControl({
166
175
  aria-label={`Reset ${label}`}
167
176
  onClick={(event) => {
168
177
  event.stopPropagation();
178
+ track("button", `Reset ${label}`);
169
179
  onReset();
170
180
  }}
171
181
  className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded text-panel-text-5 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
@@ -205,7 +215,10 @@ export function ColorGradingSliderControl({
205
215
  value={draft}
206
216
  disabled={disabled}
207
217
  aria-label={label}
208
- onChange={(event) => scheduleCommit(Number(event.currentTarget.value))}
218
+ onChange={(event) => {
219
+ interactionChangedRef.current = true;
220
+ scheduleCommit(Number(event.currentTarget.value));
221
+ }}
209
222
  onMouseUp={() => commitDraft(draft)}
210
223
  onTouchEnd={() => commitDraft(draft)}
211
224
  onBlur={() => commitDraft(draft)}
@@ -223,9 +236,10 @@ export function ColorGradingSliderControl({
223
236
  max={max / scale}
224
237
  step={step / scale}
225
238
  disabled={disabled}
226
- onChange={(event) =>
227
- setInputDraft({ value: event.currentTarget.value, source: valueRef.current })
228
- }
239
+ onChange={(event) => {
240
+ interactionChangedRef.current = true;
241
+ setInputDraft({ value: event.currentTarget.value, source: valueRef.current });
242
+ }}
229
243
  onBlur={commitInputDraft}
230
244
  onKeyDown={(event) => {
231
245
  if (event.key === "Enter") {
@@ -16,6 +16,7 @@ import {
16
16
  SliderControl,
17
17
  } from "./propertyPanelPrimitives";
18
18
  import { ColorField } from "./propertyPanelColor";
19
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
19
20
 
20
21
  /* ------------------------------------------------------------------ */
21
22
  /* Asset path helpers */
@@ -87,6 +88,7 @@ export function ImageFillField({
87
88
  onCommit: (nextValue: string) => void;
88
89
  onImportAssets?: (files: FileList) => Promise<string[]>;
89
90
  }) {
91
+ const track = useTrackDesignInput();
90
92
  const fileInputRef = useRef<HTMLInputElement | null>(null);
91
93
  const [uploading, setUploading] = useState(false);
92
94
  const imageAssets = useMemo(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
@@ -102,7 +104,10 @@ export function ImageFillField({
102
104
  try {
103
105
  const uploaded = await onImportAssets(files);
104
106
  const nextImage = uploaded.find((a) => IMAGE_EXT.test(a));
105
- if (nextImage) onCommit(`url("${toProjectRootAssetPath(nextImage)}")`);
107
+ if (nextImage) {
108
+ track("button", "Upload image");
109
+ onCommit(`url("${toProjectRootAssetPath(nextImage)}")`);
110
+ }
106
111
  } finally {
107
112
  setUploading(false);
108
113
  }
@@ -156,6 +161,7 @@ export function ImageFillField({
156
161
  disabled={disabled}
157
162
  onChange={(e) => {
158
163
  const next = e.target.value;
164
+ track("select", "Project asset");
159
165
  if (!next) {
160
166
  onCommit("none");
161
167
  return;
@@ -205,6 +211,7 @@ export function GradientField({
205
211
  disabled?: boolean;
206
212
  onCommit: (nextValue: string) => void;
207
213
  }) {
214
+ const track = useTrackDesignInput();
208
215
  const previewRef = useRef<HTMLDivElement | null>(null);
209
216
  const parsed = parseGradient(value) ?? buildDefaultGradientModel(fallbackColor);
210
217
 
@@ -226,11 +233,13 @@ export function GradientField({
226
233
  ? Math.min(100, (parsed.stops.at(-1)?.position ?? 90) + 10)
227
234
  : 100,
228
235
  );
236
+ track("button", "Add gradient stop");
229
237
  commit(nextGradient);
230
238
  };
231
239
 
232
240
  const removeStop = (index: number) => {
233
241
  if (parsed.stops.length <= 2) return;
242
+ track("button", `Remove gradient stop ${index + 1}`);
234
243
  commit({ ...parsed, stops: parsed.stops.filter((_, i) => i !== index) });
235
244
  };
236
245
 
@@ -263,6 +272,7 @@ export function GradientField({
263
272
  </div>
264
273
  <div className="flex min-w-0 flex-wrap items-center gap-2">
265
274
  <SegmentedControl
275
+ trackName="Gradient type"
266
276
  disabled={disabled}
267
277
  value={parsed.kind}
268
278
  onChange={(next) => patch({ kind: next as GradientModel["kind"] })}
@@ -277,7 +287,10 @@ export function GradientField({
277
287
  type="checkbox"
278
288
  checked={parsed.repeating}
279
289
  disabled={disabled}
280
- onChange={(e) => patch({ repeating: e.target.checked })}
290
+ onChange={(e) => {
291
+ track("toggle", "Repeat gradient");
292
+ patch({ repeating: e.target.checked });
293
+ }}
281
294
  className="h-4 w-4 rounded border-neutral-700 bg-neutral-950 text-panel-accent focus:ring-panel-accent"
282
295
  />
283
296
  Repeat
@@ -285,15 +298,16 @@ export function GradientField({
285
298
  <button
286
299
  type="button"
287
300
  disabled={disabled}
288
- onClick={() =>
301
+ onClick={() => {
302
+ track("button", "Reverse gradient");
289
303
  commit({
290
304
  ...parsed,
291
305
  stops: [...parsed.stops].reverse().map((stop) => ({
292
306
  ...stop,
293
307
  position: 100 - stop.position,
294
308
  })),
295
- })
296
- }
309
+ });
310
+ }}
297
311
  className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white disabled:cursor-not-allowed disabled:text-neutral-600"
298
312
  >
299
313
  <RotateCcw size={12} />
@@ -306,6 +320,7 @@ export function GradientField({
306
320
  <div className="grid gap-1.5">
307
321
  <span className={LABEL}>{parsed.kind === "linear" ? "Angle" : "Start angle"}</span>
308
322
  <SliderControl
323
+ trackName={parsed.kind === "linear" ? "Angle" : "Start angle"}
309
324
  value={parsed.angle}
310
325
  min={0}
311
326
  max={360}
@@ -342,6 +357,7 @@ export function GradientField({
342
357
  <div className="grid min-w-0 gap-1.5">
343
358
  <span className={LABEL}>Center X</span>
344
359
  <SliderControl
360
+ trackName="Center X"
345
361
  value={parsed.centerX}
346
362
  min={0}
347
363
  max={100}
@@ -355,6 +371,7 @@ export function GradientField({
355
371
  <div className="grid min-w-0 gap-1.5">
356
372
  <span className={LABEL}>Center Y</span>
357
373
  <SliderControl
374
+ trackName="Center Y"
358
375
  value={parsed.centerY}
359
376
  min={0}
360
377
  max={100}