@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
|
@@ -227,12 +227,14 @@ export function useDomEditSession({
|
|
|
227
227
|
const {
|
|
228
228
|
resolveImportedFontAsset,
|
|
229
229
|
handleDomStyleCommit,
|
|
230
|
+
handleDomStyleCommitForSelection,
|
|
230
231
|
handleDomAttributeCommit,
|
|
231
232
|
handleDomAttributeLiveCommit,
|
|
232
233
|
handleDomAttributeQuietCommit,
|
|
233
234
|
handleDomHtmlAttributeCommit,
|
|
234
235
|
handleDomAttributesCommit,
|
|
235
236
|
handleDomTextCommit,
|
|
237
|
+
handleDomTextCommitForSelection,
|
|
236
238
|
handleDomRichTextCommit,
|
|
237
239
|
handleDomTextFieldStyleCommit,
|
|
238
240
|
handleDomAddTextField,
|
|
@@ -478,6 +480,7 @@ export function useDomEditSession({
|
|
|
478
480
|
onClickToSource,
|
|
479
481
|
});
|
|
480
482
|
const {
|
|
483
|
+
getGsapAnimationsForSelection,
|
|
481
484
|
handleGsapAwarePathOffsetCommit,
|
|
482
485
|
handleGsapAwareGroupPathOffsetCommit,
|
|
483
486
|
handleGsapAwareBoxSizeCommit,
|
|
@@ -506,7 +509,6 @@ export function useDomEditSession({
|
|
|
506
509
|
const { handleUpdateSegmentEase, handleUpdateKeyframeEase, handleSetAllKeyframeEases } =
|
|
507
510
|
useKeyframeEaseCommits({ gsapCommitMutation, domEditSelectionRef });
|
|
508
511
|
return {
|
|
509
|
-
// State
|
|
510
512
|
domEditSelection,
|
|
511
513
|
domEditGroupSelections,
|
|
512
514
|
domEditHoverSelection,
|
|
@@ -515,7 +517,6 @@ export function useDomEditSession({
|
|
|
515
517
|
agentModalAnchorPoint,
|
|
516
518
|
copiedAgentPrompt,
|
|
517
519
|
agentPromptSelectionContext,
|
|
518
|
-
// Refs
|
|
519
520
|
domEditSelectionRef,
|
|
520
521
|
// Callbacks
|
|
521
522
|
handleTimelineElementSelect,
|
|
@@ -525,6 +526,7 @@ export function useDomEditSession({
|
|
|
525
526
|
applyDomSelection,
|
|
526
527
|
clearDomSelection,
|
|
527
528
|
handleDomStyleCommit,
|
|
529
|
+
handleDomStyleCommitForSelection,
|
|
528
530
|
handleDomAttributeCommit,
|
|
529
531
|
handleDomAttributeLiveCommit,
|
|
530
532
|
handleDomAttributeQuietCommit,
|
|
@@ -537,10 +539,12 @@ export function useDomEditSession({
|
|
|
537
539
|
handleDomRotationCommit: handleGsapAwareRotationCommit,
|
|
538
540
|
handleDomManualEditsReset,
|
|
539
541
|
handleDomTextCommit,
|
|
542
|
+
handleDomTextCommitForSelection,
|
|
540
543
|
handleDomRichTextCommit,
|
|
541
544
|
handleDomTextFieldStyleCommit,
|
|
542
545
|
handleDomAddTextField,
|
|
543
546
|
handleDomRemoveTextField,
|
|
547
|
+
getGsapAnimationsForSelection,
|
|
544
548
|
handleAskAgent,
|
|
545
549
|
handleAgentModalSubmit,
|
|
546
550
|
handleBlockedDomMove,
|
|
@@ -124,6 +124,44 @@ afterEach(() => {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
describe("useDomEditTextCommits", () => {
|
|
127
|
+
it("keeps concurrent text commit ownership isolated by target", async () => {
|
|
128
|
+
const { iframe, element: firstElement } = previewElement(
|
|
129
|
+
"<div id='first'>First</div><div id='second'>Second</div>",
|
|
130
|
+
"first",
|
|
131
|
+
);
|
|
132
|
+
const secondElement = iframe.contentDocument?.getElementById("second") as HTMLElement;
|
|
133
|
+
const firstSelection = selectionFor(firstElement);
|
|
134
|
+
const secondSelection = selectionFor(secondElement);
|
|
135
|
+
const firstPersist = createDeferred<void>();
|
|
136
|
+
const persistDomEditOperations = vi
|
|
137
|
+
.fn()
|
|
138
|
+
.mockImplementationOnce(() => firstPersist.promise)
|
|
139
|
+
.mockResolvedValueOnce(undefined);
|
|
140
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
141
|
+
const hook = renderTextCommitHook(
|
|
142
|
+
commitParams({
|
|
143
|
+
previewIframeRef: { current: iframe },
|
|
144
|
+
domEditSelection: firstSelection,
|
|
145
|
+
persistDomEditOperations,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
let firstCommit: Promise<unknown> | undefined;
|
|
150
|
+
act(() => {
|
|
151
|
+
firstCommit = hook.handleDomTextCommitForSelection(firstSelection, "Pending first", "self");
|
|
152
|
+
});
|
|
153
|
+
await act(async () => {
|
|
154
|
+
await hook.handleDomTextCommitForSelection(secondSelection, "Saved second", "self");
|
|
155
|
+
});
|
|
156
|
+
firstPersist.reject(new Error("first target failed"));
|
|
157
|
+
await act(async () => {
|
|
158
|
+
await firstCommit;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(firstElement.textContent).toBe("First");
|
|
162
|
+
expect(secondElement.textContent).toBe("Saved second");
|
|
163
|
+
});
|
|
164
|
+
|
|
127
165
|
it("does not let a stale failed fields commit revert newer text", async () => {
|
|
128
166
|
const { iframe, element } = previewElement("<div id='card'>Original</div>", "card");
|
|
129
167
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
@@ -183,8 +221,17 @@ describe("useDomEditTextCommits", () => {
|
|
|
183
221
|
it("reports a successful style commit", async () => {
|
|
184
222
|
const { iframe, element } = previewElement("<div id='card'>Original</div>", "card");
|
|
185
223
|
const selection = selectionFor(element);
|
|
224
|
+
const persistence = {
|
|
225
|
+
sourceFile: "index.html",
|
|
226
|
+
version: '"sha256:after"',
|
|
227
|
+
changed: true,
|
|
228
|
+
} as const;
|
|
186
229
|
const hook = renderTextCommitHook(
|
|
187
|
-
commitParams({
|
|
230
|
+
commitParams({
|
|
231
|
+
previewIframeRef: { current: iframe },
|
|
232
|
+
domEditSelection: selection,
|
|
233
|
+
persistDomEditOperations: vi.fn().mockResolvedValue(persistence),
|
|
234
|
+
}),
|
|
188
235
|
);
|
|
189
236
|
|
|
190
237
|
let outcome: unknown;
|
|
@@ -192,7 +239,7 @@ describe("useDomEditTextCommits", () => {
|
|
|
192
239
|
outcome = await hook.handleDomStyleCommit("color", "red");
|
|
193
240
|
});
|
|
194
241
|
|
|
195
|
-
expect(outcome).toEqual({ ok: true });
|
|
242
|
+
expect(outcome).toEqual({ ok: true, persistence });
|
|
196
243
|
});
|
|
197
244
|
|
|
198
245
|
it("declines a style commit with no selection, without reaching the writer", async () => {
|
|
@@ -286,4 +333,41 @@ describe("useDomEditTextCommits", () => {
|
|
|
286
333
|
expect(outcome).toEqual({ ok: false, reason: "no-selection" });
|
|
287
334
|
expect(persistDomEditOperations).not.toHaveBeenCalled();
|
|
288
335
|
});
|
|
336
|
+
|
|
337
|
+
it("preserves text persistence evidence for an explicit selection", async () => {
|
|
338
|
+
const { iframe, element: ambientElement } = previewElement(
|
|
339
|
+
"<div id='ambient'>Ambient</div><div id='agent'>Agent</div>",
|
|
340
|
+
"ambient",
|
|
341
|
+
);
|
|
342
|
+
const agentElement = iframe.contentDocument?.getElementById("agent") as HTMLElement;
|
|
343
|
+
const ambientSelection = selectionFor(ambientElement);
|
|
344
|
+
const agentSelection = selectionFor(agentElement);
|
|
345
|
+
const persistence = {
|
|
346
|
+
sourceFile: "index.html",
|
|
347
|
+
version: '"sha256:text"',
|
|
348
|
+
changed: true,
|
|
349
|
+
} as const;
|
|
350
|
+
const persistDomEditOperations = vi.fn().mockResolvedValue(persistence);
|
|
351
|
+
const hook = renderTextCommitHook(
|
|
352
|
+
commitParams({
|
|
353
|
+
previewIframeRef: { current: iframe },
|
|
354
|
+
domEditSelection: ambientSelection,
|
|
355
|
+
persistDomEditOperations,
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
let outcome: unknown;
|
|
360
|
+
await act(async () => {
|
|
361
|
+
outcome = await hook.handleDomTextCommitForSelection(agentSelection, "Edited", "self");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(outcome).toEqual({ ok: true, persistence });
|
|
365
|
+
expect(persistDomEditOperations).toHaveBeenCalledWith(
|
|
366
|
+
agentSelection,
|
|
367
|
+
expect.any(Array),
|
|
368
|
+
expect.any(Object),
|
|
369
|
+
);
|
|
370
|
+
expect(ambientElement.textContent).toBe("Ambient");
|
|
371
|
+
expect(agentElement.textContent).toBe("Edited");
|
|
372
|
+
});
|
|
289
373
|
});
|
|
@@ -13,11 +13,9 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
buildDomEditRichTextPatchOperation,
|
|
15
15
|
buildDomEditStylePatchOperation,
|
|
16
|
-
buildDomEditTextPatchOperation,
|
|
17
16
|
findElementForSelection,
|
|
18
17
|
getDomEditTargetKey,
|
|
19
18
|
isTextEditableSelection,
|
|
20
|
-
serializeDomEditTextFields,
|
|
21
19
|
buildDefaultDomEditTextField,
|
|
22
20
|
type DomEditTextField,
|
|
23
21
|
type DomEditSelection,
|
|
@@ -25,11 +23,10 @@ import {
|
|
|
25
23
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
26
24
|
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
27
25
|
import { canEditElementTextInline } from "../components/editor/domEditInlineText";
|
|
28
|
-
import {
|
|
26
|
+
import { buildNextDomTextFields, planDomTextCommit } from "./domEditTextCommitPlan";
|
|
29
27
|
import { reportDomEditPersistFailure } from "./domEditPersistFailure";
|
|
30
28
|
import {
|
|
31
29
|
bumpDomEditCommitMapVersion,
|
|
32
|
-
bumpDomEditCommitVersion,
|
|
33
30
|
domEditCommitDeclined,
|
|
34
31
|
runDomEditCommit,
|
|
35
32
|
runReportedDomEditCommit,
|
|
@@ -58,13 +55,6 @@ export interface UseDomEditTextCommitsParams {
|
|
|
58
55
|
resolveImportedFontAsset: (fontFamilyValue: string) => ImportedFontAsset | null;
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
interface DomTextCommitPlan {
|
|
62
|
-
usesSerializedTextFields: boolean;
|
|
63
|
-
nextContent: string;
|
|
64
|
-
childOperations: PatchOperation[] | null;
|
|
65
|
-
operations: PatchOperation[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
function canCommitInlineTextSelection(selection: DomEditSelection, element: HTMLElement): boolean {
|
|
69
59
|
if (selection.isCompositionHost || selection.isInsideLockedComposition) return false;
|
|
70
60
|
return canEditElementTextInline(element);
|
|
@@ -97,51 +87,6 @@ function buildDomStyleCommitOperations(
|
|
|
97
87
|
return operations;
|
|
98
88
|
}
|
|
99
89
|
|
|
100
|
-
function buildNextDomTextFields(
|
|
101
|
-
textFields: DomEditTextField[],
|
|
102
|
-
value: string,
|
|
103
|
-
fieldKey?: string,
|
|
104
|
-
): DomEditTextField[] {
|
|
105
|
-
if (textFields.length === 0) return [];
|
|
106
|
-
return textFields.map((field) => (field.key === fieldKey ? { ...field, value } : field));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function planDomTextCommit(
|
|
110
|
-
originalTextFields: DomEditTextField[],
|
|
111
|
-
nextTextFields: DomEditTextField[],
|
|
112
|
-
plainTextContent: string,
|
|
113
|
-
): DomTextCommitPlan {
|
|
114
|
-
const usesSerializedTextFields =
|
|
115
|
-
nextTextFields.length > 1 || nextTextFields.some((field) => field.source === "child");
|
|
116
|
-
const nextContent = usesSerializedTextFields
|
|
117
|
-
? serializeDomEditTextFields(nextTextFields)
|
|
118
|
-
: plainTextContent;
|
|
119
|
-
const childOperations = usesSerializedTextFields
|
|
120
|
-
? buildTextFieldChildOperations(originalTextFields, nextTextFields)
|
|
121
|
-
: null;
|
|
122
|
-
// Per-child operations when the layers still line up one-for-one, and the
|
|
123
|
-
// element's whole markup when they do not.
|
|
124
|
-
//
|
|
125
|
-
// `buildTextFieldChildOperations` can only address children that already
|
|
126
|
-
// exist, so it returns null for any change in how many there are — which is
|
|
127
|
-
// every delete and every add. That used to end here with "Couldn't save this
|
|
128
|
-
// text structure change": the panel offered a remove button and an Add text
|
|
129
|
-
// field row, and neither could ever save. A structure change has one honest
|
|
130
|
-
// operation, which is to write the structure.
|
|
131
|
-
const operations =
|
|
132
|
-
childOperations ??
|
|
133
|
-
(usesSerializedTextFields
|
|
134
|
-
? [buildDomEditRichTextPatchOperation(nextContent)]
|
|
135
|
-
: [buildDomEditTextPatchOperation(nextContent)]);
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
usesSerializedTextFields,
|
|
139
|
-
nextContent,
|
|
140
|
-
childOperations,
|
|
141
|
-
operations,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
90
|
async function resyncDomTextSelectionFromPreview(
|
|
146
91
|
doc: Document | null | undefined,
|
|
147
92
|
selection: DomEditSelection,
|
|
@@ -170,8 +115,8 @@ export function useDomEditTextCommits({
|
|
|
170
115
|
persistDomEditOperations,
|
|
171
116
|
resolveImportedFontAsset,
|
|
172
117
|
}: UseDomEditTextCommitsParams) {
|
|
173
|
-
const domTextCommitVersionRef = useRef(
|
|
174
|
-
const domStyleCommitVersionRef = useRef(new Map<string,
|
|
118
|
+
const domTextCommitVersionRef = useRef(new Map<string, symbol>());
|
|
119
|
+
const domStyleCommitVersionRef = useRef(new Map<string, symbol>());
|
|
175
120
|
|
|
176
121
|
const {
|
|
177
122
|
handleDomAttributeCommit,
|
|
@@ -188,15 +133,18 @@ export function useDomEditTextCommits({
|
|
|
188
133
|
persistDomEditOperations,
|
|
189
134
|
});
|
|
190
135
|
|
|
191
|
-
const
|
|
192
|
-
async (
|
|
193
|
-
|
|
136
|
+
const handleDomStyleCommitForSelection = useCallback(
|
|
137
|
+
async (
|
|
138
|
+
selection: DomEditSelection,
|
|
139
|
+
property: string,
|
|
140
|
+
value: string,
|
|
141
|
+
): Promise<DomEditCommitOutcome> => {
|
|
194
142
|
if (isManualGeometryStyleProperty(property))
|
|
195
143
|
return domEditCommitDeclined("geometry-property");
|
|
196
|
-
if (!
|
|
144
|
+
if (!selection.capabilities.canEditStyles) {
|
|
197
145
|
return domEditCommitDeclined("styles-not-editable");
|
|
198
146
|
}
|
|
199
|
-
const styleCommitKey = `${getDomEditTargetKey(
|
|
147
|
+
const styleCommitKey = `${getDomEditTargetKey(selection)}:${property}`;
|
|
200
148
|
const isLatestStyleCommit = bumpDomEditCommitMapVersion(
|
|
201
149
|
domStyleCommitVersionRef.current,
|
|
202
150
|
styleCommitKey,
|
|
@@ -219,7 +167,7 @@ export function useDomEditTextCommits({
|
|
|
219
167
|
return runReportedDomEditCommit({
|
|
220
168
|
capture: () => {
|
|
221
169
|
if (!doc) return;
|
|
222
|
-
const el = findElementForSelection(doc,
|
|
170
|
+
const el = findElementForSelection(doc, selection, activeCompPath);
|
|
223
171
|
if (!el) return;
|
|
224
172
|
editedElement = el;
|
|
225
173
|
previousInlineValue = el.style.getPropertyValue(property);
|
|
@@ -238,7 +186,7 @@ export function useDomEditTextCommits({
|
|
|
238
186
|
}
|
|
239
187
|
},
|
|
240
188
|
persist: () =>
|
|
241
|
-
persistDomEditOperations(
|
|
189
|
+
persistDomEditOperations(selection, operations, {
|
|
242
190
|
label: "Edit layer style",
|
|
243
191
|
skipRefresh,
|
|
244
192
|
prepareContent: importedFont
|
|
@@ -255,15 +203,14 @@ export function useDomEditTextCommits({
|
|
|
255
203
|
editedElement.style.setProperty(property, previousInlineValue);
|
|
256
204
|
}
|
|
257
205
|
},
|
|
258
|
-
onError: (error) =>
|
|
259
|
-
reportDomEditPersistFailure(domEditSelection, operations, error, showToast),
|
|
206
|
+
onError: (error) => reportDomEditPersistFailure(selection, operations, error, showToast),
|
|
260
207
|
shouldResync: isLatestStyleCommit,
|
|
261
|
-
resync: () => refreshDomEditSelectionFromPreview(
|
|
208
|
+
resync: () => refreshDomEditSelectionFromPreview(selection),
|
|
209
|
+
onFinally: isLatestStyleCommit.release,
|
|
262
210
|
});
|
|
263
211
|
},
|
|
264
212
|
[
|
|
265
213
|
activeCompPath,
|
|
266
|
-
domEditSelection,
|
|
267
214
|
persistDomEditOperations,
|
|
268
215
|
refreshDomEditSelectionFromPreview,
|
|
269
216
|
resolveImportedFontAsset,
|
|
@@ -272,15 +219,29 @@ export function useDomEditTextCommits({
|
|
|
272
219
|
],
|
|
273
220
|
);
|
|
274
221
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
222
|
+
const handleDomStyleCommit = useCallback(
|
|
223
|
+
(property: string, value: string): Promise<DomEditCommitOutcome> =>
|
|
224
|
+
domEditSelection
|
|
225
|
+
? handleDomStyleCommitForSelection(domEditSelection, property, value)
|
|
226
|
+
: Promise.resolve(domEditCommitDeclined("no-selection")),
|
|
227
|
+
[domEditSelection, handleDomStyleCommitForSelection],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const handleDomTextCommitForSelection = useCallback(
|
|
231
|
+
async (
|
|
232
|
+
selection: DomEditSelection,
|
|
233
|
+
value: string,
|
|
234
|
+
fieldKey?: string,
|
|
235
|
+
): Promise<DomEditCommitOutcome> => {
|
|
236
|
+
if (!isTextEditableSelection(selection)) {
|
|
279
237
|
return domEditCommitDeclined("not-text-editable");
|
|
280
238
|
}
|
|
281
|
-
const isLatestTextCommit =
|
|
282
|
-
|
|
283
|
-
|
|
239
|
+
const isLatestTextCommit = bumpDomEditCommitMapVersion(
|
|
240
|
+
domTextCommitVersionRef.current,
|
|
241
|
+
getDomEditTargetKey(selection),
|
|
242
|
+
);
|
|
243
|
+
const nextTextFields = buildNextDomTextFields(selection.textFields, value, fieldKey);
|
|
244
|
+
const textCommit = planDomTextCommit(selection.textFields, nextTextFields, value);
|
|
284
245
|
const iframe = previewIframeRef.current;
|
|
285
246
|
const doc = iframe?.contentDocument;
|
|
286
247
|
let editedElement: HTMLElement | null = null;
|
|
@@ -289,7 +250,7 @@ export function useDomEditTextCommits({
|
|
|
289
250
|
return runReportedDomEditCommit({
|
|
290
251
|
capture: () => {
|
|
291
252
|
if (!doc) return;
|
|
292
|
-
const el = findElementForSelection(doc,
|
|
253
|
+
const el = findElementForSelection(doc, selection, activeCompPath);
|
|
293
254
|
if (!el) return;
|
|
294
255
|
editedElement = el;
|
|
295
256
|
previousInnerHtml = el.innerHTML;
|
|
@@ -302,42 +263,49 @@ export function useDomEditTextCommits({
|
|
|
302
263
|
editedElement.textContent = value;
|
|
303
264
|
}
|
|
304
265
|
},
|
|
305
|
-
persist:
|
|
306
|
-
|
|
266
|
+
persist: () =>
|
|
267
|
+
persistDomEditOperations(selection, textCommit.operations, {
|
|
307
268
|
label: "Edit text",
|
|
308
269
|
skipRefresh: true,
|
|
309
270
|
shouldSave: isLatestTextCommit,
|
|
310
|
-
})
|
|
311
|
-
},
|
|
271
|
+
}),
|
|
312
272
|
shouldRevert: () => isLatestTextCommit(),
|
|
313
273
|
revert: () => {
|
|
314
274
|
if (!editedElement || previousInnerHtml === null) return;
|
|
315
275
|
editedElement.innerHTML = previousInnerHtml;
|
|
316
276
|
},
|
|
317
277
|
onError: (error) =>
|
|
318
|
-
reportDomEditPersistFailure(
|
|
278
|
+
reportDomEditPersistFailure(selection, textCommit.operations, error, showToast),
|
|
319
279
|
shouldResync: isLatestTextCommit,
|
|
320
280
|
resync: () =>
|
|
321
281
|
resyncDomTextSelectionFromPreview(
|
|
322
282
|
doc,
|
|
323
|
-
|
|
283
|
+
selection,
|
|
324
284
|
activeCompPath,
|
|
325
285
|
buildDomSelectionFromTarget,
|
|
326
286
|
applyDomSelection,
|
|
327
287
|
),
|
|
288
|
+
onFinally: isLatestTextCommit.release,
|
|
328
289
|
});
|
|
329
290
|
},
|
|
330
291
|
[
|
|
331
292
|
activeCompPath,
|
|
332
293
|
applyDomSelection,
|
|
333
294
|
buildDomSelectionFromTarget,
|
|
334
|
-
domEditSelection,
|
|
335
295
|
persistDomEditOperations,
|
|
336
296
|
previewIframeRef,
|
|
337
297
|
showToast,
|
|
338
298
|
],
|
|
339
299
|
);
|
|
340
300
|
|
|
301
|
+
const handleDomTextCommit = useCallback(
|
|
302
|
+
(value: string, fieldKey?: string): Promise<DomEditCommitOutcome> =>
|
|
303
|
+
domEditSelection
|
|
304
|
+
? handleDomTextCommitForSelection(domEditSelection, value, fieldKey)
|
|
305
|
+
: Promise.resolve(domEditCommitDeclined("no-selection")),
|
|
306
|
+
[domEditSelection, handleDomTextCommitForSelection],
|
|
307
|
+
);
|
|
308
|
+
|
|
341
309
|
/**
|
|
342
310
|
* Persist an element's own markup, for a text edit that styled part of it.
|
|
343
311
|
*
|
|
@@ -366,7 +334,10 @@ export function useDomEditTextCommits({
|
|
|
366
334
|
// A preview reload replaces the document. Never resolve this commit onto
|
|
367
335
|
// the replacement node: it did not own the edit or its rollback snapshot.
|
|
368
336
|
if (!ownsCurrentPreviewElement(domEditSelection, element, doc)) return;
|
|
369
|
-
const isLatestTextCommit =
|
|
337
|
+
const isLatestTextCommit = bumpDomEditCommitMapVersion(
|
|
338
|
+
domTextCommitVersionRef.current,
|
|
339
|
+
getDomEditTargetKey(domEditSelection),
|
|
340
|
+
);
|
|
370
341
|
const operations = [buildDomEditRichTextPatchOperation(html)];
|
|
371
342
|
let appliedHtml = "";
|
|
372
343
|
|
|
@@ -404,6 +375,7 @@ export function useDomEditTextCommits({
|
|
|
404
375
|
buildDomSelectionFromTarget,
|
|
405
376
|
applyDomSelection,
|
|
406
377
|
),
|
|
378
|
+
onFinally: isLatestTextCommit.release,
|
|
407
379
|
});
|
|
408
380
|
},
|
|
409
381
|
[
|
|
@@ -423,7 +395,10 @@ export function useDomEditTextCommits({
|
|
|
423
395
|
nextTextFields: DomEditTextField[],
|
|
424
396
|
options?: { importedFont?: ImportedFontAsset | null },
|
|
425
397
|
) => {
|
|
426
|
-
const isLatestTextCommit =
|
|
398
|
+
const isLatestTextCommit = bumpDomEditCommitMapVersion(
|
|
399
|
+
domTextCommitVersionRef.current,
|
|
400
|
+
getDomEditTargetKey(selection),
|
|
401
|
+
);
|
|
427
402
|
const textCommit = planDomTextCommit(
|
|
428
403
|
selection.textFields,
|
|
429
404
|
nextTextFields,
|
|
@@ -476,6 +451,7 @@ export function useDomEditTextCommits({
|
|
|
476
451
|
buildDomSelectionFromTarget,
|
|
477
452
|
applyDomSelection,
|
|
478
453
|
),
|
|
454
|
+
onFinally: isLatestTextCommit.release,
|
|
479
455
|
});
|
|
480
456
|
},
|
|
481
457
|
[
|
|
@@ -579,12 +555,14 @@ export function useDomEditTextCommits({
|
|
|
579
555
|
|
|
580
556
|
return {
|
|
581
557
|
handleDomStyleCommit,
|
|
558
|
+
handleDomStyleCommitForSelection,
|
|
582
559
|
handleDomAttributeCommit,
|
|
583
560
|
handleDomAttributeLiveCommit,
|
|
584
561
|
handleDomAttributeQuietCommit,
|
|
585
562
|
handleDomHtmlAttributeCommit,
|
|
586
563
|
handleDomAttributesCommit,
|
|
587
564
|
handleDomTextCommit,
|
|
565
|
+
handleDomTextCommitForSelection,
|
|
588
566
|
handleDomRichTextCommit,
|
|
589
567
|
commitDomTextFields,
|
|
590
568
|
handleDomTextFieldStyleCommit,
|
|
@@ -223,7 +223,7 @@ export function useDomEditWiring({
|
|
|
223
223
|
// ── Telemetry & fallback ──
|
|
224
224
|
|
|
225
225
|
const trackGsapInteractionFailure = useGsapInteractionFailureTelemetry(activeCompPath, showToast);
|
|
226
|
-
const makeFetchFallback = useGsapAnimationFetchFallback(projectId
|
|
226
|
+
const makeFetchFallback = useGsapAnimationFetchFallback(projectId);
|
|
227
227
|
|
|
228
228
|
// ── GSAP selection handlers ──
|
|
229
229
|
|
|
@@ -210,6 +210,7 @@ export function useDomSelection({
|
|
|
210
210
|
options?: {
|
|
211
211
|
preferClipAncestor?: boolean;
|
|
212
212
|
skipSourceProbe?: boolean;
|
|
213
|
+
exactTarget?: boolean;
|
|
213
214
|
// Override the drill-in scope (used by canvas double-click to resolve the
|
|
214
215
|
// child inside a group before the activeGroupElement state has re-rendered).
|
|
215
216
|
activeGroupElement?: HTMLElement | null;
|
|
@@ -220,6 +221,7 @@ export function useDomSelection({
|
|
|
220
221
|
isMasterView,
|
|
221
222
|
preferClipAncestor: options?.preferClipAncestor,
|
|
222
223
|
skipSourceProbe: options?.skipSourceProbe,
|
|
224
|
+
exactTarget: options?.exactTarget,
|
|
223
225
|
activeGroupElement:
|
|
224
226
|
options && "activeGroupElement" in options
|
|
225
227
|
? options.activeGroupElement
|
|
@@ -16,6 +16,8 @@ export interface ResolveDomSelectionOptions {
|
|
|
16
16
|
preferClipAncestor?: boolean;
|
|
17
17
|
skipSourceProbe?: boolean;
|
|
18
18
|
activeGroupElement?: HTMLElement | null;
|
|
19
|
+
/** Resolve this node itself instead of applying human group-capture behavior. */
|
|
20
|
+
exactTarget?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface UseDomSelectionParams {
|