@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.
Files changed (93) hide show
  1. package/dist/assets/{hyperframes-player-CTQCs_KG.js → hyperframes-player-C-CArn13.js} +1 -1
  2. package/dist/assets/{index-okdE01ky.js → index-BIXyJgK5.js} +1 -1
  3. package/dist/assets/{index-Bq3M0sjr.js → index-D5yni1t6.js} +218 -218
  4. package/dist/assets/index-DnRfAiK2.css +1 -0
  5. package/dist/assets/{index-9WJFcXTP.js → index-pfeP2Y63.js} +1 -1
  6. package/dist/{chunk-6BT6DTB4.js → chunk-A7S5SGNA.js} +2 -2
  7. package/dist/chunk-A7S5SGNA.js.map +1 -0
  8. package/dist/{domEditingLayers-URA7BLWE.js → domEditingLayers-JMMVOGTO.js} +2 -2
  9. package/dist/index.d.ts +4 -0
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +6230 -5019
  12. package/dist/index.js.map +1 -1
  13. package/package.json +8 -7
  14. package/src/components/editor/TopologyLens.test.tsx +294 -0
  15. package/src/components/editor/TopologyLens.tsx +206 -0
  16. package/src/components/editor/domEditOverlayGeometry.ts +14 -0
  17. package/src/components/editor/domEditOverlayGeometryBatch.test.ts +18 -0
  18. package/src/components/editor/domEditingLayers.test.ts +9 -0
  19. package/src/components/editor/domEditingLayers.ts +8 -8
  20. package/src/components/editor/topologyLensGeometry.test.ts +44 -0
  21. package/src/components/editor/topologyLensGeometry.ts +119 -0
  22. package/src/components/editor/topologyLensState.test.ts +116 -0
  23. package/src/components/editor/topologyLensState.ts +72 -0
  24. package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +126 -0
  25. package/src/components/nle/PreviewOverlays.tsx +24 -18
  26. package/src/components/sidebar/CompositionsTab.drag.test.tsx +16 -0
  27. package/src/components/sidebar/CompositionsTab.tsx +10 -3
  28. package/src/components/ui/HyperframesLoader.tsx +3 -48
  29. package/src/components/ui/HyperframesMark.tsx +53 -0
  30. package/src/contexts/DomEditContext.tsx +12 -0
  31. package/src/hooks/domEditCommitRunner.test.ts +59 -0
  32. package/src/hooks/domEditCommitRunner.ts +55 -20
  33. package/src/hooks/domEditCommitTypes.ts +8 -1
  34. package/src/hooks/domEditTextCommitPlan.ts +47 -0
  35. package/src/hooks/timelineTrackVisibility.test.ts +7 -1
  36. package/src/hooks/timelineTrackVisibility.ts +5 -5
  37. package/src/hooks/useDomEditAttributeCommits.ts +4 -1
  38. package/src/hooks/useDomEditCommits.test.tsx +243 -8
  39. package/src/hooks/useDomEditCommits.ts +69 -14
  40. package/src/hooks/useDomEditPositionPatchCommit.test.tsx +3 -4
  41. package/src/hooks/useDomEditPositionPatchCommit.ts +27 -29
  42. package/src/hooks/useDomEditSession.ts +6 -2
  43. package/src/hooks/useDomEditTextCommits.test.tsx +86 -2
  44. package/src/hooks/useDomEditTextCommits.ts +64 -86
  45. package/src/hooks/useDomEditWiring.ts +1 -1
  46. package/src/hooks/useDomSelection.ts +2 -0
  47. package/src/hooks/useDomSelectionTypes.ts +2 -0
  48. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +71 -4
  49. package/src/hooks/useExternalFileChangeCoordinator.ts +18 -9
  50. package/src/hooks/useGsapAnimationFetchFallback.test.ts +13 -1
  51. package/src/hooks/useGsapAnimationFetchFallback.ts +7 -3
  52. package/src/hooks/useGsapAnimationOps.test.tsx +101 -0
  53. package/src/hooks/useGsapAnimationOps.ts +4 -4
  54. package/src/hooks/useGsapAwareEditing.test.tsx +30 -2
  55. package/src/hooks/useGsapAwareEditing.ts +31 -10
  56. package/src/hooks/useGsapSelectionHandlers.test.tsx +3 -1
  57. package/src/hooks/useGsapSelectionHandlers.ts +21 -11
  58. package/src/hooks/useRenderClipContent.test.ts +27 -0
  59. package/src/hooks/useRenderClipContent.ts +5 -0
  60. package/src/hooks/useStudioExternalFileChanges.ts +5 -0
  61. package/src/player/components/CompositionThumbnail.test.ts +66 -1
  62. package/src/player/components/CompositionThumbnail.tsx +6 -0
  63. package/src/player/hooks/timelineSyncHydration.ts +10 -2
  64. package/src/player/store/playerStore.test.ts +24 -0
  65. package/src/player/store/thumbnailSlice.ts +6 -0
  66. package/src/styles/studio.css +179 -0
  67. package/src/utils/domEditSaveQueue.test.ts +5 -3
  68. package/src/utils/domEditSaveQueue.ts +6 -1
  69. package/src/webmcp/StudioAgentTools.test.ts +70 -0
  70. package/src/webmcp/StudioAgentTools.tsx +89 -21
  71. package/src/webmcp/handles.test.ts +172 -15
  72. package/src/webmcp/handles.ts +152 -35
  73. package/src/webmcp/tools/animationTools.test.ts +241 -60
  74. package/src/webmcp/tools/animationTools.ts +191 -104
  75. package/src/webmcp/tools/contentTools.test.ts +103 -41
  76. package/src/webmcp/tools/contentTools.ts +149 -105
  77. package/src/webmcp/tools/inspectTools.test.ts +47 -5
  78. package/src/webmcp/tools/inspectTools.ts +59 -19
  79. package/src/webmcp/tools/lookTools.test.ts +185 -38
  80. package/src/webmcp/tools/lookTools.ts +134 -21
  81. package/src/webmcp/tools/selectionTools.test.ts +105 -9
  82. package/src/webmcp/tools/selectionTools.ts +34 -19
  83. package/src/webmcp/tools/targetedWriteTools.test.ts +125 -0
  84. package/src/webmcp/tools/transformTools.test.ts +140 -54
  85. package/src/webmcp/tools/transformTools.ts +215 -56
  86. package/src/webmcp/useStudioAgentTools.test.tsx +310 -10
  87. package/src/webmcp/useStudioAgentTools.ts +27 -15
  88. package/src/webmcp/webmcpTestUtils.ts +37 -0
  89. package/src/webmcp/writeCoordinator.test.ts +375 -0
  90. package/src/webmcp/writeCoordinator.ts +473 -0
  91. package/dist/assets/index-yGhfxxoL.css +0 -1
  92. package/dist/chunk-6BT6DTB4.js.map +0 -1
  93. /package/dist/{domEditingLayers-URA7BLWE.js.map → domEditingLayers-JMMVOGTO.js.map} +0 -0
