@hyperframes/studio 0.7.57 → 0.7.58

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 (112) hide show
  1. package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-CtTDO63S.js} +1 -1
  2. package/dist/assets/{index-DfmYkU44.js → index-B_UvTX3E.js} +204 -204
  3. package/dist/assets/{index-KsfE1bUu.js → index-C47jAC3Q.js} +1 -1
  4. package/dist/assets/{index-Bf1x1y8H.js → index-DeQPzqwH.js} +1 -1
  5. package/dist/assets/index-uahwWkgw.css +1 -0
  6. package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
  7. package/dist/chunk-OBAG3GWK.js.map +1 -0
  8. package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
  9. package/dist/index.d.ts +15 -4
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +5954 -4474
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +3 -3
  15. package/src/components/StudioFeedbackBar.tsx +2 -2
  16. package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
  17. package/src/components/editor/CanvasContextMenu.tsx +71 -6
  18. package/src/components/editor/DomEditOverlay.tsx +5 -2
  19. package/src/components/editor/LayersPanel.tsx +110 -99
  20. package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
  21. package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
  22. package/src/components/editor/domEditingDom.ts +5 -6
  23. package/src/components/editor/domEditingElement.ts +15 -32
  24. package/src/components/editor/layersPanelSort.ts +80 -0
  25. package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
  26. package/src/components/editor/useLayerRevealOverride.ts +389 -0
  27. package/src/components/nle/PreviewOverlays.tsx +27 -2
  28. package/src/components/nle/TimelinePane.test.ts +3 -0
  29. package/src/components/nle/TimelinePane.tsx +11 -1
  30. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
  31. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
  32. package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
  33. package/src/components/nle/zLaneGesture.test.ts +176 -0
  34. package/src/components/nle/zLaneGesture.ts +77 -0
  35. package/src/hooks/domEditCommitTypes.ts +17 -1
  36. package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
  37. package/src/hooks/fetchStubTestUtils.ts +16 -0
  38. package/src/hooks/timelineEditingHelpers.test.ts +92 -1
  39. package/src/hooks/timelineEditingHelpers.ts +32 -15
  40. package/src/hooks/timelineMoveAdapter.ts +2 -1
  41. package/src/hooks/timelineTimingSync.test.ts +703 -33
  42. package/src/hooks/timelineTimingSync.ts +282 -97
  43. package/src/hooks/timelineTrackVisibility.test.ts +3 -1
  44. package/src/hooks/timelineTrackVisibility.ts +4 -3
  45. package/src/hooks/useClipboard.ts +19 -17
  46. package/src/hooks/useDomEditCommits.test.tsx +316 -38
  47. package/src/hooks/useDomEditCommits.ts +143 -78
  48. package/src/hooks/useDomEditSession.test.tsx +3 -1
  49. package/src/hooks/useDomEditSession.ts +1 -1
  50. package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
  51. package/src/hooks/useElementLifecycleOps.ts +174 -81
  52. package/src/hooks/usePreviewPersistence.ts +2 -2
  53. package/src/hooks/useTimelineEditing.test.tsx +302 -154
  54. package/src/hooks/useTimelineEditing.ts +97 -93
  55. package/src/hooks/useTimelineEditingTypes.ts +1 -1
  56. package/src/hooks/useTimelineGroupEditing.ts +35 -3
  57. package/src/player/components/Timeline.test.ts +31 -21
  58. package/src/player/components/Timeline.tsx +62 -91
  59. package/src/player/components/TimelineCanvas.tsx +36 -5
  60. package/src/player/components/TimelineLanes.tsx +32 -10
  61. package/src/player/components/TimelineOverlays.tsx +36 -0
  62. package/src/player/components/TimelineRuler.tsx +18 -5
  63. package/src/player/components/TrackGapContextMenu.tsx +116 -0
  64. package/src/player/components/timelineCallbacks.ts +4 -1
  65. package/src/player/components/timelineClipDragCommit.test.ts +279 -3
  66. package/src/player/components/timelineClipDragCommit.ts +173 -71
  67. package/src/player/components/timelineGapCommit.test.ts +163 -0
  68. package/src/player/components/timelineGapCommit.ts +103 -0
  69. package/src/player/components/timelineGaps.test.ts +221 -0
  70. package/src/player/components/timelineGaps.ts +182 -0
  71. package/src/player/components/timelineLayout.test.ts +2 -1
  72. package/src/player/components/timelineLayout.ts +22 -6
  73. package/src/player/components/timelineMarquee.test.ts +20 -10
  74. package/src/player/components/timelineMarquee.ts +9 -2
  75. package/src/player/components/timelineStackingSync.test.ts +42 -1
  76. package/src/player/components/timelineStackingSync.ts +31 -16
  77. package/src/player/components/timelineZMirror.test.ts +667 -0
  78. package/src/player/components/timelineZMirror.ts +393 -0
  79. package/src/player/components/timelineZoom.test.ts +2 -2
  80. package/src/player/components/timelineZoom.ts +7 -3
  81. package/src/player/components/useTimelineClipDrag.ts +1 -1
  82. package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
  83. package/src/player/components/useTimelineGapHighlights.ts +110 -0
  84. package/src/player/components/useTimelinePlayhead.ts +4 -3
  85. package/src/player/components/useTimelineRangeSelection.ts +6 -4
  86. package/src/player/components/useTimelineScrollViewport.ts +73 -0
  87. package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
  88. package/src/player/components/useTimelineStackingSync.ts +5 -3
  89. package/src/player/components/useTimelineTrackDerivations.ts +38 -0
  90. package/src/player/components/useTrackGapMenu.ts +155 -0
  91. package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
  92. package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
  93. package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
  94. package/src/player/lib/layerOrdering.test.ts +1 -15
  95. package/src/player/lib/layerOrdering.ts +5 -28
  96. package/src/player/lib/timelineDOM.test.ts +72 -0
  97. package/src/player/lib/timelineDOM.ts +3 -0
  98. package/src/player/lib/timelineElementHelpers.ts +35 -11
  99. package/src/player/store/playerStore.ts +12 -1
  100. package/src/telemetry/events.test.ts +10 -0
  101. package/src/telemetry/events.ts +1 -0
  102. package/src/utils/domEditSaveQueue.ts +4 -3
  103. package/src/utils/editHistory.test.ts +82 -0
  104. package/src/utils/editHistory.ts +14 -2
  105. package/src/utils/gsapSoftReload.test.ts +54 -115
  106. package/src/utils/gsapSoftReload.ts +42 -152
  107. package/src/utils/gsapUndoRestore.test.ts +269 -0
  108. package/src/utils/gsapUndoRestore.ts +269 -0
  109. package/src/utils/sourceScopedSelectorIndex.ts +29 -0
  110. package/dist/assets/index-_pqzyxB1.css +0 -1
  111. package/dist/chunk-5QSIMBEJ.js.map +0 -1
  112. /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
