@hyperframes/studio 0.7.58 → 0.7.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
  2. package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
  3. package/dist/assets/index-DXbu6IPT.css +1 -0
  4. package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
  5. package/dist/assets/index-cH6NfVV_.js +426 -0
  6. package/dist/index.d.ts +12 -3
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +13755 -8627
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.d.ts +5 -0
  11. package/dist/styles/tailwind-preset.js +8 -1
  12. package/dist/styles/tailwind-preset.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +4 -4
  15. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  16. package/src/components/StudioRightPanel.tsx +42 -53
  17. package/src/components/editor/AnimationCard.test.tsx +134 -0
  18. package/src/components/editor/AnimationCard.tsx +19 -4
  19. package/src/components/editor/ArcPathControls.tsx +1 -0
  20. package/src/components/editor/GestureRecordControl.tsx +7 -1
  21. package/src/components/editor/GsapAnimationSection.tsx +109 -14
  22. package/src/components/editor/InspectorHeaderActions.tsx +42 -4
  23. package/src/components/editor/PropertyPanel.test.tsx +948 -0
  24. package/src/components/editor/PropertyPanel.tsx +144 -148
  25. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  26. package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
  27. package/src/components/editor/PropertyPanelFlat.tsx +580 -0
  28. package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
  29. package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
  30. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
  31. package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
  32. package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
  33. package/src/components/editor/gsapLivePreview.ts +34 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +10 -0
  35. package/src/components/editor/manualEditingAvailability.ts +9 -0
  36. package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
  37. package/src/components/editor/propertyPanelColor.test.tsx +28 -0
  38. package/src/components/editor/propertyPanelColor.tsx +31 -1
  39. package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
  40. package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
  41. package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
  42. package/src/components/editor/propertyPanelFill.tsx +22 -5
  43. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
  44. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
  45. package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
  46. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
  47. package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
  48. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
  49. package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
  50. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
  51. package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
  52. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
  53. package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
  54. package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
  55. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
  56. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
  57. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
  58. package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
  59. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
  60. package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
  61. package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
  62. package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
  63. package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
  64. package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
  65. package/src/components/editor/propertyPanelFont.test.tsx +30 -0
  66. package/src/components/editor/propertyPanelFont.tsx +136 -95
  67. package/src/components/editor/propertyPanelHelpers.ts +73 -0
  68. package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
  69. package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
  70. package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
  71. package/src/components/editor/propertyPanelSections.test.tsx +161 -0
  72. package/src/components/editor/propertyPanelSections.tsx +154 -90
  73. package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
  74. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  75. package/src/components/editor/propertyPanelTypes.ts +14 -1
  76. package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
  77. package/src/components/editor/propertyPanelValueTier.ts +28 -0
  78. package/src/components/editor/useColorGradingController.test.ts +418 -0
  79. package/src/components/editor/useColorGradingController.ts +596 -0
  80. package/src/components/panels/VariablesPanel.tsx +4 -0
  81. package/src/components/sidebar/CompositionsTab.test.ts +28 -2
  82. package/src/components/sidebar/CompositionsTab.tsx +3 -1
  83. package/src/components/storyboard/FramePoster.tsx +24 -4
  84. package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
  85. package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
  86. package/src/components/storyboard/StoryboardGrid.tsx +23 -1
  87. package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
  88. package/src/components/storyboard/StoryboardView.tsx +9 -2
  89. package/src/components/storyboard/frameComments.test.ts +95 -0
  90. package/src/components/storyboard/frameComments.ts +115 -0
  91. package/src/components/storyboard/useFrameComments.ts +82 -0
  92. package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
  93. package/src/contexts/DesignPanelInputContext.tsx +48 -0
  94. package/src/contexts/DomEditContext.tsx +4 -0
  95. package/src/contexts/FileManagerContext.tsx +3 -0
  96. package/src/contexts/PanelLayoutContext.tsx +3 -0
  97. package/src/contexts/VariablePromoteContext.test.tsx +67 -0
  98. package/src/contexts/VariablePromoteContext.tsx +10 -5
  99. package/src/hooks/domEditCommitRunner.ts +11 -0
  100. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  101. package/src/hooks/timelineEditingHelpers.ts +2 -2
  102. package/src/hooks/timelineTrackVisibility.test.ts +52 -0
  103. package/src/hooks/timelineTrackVisibility.ts +27 -10
  104. package/src/hooks/useAppHotkeys.ts +9 -0
  105. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  106. package/src/hooks/useDomEditCommits.test.tsx +41 -1
  107. package/src/hooks/useDomEditCommits.ts +31 -177
  108. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  109. package/src/hooks/useDomEditSession.ts +18 -6
  110. package/src/hooks/useDomEditTextCommits.ts +14 -9
  111. package/src/hooks/useDomEditWiring.ts +10 -9
  112. package/src/hooks/useEditorSave.ts +1 -1
  113. package/src/hooks/useElementLifecycleOps.ts +8 -3
  114. package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
  115. package/src/hooks/useFileManager.ts +151 -65
  116. package/src/hooks/useGsapAnimationOps.ts +7 -6
  117. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  118. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  119. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  120. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  121. package/src/hooks/useGsapScriptCommits.ts +150 -50
  122. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  123. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  124. package/src/hooks/useInspectorSplitResize.ts +51 -0
  125. package/src/hooks/usePanelLayout.test.ts +118 -0
  126. package/src/hooks/usePanelLayout.ts +24 -1
  127. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  128. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  129. package/src/hooks/usePersistentEditHistory.ts +71 -31
  130. package/src/hooks/usePreviewPersistence.ts +4 -1
  131. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  132. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  133. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  134. package/src/hooks/useRazorSplit.history.test.tsx +12 -4
  135. package/src/hooks/useRazorSplit.test.tsx +88 -0
  136. package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
  137. package/src/hooks/useRazorSplit.ts +50 -16
  138. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  139. package/src/hooks/useSdkSession.ts +155 -14
  140. package/src/hooks/useSlideshowPersist.ts +7 -1
  141. package/src/hooks/useStoryboard.ts +23 -4
  142. package/src/hooks/useTimelineEditing.test.tsx +4 -1
  143. package/src/hooks/useTimelineEditing.ts +18 -10
  144. package/src/hooks/useTimelineEditingTypes.ts +5 -1
  145. package/src/hooks/useTimelineGroupEditing.ts +17 -3
  146. package/src/hooks/useVariablesPersist.ts +9 -7
  147. package/src/icons/SystemIcons.tsx +2 -0
  148. package/src/styles/studio.css +22 -0
  149. package/src/styles/tailwind-preset.shared.js +7 -0
  150. package/src/utils/designInputTracking.test.ts +97 -0
  151. package/src/utils/designInputTracking.ts +80 -0
  152. package/src/utils/domEditSaveQueue.test.ts +19 -0
  153. package/src/utils/domEditSaveQueue.ts +2 -1
  154. package/src/utils/sdkCutover.gate.test.ts +7 -7
  155. package/src/utils/sdkCutover.test.ts +511 -62
  156. package/src/utils/sdkCutover.ts +181 -219
  157. package/src/utils/sdkEditTransaction.ts +293 -0
  158. package/src/utils/sdkResolverAttempts.ts +94 -0
  159. package/src/utils/sdkResolverShadow.test.ts +148 -0
  160. package/src/utils/sdkResolverShadow.ts +139 -106
  161. package/src/utils/setSlideshowManifest.test.ts +0 -9
  162. package/src/utils/setSlideshowManifest.ts +20 -27
  163. package/src/utils/studioFileHistory.test.ts +41 -0
  164. package/src/utils/studioFileHistory.ts +34 -29
  165. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  166. package/src/utils/studioFileVersion.test.ts +36 -0
  167. package/src/utils/studioFileVersion.ts +23 -0
  168. package/src/utils/studioHelpers.test.ts +45 -0
  169. package/src/utils/studioHelpers.ts +45 -14
  170. package/src/utils/studioSaveDiagnostics.test.ts +18 -0
  171. package/src/utils/studioSaveDiagnostics.ts +38 -0
  172. package/dist/assets/index-B_UvTX3E.js +0 -423
  173. package/dist/assets/index-uahwWkgw.css +0 -1
