@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.
Files changed (46) hide show
  1. package/dist/assets/{index-BGUJ2C2G.js → index-BhSyGx7y.js} +1 -1
  2. package/dist/assets/{index-Bz6Eqd_G.js → index-D0yNztV_.js} +1 -1
  3. package/dist/assets/{index-CLlPjdPl.js → index-kbACg3_I.js} +139 -139
  4. package/dist/{chunk-AN2EWWK3.js → chunk-JND3XUJL.js} +38 -10
  5. package/dist/chunk-JND3XUJL.js.map +1 -0
  6. package/dist/{domEditingLayers-EK7R7R4G.js → domEditingLayers-UIQZJCOA.js} +4 -2
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.html +1 -1
  9. package/dist/index.js +1145 -675
  10. package/dist/index.js.map +1 -1
  11. package/package.json +7 -7
  12. package/src/components/editor/DomEditOverlay.test.ts +95 -0
  13. package/src/components/editor/DomEditOverlay.tsx +5 -3
  14. package/src/components/editor/domEditOverlayGestures.ts +3 -4
  15. package/src/components/editor/domEditing.ts +1 -0
  16. package/src/components/editor/domEditingLayers.test.ts +52 -0
  17. package/src/components/editor/domEditingLayers.ts +55 -19
  18. package/src/components/editor/domEditingTypes.ts +1 -0
  19. package/src/components/editor/persistSeam.integration.test.ts +264 -0
  20. package/src/components/editor/propertyPanelPrimitives.tsx +15 -1
  21. package/src/components/editor/useDomEditOverlayGestures.ts +1 -0
  22. package/src/hooks/domEditCommitRunner.ts +46 -0
  23. package/src/hooks/domEditPersistFailure.test.ts +123 -0
  24. package/src/hooks/domEditPersistFailure.ts +89 -0
  25. package/src/hooks/domEditTextFieldCommitOps.test.ts +111 -0
  26. package/src/hooks/domEditTextFieldCommitOps.ts +63 -0
  27. package/src/hooks/domSelectionTestHarness.ts +40 -0
  28. package/src/hooks/useDomEditAttributeCommits.ts +227 -0
  29. package/src/hooks/useDomEditCommits.test.tsx +775 -0
  30. package/src/hooks/useDomEditCommits.ts +33 -5
  31. package/src/hooks/useDomEditTextCommits.ts +243 -220
  32. package/src/hooks/useDomSelection.test.ts +134 -0
  33. package/src/hooks/useDomSelection.ts +29 -15
  34. package/src/hooks/usePreviewInteraction.test.ts +260 -0
  35. package/src/hooks/usePreviewInteraction.ts +60 -19
  36. package/src/utils/sdkCutoverEligibility.test.ts +17 -0
  37. package/src/utils/sdkCutoverEligibility.ts +8 -2
  38. package/src/utils/sdkResolverShadow.test.ts +180 -1
  39. package/src/utils/sdkResolverShadow.ts +94 -1
  40. package/src/utils/sourcePatcher.ts +2 -0
  41. package/src/utils/studioPreviewHelpers.test.ts +95 -1
  42. package/src/utils/studioPreviewHelpers.ts +109 -10
  43. package/src/utils/studioSaveDiagnostics.ts +5 -2
  44. package/src/utils/studioTelemetry.ts +27 -20
  45. package/dist/chunk-AN2EWWK3.js.map +0 -1
  46. /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
+ }