@hyperframes/studio 0.7.58 → 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 (156) hide show
  1. package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-Bm07FLkl.js} +1 -1
  2. package/dist/assets/{index-C47jAC3Q.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-DeQPzqwH.js → index-FvzmPhfG.js} +1 -1
  6. package/dist/index.d.ts +12 -3
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +13514 -8528
  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 +3 -3
  15. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  16. package/src/components/StudioRightPanel.tsx +29 -51
  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 +101 -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/VariablePromoteContext.test.tsx +67 -0
  96. package/src/contexts/VariablePromoteContext.tsx +10 -5
  97. package/src/hooks/domEditCommitRunner.ts +11 -0
  98. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  99. package/src/hooks/timelineTrackVisibility.test.ts +52 -0
  100. package/src/hooks/timelineTrackVisibility.ts +27 -10
  101. package/src/hooks/useAppHotkeys.ts +9 -0
  102. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  103. package/src/hooks/useDomEditCommits.ts +29 -175
  104. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  105. package/src/hooks/useDomEditSession.ts +17 -5
  106. package/src/hooks/useDomEditTextCommits.ts +14 -9
  107. package/src/hooks/useDomEditWiring.ts +10 -9
  108. package/src/hooks/useElementLifecycleOps.ts +8 -3
  109. package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
  110. package/src/hooks/useFileManager.ts +82 -60
  111. package/src/hooks/useGsapAnimationOps.ts +7 -6
  112. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  113. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  114. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  115. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  116. package/src/hooks/useGsapScriptCommits.ts +150 -50
  117. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  118. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  119. package/src/hooks/useInspectorSplitResize.ts +51 -0
  120. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  121. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  122. package/src/hooks/usePersistentEditHistory.ts +71 -31
  123. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  124. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  125. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  126. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  127. package/src/hooks/useSdkSession.ts +155 -14
  128. package/src/hooks/useSlideshowPersist.ts +7 -1
  129. package/src/hooks/useStoryboard.ts +23 -4
  130. package/src/hooks/useTimelineEditing.test.tsx +4 -1
  131. package/src/hooks/useTimelineEditing.ts +16 -10
  132. package/src/hooks/useTimelineEditingTypes.ts +3 -0
  133. package/src/hooks/useTimelineGroupEditing.ts +16 -2
  134. package/src/hooks/useVariablesPersist.ts +9 -7
  135. package/src/icons/SystemIcons.tsx +2 -0
  136. package/src/styles/studio.css +22 -0
  137. package/src/styles/tailwind-preset.shared.js +7 -0
  138. package/src/utils/designInputTracking.test.ts +97 -0
  139. package/src/utils/designInputTracking.ts +80 -0
  140. package/src/utils/sdkCutover.gate.test.ts +7 -7
  141. package/src/utils/sdkCutover.test.ts +490 -57
  142. package/src/utils/sdkCutover.ts +181 -219
  143. package/src/utils/sdkEditTransaction.ts +292 -0
  144. package/src/utils/sdkResolverAttempts.ts +94 -0
  145. package/src/utils/sdkResolverShadow.test.ts +148 -0
  146. package/src/utils/sdkResolverShadow.ts +139 -106
  147. package/src/utils/setSlideshowManifest.test.ts +0 -9
  148. package/src/utils/setSlideshowManifest.ts +20 -27
  149. package/src/utils/studioFileHistory.test.ts +41 -0
  150. package/src/utils/studioFileHistory.ts +30 -27
  151. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  152. package/src/utils/studioHelpers.test.ts +45 -0
  153. package/src/utils/studioHelpers.ts +45 -14
  154. package/src/utils/studioSaveDiagnostics.ts +17 -0
  155. package/dist/assets/index-B_UvTX3E.js +0 -423
  156. package/dist/assets/index-uahwWkgw.css +0 -1
@@ -12,11 +12,14 @@ declare namespace studioPreset {
12
12
  }
