@hyperframes/studio 0.8.23 → 0.8.25
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-oZ6g4tHJ.js → hyperframes-player-CTP_00ix.js} +1 -1
- package/dist/assets/{index-UyvIa_cE.js → index-Bjb4YqS_.js} +1 -1
- package/dist/assets/{index-Tiu74FPg.js → index-DSk_ALFr.js} +218 -218
- package/dist/assets/{index-DW-H7Y2b.js → index-Di-8Jxqc.js} +1 -1
- package/dist/assets/index-DnRfAiK2.css +1 -0
- 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 +6206 -5011
- 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/useDomEditAttributeCommits.ts +4 -1
- package/src/hooks/useDomEditCommits.test.tsx +207 -8
- package/src/hooks/useDomEditCommits.ts +54 -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/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
|
@@ -8,7 +8,9 @@ import type { DomEditSelection, DomEditTextField } from "../components/editor/do
|
|
|
8
8
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
9
9
|
import { usePlayerStore } from "../player";
|
|
10
10
|
import { StudioSaveHttpError } from "../utils/studioSaveDiagnostics";
|
|
11
|
+
import { createDomEditSaveQueue } from "../utils/domEditSaveQueue";
|
|
11
12
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
13
|
+
import type { CutoverResult } from "../utils/sdkCutover";
|
|
12
14
|
import { useDomEditCommits } from "./useDomEditCommits";
|
|
13
15
|
|
|
14
16
|
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
@@ -22,6 +24,8 @@ interface PatchResponseBody {
|
|
|
22
24
|
changed?: boolean;
|
|
23
25
|
matched?: boolean;
|
|
24
26
|
content?: string;
|
|
27
|
+
path?: string;
|
|
28
|
+
version?: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
interface RenderedDomEditCommits {
|
|
@@ -35,6 +39,9 @@ interface RenderedDomEditCommits {
|
|
|
35
39
|
interface RenderDomEditCommitsOptions {
|
|
36
40
|
importedFontAssets?: ImportedFontAsset[];
|
|
37
41
|
writeProjectFile?: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
42
|
+
onTrySdkPersist?: () => Promise<CutoverResult>;
|
|
43
|
+
queueDomEditSave?: <T>(save: () => Promise<T>) => Promise<T>;
|
|
44
|
+
projectIdRef?: MutableRefObject<string | null>;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
type FetchHandler = (
|
|
@@ -201,7 +208,7 @@ function renderDomEditCommits(
|
|
|
201
208
|
const showToast = makeShowToast();
|
|
202
209
|
const recordEdit = vi.fn(async () => {});
|
|
203
210
|
const previewIframeRef: MutableRefObject<HTMLIFrameElement | null> = { current: iframe };
|
|
204
|
-
const projectIdRef: MutableRefObject<string | null> = { current: "p1" };
|
|
211
|
+
const projectIdRef: MutableRefObject<string | null> = options.projectIdRef ?? { current: "p1" };
|
|
205
212
|
const domEditSaveTimestampRef: MutableRefObject<number> = { current: 0 };
|
|
206
213
|
const reloadPreview = vi.fn();
|
|
207
214
|
|
|
@@ -210,7 +217,7 @@ function renderDomEditCommits(
|
|
|
210
217
|
activeCompPath: "index.html",
|
|
211
218
|
previewIframeRef,
|
|
212
219
|
showToast,
|
|
213
|
-
queueDomEditSave: async (save) => save(),
|
|
220
|
+
queueDomEditSave: options.queueDomEditSave ?? (async (save) => save()),
|
|
214
221
|
writeProjectFile: options.writeProjectFile ?? (async () => {}),
|
|
215
222
|
domEditSaveTimestampRef,
|
|
216
223
|
editHistory: { recordEdit },
|
|
@@ -224,6 +231,7 @@ function renderDomEditCommits(
|
|
|
224
231
|
clearDomSelection: vi.fn(),
|
|
225
232
|
refreshDomEditSelectionFromPreview: vi.fn(),
|
|
226
233
|
buildDomSelectionFromTarget: vi.fn(async () => null),
|
|
234
|
+
onTrySdkPersist: options.onTrySdkPersist,
|
|
227
235
|
});
|
|
228
236
|
return null;
|
|
229
237
|
}
|
|
@@ -772,11 +780,13 @@ async function commitStyleAgainst(response: Parameters<typeof stubPatchFetch>[0]
|
|
|
772
780
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
773
781
|
const { iframe, element } = createPreviewElement();
|
|
774
782
|
const rendered = renderDomEditCommits(createSelection(element), iframe);
|
|
783
|
+
let outcome: Awaited<ReturnType<typeof rendered.hook.handleDomStyleCommit>> | undefined;
|
|
775
784
|
await act(async () => {
|
|
776
|
-
await rendered.hook.handleDomStyleCommit("color", "blue");
|
|
785
|
+
outcome = await rendered.hook.handleDomStyleCommit("color", "blue");
|
|
777
786
|
});
|
|
778
787
|
return {
|
|
779
788
|
element,
|
|
789
|
+
outcome,
|
|
780
790
|
rendered,
|
|
781
791
|
warnSpy,
|
|
782
792
|
cleanup: () => {
|
|
@@ -944,10 +954,12 @@ describe("useDomEditCommits style persist handling", () => {
|
|
|
944
954
|
});
|
|
945
955
|
|
|
946
956
|
it("warns without a toast when the server matched the element but reported no change", async () => {
|
|
947
|
-
const { rendered, warnSpy, cleanup } = await commitStyleAgainst({
|
|
957
|
+
const { rendered, outcome, warnSpy, cleanup } = await commitStyleAgainst({
|
|
948
958
|
ok: true,
|
|
949
959
|
changed: false,
|
|
950
960
|
matched: true,
|
|
961
|
+
path: "index.html",
|
|
962
|
+
version: '"sha256:current"',
|
|
951
963
|
});
|
|
952
964
|
|
|
953
965
|
try {
|
|
@@ -956,6 +968,14 @@ describe("useDomEditCommits style persist handling", () => {
|
|
|
956
968
|
"[Studio] DOM edit persist no-op",
|
|
957
969
|
expect.objectContaining({ operations: "inline-style:color" }),
|
|
958
970
|
);
|
|
971
|
+
expect(outcome).toEqual({
|
|
972
|
+
ok: true,
|
|
973
|
+
persistence: {
|
|
974
|
+
sourceFile: "index.html",
|
|
975
|
+
version: '"sha256:current"',
|
|
976
|
+
changed: false,
|
|
977
|
+
},
|
|
978
|
+
});
|
|
959
979
|
} finally {
|
|
960
980
|
cleanup();
|
|
961
981
|
}
|
|
@@ -976,22 +996,203 @@ describe("useDomEditCommits style persist handling", () => {
|
|
|
976
996
|
});
|
|
977
997
|
|
|
978
998
|
it("keeps the optimistic style and records history when the patch succeeds", async () => {
|
|
979
|
-
const { element, rendered, cleanup } = await commitStyleAgainst({
|
|
999
|
+
const { element, rendered, outcome, cleanup } = await commitStyleAgainst({
|
|
980
1000
|
ok: true,
|
|
981
1001
|
changed: true,
|
|
982
1002
|
matched: true,
|
|
983
1003
|
content: '<div data-hf-id="hf-card" style="color: blue">Card</div>',
|
|
1004
|
+
path: "index.html",
|
|
1005
|
+
version: '"sha256:after"',
|
|
984
1006
|
});
|
|
985
1007
|
|
|
986
1008
|
try {
|
|
987
1009
|
expect(rendered.showToast).not.toHaveBeenCalled();
|
|
988
1010
|
expect(element.style.getPropertyValue("color")).toBe("blue");
|
|
989
1011
|
expect(rendered.recordEdit).toHaveBeenCalledTimes(1);
|
|
1012
|
+
expect(outcome).toEqual({
|
|
1013
|
+
ok: true,
|
|
1014
|
+
persistence: {
|
|
1015
|
+
sourceFile: "index.html",
|
|
1016
|
+
version: '"sha256:after"',
|
|
1017
|
+
changed: true,
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
990
1020
|
} finally {
|
|
991
1021
|
cleanup();
|
|
992
1022
|
}
|
|
993
1023
|
});
|
|
994
1024
|
|
|
1025
|
+
it("serializes the full read, write, and history transaction across overlapping commits", async () => {
|
|
1026
|
+
const firstPatch = createDeferred<Response>();
|
|
1027
|
+
let readCount = 0;
|
|
1028
|
+
let patchCount = 0;
|
|
1029
|
+
vi.stubGlobal(
|
|
1030
|
+
"fetch",
|
|
1031
|
+
vi.fn(async (input: Parameters<typeof fetch>[0]) => {
|
|
1032
|
+
const url = requestUrl(input);
|
|
1033
|
+
if (url.includes("/api/projects/p1/files/")) {
|
|
1034
|
+
readCount += 1;
|
|
1035
|
+
return jsonResponse({
|
|
1036
|
+
content:
|
|
1037
|
+
readCount === 1
|
|
1038
|
+
? '<div data-hf-id="hf-card" style="color: red">Card</div>'
|
|
1039
|
+
: '<div data-hf-id="hf-card" style="color: blue">Card</div>',
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (url.includes("/api/projects/p1/file-mutations/patch-element/")) {
|
|
1043
|
+
patchCount += 1;
|
|
1044
|
+
if (patchCount === 1) return firstPatch.promise;
|
|
1045
|
+
return jsonResponse({
|
|
1046
|
+
ok: true,
|
|
1047
|
+
changed: true,
|
|
1048
|
+
matched: true,
|
|
1049
|
+
content: '<div data-hf-id="hf-card" style="color: green">Card</div>',
|
|
1050
|
+
path: "index.html",
|
|
1051
|
+
version: '"sha256:green"',
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
1055
|
+
}),
|
|
1056
|
+
);
|
|
1057
|
+
const queue = createDomEditSaveQueue();
|
|
1058
|
+
const { iframe, element } = createPreviewElement();
|
|
1059
|
+
const rendered = renderDomEditCommits(createSelection(element), iframe, {
|
|
1060
|
+
queueDomEditSave: queue.enqueue,
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
const first = rendered.hook.handleDomStyleCommit("color", "blue");
|
|
1065
|
+
await flushAsyncWork();
|
|
1066
|
+
const second = rendered.hook.handleDomStyleCommit("color", "green");
|
|
1067
|
+
await flushAsyncWork();
|
|
1068
|
+
|
|
1069
|
+
expect(readCount).toBe(1);
|
|
1070
|
+
expect(patchCount).toBe(1);
|
|
1071
|
+
|
|
1072
|
+
firstPatch.resolve(
|
|
1073
|
+
jsonResponse({
|
|
1074
|
+
ok: true,
|
|
1075
|
+
changed: true,
|
|
1076
|
+
matched: true,
|
|
1077
|
+
content: '<div data-hf-id="hf-card" style="color: blue">Card</div>',
|
|
1078
|
+
path: "index.html",
|
|
1079
|
+
version: '"sha256:blue"',
|
|
1080
|
+
}),
|
|
1081
|
+
);
|
|
1082
|
+
await first;
|
|
1083
|
+
await second;
|
|
1084
|
+
|
|
1085
|
+
expect(readCount).toBe(2);
|
|
1086
|
+
expect(patchCount).toBe(2);
|
|
1087
|
+
expect(rendered.recordEdit).toHaveBeenNthCalledWith(
|
|
1088
|
+
1,
|
|
1089
|
+
expect.objectContaining({
|
|
1090
|
+
files: {
|
|
1091
|
+
"index.html": expect.objectContaining({
|
|
1092
|
+
before: expect.stringContaining("color: red"),
|
|
1093
|
+
after: expect.stringContaining("color: blue"),
|
|
1094
|
+
}),
|
|
1095
|
+
},
|
|
1096
|
+
}),
|
|
1097
|
+
);
|
|
1098
|
+
expect(rendered.recordEdit).toHaveBeenNthCalledWith(
|
|
1099
|
+
2,
|
|
1100
|
+
expect.objectContaining({
|
|
1101
|
+
files: {
|
|
1102
|
+
"index.html": expect.objectContaining({
|
|
1103
|
+
before: expect.stringContaining("color: blue"),
|
|
1104
|
+
after: expect.stringContaining("color: green"),
|
|
1105
|
+
}),
|
|
1106
|
+
},
|
|
1107
|
+
}),
|
|
1108
|
+
);
|
|
1109
|
+
} finally {
|
|
1110
|
+
queue.destroy();
|
|
1111
|
+
rendered.cleanup();
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it("refuses queued persistence after the active project changes", async () => {
|
|
1116
|
+
const firstPatch = createDeferred<Response>();
|
|
1117
|
+
const projectIdRef: MutableRefObject<string | null> = { current: "p1" };
|
|
1118
|
+
const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]) => {
|
|
1119
|
+
const url = requestUrl(input);
|
|
1120
|
+
if (url.includes("/api/projects/p1/files/")) {
|
|
1121
|
+
return jsonResponse({
|
|
1122
|
+
content: '<div data-hf-id="hf-card" style="color: red">Card</div>',
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
if (url.includes("/api/projects/p1/file-mutations/patch-element/")) {
|
|
1126
|
+
return firstPatch.promise;
|
|
1127
|
+
}
|
|
1128
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
1129
|
+
});
|
|
1130
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1131
|
+
const queue = createDomEditSaveQueue();
|
|
1132
|
+
const { iframe, element } = createPreviewElement();
|
|
1133
|
+
const rendered = renderDomEditCommits(createSelection(element), iframe, {
|
|
1134
|
+
queueDomEditSave: queue.enqueue,
|
|
1135
|
+
projectIdRef,
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
try {
|
|
1139
|
+
const first = rendered.hook.handleDomStyleCommit("color", "blue");
|
|
1140
|
+
await flushAsyncWork();
|
|
1141
|
+
const second = rendered.hook.handleDomStyleCommit("color", "green");
|
|
1142
|
+
projectIdRef.current = "p2";
|
|
1143
|
+
firstPatch.resolve(
|
|
1144
|
+
jsonResponse({
|
|
1145
|
+
ok: true,
|
|
1146
|
+
changed: true,
|
|
1147
|
+
matched: true,
|
|
1148
|
+
content: '<div data-hf-id="hf-card" style="color: blue">Card</div>',
|
|
1149
|
+
path: "index.html",
|
|
1150
|
+
version: '"sha256:blue"',
|
|
1151
|
+
}),
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
await first;
|
|
1155
|
+
await expect(second).resolves.toMatchObject({
|
|
1156
|
+
ok: false,
|
|
1157
|
+
reason: "persist-failed",
|
|
1158
|
+
});
|
|
1159
|
+
expect(fetchMock.mock.calls.some(([input]) => requestUrl(input).includes("/p2/"))).toBe(
|
|
1160
|
+
false,
|
|
1161
|
+
);
|
|
1162
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
1163
|
+
} finally {
|
|
1164
|
+
queue.destroy();
|
|
1165
|
+
rendered.cleanup();
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
it("preserves the SDK cutover version as the style commit's durable evidence", async () => {
|
|
1170
|
+
const fetchMock = stubPatchFetch({ ok: true, changed: true, matched: true });
|
|
1171
|
+
const { iframe, element } = createPreviewElement();
|
|
1172
|
+
const rendered = renderDomEditCommits(createSelection(element), iframe, {
|
|
1173
|
+
onTrySdkPersist: async () => ({ status: "committed", version: "sdk-version-2" }),
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
try {
|
|
1177
|
+
let outcome: Awaited<ReturnType<typeof rendered.hook.handleDomStyleCommit>> | undefined;
|
|
1178
|
+
await act(async () => {
|
|
1179
|
+
outcome = await rendered.hook.handleDomStyleCommit("color", "blue");
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
expect(outcome).toEqual({
|
|
1183
|
+
ok: true,
|
|
1184
|
+
persistence: { sourceFile: "index.html", version: "sdk-version-2", changed: true },
|
|
1185
|
+
});
|
|
1186
|
+
expect(
|
|
1187
|
+
fetchMock.mock.calls.some(([input]) =>
|
|
1188
|
+
requestUrl(input).includes("/file-mutations/patch-element/"),
|
|
1189
|
+
),
|
|
1190
|
+
).toBe(false);
|
|
1191
|
+
} finally {
|
|
1192
|
+
rendered.cleanup();
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
|
|
995
1196
|
it("keeps a newer style value when an older overlapping commit later fails", async () => {
|
|
996
1197
|
const firstPatch = createDeferred<Response>();
|
|
997
1198
|
const secondPatch = createDeferred<Response>();
|
|
@@ -1466,9 +1667,7 @@ describe("useDomEditCommits attribute persist handling", () => {
|
|
|
1466
1667
|
// optimistic apply) and succeeds before the older one rejects. Without the
|
|
1467
1668
|
// per-key version guard, the stale rejection would revert to the older
|
|
1468
1669
|
// commit's own previousValue (null) and stomp the newer commit's value.
|
|
1469
|
-
const firstCommit =
|
|
1470
|
-
await rendered.hook.handleDomHtmlAttributeCommit("muted", "first-value");
|
|
1471
|
-
});
|
|
1670
|
+
const firstCommit = rendered.hook.handleDomHtmlAttributeCommit("muted", "first-value");
|
|
1472
1671
|
await act(async () => {
|
|
1473
1672
|
await rendered.hook.handleDomHtmlAttributeCommit("muted", "second-value");
|
|
1474
1673
|
});
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
patchElementBatches,
|
|
33
33
|
readErrorResponseBody,
|
|
34
34
|
} from "./useDomEditCommitsHelpers";
|
|
35
|
-
import {
|
|
35
|
+
import type { CutoverResult } from "../utils/sdkCutover";
|
|
36
36
|
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
|
37
37
|
interface RecordEditInput {
|
|
38
38
|
label: string;
|
|
@@ -139,11 +139,18 @@ export function useDomEditCommits({
|
|
|
139
139
|
const reportedUnresolvableRef = useRef(new Set<string>());
|
|
140
140
|
|
|
141
141
|
// fallow-ignore-next-line complexity
|
|
142
|
-
const
|
|
142
|
+
const performPersistDomEditOperations = useCallback(
|
|
143
143
|
// fallow-ignore-next-line complexity
|
|
144
|
-
async (
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
async (
|
|
145
|
+
selection: DomEditSelection,
|
|
146
|
+
operations: PatchOperation[],
|
|
147
|
+
options: Parameters<PersistDomEditOperations>[2],
|
|
148
|
+
expectedProjectId: string,
|
|
149
|
+
) => {
|
|
150
|
+
if (projectIdRef.current !== expectedProjectId) {
|
|
151
|
+
throw new Error("Active project changed before the edit could be saved");
|
|
152
|
+
}
|
|
153
|
+
const pid = expectedProjectId;
|
|
147
154
|
if (options?.shouldSave && !options.shouldSave()) return;
|
|
148
155
|
|
|
149
156
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
@@ -160,6 +167,10 @@ export function useDomEditCommits({
|
|
|
160
167
|
throw new Error(`Missing file contents for ${targetPath}`);
|
|
161
168
|
}
|
|
162
169
|
|
|
170
|
+
if (projectIdRef.current !== expectedProjectId) {
|
|
171
|
+
throw new Error("Active project changed before the edit could be saved");
|
|
172
|
+
}
|
|
173
|
+
|
|
163
174
|
if (options?.shouldSave && !options.shouldSave()) return;
|
|
164
175
|
|
|
165
176
|
// Validate layout values BEFORE any persist path runs. The SDK cutover
|
|
@@ -186,10 +197,11 @@ export function useDomEditCommits({
|
|
|
186
197
|
coalesceKey: options?.coalesceKey,
|
|
187
198
|
skipRefresh: options?.skipRefresh,
|
|
188
199
|
});
|
|
189
|
-
if (
|
|
200
|
+
if (cutover.status === "failed") throw cutover.error;
|
|
201
|
+
if (cutover.status === "committed") {
|
|
190
202
|
// SDK handled it — its in-memory doc is already current, so do NOT
|
|
191
203
|
// forceReload (that would echo-reload the session we just wrote).
|
|
192
|
-
return;
|
|
204
|
+
return { sourceFile: targetPath, version: cutover.version, changed: true };
|
|
193
205
|
}
|
|
194
206
|
}
|
|
195
207
|
|
|
@@ -218,6 +230,8 @@ export function useDomEditCommits({
|
|
|
218
230
|
changed?: boolean;
|
|
219
231
|
matched?: boolean;
|
|
220
232
|
content?: string;
|
|
233
|
+
path?: string;
|
|
234
|
+
version?: string;
|
|
221
235
|
};
|
|
222
236
|
|
|
223
237
|
if (!patchData.changed) {
|
|
@@ -235,7 +249,9 @@ export function useDomEditCommits({
|
|
|
235
249
|
throw new DomEditPersistUnresolvableError(targetPath);
|
|
236
250
|
}
|
|
237
251
|
warnDomEditPersistNoOp(selection, operations);
|
|
238
|
-
return
|
|
252
|
+
return typeof patchData.path === "string" && typeof patchData.version === "string"
|
|
253
|
+
? { sourceFile: patchData.path, version: patchData.version, changed: false }
|
|
254
|
+
: undefined;
|
|
239
255
|
}
|
|
240
256
|
|
|
241
257
|
const patchedContent =
|
|
@@ -273,6 +289,11 @@ export function useDomEditCommits({
|
|
|
273
289
|
if (!options?.skipRefresh) {
|
|
274
290
|
reloadPreview();
|
|
275
291
|
}
|
|
292
|
+
return finalContent === patchedContent &&
|
|
293
|
+
typeof patchData.path === "string" &&
|
|
294
|
+
typeof patchData.version === "string"
|
|
295
|
+
? { sourceFile: patchData.path, version: patchData.version, changed: true }
|
|
296
|
+
: undefined;
|
|
276
297
|
},
|
|
277
298
|
[
|
|
278
299
|
activeCompPath,
|
|
@@ -287,15 +308,30 @@ export function useDomEditCommits({
|
|
|
287
308
|
],
|
|
288
309
|
);
|
|
289
310
|
|
|
311
|
+
const persistDomEditOperations: PersistDomEditOperations = useCallback(
|
|
312
|
+
(selection, operations, options) => {
|
|
313
|
+
const expectedProjectId = projectIdRef.current;
|
|
314
|
+
if (!expectedProjectId) return Promise.reject(new Error("No active project"));
|
|
315
|
+
return queueDomEditSave(() =>
|
|
316
|
+
performPersistDomEditOperations(selection, operations, options, expectedProjectId),
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
[performPersistDomEditOperations, projectIdRef, queueDomEditSave],
|
|
320
|
+
);
|
|
321
|
+
|
|
290
322
|
const commitDomEditPatchBatches: CommitDomEditPatchBatches = useCallback(
|
|
291
|
-
(batches, options) =>
|
|
292
|
-
|
|
323
|
+
(batches, options) => {
|
|
324
|
+
const expectedProjectId = projectIdRef.current;
|
|
325
|
+
if (!expectedProjectId) return Promise.reject(new Error("No active project"));
|
|
326
|
+
return queueDomEditSave(
|
|
293
327
|
// One queued transaction owns validation, persistence, history, reload,
|
|
294
328
|
// and its durable result; splitting those phases risks partial commits.
|
|
295
329
|
// fallow-ignore-next-line complexity
|
|
296
330
|
async () => {
|
|
297
|
-
|
|
298
|
-
|
|
331
|
+
if (projectIdRef.current !== expectedProjectId) {
|
|
332
|
+
throw new Error("Active project changed before the edit could be saved");
|
|
333
|
+
}
|
|
334
|
+
const pid = expectedProjectId;
|
|
299
335
|
const unsafeFields = batches.flatMap((batch) =>
|
|
300
336
|
batch.patches.flatMap((patch) => findUnsafeDomPatchValues(patch)),
|
|
301
337
|
);
|
|
@@ -357,7 +393,8 @@ export function useDomEditCommits({
|
|
|
357
393
|
label: options.label,
|
|
358
394
|
});
|
|
359
395
|
throw error;
|
|
360
|
-
})
|
|
396
|
+
});
|
|
397
|
+
},
|
|
361
398
|
[
|
|
362
399
|
domEditSaveTimestampRef,
|
|
363
400
|
editHistory,
|
|
@@ -373,12 +410,14 @@ export function useDomEditCommits({
|
|
|
373
410
|
|
|
374
411
|
const {
|
|
375
412
|
handleDomStyleCommit,
|
|
413
|
+
handleDomStyleCommitForSelection,
|
|
376
414
|
handleDomAttributeCommit,
|
|
377
415
|
handleDomAttributeLiveCommit,
|
|
378
416
|
handleDomAttributeQuietCommit,
|
|
379
417
|
handleDomHtmlAttributeCommit,
|
|
380
418
|
handleDomAttributesCommit,
|
|
381
419
|
handleDomTextCommit,
|
|
420
|
+
handleDomTextCommitForSelection,
|
|
382
421
|
handleDomRichTextCommit,
|
|
383
422
|
commitDomTextFields,
|
|
384
423
|
handleDomTextFieldStyleCommit,
|
|
@@ -401,7 +440,6 @@ export function useDomEditCommits({
|
|
|
401
440
|
const commitPositionPatchToHtml = useDomEditPositionPatchCommit({
|
|
402
441
|
activeCompPath,
|
|
403
442
|
persistDomEditOperations,
|
|
404
|
-
queueDomEditSave,
|
|
405
443
|
showToast,
|
|
406
444
|
});
|
|
407
445
|
|
|
@@ -439,12 +477,14 @@ export function useDomEditCommits({
|
|
|
439
477
|
return {
|
|
440
478
|
resolveImportedFontAsset,
|
|
441
479
|
handleDomStyleCommit,
|
|
480
|
+
handleDomStyleCommitForSelection,
|
|
442
481
|
handleDomAttributeCommit,
|
|
443
482
|
handleDomAttributeLiveCommit,
|
|
444
483
|
handleDomAttributeQuietCommit,
|
|
445
484
|
handleDomHtmlAttributeCommit,
|
|
446
485
|
handleDomAttributesCommit,
|
|
447
486
|
handleDomTextCommit,
|
|
487
|
+
handleDomTextCommitForSelection,
|
|
448
488
|
handleDomRichTextCommit,
|
|
449
489
|
commitDomTextFields,
|
|
450
490
|
handleDomTextFieldStyleCommit,
|
|
@@ -55,14 +55,13 @@ function renderCommit(params: Parameters<typeof useDomEditPositionPatchCommit>[0
|
|
|
55
55
|
return captured.commit;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function paramsWith(
|
|
58
|
+
function paramsWith(persistDomEditOperations: () => Promise<undefined>) {
|
|
59
59
|
const showToast = vi.fn();
|
|
60
60
|
return {
|
|
61
61
|
showToast,
|
|
62
62
|
params: {
|
|
63
63
|
activeCompPath: "index.html",
|
|
64
|
-
persistDomEditOperations
|
|
65
|
-
queueDomEditSave,
|
|
64
|
+
persistDomEditOperations,
|
|
66
65
|
showToast,
|
|
67
66
|
},
|
|
68
67
|
};
|
|
@@ -104,7 +103,7 @@ describe("useDomEditPositionPatchCommit", () => {
|
|
|
104
103
|
});
|
|
105
104
|
|
|
106
105
|
it("resolves when the write lands", async () => {
|
|
107
|
-
const { showToast, params } = paramsWith((
|
|
106
|
+
const { showToast, params } = paramsWith(() => Promise.resolve(undefined));
|
|
108
107
|
const commit = renderCommit(params);
|
|
109
108
|
|
|
110
109
|
await act(async () => {
|
|
@@ -8,7 +8,6 @@ import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
|
8
8
|
interface UseDomEditPositionPatchCommitParams {
|
|
9
9
|
activeCompPath: string | null;
|
|
10
10
|
persistDomEditOperations: PersistDomEditOperations;
|
|
11
|
-
queueDomEditSave: (save: () => Promise<void>) => Promise<void>;
|
|
12
11
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
13
12
|
}
|
|
14
13
|
|
|
@@ -22,39 +21,38 @@ interface PositionPatchOptions {
|
|
|
22
21
|
export function useDomEditPositionPatchCommit({
|
|
23
22
|
activeCompPath,
|
|
24
23
|
persistDomEditOperations,
|
|
25
|
-
queueDomEditSave,
|
|
26
24
|
showToast,
|
|
27
25
|
}: UseDomEditPositionPatchCommitParams) {
|
|
28
26
|
return useCallback(
|
|
29
27
|
(selection: DomEditSelection, patches: PatchOperation[], options: PositionPatchOptions) => {
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
return persistDomEditOperations(selection, patches, {
|
|
29
|
+
label: options.label,
|
|
30
|
+
coalesceKey: options.coalesceKey,
|
|
31
|
+
coalesceMs: options.coalesceMs,
|
|
32
|
+
skipRefresh: options.skipRefresh ?? true,
|
|
33
|
+
})
|
|
34
|
+
.then(() => undefined)
|
|
35
|
+
.catch((error) => {
|
|
36
|
+
// A paused save queue is not worth a toast: the paused-save banner is
|
|
37
|
+
// already on screen, and one toast per blocked edit is what this branch
|
|
38
|
+
// exists to prevent. It still has to REJECT, though. Swallowing it
|
|
39
|
+
// resolved the commit, which skipped the caller's revert, so the element
|
|
40
|
+
// stayed where the drag put it while nothing reached the file.
|
|
41
|
+
if (error instanceof DomEditSaveQueueOpenError) throw error;
|
|
42
|
+
showToast(error instanceof Error ? error.message : "Failed to save position");
|
|
43
|
+
trackStudioSaveFailure({
|
|
44
|
+
source: "dom_edit",
|
|
45
|
+
error,
|
|
46
|
+
filePath: selection.sourceFile ?? activeCompPath ?? "index.html",
|
|
47
|
+
mutationType: "position",
|
|
48
|
+
label: options.label,
|
|
49
|
+
targetId: selection.id,
|
|
50
|
+
targetSelector: selection.selector,
|
|
51
|
+
targetSourceFile: selection.sourceFile,
|
|
52
|
+
});
|
|
53
|
+
throw error;
|
|
36
54
|
});
|
|
37
|
-
}).catch((error) => {
|
|
38
|
-
// A paused save queue is not worth a toast: the paused-save banner is
|
|
39
|
-
// already on screen, and one toast per blocked edit is what this branch
|
|
40
|
-
// exists to prevent. It still has to REJECT, though. Swallowing it
|
|
41
|
-
// resolved the commit, which skipped the caller's revert, so the element
|
|
42
|
-
// stayed where the drag put it while nothing reached the file.
|
|
43
|
-
if (error instanceof DomEditSaveQueueOpenError) throw error;
|
|
44
|
-
showToast(error instanceof Error ? error.message : "Failed to save position");
|
|
45
|
-
trackStudioSaveFailure({
|
|
46
|
-
source: "dom_edit",
|
|
47
|
-
error,
|
|
48
|
-
filePath: selection.sourceFile ?? activeCompPath ?? "index.html",
|
|
49
|
-
mutationType: "position",
|
|
50
|
-
label: options.label,
|
|
51
|
-
targetId: selection.id,
|
|
52
|
-
targetSelector: selection.selector,
|
|
53
|
-
targetSourceFile: selection.sourceFile,
|
|
54
|
-
});
|
|
55
|
-
throw error;
|
|
56
|
-
});
|
|
57
55
|
},
|
|
58
|
-
[activeCompPath, persistDomEditOperations,
|
|
56
|
+
[activeCompPath, persistDomEditOperations, showToast],
|
|
59
57
|
);
|
|
60
58
|
}
|
|
@@ -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,
|