@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.
- package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-CtTDO63S.js} +1 -1
- package/dist/assets/{index-DfmYkU44.js → index-B_UvTX3E.js} +204 -204
- package/dist/assets/{index-KsfE1bUu.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-Bf1x1y8H.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 +15 -4
- package/dist/index.html +2 -2
- package/dist/index.js +5954 -4474
- 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 +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- 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/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- 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/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +143 -78
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +174 -81
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineEditing.test.tsx +302 -154
- package/src/hooks/useTimelineEditing.ts +97 -93
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +35 -3
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- 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 +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- 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/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- 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 +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -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/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
LAYER_REVEAL_PRIOR_POSITION_ATTR,
|
|
4
|
+
LAYER_REVEAL_PRIOR_Z_ATTR,
|
|
5
|
+
} from "../../player/lib/timelineElementHelpers";
|
|
6
|
+
import { readEffectiveZIndex } from "./canvasContextMenuZOrder";
|
|
7
|
+
|
|
8
|
+
/** The lifted paint order — far above any authored z. Only the RENDERER sees
|
|
9
|
+
* it: every studio z reader is reveal-transparent (readLayerRevealPriorZ). */
|
|
10
|
+
export const LAYER_REVEAL_LIFT_Z = "2147483000";
|
|
11
|
+
export const LAYER_REVEAL_PENDING_COMMIT_ATTR = "data-hf-studio-reveal-pending-commit";
|
|
12
|
+
|
|
13
|
+
interface RevealedNode {
|
|
14
|
+
element: HTMLElement;
|
|
15
|
+
priors: { display: string; visibility: string; opacity: string };
|
|
16
|
+
/** Values THIS override wrote — restore only while they are still in place. */
|
|
17
|
+
applied: { display?: string; visibility?: string; opacity?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RevealLift {
|
|
21
|
+
priors: { zIndex: string; position: string };
|
|
22
|
+
positionLifted: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type PendingRevealCommit = {
|
|
26
|
+
token: string;
|
|
27
|
+
releasedPriors?: RevealLift["priors"];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type LayerRevealCommitOwnership = {
|
|
31
|
+
token: string;
|
|
32
|
+
priorZ: string;
|
|
33
|
+
priorPosition: string | null;
|
|
34
|
+
activeLiftStyles: RevealLift["priors"];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let revealCommitSequence = 0;
|
|
38
|
+
const zPersistCounts = new WeakMap<HTMLElement, number>();
|
|
39
|
+
const REVEAL_RETRY_MS = 16;
|
|
40
|
+
|
|
41
|
+
/** Prevent delayed reveals from capturing an optimistic z as authored state. */
|
|
42
|
+
export function beginLayerZPersist(element: HTMLElement): () => void {
|
|
43
|
+
zPersistCounts.set(element, (zPersistCounts.get(element) ?? 0) + 1);
|
|
44
|
+
let released = false;
|
|
45
|
+
return () => {
|
|
46
|
+
if (released) return;
|
|
47
|
+
released = true;
|
|
48
|
+
const remaining = (zPersistCounts.get(element) ?? 1) - 1;
|
|
49
|
+
if (remaining > 0) zPersistCounts.set(element, remaining);
|
|
50
|
+
else zPersistCounts.delete(element);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface RevealState {
|
|
55
|
+
/** The layer element the reveal was applied for (deselect detection). */
|
|
56
|
+
base: HTMLElement;
|
|
57
|
+
nodes: RevealedNode[];
|
|
58
|
+
lift: RevealLift | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function restoreInline(el: HTMLElement, property: string, prior: string): void {
|
|
62
|
+
if (prior) el.style.setProperty(property, prior);
|
|
63
|
+
else el.style.removeProperty(property);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// This decoder owns the complete serialized reveal-commit schema. Splitting
|
|
67
|
+
// field checks across helpers would create multiple authorities for validity.
|
|
68
|
+
// fallow-ignore-next-line complexity
|
|
69
|
+
function readPendingRevealCommit(element: HTMLElement): PendingRevealCommit | null {
|
|
70
|
+
const raw = element.getAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR);
|
|
71
|
+
if (!raw) return null;
|
|
72
|
+
try {
|
|
73
|
+
const value: unknown = JSON.parse(raw);
|
|
74
|
+
if (typeof value !== "object" || value === null || !("token" in value)) return null;
|
|
75
|
+
const token = value.token;
|
|
76
|
+
if (typeof token !== "string") return null;
|
|
77
|
+
if (!("releasedPriors" in value) || value.releasedPriors === undefined) return { token };
|
|
78
|
+
const releasedPriors = value.releasedPriors;
|
|
79
|
+
if (
|
|
80
|
+
typeof releasedPriors !== "object" ||
|
|
81
|
+
releasedPriors === null ||
|
|
82
|
+
!("zIndex" in releasedPriors) ||
|
|
83
|
+
!("position" in releasedPriors)
|
|
84
|
+
) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const { zIndex, position } = releasedPriors;
|
|
88
|
+
return typeof zIndex === "string" && typeof position === "string"
|
|
89
|
+
? { token, releasedPriors: { zIndex, position } }
|
|
90
|
+
: null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writePendingRevealCommit(element: HTMLElement, state: PendingRevealCommit): void {
|
|
97
|
+
element.setAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR, JSON.stringify(state));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Hand an active reveal lift to one pending durable z commit. */
|
|
101
|
+
export function beginLayerRevealCommit(element: HTMLElement): LayerRevealCommitOwnership | null {
|
|
102
|
+
const priorZ = element.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);
|
|
103
|
+
if (priorZ == null) return null;
|
|
104
|
+
const ownership = {
|
|
105
|
+
token: `reveal-z-${revealCommitSequence++}`,
|
|
106
|
+
priorZ,
|
|
107
|
+
priorPosition: element.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR),
|
|
108
|
+
activeLiftStyles: { zIndex: element.style.zIndex, position: element.style.position },
|
|
109
|
+
};
|
|
110
|
+
writePendingRevealCommit(element, { token: ownership.token });
|
|
111
|
+
element.removeAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);
|
|
112
|
+
element.removeAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR);
|
|
113
|
+
return ownership;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Durable success consumes the reveal regardless of whether selection released it meanwhile. */
|
|
117
|
+
export function completeLayerRevealCommit(
|
|
118
|
+
element: HTMLElement,
|
|
119
|
+
ownership: LayerRevealCommitOwnership,
|
|
120
|
+
): void {
|
|
121
|
+
if (readPendingRevealCommit(element)?.token === ownership.token) {
|
|
122
|
+
element.removeAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Roll back only the reveal transaction this commit owns. An active reveal gets
|
|
128
|
+
* its temporary lift back; a reveal released while persistence was pending gets
|
|
129
|
+
* its exact authored inline styles back, with no resurrected metadata.
|
|
130
|
+
*/
|
|
131
|
+
export function rollbackLayerRevealCommit(
|
|
132
|
+
element: HTMLElement,
|
|
133
|
+
ownership: LayerRevealCommitOwnership,
|
|
134
|
+
): void {
|
|
135
|
+
const pending = readPendingRevealCommit(element);
|
|
136
|
+
if (pending?.token !== ownership.token) return;
|
|
137
|
+
element.removeAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR);
|
|
138
|
+
element.removeAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);
|
|
139
|
+
element.removeAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR);
|
|
140
|
+
if (pending.releasedPriors) {
|
|
141
|
+
restoreInline(element, "z-index", pending.releasedPriors.zIndex);
|
|
142
|
+
restoreInline(element, "position", pending.releasedPriors.position);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
restoreInline(element, "z-index", ownership.activeLiftStyles.zIndex);
|
|
146
|
+
restoreInline(element, "position", ownership.activeLiftStyles.position);
|
|
147
|
+
element.setAttribute(LAYER_REVEAL_PRIOR_Z_ATTR, ownership.priorZ);
|
|
148
|
+
if (ownership.priorPosition != null) {
|
|
149
|
+
element.setAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR, ownership.priorPosition);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Restore a property ONLY when its current inline value is still the one this
|
|
154
|
+
* override wrote — a later real edit (commit, animation seek) is the new
|
|
155
|
+
* truth and must not be clobbered. */
|
|
156
|
+
function restoreIfOurs(
|
|
157
|
+
el: HTMLElement,
|
|
158
|
+
property: "display" | "visibility" | "opacity",
|
|
159
|
+
applied: string | undefined,
|
|
160
|
+
prior: string,
|
|
161
|
+
): void {
|
|
162
|
+
if (applied == null) return;
|
|
163
|
+
if (el.style.getPropertyValue(property) !== applied) return;
|
|
164
|
+
restoreInline(el, property, prior);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** What hides this node at the current frame, per computed style. */
|
|
168
|
+
function readHideSignals(el: HTMLElement, win: Window) {
|
|
169
|
+
const computed = win.getComputedStyle(el);
|
|
170
|
+
const opacity = Number.parseFloat(computed.opacity);
|
|
171
|
+
return {
|
|
172
|
+
display: computed.display === "none",
|
|
173
|
+
visibility: computed.visibility === "hidden" || computed.visibility === "collapse",
|
|
174
|
+
opacity: Number.isFinite(opacity) && opacity <= 0.01,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Force one hidden node visible with inline styles; returns priors + applied. */
|
|
179
|
+
function revealNode(
|
|
180
|
+
el: HTMLElement,
|
|
181
|
+
win: Window,
|
|
182
|
+
needs: ReturnType<typeof readHideSignals>,
|
|
183
|
+
): RevealedNode {
|
|
184
|
+
const priors = {
|
|
185
|
+
display: el.style.display,
|
|
186
|
+
visibility: el.style.visibility,
|
|
187
|
+
opacity: el.style.opacity,
|
|
188
|
+
};
|
|
189
|
+
const applied: RevealedNode["applied"] = {};
|
|
190
|
+
if (needs.display) {
|
|
191
|
+
// Prefer whatever the stylesheet says once the inline hide is lifted;
|
|
192
|
+
// only force block when the sheet itself hides it.
|
|
193
|
+
el.style.removeProperty("display");
|
|
194
|
+
if (win.getComputedStyle(el).display === "none") el.style.display = "block";
|
|
195
|
+
applied.display = el.style.display;
|
|
196
|
+
}
|
|
197
|
+
if (needs.visibility) {
|
|
198
|
+
el.style.visibility = "visible";
|
|
199
|
+
applied.visibility = "visible";
|
|
200
|
+
}
|
|
201
|
+
if (needs.opacity) {
|
|
202
|
+
el.style.opacity = "1";
|
|
203
|
+
applied.opacity = "1";
|
|
204
|
+
}
|
|
205
|
+
return { element: el, priors, applied };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Walk `element` → body, force-revealing every hiding node; returns the touched nodes. */
|
|
209
|
+
function revealHiddenChain(element: HTMLElement): RevealedNode[] {
|
|
210
|
+
const doc = element.ownerDocument;
|
|
211
|
+
const win = doc.defaultView;
|
|
212
|
+
if (!win) return [];
|
|
213
|
+
const nodes: RevealedNode[] = [];
|
|
214
|
+
let el: HTMLElement | null = element;
|
|
215
|
+
while (el && el !== doc.body && el !== doc.documentElement) {
|
|
216
|
+
let needs: ReturnType<typeof readHideSignals>;
|
|
217
|
+
try {
|
|
218
|
+
needs = readHideSignals(el, win);
|
|
219
|
+
} catch {
|
|
220
|
+
break; // detached / cross-realm — leave the rest alone
|
|
221
|
+
}
|
|
222
|
+
if (needs.display || needs.visibility || needs.opacity) nodes.push(revealNode(el, win, needs));
|
|
223
|
+
el = el.parentElement;
|
|
224
|
+
}
|
|
225
|
+
return nodes;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Lift the selected element to the TOP of the paint order while selected —
|
|
230
|
+
* regardless of its authored z or panel position. The true z is parked in
|
|
231
|
+
* LAYER_REVEAL_PRIOR_Z_ATTR so every studio z reader keeps reporting it (the
|
|
232
|
+
* lift is invisible to menus, badges, the lane mirror, and the panel sort);
|
|
233
|
+
* only the renderer sees the lifted inline value. A static element gets a
|
|
234
|
+
* temporary position:relative (layout-preserving) so the z applies, with the
|
|
235
|
+
* prior position parked in LAYER_REVEAL_PRIOR_POSITION_ATTR for the z-commit's
|
|
236
|
+
* static check. Exported for direct unit testing.
|
|
237
|
+
*/
|
|
238
|
+
export function liftElementToTop(element: HTMLElement): RevealLift | null {
|
|
239
|
+
const win = element.ownerDocument.defaultView;
|
|
240
|
+
if (!win) return null;
|
|
241
|
+
const priors = { zIndex: element.style.zIndex, position: element.style.position };
|
|
242
|
+
let positionLifted = false;
|
|
243
|
+
try {
|
|
244
|
+
element.setAttribute(LAYER_REVEAL_PRIOR_Z_ATTR, String(readEffectiveZIndex(element)));
|
|
245
|
+
if (win.getComputedStyle(element).position === "static") {
|
|
246
|
+
element.setAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR, "static");
|
|
247
|
+
element.style.position = "relative";
|
|
248
|
+
positionLifted = true;
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
element.removeAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);
|
|
252
|
+
return null; // detached / cross-realm — no lift
|
|
253
|
+
}
|
|
254
|
+
element.style.zIndex = LAYER_REVEAL_LIFT_Z;
|
|
255
|
+
return { priors, positionLifted };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Undo an active lift. Skipped entirely when the prior-z attribute is gone —
|
|
260
|
+
* a z-reorder commit consumed the lift (handleDomZIndexReorderCommit removes
|
|
261
|
+
* the attributes and writes the new real z), and that commit is the truth.
|
|
262
|
+
* Exported for direct unit testing.
|
|
263
|
+
*/
|
|
264
|
+
export function restoreLiftedElement(element: HTMLElement, lift: RevealLift): void {
|
|
265
|
+
if (!element.hasAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)) {
|
|
266
|
+
const pending = readPendingRevealCommit(element);
|
|
267
|
+
if (pending && !pending.releasedPriors) {
|
|
268
|
+
// Persistence temporarily owns the live z. Hand it the exact authored
|
|
269
|
+
// priors so a later rejection can restore them after this hook lets go.
|
|
270
|
+
writePendingRevealCommit(element, { ...pending, releasedPriors: lift.priors });
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
element.removeAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);
|
|
275
|
+
element.removeAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR);
|
|
276
|
+
if (element.style.zIndex === LAYER_REVEAL_LIFT_Z) {
|
|
277
|
+
restoreInline(element, "z-index", lift.priors.zIndex);
|
|
278
|
+
}
|
|
279
|
+
if (lift.positionLifted && element.style.position === "relative") {
|
|
280
|
+
restoreInline(element, "position", lift.priors.position);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Temporary "show me this element" override for the Layers panel
|
|
286
|
+
* (Webflow-navigator style): clicking a layer forces it (and every hiding
|
|
287
|
+
* ancestor up to the body) visible with LIVE inline styles, and paints it on
|
|
288
|
+
* TOP of the stack while selected (see liftElementToTop).
|
|
289
|
+
*
|
|
290
|
+
* Strictly ephemeral by construction:
|
|
291
|
+
* - Exact prior inline values are recorded per touched node and restored on
|
|
292
|
+
* every exit path — reveal of a different layer, deselect, playback start,
|
|
293
|
+
* unmount. Nothing is ever sent to a persist path, and each property is
|
|
294
|
+
* restored only while it still holds the value this override wrote.
|
|
295
|
+
* - A post-edit iframe reload replaces the DOM; detached nodes are skipped on
|
|
296
|
+
* restore (the fresh document never had the override).
|
|
297
|
+
* - Scrubbing/playing lets the runtime and GSAP rewrite these same inline
|
|
298
|
+
* styles — that is the animation showing reality, and the override is
|
|
299
|
+
* dropped on play for exactly that reason.
|
|
300
|
+
*/
|
|
301
|
+
export function useLayerRevealOverride({
|
|
302
|
+
isPlaying,
|
|
303
|
+
selectedElement,
|
|
304
|
+
}: {
|
|
305
|
+
isPlaying: boolean;
|
|
306
|
+
selectedElement: HTMLElement | null;
|
|
307
|
+
}): {
|
|
308
|
+
scheduleReveal: (element: HTMLElement, delayMs: number) => void;
|
|
309
|
+
} {
|
|
310
|
+
const stateRef = useRef<RevealState | null>(null);
|
|
311
|
+
const pendingRevealRef = useRef<{ timer: number; element: HTMLElement } | null>(null);
|
|
312
|
+
const currentRef = useRef({ isPlaying, selectedElement });
|
|
313
|
+
currentRef.current = { isPlaying, selectedElement };
|
|
314
|
+
|
|
315
|
+
const restoreReveal = useCallback(() => {
|
|
316
|
+
const state = stateRef.current;
|
|
317
|
+
stateRef.current = null;
|
|
318
|
+
if (!state) return;
|
|
319
|
+
for (const { element, priors, applied } of state.nodes) {
|
|
320
|
+
if (!element.isConnected) continue;
|
|
321
|
+
restoreIfOurs(element, "display", applied.display, priors.display);
|
|
322
|
+
restoreIfOurs(element, "visibility", applied.visibility, priors.visibility);
|
|
323
|
+
restoreIfOurs(element, "opacity", applied.opacity, priors.opacity);
|
|
324
|
+
}
|
|
325
|
+
if (state.lift && state.base.isConnected) restoreLiftedElement(state.base, state.lift);
|
|
326
|
+
}, []);
|
|
327
|
+
|
|
328
|
+
const reveal = useCallback(
|
|
329
|
+
(element: HTMLElement) => {
|
|
330
|
+
restoreReveal();
|
|
331
|
+
const nodes = revealHiddenChain(element);
|
|
332
|
+
const lift = liftElementToTop(element);
|
|
333
|
+
if (nodes.length > 0 || lift) stateRef.current = { base: element, nodes, lift };
|
|
334
|
+
},
|
|
335
|
+
[restoreReveal],
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const cancelScheduledReveal = useCallback(() => {
|
|
339
|
+
const pending = pendingRevealRef.current;
|
|
340
|
+
pendingRevealRef.current = null;
|
|
341
|
+
if (pending) window.clearTimeout(pending.timer);
|
|
342
|
+
}, []);
|
|
343
|
+
|
|
344
|
+
const scheduleReveal = useCallback(
|
|
345
|
+
(element: HTMLElement, delayMs: number) => {
|
|
346
|
+
cancelScheduledReveal();
|
|
347
|
+
const current = currentRef.current;
|
|
348
|
+
if (current.isPlaying) return;
|
|
349
|
+
const attemptReveal = () => {
|
|
350
|
+
const latest = currentRef.current;
|
|
351
|
+
if (latest.isPlaying || latest.selectedElement !== element || !element.isConnected) {
|
|
352
|
+
pendingRevealRef.current = null;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (zPersistCounts.has(element)) {
|
|
356
|
+
const timer = window.setTimeout(attemptReveal, REVEAL_RETRY_MS);
|
|
357
|
+
pendingRevealRef.current = { timer, element };
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
pendingRevealRef.current = null;
|
|
361
|
+
reveal(element);
|
|
362
|
+
};
|
|
363
|
+
const timer = window.setTimeout(attemptReveal, delayMs);
|
|
364
|
+
pendingRevealRef.current = { timer, element };
|
|
365
|
+
},
|
|
366
|
+
[cancelScheduledReveal, reveal],
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Selection and playback jointly own eligibility for both pending and active
|
|
370
|
+
// reveals. Cancel first; playback or a different selection then restores the
|
|
371
|
+
// animation/authored styles.
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
const pending = pendingRevealRef.current;
|
|
374
|
+
if (isPlaying || (pending && pending.element !== selectedElement)) cancelScheduledReveal();
|
|
375
|
+
const base = stateRef.current?.base;
|
|
376
|
+
if (isPlaying || (base && selectedElement !== base)) restoreReveal();
|
|
377
|
+
}, [cancelScheduledReveal, isPlaying, restoreReveal, selectedElement]);
|
|
378
|
+
|
|
379
|
+
// Unmount: never leave a timer or override behind.
|
|
380
|
+
useEffect(
|
|
381
|
+
() => () => {
|
|
382
|
+
cancelScheduledReveal();
|
|
383
|
+
restoreReveal();
|
|
384
|
+
},
|
|
385
|
+
[cancelScheduledReveal, restoreReveal],
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
return { scheduleReveal };
|
|
389
|
+
}
|
|
@@ -19,6 +19,9 @@ import { readStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
|
19
19
|
import { readHfId, type DomEditSelection } from "../editor/domEditing";
|
|
20
20
|
import { buildStableSelector } from "../editor/domEditingDom";
|
|
21
21
|
import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
|
|
22
|
+
import { zReorderCoalesceKey } from "../../hooks/useElementLifecycleOps";
|
|
23
|
+
import { useCanvasZOrderTimelineMirror } from "./useCanvasZOrderTimelineMirror";
|
|
24
|
+
import { runZLaneGesture } from "./zLaneGesture";
|
|
22
25
|
import type { BlockPreviewInfo } from "../sidebar/BlocksTab";
|
|
23
26
|
import type { GestureRecordingState } from "../editor/GestureRecordControl";
|
|
24
27
|
import type { ReactNode } from "react";
|
|
@@ -158,6 +161,7 @@ export function PreviewOverlays({
|
|
|
158
161
|
handleDomEditElementDelete,
|
|
159
162
|
handleDomZIndexReorderCommit,
|
|
160
163
|
} = useDomEditActionsContext();
|
|
164
|
+
const mirrorZOrderToTimeline = useCanvasZOrderTimelineMirror();
|
|
161
165
|
|
|
162
166
|
// fallow-ignore-next-line complexity
|
|
163
167
|
const [snapPrefs, setSnapPrefs] = useState(() => {
|
|
@@ -224,7 +228,7 @@ export function PreviewOverlays({
|
|
|
224
228
|
onRotationCommit={handleDomRotationCommit}
|
|
225
229
|
onStyleCommit={handleDomStyleCommit}
|
|
226
230
|
onDeleteSelection={handleDomEditElementDelete}
|
|
227
|
-
onApplyZIndex={(sel, patches, action) => {
|
|
231
|
+
onApplyZIndex={(sel, patches, action, crossed) => {
|
|
228
232
|
const { entries, dropped } = resolveZIndexEntries(sel, patches);
|
|
229
233
|
if (dropped.length > 0) {
|
|
230
234
|
// These siblings can't be written to source. Apply their live z
|
|
@@ -237,7 +241,28 @@ export function PreviewOverlays({
|
|
|
237
241
|
dropped.map((patch) => describeZIndexElement(patch.element)).join(", "),
|
|
238
242
|
);
|
|
239
243
|
}
|
|
240
|
-
if (entries.length
|
|
244
|
+
if (entries.length === 0) return;
|
|
245
|
+
// Shared undo coalesce key: passed to BOTH the z persist and the
|
|
246
|
+
// timeline lane mirror below so editHistory folds the two records
|
|
247
|
+
// into one undo entry (same value handleDomZIndexReorderCommit would
|
|
248
|
+
// default to — passed explicitly so the mirror shares it by
|
|
249
|
+
// construction, not by formula duplication).
|
|
250
|
+
const coalesceKey = zReorderCoalesceKey(entries, action);
|
|
251
|
+
// One serialized z→lane transaction: the mirror runs only AFTER the
|
|
252
|
+
// z commit resolved AND reported durable targets, and a second rapid
|
|
253
|
+
// gesture cannot interleave between the two phases — see
|
|
254
|
+
// runZLaneGesture. A failed z commit already toasted + rolled back.
|
|
255
|
+
runZLaneGesture({
|
|
256
|
+
commitZ: () => handleDomZIndexReorderCommit(entries, coalesceKey, action),
|
|
257
|
+
mirror: () =>
|
|
258
|
+
mirrorZOrderToTimeline({
|
|
259
|
+
selectionKey: entries.find((e) => e.element === sel.element)?.key,
|
|
260
|
+
action,
|
|
261
|
+
crossed,
|
|
262
|
+
sourceFile: sel.sourceFile,
|
|
263
|
+
coalesceKey,
|
|
264
|
+
}),
|
|
265
|
+
}).catch(() => undefined);
|
|
241
266
|
}}
|
|
242
267
|
gridVisible={snapPrefs.gridVisible}
|
|
243
268
|
gridSpacing={snapPrefs.gridSpacing}
|
|
@@ -22,6 +22,7 @@ describe("TimelinePane move wrapper", () => {
|
|
|
22
22
|
"clip-lane-move:7",
|
|
23
23
|
"track-insert",
|
|
24
24
|
onMoveElements,
|
|
25
|
+
Number.POSITIVE_INFINITY,
|
|
25
26
|
);
|
|
26
27
|
expect(onMoveElements).toHaveBeenCalledWith(
|
|
27
28
|
[
|
|
@@ -32,6 +33,8 @@ describe("TimelinePane move wrapper", () => {
|
|
|
32
33
|
],
|
|
33
34
|
"clip-lane-move:7",
|
|
34
35
|
"track-insert",
|
|
36
|
+
// The per-gesture coalesce window rides along with the shared key.
|
|
37
|
+
Number.POSITIVE_INFINITY,
|
|
35
38
|
);
|
|
36
39
|
});
|
|
37
40
|
|
|
@@ -21,7 +21,9 @@ export function forwardRebasedTimelineMoveElements(
|
|
|
21
21
|
edits: TimelineMoveEdit[],
|
|
22
22
|
coalesceKey?: string,
|
|
23
23
|
operation?: TimelineMoveOperation,
|
|
24
|
+
coalesceMs?: number,
|
|
24
25
|
) => Promise<void> | void,
|
|
26
|
+
coalesceMs?: number,
|
|
25
27
|
) {
|
|
26
28
|
return onMoveElements(
|
|
27
29
|
edits.map(({ element, updates }) => {
|
|
@@ -34,6 +36,7 @@ export function forwardRebasedTimelineMoveElements(
|
|
|
34
36
|
}),
|
|
35
37
|
coalesceKey,
|
|
36
38
|
operation,
|
|
39
|
+
coalesceMs,
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -160,6 +163,7 @@ export function TimelinePane({
|
|
|
160
163
|
edits: Array<{ element: TimelineElement; updates: Pick<TimelineElement, "start" | "track"> }>,
|
|
161
164
|
coalesceKey?: string,
|
|
162
165
|
operation?: TimelineMoveOperation,
|
|
166
|
+
coalesceMs?: number,
|
|
163
167
|
) => {
|
|
164
168
|
// Match the sibling handlers: report the telemetry when the batch touches at
|
|
165
169
|
// least one expanded sub-comp child (the clips being rebased to local coords).
|
|
@@ -167,7 +171,13 @@ export function TimelinePane({
|
|
|
167
171
|
trackStudioExpandedClipEdit({ action: "move" });
|
|
168
172
|
}
|
|
169
173
|
if (!onMoveElements) return;
|
|
170
|
-
return forwardRebasedTimelineMoveElements(
|
|
174
|
+
return forwardRebasedTimelineMoveElements(
|
|
175
|
+
edits,
|
|
176
|
+
coalesceKey,
|
|
177
|
+
operation,
|
|
178
|
+
onMoveElements,
|
|
179
|
+
coalesceMs,
|
|
180
|
+
);
|
|
171
181
|
},
|
|
172
182
|
[onMoveElements],
|
|
173
183
|
);
|