@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,293 @@
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 {
7
+ FlatLayoutSection,
8
+ LayoutFlexBlock,
9
+ LayoutGeometryRows,
10
+ LayoutTransform3DBlock,
11
+ LayoutZIndexRow,
12
+ } from "./propertyPanelFlatLayoutSection";
13
+
14
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
15
+
16
+ afterEach(() => {
17
+ document.body.innerHTML = "";
18
+ });
19
+
20
+ function renderInto(node: React.ReactElement) {
21
+ const host = document.createElement("div");
22
+ document.body.append(host);
23
+ const root = createRoot(host);
24
+ act(() => {
25
+ root.render(node);
26
+ });
27
+ return { host, root };
28
+ }
29
+
30
+ function getFlatRowInput(host: HTMLElement, label: string): HTMLInputElement {
31
+ const rows = Array.from(host.querySelectorAll<HTMLElement>(".group"));
32
+ const row = rows.find((el) => el.querySelector("span")?.textContent === label);
33
+ const input = row?.querySelector<HTMLInputElement>("input");
34
+ if (!input) throw new Error(`expected an input for row "${label}"`);
35
+ return input;
36
+ }
37
+
38
+ function baseGeometryProps(overrides: Partial<Parameters<typeof LayoutGeometryRows>[0]> = {}) {
39
+ return {
40
+ element: {} as never,
41
+ displayX: 0,
42
+ displayY: -24,
43
+ displayW: 257.4,
44
+ displayH: 29,
45
+ displayR: 0,
46
+ manualOffsetEditingDisabled: false,
47
+ manualSizeEditingDisabled: false,
48
+ manualRotationEditingDisabled: false,
49
+ commitManualOffset: vi.fn(),
50
+ commitManualSize: vi.fn(),
51
+ commitManualRotation: vi.fn(),
52
+ gsapAnimId: null,
53
+ navKeyframes: null,
54
+ currentPct: 0,
55
+ seekFromKfPct: vi.fn(),
56
+ animIdForProp: (prop: string) => prop,
57
+ onCommitAnimatedProperty: vi.fn(),
58
+ onRemoveKeyframe: vi.fn(),
59
+ onConvertToKeyframes: vi.fn(),
60
+ ...overrides,
61
+ };
62
+ }
63
+
64
+ describe("LayoutGeometryRows", () => {
65
+ it("renders X, Y, W, H, Angle labels and formatted values", () => {
66
+ const { host, root } = renderInto(<LayoutGeometryRows {...baseGeometryProps()} />);
67
+ expect(host.textContent).toContain("X");
68
+ expect(host.textContent).toContain("Y");
69
+ expect(host.textContent).toContain("W");
70
+ expect(host.textContent).toContain("H");
71
+ expect(host.textContent).toContain("Angle");
72
+ expect(getFlatRowInput(host, "W").value).toBe("257.4px");
73
+ expect(getFlatRowInput(host, "Y").value).toBe("-24px");
74
+ act(() => root.unmount());
75
+ });
76
+
77
+ it("commits an X edit through commitManualOffset", () => {
78
+ const commitManualOffset = vi.fn();
79
+ const { host, root } = renderInto(
80
+ <LayoutGeometryRows {...baseGeometryProps({ commitManualOffset })} />,
81
+ );
82
+ const input = host.querySelectorAll("input")[0];
83
+ if (!input) throw new Error("expected an X input");
84
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
85
+ act(() => {
86
+ setter.call(input, "40px");
87
+ input.dispatchEvent(new Event("input", { bubbles: true }));
88
+ input.dispatchEvent(new Event("focusout", { bubbles: true }));
89
+ });
90
+ expect(commitManualOffset).toHaveBeenCalledWith("x", "40px");
91
+ act(() => root.unmount());
92
+ });
93
+
94
+ it("wraps the keyframe gutter cluster at 30% opacity when the property has no keyframes", () => {
95
+ const { host, root } = renderInto(
96
+ <LayoutGeometryRows {...baseGeometryProps({ gsapAnimId: "anim-1", navKeyframes: null })} />,
97
+ );
98
+ const dimmed = host.querySelectorAll('[data-flat-kf-gutter="true"][style*="opacity: 0.3"]');
99
+ expect(dimmed.length).toBeGreaterThan(0);
100
+ act(() => root.unmount());
101
+ });
102
+
103
+ it("does not dim the gutter cluster when the property has keyframes", () => {
104
+ const { host, root } = renderInto(
105
+ <LayoutGeometryRows
106
+ {...baseGeometryProps({
107
+ gsapAnimId: "anim-1",
108
+ navKeyframes: [{ percentage: 0, properties: { x: 0 } }],
109
+ })}
110
+ />,
111
+ );
112
+ const full = host.querySelectorAll('[data-flat-kf-gutter="true"][style*="opacity: 1"]');
113
+ expect(full.length).toBeGreaterThan(0);
114
+ act(() => root.unmount());
115
+ });
116
+
117
+ it("passes the real element/selection (not null) to onCommitAnimatedProperty when adding a keyframe", () => {
118
+ const onCommitAnimatedProperty = vi.fn();
119
+ const element = { id: "el-1" } as unknown as Parameters<
120
+ typeof LayoutGeometryRows
121
+ >[0]["element"];
122
+ const { host, root } = renderInto(
123
+ <LayoutGeometryRows
124
+ {...baseGeometryProps({
125
+ element,
126
+ gsapAnimId: "anim-1",
127
+ navKeyframes: [{ percentage: 50, properties: { x: 5 } }],
128
+ currentPct: 0,
129
+ onCommitAnimatedProperty,
130
+ })}
131
+ />,
132
+ );
133
+ const addButton = host.querySelector('[title="Add x keyframe"]');
134
+ if (!addButton) throw new Error("expected an Add x keyframe button");
135
+ act(() => {
136
+ (addButton as HTMLElement).dispatchEvent(new MouseEvent("click", { bubbles: true }));
137
+ });
138
+ expect(onCommitAnimatedProperty).toHaveBeenCalledWith(element, "x", 0);
139
+ expect(onCommitAnimatedProperty).not.toHaveBeenCalledWith(null, "x", 0);
140
+ act(() => root.unmount());
141
+ });
142
+ });
143
+
144
+ describe("LayoutZIndexRow", () => {
145
+ it("renders the current z-index at the default tier and commits edits", () => {
146
+ const onSetStyle = vi.fn();
147
+ const { host, root } = renderInto(
148
+ <LayoutZIndexRow styles={{ "z-index": "3" }} onSetStyle={onSetStyle} />,
149
+ );
150
+ expect(host.textContent).toContain("Z-index");
151
+ const input = host.querySelector("input");
152
+ if (!input) throw new Error("expected an input");
153
+ expect(input.value).toBe("3");
154
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
155
+ act(() => {
156
+ setter.call(input, "5");
157
+ input.dispatchEvent(new Event("input", { bubbles: true }));
158
+ input.dispatchEvent(new Event("focusout", { bubbles: true }));
159
+ });
160
+ expect(onSetStyle).toHaveBeenCalledWith("z-index", "5");
161
+ act(() => root.unmount());
162
+ });
163
+ });
164
+
165
+ describe("LayoutFlexBlock", () => {
166
+ it("renders nothing when the element is not flex", () => {
167
+ const { host, root } = renderInto(
168
+ <LayoutFlexBlock styles={{ display: "block" }} onSetStyle={vi.fn()} disabled={false} />,
169
+ );
170
+ expect(host.textContent).toBe("");
171
+ act(() => root.unmount());
172
+ });
173
+
174
+ it("renders direction/justify/align/gap and commits a direction change", () => {
175
+ const onSetStyle = vi.fn();
176
+ const { host, root } = renderInto(
177
+ <LayoutFlexBlock
178
+ styles={{ display: "flex", "flex-direction": "row", gap: "8px" }}
179
+ onSetStyle={onSetStyle}
180
+ disabled={false}
181
+ />,
182
+ );
183
+ expect(host.textContent).toContain("Flex");
184
+ const columnOption = Array.from(host.querySelectorAll('[data-flat-segment="true"]')).find(
185
+ (el) => el.textContent === "Column",
186
+ );
187
+ if (!columnOption) throw new Error("expected a Column segment option");
188
+ act(() =>
189
+ (columnOption as HTMLElement).dispatchEvent(new MouseEvent("click", { bubbles: true })),
190
+ );
191
+ expect(onSetStyle).toHaveBeenCalledWith("flex-direction", "column");
192
+ act(() => root.unmount());
193
+ });
194
+ });
195
+
196
+ describe("LayoutTransform3DBlock", () => {
197
+ it("renders the nested 3D transform sub-view", () => {
198
+ const { host, root } = renderInto(
199
+ <LayoutTransform3DBlock
200
+ gsapRuntimeValues={{}}
201
+ gsapAnimId={null}
202
+ gsapKeyframes={null}
203
+ currentPct={0}
204
+ elStart={0}
205
+ elDuration={0}
206
+ element={{} as never}
207
+ onCommitAnimatedProperty={vi.fn()}
208
+ onCommitAnimatedProperties={vi.fn()}
209
+ onSeekToTime={vi.fn()}
210
+ onRemoveKeyframe={vi.fn()}
211
+ onConvertToKeyframes={vi.fn()}
212
+ />,
213
+ );
214
+ // PropertyPanel3dTransform's own internals aren't this task's concern (it's
215
+ // reused unmodified) — just confirm the wrapper mounted something.
216
+ expect(host.children.length).toBeGreaterThan(0);
217
+ act(() => root.unmount());
218
+ });
219
+ });
220
+
221
+ describe("FlatLayoutSection", () => {
222
+ it("renders geometry rows, z-index, flex (when applicable), and the 3D transform block in order", () => {
223
+ const { host, root } = renderInto(
224
+ <FlatLayoutSection
225
+ element={{} as never}
226
+ styles={{ display: "flex", "flex-direction": "row" }}
227
+ onSetStyle={vi.fn()}
228
+ disabled={false}
229
+ displayX={0}
230
+ displayY={0}
231
+ displayW={100}
232
+ displayH={100}
233
+ displayR={0}
234
+ manualOffsetEditingDisabled={false}
235
+ manualSizeEditingDisabled={false}
236
+ manualRotationEditingDisabled={false}
237
+ commitManualOffset={vi.fn()}
238
+ commitManualSize={vi.fn()}
239
+ commitManualRotation={vi.fn()}
240
+ gsapAnimId={null}
241
+ navKeyframes={null}
242
+ currentPct={0}
243
+ seekFromKfPct={vi.fn()}
244
+ animIdForProp={(p) => p}
245
+ gsapRuntimeValues={{}}
246
+ gsapKeyframes={null}
247
+ elStart={0}
248
+ elDuration={0}
249
+ onSeekToTime={vi.fn()}
250
+ />,
251
+ );
252
+ const text = host.textContent ?? "";
253
+ expect(text).toContain("X");
254
+ expect(text).toContain("Z-index");
255
+ expect(text).toContain("Flex");
256
+ expect(text).toContain("3D Transform");
257
+ act(() => root.unmount());
258
+ });
259
+
260
+ it("omits the Flex block for a non-flex element", () => {
261
+ const { host, root } = renderInto(
262
+ <FlatLayoutSection
263
+ element={{} as never}
264
+ styles={{ display: "block" }}
265
+ onSetStyle={vi.fn()}
266
+ disabled={false}
267
+ displayX={0}
268
+ displayY={0}
269
+ displayW={100}
270
+ displayH={100}
271
+ displayR={0}
272
+ manualOffsetEditingDisabled={false}
273
+ manualSizeEditingDisabled={false}
274
+ manualRotationEditingDisabled={false}
275
+ commitManualOffset={vi.fn()}
276
+ commitManualSize={vi.fn()}
277
+ commitManualRotation={vi.fn()}
278
+ gsapAnimId={null}
279
+ navKeyframes={null}
280
+ currentPct={0}
281
+ seekFromKfPct={vi.fn()}
282
+ animIdForProp={(p) => p}
283
+ gsapRuntimeValues={{}}
284
+ gsapKeyframes={null}
285
+ elStart={0}
286
+ elDuration={0}
287
+ onSeekToTime={vi.fn()}
288
+ />,
289
+ );
290
+ expect(host.textContent).not.toContain("Flex");
291
+ act(() => root.unmount());
292
+ });
293
+ });
@@ -0,0 +1,383 @@
1
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
2
+ import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives";
3
+ import { KeyframeNavigation } from "./KeyframeNavigation";
4
+ import { formatPxMetricValue } from "./propertyPanelHelpers";
5
+ import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
6
+ import { resolveValueTier } from "./propertyPanelValueTier";
7
+ import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
8
+ import type { DomEditSelection } from "./domEditingTypes";
9
+
10
+ type KeyframeEntry = Array<{
11
+ percentage: number;
12
+ tweenPercentage?: number;
13
+ properties: Record<string, number | string>;
14
+ ease?: string;
15
+ }> | null;
16
+
17
+ interface GeometryRowsProps {
18
+ element: DomEditSelection;
19
+ displayX: number;
20
+ displayY: number;
21
+ displayW: number;
22
+ displayH: number;
23
+ displayR: number;
24
+ manualOffsetEditingDisabled: boolean;
25
+ manualSizeEditingDisabled: boolean;
26
+ manualRotationEditingDisabled: boolean;
27
+ commitManualOffset: (axis: "x" | "y", value: string) => void;
28
+ commitManualSize: (dimension: "width" | "height", value: string) => void;
29
+ commitManualRotation: (value: string) => void;
30
+ gsapAnimId: string | null;
31
+ navKeyframes: KeyframeEntry;
32
+ currentPct: number;
33
+ seekFromKfPct: (pct: number) => void;
34
+ animIdForProp: (prop: string) => string;
35
+ onCommitAnimatedProperty?: (
36
+ element: DomEditSelection,
37
+ property: string,
38
+ value: number,
39
+ ) => Promise<void>;
40
+ onRemoveKeyframe?: (animId: string, pct: number) => void;
41
+ onConvertToKeyframes?: (animId: string) => void;
42
+ }
43
+
44
+ function KeyframeGutter({
45
+ element,
46
+ property,
47
+ displayValue,
48
+ gsapAnimId,
49
+ navKeyframes,
50
+ currentPct,
51
+ seekFromKfPct,
52
+ animIdForProp,
53
+ onCommitAnimatedProperty,
54
+ onRemoveKeyframe,
55
+ onConvertToKeyframes,
56
+ }: {
57
+ property: string;
58
+ displayValue: number;
59
+ } & Pick<
60
+ GeometryRowsProps,
61
+ | "element"
62
+ | "gsapAnimId"
63
+ | "navKeyframes"
64
+ | "currentPct"
65
+ | "seekFromKfPct"
66
+ | "animIdForProp"
67
+ | "onCommitAnimatedProperty"
68
+ | "onRemoveKeyframe"
69
+ | "onConvertToKeyframes"
70
+ >) {
71
+ const track = useTrackDesignInput();
72
+ if (!STUDIO_KEYFRAMES_ENABLED || !gsapAnimId) return null;
73
+ const hasKeyframesOnProp = Boolean(navKeyframes?.some((kf) => property in kf.properties));
74
+ return (
75
+ <span data-flat-kf-gutter="true" style={{ opacity: hasKeyframesOnProp ? 1 : 0.3 }}>
76
+ <KeyframeNavigation
77
+ property={property}
78
+ keyframes={navKeyframes}
79
+ currentPercentage={currentPct}
80
+ onSeek={seekFromKfPct}
81
+ onAddKeyframe={() => {
82
+ if (!onCommitAnimatedProperty) return;
83
+ track("button", `Add ${property} keyframe`);
84
+ void onCommitAnimatedProperty(element, property, displayValue);
85
+ }}
86
+ onRemoveKeyframe={(pct) => {
87
+ if (!onRemoveKeyframe) return;
88
+ track("button", `Remove ${property} keyframe`);
89
+ onRemoveKeyframe(animIdForProp(property), pct);
90
+ }}
91
+ onConvertToKeyframes={() => {
92
+ if (!onConvertToKeyframes) return;
93
+ track("button", `Convert ${property} to keyframes`);
94
+ onConvertToKeyframes(animIdForProp(property));
95
+ }}
96
+ />
97
+ </span>
98
+ );
99
+ }
100
+
101
+ export function LayoutGeometryRows({
102
+ element,
103
+ displayX,
104
+ displayY,
105
+ displayW,
106
+ displayH,
107
+ displayR,
108
+ manualOffsetEditingDisabled,
109
+ manualSizeEditingDisabled,
110
+ manualRotationEditingDisabled,
111
+ commitManualOffset,
112
+ commitManualSize,
113
+ commitManualRotation,
114
+ gsapAnimId,
115
+ navKeyframes,
116
+ currentPct,
117
+ seekFromKfPct,
118
+ animIdForProp,
119
+ onCommitAnimatedProperty,
120
+ onRemoveKeyframe,
121
+ onConvertToKeyframes,
122
+ }: GeometryRowsProps) {
123
+ const gutterProps = {
124
+ element,
125
+ gsapAnimId,
126
+ navKeyframes,
127
+ currentPct,
128
+ seekFromKfPct,
129
+ animIdForProp,
130
+ onCommitAnimatedProperty,
131
+ onRemoveKeyframe,
132
+ onConvertToKeyframes,
133
+ };
134
+ return (
135
+ <>
136
+ <FlatRow
137
+ label="X"
138
+ value={formatPxMetricValue(displayX)}
139
+ tier={displayX === 0 ? "default" : "explicitCustom"}
140
+ disabled={manualOffsetEditingDisabled}
141
+ onCommit={(next) => commitManualOffset("x", next)}
142
+ suffix={<KeyframeGutter property="x" displayValue={displayX} {...gutterProps} />}
143
+ />
144
+ <FlatRow
145
+ label="Y"
146
+ value={formatPxMetricValue(displayY)}
147
+ tier={displayY === 0 ? "default" : "explicitCustom"}
148
+ disabled={manualOffsetEditingDisabled}
149
+ onCommit={(next) => commitManualOffset("y", next)}
150
+ suffix={<KeyframeGutter property="y" displayValue={displayY} {...gutterProps} />}
151
+ />
152
+ <FlatRow
153
+ label="W"
154
+ value={formatPxMetricValue(displayW)}
155
+ tier="default"
156
+ disabled={manualSizeEditingDisabled}
157
+ onCommit={(next) => commitManualSize("width", next)}
158
+ suffix={<KeyframeGutter property="width" displayValue={displayW} {...gutterProps} />}
159
+ />
160
+ <FlatRow
161
+ label="H"
162
+ value={formatPxMetricValue(displayH)}
163
+ tier="default"
164
+ disabled={manualSizeEditingDisabled}
165
+ onCommit={(next) => commitManualSize("height", next)}
166
+ suffix={<KeyframeGutter property="height" displayValue={displayH} {...gutterProps} />}
167
+ />
168
+ <FlatRow
169
+ label="Angle"
170
+ value={`${displayR}°`}
171
+ tier="default"
172
+ disabled={manualRotationEditingDisabled}
173
+ onCommit={(next) => commitManualRotation(next.replace("°", ""))}
174
+ suffix={<KeyframeGutter property="rotation" displayValue={displayR} {...gutterProps} />}
175
+ />
176
+ </>
177
+ );
178
+ }
179
+
180
+ export function LayoutZIndexRow({
181
+ styles,
182
+ onSetStyle,
183
+ }: {
184
+ styles: Record<string, string>;
185
+ onSetStyle: (prop: string, value: string) => void | Promise<void>;
186
+ }) {
187
+ const zIndex = String(parseInt(styles["z-index"] || "auto", 10) || 0);
188
+ return (
189
+ <FlatRow
190
+ label="Z-index"
191
+ value={zIndex}
192
+ tier="default"
193
+ onCommit={(next) => void onSetStyle("z-index", next)}
194
+ />
195
+ );
196
+ }
197
+
198
+ export function LayoutFlexBlock({
199
+ styles,
200
+ onSetStyle,
201
+ disabled,
202
+ }: {
203
+ styles: Record<string, string>;
204
+ onSetStyle: (prop: string, value: string) => void | Promise<void>;
205
+ disabled: boolean;
206
+ }) {
207
+ const isFlex = styles.display === "flex" || styles.display === "inline-flex";
208
+ if (!isFlex) return null;
209
+ const direction = styles["flex-direction"] || "row";
210
+ return (
211
+ <div className="border-l-2 border-panel-border-input py-0.5 pl-[10px]">
212
+ <div className="mb-[3px] text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
213
+ Flex
214
+ </div>
215
+ <FlatSegmentedRow
216
+ label="Direction"
217
+ options={[
218
+ { key: "row", node: "→ Row", label: "Row", active: direction === "row" },
219
+ { key: "column", node: "Column", label: "Column", active: direction === "column" },
220
+ ]}
221
+ disabled={disabled}
222
+ onChange={(next) => void onSetStyle("flex-direction", next)}
223
+ />
224
+ <FlatSelectRow
225
+ label="Justify"
226
+ value={styles["justify-content"] || "flex-start"}
227
+ tier={resolveValueTier(styles["justify-content"], "flex-start")}
228
+ disabled={disabled}
229
+ options={[
230
+ "flex-start",
231
+ "center",
232
+ "space-between",
233
+ "space-around",
234
+ "space-evenly",
235
+ "flex-end",
236
+ ]}
237
+ onChange={(next) => void onSetStyle("justify-content", next)}
238
+ />
239
+ <FlatSelectRow
240
+ label="Align"
241
+ value={styles["align-items"] || "stretch"}
242
+ tier={resolveValueTier(styles["align-items"], "stretch")}
243
+ disabled={disabled}
244
+ options={["stretch", "flex-start", "center", "flex-end", "baseline"]}
245
+ onChange={(next) => void onSetStyle("align-items", next)}
246
+ />
247
+ <FlatRow
248
+ label="Gap"
249
+ value={styles.gap ?? "0px"}
250
+ tier={resolveValueTier(styles.gap, "0px")}
251
+ disabled={disabled}
252
+ onCommit={(next) => void onSetStyle("gap", next.endsWith("px") ? next : `${next}px`)}
253
+ />
254
+ </div>
255
+ );
256
+ }
257
+
258
+ export function LayoutTransform3DBlock({
259
+ gsapRuntimeValues,
260
+ gsapAnimId,
261
+ resolveAnimIdForProp,
262
+ gsapKeyframes,
263
+ currentPct,
264
+ elStart,
265
+ elDuration,
266
+ element,
267
+ onCommitAnimatedProperty,
268
+ onCommitAnimatedProperties,
269
+ onSeekToTime,
270
+ onRemoveKeyframe,
271
+ onConvertToKeyframes,
272
+ onLivePreviewProps,
273
+ }: {
274
+ gsapRuntimeValues: Record<string, number>;
275
+ gsapAnimId: string | null;
276
+ resolveAnimIdForProp?: (prop: string) => string | null;
277
+ gsapKeyframes: Array<{
278
+ percentage: number;
279
+ properties: Record<string, number | string>;
280
+ ease?: string;
281
+ }> | null;
282
+ currentPct: number;
283
+ elStart: number;
284
+ elDuration: number;
285
+ element: DomEditSelection;
286
+ onCommitAnimatedProperty?: (
287
+ element: DomEditSelection,
288
+ property: string,
289
+ value: number,
290
+ ) => Promise<void>;
291
+ onCommitAnimatedProperties?: (
292
+ element: DomEditSelection,
293
+ props: Record<string, number | string>,
294
+ ) => Promise<void>;
295
+ onSeekToTime?: (time: number) => void;
296
+ onRemoveKeyframe?: (animId: string, pct: number) => void;
297
+ onConvertToKeyframes?: (animId: string, duration?: number) => void;
298
+ onLivePreviewProps?: (element: DomEditSelection, props: Record<string, number>) => void;
299
+ }) {
300
+ return (
301
+ <div className="border-t border-panel-hairline pt-2.5">
302
+ <div className="mb-[3px] text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
303
+ 3D Transform
304
+ </div>
305
+ <PropertyPanel3dTransform
306
+ gsapRuntimeValues={gsapRuntimeValues}
307
+ gsapAnimId={gsapAnimId}
308
+ resolveAnimIdForProp={resolveAnimIdForProp}
309
+ gsapKeyframes={gsapKeyframes}
310
+ currentPct={currentPct}
311
+ elStart={elStart}
312
+ elDuration={elDuration}
313
+ element={element}
314
+ onCommitAnimatedProperty={onCommitAnimatedProperty}
315
+ onCommitAnimatedProperties={onCommitAnimatedProperties}
316
+ onSeekToTime={onSeekToTime}
317
+ onRemoveKeyframe={onRemoveKeyframe}
318
+ onConvertToKeyframes={onConvertToKeyframes}
319
+ onLivePreviewProps={onLivePreviewProps}
320
+ />
321
+ </div>
322
+ );
323
+ }
324
+
325
+ interface FlatLayoutSectionProps
326
+ extends
327
+ Omit<GeometryRowsProps, never>,
328
+ Pick<
329
+ Parameters<typeof LayoutTransform3DBlock>[0],
330
+ | "gsapRuntimeValues"
331
+ | "resolveAnimIdForProp"
332
+ | "gsapKeyframes"
333
+ | "elStart"
334
+ | "elDuration"
335
+ | "onCommitAnimatedProperties"
336
+ | "onSeekToTime"
337
+ | "onLivePreviewProps"
338
+ > {
339
+ element: DomEditSelection;
340
+ styles: Record<string, string>;
341
+ onSetStyle: (prop: string, value: string) => void | Promise<void>;
342
+ disabled: boolean;
343
+ }
344
+
345
+ export function FlatLayoutSection({
346
+ element,
347
+ styles,
348
+ onSetStyle,
349
+ disabled,
350
+ gsapRuntimeValues,
351
+ resolveAnimIdForProp,
352
+ gsapKeyframes,
353
+ elStart,
354
+ elDuration,
355
+ onCommitAnimatedProperties,
356
+ onSeekToTime,
357
+ onLivePreviewProps,
358
+ ...geometry
359
+ }: FlatLayoutSectionProps) {
360
+ return (
361
+ <div className="space-y-1.5">
362
+ <LayoutGeometryRows element={element} {...geometry} />
363
+ <LayoutZIndexRow styles={styles} onSetStyle={onSetStyle} />
364
+ <LayoutFlexBlock styles={styles} onSetStyle={onSetStyle} disabled={disabled} />
365
+ <LayoutTransform3DBlock
366
+ gsapRuntimeValues={gsapRuntimeValues}
367
+ gsapAnimId={geometry.gsapAnimId}
368
+ resolveAnimIdForProp={resolveAnimIdForProp}
369
+ gsapKeyframes={gsapKeyframes}
370
+ currentPct={geometry.currentPct}
371
+ elStart={elStart}
372
+ elDuration={elDuration}
373
+ element={element}
374
+ onCommitAnimatedProperty={geometry.onCommitAnimatedProperty}
375
+ onCommitAnimatedProperties={onCommitAnimatedProperties}
376
+ onSeekToTime={onSeekToTime}
377
+ onRemoveKeyframe={geometry.onRemoveKeyframe}
378
+ onConvertToKeyframes={geometry.onConvertToKeyframes}
379
+ onLivePreviewProps={onLivePreviewProps}
380
+ />
381
+ </div>
382
+ );
383
+ }