@@ -4,6 +4,7 @@
4
4
  * No imports from other domEditing* modules — safe to import from anywhere.
5
5
  */
6
6
  import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
7
+ import { getSourceScopedSelectorIndex } from "../../utils/sourceScopedSelectorIndex";
7
8
  import { CURATED_STYLE_PROPERTIES } from "./domEditingTypes";
8
9
 
9
10
  // ─── Type guard ───────────────────────────────────────────────────────────────
@@ -296,11 +297,9 @@ export function getSelectorIndex(
296
297
  ): number | undefined {
297
298
  if (!selector?.startsWith(".")) return undefined;
298
299
 
299
- const candidates = querySelectorAllSafely(doc, selector).filter(
300
- (candidate): candidate is HTMLElement =>
301
- isHtmlElement(candidate) &&
302
- getSourceFileForElement(candidate, activeCompositionPath).sourceFile === sourceFile,
300
+ return getSourceScopedSelectorIndex(doc, el, selector, sourceFile, (candidate) =>
301
+ isHtmlElement(candidate)
302
+ ? getSourceFileForElement(candidate, activeCompositionPath).sourceFile
303
+ : undefined,
303
304
  );
304
- const index = candidates.indexOf(el);
305
- return index >= 0 ? index : undefined;
306
305
  }
@@ -246,45 +246,28 @@ export function findElementForSelection(
246
246
  selection: FindElementSelection,
247
247
  activeCompositionPath: string | null = null,
248
248
  ): HTMLElement | null {
249
+ const sourceMatches = (candidate: Element): candidate is HTMLElement =>
250
+ isHtmlElement(candidate) &&
251
+ (!selection.sourceFile ||
252
+ getSourceFileForElement(candidate, activeCompositionPath).sourceFile ===
253
+ selection.sourceFile);
254
+ const findAll = (selector: string): HTMLElement[] =>
255
+ querySelectorAllSafely(doc, selector).filter(sourceMatches);
256
+
249
257
  if (selection.hfId) {
250
- const byHfId = doc.querySelector(`[data-hf-id="${CSS.escape(selection.hfId)}"]`);
251
- if (isHtmlElement(byHfId)) return byHfId;
258
+ const byHfId = findAll(`[data-hf-id="${escapeCssString(selection.hfId)}"]`)[0];
259
+ if (byHfId) return byHfId;
252
260
  }
253
261
 
254
262
  if (selection.id) {
255
- const byId = doc.getElementById(selection.id);
256
- if (
257
- isHtmlElement(byId) &&
258
- (!selection.sourceFile ||
259
- getSourceFileForElement(byId, activeCompositionPath).sourceFile === selection.sourceFile)
260
- ) {
261
- return byId;
262
- }
263
+ // Flattened sub-compositions can repeat authored ids. getElementById returns
264
+ // only the first document match, so filter every id match by source first.
265
+ const byId = findAll(`[id="${escapeCssString(selection.id)}"]`)[0];
266
+ if (byId) return byId;
263
267
  }
264
268
 
265
269
  if (!selection.selector) return null;
266
-
267
- // fallow-ignore-next-line code-duplication
268
- if (selection.selector.startsWith(".") && selection.selectorIndex != null) {
269
- const matches = querySelectorAllSafely(doc, selection.selector).filter(
270
- (candidate): candidate is HTMLElement =>
271
- isHtmlElement(candidate) &&
272
- (!selection.sourceFile ||
273
- getSourceFileForElement(candidate, activeCompositionPath).sourceFile ===
274
- selection.sourceFile),
275
- );
276
- return matches[selection.selectorIndex] ?? null;
277
- }
278
-
279
- // fallow-ignore-next-line code-duplication
280
- const matches = querySelectorAllSafely(doc, selection.selector).filter(
281
- (candidate): candidate is HTMLElement =>
282
- isHtmlElement(candidate) &&
283
- (!selection.sourceFile ||
284
- getSourceFileForElement(candidate, activeCompositionPath).sourceFile ===
285
- selection.sourceFile),
286
- );
287
- return matches[0] ?? null;
270
+ return findAll(selection.selector)[selection.selectorIndex ?? 0] ?? null;
288
271
  }
289
272
 
290
273
  // fallow-ignore-next-line complexity
@@ -0,0 +1,80 @@
1
+ import type { DomEditLayerItem } from "./domEditingTypes";
2
+ import { getElementZIndex } from "../../player/lib/layerOrdering";
3
+
4
+ interface CollapsedState {
5
+ [key: string]: boolean;
6
+ }
7
+
8
+ // ── Pure helpers ──────────────────────────────────────────────────────
9
+
10
+ // fallow-ignore-next-line complexity
11
+ export function sortLayersByZIndex(layers: DomEditLayerItem[]): DomEditLayerItem[] {
12
+ if (layers.length <= 1) return layers;
13
+
14
+ const minDepth = layers[0].depth;
15
+ for (let i = 1; i < layers.length; i++) {
16
+ if (layers[i].depth < minDepth) return layers;
17
+ }
18
+
19
+ const chunks: Array<{ root: DomEditLayerItem; children: DomEditLayerItem[]; domIndex: number }> =
20
+ [];
21
+
22
+ for (let i = 0; i < layers.length; i++) {
23
+ if (layers[i].depth === minDepth) {
24
+ const children: DomEditLayerItem[] = [];
25
+ let j = i + 1;
26
+ while (j < layers.length && layers[j].depth > minDepth) {
27
+ children.push(layers[j]);
28
+ j++;
29
+ }
30
+ chunks.push({ root: layers[i], children, domIndex: chunks.length });
31
+ }
32
+ }
33
+
34
+ if (chunks.length <= 1) {
35
+ if (chunks.length === 1 && chunks[0].children.length > 0) {
36
+ const sorted = sortLayersByZIndex(chunks[0].children);
37
+ return [chunks[0].root, ...sorted];
38
+ }
39
+ return layers;
40
+ }
41
+
42
+ chunks.sort((a, b) => {
43
+ const zA = getElementZIndex(a.root.element);
44
+ const zB = getElementZIndex(b.root.element);
45
+ if (zA !== zB) return zB - zA;
46
+ return b.domIndex - a.domIndex;
47
+ });
48
+
49
+ const result: DomEditLayerItem[] = [];
50
+ for (const chunk of chunks) {
51
+ result.push(chunk.root);
52
+ if (chunk.children.length > 0) {
53
+ result.push(...sortLayersByZIndex(chunk.children));
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ export function getVisibleLayers(
60
+ layers: DomEditLayerItem[],
61
+ collapsed: CollapsedState,
62
+ ): DomEditLayerItem[] {
63
+ if (Object.keys(collapsed).length === 0) return layers;
64
+
65
+ const result: DomEditLayerItem[] = [];
66
+ let skipDepth = -1;
67
+
68
+ for (const layer of layers) {
69
+ if (skipDepth >= 0 && layer.depth > skipDepth) continue;
70
+ skipDepth = -1;
71
+
72
+ result.push(layer);
73
+
74
+ if (collapsed[layer.key] && layer.childCount > 0) {
75
+ skipDepth = layer.depth;
76
+ }
77
+ }
78
+
79
+ return result;
80
+ }
@@ -0,0 +1,169 @@
1
+ // @vitest-environment jsdom
2
+ import React, { act } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import {
6
+ LAYER_REVEAL_LIFT_Z,
7
+ LAYER_REVEAL_PENDING_COMMIT_ATTR,
8
+ beginLayerRevealCommit,
9
+ completeLayerRevealCommit,
10
+ liftElementToTop,
11
+ restoreLiftedElement,
12
+ useLayerRevealOverride,
13
+ } from "./useLayerRevealOverride";
14
+ import { readEffectiveZIndex } from "./canvasContextMenuZOrder";
15
+ import { getElementZIndex } from "../../player/lib/layerOrdering";
16
+ import {
17
+ LAYER_REVEAL_PRIOR_Z_ATTR,
18
+ readTimelineElementZIndex,
19
+ } from "../../player/lib/timelineElementHelpers";
20
+ import { installReactActEnvironment } from "../../hooks/domSelectionTestHarness";
21
+
22
+ installReactActEnvironment();
23
+
24
+ function makeEl(zIndex?: string, position?: string): HTMLElement {
25
+ const el = document.createElement("div");
26
+ if (zIndex != null) el.style.zIndex = zIndex;
27
+ if (position != null) el.style.position = position;
28
+ document.body.appendChild(el);
29
+ return el;
30
+ }
31
+
32
+ describe("liftElementToTop / restoreLiftedElement", () => {
33
+ it("paints on top but every z reader keeps reporting the TRUE z", () => {
34
+ const el = makeEl("6", "absolute");
35
+ const lift = liftElementToTop(el);
36
+ expect(lift).not.toBeNull();
37
+ // The renderer sees the lifted value…
38
+ expect(el.style.zIndex).toBe(LAYER_REVEAL_LIFT_Z);
39
+ // …every studio reader sees the true z.
40
+ expect(readEffectiveZIndex(el)).toBe(6);
41
+ expect(getElementZIndex(el)).toBe(6);
42
+ expect(readTimelineElementZIndex(el)).toBe(6);
43
+
44
+ restoreLiftedElement(el, lift!);
45
+ expect(el.style.zIndex).toBe("6");
46
+ expect(el.hasAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe(false);
47
+ });
48
+
49
+ it("gives a static element a temporary position:relative and restores it", () => {
50
+ const el = makeEl();
51
+ const lift = liftElementToTop(el)!;
52
+ expect(el.style.position).toBe("relative");
53
+ expect(lift.positionLifted).toBe(true);
54
+ restoreLiftedElement(el, lift);
55
+ expect(el.style.position).toBe("");
56
+ expect(el.style.zIndex).toBe("");
57
+ });
58
+
59
+ it("a z-reorder commit consumes the lift: restore becomes a no-op", () => {
60
+ const el = makeEl("3", "absolute");
61
+ const lift = liftElementToTop(el)!;
62
+ // Simulate handleDomZIndexReorderCommit: real z written, attrs removed.
63
+ el.removeAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);
64
+ el.style.zIndex = "8";
65
+ restoreLiftedElement(el, lift);
66
+ expect(el.style.zIndex).toBe("8"); // the commit's value survives
67
+ expect(readEffectiveZIndex(el)).toBe(8);
68
+ });
69
+
70
+ it("durable z persistence consumes its pending reveal ownership", () => {
71
+ const el = makeEl("3", "absolute");
72
+ const lift = liftElementToTop(el)!;
73
+ const ownership = beginLayerRevealCommit(el)!;
74
+ el.style.zIndex = "8";
75
+
76
+ expect(el.hasAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR)).toBe(true);
77
+ completeLayerRevealCommit(el, ownership);
78
+ restoreLiftedElement(el, lift);
79
+
80
+ expect(el.style.zIndex).toBe("8");
81
+ expect(el.hasAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR)).toBe(false);
82
+ expect(el.hasAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe(false);
83
+ });
84
+
85
+ it("does not clobber a z someone else wrote while lifted", () => {
86
+ const el = makeEl("3", "absolute");
87
+ const lift = liftElementToTop(el)!;
88
+ el.style.zIndex = "42"; // e.g. a GSAP seek or manual edit
89
+ restoreLiftedElement(el, lift);
90
+ expect(el.style.zIndex).toBe("42");
91
+ });
92
+ });
93
+
94
+ describe("useLayerRevealOverride — delayed reveal ownership", () => {
95
+ beforeEach(() => {
96
+ vi.useFakeTimers();
97
+ });
98
+
99
+ afterEach(() => {
100
+ vi.useRealTimers();
101
+ document.body.innerHTML = "";
102
+ });
103
+
104
+ it("cancels a pending reveal when selection changes or playback begins", () => {
105
+ const host = document.createElement("div");
106
+ const selected = makeEl("2", "absolute");
107
+ const other = makeEl("3", "absolute");
108
+ document.body.appendChild(host);
109
+ const root = createRoot(host);
110
+ let scheduleReveal: ((element: HTMLElement, delayMs: number) => void) | undefined;
111
+
112
+ function Harness({ element, isPlaying }: { element: HTMLElement; isPlaying: boolean }) {
113
+ ({ scheduleReveal } = useLayerRevealOverride({
114
+ isPlaying,
115
+ selectedElement: element,
116
+ }));
117
+ return null;
118
+ }
119
+
120
+ act(() => {
121
+ root.render(React.createElement(Harness, { element: selected, isPlaying: false }));
122
+ });
123
+ act(() => scheduleReveal!(selected, 150));
124
+ expect(vi.getTimerCount()).toBe(1);
125
+
126
+ act(() => {
127
+ root.render(React.createElement(Harness, { element: other, isPlaying: false }));
128
+ });
129
+ expect(vi.getTimerCount()).toBe(0);
130
+
131
+ act(() => scheduleReveal!(other, 150));
132
+ expect(vi.getTimerCount()).toBe(1);
133
+ act(() => {
134
+ root.render(React.createElement(Harness, { element: other, isPlaying: true }));
135
+ });
136
+ expect(vi.getTimerCount()).toBe(0);
137
+
138
+ act(() => vi.advanceTimersByTime(150));
139
+ expect(selected.style.zIndex).toBe("2");
140
+ expect(other.style.zIndex).toBe("3");
141
+ act(() => root.unmount());
142
+ });
143
+
144
+ it("revalidates the current selection before a delayed reveal runs", () => {
145
+ const host = document.createElement("div");
146
+ const selected = makeEl("2", "absolute");
147
+ const stale = makeEl("3", "absolute");
148
+ document.body.appendChild(host);
149
+ const root = createRoot(host);
150
+ let scheduleReveal: ((element: HTMLElement, delayMs: number) => void) | undefined;
151
+
152
+ function Harness() {
153
+ ({ scheduleReveal } = useLayerRevealOverride({
154
+ isPlaying: false,
155
+ selectedElement: selected,
156
+ }));
157
+ return null;
158
+ }
159
+
160
+ act(() => root.render(React.createElement(Harness)));
161
+ act(() => scheduleReveal!(stale, 150));
162
+ expect(vi.getTimerCount()).toBe(1);
163
+ act(() => vi.advanceTimersByTime(150));
164
+
165
+ expect(stale.style.zIndex).toBe("3");
166
+ expect(stale.hasAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe(false);
167
+ act(() => root.unmount());
168
+ });
169
+ });