@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
|
@@ -209,6 +209,67 @@ describe("edit history", () => {
|
|
|
209
209
|
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
it("folds a slow GSAP follow-up into the timing edit via a per-entry coalesceMs override", () => {
|
|
213
|
+
const timing = buildEditHistoryEntry({
|
|
214
|
+
projectId: "project-1",
|
|
215
|
+
label: "Resize timeline clip",
|
|
216
|
+
kind: "timeline",
|
|
217
|
+
coalesceKey: "timeline-resize:clip",
|
|
218
|
+
files: { "index.html": { before: "orig", after: "timing" } },
|
|
219
|
+
now: 0,
|
|
220
|
+
id: "timing",
|
|
221
|
+
});
|
|
222
|
+
// The server GSAP rewrite lands ~2s later, past the 300ms default window, but the
|
|
223
|
+
// follow-up carries a large coalesceMs so undo still collapses to a single step.
|
|
224
|
+
const gsap = buildEditHistoryEntry({
|
|
225
|
+
projectId: "project-1",
|
|
226
|
+
label: "Resize timeline clip",
|
|
227
|
+
kind: "timeline",
|
|
228
|
+
coalesceKey: "timeline-resize:clip",
|
|
229
|
+
coalesceMs: 10_000,
|
|
230
|
+
files: { "index.html": { before: "timing", after: "timing+gsap" } },
|
|
231
|
+
now: 2000,
|
|
232
|
+
id: "gsap",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const state = pushEditHistoryEntry(
|
|
236
|
+
pushEditHistoryEntry(createEmptyEditHistory(), timing),
|
|
237
|
+
gsap,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(state.undo).toHaveLength(1);
|
|
241
|
+
expect(state.undo[0].files["index.html"].before).toBe("orig");
|
|
242
|
+
expect(state.undo[0].files["index.html"].after).toBe("timing+gsap");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("does not merge a slow follow-up without the coalesceMs override", () => {
|
|
246
|
+
const timing = buildEditHistoryEntry({
|
|
247
|
+
projectId: "project-1",
|
|
248
|
+
label: "Resize timeline clip",
|
|
249
|
+
kind: "timeline",
|
|
250
|
+
coalesceKey: "timeline-resize:clip",
|
|
251
|
+
files: { "index.html": { before: "orig", after: "timing" } },
|
|
252
|
+
now: 0,
|
|
253
|
+
id: "timing",
|
|
254
|
+
});
|
|
255
|
+
const late = buildEditHistoryEntry({
|
|
256
|
+
projectId: "project-1",
|
|
257
|
+
label: "Resize timeline clip",
|
|
258
|
+
kind: "timeline",
|
|
259
|
+
coalesceKey: "timeline-resize:clip",
|
|
260
|
+
files: { "index.html": { before: "timing", after: "timing+gsap" } },
|
|
261
|
+
now: 2000,
|
|
262
|
+
id: "late",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const state = pushEditHistoryEntry(
|
|
266
|
+
pushEditHistoryEntry(createEmptyEditHistory(), timing),
|
|
267
|
+
late,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(state.undo).toHaveLength(2);
|
|
271
|
+
});
|
|
272
|
+
|
|
212
273
|
it("coalesces entries with the same coalesceKey within the window (prop: format)", () => {
|
|
213
274
|
const first = buildEditHistoryEntry({
|
|
214
275
|
projectId: "project-1",
|
package/src/utils/editHistory.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface EditHistoryEntry {
|
|
|
13
13
|
label: string;
|
|
14
14
|
kind: EditHistoryKind;
|
|
15
15
|
coalesceKey?: string;
|
|
16
|
+
/** Per-entry coalesce window override (ms). Falls back to the reducer default. */
|
|
17
|
+
coalesceMs?: number;
|
|
16
18
|
createdAt: number;
|
|
17
19
|
files: Record<string, EditHistoryFileSnapshot>;
|
|
18
20
|
}
|
|
@@ -35,6 +37,7 @@ export interface BuildEditHistoryEntryInput {
|
|
|
35
37
|
label: string;
|
|
36
38
|
kind?: EditHistoryKind;
|
|
37
39
|
coalesceKey?: string;
|
|
40
|
+
coalesceMs?: number;
|
|
38
41
|
now: number;
|
|
39
42
|
files: Record<string, { before: string; after: string }>;
|
|
40
43
|
}
|
|
@@ -99,6 +102,7 @@ export function buildEditHistoryEntry(input: BuildEditHistoryEntryInput): EditHi
|
|
|
99
102
|
label: input.label,
|
|
100
103
|
kind: input.kind ?? "manual",
|
|
101
104
|
coalesceKey: input.coalesceKey,
|
|
105
|
+
coalesceMs: input.coalesceMs,
|
|
102
106
|
createdAt: input.now,
|
|
103
107
|
files,
|
|
104
108
|
};
|
|
@@ -111,7 +115,9 @@ export function pushEditHistoryEntry(
|
|
|
111
115
|
): EditHistoryState {
|
|
112
116
|
if (Object.keys(entry.files).length === 0) return state;
|
|
113
117
|
|
|
114
|
-
|
|
118
|
+
// The incoming entry's own window wins so a caller can guarantee a merge even when a
|
|
119
|
+
// slow async step (e.g. a server GSAP rewrite) sits between the two records.
|
|
120
|
+
const coalesceMs = entry.coalesceMs ?? options?.coalesceMs ?? DEFAULT_COALESCE_MS;
|
|
115
121
|
const maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
116
122
|
const previous = state.undo[state.undo.length - 1];
|
|
117
123
|
let undo = state.undo;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extendRootDurationInSource } from "./rootDuration";
|
|
3
|
+
|
|
4
|
+
describe("extendRootDurationInSource", () => {
|
|
5
|
+
it("extends data-duration when the new end is bigger than the root duration", () => {
|
|
6
|
+
const source = [
|
|
7
|
+
`<div data-composition-id="main" data-duration="4">`,
|
|
8
|
+
` <section id="clip" data-start="2" data-duration="3"></section>`,
|
|
9
|
+
`</div>`,
|
|
10
|
+
].join("\n");
|
|
11
|
+
|
|
12
|
+
expect(extendRootDurationInSource(source, 5.25)).toContain(
|
|
13
|
+
`data-composition-id="main" data-duration="5.25"`,
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("does nothing when the new end is smaller than or equal to the root duration", () => {
|
|
18
|
+
const source = `<div data-composition-id="main" data-duration="6"></div>`;
|
|
19
|
+
|
|
20
|
+
expect(extendRootDurationInSource(source, 5)).toBe(source);
|
|
21
|
+
expect(extendRootDurationInSource(source, 6)).toBe(source);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("leaves non-root data-duration attributes untouched by the extension", () => {
|
|
25
|
+
const source = [
|
|
26
|
+
`<div data-duration="3"></div>`,
|
|
27
|
+
`<div data-composition-id="main" data-duration="4"></div>`,
|
|
28
|
+
].join("\n");
|
|
29
|
+
const patched = extendRootDurationInSource(source, 7);
|
|
30
|
+
|
|
31
|
+
expect(patched).toContain(`<div data-duration="3"></div>`);
|
|
32
|
+
expect(patched).toContain(`<div data-composition-id="main" data-duration="7"></div>`);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
|
2
|
+
|
|
3
|
+
export function extendRootDurationInSource(source: string, newEnd: number): string {
|
|
4
|
+
const rootDurMatch = source.match(
|
|
5
|
+
/(<[^>]*data-composition-id="[^"]*"[^>]*data-duration=")([^"]*)(")/,
|
|
6
|
+
);
|
|
7
|
+
if (rootDurMatch) {
|
|
8
|
+
const rootDur = parseFloat(rootDurMatch[2]!);
|
|
9
|
+
if (newEnd > rootDur) {
|
|
10
|
+
return source.replace(
|
|
11
|
+
rootDurMatch[0],
|
|
12
|
+
`${rootDurMatch[1]}${formatTimelineAttributeNumber(newEnd)}${rootDurMatch[3]}`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return source;
|
|
17
|
+
}
|
package/src/utils/sdkCutover.ts
CHANGED
|
@@ -196,6 +196,51 @@ export async function sdkTimingPersist(
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
export async function sdkTimingBatchPersist(
|
|
200
|
+
changes: Array<{
|
|
201
|
+
hfId: string;
|
|
202
|
+
timingUpdate: { start?: number; duration?: number; trackIndex?: number };
|
|
203
|
+
}>,
|
|
204
|
+
targetPath: string,
|
|
205
|
+
sdkSession: Composition | null | undefined,
|
|
206
|
+
deps: CutoverDeps,
|
|
207
|
+
options?: CutoverOptions,
|
|
208
|
+
): Promise<boolean> {
|
|
209
|
+
const timingSrc = deps.readProjectFile;
|
|
210
|
+
for (const change of changes) {
|
|
211
|
+
void recordResolverParity(
|
|
212
|
+
sdkSession,
|
|
213
|
+
change.hfId,
|
|
214
|
+
"setTiming",
|
|
215
|
+
timingSrc ? () => timingSrc(targetPath) : undefined,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (!STUDIO_SDK_CUTOVER_ENABLED) return false;
|
|
219
|
+
if (!sdkSession || wrongCompositionFile(deps, targetPath)) return false;
|
|
220
|
+
if (changes.some((change) => !sdkSession.getElement(change.hfId))) return false;
|
|
221
|
+
try {
|
|
222
|
+
const serializedBefore = sdkSession.serialize();
|
|
223
|
+
sdkSession.batch(() => {
|
|
224
|
+
for (const change of changes) sdkSession.setTiming(change.hfId, change.timingUpdate);
|
|
225
|
+
});
|
|
226
|
+
const after = sdkSession.serialize();
|
|
227
|
+
if (after === serializedBefore) return false;
|
|
228
|
+
const undoBefore = await captureOnDiskBefore(deps, targetPath, serializedBefore);
|
|
229
|
+
await persistSdkSerialize(after, targetPath, undoBefore, deps, options);
|
|
230
|
+
trackStudioEvent("sdk_cutover_success", {
|
|
231
|
+
hfId: changes[0]?.hfId ?? null,
|
|
232
|
+
opCount: changes.length,
|
|
233
|
+
});
|
|
234
|
+
return true;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
trackStudioEvent("sdk_cutover_fallback", {
|
|
237
|
+
hfId: changes[0]?.hfId ?? null,
|
|
238
|
+
error: String(err),
|
|
239
|
+
});
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
199
244
|
type SdkGsapTweenOp =
|
|
200
245
|
| { kind: "add"; target: string; spec: GsapTweenSpec }
|
|
201
246
|
| { kind: "set"; animationId: string; properties: Partial<GsapTweenSpec> }
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
|
|
4
4
|
import {
|
|
5
5
|
findMatchingTimelineElementId,
|
|
6
6
|
findTimelineIdByAncestor,
|
|
7
|
+
resolveTimelineIdForSelection,
|
|
7
8
|
resolveTimelineSelectionSeekTime,
|
|
8
9
|
} from "./studioHelpers";
|
|
9
10
|
|
|
@@ -72,3 +73,33 @@ describe("findTimelineIdByAncestor", () => {
|
|
|
72
73
|
expect(findTimelineIdByAncestor(child, [], "index.html")).toBe(null);
|
|
73
74
|
});
|
|
74
75
|
});
|
|
76
|
+
|
|
77
|
+
describe("resolveTimelineIdForSelection", () => {
|
|
78
|
+
const el = (over: Record<string, unknown>) =>
|
|
79
|
+
({ id: "x", start: 0, duration: 1, track: 0, tag: "div", ...over }) as never;
|
|
80
|
+
|
|
81
|
+
it("resolves an ancestor clip against activeCompPath when the selection has no sourceFile", () => {
|
|
82
|
+
// #card (a clip in a sub-composition) > .leaf (selected, not itself a clip)
|
|
83
|
+
const card = document.createElement("div");
|
|
84
|
+
card.id = "card";
|
|
85
|
+
const leaf = document.createElement("span");
|
|
86
|
+
leaf.className = "leaf";
|
|
87
|
+
card.appendChild(leaf);
|
|
88
|
+
|
|
89
|
+
const els = [
|
|
90
|
+
el({
|
|
91
|
+
id: "card",
|
|
92
|
+
domId: "card",
|
|
93
|
+
key: "comps/panel.html#card",
|
|
94
|
+
sourceFile: "comps/panel.html",
|
|
95
|
+
}),
|
|
96
|
+
];
|
|
97
|
+
const selection = { element: leaf } as never;
|
|
98
|
+
|
|
99
|
+
// Falling back to the active comp matches; the old index.html-only fallback would miss.
|
|
100
|
+
expect(resolveTimelineIdForSelection(selection, els, "comps/panel.html")).toBe(
|
|
101
|
+
"comps/panel.html#card",
|
|
102
|
+
);
|
|
103
|
+
expect(resolveTimelineIdForSelection(selection, els, null)).toBe(null);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -13,7 +13,13 @@ export interface AppToast {
|
|
|
13
13
|
tone: "error" | "info";
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export type RightPanelTab =
|
|
16
|
+
export type RightPanelTab =
|
|
17
|
+
| "layers"
|
|
18
|
+
| "design"
|
|
19
|
+
| "renders"
|
|
20
|
+
| "block-params"
|
|
21
|
+
| "slideshow"
|
|
22
|
+
| "variables";
|
|
17
23
|
export type RightInspectorPane = "layers" | "design";
|
|
18
24
|
|
|
19
25
|
export interface RightInspectorPanes {
|
|
@@ -209,6 +215,28 @@ export function findTimelineIdByAncestor(
|
|
|
209
215
|
return null;
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Resolve the timeline element id for a DOM selection: direct match first, then
|
|
220
|
+
* nearest clip ancestor. The ancestor lookup resolves against the selection's own
|
|
221
|
+
* source file, falling back to the active composition path, then index.html — so a
|
|
222
|
+
* sub-composition selection with no explicit sourceFile resolves against the comp
|
|
223
|
+
* currently open, not always the root file.
|
|
224
|
+
*/
|
|
225
|
+
export function resolveTimelineIdForSelection(
|
|
226
|
+
selection: DomEditSelection,
|
|
227
|
+
elements: TimelineElement[],
|
|
228
|
+
activeCompPath: string | null,
|
|
229
|
+
): string | null {
|
|
230
|
+
return (
|
|
231
|
+
findMatchingTimelineElementId(selection, elements) ??
|
|
232
|
+
findTimelineIdByAncestor(
|
|
233
|
+
selection.element,
|
|
234
|
+
elements,
|
|
235
|
+
selection.sourceFile || activeCompPath || "index.html",
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
212
240
|
export function resolveTimelineSelectionSeekTime(
|
|
213
241
|
currentTime: number,
|
|
214
242
|
element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
|
|
@@ -8,12 +8,41 @@ import {
|
|
|
8
8
|
normalizeStudioCompositionPath,
|
|
9
9
|
normalizeStudioUrlPanelTab,
|
|
10
10
|
parseStudioUrlStateFromHash,
|
|
11
|
+
resolveMasterCompositionPath,
|
|
11
12
|
} from "./studioUrlState";
|
|
12
13
|
import { useStudioUrlState } from "../hooks/useStudioUrlState";
|
|
13
14
|
import { usePlayerStore } from "../player";
|
|
14
15
|
|
|
15
16
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
16
17
|
|
|
18
|
+
describe("resolveMasterCompositionPath", () => {
|
|
19
|
+
it("prefers index.html when present", () => {
|
|
20
|
+
expect(resolveMasterCompositionPath(["frames/a.html", "index.html", "b.html"])).toBe(
|
|
21
|
+
"index.html",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("falls back to the first .html when there is no index.html", () => {
|
|
26
|
+
expect(resolveMasterCompositionPath(["notes.md", "card.html", "hero.html"])).toBe("card.html");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns null when the project carries no composition", () => {
|
|
30
|
+
expect(resolveMasterCompositionPath(["notes.md", "styles.css"])).toBeNull();
|
|
31
|
+
expect(resolveMasterCompositionPath([])).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("normalizeStudioUrlPanelTab", () => {
|
|
36
|
+
it("accepts slideshow and variables as valid tabs", () => {
|
|
37
|
+
expect(normalizeStudioUrlPanelTab("slideshow", { inspectorPanelsEnabled: true })).toBe(
|
|
38
|
+
"slideshow",
|
|
39
|
+
);
|
|
40
|
+
expect(normalizeStudioUrlPanelTab("variables", { inspectorPanelsEnabled: true })).toBe(
|
|
41
|
+
"variables",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
17
46
|
function resetPlayerStore() {
|
|
18
47
|
usePlayerStore.setState({
|
|
19
48
|
isPlaying: false,
|
|
@@ -19,7 +19,20 @@ export interface StudioUrlState {
|
|
|
19
19
|
selection: StudioUrlSelectionState | null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const VALID_TABS: RightPanelTab[] = ["layers", "design", "renders"];
|
|
22
|
+
const VALID_TABS: RightPanelTab[] = ["layers", "design", "renders", "slideshow", "variables"];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The composition a schema-level panel (Variables / Slideshow) targets on the
|
|
26
|
+
* master view, where there is no explicit `activeCompPath`. Prefer the
|
|
27
|
+
* `index.html` convention, but fall back to the first `.html` in the file tree
|
|
28
|
+
* (composition-browser order) so projects whose entry file is `card.html`,
|
|
29
|
+
* `hero.html`, etc. don't silently mis-target a non-existent `index.html`.
|
|
30
|
+
* Returns null when the project carries no composition file at all.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveMasterCompositionPath(fileTree: string[]): string | null {
|
|
33
|
+
if (fileTree.includes("index.html")) return "index.html";
|
|
34
|
+
return fileTree.find((p) => p.endsWith(".html")) ?? null;
|
|
35
|
+
}
|
|
23
36
|
|
|
24
37
|
export function normalizeStudioUrlPanelTab(
|
|
25
38
|
tab: RightPanelTab | null,
|
|
@@ -106,6 +119,8 @@ export function readStudioUrlStateFromWindow(): StudioUrlState {
|
|
|
106
119
|
return parseStudioUrlStateFromHash(window.location.hash);
|
|
107
120
|
}
|
|
108
121
|
|
|
122
|
+
// Pre-existing param-assembly complexity — surfaced by this PR's line shifts.
|
|
123
|
+
// fallow-ignore-next-line complexity
|
|
109
124
|
export function buildStudioHash(projectId: string, state: StudioUrlState): string {
|
|
110
125
|
const params = new URLSearchParams();
|
|
111
126
|
|