@hyperframes/studio 0.7.46 → 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.
- package/dist/assets/index-Dq7FEg0K.css +1 -0
- package/dist/assets/{index-BGVoOXdg.js → index-DsckwbdW.js} +1 -1
- package/dist/assets/index-VsAbY3rl.js +423 -0
- package/dist/assets/{index-DO2MAFSG.js → index-uvB30_yI.js} +1 -1
- package/dist/index.d.ts +96 -29
- package/dist/index.html +2 -2
- package/dist/index.js +8443 -4305
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -32
- package/src/components/DesignPanelPromoteProvider.tsx +45 -0
- package/src/components/PanelTabButton.tsx +31 -0
- package/src/components/StudioPreviewArea.tsx +75 -3
- package/src/components/StudioRightPanel.tsx +111 -113
- package/src/components/editor/LayersPanel.tsx +2 -22
- package/src/components/editor/PromotableControl.tsx +105 -0
- package/src/components/editor/propertyPanelSections.tsx +42 -21
- package/src/components/panels/VariablesBindElement.tsx +266 -0
- package/src/components/panels/VariablesDeclarationForm.test.ts +135 -0
- package/src/components/panels/VariablesDeclarationForm.tsx +321 -0
- package/src/components/panels/VariablesOtherCompositions.tsx +159 -0
- package/src/components/panels/VariablesPanel.tsx +554 -0
- package/src/components/panels/VariablesRowAction.tsx +25 -0
- package/src/components/panels/VariablesValueControls.tsx +215 -0
- package/src/components/renders/useRenderQueue.ts +12 -0
- package/src/contexts/TimelineEditContext.tsx +7 -2
- package/src/contexts/VariablePromoteContext.tsx +138 -0
- package/src/contexts/variablePromoteHelpers.test.ts +124 -0
- package/src/contexts/variablePromoteHelpers.ts +53 -0
- package/src/contexts/variablePromoteIntegration.test.ts +80 -0
- package/src/hooks/previewVariablesStore.ts +34 -0
- package/src/hooks/timelineEditingHelpers.test.ts +104 -0
- package/src/hooks/timelineEditingHelpers.ts +386 -10
- package/src/hooks/useDomEditWiring.ts +9 -7
- package/src/hooks/useDomSelection.test.ts +36 -2
- package/src/hooks/useDomSelection.ts +56 -75
- package/src/hooks/useElementLifecycleOps.ts +57 -17
- package/src/hooks/usePreviewDocumentVersion.ts +27 -0
- package/src/hooks/useProjectCompositionVariables.ts +131 -0
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useStudioSdkSessions.ts +37 -0
- package/src/hooks/useTimelineEditing.test.tsx +963 -0
- package/src/hooks/useTimelineEditing.ts +122 -100
- package/src/hooks/useTimelineEditingTypes.ts +15 -0
- package/src/hooks/useTimelineGroupEditing.ts +370 -0
- package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +129 -0
- package/src/hooks/useTimelineSelectionPreviewSync.ts +130 -0
- package/src/hooks/useVariablesPersist.ts +61 -0
- package/src/player/components/BeatStrip.tsx +19 -3
- package/src/player/components/Player.tsx +7 -1
- package/src/player/components/Timeline.test.ts +38 -0
- package/src/player/components/Timeline.tsx +102 -80
- package/src/player/components/TimelineCanvas.tsx +340 -296
- package/src/player/components/TimelineClip.test.tsx +11 -0
- package/src/player/components/TimelineClipDiamonds.tsx +40 -9
- package/src/player/components/TimelineDragGhost.tsx +59 -0
- package/src/player/components/TimelineDropInsertionLine.tsx +32 -0
- package/src/player/components/TimelineLayerGroupHeader.tsx +97 -0
- package/src/player/components/TimelineLayerGutter.tsx +61 -0
- package/src/player/components/TimelineRuler.tsx +4 -2
- package/src/player/components/TimelineSelectionOverlays.tsx +55 -0
- package/src/player/components/timelineCallbacks.ts +20 -2
- package/src/player/components/timelineClipDragPreview.ts +52 -0
- package/src/player/components/timelineDragDrop.ts +19 -3
- package/src/player/components/timelineDropIndicator.test.ts +48 -0
- package/src/player/components/timelineDropIndicator.ts +33 -0
- package/src/player/components/timelineEditing.test.ts +251 -1
- package/src/player/components/timelineEditing.ts +178 -19
- package/src/player/components/timelineGroupEditing.ts +159 -0
- package/src/player/components/timelineLayerDrag.test.ts +200 -0
- package/src/player/components/timelineLayerDrag.ts +383 -0
- package/src/player/components/timelineLayout.test.ts +34 -0
- package/src/player/components/timelineLayout.ts +47 -0
- package/src/player/components/timelineMarqueeSelection.test.ts +87 -0
- package/src/player/components/timelineSnapTargets.test.ts +144 -0
- package/src/player/components/timelineSnapTargets.ts +164 -0
- package/src/player/components/timelineStacking.ts +62 -0
- package/src/player/components/timelineTrackOrder.test.ts +144 -0
- package/src/player/components/timelineTrackOrder.ts +173 -0
- package/src/player/components/useTimelineActiveClips.test.ts +20 -0
- package/src/player/components/useTimelineActiveClips.ts +15 -4
- package/src/player/components/useTimelineClipDrag.test.tsx +506 -0
- package/src/player/components/useTimelineClipDrag.ts +172 -126
- package/src/player/components/useTimelineClipGroupDrag.ts +417 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +73 -0
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +231 -0
- package/src/player/components/useTimelineMarqueeSelection.ts +276 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +4 -1
- package/src/player/hooks/useTimelinePlayer.ts +7 -0
- package/src/player/lib/layerOrdering.test.ts +77 -0
- package/src/player/lib/layerOrdering.ts +98 -0
- package/src/player/lib/playbackTypes.ts +3 -0
- package/src/player/lib/timelineDOM.test.ts +103 -0
- package/src/player/lib/timelineDOM.ts +80 -0
- package/src/player/lib/timelineElementHelpers.ts +1 -1
- package/src/player/store/playerStore.test.ts +92 -0
- package/src/player/store/playerStore.ts +69 -9
- package/src/utils/blockInstaller.ts +2 -14
- package/src/utils/editHistory.test.ts +61 -0
- package/src/utils/editHistory.ts +7 -1
- package/src/utils/rootDuration.test.ts +34 -0
- package/src/utils/rootDuration.ts +17 -0
- package/src/utils/sdkCutover.ts +45 -0
- package/src/utils/studioHelpers.test.ts +31 -0
- package/src/utils/studioHelpers.ts +29 -1
- package/src/utils/studioUrlState.test.ts +29 -0
- package/src/utils/studioUrlState.ts +16 -1
- package/dist/assets/index-BsLQ2qGn.js +0 -420
- 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(
|
|
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
|
);
|