@hyperframes/studio 0.7.45 → 0.7.47

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 (110) hide show
  1. package/dist/assets/index-Dq7FEg0K.css +1 -0
  2. package/dist/assets/{index-CZkdm5YL.js → index-DsckwbdW.js} +1 -1
  3. package/dist/assets/index-VsAbY3rl.js +423 -0
  4. package/dist/assets/{index-CbQKp0Ek.js → index-uvB30_yI.js} +1 -1
  5. package/dist/index.d.ts +96 -29
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +8443 -4305
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +26 -32
  11. package/src/components/DesignPanelPromoteProvider.tsx +45 -0
  12. package/src/components/PanelTabButton.tsx +31 -0
  13. package/src/components/StudioPreviewArea.tsx +75 -3
  14. package/src/components/StudioRightPanel.tsx +111 -113
  15. package/src/components/editor/LayersPanel.tsx +2 -22
  16. package/src/components/editor/PromotableControl.tsx +105 -0
  17. package/src/components/editor/propertyPanelSections.tsx +42 -21
  18. package/src/components/panels/VariablesBindElement.tsx +266 -0
  19. package/src/components/panels/VariablesDeclarationForm.test.ts +135 -0
  20. package/src/components/panels/VariablesDeclarationForm.tsx +321 -0
  21. package/src/components/panels/VariablesOtherCompositions.tsx +159 -0
  22. package/src/components/panels/VariablesPanel.tsx +554 -0
  23. package/src/components/panels/VariablesRowAction.tsx +25 -0
  24. package/src/components/panels/VariablesValueControls.tsx +215 -0
  25. package/src/components/renders/useRenderQueue.ts +12 -0
  26. package/src/contexts/TimelineEditContext.tsx +7 -2
  27. package/src/contexts/VariablePromoteContext.tsx +138 -0
  28. package/src/contexts/variablePromoteHelpers.test.ts +124 -0
  29. package/src/contexts/variablePromoteHelpers.ts +53 -0
  30. package/src/contexts/variablePromoteIntegration.test.ts +80 -0
  31. package/src/hooks/previewVariablesStore.ts +34 -0
  32. package/src/hooks/timelineEditingHelpers.test.ts +104 -0
  33. package/src/hooks/timelineEditingHelpers.ts +386 -10
  34. package/src/hooks/useDomEditWiring.ts +9 -7
  35. package/src/hooks/useDomSelection.test.ts +36 -2
  36. package/src/hooks/useDomSelection.ts +56 -75
  37. package/src/hooks/useElementLifecycleOps.ts +57 -17
  38. package/src/hooks/usePreviewDocumentVersion.ts +27 -0
  39. package/src/hooks/useProjectCompositionVariables.ts +131 -0
  40. package/src/hooks/useStudioContextValue.ts +6 -1
  41. package/src/hooks/useStudioSdkSessions.ts +37 -0
  42. package/src/hooks/useTimelineEditing.test.tsx +963 -0
  43. package/src/hooks/useTimelineEditing.ts +122 -100
  44. package/src/hooks/useTimelineEditingTypes.ts +15 -0
  45. package/src/hooks/useTimelineGroupEditing.ts +370 -0
  46. package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +129 -0
  47. package/src/hooks/useTimelineSelectionPreviewSync.ts +130 -0
  48. package/src/hooks/useVariablesPersist.ts +61 -0
  49. package/src/player/components/BeatStrip.tsx +19 -3
  50. package/src/player/components/Player.tsx +7 -1
  51. package/src/player/components/Timeline.test.ts +38 -0
  52. package/src/player/components/Timeline.tsx +102 -80
  53. package/src/player/components/TimelineCanvas.tsx +340 -296
  54. package/src/player/components/TimelineClip.test.tsx +11 -0
  55. package/src/player/components/TimelineClipDiamonds.tsx +40 -9
  56. package/src/player/components/TimelineDragGhost.tsx +59 -0
  57. package/src/player/components/TimelineDropInsertionLine.tsx +32 -0
  58. package/src/player/components/TimelineLayerGroupHeader.tsx +97 -0
  59. package/src/player/components/TimelineLayerGutter.tsx +61 -0
  60. package/src/player/components/TimelineRuler.tsx +4 -2
  61. package/src/player/components/TimelineSelectionOverlays.tsx +55 -0
  62. package/src/player/components/timelineCallbacks.ts +20 -2
  63. package/src/player/components/timelineClipDragPreview.ts +52 -0
  64. package/src/player/components/timelineDragDrop.ts +19 -3
  65. package/src/player/components/timelineDropIndicator.test.ts +48 -0
  66. package/src/player/components/timelineDropIndicator.ts +33 -0
  67. package/src/player/components/timelineEditing.test.ts +251 -1
  68. package/src/player/components/timelineEditing.ts +178 -19
  69. package/src/player/components/timelineGroupEditing.ts +159 -0
  70. package/src/player/components/timelineLayerDrag.test.ts +200 -0
  71. package/src/player/components/timelineLayerDrag.ts +383 -0
  72. package/src/player/components/timelineLayout.test.ts +34 -0
  73. package/src/player/components/timelineLayout.ts +47 -0
  74. package/src/player/components/timelineMarqueeSelection.test.ts +87 -0
  75. package/src/player/components/timelineSnapTargets.test.ts +144 -0
  76. package/src/player/components/timelineSnapTargets.ts +164 -0
  77. package/src/player/components/timelineStacking.ts +62 -0
  78. package/src/player/components/timelineTrackOrder.test.ts +144 -0
  79. package/src/player/components/timelineTrackOrder.ts +173 -0
  80. package/src/player/components/useTimelineActiveClips.test.ts +20 -0
  81. package/src/player/components/useTimelineActiveClips.ts +15 -4
  82. package/src/player/components/useTimelineClipDrag.test.tsx +506 -0
  83. package/src/player/components/useTimelineClipDrag.ts +172 -126
  84. package/src/player/components/useTimelineClipGroupDrag.ts +417 -0
  85. package/src/player/components/useTimelineKeyframeHandlers.ts +73 -0
  86. package/src/player/components/useTimelineMarqueeSelection.test.tsx +231 -0
  87. package/src/player/components/useTimelineMarqueeSelection.ts +276 -0
  88. package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
  89. package/src/player/hooks/useTimelinePlayer.test.ts +4 -1
  90. package/src/player/hooks/useTimelinePlayer.ts +7 -0
  91. package/src/player/lib/layerOrdering.test.ts +77 -0
  92. package/src/player/lib/layerOrdering.ts +98 -0
  93. package/src/player/lib/playbackTypes.ts +3 -0
  94. package/src/player/lib/timelineDOM.test.ts +103 -0
  95. package/src/player/lib/timelineDOM.ts +80 -0
  96. package/src/player/lib/timelineElementHelpers.ts +1 -1
  97. package/src/player/store/playerStore.test.ts +92 -0
  98. package/src/player/store/playerStore.ts +69 -9
  99. package/src/utils/blockInstaller.ts +2 -14
  100. package/src/utils/editHistory.test.ts +61 -0
  101. package/src/utils/editHistory.ts +7 -1
  102. package/src/utils/rootDuration.test.ts +34 -0
  103. package/src/utils/rootDuration.ts +17 -0
  104. package/src/utils/sdkCutover.ts +45 -0
  105. package/src/utils/studioHelpers.test.ts +31 -0
  106. package/src/utils/studioHelpers.ts +29 -1
  107. package/src/utils/studioUrlState.test.ts +29 -0
  108. package/src/utils/studioUrlState.ts +16 -1
  109. package/dist/assets/index-CZUbpYhQ.js +0 -416
  110. package/dist/assets/index-DoVLXke0.css +0 -1