@@ -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
- if (address.hfId) return `hf${SEPARATOR}${address.hfId}`;
39
- if (address.domId) return `dom${SEPARATOR}${address.domId}`;
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(element: TimelineElement): ElementAddress {
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(target: PatchTarget): ElementAddress {
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") return { scheme, value: rest, index: 0 };
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) return { scheme, value: rest, index: 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) return { scheme, value: rest, index: 0 };
85
- return { scheme, value: rest.slice(0, indexAt), index };
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
- if (parsed.scheme === "dom") return asHtmlElement(doc, doc.getElementById(parsed.value));
100
- if (parsed.scheme === "hf") {
101
- return asHtmlElement(doc, doc.querySelector(`[data-hf-id="${cssEscape(parsed.value)}"]`));
102
- }
103
-
104
- let matches: NodeListOf<Element>;
105
- try {
106
- matches = doc.querySelectorAll(parsed.value);
107
- } catch {
108
- // A selector minted from a previous document can be invalid in this one.
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
- function cssEscape(value: string): string {
115
- // ponytail: happy-dom and jsdom don't always ship CSS.escape; quoting the two
116
- // characters that can break out of an attribute selector covers this use.
117
- return typeof CSS?.escape === "function" ? CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
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
- * `instanceof HTMLElement` is checked against the OWNING document's realm.
122
- * The preview lives in an iframe, so Studio's own `HTMLElement` is a different
123
- * constructor and the naive check fails on every real preview element.
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
- function asHtmlElement(doc: Document, node: Element | null): HTMLElement | null {
126
- if (!node) return null;
127
- const ctor = doc.defaultView?.HTMLElement;
128
- return ctor && node instanceof ctor ? node : null;
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
  }