@hyperframes/studio 0.7.28 → 0.7.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-BGUJ2C2G.js → index-BhSyGx7y.js} +1 -1
- package/dist/assets/{index-Bz6Eqd_G.js → index-D0yNztV_.js} +1 -1
- package/dist/assets/{index-CLlPjdPl.js → index-kbACg3_I.js} +139 -139
- package/dist/{chunk-AN2EWWK3.js → chunk-JND3XUJL.js} +38 -10
- package/dist/chunk-JND3XUJL.js.map +1 -0
- package/dist/{domEditingLayers-EK7R7R4G.js → domEditingLayers-UIQZJCOA.js} +4 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.html +1 -1
- package/dist/index.js +1145 -675
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/editor/DomEditOverlay.test.ts +95 -0
- package/src/components/editor/DomEditOverlay.tsx +5 -3
- package/src/components/editor/domEditOverlayGestures.ts +3 -4
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingLayers.test.ts +52 -0
- package/src/components/editor/domEditingLayers.ts +55 -19
- package/src/components/editor/domEditingTypes.ts +1 -0
- package/src/components/editor/persistSeam.integration.test.ts +264 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +15 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -0
- package/src/hooks/domEditCommitRunner.ts +46 -0
- package/src/hooks/domEditPersistFailure.test.ts +123 -0
- package/src/hooks/domEditPersistFailure.ts +89 -0
- package/src/hooks/domEditTextFieldCommitOps.test.ts +111 -0
- package/src/hooks/domEditTextFieldCommitOps.ts +63 -0
- package/src/hooks/domSelectionTestHarness.ts +40 -0
- package/src/hooks/useDomEditAttributeCommits.ts +227 -0
- package/src/hooks/useDomEditCommits.test.tsx +775 -0
- package/src/hooks/useDomEditCommits.ts +33 -5
- package/src/hooks/useDomEditTextCommits.ts +243 -220
- package/src/hooks/useDomSelection.test.ts +134 -0
- package/src/hooks/useDomSelection.ts +29 -15
- package/src/hooks/usePreviewInteraction.test.ts +260 -0
- package/src/hooks/usePreviewInteraction.ts +60 -19
- package/src/utils/sdkCutoverEligibility.test.ts +17 -0
- package/src/utils/sdkCutoverEligibility.ts +8 -2
- package/src/utils/sdkResolverShadow.test.ts +180 -1
- package/src/utils/sdkResolverShadow.ts +94 -1
- package/src/utils/sourcePatcher.ts +2 -0
- package/src/utils/studioPreviewHelpers.test.ts +95 -1
- package/src/utils/studioPreviewHelpers.ts +109 -10
- package/src/utils/studioSaveDiagnostics.ts +5 -2
- package/src/utils/studioTelemetry.ts +27 -20
- package/dist/chunk-AN2EWWK3.js.map +0 -1
- /package/dist/{domEditingLayers-EK7R7R4G.js.map → domEditingLayers-UIQZJCOA.js.map} +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDomEditStylePatchOperation,
|
|
3
|
+
buildDomEditTextPatchOperation,
|
|
4
|
+
buildTextFieldChildLocator,
|
|
5
|
+
type DomEditTextField,
|
|
6
|
+
} from "../components/editor/domEditing";
|
|
7
|
+
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
8
|
+
|
|
9
|
+
function hasSameKeysInSamePositions(
|
|
10
|
+
originalFields: DomEditTextField[],
|
|
11
|
+
nextFields: DomEditTextField[],
|
|
12
|
+
): boolean {
|
|
13
|
+
return originalFields.every((field, index) => nextFields[index]?.key === field.key);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function inlineStyleValue(styles: Record<string, string>, property: string): string | null {
|
|
17
|
+
return Object.prototype.hasOwnProperty.call(styles, property) ? styles[property] : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function inlineStyleProperties(
|
|
21
|
+
originalStyles: Record<string, string>,
|
|
22
|
+
nextStyles: Record<string, string>,
|
|
23
|
+
): string[] {
|
|
24
|
+
return Array.from(new Set([...Object.keys(originalStyles), ...Object.keys(nextStyles)]));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// fallow-ignore-next-line complexity
|
|
28
|
+
export function buildTextFieldChildOperations(
|
|
29
|
+
originalFields: DomEditTextField[],
|
|
30
|
+
nextFields: DomEditTextField[],
|
|
31
|
+
): PatchOperation[] | null {
|
|
32
|
+
if (originalFields.length !== nextFields.length) return null;
|
|
33
|
+
if (!hasSameKeysInSamePositions(originalFields, nextFields)) return null;
|
|
34
|
+
if (nextFields.some((field) => field.source === "text-node")) return null;
|
|
35
|
+
if (nextFields.some((field) => field.source !== "child")) return null;
|
|
36
|
+
if (originalFields.some((field) => field.source !== "child")) return null;
|
|
37
|
+
|
|
38
|
+
const originalByKey = new Map(originalFields.map((field) => [field.key, field]));
|
|
39
|
+
const operations: PatchOperation[] = [];
|
|
40
|
+
|
|
41
|
+
for (const nextField of nextFields) {
|
|
42
|
+
const originalField = originalByKey.get(nextField.key);
|
|
43
|
+
const locator = buildTextFieldChildLocator(originalFields, nextField.key);
|
|
44
|
+
if (!originalField || !locator) return null;
|
|
45
|
+
|
|
46
|
+
if (nextField.value !== originalField.value) {
|
|
47
|
+
operations.push(buildDomEditTextPatchOperation(nextField.value, locator));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const property of inlineStyleProperties(
|
|
51
|
+
originalField.inlineStyles,
|
|
52
|
+
nextField.inlineStyles,
|
|
53
|
+
)) {
|
|
54
|
+
const originalValue = inlineStyleValue(originalField.inlineStyles, property);
|
|
55
|
+
const nextValue = inlineStyleValue(nextField.inlineStyles, property);
|
|
56
|
+
if (nextValue !== originalValue) {
|
|
57
|
+
operations.push(buildDomEditStylePatchOperation(property, nextValue, locator));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return operations;
|
|
63
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Shared harness helpers for selection hook tests (useDomSelection,
|
|
2
|
+
// usePreviewInteraction). Test-only module.
|
|
3
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
|
+
|
|
5
|
+
export function installReactActEnvironment(): void {
|
|
6
|
+
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", {
|
|
7
|
+
value: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function makeSelection(label: string, element: HTMLElement): DomEditSelection {
|
|
13
|
+
return {
|
|
14
|
+
element,
|
|
15
|
+
id: element.id || undefined,
|
|
16
|
+
selector: `#${element.id || label}`,
|
|
17
|
+
selectorIndex: 0,
|
|
18
|
+
sourceFile: "index.html",
|
|
19
|
+
compositionPath: "index.html",
|
|
20
|
+
label,
|
|
21
|
+
tagName: element.tagName.toLowerCase(),
|
|
22
|
+
isCompositionHost: false,
|
|
23
|
+
isInsideLockedComposition: false,
|
|
24
|
+
boundingBox: { x: 0, y: 0, width: 100, height: 40 },
|
|
25
|
+
textContent: element.textContent,
|
|
26
|
+
dataAttributes: {},
|
|
27
|
+
inlineStyles: {},
|
|
28
|
+
computedStyles: {},
|
|
29
|
+
textFields: [],
|
|
30
|
+
capabilities: {
|
|
31
|
+
canSelect: true,
|
|
32
|
+
canEditStyles: true,
|
|
33
|
+
canMove: true,
|
|
34
|
+
canResize: true,
|
|
35
|
+
canApplyManualOffset: true,
|
|
36
|
+
canApplyManualSize: true,
|
|
37
|
+
canApplyManualRotation: false,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
3
|
+
import {
|
|
4
|
+
findElementForSelection,
|
|
5
|
+
getDomEditTargetKey,
|
|
6
|
+
type DomEditSelection,
|
|
7
|
+
} from "../components/editor/domEditing";
|
|
8
|
+
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
9
|
+
import { reportDomEditPersistFailure } from "./domEditPersistFailure";
|
|
10
|
+
import { bumpDomEditCommitMapVersion, runDomEditCommit } from "./domEditCommitRunner";
|
|
11
|
+
|
|
12
|
+
// ── Types ──
|
|
13
|
+
|
|
14
|
+
export interface UseDomEditAttributeCommitsParams {
|
|
15
|
+
activeCompPath: string | null;
|
|
16
|
+
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
17
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
18
|
+
domEditSelection: DomEditSelection | null;
|
|
19
|
+
refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => void;
|
|
20
|
+
persistDomEditOperations: PersistDomEditOperations;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DataAttributeCommitOptions {
|
|
24
|
+
label: string;
|
|
25
|
+
coalescePrefix: string;
|
|
26
|
+
skipRefresh: boolean;
|
|
27
|
+
refreshAfter?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveFullAttrName(attr: string, prefixData: boolean | undefined): string {
|
|
31
|
+
return prefixData && !attr.startsWith("data-") ? `data-${attr}` : attr;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setOrRemovePreviewAttribute(
|
|
35
|
+
el: HTMLElement,
|
|
36
|
+
fullAttr: string,
|
|
37
|
+
value: string | null,
|
|
38
|
+
): void {
|
|
39
|
+
if (value === null) {
|
|
40
|
+
el.removeAttribute(fullAttr);
|
|
41
|
+
} else {
|
|
42
|
+
el.setAttribute(fullAttr, value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findPreviewAttributeElement(
|
|
47
|
+
doc: Document | null | undefined,
|
|
48
|
+
selection: DomEditSelection,
|
|
49
|
+
activeCompPath: string | null,
|
|
50
|
+
): HTMLElement | null {
|
|
51
|
+
if (!doc) return null;
|
|
52
|
+
return findElementForSelection(doc, selection, activeCompPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CapturedAttributeElement {
|
|
56
|
+
element: HTMLElement;
|
|
57
|
+
previousValue: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function captureAttributeElement(
|
|
61
|
+
doc: Document | null | undefined,
|
|
62
|
+
selection: DomEditSelection,
|
|
63
|
+
activeCompPath: string | null,
|
|
64
|
+
fullAttr: string,
|
|
65
|
+
): CapturedAttributeElement | null {
|
|
66
|
+
const el = findPreviewAttributeElement(doc, selection, activeCompPath);
|
|
67
|
+
if (!el) return null;
|
|
68
|
+
return { element: el, previousValue: el.getAttribute(fullAttr) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Hook ──
|
|
72
|
+
|
|
73
|
+
// data-* attribute commits and raw HTML-attribute commits (e.g. muted, loop):
|
|
74
|
+
// both revert the optimistic write on persist failure, version-guarded per
|
|
75
|
+
// target+attribute so a stale failure can't stomp a newer successful commit.
|
|
76
|
+
export function useDomEditAttributeCommits({
|
|
77
|
+
activeCompPath,
|
|
78
|
+
previewIframeRef,
|
|
79
|
+
showToast,
|
|
80
|
+
domEditSelection,
|
|
81
|
+
refreshDomEditSelectionFromPreview,
|
|
82
|
+
persistDomEditOperations,
|
|
83
|
+
}: UseDomEditAttributeCommitsParams) {
|
|
84
|
+
const domAttributeCommitVersionRef = useRef(new Map<string, number>());
|
|
85
|
+
|
|
86
|
+
const commitDataAttribute = useCallback(
|
|
87
|
+
async (attr: string, value: string | null, options: DataAttributeCommitOptions) => {
|
|
88
|
+
if (!domEditSelection) return;
|
|
89
|
+
const iframe = previewIframeRef.current;
|
|
90
|
+
const fullAttr = resolveFullAttrName(attr, true);
|
|
91
|
+
const commitKey = `${options.coalescePrefix}:${attr}:${getDomEditTargetKey(domEditSelection)}`;
|
|
92
|
+
const isLatestCommit = bumpDomEditCommitMapVersion(
|
|
93
|
+
domAttributeCommitVersionRef.current,
|
|
94
|
+
commitKey,
|
|
95
|
+
);
|
|
96
|
+
const op: PatchOperation = { type: "attribute", property: attr, value };
|
|
97
|
+
let editedElement: HTMLElement | null = null;
|
|
98
|
+
let previousValue: string | null = null;
|
|
99
|
+
|
|
100
|
+
await runDomEditCommit({
|
|
101
|
+
capture: () => {
|
|
102
|
+
const captured = captureAttributeElement(
|
|
103
|
+
iframe?.contentDocument,
|
|
104
|
+
domEditSelection,
|
|
105
|
+
activeCompPath,
|
|
106
|
+
fullAttr,
|
|
107
|
+
);
|
|
108
|
+
if (!captured) return;
|
|
109
|
+
editedElement = captured.element;
|
|
110
|
+
previousValue = captured.previousValue;
|
|
111
|
+
},
|
|
112
|
+
apply: () => {
|
|
113
|
+
if (!editedElement) return;
|
|
114
|
+
const nextValue = value === null || value === "" ? null : value;
|
|
115
|
+
setOrRemovePreviewAttribute(editedElement, fullAttr, nextValue);
|
|
116
|
+
},
|
|
117
|
+
persist: () =>
|
|
118
|
+
persistDomEditOperations(domEditSelection, [op], {
|
|
119
|
+
label: options.label,
|
|
120
|
+
coalesceKey: commitKey,
|
|
121
|
+
skipRefresh: options.skipRefresh,
|
|
122
|
+
}),
|
|
123
|
+
shouldRevert: () => isLatestCommit(),
|
|
124
|
+
revert: () => {
|
|
125
|
+
if (!editedElement) return;
|
|
126
|
+
setOrRemovePreviewAttribute(editedElement, fullAttr, previousValue);
|
|
127
|
+
},
|
|
128
|
+
onError: (error) => reportDomEditPersistFailure(domEditSelection, [op], error, showToast),
|
|
129
|
+
shouldResync: () => isLatestCommit() && !!options.refreshAfter,
|
|
130
|
+
resync: () => refreshDomEditSelectionFromPreview(domEditSelection),
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
[
|
|
134
|
+
activeCompPath,
|
|
135
|
+
domEditSelection,
|
|
136
|
+
persistDomEditOperations,
|
|
137
|
+
refreshDomEditSelectionFromPreview,
|
|
138
|
+
showToast,
|
|
139
|
+
previewIframeRef,
|
|
140
|
+
],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const handleDomAttributeCommit = useCallback(
|
|
144
|
+
async (attr: string, value: string) => {
|
|
145
|
+
await commitDataAttribute(attr, value, {
|
|
146
|
+
label: `Edit ${attr.replace(/-/g, " ")}`,
|
|
147
|
+
coalescePrefix: "attr",
|
|
148
|
+
skipRefresh: false,
|
|
149
|
+
refreshAfter: true,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
[commitDataAttribute],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const handleDomAttributeLiveCommit = useCallback(
|
|
156
|
+
async (attr: string, value: string | null) => {
|
|
157
|
+
await commitDataAttribute(attr, value, {
|
|
158
|
+
label: `Edit ${attr.replace(/^(data-)?/, "").replace(/-/g, " ")}`,
|
|
159
|
+
coalescePrefix: "attr-live",
|
|
160
|
+
skipRefresh: true,
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
[commitDataAttribute],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const handleDomHtmlAttributeCommit = useCallback(
|
|
167
|
+
async (attr: string, value: string | null) => {
|
|
168
|
+
if (!domEditSelection) return;
|
|
169
|
+
const iframe = previewIframeRef.current;
|
|
170
|
+
const commitKey = `html-attr:${attr}:${getDomEditTargetKey(domEditSelection)}`;
|
|
171
|
+
const isLatestCommit = bumpDomEditCommitMapVersion(
|
|
172
|
+
domAttributeCommitVersionRef.current,
|
|
173
|
+
commitKey,
|
|
174
|
+
);
|
|
175
|
+
const op: PatchOperation = { type: "html-attribute", property: attr, value };
|
|
176
|
+
let editedElement: HTMLElement | null = null;
|
|
177
|
+
let previousValue: string | null = null;
|
|
178
|
+
|
|
179
|
+
await runDomEditCommit({
|
|
180
|
+
capture: () => {
|
|
181
|
+
const captured = captureAttributeElement(
|
|
182
|
+
iframe?.contentDocument,
|
|
183
|
+
domEditSelection,
|
|
184
|
+
activeCompPath,
|
|
185
|
+
attr,
|
|
186
|
+
);
|
|
187
|
+
if (!captured) return;
|
|
188
|
+
editedElement = captured.element;
|
|
189
|
+
previousValue = captured.previousValue;
|
|
190
|
+
},
|
|
191
|
+
apply: () => {
|
|
192
|
+
if (!editedElement) return;
|
|
193
|
+
const nextValue = value === null || value === "false" ? null : value;
|
|
194
|
+
setOrRemovePreviewAttribute(editedElement, attr, nextValue);
|
|
195
|
+
},
|
|
196
|
+
persist: () =>
|
|
197
|
+
persistDomEditOperations(domEditSelection, [op], {
|
|
198
|
+
label: `Edit ${attr}`,
|
|
199
|
+
coalesceKey: commitKey,
|
|
200
|
+
skipRefresh: false,
|
|
201
|
+
}),
|
|
202
|
+
shouldRevert: () => isLatestCommit(),
|
|
203
|
+
revert: () => {
|
|
204
|
+
if (!editedElement) return;
|
|
205
|
+
setOrRemovePreviewAttribute(editedElement, attr, previousValue);
|
|
206
|
+
},
|
|
207
|
+
onError: (error) => reportDomEditPersistFailure(domEditSelection, [op], error, showToast),
|
|
208
|
+
shouldResync: () => isLatestCommit(),
|
|
209
|
+
resync: () => refreshDomEditSelectionFromPreview(domEditSelection),
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
[
|
|
213
|
+
activeCompPath,
|
|
214
|
+
domEditSelection,
|
|
215
|
+
persistDomEditOperations,
|
|
216
|
+
refreshDomEditSelectionFromPreview,
|
|
217
|
+
showToast,
|
|
218
|
+
previewIframeRef,
|
|
219
|
+
],
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
handleDomAttributeCommit,
|
|
224
|
+
handleDomAttributeLiveCommit,
|
|
225
|
+
handleDomHtmlAttributeCommit,
|
|
226
|
+
};
|
|
227
|
+
}
|