@hyperframes/studio 0.7.56 → 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.
- package/dist/assets/hyperframes-player-CtTDO63S.js +459 -0
- package/dist/assets/{index-CmVCjZjp.js → index-B_UvTX3E.js} +205 -205
- package/dist/assets/{index-BWnOxAiH.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-C4csZims.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +41 -4
- package/dist/index.html +2 -2
- package/dist/index.js +7436 -5423
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +166 -2
- package/src/components/editor/CanvasContextMenu.tsx +100 -20
- package/src/components/editor/DomEditOverlay.tsx +34 -62
- package/src/components/editor/LayersPanel.tsx +111 -91
- package/src/components/editor/canvasContextMenuZOrder.test.ts +243 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +184 -23
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +61 -37
- package/src/components/nle/PreviewOverlays.tsx +61 -16
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +19 -2
- package/src/components/sidebar/AudioRow.tsx +19 -2
- package/src/hooks/domEditCommitTypes.ts +29 -2
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +183 -1
- package/src/hooks/timelineEditingHelpers.ts +63 -259
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +7 -4
- package/src/hooks/timelineTimingSync.test.ts +820 -0
- package/src/hooks/timelineTimingSync.ts +597 -0
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +392 -23
- package/src/hooks/useDomEditCommits.ts +180 -53
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +559 -28
- package/src/hooks/useElementLifecycleOps.ts +175 -69
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +591 -277
- package/src/hooks/useTimelineEditing.ts +202 -282
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +187 -134
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +105 -18
- package/src/player/components/Timeline.tsx +69 -98
- package/src/player/components/TimelineCanvas.tsx +47 -7
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +70 -47
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +370 -11
- package/src/player/components/timelineClipDragCommit.ts +267 -68
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +119 -30
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +102 -1
- package/src/player/components/timelineStackingSync.ts +45 -5
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -2
- package/src/player/components/useTimelineStackingSync.ts +17 -4
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +19 -1
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +11 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/lib/timelineElementHelpers.ts +54 -11
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +49 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +62 -86
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-D-GyYi2d.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
* the computed value. Treat missing / "auto" as 0 for comparison purposes.
|
|
8
8
|
*
|
|
9
9
|
* "Overlapping siblings" = siblings whose bounding rects intersect the
|
|
10
|
-
* target's bounding rect
|
|
11
|
-
* front/back operate across all
|
|
10
|
+
* target's bounding rect AND are actually visible at the current frame.
|
|
11
|
+
* Forward/backward operate within that set; front/back operate across all
|
|
12
|
+
* siblings (full painting family, visible or not — unchanged semantics).
|
|
13
|
+
*
|
|
14
|
+
* ── Visibility ───────────────────────────────────────────────────────────────
|
|
15
|
+
* In HyperFrames compositions the nearest z-neighbor is often INVISIBLE at the
|
|
16
|
+
* paused frame: the runtime hides time-inactive clips with inline
|
|
17
|
+
* `visibility:hidden` / `display:none` (see core runtime
|
|
18
|
+
* syncTimedElementVisibility), and GSAP timelines park elements at `opacity:0`.
|
|
19
|
+
* Stepping "forward" over such a sibling looks like a silent no-op. The
|
|
20
|
+
* forward/backward comparison set therefore keeps only siblings whose
|
|
21
|
+
* element-level computed style is visible (display ≠ none, visibility ≠
|
|
22
|
+
* hidden, opacity > 0.01) — all runtime hiding signals are inline styles, so
|
|
23
|
+
* computed style covers them. Ancestor-chain checks are unnecessary here:
|
|
24
|
+
* siblings share the target's ancestors. The probe is injectable
|
|
25
|
+
* (ZOrderResolveOptions.isVisible) so the pure-module tests stay meaningful
|
|
26
|
+
* without a real style engine, mirroring how tests stub rect reading.
|
|
12
27
|
*
|
|
13
28
|
* ── Tie-awareness ────────────────────────────────────────────────────────────
|
|
14
29
|
* CSS paint order for elements that share a z-index is DOM document order:
|
|
@@ -26,6 +41,9 @@
|
|
|
26
41
|
* (project convention clamps z ≥ 0).
|
|
27
42
|
*/
|
|
28
43
|
|
|
44
|
+
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
|
|
45
|
+
import { readLayerRevealPriorZ } from "../../player/lib/timelineElementHelpers";
|
|
46
|
+
|
|
29
47
|
export type ZOrderAction = "bring-forward" | "send-backward" | "bring-to-front" | "send-to-back";
|
|
30
48
|
|
|
31
49
|
/** A resolved change: set `element`'s z-index to `zIndex`. */
|
|
@@ -34,6 +52,48 @@ export interface ZOrderPatch {
|
|
|
34
52
|
zIndex: number;
|
|
35
53
|
}
|
|
36
54
|
|
|
55
|
+
/** Injectable knobs for the pure resolver (kept mockable like rect reading). */
|
|
56
|
+
export interface ZOrderResolveOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Element-level visibility probe used to scope the forward/backward
|
|
59
|
+
* comparison set. Defaults to `isElementVisibleForZOrder` (computed-style
|
|
60
|
+
* display/visibility/opacity). Injectable so tests can run without a real
|
|
61
|
+
* style engine.
|
|
62
|
+
*/
|
|
63
|
+
isVisible?: (el: HTMLElement) => boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Default visibility probe: is this element itself visible at the current
|
|
68
|
+
* frame? Element-level only (siblings share the target's ancestor chain).
|
|
69
|
+
* Covers the runtime's inactive-clip hiding (inline `visibility:hidden` /
|
|
70
|
+
* `display:none`) and animation-parked `opacity:0`, all of which surface
|
|
71
|
+
* through computed style. A color-grading source (hidden at opacity:0 while
|
|
72
|
+
* its canvas paints in its place) still counts as visible, matching
|
|
73
|
+
* isElementVisibleThroughAncestors in domEditingDom.
|
|
74
|
+
*/
|
|
75
|
+
export function isElementVisibleForZOrder(el: HTMLElement): boolean {
|
|
76
|
+
try {
|
|
77
|
+
const win = el.ownerDocument?.defaultView;
|
|
78
|
+
if (!win) return true;
|
|
79
|
+
const computed = win.getComputedStyle(el);
|
|
80
|
+
if (computed.display === "none") return false;
|
|
81
|
+
if (computed.visibility === "hidden" || computed.visibility === "collapse") return false;
|
|
82
|
+
const opacity = Number.parseFloat(computed.opacity);
|
|
83
|
+
if (
|
|
84
|
+
Number.isFinite(opacity) &&
|
|
85
|
+
opacity <= 0.01 &&
|
|
86
|
+
!el.hasAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR)
|
|
87
|
+
) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
/* cross-origin / detached — assume visible (fail open, matches rect fallback) */
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
37
97
|
interface RenderEntry {
|
|
38
98
|
element: HTMLElement;
|
|
39
99
|
zIndex: number;
|
|
@@ -48,8 +108,11 @@ export function parseZIndex(value: string | null | undefined): number {
|
|
|
48
108
|
return Number.isFinite(n) ? n : 0;
|
|
49
109
|
}
|
|
50
110
|
|
|
51
|
-
/** Read the effective z-index for an element (inline style preferred).
|
|
111
|
+
/** Read the effective z-index for an element (inline style preferred).
|
|
112
|
+
* Reveal-lift transparent: an active Layers-panel lift reports the TRUE z. */
|
|
52
113
|
export function readEffectiveZIndex(el: HTMLElement): number {
|
|
114
|
+
const prior = readLayerRevealPriorZ(el);
|
|
115
|
+
if (prior != null) return prior;
|
|
53
116
|
const inline = el.style.zIndex;
|
|
54
117
|
if (inline && inline !== "auto") return parseZIndex(inline);
|
|
55
118
|
try {
|
|
@@ -81,8 +144,18 @@ function isElementNode(node: Node): node is HTMLElement {
|
|
|
81
144
|
* z-index onto the qa-clean audio element, and counting it as a sibling skews the
|
|
82
145
|
* renumber for the visible elements. `<script>/<style>/<link>/<meta>` are also
|
|
83
146
|
* non-painting and could otherwise pad the family / eat a z slot.
|
|
147
|
+
* `<template>/<noscript>` never paint either — letting them in meant renumber
|
|
148
|
+
* fallbacks wrote z-index/position into template source markup.
|
|
84
149
|
*/
|
|
85
|
-
const NON_PAINTING_TAGS = new Set([
|
|
150
|
+
const NON_PAINTING_TAGS = new Set([
|
|
151
|
+
"AUDIO",
|
|
152
|
+
"SCRIPT",
|
|
153
|
+
"STYLE",
|
|
154
|
+
"LINK",
|
|
155
|
+
"META",
|
|
156
|
+
"TEMPLATE",
|
|
157
|
+
"NOSCRIPT",
|
|
158
|
+
]);
|
|
86
159
|
|
|
87
160
|
/** A painting element: an element node whose tag actually renders pixels. */
|
|
88
161
|
function isPaintingElement(node: Node): node is HTMLElement {
|
|
@@ -112,7 +185,7 @@ function getFamily(target: HTMLElement): { entries: RenderEntry[]; targetIndex:
|
|
|
112
185
|
return { entries, targetIndex };
|
|
113
186
|
}
|
|
114
187
|
|
|
115
|
-
/** True if two DOM bounding rects
|
|
188
|
+
/** True if two DOM bounding rects strictly overlap (rects that merely touch do NOT intersect). */
|
|
116
189
|
function rectsIntersect(
|
|
117
190
|
a: { left: number; top: number; right: number; bottom: number },
|
|
118
191
|
b: { left: number; top: number; right: number; bottom: number },
|
|
@@ -121,26 +194,35 @@ function rectsIntersect(
|
|
|
121
194
|
}
|
|
122
195
|
|
|
123
196
|
/**
|
|
124
|
-
* Restrict a family to the target plus siblings
|
|
125
|
-
* the target's rect. The target is always retained
|
|
126
|
-
*
|
|
127
|
-
*
|
|
197
|
+
* Restrict a family to the target plus siblings that are VISIBLE and whose
|
|
198
|
+
* bounding rect overlaps the target's rect. The target is always retained
|
|
199
|
+
* (even when itself hidden at the current frame — it is the user's explicit
|
|
200
|
+
* selection). If the target's rect is unavailable or empty (headless /
|
|
201
|
+
* happy-dom returns 0×0), the overlap filter is skipped and all VISIBLE
|
|
202
|
+
* entries are kept — matching the prior rect-fallback behavior.
|
|
128
203
|
*/
|
|
129
|
-
function getOverlappingFamily(
|
|
204
|
+
function getOverlappingFamily(
|
|
205
|
+
target: HTMLElement,
|
|
206
|
+
entries: RenderEntry[],
|
|
207
|
+
isVisible: (el: HTMLElement) => boolean,
|
|
208
|
+
): RenderEntry[] {
|
|
209
|
+
const visibleEntries = entries.filter(
|
|
210
|
+
(entry) => entry.element === target || isVisible(entry.element),
|
|
211
|
+
);
|
|
130
212
|
let targetRect: DOMRect;
|
|
131
213
|
try {
|
|
132
214
|
targetRect = target.getBoundingClientRect();
|
|
133
215
|
} catch {
|
|
134
|
-
return
|
|
216
|
+
return visibleEntries;
|
|
135
217
|
}
|
|
136
|
-
if (targetRect.width === 0 && targetRect.height === 0) return
|
|
218
|
+
if (targetRect.width === 0 && targetRect.height === 0) return visibleEntries;
|
|
137
219
|
const tr = {
|
|
138
220
|
left: targetRect.left,
|
|
139
221
|
top: targetRect.top,
|
|
140
222
|
right: targetRect.right,
|
|
141
223
|
bottom: targetRect.bottom,
|
|
142
224
|
};
|
|
143
|
-
return
|
|
225
|
+
return visibleEntries.filter((entry) => {
|
|
144
226
|
if (entry.element === target) return true;
|
|
145
227
|
try {
|
|
146
228
|
const r = entry.element.getBoundingClientRect();
|
|
@@ -296,28 +378,46 @@ function buildGlobalOrder(
|
|
|
296
378
|
}
|
|
297
379
|
|
|
298
380
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* end of its set), otherwise the minimal list of {element, zIndex} changes.
|
|
381
|
+
* The shared scoping pipeline: full painting family for front/back, visible
|
|
382
|
+
* overlapping siblings for forward/backward, sorted into render order with the
|
|
383
|
+
* target's position. Null when the family/scope is too small to act on.
|
|
303
384
|
*/
|
|
304
|
-
|
|
385
|
+
function resolveScopedRenderOrder(
|
|
305
386
|
target: HTMLElement,
|
|
306
387
|
action: ZOrderAction,
|
|
307
|
-
|
|
388
|
+
options?: ZOrderResolveOptions,
|
|
389
|
+
): { entries: RenderEntry[]; order: RenderEntry[]; pos: number } | null {
|
|
308
390
|
const { entries } = getFamily(target);
|
|
309
391
|
// Family always includes the target; fewer than 2 means no siblings at all.
|
|
310
392
|
if (entries.length < 2) return null;
|
|
311
393
|
|
|
394
|
+
const isVisible = options?.isVisible ?? isElementVisibleForZOrder;
|
|
312
395
|
const scoped =
|
|
313
396
|
action === "bring-to-front" || action === "send-to-back"
|
|
314
397
|
? entries
|
|
315
|
-
: getOverlappingFamily(target, entries);
|
|
398
|
+
: getOverlappingFamily(target, entries, isVisible);
|
|
316
399
|
if (scoped.length < 2) return null;
|
|
317
400
|
|
|
318
401
|
const order = toRenderOrder(scoped);
|
|
319
402
|
const pos = order.findIndex((e) => e.element === target);
|
|
320
403
|
if (pos === -1) return null;
|
|
404
|
+
return { entries, order, pos };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Resolve the z-order patches for an action.
|
|
409
|
+
*
|
|
410
|
+
* Returns null when the action is a no-op (target already at the relevant
|
|
411
|
+
* end of its set), otherwise the minimal list of {element, zIndex} changes.
|
|
412
|
+
*/
|
|
413
|
+
export function resolveZOrderChange(
|
|
414
|
+
target: HTMLElement,
|
|
415
|
+
action: ZOrderAction,
|
|
416
|
+
options?: ZOrderResolveOptions,
|
|
417
|
+
): ZOrderPatch[] | null {
|
|
418
|
+
const resolved = resolveScopedRenderOrder(target, action, options);
|
|
419
|
+
if (!resolved) return null;
|
|
420
|
+
const { entries, order, pos } = resolved;
|
|
321
421
|
|
|
322
422
|
const desired = [...order];
|
|
323
423
|
const [moved] = desired.splice(pos, 1);
|
|
@@ -343,10 +443,71 @@ export function resolveZOrderChange(
|
|
|
343
443
|
return realizeOrder(order, desired, target, entries);
|
|
344
444
|
}
|
|
345
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Realize an ARBITRARY repositioning of `target` within a scoped sibling set —
|
|
448
|
+
* the Layers-panel drag, which can jump several siblings in one drop, unlike
|
|
449
|
+
* the menu's four fixed actions. `desiredOrderBottomToTop` is the scoped set
|
|
450
|
+
* (target included at its new slot) in the intended render order. Reuses the
|
|
451
|
+
* menu's minimal-write realization (realizeOrder): one between-z write when a
|
|
452
|
+
* strict gap exists, band-safe scoped renumber otherwise — replacing the old
|
|
453
|
+
* LayersPanel computeReorderZValues path that stamped EVERY sibling.
|
|
454
|
+
*
|
|
455
|
+
* Null when nothing changes, the set is too small, or an element in the
|
|
456
|
+
* desired order is not actually a painting sibling of `target`.
|
|
457
|
+
*/
|
|
458
|
+
export function resolveZOrderReposition(
|
|
459
|
+
target: HTMLElement,
|
|
460
|
+
desiredOrderBottomToTop: readonly HTMLElement[],
|
|
461
|
+
): ZOrderPatch[] | null {
|
|
462
|
+
const { entries } = getFamily(target);
|
|
463
|
+
if (entries.length < 2) return null;
|
|
464
|
+
const byElement = new Map(entries.map((entry) => [entry.element, entry]));
|
|
465
|
+
const desired: RenderEntry[] = [];
|
|
466
|
+
for (const el of desiredOrderBottomToTop) {
|
|
467
|
+
const entry = byElement.get(el);
|
|
468
|
+
if (!entry) return null;
|
|
469
|
+
desired.push(entry);
|
|
470
|
+
}
|
|
471
|
+
if (desired.length < 2 || !desired.some((entry) => entry.element === target)) return null;
|
|
472
|
+
const currentOrder = toRenderOrder(desired);
|
|
473
|
+
// A drop back into the same slot is a no-op. The menu actions guard this via
|
|
474
|
+
// their position checks before realizeOrder; an arbitrary reposition must
|
|
475
|
+
// compare the orders itself — realizeOrder would otherwise "normalize" an
|
|
476
|
+
// end-of-set target to a fresh z value it doesn't need.
|
|
477
|
+
if (currentOrder.every((entry, i) => entry.element === desired[i].element)) return null;
|
|
478
|
+
return realizeOrder(currentOrder, desired, target, entries);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* The sibling a forward/backward step crosses: the visible overlapping
|
|
483
|
+
* neighbor directly above (bring-forward) or below (send-backward) the target
|
|
484
|
+
* in render order. Null for front/back, for a no-op step, or when the scope is
|
|
485
|
+
* too small. Uses the SAME scoping as resolveZOrderChange, so call it with the
|
|
486
|
+
* same options BEFORE any live styles are applied.
|
|
487
|
+
*/
|
|
488
|
+
export function resolveCrossedNeighbor(
|
|
489
|
+
target: HTMLElement,
|
|
490
|
+
action: ZOrderAction,
|
|
491
|
+
options?: ZOrderResolveOptions,
|
|
492
|
+
): HTMLElement | null {
|
|
493
|
+
if (action !== "bring-forward" && action !== "send-backward") return null;
|
|
494
|
+
const resolved = resolveScopedRenderOrder(target, action, options);
|
|
495
|
+
if (!resolved) return null;
|
|
496
|
+
const { order, pos } = resolved;
|
|
497
|
+
const neighbor = action === "bring-forward" ? order[pos + 1] : order[pos - 1];
|
|
498
|
+
return neighbor?.element ?? null;
|
|
499
|
+
}
|
|
500
|
+
|
|
346
501
|
/**
|
|
347
502
|
* Whether a z-order action is available for the target.
|
|
348
|
-
* "disabled" = the element is already at that limit.
|
|
503
|
+
* "disabled" = the element is already at that limit. Shares the resolver (and
|
|
504
|
+
* its visibility scoping), so enable/disable always matches what the action
|
|
505
|
+
* would actually do.
|
|
349
506
|
*/
|
|
350
|
-
export function isZOrderActionEnabled(
|
|
351
|
-
|
|
507
|
+
export function isZOrderActionEnabled(
|
|
508
|
+
target: HTMLElement,
|
|
509
|
+
action: ZOrderAction,
|
|
510
|
+
options?: ZOrderResolveOptions,
|
|
511
|
+
): boolean {
|
|
512
|
+
return resolveZOrderChange(target, action, options) !== null;
|
|
352
513
|
}
|
|
@@ -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
|
-
|
|
300
|
-
(candidate)
|
|
301
|
-
|
|
302
|
-
|
|
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 =
|
|
251
|
-
if (
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
}
|
|
@@ -24,6 +24,10 @@ import type { DomEditSelection } from "./domEditing";
|
|
|
24
24
|
import { ColorGradingControls } from "./propertyPanelColorGradingControls";
|
|
25
25
|
import { stripQueryAndHash } from "./propertyPanelHelpers";
|
|
26
26
|
import { Section } from "./propertyPanelPrimitives";
|
|
27
|
+
import {
|
|
28
|
+
acceptStudioRuntimeMessage,
|
|
29
|
+
postRuntimeControlMessage,
|
|
30
|
+
} from "../../player/lib/runtimeProtocol";
|
|
27
31
|
|
|
28
32
|
const COLOR_GRADING_DATA_KEY = HF_COLOR_GRADING_ATTR.replace(/^data-/, "");
|
|
29
33
|
const RUNTIME_STATUS_REFRESH_DELAYS = [50, 250, 1000, 2500] as const;
|
|
@@ -393,35 +397,23 @@ export function ColorGradingSection({
|
|
|
393
397
|
|
|
394
398
|
const postColorGrading = useCallback(
|
|
395
399
|
(nextGrading: NormalizedHfColorGrading) => {
|
|
396
|
-
previewIframeRef?.current?.contentWindow
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
action: "set-color-grading",
|
|
401
|
-
target,
|
|
402
|
-
grading: toBridgeColorGrading(nextGrading),
|
|
403
|
-
},
|
|
404
|
-
"*",
|
|
405
|
-
);
|
|
400
|
+
postRuntimeControlMessage(previewIframeRef?.current?.contentWindow, "set-color-grading", {
|
|
401
|
+
target,
|
|
402
|
+
grading: toBridgeColorGrading(nextGrading),
|
|
403
|
+
});
|
|
406
404
|
},
|
|
407
405
|
[previewIframeRef, target],
|
|
408
406
|
);
|
|
409
407
|
|
|
410
408
|
const postCompare = useCallback(
|
|
411
409
|
(enabled: boolean) => {
|
|
412
|
-
|
|
410
|
+
postRuntimeControlMessage(
|
|
411
|
+
previewIframeRef?.current?.contentWindow,
|
|
412
|
+
"set-color-grading-compare",
|
|
413
413
|
{
|
|
414
|
-
source: "hf-parent",
|
|
415
|
-
type: "control",
|
|
416
|
-
action: "set-color-grading-compare",
|
|
417
414
|
target,
|
|
418
|
-
compare: {
|
|
419
|
-
enabled,
|
|
420
|
-
position: 1,
|
|
421
|
-
lineWidth: 0,
|
|
422
|
-
},
|
|
415
|
+
compare: { enabled, position: 1, lineWidth: 0 },
|
|
423
416
|
},
|
|
424
|
-
"*",
|
|
425
417
|
);
|
|
426
418
|
},
|
|
427
419
|
[previewIframeRef, target],
|
|
@@ -440,7 +432,9 @@ export function ColorGradingSection({
|
|
|
440
432
|
const onMessage = (event: MessageEvent) => {
|
|
441
433
|
if (event.source !== iframe.contentWindow) return;
|
|
442
434
|
const data = event.data as { source?: unknown; type?: unknown } | null;
|
|
443
|
-
if (data?.source
|
|
435
|
+
if (data?.source !== "hf-preview" || data.type !== "ready") return;
|
|
436
|
+
if (!acceptStudioRuntimeMessage(data)) return;
|
|
437
|
+
refreshAndReplay();
|
|
444
438
|
};
|
|
445
439
|
iframe.addEventListener("load", refreshAndReplay);
|
|
446
440
|
window.addEventListener("message", onMessage);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas right-click context-menu state for DomEditOverlay: where the menu is
|
|
3
|
+
* open (viewport x/y) and which selection it targets, plus the right-click
|
|
4
|
+
* handler that resolves/selects the element under the pointer before opening.
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useState, type RefObject } from "react";
|
|
7
|
+
import type { DomEditSelection } from "./domEditing";
|
|
8
|
+
|
|
9
|
+
export interface CanvasContextMenuState {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
sel: DomEditSelection;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseCanvasContextMenuStateParams {
|
|
16
|
+
selection: DomEditSelection | null;
|
|
17
|
+
selectionRef: RefObject<DomEditSelection | null>;
|
|
18
|
+
hoverSelectionRef: RefObject<DomEditSelection | null>;
|
|
19
|
+
onCanvasPointerMoveRef: RefObject<
|
|
20
|
+
(
|
|
21
|
+
event: React.PointerEvent<HTMLDivElement>,
|
|
22
|
+
options?: { preferClipAncestor?: boolean },
|
|
23
|
+
) => Promise<DomEditSelection | null>
|
|
24
|
+
>;
|
|
25
|
+
onSelectionChangeRef: RefObject<
|
|
26
|
+
(selection: DomEditSelection, options?: { revealPanel?: boolean; additive?: boolean }) => void
|
|
27
|
+
>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useCanvasContextMenuState({
|
|
31
|
+
selection,
|
|
32
|
+
selectionRef,
|
|
33
|
+
hoverSelectionRef,
|
|
34
|
+
onCanvasPointerMoveRef,
|
|
35
|
+
onSelectionChangeRef,
|
|
36
|
+
}: UseCanvasContextMenuStateParams): {
|
|
37
|
+
contextMenu: CanvasContextMenuState | null;
|
|
38
|
+
closeContextMenu: () => void;
|
|
39
|
+
handleContextMenu: (event: React.MouseEvent<HTMLDivElement>) => Promise<void>;
|
|
40
|
+
} {
|
|
41
|
+
// Context menu state: position of the right-click that opened it.
|
|
42
|
+
// contextMenu.sel is the element the menu targets — captured at right-click
|
|
43
|
+
// time so the menu can open even before the React selection state settles.
|
|
44
|
+
const [contextMenu, setContextMenu] = useState<CanvasContextMenuState | null>(null);
|
|
45
|
+
const closeContextMenu = useCallback(() => setContextMenu(null), []);
|
|
46
|
+
|
|
47
|
+
// Close the context menu whenever the selection moves off the element the menu
|
|
48
|
+
// targets (a click that reselects elsewhere, a deselect, or a preview reload
|
|
49
|
+
// that rebuilds the selection). Without this the menu can linger — orphaned —
|
|
50
|
+
// over a stale target after the underlying element is gone. A right-click that
|
|
51
|
+
// OPENS the menu also selects its target, so the common open path keeps the
|
|
52
|
+
// menu (same element) rather than immediately dismissing it.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!contextMenu) return;
|
|
55
|
+
if (!selection || selection.element !== contextMenu.sel.element) {
|
|
56
|
+
setContextMenu(null);
|
|
57
|
+
}
|
|
58
|
+
}, [selection, contextMenu]);
|
|
59
|
+
|
|
60
|
+
// Right-click: select element first (if not already selected), then open menu.
|
|
61
|
+
const handleContextMenu = useCallback(
|
|
62
|
+
async (event: React.MouseEvent<HTMLDivElement>) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
|
|
65
|
+
// If no element is selected yet, resolve it from the pointer position first.
|
|
66
|
+
const currentSel = selectionRef.current;
|
|
67
|
+
let activeSel: DomEditSelection | null = currentSel;
|
|
68
|
+
if (!currentSel) {
|
|
69
|
+
const pointerEvent = event as unknown as React.PointerEvent<HTMLDivElement>;
|
|
70
|
+
const resolved = await onCanvasPointerMoveRef.current(pointerEvent);
|
|
71
|
+
if (!resolved) return; // Nothing under the cursor — skip menu.
|
|
72
|
+
onSelectionChangeRef.current(resolved, { revealPanel: true });
|
|
73
|
+
// Use `resolved` directly: React state (and therefore selectionRef) won't
|
|
74
|
+
// update synchronously after onSelectionChange — we'd be reading stale null.
|
|
75
|
+
activeSel = resolved;
|
|
76
|
+
} else {
|
|
77
|
+
// Check if the user right-clicked on an unselected element (hover target).
|
|
78
|
+
const hover = hoverSelectionRef.current;
|
|
79
|
+
if (hover && hover.element !== currentSel.element) {
|
|
80
|
+
onSelectionChangeRef.current(hover, { revealPanel: true });
|
|
81
|
+
activeSel = hover;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!activeSel) return;
|
|
86
|
+
setContextMenu({ x: event.clientX, y: event.clientY, sel: activeSel });
|
|
87
|
+
},
|
|
88
|
+
[selectionRef, hoverSelectionRef, onCanvasPointerMoveRef, onSelectionChangeRef],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return { contextMenu, closeContextMenu, handleContextMenu };
|
|
92
|
+
}
|