@@ -0,0 +1,173 @@
1
+ import { type TimelineElement } from "../store/playerStore";
2
+ import { resolveContextOrder, resolveStackingContextKey } from "../lib/layerOrdering";
3
+ import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
4
+ import { toStackingOrderItem, type TimelineStackingOrderItem } from "./timelineStacking";
5
+
6
+ export type TimelineLayerId = string;
7
+
8
+ export interface StackingTimelineLayer {
9
+ id: TimelineLayerId;
10
+ kind: "visual" | "audio";
11
+ contextKey: string;
12
+ zIndex: number;
13
+ placementTrack: number;
14
+ elements: TimelineElement[];
15
+ }
16
+
17
+ export interface StackingTimelineLayerGroups {
18
+ visualLayers: StackingTimelineLayer[];
19
+ audioLayers: StackingTimelineLayer[];
20
+ rows: StackingTimelineLayer[];
21
+ }
22
+
23
+ type TimelineLayerOrderItem = TimelineStackingOrderItem & {
24
+ start: number;
25
+ duration: number;
26
+ index: number;
27
+ element: TimelineElement;
28
+ };
29
+
30
+ type BuildLayer = Omit<StackingTimelineLayer, "id">;
31
+
32
+ function toTimelineLayerOrderItem(element: TimelineElement, index: number): TimelineLayerOrderItem {
33
+ return {
34
+ ...toStackingOrderItem(element),
35
+ start: element.start,
36
+ duration: element.duration,
37
+ index,
38
+ element,
39
+ };
40
+ }
41
+
42
+ export function timelineElementsOverlap(
43
+ a: Pick<TimelineElement, "start" | "duration">,
44
+ b: Pick<TimelineElement, "start" | "duration">,
45
+ ): boolean {
46
+ return a.start < b.start + b.duration && b.start < a.start + a.duration;
47
+ }
48
+
49
+ function compareLayerItems(a: TimelineLayerOrderItem, b: TimelineLayerOrderItem): number {
50
+ if (a.zIndex !== b.zIndex) return b.zIndex - a.zIndex;
51
+ return a.index - b.index;
52
+ }
53
+
54
+ function buildElementLayerId(
55
+ prefix: string,
56
+ contextKey: string,
57
+ element: TimelineElement,
58
+ ): TimelineLayerId {
59
+ return `${prefix}:${contextKey}:${getTimelineElementIdentity(element)}`;
60
+ }
61
+
62
+ function buildLaneId(
63
+ prefix: string,
64
+ contextKey: string,
65
+ elements: TimelineElement[],
66
+ ): TimelineLayerId {
67
+ const memberKey = elements.map(getTimelineElementIdentity).sort().join("|");
68
+ return `${prefix}:${contextKey}:${memberKey}`;
69
+ }
70
+
71
+ function getOrderedContextKeys(items: readonly TimelineLayerOrderItem[]): string[] {
72
+ const keys: string[] = [];
73
+ for (const item of resolveContextOrder(items)) {
74
+ const key = resolveStackingContextKey(item);
75
+ if (!keys.includes(key)) keys.push(key);
76
+ }
77
+ return keys;
78
+ }
79
+
80
+ function canJoinLayer(layer: BuildLayer, item: TimelineLayerOrderItem): boolean {
81
+ // A row IS a z-band: clips only share a lane when they carry the same z-index
82
+ // (and don't overlap in time). Without the z-index gate a non-overlapping clip
83
+ // greedily packs into the first time-compatible row — the topmost one — so a
84
+ // vertical restack changes z but the row never follows it.
85
+ return (
86
+ layer.contextKey === resolveStackingContextKey(item) &&
87
+ layer.zIndex === item.zIndex &&
88
+ layer.elements.every((element) => !timelineElementsOverlap(element, item.element))
89
+ );
90
+ }
91
+
92
+ function compareElementsByStart(a: TimelineElement, b: TimelineElement): number {
93
+ if (a.start !== b.start) return a.start - b.start;
94
+ return getTimelineElementIdentity(a).localeCompare(getTimelineElementIdentity(b));
95
+ }
96
+
97
+ function buildVisualLayerRows(items: readonly TimelineLayerOrderItem[]): StackingTimelineLayer[] {
98
+ const byContext = new Map<string, TimelineLayerOrderItem[]>();
99
+ for (const item of items) {
100
+ const key = resolveStackingContextKey(item);
101
+ const list = byContext.get(key);
102
+ if (list) list.push(item);
103
+ else byContext.set(key, [item]);
104
+ }
105
+
106
+ const rows: StackingTimelineLayer[] = [];
107
+ for (const contextKey of getOrderedContextKeys(items)) {
108
+ const contextRows: BuildLayer[] = [];
109
+ const contextItems = [...(byContext.get(contextKey) ?? [])].sort(compareLayerItems);
110
+ for (const item of contextItems) {
111
+ const existing = contextRows.find((row) => canJoinLayer(row, item));
112
+ if (existing) {
113
+ existing.elements.push(item.element);
114
+ existing.zIndex = Math.max(existing.zIndex, item.zIndex);
115
+ continue;
116
+ }
117
+ contextRows.push({
118
+ kind: "visual",
119
+ contextKey,
120
+ zIndex: item.zIndex,
121
+ placementTrack: item.element.track,
122
+ elements: [item.element],
123
+ });
124
+ }
125
+ rows.push(
126
+ ...contextRows.map((row) => {
127
+ const elements = [...row.elements].sort(compareElementsByStart);
128
+ return {
129
+ ...row,
130
+ id: buildLaneId("lane", contextKey, elements),
131
+ elements,
132
+ };
133
+ }),
134
+ );
135
+ }
136
+ return rows;
137
+ }
138
+
139
+ function buildAudioLayerRows(items: readonly TimelineLayerOrderItem[]): StackingTimelineLayer[] {
140
+ return items.map((item) => ({
141
+ id: buildElementLayerId("audio", resolveStackingContextKey(item), item.element),
142
+ kind: "audio",
143
+ contextKey: resolveStackingContextKey(item),
144
+ zIndex: item.zIndex,
145
+ placementTrack: item.element.track,
146
+ elements: [item.element],
147
+ }));
148
+ }
149
+
150
+ export function buildStackingTimelineLayers(
151
+ elements: readonly TimelineElement[],
152
+ ): StackingTimelineLayerGroups {
153
+ const items = elements.map(toTimelineLayerOrderItem);
154
+ const visualItems = items.filter((item) => item.element.tag !== "audio");
155
+ const audioItems = items.filter((item) => item.element.tag === "audio");
156
+ const visualLayers = buildVisualLayerRows(visualItems);
157
+ const audioLayers = buildAudioLayerRows(audioItems);
158
+ return {
159
+ visualLayers,
160
+ audioLayers,
161
+ rows: [...visualLayers, ...audioLayers],
162
+ };
163
+ }
164
+
165
+ export function insertPreviewTrackOrder(
166
+ layerOrder: readonly TimelineLayerId[],
167
+ previewLayerId: TimelineLayerId,
168
+ previewIndex: number,
169
+ ): TimelineLayerId[] {
170
+ if (layerOrder.includes(previewLayerId)) return [...layerOrder];
171
+ const index = Math.max(0, Math.min(layerOrder.length, Math.round(previewIndex)));
172
+ return [...layerOrder.slice(0, index), previewLayerId, ...layerOrder.slice(index)];
173
+ }
@@ -78,6 +78,26 @@ describe("updateTimelineActiveClipClasses", () => {
78
78
  expect(previous).toEqual(new Set());
79
79
  });
