@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
@@ -0,0 +1,23 @@
1
+ /** Browser-safe SHA-256 version matching studio-server's strong ETag format. */
2
+ export async function studioFileContentVersion(content: string): Promise<string> {
3
+ const bytes = new TextEncoder().encode(content);
4
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
5
+ const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(
6
+ "",
7
+ );
8
+ return `"sha256:${hex}"`;
9
+ }
10
+
11
+ /** Prefer an explicit content precondition, then the version observed during the read. */
12
+ export async function studioExpectedFileVersion(
13
+ versions: ReadonlyMap<string, string | null>,
14
+ path: string,
15
+ expectedContent?: string,
16
+ ): Promise<string | null | undefined> {
17
+ if (expectedContent !== undefined) return studioFileContentVersion(expectedContent);
18
+ return versions.get(path);
19
+ }
20
+
21
+ export function createStudioWriteToken(): string {
22
+ return globalThis.crypto.randomUUID();
23
+ }
@@ -48,6 +48,51 @@ describe("findMatchingTimelineElementId", () => {
48
48
  it("returns null for an unmatched element in index.html", () => {
49
49
  expect(findMatchingTimelineElementId({ id: "ghost", sourceFile: "index.html" }, [])).toBe(null);
50
50
  });
51
+
52
+ it("resolves the correct repeated composition host by domId, not the first host sharing the same compositionSrc", () => {
53
+ // Two hosts import the SAME sub-composition — only compositionSrc alone
54
+ // can't tell them apart, but each still has its own domId (as any
55
+ // element does). Selecting the SECOND host must not collapse to the
56
+ // first one just because an earlier, unrelated element also happens to
57
+ // share its compositionSrc.
58
+ const els = [
59
+ el({ id: "host-a", domId: "host-a", sourceFile: "index.html", compositionSrc: "scene.html" }),
60
+ el({ id: "host-b", domId: "host-b", sourceFile: "index.html", compositionSrc: "scene.html" }),
61
+ ];
62
+ expect(
63
+ findMatchingTimelineElementId(
64
+ {
65
+ id: "host-b",
66
+ sourceFile: "index.html",
67
+ compositionSrc: "scene.html",
68
+ isCompositionHost: true,
69
+ },
70
+ els,
71
+ ),
72
+ ).toBe("host-b");
73
+ });
74
+
75
+ it("falls back to compositionSrc-only matching when the host selection has neither a domId nor a selector", () => {
76
+ const els = [
77
+ el({
78
+ id: "host-only",
79
+ domId: undefined,
80
+ sourceFile: "index.html",
81
+ compositionSrc: "scene.html",
82
+ }),
83
+ ];
84
+ expect(
85
+ findMatchingTimelineElementId(
86
+ {
87
+ id: undefined,
88
+ sourceFile: "index.html",
89
+ compositionSrc: "scene.html",
90
+ isCompositionHost: true,
91
+ },
92
+ els,
93
+ ),
94
+ ).toBe("host-only");
95
+ });
51
96
  });
52
97
 
