@hyperframes/studio 0.8.24 → 0.8.26
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-CTQCs_KG.js → hyperframes-player-C-CArn13.js} +1 -1
- package/dist/assets/{index-okdE01ky.js → index-BIXyJgK5.js} +1 -1
- package/dist/assets/{index-Bq3M0sjr.js → index-D5yni1t6.js} +218 -218
- package/dist/assets/index-DnRfAiK2.css +1 -0
- package/dist/assets/{index-9WJFcXTP.js → index-pfeP2Y63.js} +1 -1
- package/dist/{chunk-6BT6DTB4.js → chunk-A7S5SGNA.js} +2 -2
- package/dist/chunk-A7S5SGNA.js.map +1 -0
- package/dist/{domEditingLayers-URA7BLWE.js → domEditingLayers-JMMVOGTO.js} +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.html +2 -2
- package/dist/index.js +6230 -5019
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/editor/TopologyLens.test.tsx +294 -0
- package/src/components/editor/TopologyLens.tsx +206 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayGeometryBatch.test.ts +18 -0
- package/src/components/editor/domEditingLayers.test.ts +9 -0
- package/src/components/editor/domEditingLayers.ts +8 -8
- package/src/components/editor/topologyLensGeometry.test.ts +44 -0
- package/src/components/editor/topologyLensGeometry.ts +119 -0
- package/src/components/editor/topologyLensState.test.ts +116 -0
- package/src/components/editor/topologyLensState.ts +72 -0
- package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +126 -0
- package/src/components/nle/PreviewOverlays.tsx +24 -18
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +16 -0
- package/src/components/sidebar/CompositionsTab.tsx +10 -3
- package/src/components/ui/HyperframesLoader.tsx +3 -48
- package/src/components/ui/HyperframesMark.tsx +53 -0
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/hooks/domEditCommitRunner.test.ts +59 -0
- package/src/hooks/domEditCommitRunner.ts +55 -20
- package/src/hooks/domEditCommitTypes.ts +8 -1
- package/src/hooks/domEditTextCommitPlan.ts +47 -0
- package/src/hooks/timelineTrackVisibility.test.ts +7 -1
- package/src/hooks/timelineTrackVisibility.ts +5 -5
- package/src/hooks/useDomEditAttributeCommits.ts +4 -1
- package/src/hooks/useDomEditCommits.test.tsx +243 -8
- package/src/hooks/useDomEditCommits.ts +69 -14
- package/src/hooks/useDomEditPositionPatchCommit.test.tsx +3 -4
- package/src/hooks/useDomEditPositionPatchCommit.ts +27 -29
- package/src/hooks/useDomEditSession.ts +6 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +86 -2
- package/src/hooks/useDomEditTextCommits.ts +64 -86
- package/src/hooks/useDomEditWiring.ts +1 -1
- package/src/hooks/useDomSelection.ts +2 -0
- package/src/hooks/useDomSelectionTypes.ts +2 -0
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +71 -4
- package/src/hooks/useExternalFileChangeCoordinator.ts +18 -9
- package/src/hooks/useGsapAnimationFetchFallback.test.ts +13 -1
- package/src/hooks/useGsapAnimationFetchFallback.ts +7 -3
- package/src/hooks/useGsapAnimationOps.test.tsx +101 -0
- package/src/hooks/useGsapAnimationOps.ts +4 -4
- package/src/hooks/useGsapAwareEditing.test.tsx +30 -2
- package/src/hooks/useGsapAwareEditing.ts +31 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +3 -1
- package/src/hooks/useGsapSelectionHandlers.ts +21 -11
- package/src/hooks/useRenderClipContent.test.ts +27 -0
- package/src/hooks/useRenderClipContent.ts +5 -0
- package/src/hooks/useStudioExternalFileChanges.ts +5 -0
- package/src/player/components/CompositionThumbnail.test.ts +66 -1
- package/src/player/components/CompositionThumbnail.tsx +6 -0
- package/src/player/hooks/timelineSyncHydration.ts +10 -2
- package/src/player/store/playerStore.test.ts +24 -0
- package/src/player/store/thumbnailSlice.ts +6 -0
- package/src/styles/studio.css +179 -0
- package/src/utils/domEditSaveQueue.test.ts +5 -3
- package/src/utils/domEditSaveQueue.ts +6 -1
- package/src/webmcp/StudioAgentTools.test.ts +70 -0
- package/src/webmcp/StudioAgentTools.tsx +89 -21
- package/src/webmcp/handles.test.ts +172 -15
- package/src/webmcp/handles.ts +152 -35
- package/src/webmcp/tools/animationTools.test.ts +241 -60
- package/src/webmcp/tools/animationTools.ts +191 -104
- package/src/webmcp/tools/contentTools.test.ts +103 -41
- package/src/webmcp/tools/contentTools.ts +149 -105
- package/src/webmcp/tools/inspectTools.test.ts +47 -5
- package/src/webmcp/tools/inspectTools.ts +59 -19
- package/src/webmcp/tools/lookTools.test.ts +185 -38
- package/src/webmcp/tools/lookTools.ts +134 -21
- package/src/webmcp/tools/selectionTools.test.ts +105 -9
- package/src/webmcp/tools/selectionTools.ts +34 -19
- package/src/webmcp/tools/targetedWriteTools.test.ts +125 -0
- package/src/webmcp/tools/transformTools.test.ts +140 -54
- package/src/webmcp/tools/transformTools.ts +215 -56
- package/src/webmcp/useStudioAgentTools.test.tsx +310 -10
- package/src/webmcp/useStudioAgentTools.ts +27 -15
- package/src/webmcp/webmcpTestUtils.ts +37 -0
- package/src/webmcp/writeCoordinator.test.ts +375 -0
- package/src/webmcp/writeCoordinator.ts +473 -0
- package/dist/assets/index-yGhfxxoL.css +0 -1
- package/dist/chunk-6BT6DTB4.js.map +0 -1
- /package/dist/{domEditingLayers-URA7BLWE.js.map → domEditingLayers-JMMVOGTO.js.map} +0 -0
package/src/webmcp/handles.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { TimelineElement } from "../player/store/timelineElement";
|
|
16
|
+
import { findElementForSelection } from "../components/editor/domEditingElement";
|
|
16
17
|
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
17
18
|
|
|
18
19
|
const SEPARATOR = ":";
|
|
@@ -24,10 +25,13 @@ const INDEX_SEPARATOR = "#";
|
|
|
24
25
|
* knowing about either.
|
|
25
26
|
*/
|
|
26
27
|
export interface ElementAddress {
|
|
28
|
+
projectId?: string | null;
|
|
27
29
|
hfId?: string;
|
|
28
30
|
domId?: string | null;
|
|
29
31
|
selector?: string;
|
|
30
32
|
selectorIndex?: number;
|
|
33
|
+
sourceFile?: string;
|
|
34
|
+
activeCompositionPath?: string | null;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -35,37 +39,108 @@ export interface ElementAddress {
|
|
|
35
39
|
* `data-hf-id` survives edits that renumber or reorder; a bare selector does not.
|
|
36
40
|
*/
|
|
37
41
|
export function mintElementHandle(address: ElementAddress): string | null {
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
const scoped = (value: string) => {
|
|
43
|
+
if (!address.sourceFile || !address.activeCompositionPath) return value;
|
|
44
|
+
if (address.projectId) {
|
|
45
|
+
return [
|
|
46
|
+
"v2",
|
|
47
|
+
encodeURIComponent(address.projectId),
|
|
48
|
+
encodeURIComponent(address.activeCompositionPath),
|
|
49
|
+
encodeURIComponent(address.sourceFile),
|
|
50
|
+
encodeURIComponent(value),
|
|
51
|
+
].join(SEPARATOR);
|
|
52
|
+
}
|
|
53
|
+
return [
|
|
54
|
+
"v1",
|
|
55
|
+
encodeURIComponent(address.activeCompositionPath),
|
|
56
|
+
encodeURIComponent(address.sourceFile),
|
|
57
|
+
encodeURIComponent(value),
|
|
58
|
+
].join(SEPARATOR);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (address.hfId) return `hf${SEPARATOR}${scoped(address.hfId)}`;
|
|
62
|
+
if (address.domId) return `dom${SEPARATOR}${scoped(address.domId)}`;
|
|
40
63
|
if (address.selector) {
|
|
41
64
|
const index = address.selectorIndex ?? 0;
|
|
42
|
-
return `sel${SEPARATOR}${address.selector}${INDEX_SEPARATOR}${index}`;
|
|
65
|
+
return `sel${SEPARATOR}${scoped(address.selector)}${INDEX_SEPARATOR}${index}`;
|
|
43
66
|
}
|
|
44
67
|
return null;
|
|
45
68
|
}
|
|
46
69
|
|
|
47
|
-
export function timelineElementAddress(
|
|
70
|
+
export function timelineElementAddress(
|
|
71
|
+
element: TimelineElement,
|
|
72
|
+
activeCompositionPath: string | null = "index.html",
|
|
73
|
+
projectId?: string | null,
|
|
74
|
+
): ElementAddress {
|
|
75
|
+
const rootFile = activeCompositionPath ?? "index.html";
|
|
48
76
|
return {
|
|
77
|
+
projectId,
|
|
49
78
|
hfId: element.hfId,
|
|
50
79
|
domId: element.domId,
|
|
51
80
|
selector: element.selector,
|
|
52
81
|
selectorIndex: element.selectorIndex,
|
|
82
|
+
sourceFile: element.sourceFile ?? rootFile,
|
|
83
|
+
activeCompositionPath: rootFile,
|
|
53
84
|
};
|
|
54
85
|
}
|
|
55
86
|
|
|
56
|
-
export function patchTargetAddress(
|
|
87
|
+
export function patchTargetAddress(
|
|
88
|
+
target: PatchTarget & { sourceFile?: string },
|
|
89
|
+
activeCompositionPath?: string | null,
|
|
90
|
+
projectId?: string | null,
|
|
91
|
+
): ElementAddress {
|
|
57
92
|
return {
|
|
93
|
+
projectId,
|
|
58
94
|
hfId: target.hfId,
|
|
59
95
|
domId: target.id,
|
|
60
96
|
selector: target.selector,
|
|
61
97
|
selectorIndex: target.selectorIndex,
|
|
98
|
+
sourceFile: target.sourceFile,
|
|
99
|
+
activeCompositionPath,
|
|
62
100
|
};
|
|
63
101
|
}
|
|
64
102
|
|
|
65
|
-
interface ParsedHandle {
|
|
103
|
+
export interface ParsedHandle {
|
|
66
104
|
scheme: "hf" | "dom" | "sel";
|
|
105
|
+
version: 0 | 1 | 2;
|
|
67
106
|
value: string;
|
|
68
107
|
index: number;
|
|
108
|
+
projectId?: string;
|
|
109
|
+
sourceFile?: string;
|
|
110
|
+
activeCompositionPath?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function decodeScopedParts(rest: string, version: "v1" | "v2", count: number) {
|
|
114
|
+
const parts = rest.split(SEPARATOR);
|
|
115
|
+
if (parts[0] !== version || parts.length !== count + 1) return null;
|
|
116
|
+
return parts.slice(1).map(decodeHandlePart);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseV1ScopedValue(rest: string): Omit<ParsedHandle, "scheme" | "index"> | null {
|
|
120
|
+
const [activeCompositionPath, sourceFile, value] = decodeScopedParts(rest, "v1", 3) ?? [];
|
|
121
|
+
if (!activeCompositionPath || !sourceFile || !value) return null;
|
|
122
|
+
return { version: 1, value, sourceFile, activeCompositionPath };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseV2ScopedValue(rest: string): Omit<ParsedHandle, "scheme" | "index"> | null {
|
|
126
|
+
const [projectId, activeCompositionPath, sourceFile, value] =
|
|
127
|
+
decodeScopedParts(rest, "v2", 4) ?? [];
|
|
128
|
+
if (!projectId || !activeCompositionPath || !sourceFile || !value) return null;
|
|
129
|
+
return { version: 2, projectId, value, sourceFile, activeCompositionPath };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseScopedValue(rest: string): Omit<ParsedHandle, "scheme" | "index"> | null {
|
|
133
|
+
if (rest.startsWith(`v1${SEPARATOR}`)) return parseV1ScopedValue(rest);
|
|
134
|
+
if (rest.startsWith(`v2${SEPARATOR}`)) return parseV2ScopedValue(rest);
|
|
135
|
+
return { value: rest, version: 0 };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function decodeHandlePart(value: string): string | null {
|
|
139
|
+
try {
|
|
140
|
+
return decodeURIComponent(value) || null;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
69
144
|
}
|
|
70
145
|
|
|
71
146
|
export function parseElementHandle(handle: string): ParsedHandle | null {
|
|
@@ -74,15 +149,25 @@ export function parseElementHandle(handle: string): ParsedHandle | null {
|
|
|
74
149
|
const scheme = handle.slice(0, separatorAt);
|
|
75
150
|
const rest = handle.slice(separatorAt + 1);
|
|
76
151
|
if (!rest) return null;
|
|
77
|
-
if (scheme === "hf" || scheme === "dom")
|
|
152
|
+
if (scheme === "hf" || scheme === "dom") {
|
|
153
|
+
const scoped = parseScopedValue(rest);
|
|
154
|
+
return scoped ? { scheme, ...scoped, index: 0 } : null;
|
|
155
|
+
}
|
|
78
156
|
if (scheme !== "sel") return null;
|
|
79
157
|
|
|
80
158
|
// Only the LAST `#` splits the index off: CSS selectors contain `#` themselves.
|
|
81
159
|
const indexAt = rest.lastIndexOf(INDEX_SEPARATOR);
|
|
82
|
-
if (indexAt <= 0)
|
|
160
|
+
if (indexAt <= 0) {
|
|
161
|
+
const scoped = parseScopedValue(rest);
|
|
162
|
+
return scoped ? { scheme, ...scoped, index: 0 } : null;
|
|
163
|
+
}
|
|
83
164
|
const index = Number(rest.slice(indexAt + 1));
|
|
84
|
-
if (!Number.isInteger(index) || index < 0)
|
|
85
|
-
|
|
165
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
166
|
+
const scoped = parseScopedValue(rest);
|
|
167
|
+
return scoped ? { scheme, ...scoped, index: 0 } : null;
|
|
168
|
+
}
|
|
169
|
+
const scoped = parseScopedValue(rest.slice(0, indexAt));
|
|
170
|
+
return scoped ? { scheme, ...scoped, index } : null;
|
|
86
171
|
}
|
|
87
172
|
|
|
88
173
|
/**
|
|
@@ -95,35 +180,67 @@ export function parseElementHandle(handle: string): ParsedHandle | null {
|
|
|
95
180
|
export function resolveElementHandle(doc: Document, handle: string): HTMLElement | null {
|
|
96
181
|
const parsed = parseElementHandle(handle);
|
|
97
182
|
if (!parsed) return null;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
return asHtmlElement(doc, matches.item(parsed.index));
|
|
183
|
+
return findElementForSelection(
|
|
184
|
+
doc,
|
|
185
|
+
{
|
|
186
|
+
hfId: parsed.scheme === "hf" ? parsed.value : undefined,
|
|
187
|
+
id: parsed.scheme === "dom" ? parsed.value : undefined,
|
|
188
|
+
selector: parsed.scheme === "sel" ? parsed.value : undefined,
|
|
189
|
+
selectorIndex: parsed.scheme === "sel" ? parsed.index : undefined,
|
|
190
|
+
sourceFile: parsed.sourceFile,
|
|
191
|
+
},
|
|
192
|
+
parsed.activeCompositionPath ?? null,
|
|
193
|
+
);
|
|
112
194
|
}
|
|
113
195
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
196
|
+
/** Legacy read handles remain valid, but a scoped handle must name the active project. */
|
|
197
|
+
export function elementHandleMatchesProject(handle: string, projectId: string | null): boolean {
|
|
198
|
+
const parsed = parseElementHandle(handle);
|
|
199
|
+
if (!parsed) return false;
|
|
200
|
+
return parsed.version !== 2 || (!!projectId && parsed.projectId === projectId);
|
|
118
201
|
}
|
|
119
202
|
|
|
203
|
+
export type LiveHandleResolution<T extends { element: HTMLElement }> =
|
|
204
|
+
| { status: "ready"; selection: T }
|
|
205
|
+
| { status: "preview-unavailable" }
|
|
206
|
+
| { status: "not-found" }
|
|
207
|
+
| { status: "unsupported" }
|
|
208
|
+
| { status: "changed" };
|
|
209
|
+
|
|
120
210
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
211
|
+
* Resolve and build a selection across one possible preview reload.
|
|
212
|
+
*
|
|
213
|
+
* Building a selection can await a source probe. A thumbnail refresh may
|
|
214
|
+
* replace the preview document during that await, leaving an otherwise valid
|
|
215
|
+
* selection attached to the old document with a truthful-looking 0x0 box.
|
|
216
|
+
* Reacquire once from the current document, then refuse if it moves again.
|
|
124
217
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
218
|
+
export async function resolveLiveHandleSelection<T extends { element: HTMLElement }>(
|
|
219
|
+
getPreviewDocument: () => Document | null,
|
|
220
|
+
handle: string,
|
|
221
|
+
buildSelection: (element: HTMLElement) => Promise<T | null>,
|
|
222
|
+
): Promise<LiveHandleResolution<T>> {
|
|
223
|
+
let reloadObserved = false;
|
|
224
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
225
|
+
const doc = getPreviewDocument();
|
|
226
|
+
if (!doc) return { status: "preview-unavailable" };
|
|
227
|
+
const element = resolveElementHandle(doc, handle);
|
|
228
|
+
if (!element) return { status: reloadObserved ? "changed" : "not-found" };
|
|
229
|
+
|
|
230
|
+
const selection = await buildSelection(element);
|
|
231
|
+
const currentDoc = getPreviewDocument();
|
|
232
|
+
const previewChanged =
|
|
233
|
+
currentDoc !== doc ||
|
|
234
|
+
element.ownerDocument !== doc ||
|
|
235
|
+
!element.isConnected ||
|
|
236
|
+
(selection !== null &&
|
|
237
|
+
(selection.element.ownerDocument !== doc || !selection.element.isConnected));
|
|
238
|
+
if (previewChanged) {
|
|
239
|
+
reloadObserved = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!selection) return { status: "unsupported" };
|
|
243
|
+
return { status: "ready", selection };
|
|
244
|
+
}
|
|
245
|
+
return { status: "changed" };
|
|
129
246
|
}
|