@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
@@ -0,0 +1,587 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act, type ReactElement } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
7
+ import { __resetDesignInputThrottle } from "../../utils/designInputTracking";
8
+ import type { PropertyPanelProps } from "./propertyPanelHelpers";
9
+ import { ColorField } from "./propertyPanelColor";
10
+ import { FontFamilyField } from "./propertyPanelFont";
11
+ import {
12
+ FlatRow,
13
+ FlatSegmentedRow,
14
+ FlatSelectRow,
15
+ FlatSlider,
16
+ } from "./propertyPanelFlatPrimitives";
17
+ import { FlatToggle } from "./propertyPanelFlatToggle";
18
+ import {
19
+ DetailField,
20
+ MetricField,
21
+ Section,
22
+ SegmentedControl,
23
+ SelectField,
24
+ SliderControl,
25
+ } from "./propertyPanelPrimitives";
26
+ import { TextAreaField } from "./propertyPanelSections";
27
+
28
+ const trackStudioEvent = vi.hoisted(() => vi.fn());
29
+
30
+ vi.mock("../../utils/studioTelemetry", () => ({
31
+ trackStudioEvent: (...args: unknown[]) => trackStudioEvent(...args),
32
+ }));
33
+
34
+ vi.mock("../../contexts/StudioContext", async () => {
35
+ const actual = await vi.importActual<typeof import("../../contexts/StudioContext")>(
36
+ "../../contexts/StudioContext",
37
+ );
38
+ return { ...actual, useStudioShellContext: () => ({ showToast: vi.fn() }) };
39
+ });
40
+
41
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
42
+
43
+ let roots: Root[] = [];
44
+
45
+ beforeEach(() => {
46
+ trackStudioEvent.mockReset();
47
+ __resetDesignInputThrottle();
48
+ vi.stubGlobal(
49
+ "fetch",
50
+ vi.fn(async () => new Response(JSON.stringify([]), { status: 200 })),
51
+ );
52
+ });
53
+
54
+ afterEach(() => {
55
+ for (const root of roots) act(() => root.unmount());
56
+ roots = [];
57
+ document.body.innerHTML = "";
58
+ vi.useRealTimers();
59
+ vi.doUnmock("./manualEditingAvailability");
60
+ vi.resetModules();
61
+ vi.unstubAllGlobals();
62
+ });
63
+
64
+ function render(ui: ReactElement): HTMLElement {
65
+ const host = document.createElement("div");
66
+ document.body.append(host);
67
+ const root = createRoot(host);
68
+ roots.push(root);
69
+ act(() => root.render(ui));
70
+ return host;
71
+ }
72
+
73
+ function changeInput(input: HTMLInputElement, value: string) {
74
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
75
+ if (!setter) throw new Error("expected native input value setter");
76
+ setter.call(input, value);
77
+ input.dispatchEvent(new Event("input", { bubbles: true }));
78
+ }
79
+
80
+ function changeTextarea(textarea: HTMLTextAreaElement, value: string) {
81
+ const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
82
+ if (!setter) throw new Error("expected native textarea value setter");
83
+ setter.call(textarea, value);
84
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
85
+ }
86
+
87
+ function blurInput(input: HTMLInputElement) {
88
+ input.focus();
89
+ input.blur();
90
+ }
91
+
92
+ function expectTracked(control: string, name: string, section = "style") {
93
+ expect(trackStudioEvent).toHaveBeenLastCalledWith("design_input", {
94
+ ui: "classic",
95
+ section,
96
+ control,
97
+ name,
98
+ });
99
+ }
100
+
101
+ function expectFlatTracked(control: string, name: string, section = "layout") {
102
+ expect(trackStudioEvent).toHaveBeenLastCalledWith("design_input", {
103
+ ui: "flat",
104
+ section,
105
+ control,
106
+ name,
107
+ });
108
+ }
109
+
110
+ function flatSection(children: ReactElement) {
111
+ return (
112
+ <DesignPanelInputProvider ui="flat" section="layout">
113
+ {children}
114
+ </DesignPanelInputProvider>
115
+ );
116
+ }
117
+
118
+ function classicSection(children: ReactElement) {
119
+ return (
120
+ <DesignPanelInputProvider ui="classic">
121
+ <Section title="Style" icon={null}>
122
+ {children}
123
+ </Section>
124
+ </DesignPanelInputProvider>
125
+ );
126
+ }
127
+
128
+ describe("classic property-panel primitive telemetry", () => {
129
+ it("tracks MetricField only when a changed value commits", () => {
130
+ const onCommit = vi.fn();
131
+ const host = render(
132
+ classicSection(<MetricField label="Opacity" value="20" onCommit={onCommit} />),
133
+ );
134
+ const input = host.querySelector("input");
135
+ if (!input) throw new Error("expected metric input");
136
+
137
+ act(() => blurInput(input));
138
+ expect(trackStudioEvent).not.toHaveBeenCalled();
139
+
140
+ act(() => {
141
+ changeInput(input, "40");
142
+ });
143
+ act(() => blurInput(input));
144
+
145
+ expect(onCommit).toHaveBeenCalledWith("40");
146
+ expectTracked("metric", "opacity");
147
+ });
148
+
149
+ it("tracks SliderControl on settle, not on its scheduled commit tick", () => {
150
+ vi.useFakeTimers();
151
+ const onCommit = vi.fn();
152
+ const host = render(
153
+ classicSection(
154
+ <SliderControl
155
+ trackName="Opacity"
156
+ value={20}
157
+ min={0}
158
+ max={100}
159
+ step={1}
160
+ displayValue="20%"
161
+ onCommit={onCommit}
162
+ />,
163
+ ),
164
+ );
165
+ const input = host.querySelector<HTMLInputElement>('input[type="range"]');
166
+ if (!input) throw new Error("expected slider input");
167
+
168
+ act(() => {
169
+ changeInput(input, "40");
170
+ });
171
+ act(() => vi.advanceTimersByTime(40));
172
+ expect(trackStudioEvent).not.toHaveBeenCalled();
173
+
174
+ act(() => input.dispatchEvent(new MouseEvent("mouseup", { bubbles: true })));
175
+ expectTracked("slider", "opacity");
176
+ });
177
+
178
+ it("tracks SelectField with its label", () => {
179
+ const host = render(
180
+ classicSection(
181
+ <SelectField
182
+ label="Blend mode"
183
+ value="normal"
184
+ options={["normal", "multiply"]}
185
+ onChange={vi.fn()}
186
+ />,
187
+ ),
188
+ );
189
+ const select = host.querySelector("select");
190
+ if (!select) throw new Error("expected select");
191
+ act(() => {
192
+ select.value = "multiply";
193
+ select.dispatchEvent(new Event("change", { bubbles: true }));
194
+ });
195
+ expectTracked("select", "blend-mode");
196
+ });
197
+
198
+ it("tracks DetailField with its label", () => {
199
+ const host = render(
200
+ classicSection(<DetailField label="External URL" value="old.png" onCommit={vi.fn()} />),
201
+ );
202
+ const input = host.querySelector("input");
203
+ if (!input) throw new Error("expected detail input");
204
+ act(() => changeInput(input, "new.png"));
205
+ act(() => blurInput(input));
206
+ expectTracked("text", "external-url");
207
+ });
208
+
209
+ it("tracks SegmentedControl with its explicit name", () => {
210
+ const host = render(
211
+ classicSection(
212
+ <SegmentedControl
213
+ trackName="Fill type"
214
+ value="solid"
215
+ options={[
216
+ { label: "Solid", value: "solid" },
217
+ { label: "Gradient", value: "gradient" },
218
+ ]}
219
+ onChange={vi.fn()}
220
+ />,
221
+ ),
222
+ );
223
+ const gradient = Array.from(host.querySelectorAll("button")).find(
224
+ (button) => button.textContent === "Gradient",
225
+ );
226
+ if (!gradient) throw new Error("expected Gradient segment");
227
+ act(() => gradient.dispatchEvent(new MouseEvent("click", { bubbles: true })));
228
+ expectTracked("segmented", "fill-type");
229
+ });
230
+ });
231
+
232
+ describe("flat property-panel primitive telemetry", () => {
233
+ it("tracks FlatRow commits with its label", () => {
234
+ const host = render(
235
+ flatSection(<FlatRow label="Z-index" value="1" tier="default" onCommit={vi.fn()} />),
236
+ );
237
+ const input = host.querySelector("input");
238
+ if (!input) throw new Error("expected flat row input");
239
+
240
+ act(() => changeInput(input, "2"));
241
+ act(() => blurInput(input));
242
+
243
+ expectFlatTracked("metric", "z-index");
244
+ });
245
+
246
+ it("tracks FlatSlider once on pointer settle, not during drag commits", () => {
247
+ const host = render(
248
+ flatSection(
249
+ <FlatSlider
250
+ label="Opacity"
251
+ value={10}
252
+ min={0}
253
+ max={100}
254
+ tier="explicitCustom"
255
+ displayValue="10%"
256
+ onCommit={vi.fn()}
257
+ />,
258
+ ),
259
+ );
260
+ const slider = host.querySelector<HTMLElement>('[data-flat-slider-track="true"]');
261
+ if (!slider) throw new Error("expected flat slider");
262
+ Object.defineProperty(slider, "getBoundingClientRect", {
263
+ value: () => ({ left: 0, width: 100, top: 0, height: 20, right: 100, bottom: 20 }),
264
+ });
265
+
266
+ act(() => {
267
+ slider.dispatchEvent(
268
+ new PointerEvent("pointerdown", { bubbles: true, clientX: 20, pointerId: 1 }),
269
+ );
270
+ slider.dispatchEvent(
271
+ new PointerEvent("pointermove", { bubbles: true, clientX: 80, pointerId: 1 }),
272
+ );
273
+ });
274
+ expect(trackStudioEvent).not.toHaveBeenCalled();
275
+
276
+ act(() => {
277
+ slider.dispatchEvent(
278
+ new PointerEvent("pointerup", { bubbles: true, clientX: 80, pointerId: 1 }),
279
+ );
280
+ });
281
+ expect(trackStudioEvent).toHaveBeenCalledTimes(1);
282
+ expectFlatTracked("slider", "opacity");
283
+ });
284
+
285
+ it("tracks FlatSegmentedRow changes with its label", () => {
286
+ const host = render(
287
+ flatSection(
288
+ <FlatSegmentedRow
289
+ label="Direction"
290
+ options={[
291
+ { key: "row", node: "Row", label: "Row", active: true },
292
+ { key: "column", node: "Column", label: "Column", active: false },
293
+ ]}
294
+ onChange={vi.fn()}
295
+ />,
296
+ ),
297
+ );
298
+ const column = host.querySelector<HTMLButtonElement>('[aria-label="Column"]');
299
+ act(() => column?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
300
+ expectFlatTracked("segmented", "direction");
301
+ });
302
+
303
+ it("tracks FlatToggle changes with its label", () => {
304
+ const host = render(
305
+ flatSection(<FlatToggle label="Loop" checked={false} onChange={vi.fn()} />),
306
+ );
307
+ const toggle = host.querySelector<HTMLButtonElement>('[data-flat-toggle="true"]');
308
+ act(() => toggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
309
+ expectFlatTracked("toggle", "loop");
310
+ });
311
+
312
+ it("tracks FlatSelectRow changes with its accessible label", () => {
313
+ const host = render(
314
+ flatSection(
315
+ <FlatSelectRow
316
+ label=""
317
+ ariaLabel="Preset"
318
+ value="neutral"
319
+ options={["neutral", "warm"]}
320
+ tier="default"
321
+ onChange={vi.fn()}
322
+ />,
323
+ ),
324
+ );
325
+ const select = host.querySelector("select");
326
+ if (!select) throw new Error("expected flat select");
327
+ act(() => {
328
+ select.value = "warm";
329
+ select.dispatchEvent(new Event("change", { bubbles: true }));
330
+ });
331
+ expectFlatTracked("select", "preset");
332
+ });
333
+ });
334
+
335
+ describe.each(["classic", "flat"] as const)("shared %s input telemetry", (ui) => {
336
+ const section = (children: ReactElement) => (
337
+ <DesignPanelInputProvider ui={ui} section="text">
338
+ {children}
339
+ </DesignPanelInputProvider>
340
+ );
341
+
342
+ it("tracks ColorField exactly once for a real color change", () => {
343
+ const host = render(
344
+ section(<ColorField flat={ui === "flat"} label="Color" value="#FF0000" onCommit={vi.fn()} />),
345
+ );
346
+ const trigger = host.querySelector<HTMLButtonElement>('[aria-label="Pick color color"]');
347
+ act(() => trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
348
+ const hex = Array.from(document.body.querySelectorAll<HTMLInputElement>("input")).find(
349
+ (input) => input.value === "#FF0000",
350
+ );
351
+ if (!hex) throw new Error("expected color hex input");
352
+ act(() => changeInput(hex, "#00FF00"));
353
+
354
+ expect(trackStudioEvent).toHaveBeenCalledTimes(1);
355
+ expect(trackStudioEvent).toHaveBeenLastCalledWith("design_input", {
356
+ ui,
357
+ section: "text",
358
+ control: "color",
359
+ name: "color",
360
+ });
361
+ });
362
+
363
+ it("tracks FontFamilyField exactly once for a real selection", () => {
364
+ const host = render(
365
+ section(
366
+ <FontFamilyField
367
+ flat={ui === "flat"}
368
+ value="Arial"
369
+ importedFonts={[]}
370
+ onCommit={vi.fn()}
371
+ />,
372
+ ),
373
+ );
374
+ const trigger = host.querySelector<HTMLButtonElement>(
375
+ ui === "flat" ? '[data-flat-font-trigger="true"]' : "button",
376
+ );
377
+ act(() => trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
378
+ const option = Array.from(host.querySelectorAll<HTMLButtonElement>("button")).find((button) =>
379
+ button.textContent?.includes("sans-serif"),
380
+ );
381
+ if (!option) throw new Error("expected sans-serif font option");
382
+ act(() => option.dispatchEvent(new MouseEvent("click", { bubbles: true })));
383
+
384
+ expect(trackStudioEvent).toHaveBeenCalledTimes(1);
385
+ expect(trackStudioEvent).toHaveBeenLastCalledWith("design_input", {
386
+ ui,
387
+ section: "text",
388
+ control: "select",
389
+ name: "font-family",
390
+ });
391
+ });
392
+
393
+ it("tracks TextAreaField exactly once across scheduled commit and blur", () => {
394
+ vi.useFakeTimers();
395
+ const host = render(
396
+ section(
397
+ <TextAreaField flat={ui === "flat"} label="Content" value="Before" onCommit={vi.fn()} />,
398
+ ),
399
+ );
400
+ const textarea = host.querySelector("textarea");
401
+ if (!textarea) throw new Error("expected text area");
402
+ act(() => changeTextarea(textarea, "After"));
403
+ act(() => vi.advanceTimersByTime(120));
404
+ act(() => {
405
+ textarea.focus();
406
+ textarea.blur();
407
+ });
408
+
409
+ expect(trackStudioEvent).toHaveBeenCalledTimes(1);
410
+ expect(trackStudioEvent).toHaveBeenLastCalledWith("design_input", {
411
+ ui,
412
+ section: "text",
413
+ control: "text",
414
+ name: "content",
415
+ });
416
+ });
417
+ });
418
+
419
+ function representativeElement() {
420
+ return {
421
+ element: document.createElement("div"),
422
+ id: "panel-target",
423
+ selector: "#panel-target",
424
+ label: "Panel Target",
425
+ tagName: "div",
426
+ sourceFile: "index.html",
427
+ compositionPath: "index.html",
428
+ isCompositionHost: false,
429
+ isInsideLockedComposition: false,
430
+ boundingBox: { x: 0, y: 0, width: 320, height: 180 },
431
+ textContent: "",
432
+ dataAttributes: {},
433
+ inlineStyles: {},
434
+ computedStyles: {},
435
+ textFields: [],
436
+ capabilities: {
437
+ canSelect: true,
438
+ canEditStyles: false,
439
+ canCrop: true,
440
+ canMove: true,
441
+ canResize: true,
442
+ canApplyManualOffset: true,
443
+ canApplyManualSize: true,
444
+ canApplyManualRotation: true,
445
+ },
446
+ };
447
+ }
448
+
449
+ describe("classic PropertyPanel input coverage", () => {
450
+ it("emits only named, known-section events across body inputs and header/footer chrome", async () => {
451
+ const { PropertyPanel } = await import("./PropertyPanel");
452
+ const host = render(
453
+ <PropertyPanel
454
+ {...({
455
+ element: representativeElement(),
456
+ assets: [],
457
+ onSetStyle: vi.fn(),
458
+ onSetText: vi.fn(),
459
+ onSetAttributeLive: vi.fn(),
460
+ onSetManualOffset: vi.fn(),
461
+ onSetManualSize: vi.fn(),
462
+ onSetManualRotation: vi.fn(),
463
+ onClearSelection: vi.fn(),
464
+ onAskAgent: vi.fn(),
465
+ onToggleElementHidden: vi.fn(),
466
+ recordingState: "idle",
467
+ onToggleRecording: vi.fn(),
468
+ } as unknown as PropertyPanelProps)}
469
+ />,
470
+ );
471
+
472
+ // Fire every body text input across the WHOLE panel (not just the layout
473
+ // section): a section rendered without a <DesignPanelInputProvider section="X">
474
+ // would surface here as section "unknown" and fail the invariant below.
475
+ const bodyInputs = Array.from(host.querySelectorAll<HTMLInputElement>('input[type="text"]'));
476
+ expect(bodyInputs.length).toBeGreaterThan(0);
477
+ for (const [index, input] of bodyInputs.entries()) {
478
+ act(() => changeInput(input, String(100 + index)));
479
+ act(() => blurInput(input));
480
+ }
481
+
482
+ // Header + footer chrome — the classic siblings of the flat header/footer.
483
+ // (Copy is skipped: its handler reaches for the clipboard, unavailable here.
484
+ // The visibility toggle is store-gated on a live selection this unit mock does
485
+ // not model; Clear selection already exercises the header section.)
486
+ const clear = host.querySelector<HTMLButtonElement>('[aria-label="Clear selection"]');
487
+ if (!clear) throw new Error("expected classic Clear selection control");
488
+ act(() => clear.dispatchEvent(new MouseEvent("click", { bubbles: true })));
489
+ const recordButton = Array.from(host.querySelectorAll("button")).find((b) =>
490
+ b.textContent?.includes("Record gesture"),
491
+ );
492
+ if (!recordButton) throw new Error("expected classic gesture record button");
493
+ act(() => recordButton.dispatchEvent(new MouseEvent("click", { bubbles: true })));
494
+
495
+ expect(trackStudioEvent).toHaveBeenCalled();
496
+ const sections = new Set<string>();
497
+ for (const [, payload] of trackStudioEvent.mock.calls) {
498
+ expect(payload.ui).toBe("classic");
499
+ expect(payload.name).not.toBe("");
500
+ expect(payload.name).not.toBe("unnamed");
501
+ expect(payload.section).not.toBe("");
502
+ expect(payload.section).not.toBe("unknown");
503
+ sections.add(payload.section as string);
504
+ }
505
+ // A body section plus both chrome regions: proves coverage beyond one section
506
+ // and that classic chrome is wired in parallel with the flat header/footer.
507
+ expect(sections.has("header")).toBe(true);
508
+ expect(sections.has("footer")).toBe(true);
509
+ expect(sections.size).toBeGreaterThan(2);
510
+ });
511
+ });
512
+
513
+ describe("flat PropertyPanel input coverage", () => {
514
+ it("emits only named flat events from known sections for every visible layout input", async () => {
515
+ vi.resetModules();
516
+ vi.doMock("./manualEditingAvailability", async () => {
517
+ const actual = await vi.importActual<typeof import("./manualEditingAvailability")>(
518
+ "./manualEditingAvailability",
519
+ );
520
+ return { ...actual, STUDIO_FLAT_INSPECTOR_ENABLED: true };
521
+ });
522
+ const { PropertyPanel } = await import("./PropertyPanel");
523
+ const host = render(
524
+ <PropertyPanel
525
+ {...({
526
+ element: representativeElement(),
527
+ assets: [],
528
+ onSetStyle: vi.fn(),
529
+ onSetText: vi.fn(),
530
+ onSetAttributeLive: vi.fn(),
531
+ onSetManualOffset: vi.fn(),
532
+ onSetManualSize: vi.fn(),
533
+ onSetManualRotation: vi.fn(),
534
+ // Header/footer controls render only when their callbacks are wired —
535
+ // supply them so the coverage guard exercises the header + footer sections.
536
+ selectedElementId: "el-1",
537
+ selectedElementHidden: false,
538
+ onToggleElementHidden: vi.fn(),
539
+ onCopyElementInfo: vi.fn(),
540
+ onClearSelection: vi.fn(),
541
+ onAskAgent: vi.fn(),
542
+ onToggleRecording: vi.fn(),
543
+ } as unknown as PropertyPanelProps)}
544
+ />,
545
+ );
546
+ const layout = host.querySelector('[data-flat-group-open="true"]');
547
+ if (!layout || !layout.textContent?.includes("Layout")) {
548
+ throw new Error("expected open flat Layout group");
549
+ }
550
+ const inputs = Array.from(layout.querySelectorAll<HTMLInputElement>('input[type="text"]'));
551
+ expect(inputs.length).toBeGreaterThan(0);
552
+
553
+ for (const [index, input] of inputs.entries()) {
554
+ act(() => changeInput(input, String(200 + index)));
555
+ act(() => blurInput(input));
556
+ }
557
+ // Header (Clear selection) and footer (ask + record) controls — exercises the
558
+ // "header" and "footer" sections. The visibility toggle is intentionally omitted:
559
+ // it renders only when the dispatcher forwards a live selection handle, which this
560
+ // unit-level mock does not model. Clear selection already covers the header section.
561
+ for (const selector of [
562
+ '[aria-label="Clear selection"]',
563
+ '[data-flat-footer-ask="true"]',
564
+ '[data-flat-footer-record="true"]',
565
+ ]) {
566
+ const button = host.querySelector<HTMLButtonElement>(selector);
567
+ if (!button) throw new Error(`expected flat panel control ${selector}`);
568
+ act(() => button.dispatchEvent(new MouseEvent("click", { bubbles: true })));
569
+ }
570
+
571
+ expect(trackStudioEvent).toHaveBeenCalled();
572
+ expect(new Set(trackStudioEvent.mock.calls.map(([, payload]) => payload.section))).toEqual(
573
+ new Set(["layout", "header", "footer"]),
574
+ );
575
+ for (const [, payload] of trackStudioEvent.mock.calls) {
576
+ expect(payload).toEqual(
577
+ expect.objectContaining({
578
+ ui: "flat",
579
+ }),
580
+ );
581
+ expect(payload.name).not.toBe("");
582
+ expect(payload.name).not.toBe("unnamed");
583
+ expect(payload.section).not.toBe("");
584
+ expect(payload.section).not.toBe("unknown");
585
+ }
586
+ });
587
+ });
@@ -12,6 +12,7 @@ import {
12
12
  stripQueryAndHash,
13
13
  } from "./propertyPanelHelpers";
14
14
  import { Section, SegmentedControl, SelectField, SliderControl } from "./propertyPanelPrimitives";
15
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
15
16
 
16
17
  // fallow-ignore-next-line complexity
17
18
  export function MediaSection({
@@ -38,6 +39,7 @@ export function MediaSection({
38
39
  },
39
40
  ) => Promise<BackgroundRemovalResult>;
40
41
  }) {
42
+ const track = useTrackDesignInput();
41
43
  const isVideo = element.tagName === "video";
42
44
  const isAudio = element.tagName === "audio";
43
45
  const isImage = element.tagName === "img";
@@ -98,6 +100,7 @@ export function MediaSection({
98
100
 
99
101
  const runBackgroundRemoval = async () => {
100
102
  if (!onRemoveBackground || !projectSrc || removeBusy) return;
103
+ track("button", "Remove background");
101
104
  setRemoveBusy(true);
102
105
  setRemoveProgress({ status: "processing", progress: 0, stage: "Preparing" });
103
106
  try {
@@ -194,6 +197,7 @@ export function MediaSection({
194
197
  <div className="grid min-w-0 gap-1.5">
195
198
  <span className={LABEL}>BG plate</span>
196
199
  <SegmentedControl
200
+ trackName="BG plate"
197
201
  value={createPlate ? "on" : "off"}
198
202
  onChange={(next) => setCreatePlate(next === "on")}
199
203
  options={[
@@ -245,6 +249,7 @@ export function MediaSection({
245
249
  <div className="grid min-w-0 gap-1.5">
246
250
  <span className={LABEL}>Volume</span>
247
251
  <SliderControl
252
+ trackName="Volume"
248
253
  value={volumePercent}
249
254
  min={0}
250
255
  max={100}
@@ -260,6 +265,7 @@ export function MediaSection({
260
265
  <div className="grid min-w-0 gap-1.5">
261
266
  <span className={LABEL}>Playback rate</span>
262
267
  <SliderControl
268
+ trackName="Playback rate"
263
269
  value={playbackRate * 100}
264
270
  min={25}
265
271
  max={300}
@@ -275,6 +281,7 @@ export function MediaSection({
275
281
  <div className="grid min-w-0 gap-1.5">
276
282
  <span className={LABEL}>Media start</span>
277
283
  <SliderControl
284
+ trackName="Media start"
278
285
  value={Math.round(mediaStart * 100)}
279
286
  min={0}
280
287
  max={mediaStartMax * 100}
@@ -291,6 +298,7 @@ export function MediaSection({
291
298
  <div className="grid min-w-0 gap-1.5">
292
299
  <span className={LABEL}>Loop</span>
293
300
  <SegmentedControl
301
+ trackName="Loop"
294
302
  value={hasLoop ? "on" : "off"}
295
303
  onChange={(next) => {
296
304
  void onSetHtmlAttribute("loop", next === "on" ? "true" : null);
@@ -304,6 +312,7 @@ export function MediaSection({
304
312
  <div className="grid min-w-0 gap-1.5">
305
313
  <span className={LABEL}>Muted</span>
306
314
  <SegmentedControl
315
+ trackName="Muted"
307
316
  value={hasMuted ? "on" : "off"}
308
317
  onChange={(next) => {
309
318
  void onSetHtmlAttribute("muted", next === "on" ? "true" : null);
@@ -320,6 +329,7 @@ export function MediaSection({
320
329
  <div className="grid min-w-0 gap-1.5">
321
330
  <span className={LABEL}>Has audio track</span>
322
331
  <SegmentedControl
332
+ trackName="Has audio track"
323
333
  value={hasAudio ? "yes" : "no"}
324
334
  onChange={(next) => {
325
335
  if (next === "yes") {