53
98
  describe("findTimelineIdByAncestor", () => {
@@ -161,25 +161,34 @@ function matchesBySelector(selection: ElementMatchSelection, element: TimelineEl
161
161
  );
162
162
  }
163
163
 
164
- function elementMatchesSelection(
165
- selection: ElementMatchSelection,
166
- element: TimelineElement,
167
- selectionSourceFile: string,
168
- ): boolean {
169
- return (
170
- matchesByDomId(selection, element, selectionSourceFile) ||
171
- matchesByCompositionHost(selection, element) ||
172
- matchesBySelector(selection, element)
173
- );
174
- }
175
-
176
164
  export function findMatchingTimelineElementId(
177
165
  selection: ElementMatchSelection,
178
166
  elements: TimelineElement[],
179
167
  ): string | null {
180
168
  const selectionSourceFile = selection.sourceFile || "index.html";
181
- const match = elements.find((el) => elementMatchesSelection(selection, el, selectionSourceFile));
182
- if (match) return match.key ?? match.id;
169
+ // Priority matters, not just "any of the three": a composition-host
170
+ // selection always carries its OWN id/selector too (computed generically
171
+ // for any element), so two repeated hosts sharing the same compositionSrc
172
+ // are still individually addressable by id/selector. Checking
173
+ // matchesByCompositionHost with equal priority in a single OR-per-element
174
+ // scan let `.find()` stop at an EARLIER, unrelated host that merely shares
175
+ // the compositionSrc, before the scan ever reached the correct id/selector
176
+ // match further down the list — collapsing every repeated host to the
177
+ // first one. Try id, then selector, across the WHOLE list first; only fall
178
+ // back to the coarser compositionSrc-only match when neither identifies a
179
+ // specific element.
180
+ const byId = selection.id
181
+ ? elements.find((el) => matchesByDomId(selection, el, selectionSourceFile))
182
+ : undefined;
183
+ if (byId) return byId.key ?? byId.id;
184
+
185
+ const bySelector = selection.selector
186
+ ? elements.find((el) => matchesBySelector(selection, el))
187
+ : undefined;
188
+ if (bySelector) return bySelector.key ?? bySelector.id;
189
+
190
+ const byHost = elements.find((el) => matchesByCompositionHost(selection, el));
191
+ if (byHost) return byHost.key ?? byHost.id;
183
192
 
184
193
  // Child inside a sub-composition: return a qualified ID so the expansion
185
194
  // hook can resolve the child via clipParentMap even though no timeline
@@ -237,6 +246,28 @@ export function resolveTimelineIdForSelection(
237
246
  );
238
247
  }
239
248
 
249
+ /**
250
+ * Resolve every multi-selected element to its scope-qualified timeline key
251
+ * (dropping unresolvable ones). Selections carry bare DOM ids/selectors, but
252
+ * the visibility toggle matches keys like "index.html#hero" — and callers must
253
+ * hide all keys in ONE call so the file is patched in a single atomic write
254
+ * (per-element calls would clobber each other's reads).
255
+ */
256
+ export function timelineKeysForSelections(
257
+ selections: readonly DomEditSelection[],
258
+ elements: TimelineElement[],
259
+ activeCompPath: string | null,
260
+ ): string[] {
261
+ return selections
262
+ .map((selection) => resolveTimelineIdForSelection(selection, elements, activeCompPath))
263
+ .filter((key): key is string => key !== null);
264
+ }
265
+
266
+ export type ToggleHiddenHandler = (
267
+ elementKey: string | readonly string[],
268
+ hidden: boolean,
269
+ ) => Promise<void> | void;
270
+
240
271
  export function resolveTimelineSelectionSeekTime(
241
272
  currentTime: number,
242
273
  element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import {
3
+ StudioFileConflictError,
3
4
  StudioSaveHttpError,
4
5
  StudioSaveNetworkError,
5
6
  buildStudioSaveFailureProperties,
@@ -8,6 +9,23 @@ import {
8
9
  } from "./studioSaveDiagnostics";
9
10
 
10
11
  describe("studio save diagnostics", () => {
12
+ it("preserves conflict versions and both sides for explicit recovery UI", () => {
13
+ const error = new StudioFileConflictError({
14
+ filePath: "index.html",
15
+ currentVersion: '"sha256:new"',
16
+ currentContent: "external",
17
+ attemptedContent: "local",
18
+ });
19
+
20
+ expect(getStudioSaveStatusCode(error)).toBe(409);
21
+ expect(error).toMatchObject({
22
+ filePath: "index.html",
23
+ currentVersion: '"sha256:new"',
24
+ currentContent: "external",
25
+ attemptedContent: "local",
26
+ });
27
+ });
28
+
11
29
  it("builds save_failure properties with stable diagnostics", () => {
12
30
  const error = new StudioSaveHttpError("Failed to save index.html (503)", 503);
13
31
 
@@ -35,6 +35,27 @@ export class StudioSaveNetworkError extends Error {
35
35
  }
36
36
  }
37
37
 
38
+ export class StudioFileConflictError extends StudioSaveHttpError {
39
+ readonly filePath: string;
40
+ readonly currentVersion: string | null;
41
+ readonly currentContent: string | null;
42
+ readonly attemptedContent: string;
43
+
44
+ constructor(input: {
45
+ filePath: string;
46
+ currentVersion: string | null;
47
+ currentContent: string | null;
48
+ attemptedContent: string;
49
+ }) {
50
+ super(`Save conflict: ${input.filePath} changed outside this Studio session`, 409);
51
+ this.name = "StudioFileConflictError";
52
+ this.filePath = input.filePath;
53
+ this.currentVersion = input.currentVersion;
54
+ this.currentContent = input.currentContent;
55
+ this.attemptedContent = input.attemptedContent;
56
+ }
57
+ }
58
+
38
59
  function readNumericProperty(value: object, key: string): number | undefined {
39
60
  const record = value as Record<string, unknown>;
40
61
  const property = record[key];
@@ -71,6 +92,23 @@ export function getStudioSaveErrorMessage(error: unknown): string {
71
92
  return "Unknown save failure";
72
93
  }
73
94
 
95
+ export function markStudioSaveErrorAlreadyToasted<T>(error: T): T {
96
+ if (!error || typeof error !== "object") return error;
97
+ try {
98
+ Object.defineProperty(error, "alreadyToasted", { value: true, configurable: true });
99
+ } catch {
100
+ // Best effort: failure reporting must not replace the original save error.
101
+ }
102
+ return error;
103
+ }
104
+
105
+ export function isStudioSaveErrorAlreadyToasted(error: unknown): boolean {
106
+ if (!error || typeof error !== "object") return false;
107
+ if ((error as { alreadyToasted?: unknown }).alreadyToasted === true) return true;
108
+ const cause = (error as { cause?: unknown }).cause;
109
+ return cause !== error && isStudioSaveErrorAlreadyToasted(cause);
110
+ }
111
+
74
112
  export function getStudioSaveStatusCode(error: unknown): number | undefined {
75
113
  if (!error || typeof error !== "object") return undefined;
76
114
  const direct =