13
13
  let panel: {
14
14
  bg: string;
15
+ "bg-inset": string;
15
16
  input: string;
16
17
  surface: string;
17
18
  hover: string;
18
19
  border: string;
19
20
  "border-input": string;
21
+ hairline: string;
22
+ "text-0": string;
20
23
  "text-1": string;
21
24
  "text-2": string;
22
25
  "text-3": string;
@@ -24,6 +27,8 @@ declare namespace studioPreset {
24
27
  "text-5": string;
25
28
  accent: string;
26
29
  danger: string;
30
+ media: string;
31
+ container: string;
27
32
  };
28
33
  }
29
34
  }
@@ -13,18 +13,25 @@ var studioPreset = {
13
13
  },
14
14
  panel: {
15
15
  bg: "#0C0C0E",
16
+ // Open inspector-section body — slightly lighter than headers (bg)
17
+ // so the recessed scrollable region reads distinct.
18
+ "bg-inset": "#121214",
16
19
  input: "#161618",
17
20
  surface: "#18181B",
18
21
  hover: "#27272A",
19
22
  border: "#1E1E1E",
20
23
  "border-input": "#27272A",
24
+ hairline: "#1A1A1C",
25
+ "text-0": "#FAFAFA",
21
26
  "text-1": "#E4E4E7",
22
27
  "text-2": "#A1A1AA",
23
28
  "text-3": "#71717A",
24
29
  "text-4": "#52525B",
25
30
  "text-5": "#3F3F46",
26
31
  accent: "#3CE6AC",
27
- danger: "#EF4444"
32
+ danger: "#EF4444",
33
+ media: "#00E3FF",
34
+ container: "#F5A623"
28
35
  }
29
36
  }