@@ -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.60",
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.60",
50
+ "@hyperframes/player": "0.7.60",
51
+ "@hyperframes/sdk": "0.7.60",
52
+ "@hyperframes/parsers": "0.7.60",
53
+ "@hyperframes/studio-server": "0.7.60"
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.60"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19",
package/src/App.tsx CHANGED
@@ -64,21 +64,18 @@ import {
64
64
  } from "./utils/studioUrlState";
65
65
  import { trackStudioSessionStart } from "./telemetry/events";
66
66
  import { hasFiredSessionStart, markSessionStartFired } from "./telemetry/config";
67
-
68
67
  type CanvasRect = { left: number; top: number; width: number; height: number };
69
68
  // fallow-ignore-next-line complexity
70
69
  export function StudioApp() {
71
70
  const { projectId, resolving, waitingForServer } = useServerConnection();
72
71
  const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
73
72
  const viewModeValue = useViewModeState();
74
-
75
73
  useEffect(() => {
76
74
  if (resolving || waitingForServer) return;
77
75
  if (hasFiredSessionStart()) return;
78
76
  markSessionStartFired();
79
77
  trackStudioSessionStart({ has_project: projectId != null });
80
78
  }, [projectId, resolving, waitingForServer]);
81
-
82
79
  const [activeCompPath, setActiveCompPath] = useState<string | null>(null);
83
80
  const [activeCompPathHydrated, setActiveCompPathHydrated] = useState(
84
81
  () => initialUrlStateRef.current.activeCompPath == null,
@@ -168,6 +165,7 @@ export function StudioApp() {
168
165
  timelineElements,
169
166
  showToast,
170
167
  writeProjectFile: fileManager.writeProjectFile,
168
+ observeProjectFileVersion: fileManager.observeProjectFileVersion,
171
169
  recordEdit: editHistory.recordEdit,
172
170
  domEditSaveTimestampRef,
173
171
  reloadPreview,
@@ -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";
@@ -20,10 +12,13 @@ import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
20
12
  import type { RenderJob } from "./renders/useRenderQueue";
21
13
  import type { BlockParam } from "@hyperframes/core/registry";
22
14
  import type { IframeWindow } from "../player/lib/playbackTypes";
23
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "./editor/manualEditingAvailability";
15
+ import {
16
+ STUDIO_FLAT_INSPECTOR_ENABLED,
17
+ STUDIO_INSPECTOR_PANELS_ENABLED,
18
+ } from "./editor/manualEditingAvailability";
24
19
  import type { Composition } from "@hyperframes/sdk";
25
20
  import type { EditHistoryKind } from "../utils/editHistory";
26
- import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
21
+ import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
27
22
  import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
28
23
 
29
24
  import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
@@ -38,9 +33,8 @@ import {
38
33
  type ColorGradingScope,
39
34
  } from "./studioColorGradingScope";
40
35
  import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
41
-
42
- const MIN_INSPECTOR_SPLIT_PERCENT = 20;
43
- const MAX_INSPECTOR_SPLIT_PERCENT = 75;
36
+ import { timelineKeysForSelections, type ToggleHiddenHandler } from "../utils/studioHelpers";
37
+ import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
44
38
 
45
39
  export interface StudioRightPanelProps {
46
40
  designPanelActive: boolean;
@@ -56,6 +50,7 @@ export interface StudioRightPanelProps {
56
50
  onToggleRecording?: () => void;
57
51
  /** Dependencies for the Slideshow persist callback, threaded from App.tsx. */
58
52
  sdkSession: Composition | null;
53
+ publishSdkSession: NonNullable<UseSlideshowPersistParams["publishSdkSession"]>;
59
54
  reloadPreview: () => void;
60
55
  domEditSaveTimestampRef: MutableRefObject<number>;
61
56
  recordEdit: (entry: {
@@ -63,7 +58,7 @@ export interface StudioRightPanelProps {
63
58
  kind: EditHistoryKind;
64
59
  files: Record<string, { before: string; after: string }>;
65
60
  }) => Promise<void>;
66
- onToggleElementHidden?: (elementKey: string, hidden: boolean) => Promise<void> | void;
61
+ onToggleElementHidden?: ToggleHiddenHandler;
67
62
  }
68
63
 
69
64
  // fallow-ignore-next-line complexity
@@ -75,6 +70,7 @@ export function StudioRightPanel({
75
70
  recordingDuration,
76
71
  onToggleRecording,
77
72
  sdkSession,
73
+ publishSdkSession,
78
74
  reloadPreview,
79
75
  domEditSaveTimestampRef,
80
76
  recordEdit,
@@ -87,6 +83,7 @@ export function StudioRightPanel({
87
83
  setRightPanelTab,
88
84
  rightInspectorPanes,
89
85
  toggleRightInspectorPane,
86
+ setExclusiveRightInspectorPane,
90
87
  handlePanelResizeStart,
91
88
  handlePanelResizeMove,
92
89
  handlePanelResizeEnd,
@@ -109,10 +106,12 @@ export function StudioRightPanel({
109
106
  copiedAgentPrompt,
110
107
  clearDomSelection,
111
108
  handleUngroupSelection,
109
+ handleGroupSelection,
112
110
  handleDomStyleCommit,
113
111
  handleDomAttributeCommit,
114
112
  handleDomAttributeLiveCommit,
115
113
  handleDomHtmlAttributeCommit,
114
+ handleDomAttributesCommit,
116
115
  handleDomPathOffsetCommit,
117
116
  handleDomBoxSizeCommit,
118
117
  handleDomRotationCommit,
@@ -167,6 +166,7 @@ export function StudioRightPanel({
167
166
  recordEdit,
168
167
  reloadPreview,
169
168
  domEditSaveTimestampRef,
169
+ publishSdkSession,
170
170
  });
171
171
 
172
172
  // Notes path: persists are debounced in SlideshowPanel; coalesceKey ensures
@@ -179,16 +179,17 @@ export function StudioRightPanel({
179
179
  recordEdit,
180
180
  reloadPreview,
181
181
  domEditSaveTimestampRef,
182
+ publishSdkSession,
182
183
  coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
183
184
  });
184
185
 
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);
186
+ const {
187
+ layersPanePercent,
188
+ splitContainerRef,
189
+ handleInspectorSplitResizeStart,
190
+ handleInspectorSplitResizeMove,
191
+ handleInspectorSplitResizeEnd,
192
+ } = useInspectorSplitResize();
192
193
  const backgroundRemovalAbortRef = useRef<AbortController | null>(null);
193
194
 
194
195
  useEffect(
@@ -226,38 +227,16 @@ export function StudioRightPanel({
226
227
  setRightPanelTab(pane);
227
228
  return;
228
229
  }
230
+ // Flat inspector: Layers always renders full-height by itself (see the
231
+ // render branch below), so the two panes are mutually exclusive here —
232
+ // otherwise both tabs could show "active" while only one actually shows.
233
+ if (STUDIO_FLAT_INSPECTOR_ENABLED) {
234
+ setExclusiveRightInspectorPane(pane);
235
+ return;
236
+ }
229
237
  toggleRightInspectorPane(pane);
230
238
  };
231
239
 
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
240
  const handleApplyColorGradingScope = useCallback(
262
241
  async (scope: ColorGradingScope, value: string | null) =>
263
242
  applyColorGradingScopeUpdate({
@@ -341,12 +320,17 @@ export function StudioRightPanel({
341
320
  },
342
321
  [projectId, refreshFileTree, showToast],
343
322
  );
344
-
323
+ const handleHideAllSelected = () => {
324
+ const { elements } = usePlayerStore.getState();
325
+ const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath);
326
+ if (keys.length > 0) void onToggleElementHidden?.(keys, true);
327
+ };
345
328
  const propertyPanel = (
346
329
  <DesignPanelPromoteProvider
347
330
  selection={domEditGroupSelections.length > 1 ? null : domEditSelection}
348
331
  projectId={projectId}
349
332
  activeCompPath={activeCompPath}
333
+ showToast={showToast}
350
334
  readProjectFile={readProjectFile}
351
335
  writeProjectFile={writeProjectFile}
352
336
  recordEdit={recordEdit}
@@ -359,12 +343,16 @@ export function StudioRightPanel({
359
343
  assets={assets}
360
344
  element={domEditGroupSelections.length > 1 ? null : domEditSelection}
361
345
  multiSelectCount={domEditGroupSelections.length}
346
+ multiSelectedElements={domEditGroupSelections}
347
+ onGroupSelection={handleGroupSelection}
348
+ onHideAllSelected={handleHideAllSelected}
362
349
  copiedAgentPrompt={copiedAgentPrompt}
363
350
  onClearSelection={clearDomSelection}
364
351
  onToggleElementHidden={onToggleElementHidden}
365
352
  onUngroup={handleUngroupSelection}
366
353
  onSetStyle={handleDomStyleCommit}
367
354
  onSetAttribute={handleDomAttributeCommit}
355
+ onSetAttributes={handleDomAttributesCommit}
368
356
  onSetAttributeLive={handleDomAttributeLiveCommit}
369
357
  onApplyColorGradingScope={handleApplyColorGradingScope}
370
358
  onSetHtmlAttribute={handleDomHtmlAttributeCommit}
@@ -534,11 +522,12 @@ export function StudioRightPanel({
534
522
  ) : rightPanelTab === "variables" ? (
535
523
  <VariablesPanel
536
524
  sdkSession={sdkSession}
525
+ publishSdkSession={publishSdkSession}
537
526
  reloadPreview={reloadPreview}
538
527
  domEditSaveTimestampRef={domEditSaveTimestampRef}
539
528
  recordEdit={recordEdit}
540
529
  />
541
- ) : layersPaneOpen && designPaneOpen ? (
530
+ ) : layersPaneOpen && designPaneOpen && !STUDIO_FLAT_INSPECTOR_ENABLED ? (
542
531
  <div ref={splitContainerRef} className="flex h-full min-h-0 min-w-0 flex-col">
543
532
  <div
544
533
  className="min-h-[120px] overflow-hidden"
@@ -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"