80
80
 
81
+ it("re-applies data-active to a fresh DOM node that stayed active across a re-render", () => {
82
+ // A clip that moves lanes on a reorder remounts as a new element. It stays
83
+ // in the previous active set, so the plain diff would skip it and leave the
84
+ // new node without data-active. syncAll must force the attribute on.
85
+ const container = document.createElement("div");
86
+ appendClip(container, "hero", "0", "5");
87
+ const previous = new Set<string>();
88
+ updateTimelineActiveClipClasses(container, previous, 2);
89
+ expect(previous).toEqual(new Set(["hero"]));
90
+
91
+ // Simulate a remount: replace the hero clip's DOM node (no data-active).
92
+ container.replaceChildren();
93
+ const heroReborn = appendClip(container, "hero", "0", "5");
94
+ expect(heroReborn.hasAttribute("data-active")).toBe(false);
95
+
96
+ // Diff-only would skip it (still active → unchanged); syncAll re-applies.
97
+ updateTimelineActiveClipClasses(container, previous, 2, true);
98
+ expect(heroReborn.hasAttribute("data-active")).toBe(true);
99
+ });
100
+
81
101
  it("ignores clips with invalid timing data", () => {
82
102
  const container = document.createElement("div");
83
103
  const missingId = appendClip(container, "", "0", "2");
@@ -65,13 +65,23 @@ function setsMatch(left: Set<string>, right: Set<string>): boolean {
65
65
  return true;
66
66
  }
67
67
 
68
- function applyActiveClipDiff(records: ActiveClipRecord[], previous: Set<string>, time: number) {
68
+ function applyActiveClipDiff(
69
+ records: ActiveClipRecord[],
70
+ previous: Set<string>,
71
+ time: number,
72
+ // Force every record's attribute to match its active state instead of only
73
+ // touching clips whose active-state changed. Required whenever `records` were
74
+ // freshly re-queried after a render: a clip that stayed active but got a new
75
+ // DOM node (e.g. moved lanes on a reorder) would otherwise be skipped by the
76
+ // diff and render without `data-active` despite still being at the playhead.
77
+ syncAll = false,
78
+ ) {
69
79
  const next = getActiveClipIds(records, time);
70
80
  const changed = !setsMatch(previous, next);
71
81
  for (const record of records) {
72
82
  const wasActive = previous.has(record.id);
73
83
  const isActive = next.has(record.id);
74
- if (wasActive === isActive) continue;
84
+ if (!syncAll && wasActive === isActive) continue;
75
85
  record.element.toggleAttribute("data-active", isActive);
76
86
  }
77
87
  previous.clear();
@@ -83,8 +93,9 @@ export function updateTimelineActiveClipClasses(
83
93
  container: HTMLElement,
84
94
  previous: Set<string>,
85
95
  time: number,
96
+ syncAll = false,
86
97
  ) {
87
- applyActiveClipDiff(collectTimelineClipRecords(container), previous, time);
98
+ applyActiveClipDiff(collectTimelineClipRecords(container), previous, time, syncAll);
88
99
  }
89
100
 
90
101
  export function useTimelineActiveClips({
@@ -107,7 +118,7 @@ export function useTimelineActiveClips({
107
118
  }
108
119
  recordsRef.current = collectTimelineClipRecords(scroll);
109
120
  recordsByIdRef.current = indexClipRecordsById(recordsRef.current);
110
- applyActiveClipDiff(recordsRef.current, previousActiveIdsRef.current, time);
121
+ applyActiveClipDiff(recordsRef.current, previousActiveIdsRef.current, time, true);
111
122
  },
112
123
  [scrollRef],
113
124
  );