30
37
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/styles/tailwind-preset.shared.js","../../src/styles/tailwind-preset.ts"],"sourcesContent":["const studioPreset = {\n theme: {\n extend: {\n colors: {\n studio: {\n bg: \"#0a0a0a\",\n surface: \"#141414\",\n border: \"#262626\",\n text: \"#e5e5e5\",\n muted: \"#737373\",\n accent: \"#3CE6AC\",\n },\n panel: {\n bg: \"#0C0C0E\",\n input: \"#161618\",\n surface: \"#18181B\",\n hover: \"#27272A\",\n border: \"#1E1E1E\",\n \"border-input\": \"#27272A\",\n \"text-1\": \"#E4E4E7\",\n \"text-2\": \"#A1A1AA\",\n \"text-3\": \"#71717A\",\n \"text-4\": \"#52525B\",\n \"text-5\": \"#3F3F46\",\n accent: \"#3CE6AC\",\n danger: \"#EF4444\",\n },\n },\n },\n },\n plugins: [],\n};\n\nexport default studioPreset;\n","import studioPreset from \"./tailwind-preset.shared.js\";\n\nexport default studioPreset;\n"],"mappings":";AAAA,IAAM,eAAe;AAAA,EACnB,OAAO;AAAA,IACL,QAAQ;AAAA,MACN,QAAQ;AAAA,QACN,QAAQ;AAAA,UACN,IAAI;AAAA,UACJ,SAAS;AAAA,UACT,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,QACA,OAAO;AAAA,UACL,IAAI;AAAA,UACJ,OAAO;AAAA,UACP,SAAS;AAAA,UACT,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,gBAAgB;AAAA,UAChB,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,CAAC;AACZ;AAEA,IAAO,iCAAQ;;;AC/Bf,IAAO,0BAAQ;","names":[]}
1
+ {"version":3,"sources":["../../src/styles/tailwind-preset.shared.js","../../src/styles/tailwind-preset.ts"],"sourcesContent":["const studioPreset = {\n theme: {\n extend: {\n colors: {\n studio: {\n bg: \"#0a0a0a\",\n surface: \"#141414\",\n border: \"#262626\",\n text: \"#e5e5e5\",\n muted: \"#737373\",\n accent: \"#3CE6AC\",\n },\n panel: {\n bg: \"#0C0C0E\",\n // Open inspector-section body — slightly lighter than headers (bg)\n // so the recessed scrollable region reads distinct.\n \"bg-inset\": \"#121214\",\n input: \"#161618\",\n surface: \"#18181B\",\n hover: \"#27272A\",\n border: \"#1E1E1E\",\n \"border-input\": \"#27272A\",\n hairline: \"#1A1A1C\",\n \"text-0\": \"#FAFAFA\",\n \"text-1\": \"#E4E4E7\",\n \"text-2\": \"#A1A1AA\",\n \"text-3\": \"#71717A\",\n \"text-4\": \"#52525B\",\n \"text-5\": \"#3F3F46\",\n accent: \"#3CE6AC\",\n danger: \"#EF4444\",\n media: \"#00E3FF\",\n container: \"#F5A623\",\n },\n },\n },\n },\n plugins: [],\n};\n\nexport default studioPreset;\n","import studioPreset from \"./tailwind-preset.shared.js\";\n\nexport default studioPreset;\n"],"mappings":";AAAA,IAAM,eAAe;AAAA,EACnB,OAAO;AAAA,IACL,QAAQ;AAAA,MACN,QAAQ;AAAA,QACN,QAAQ;AAAA,UACN,IAAI;AAAA,UACJ,SAAS;AAAA,UACT,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,QACA,OAAO;AAAA,UACL,IAAI;AAAA;AAAA;AAAA,UAGJ,YAAY;AAAA,UACZ,OAAO;AAAA,UACP,SAAS;AAAA,UACT,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,gBAAgB;AAAA,UAChB,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,WAAW;AAAA,QACb;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,CAAC;AACZ;AAEA,IAAO,iCAAQ;;;ACtCf,IAAO,0BAAQ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.7.58",
3
+ "version": "0.7.59",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,11 +46,11 @@
46
46
  "gsap": "^3.13.0",
47
47
  "marked": "^14.1.4",
48
48
  "mediabunny": "^1.45.3",
49
- "@hyperframes/core": "0.7.58",
50
- "@hyperframes/parsers": "0.7.58",
51
- "@hyperframes/player": "0.7.58",
52
- "@hyperframes/sdk": "0.7.58",
53
- "@hyperframes/studio-server": "0.7.58"
49
+ "@hyperframes/core": "0.7.59",
50
+ "@hyperframes/studio-server": "0.7.59",
51
+ "@hyperframes/sdk": "0.7.59",
52
+ "@hyperframes/parsers": "0.7.59",
53
+ "@hyperframes/player": "0.7.59"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "19",
@@ -65,7 +65,7 @@
65
65
  "vite": "^6.4.2",
66
66
  "vitest": "^3.2.4",
67
67
  "zustand": "^5.0.0",
68
- "@hyperframes/producer": "0.7.58"
68
+ "@hyperframes/producer": "0.7.59"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19",
package/src/App.tsx CHANGED
@@ -71,14 +71,12 @@ export function StudioApp() {
71
71
  const { projectId, resolving, waitingForServer } = useServerConnection();
72
72
  const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
73
73
  const viewModeValue = useViewModeState();
74
-
75
74
  useEffect(() => {
76
75
  if (resolving || waitingForServer) return;
77
76
  if (hasFiredSessionStart()) return;
78
77
  markSessionStartFired();
79
78
  trackStudioSessionStart({ has_project: projectId != null });
80
79
  }, [projectId, resolving, waitingForServer]);
81
-
82
80
  const [activeCompPath, setActiveCompPath] = useState<string | null>(null);
83
81
  const [activeCompPathHydrated, setActiveCompPathHydrated] = useState(
84
82
  () => initialUrlStateRef.current.activeCompPath == null,
@@ -176,6 +174,7 @@ export function StudioApp() {
176
174
  uploadProjectFiles: fileManager.uploadProjectFiles,
177
175
  isRecordingRef: isGestureRecordingRef,
178
176
  sdkSession: editFlowSdkSession,
177
+ publishSdkSession: sdkHandle.publish,
179
178
  forceReloadSdkSession: sdkHandle.forceReload,
180
179
  handleDomZIndexReorderCommitRef,
181
180
  });
@@ -317,6 +316,7 @@ export function StudioApp() {
317
316
  selectSidebarTab: sidebarTabRef.current.select,
318
317
  getSidebarTab: sidebarTabRef.current.get,
319
318
  sdkSession: editFlowSdkSession,
319
+ publishSdkSession: sdkHandle.publish,
320
320
  forceReloadSdkSession: sdkHandle.forceReload,
321
321
  });
322
322
  domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
@@ -331,7 +331,6 @@ export function StudioApp() {
331
331
  domEditSession.domEditSelection,
332
332
  domEditSession.domEditGroupSelections,
333
333
  );
334
-
335
334
  useCaptionDetection({
336
335
  projectId,
337
336
  activeCompPath,
@@ -533,6 +532,7 @@ export function StudioApp() {
533
532
  recordingDuration={gestureRecording.recordingDuration}
534
533
  onToggleRecording={recordingToggle}
535
534
  sdkSession={sdkHandle.session}
535
+ publishSdkSession={sdkHandle.publish}
536
536
  reloadPreview={reloadPreview}
537
537
  domEditSaveTimestampRef={domEditSaveTimestampRef}
538
538
  recordEdit={editHistory.recordEdit}
@@ -9,11 +9,12 @@
9
9
  * composition, so behavior there is unchanged.
10
10
  */
11
11
 
12
- import type { ReactNode } from "react";
12
+ import { useCallback, type ReactNode } from "react";
13
13
  import type { DomEditSelection } from "./editor/domEditingTypes";
14
14
  import { useSdkSession } from "../hooks/useSdkSession";
15
15
  import { useVariablesPersist, type UseVariablesPersistParams } from "../hooks/useVariablesPersist";
16
16
  import { VariablePromoteProvider } from "../contexts/VariablePromoteContext";
17
+ import { getStudioSaveErrorMessage } from "../utils/studioSaveDiagnostics";
17
18
 
18
19
  /** Persist wiring minus the target — this provider derives the target from the selection. */
19
20
  type PersistDeps = Omit<UseVariablesPersistParams, "sdkSession" | "activeCompPath">;
@@ -22,12 +23,14 @@ export function DesignPanelPromoteProvider({
22
23
  selection,
23
24
  projectId,
24
25
  activeCompPath,
26
+ showToast,
25
27
  children,
26
28
  ...persistDeps
27
29
  }: PersistDeps & {
28
30
  selection: DomEditSelection | null;
29
31
  projectId: string | null;
30
32
  activeCompPath: string | null;
33
+ showToast: (message: string, tone?: "error" | "info") => void;
31
34
  children: ReactNode;
32
35
  }) {
33
36
  const targetPath = selection?.sourceFile || activeCompPath || "index.html";
@@ -35,10 +38,20 @@ export function DesignPanelPromoteProvider({
35
38
  const persist = useVariablesPersist({
36
39
  ...persistDeps,
37
40
  sdkSession: handle.session,
41
+ publishSdkSession: handle.publish,
38
42
  activeCompPath: targetPath,
39
43
  });
44
+ const handlePersistError = useCallback(
45
+ (error: unknown) => showToast(getStudioSaveErrorMessage(error), "error"),
46
+ [showToast],
47
+ );
40
48
  return (
41
- <VariablePromoteProvider session={handle.session} selection={selection} persist={persist}>
49
+ <VariablePromoteProvider
50
+ session={handle.session}
51
+ selection={selection}
52
+ persist={persist}
53
+ onPersistError={handlePersistError}
54
+ >
42
55
  {children}
43
56
  </VariablePromoteProvider>
44
57
  );
@@ -1,12 +1,4 @@
1
- import {
2
- useCallback,
3
- useEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- type MutableRefObject,
8
- type PointerEvent as ReactPointerEvent,
9
- } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, type MutableRefObject } from "react";
10
2
  import { PropertyPanel } from "./editor/PropertyPanel";
11
3
  import { LayersPanel } from "./editor/LayersPanel";
12
4
  import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
@@ -23,7 +15,7 @@ import type { IframeWindow } from "../player/lib/playbackTypes";
23
15
  import { STUDIO_INSPECTOR_PANELS_ENABLED } from "./editor/manualEditingAvailability";
24
16
  import type { Composition } from "@hyperframes/sdk";
25
17
  import type { EditHistoryKind } from "../utils/editHistory";
26
- import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
18
+ import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
27
19
  import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
28
20
 
29
21
  import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
@@ -38,9 +30,8 @@ import {
38
30
  type ColorGradingScope,
39
31
  } from "./studioColorGradingScope";
40
32
  import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
41
-
42
- const MIN_INSPECTOR_SPLIT_PERCENT = 20;
43
- const MAX_INSPECTOR_SPLIT_PERCENT = 75;
33
+ import { timelineKeysForSelections, type ToggleHiddenHandler } from "../utils/studioHelpers";
34
+ import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
44
35
 
45
36
  export interface StudioRightPanelProps {
46
37
  designPanelActive: boolean;
@@ -56,6 +47,7 @@ export interface StudioRightPanelProps {
56
47
  onToggleRecording?: () => void;
57
48
  /** Dependencies for the Slideshow persist callback, threaded from App.tsx. */
58
49
  sdkSession: Composition | null;
50
+ publishSdkSession: NonNullable<UseSlideshowPersistParams["publishSdkSession"]>;
59
51
  reloadPreview: () => void;
60
52
  domEditSaveTimestampRef: MutableRefObject<number>;
61
53
  recordEdit: (entry: {
@@ -63,7 +55,7 @@ export interface StudioRightPanelProps {
63
55
  kind: EditHistoryKind;
64
56
  files: Record<string, { before: string; after: string }>;
65
57
  }) => Promise<void>;
66
- onToggleElementHidden?: (elementKey: string, hidden: boolean) => Promise<void> | void;
58
+ onToggleElementHidden?: ToggleHiddenHandler;
67
59
  }
68
60
 
69
61
  // fallow-ignore-next-line complexity
@@ -75,6 +67,7 @@ export function StudioRightPanel({
75
67
  recordingDuration,
76
68
  onToggleRecording,
77
69
  sdkSession,
70
+ publishSdkSession,
78
71
  reloadPreview,
79
72
  domEditSaveTimestampRef,
80
73
  recordEdit,
@@ -109,10 +102,12 @@ export function StudioRightPanel({
109
102
  copiedAgentPrompt,
110
103
  clearDomSelection,
111
104
  handleUngroupSelection,
105
+ handleGroupSelection,
112
106
  handleDomStyleCommit,
113
107
  handleDomAttributeCommit,
114
108
  handleDomAttributeLiveCommit,
115
109
  handleDomHtmlAttributeCommit,
110
+ handleDomAttributesCommit,
116
111
  handleDomPathOffsetCommit,
117
112
  handleDomBoxSizeCommit,
118
113
  handleDomRotationCommit,
@@ -167,6 +162,7 @@ export function StudioRightPanel({
167
162
  recordEdit,
168
163
  reloadPreview,
169
164
  domEditSaveTimestampRef,
165
+ publishSdkSession,
170
166
  });
171
167
 
172
168
  // Notes path: persists are debounced in SlideshowPanel; coalesceKey ensures
@@ -179,16 +175,17 @@ export function StudioRightPanel({
179
175
  recordEdit,
180
176
  reloadPreview,
181
177
  domEditSaveTimestampRef,
178
+ publishSdkSession,
182
179
  coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
183
180
  });
184
181
 
185
- const [layersPanePercent, setLayersPanePercent] = useState(40);
186
- const splitContainerRef = useRef<HTMLDivElement>(null);
187
- const splitDragRef = useRef<{
188
- startY: number;
189
- startPercent: number;
190
- height: number;
191
- } | null>(null);
182
+ const {
183
+ layersPanePercent,
184
+ splitContainerRef,
185
+ handleInspectorSplitResizeStart,
186
+ handleInspectorSplitResizeMove,
187
+ handleInspectorSplitResizeEnd,
188
+ } = useInspectorSplitResize();
192
189
  const backgroundRemovalAbortRef = useRef<AbortController | null>(null);
193
190
 
194
191
  useEffect(
@@ -229,35 +226,6 @@ export function StudioRightPanel({
229
226
  toggleRightInspectorPane(pane);
230
227
  };
231
228
 
232
- const handleInspectorSplitResizeStart = useCallback(
233
- (event: ReactPointerEvent<HTMLDivElement>) => {
234
- event.preventDefault();
235
- event.currentTarget.setPointerCapture(event.pointerId);
236
- const height = splitContainerRef.current?.getBoundingClientRect().height ?? 0;
237
- splitDragRef.current = {
238
- startY: event.clientY,
239
- startPercent: layersPanePercent,
240
- height,
241
- };
242
- },
243
- [layersPanePercent],
244
- );
245
-
246
- const handleInspectorSplitResizeMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
247
- const drag = splitDragRef.current;
248
- if (!drag || drag.height <= 0) return;
249
- const deltaPercent = ((event.clientY - drag.startY) / drag.height) * 100;
250
- const next = Math.min(
251
- MAX_INSPECTOR_SPLIT_PERCENT,
252
- Math.max(MIN_INSPECTOR_SPLIT_PERCENT, drag.startPercent + deltaPercent),
253
- );
254
- setLayersPanePercent(next);
255
- }, []);
256
-
257
- const handleInspectorSplitResizeEnd = useCallback(() => {
258
- splitDragRef.current = null;
259
- }, []);
260
-
261
229
  const handleApplyColorGradingScope = useCallback(
262
230
  async (scope: ColorGradingScope, value: string | null) =>
263
231
  applyColorGradingScopeUpdate({
@@ -341,12 +309,17 @@ export function StudioRightPanel({
341
309
  },
342
310
  [projectId, refreshFileTree, showToast],
343
311
  );
344
-
312
+ const handleHideAllSelected = () => {
313
+ const { elements } = usePlayerStore.getState();
314
+ const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath);
315
+ if (keys.length > 0) void onToggleElementHidden?.(keys, true);
316
+ };
345
317
  const propertyPanel = (
346
318
  <DesignPanelPromoteProvider
347
319
  selection={domEditGroupSelections.length > 1 ? null : domEditSelection}
348
320
  projectId={projectId}
349
321
  activeCompPath={activeCompPath}
322
+ showToast={showToast}
350
323
  readProjectFile={readProjectFile}
351
324
  writeProjectFile={writeProjectFile}
352
325
  recordEdit={recordEdit}
@@ -359,12 +332,16 @@ export function StudioRightPanel({
359
332
  assets={assets}
360
333
  element={domEditGroupSelections.length > 1 ? null : domEditSelection}
361
334
  multiSelectCount={domEditGroupSelections.length}
335
+ multiSelectedElements={domEditGroupSelections}
336
+ onGroupSelection={handleGroupSelection}
337
+ onHideAllSelected={handleHideAllSelected}
362
338
  copiedAgentPrompt={copiedAgentPrompt}
363
339
  onClearSelection={clearDomSelection}
364
340
  onToggleElementHidden={onToggleElementHidden}
365
341
  onUngroup={handleUngroupSelection}
366
342
  onSetStyle={handleDomStyleCommit}
367
343
  onSetAttribute={handleDomAttributeCommit}
344
+ onSetAttributes={handleDomAttributesCommit}
368
345
  onSetAttributeLive={handleDomAttributeLiveCommit}
369
346
  onApplyColorGradingScope={handleApplyColorGradingScope}
370
347
  onSetHtmlAttribute={handleDomHtmlAttributeCommit}
@@ -534,6 +511,7 @@ export function StudioRightPanel({
534
511
  ) : rightPanelTab === "variables" ? (
535
512
  <VariablesPanel
536
513
  sdkSession={sdkSession}
514
+ publishSdkSession={publishSdkSession}
537
515
  reloadPreview={reloadPreview}
538
516
  domEditSaveTimestampRef={domEditSaveTimestampRef}
539
517
  recordEdit={recordEdit}
@@ -0,0 +1,134 @@
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 { AnimationCard } from "./AnimationCard";
7
+ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
8
+
9
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
10
+
11
+ afterEach(() => {
12
+ document.body.innerHTML = "";
13
+ });
14
+
15
+ function baseAnimation(overrides: Partial<GsapAnimation> = {}): GsapAnimation {
16
+ return {
17
+ id: "anim-1",
18
+ method: "to",
19
+ position: 0.8,
20
+ duration: 1.2,
21
+ ease: "power2.out",
22
+ properties: { opacity: 1 },
23
+ ...overrides,
24
+ } as GsapAnimation;
25
+ }
26
+
27
+ const noop = () => {};
28
+
29
+ describe("AnimationCard flat branch", () => {
30
+ it("renders a mint border-left and panel-token colors when flat", () => {
31
+ const host = document.createElement("div");
32
+ document.body.append(host);
33
+ const root = createRoot(host);
34
+ act(() => {
35
+ root.render(
36
+ <AnimationCard
37
+ animation={baseAnimation()}
38
+ defaultExpanded={false}
39
+ flat
40
+ onUpdateProperty={noop}
41
+ onUpdateMeta={noop}
42
+ onDeleteAnimation={noop}
43
+ onAddProperty={noop}
44
+ onRemoveProperty={noop}
45
+ />,
46
+ );
47
+ });
48
+ const card = host.querySelector('[data-flat-effect-card="true"]');
49
+ expect(card).not.toBeNull();
50
+ expect(card?.className).toContain("border-panel-accent");
51
+ act(() => root.unmount());
52
+ });
53
+
54
+ it("still renders the legacy (non-flat) appearance when flat is omitted", () => {
55
+ const host = document.createElement("div");
56
+ document.body.append(host);
57
+ const root = createRoot(host);
58
+ act(() => {
59
+ root.render(
60
+ <AnimationCard
61
+ animation={baseAnimation()}
62
+ defaultExpanded={false}
63
+ onUpdateProperty={noop}
64
+ onUpdateMeta={noop}
65
+ onDeleteAnimation={noop}
66
+ onAddProperty={noop}
67
+ onRemoveProperty={noop}
68
+ />,
69
+ );
70
+ });
71
+ expect(host.querySelector('[data-flat-effect-card="true"]')).toBeNull();
72
+ expect(host.textContent).toContain("power2.out");
73
+ act(() => root.unmount());
74
+ });
75
+
76
+ it("toggles expanded state when the collapsed header button is clicked, in both modes", () => {
77
+ for (const flat of [false, true]) {
78
+ const host = document.createElement("div");
79
+ document.body.append(host);
80
+ const root = createRoot(host);
81
+ act(() => {
82
+ root.render(
83
+ <AnimationCard
84
+ animation={baseAnimation()}
85
+ defaultExpanded={false}
86
+ flat={flat || undefined}
87
+ onUpdateProperty={noop}
88
+ onUpdateMeta={noop}
89
+ onDeleteAnimation={noop}
90
+ onAddProperty={noop}
91
+ onRemoveProperty={noop}
92
+ />,
93
+ );
94
+ });
95
+ expect(host.textContent).not.toContain("Remove");
96
+ const button = host.querySelector("button");
97
+ expect(button).not.toBeNull();
98
+ act(() => {
99
+ button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
100
+ });
101
+ expect(host.textContent).toContain("Remove");
102
+ act(() => root.unmount());
103
+ }
104
+ });
105
+
106
+ it("invokes onDeleteAnimation with the animation id when Remove is clicked, in flat mode", () => {
107
+ const onDeleteAnimation = vi.fn();
108
+ const host = document.createElement("div");
109
+ document.body.append(host);
110
+ const root = createRoot(host);
111
+ act(() => {
112
+ root.render(
113
+ <AnimationCard
114
+ animation={baseAnimation()}
115
+ defaultExpanded={true}
116
+ flat
117
+ onUpdateProperty={noop}
118
+ onUpdateMeta={noop}
119
+ onDeleteAnimation={onDeleteAnimation}
120
+ onAddProperty={noop}
121
+ onRemoveProperty={noop}
122
+ />,
123
+ );
124
+ });
125
+ const buttons = Array.from(host.querySelectorAll("button"));
126
+ const removeButton = buttons.find((b) => b.textContent === "Remove");
127
+ expect(removeButton).not.toBeUndefined();
128
+ act(() => {
129
+ removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
130
+ });
131
+ expect(onDeleteAnimation).toHaveBeenCalledWith("anim-1");
132
+ act(() => root.unmount());
133
+ });
134
+ });
@@ -21,12 +21,14 @@ import {
21
21
  interface AnimationCardProps extends GsapAnimationEditCallbacks {
22
22
  animation: GsapAnimation;
23
23
  defaultExpanded: boolean;
24
+ flat?: boolean;
24
25
  }
25
26
 
26
27
  // fallow-ignore-next-line complexity
27
28
  export const AnimationCard = memo(function AnimationCard({
28
29
  animation,
29
30
  defaultExpanded,
31
+ flat,
30
32
  onUpdateProperty,
31
33
  onUpdateMeta,
32
34
  onDeleteAnimation,
@@ -150,7 +152,14 @@ export const AnimationCard = memo(function AnimationCard({
150
152
  );
151
153
 
152
154
  return (
153
- <div className="border-b border-neutral-800 pb-3">
155
+ <div
156
+ data-flat-effect-card={flat ? "true" : undefined}
157
+ className={
158
+ flat
159
+ ? "border-b border-l-2 border-panel-accent/40 border-b-panel-hairline pb-3 pl-2"
160
+ : "border-b border-neutral-800 pb-3"
161
+ }
162
+ >
154
163
  <button
155
164
  type="button"
156
165
  onClick={() => setExpanded((v) => !v)}
@@ -162,13 +171,19 @@ export const AnimationCard = memo(function AnimationCard({
162
171
  >
163
172
  {methodLabel}
164
173
  </span>
165
- <span className="text-[11px] font-medium text-neutral-400" title="When this effect plays">
174
+ <span
175
+ className={`text-[11px] font-medium ${flat ? "text-panel-text-3" : "text-neutral-400"}`}
176
+ title="When this effect plays"
177
+ >
166
178
  {typeof animation.position === "number"
167
179
  ? `${parseFloat(animation.position.toFixed(3))}s`
168
180
  : animation.position}{" "}
169
181
  – {typeof endTime === "number" ? `${parseFloat(endTime.toFixed(3))}s` : endTime}
170
182
  </span>
171
- <span className="ml-auto text-[10px] text-neutral-500" title={easeName}>
183
+ <span
184
+ className={`ml-auto text-[10px] ${flat ? "text-panel-text-3" : "text-neutral-500"}`}
185
+ title={easeName}
186
+ >
172
187
  {easeLabel}
173
188
  </span>
174
189
  <svg
@@ -176,7 +191,7 @@ export const AnimationCard = memo(function AnimationCard({
176
191
  height="10"
177
192
  viewBox="0 0 10 10"
178
193
  fill="currentColor"
179
- className={`flex-shrink-0 text-neutral-500 transition-transform ${expanded ? "" : "-rotate-90"}`}
194
+ className={`flex-shrink-0 transition-transform ${flat ? "text-panel-text-5" : "text-neutral-500"} ${expanded ? "" : "-rotate-90"}`}
180
195
  >
181
196
  <path d="M2 3l3 4 3-4z" />
182
197
  </svg>
@@ -113,6 +113,7 @@ export const ArcPathControls = memo(function ArcPathControls({
113
113
  )}
114
114
  </div>
115
115
  <SliderControl
116
+ trackName={segmentCount === 1 ? "Curviness" : `Segment ${i + 1} curviness`}
116
117
  value={seg.curviness}
117
118
  min={0}
118
119
  max={3}
@@ -1,3 +1,5 @@
1
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
2
+
1
3
  export type GestureRecordingState = "idle" | "recording" | "preview";
2
4
 
3
5
  interface GestureRecordIconProps {
@@ -28,13 +30,17 @@ export function GestureRecordPanelButton({
28
30
  onToggleRecording,
29
31
  }: GestureRecordPanelButtonProps) {
30
32
  const recording = recordingState === "recording";
33
+ const track = useTrackDesignInput();
31
34
 
32
35
  return (
33
36
  <div className="px-4 pb-3">
34
37
  <button
35
38
  type="button"
36
39
  onMouseDown={(e) => e.preventDefault()}
37
- onClick={onToggleRecording}
40
+ onClick={() => {
41
+ track("button", "Gesture recording");
42
+ onToggleRecording();
43
+ }}
38
44
  className={`w-full flex items-center justify-center gap-2 rounded-lg py-2 text-[11px] font-medium transition-colors ${
39
45
  recording
40
46
  ? "bg-red-500/15 text-red-400 border border-red-500/30 animate-pulse"