@agent-native/core 0.80.9 → 0.80.10
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/skills.ts +45 -24
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
- package/corpus/templates/design/actions/apply-design-state.ts +200 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/get-component-details.ts +242 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +72 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +237 -0
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +421 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
- package/corpus/templates/design/app/i18n-data.ts +269 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +10 -4
- package/corpus/templates/design/server/db/schema.ts +123 -0
- package/corpus/templates/design/server/plugins/db.ts +90 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1016 -71
- package/corpus/templates/design/shared/component-model.ts +239 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/motion-compiler.ts +278 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
- package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +37 -21
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -15,6 +15,249 @@ import { isTrustedCanvasBridgeMessage } from "./bridge-security";
|
|
|
15
15
|
import { DeviceFrame } from "./DeviceFrame";
|
|
16
16
|
import type { ElementInfo, DeviceFrameType } from "./types";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Allowlist check for Fusion (Builder-hosted) frame origins.
|
|
20
|
+
*
|
|
21
|
+
* Fusion frames are served cross-origin from the Builder-hosted app, so the
|
|
22
|
+
* strict `origin === parentOrigin` bridge check can never match. Before relaxing
|
|
23
|
+
* trust to window-identity only, we must confirm the message origin is actually
|
|
24
|
+
* a Builder host — the exact origin of the `fusionUrl` we were asked to render,
|
|
25
|
+
* or any `*.builder.io` host (plus the bare `builder.io`), over https. This
|
|
26
|
+
* prevents the relaxed-trust path from accepting messages from an arbitrary
|
|
27
|
+
* cross-origin frame that merely shares our iframe's window reference.
|
|
28
|
+
*/
|
|
29
|
+
function isAllowedFusionOrigin(
|
|
30
|
+
origin: string,
|
|
31
|
+
fusionUrl: string | undefined,
|
|
32
|
+
): boolean {
|
|
33
|
+
if (!origin || origin === "null") return false;
|
|
34
|
+
let host: string;
|
|
35
|
+
let protocol: string;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(origin);
|
|
38
|
+
host = parsed.hostname.toLowerCase();
|
|
39
|
+
protocol = parsed.protocol;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// Only allow secure (https) Builder origins.
|
|
44
|
+
if (protocol !== "https:") return false;
|
|
45
|
+
// Exact match against the configured fusion URL's origin.
|
|
46
|
+
if (fusionUrl) {
|
|
47
|
+
try {
|
|
48
|
+
if (new URL(fusionUrl).origin === origin) return true;
|
|
49
|
+
} catch {
|
|
50
|
+
// Malformed fusionUrl — fall through to the host-family allowlist.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Builder host family: builder.io and any subdomain of it.
|
|
54
|
+
return host === "builder.io" || host.endsWith(".builder.io");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Wire shape for a single motion track sent via the `motion-load-tracks`
|
|
59
|
+
* postMessage. Matches the serialisable subset of `MotionTrack` from
|
|
60
|
+
* `shared/motion-timeline.ts` without requiring an import at the UI layer.
|
|
61
|
+
*/
|
|
62
|
+
export interface MotionTrackWire {
|
|
63
|
+
targetNodeId: string;
|
|
64
|
+
property: string;
|
|
65
|
+
keyframes: Array<{ t: number; value: string; ease?: string }>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Motion-preview bridge. Injected alongside the other bridge scripts so the
|
|
70
|
+
* MotionDock's scrubbing preview works in ALL editor modes without writing
|
|
71
|
+
* anything to the DB, Yjs state, or source files.
|
|
72
|
+
*
|
|
73
|
+
* Protocol (parent → iframe):
|
|
74
|
+
*
|
|
75
|
+
* { type: 'motion-load-tracks', tracks: MotionTrackWire[] }
|
|
76
|
+
* Load (or replace) the track list for this document. Each entry:
|
|
77
|
+
* { targetNodeId, property, keyframes: [{ t, value, ease? }] }
|
|
78
|
+
* where t ∈ [0, 1]. Sent whenever the active timeline changes.
|
|
79
|
+
*
|
|
80
|
+
* { type: 'motion-preview', t, durationMs }
|
|
81
|
+
* Seek all loaded tracks to normalised position t ∈ [0, 1] and apply
|
|
82
|
+
* the interpolated CSS property values as inline styles on the matching
|
|
83
|
+
* [data-agent-native-node-id="…"] elements. Never writes to storage.
|
|
84
|
+
*
|
|
85
|
+
* { type: 'motion-preview-clear' }
|
|
86
|
+
* Remove all motion-preview inline-style overrides and the in-memory
|
|
87
|
+
* track list. Called when the dock is closed or the timeline is
|
|
88
|
+
* discarded.
|
|
89
|
+
*
|
|
90
|
+
* Easing is deliberately simple: linear interpolation between keyframe
|
|
91
|
+
* values (CSS handles easing when the compiled CSS is actually applied;
|
|
92
|
+
* preview is a live visualisation, not a perfect recreation of the final
|
|
93
|
+
* animation).
|
|
94
|
+
*/
|
|
95
|
+
const MOTION_PREVIEW_BRIDGE_SCRIPT = `
|
|
96
|
+
<script data-agent-native-motion-preview-bridge>
|
|
97
|
+
(function() {
|
|
98
|
+
// Track list loaded by 'motion-load-tracks'.
|
|
99
|
+
var loadedTracks = [];
|
|
100
|
+
// Map of nodeId -> [property, ...] we have touched, for cleanup.
|
|
101
|
+
var touchedProps = {};
|
|
102
|
+
|
|
103
|
+
function lerp(a, b, ratio) {
|
|
104
|
+
var numA = parseFloat(a);
|
|
105
|
+
var numB = parseFloat(b);
|
|
106
|
+
if (Number.isFinite(numA) && Number.isFinite(numB)) {
|
|
107
|
+
var suffix = a.replace(/^-?[\\d.]+/, '') || b.replace(/^-?[\\d.]+/, '');
|
|
108
|
+
return (numA + (numB - numA) * ratio).toFixed(4).replace(/\\.?0+$/, '') + suffix;
|
|
109
|
+
}
|
|
110
|
+
// Non-numeric: snap to b after the midpoint.
|
|
111
|
+
return ratio < 0.5 ? a : b;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function interpolate(keyframes, t) {
|
|
115
|
+
if (!keyframes || keyframes.length === 0) return '';
|
|
116
|
+
if (keyframes.length === 1) return keyframes[0].value;
|
|
117
|
+
// Find surrounding keyframes.
|
|
118
|
+
var prev = keyframes[0];
|
|
119
|
+
var next = keyframes[keyframes.length - 1];
|
|
120
|
+
for (var i = 0; i < keyframes.length - 1; i++) {
|
|
121
|
+
if (t >= keyframes[i].t && t <= keyframes[i + 1].t) {
|
|
122
|
+
prev = keyframes[i];
|
|
123
|
+
next = keyframes[i + 1];
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
var span = next.t - prev.t;
|
|
128
|
+
if (span <= 0) return prev.value;
|
|
129
|
+
var ratio = Math.max(0, Math.min(1, (t - prev.t) / span));
|
|
130
|
+
return lerp(prev.value, next.value, ratio);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyPreview(t) {
|
|
134
|
+
for (var i = 0; i < loadedTracks.length; i++) {
|
|
135
|
+
var track = loadedTracks[i];
|
|
136
|
+
var el = document.querySelector('[data-agent-native-node-id="' + track.targetNodeId + '"]');
|
|
137
|
+
if (!el) continue;
|
|
138
|
+
var value = interpolate(track.keyframes, t);
|
|
139
|
+
if (value === '') continue;
|
|
140
|
+
el.style[track.property] = value;
|
|
141
|
+
if (!touchedProps[track.targetNodeId]) touchedProps[track.targetNodeId] = [];
|
|
142
|
+
if (touchedProps[track.targetNodeId].indexOf(track.property) === -1) {
|
|
143
|
+
touchedProps[track.targetNodeId].push(track.property);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clearPreview() {
|
|
149
|
+
var nodeIds = Object.keys(touchedProps);
|
|
150
|
+
for (var i = 0; i < nodeIds.length; i++) {
|
|
151
|
+
var el = document.querySelector('[data-agent-native-node-id="' + nodeIds[i] + '"]');
|
|
152
|
+
if (!el) continue;
|
|
153
|
+
var props = touchedProps[nodeIds[i]];
|
|
154
|
+
for (var j = 0; j < props.length; j++) {
|
|
155
|
+
el.style[props[j]] = '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
touchedProps = {};
|
|
159
|
+
loadedTracks = [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
window.addEventListener('message', function(e) {
|
|
163
|
+
if (e.source !== window.parent) return;
|
|
164
|
+
if (!e.data || typeof e.data.type !== 'string') return;
|
|
165
|
+
if (e.data.type === 'motion-load-tracks') {
|
|
166
|
+
loadedTracks = Array.isArray(e.data.tracks) ? e.data.tracks : [];
|
|
167
|
+
touchedProps = {};
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (e.data.type === 'motion-preview') {
|
|
171
|
+
var t = Number(e.data.t);
|
|
172
|
+
if (!Number.isFinite(t)) return;
|
|
173
|
+
t = Math.max(0, Math.min(1, t));
|
|
174
|
+
applyPreview(t);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (e.data.type === 'motion-preview-clear') {
|
|
178
|
+
clearPreview();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
})();
|
|
183
|
+
</script>
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Shader-fill preview bridge. ALWAYS injected alongside the other bridge
|
|
188
|
+
* scripts so the parent can apply a CSS gradient approximation of a shader
|
|
189
|
+
* fill to the currently-selected element **without** persisting anything.
|
|
190
|
+
*
|
|
191
|
+
* Protocol (parent → iframe):
|
|
192
|
+
*
|
|
193
|
+
* { type: 'shader-fill-preview', selector, nodeId, css }
|
|
194
|
+
* Apply `css` as the `background` inline style on the first element that
|
|
195
|
+
* matches `selector` (preferred) or `[data-agent-native-node-id="nodeId"]`.
|
|
196
|
+
* When both are absent, targets `document.body`. Stores the previous
|
|
197
|
+
* background value so it can be restored on clear.
|
|
198
|
+
* Preview-only — never writes to DB, Yjs, or source files.
|
|
199
|
+
*
|
|
200
|
+
* { type: 'shader-fill-preview-clear' }
|
|
201
|
+
* Remove the applied background override and restore the previous value.
|
|
202
|
+
* Called when the user discards the preview or switches selections.
|
|
203
|
+
*/
|
|
204
|
+
const SHADER_FILL_PREVIEW_BRIDGE_SCRIPT = `
|
|
205
|
+
<script data-agent-native-shader-fill-preview-bridge>
|
|
206
|
+
(function() {
|
|
207
|
+
// Track the element we patched and its original background so we can undo.
|
|
208
|
+
var patchedEl = null;
|
|
209
|
+
var originalBackground = '';
|
|
210
|
+
|
|
211
|
+
function resolveTarget(selector, nodeId) {
|
|
212
|
+
if (selector) {
|
|
213
|
+
try {
|
|
214
|
+
var hit = document.querySelector(selector);
|
|
215
|
+
if (hit) return hit;
|
|
216
|
+
} catch (_err) {}
|
|
217
|
+
}
|
|
218
|
+
if (nodeId) {
|
|
219
|
+
var byId = document.querySelector('[data-agent-native-node-id="' + nodeId.replace(/"/g, '\\\\"') + '"]');
|
|
220
|
+
if (byId) return byId;
|
|
221
|
+
}
|
|
222
|
+
return document.body;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function applyPreview(selector, nodeId, css) {
|
|
226
|
+
// Clear any prior patch first so we don't stack patches.
|
|
227
|
+
clearPreview();
|
|
228
|
+
var el = resolveTarget(selector, nodeId);
|
|
229
|
+
if (!el) return;
|
|
230
|
+
originalBackground = el.style.background || '';
|
|
231
|
+
el.style.background = css || '';
|
|
232
|
+
patchedEl = el;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function clearPreview() {
|
|
236
|
+
if (!patchedEl) return;
|
|
237
|
+
patchedEl.style.background = originalBackground;
|
|
238
|
+
patchedEl = null;
|
|
239
|
+
originalBackground = '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
window.addEventListener('message', function(e) {
|
|
243
|
+
if (e.source !== window.parent) return;
|
|
244
|
+
if (!e.data || typeof e.data.type !== 'string') return;
|
|
245
|
+
if (e.data.type === 'shader-fill-preview') {
|
|
246
|
+
var selector = typeof e.data.selector === 'string' ? e.data.selector : '';
|
|
247
|
+
var nodeId = typeof e.data.nodeId === 'string' ? e.data.nodeId : '';
|
|
248
|
+
var css = typeof e.data.css === 'string' ? e.data.css : '';
|
|
249
|
+
applyPreview(selector, nodeId, css);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (e.data.type === 'shader-fill-preview-clear') {
|
|
253
|
+
clearPreview();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
})();
|
|
258
|
+
</script>
|
|
259
|
+
`;
|
|
260
|
+
|
|
18
261
|
/**
|
|
19
262
|
* Tweak-bridge script. ALWAYS injected so the parent's postMessage
|
|
20
263
|
* (`tweak-values`) can update CSS custom properties on the iframe's :root
|
|
@@ -260,7 +503,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
260
503
|
var chromeTransitionStyle = document.createElement('style');
|
|
261
504
|
chromeTransitionStyle.textContent =
|
|
262
505
|
'[data-agent-native-edit-overlay="selection"]{transition:border-width 150ms ease-out}' +
|
|
263
|
-
'[data-agent-native-edge-handle],[data-agent-native-edit-handle],[data-agent-native-rotate-handle]{transition:width 150ms ease-out,height 150ms ease-out,border-width 150ms ease-out,top 150ms ease-out,bottom 150ms ease-out,left 150ms ease-out,right 150ms ease-out}'
|
|
506
|
+
'[data-agent-native-edge-handle],[data-agent-native-edit-handle],[data-agent-native-rotate-handle]{transition:width 150ms ease-out,height 150ms ease-out,border-width 150ms ease-out,top 150ms ease-out,bottom 150ms ease-out,left 150ms ease-out,right 150ms ease-out}' +
|
|
507
|
+
'[data-agent-native-spacing-line]{position:absolute;display:none;pointer-events:none;border-radius:999px}' +
|
|
508
|
+
'[data-agent-native-spacing-region]{position:absolute;display:none;box-sizing:border-box;pointer-events:auto;background-size:6px 6px}' +
|
|
509
|
+
'[data-agent-native-spacing-region][data-orientation="vertical"]{cursor:ew-resize}' +
|
|
510
|
+
'[data-agent-native-spacing-region][data-orientation="horizontal"]{cursor:ns-resize}';
|
|
264
511
|
(document.head || document.documentElement).appendChild(chromeTransitionStyle);
|
|
265
512
|
})();
|
|
266
513
|
|
|
@@ -484,6 +731,38 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
484
731
|
reason: 'Parent layout context decides whether movement means gap, order, alignment, or wrapper structure.',
|
|
485
732
|
});
|
|
486
733
|
}
|
|
734
|
+
// --- provenance: read source-location attributes when present ---
|
|
735
|
+
// These are emitted by connected apps via @vitejs/plugin-react jsxDEV or a
|
|
736
|
+
// Babel source plugin. Cross-origin localhost iframes cannot be read (CSP /
|
|
737
|
+
// same-origin policy), so this will be undefined in that case — expected.
|
|
738
|
+
var provenance = undefined;
|
|
739
|
+
var dataSourceFile = el.getAttribute('data-source-file');
|
|
740
|
+
var dataSourceLine = el.getAttribute('data-source-line');
|
|
741
|
+
var dataSourceColumn = el.getAttribute('data-source-column');
|
|
742
|
+
var dataComponentName = el.getAttribute('data-component-name');
|
|
743
|
+
var dataLoc = el.getAttribute('data-loc');
|
|
744
|
+
// data-loc may encode "file:line:col" (Babel source plugin convention).
|
|
745
|
+
// Only parse it when data-source-file is absent, to avoid double-reads.
|
|
746
|
+
if (!dataSourceFile && dataLoc) {
|
|
747
|
+
var lastColonIndex = dataLoc.lastIndexOf(':');
|
|
748
|
+
var lastPart = lastColonIndex >= 0 ? dataLoc.slice(lastColonIndex + 1) : '';
|
|
749
|
+
if (lastColonIndex >= 0 && /^\\d+$/.test(lastPart)) {
|
|
750
|
+
var beforeLastPart = dataLoc.slice(0, lastColonIndex);
|
|
751
|
+
var previousColonIndex = beforeLastPart.lastIndexOf(':');
|
|
752
|
+
var previousPart = previousColonIndex >= 0 ? beforeLastPart.slice(previousColonIndex + 1) : '';
|
|
753
|
+
var hasColumn = /^\\d+$/.test(previousPart);
|
|
754
|
+
dataSourceFile = hasColumn ? beforeLastPart.slice(0, previousColonIndex) : beforeLastPart;
|
|
755
|
+
dataSourceLine = hasColumn ? previousPart : lastPart;
|
|
756
|
+
if (hasColumn) dataSourceColumn = lastPart;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (dataSourceFile || dataSourceLine || dataSourceColumn || dataComponentName) {
|
|
760
|
+
provenance = {};
|
|
761
|
+
if (dataSourceFile) provenance.sourceFile = dataSourceFile;
|
|
762
|
+
if (dataSourceLine) { var ln = parseInt(dataSourceLine, 10); if (!isNaN(ln)) provenance.line = ln; }
|
|
763
|
+
if (dataSourceColumn) { var col = parseInt(dataSourceColumn, 10); if (!isNaN(col)) provenance.column = col; }
|
|
764
|
+
if (dataComponentName) provenance.component = dataComponentName;
|
|
765
|
+
}
|
|
487
766
|
return {
|
|
488
767
|
tagName: el.tagName.toLowerCase(),
|
|
489
768
|
id: el.id || undefined,
|
|
@@ -559,6 +838,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
559
838
|
confidence: capabilities.reduce(function(best, item) {
|
|
560
839
|
return Math.max(best, item.confidence || 0);
|
|
561
840
|
}, 0),
|
|
841
|
+
provenance: provenance,
|
|
562
842
|
};
|
|
563
843
|
}
|
|
564
844
|
|
|
@@ -635,10 +915,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
635
915
|
if (pos.indexOf('e') !== -1) rotate.style.right = '-26px';
|
|
636
916
|
selectionOverlay.appendChild(rotate);
|
|
637
917
|
});
|
|
638
|
-
var
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
selectionOverlay.appendChild(
|
|
918
|
+
var spacingOverlay = document.createElement('div');
|
|
919
|
+
spacingOverlay.setAttribute('data-agent-native-spacing-overlay', '');
|
|
920
|
+
spacingOverlay.style.cssText = 'position:absolute;inset:0;display:none;pointer-events:none;';
|
|
921
|
+
selectionOverlay.appendChild(spacingOverlay);
|
|
642
922
|
document.body.appendChild(selectionOverlay);
|
|
643
923
|
|
|
644
924
|
var transformBadge = document.createElement('div');
|
|
@@ -646,6 +926,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
646
926
|
transformBadge.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--background) / 0.96);color:hsl(var(--foreground));font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;padding:3px 5px;box-shadow:0 8px 20px color-mix(in srgb, hsl(var(--foreground)) 16%, transparent);';
|
|
647
927
|
document.body.appendChild(transformBadge);
|
|
648
928
|
|
|
929
|
+
var spacingBadge = document.createElement('div');
|
|
930
|
+
spacingBadge.setAttribute('data-agent-native-spacing-badge', '');
|
|
931
|
+
spacingBadge.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;border-radius:3px;color:white;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:700;padding:2px 4px;box-shadow:0 4px 14px rgba(0,0,0,0.18);';
|
|
932
|
+
document.body.appendChild(spacingBadge);
|
|
933
|
+
|
|
649
934
|
var insertionGuide = document.createElement('div');
|
|
650
935
|
insertionGuide.setAttribute('data-agent-native-insertion-guide', '');
|
|
651
936
|
insertionGuide.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;background:var(--design-editor-accent-color);border-radius:999px;box-shadow:0 0 0 1px var(--design-editor-accent-color);';
|
|
@@ -656,20 +941,116 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
656
941
|
measurementOverlay.style.cssText = 'position:fixed;inset:0;z-index:100001;display:none;pointer-events:none;color:var(--design-editor-measure-color);font:11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;';
|
|
657
942
|
document.body.appendChild(measurementOverlay);
|
|
658
943
|
|
|
944
|
+
// Component-instance tag: a small pill that floats above the selection
|
|
945
|
+
// outline whenever the selected element carries a data-agent-native-component
|
|
946
|
+
// attribute. Clicking it sends a 'component-source-jump' message to the
|
|
947
|
+
// parent so the editor can invoke open-component-source.
|
|
948
|
+
var componentTagOverlay = document.createElement('div');
|
|
949
|
+
componentTagOverlay.setAttribute('data-agent-native-edit-overlay', 'component-tag');
|
|
950
|
+
componentTagOverlay.style.cssText = [
|
|
951
|
+
'position:fixed',
|
|
952
|
+
'z-index:100002',
|
|
953
|
+
'display:none',
|
|
954
|
+
'pointer-events:auto',
|
|
955
|
+
'cursor:pointer',
|
|
956
|
+
'padding:2px 6px',
|
|
957
|
+
'border-radius:4px',
|
|
958
|
+
'font:11px/1.6 ui-sans-serif,system-ui,sans-serif',
|
|
959
|
+
'white-space:nowrap',
|
|
960
|
+
'user-select:none',
|
|
961
|
+
'-webkit-user-select:none',
|
|
962
|
+
'background:var(--design-editor-accent-color)',
|
|
963
|
+
'color:var(--design-editor-accent-contrast-color)',
|
|
964
|
+
'box-shadow:0 1px 4px color-mix(in srgb,var(--design-editor-accent-color) 40%,transparent)',
|
|
965
|
+
'border:1px solid color-mix(in srgb,var(--design-editor-accent-strong-color) 60%,transparent)',
|
|
966
|
+
'outline:2px solid transparent',
|
|
967
|
+
'transition:opacity 0.1s',
|
|
968
|
+
].join(';') + ';';
|
|
969
|
+
document.body.appendChild(componentTagOverlay);
|
|
970
|
+
|
|
971
|
+
componentTagOverlay.addEventListener('click', function(e) {
|
|
972
|
+
e.stopPropagation();
|
|
973
|
+
e.preventDefault();
|
|
974
|
+
var nodeId = componentTagOverlay.getAttribute('data-component-node-id') || '';
|
|
975
|
+
var componentName = componentTagOverlay.getAttribute('data-component-name') || '';
|
|
976
|
+
if (!nodeId || !componentName) return;
|
|
977
|
+
try {
|
|
978
|
+
window.parent.postMessage({
|
|
979
|
+
type: 'component-source-jump',
|
|
980
|
+
nodeId: nodeId,
|
|
981
|
+
componentName: componentName,
|
|
982
|
+
}, '*');
|
|
983
|
+
} catch (_err) {}
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
function updateComponentTag(el) {
|
|
987
|
+
if (!el) {
|
|
988
|
+
clearComponentTag();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
var compName = el.getAttribute && el.getAttribute('data-agent-native-component');
|
|
992
|
+
if (!compName) {
|
|
993
|
+
clearComponentTag();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
var nodeId = (
|
|
997
|
+
el.getAttribute('data-agent-native-node-id') ||
|
|
998
|
+
el.getAttribute('data-code-layer-id') ||
|
|
999
|
+
el.getAttribute('data-layer-id') ||
|
|
1000
|
+
el.id ||
|
|
1001
|
+
''
|
|
1002
|
+
);
|
|
1003
|
+
componentTagOverlay.textContent = compName + ' →';
|
|
1004
|
+
componentTagOverlay.setAttribute('data-component-node-id', nodeId);
|
|
1005
|
+
componentTagOverlay.setAttribute('data-component-name', compName);
|
|
1006
|
+
|
|
1007
|
+
var rect = el.getBoundingClientRect();
|
|
1008
|
+
var tagHeight = 22;
|
|
1009
|
+
var tagTop = rect.top - tagHeight - 4;
|
|
1010
|
+
if (tagTop < 4) tagTop = rect.top + 4;
|
|
1011
|
+
componentTagOverlay.style.display = 'block';
|
|
1012
|
+
componentTagOverlay.style.left = rect.left + 'px';
|
|
1013
|
+
componentTagOverlay.style.top = tagTop + 'px';
|
|
1014
|
+
// Accent outline on the selection overlay to distinguish component roots.
|
|
1015
|
+
selectionOverlay.style.outline = '2px solid var(--design-editor-accent-strong-color)';
|
|
1016
|
+
selectionOverlay.style.outlineOffset = '2px';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function clearComponentTag() {
|
|
1020
|
+
componentTagOverlay.style.display = 'none';
|
|
1021
|
+
componentTagOverlay.removeAttribute('data-component-node-id');
|
|
1022
|
+
componentTagOverlay.removeAttribute('data-component-name');
|
|
1023
|
+
selectionOverlay.style.outline = '';
|
|
1024
|
+
selectionOverlay.style.outlineOffset = '';
|
|
1025
|
+
}
|
|
1026
|
+
|
|
659
1027
|
var selectedEl = null;
|
|
660
1028
|
var hoveredEl = null;
|
|
661
1029
|
var activeTextEditEl = null;
|
|
662
1030
|
var textEditPointerState = null;
|
|
663
1031
|
var pendingStructureMove = null;
|
|
1032
|
+
var pendingShieldDrag = null;
|
|
1033
|
+
var suppressNextShieldClick = false;
|
|
1034
|
+
var suppressNextShieldClickTimer = null;
|
|
1035
|
+
var selectedSpacingHovered = false;
|
|
1036
|
+
var hoveredSpacingHandleKey = '';
|
|
1037
|
+
var spacingHandleStateByKey = {};
|
|
1038
|
+
var spacingHandleNodesByKey = {};
|
|
1039
|
+
var spacingDrag = null;
|
|
664
1040
|
var lockedSelectors = [];
|
|
665
1041
|
var hiddenSelectors = [];
|
|
666
1042
|
|
|
667
1043
|
function clearRuntimeSelection() {
|
|
668
1044
|
selectedEl = null;
|
|
669
1045
|
hoveredEl = null;
|
|
1046
|
+
selectedSpacingHovered = false;
|
|
1047
|
+
hoveredSpacingHandleKey = '';
|
|
1048
|
+
spacingDrag = null;
|
|
670
1049
|
selectionOverlay.style.display = 'none';
|
|
671
1050
|
highlightOverlay.style.display = 'none';
|
|
1051
|
+
hideSpacingOverlay();
|
|
672
1052
|
hideMeasurements();
|
|
1053
|
+
clearComponentTag();
|
|
673
1054
|
}
|
|
674
1055
|
|
|
675
1056
|
function matchesSelectorList(el, selectors) {
|
|
@@ -849,22 +1230,365 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
849
1230
|
refreshOverlays();
|
|
850
1231
|
}
|
|
851
1232
|
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1233
|
+
function hideSpacingOverlay() {
|
|
1234
|
+
spacingOverlay.style.display = 'none';
|
|
1235
|
+
spacingOverlay.innerHTML = '';
|
|
1236
|
+
spacingHandleStateByKey = {};
|
|
1237
|
+
spacingHandleNodesByKey = {};
|
|
1238
|
+
if (!spacingDrag) spacingBadge.style.display = 'none';
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function visibleLayoutChildren(el) {
|
|
1242
|
+
if (!el || !el.children) return [];
|
|
1243
|
+
return Array.prototype.slice.call(el.children).filter(function(child) {
|
|
1244
|
+
if (!child || child.nodeType !== 1 || isOverlayElement(child) || isLayerInteractionBlocked(child)) return false;
|
|
1245
|
+
var cs = window.getComputedStyle(child);
|
|
1246
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.position === 'fixed') return false;
|
|
1247
|
+
var rect = child.getBoundingClientRect();
|
|
1248
|
+
return rect.width > 0 && rect.height > 0;
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function spacingColor(kind) {
|
|
1253
|
+
return kind === 'gap' ? '#ff4fd8' : 'var(--design-editor-accent-color)';
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function spacingFill(kind, orientation) {
|
|
1257
|
+
var tint = kind === 'gap' ? 'rgba(255, 79, 216, 0.28)' : 'rgba(46, 168, 255, 0.24)';
|
|
1258
|
+
var stripe = kind === 'gap' ? 'rgba(255, 79, 216, 0.58)' : 'rgba(46, 168, 255, 0.52)';
|
|
1259
|
+
var angle = orientation === 'vertical' ? '135deg' : '45deg';
|
|
1260
|
+
return 'repeating-linear-gradient(' + angle + ', ' + stripe + ' 0 1px, ' + tint + ' 1px 4px, transparent 4px 7px)';
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function clampSpacingValue(value) {
|
|
1264
|
+
var rounded = Math.round(value);
|
|
1265
|
+
if (!Number.isFinite(rounded)) return 0;
|
|
1266
|
+
return Math.max(0, Math.min(999, rounded));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function makeSpacingHandle(config) {
|
|
1270
|
+
var region = config.region;
|
|
1271
|
+
if (!region || region.width <= 0 || region.height <= 0) return null;
|
|
1272
|
+
return {
|
|
1273
|
+
key: config.key,
|
|
1274
|
+
groupKey: config.groupKey || config.key,
|
|
1275
|
+
kind: config.kind,
|
|
1276
|
+
property: config.property,
|
|
1277
|
+
oppositeProperty: config.oppositeProperty || '',
|
|
1278
|
+
side: config.side || '',
|
|
1279
|
+
orientation: config.orientation,
|
|
1280
|
+
value: clampSpacingValue(config.value),
|
|
1281
|
+
region: {
|
|
1282
|
+
x: Math.round(region.x),
|
|
1283
|
+
y: Math.round(region.y),
|
|
1284
|
+
width: Math.max(1, Math.round(region.width)),
|
|
1285
|
+
height: Math.max(1, Math.round(region.height)),
|
|
1286
|
+
},
|
|
1287
|
+
line: config.line,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function childLocalRect(child, containerRect) {
|
|
1292
|
+
var rect = child.getBoundingClientRect();
|
|
1293
|
+
return {
|
|
1294
|
+
left: rect.left - containerRect.left,
|
|
1295
|
+
top: rect.top - containerRect.top,
|
|
1296
|
+
right: rect.right - containerRect.left,
|
|
1297
|
+
bottom: rect.bottom - containerRect.top,
|
|
1298
|
+
width: rect.width,
|
|
1299
|
+
height: rect.height,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function childRectsOverlap(a, b, axis) {
|
|
1304
|
+
if (axis === 'x') {
|
|
1305
|
+
return Math.max(a.top, b.top) < Math.min(a.bottom, b.bottom);
|
|
856
1306
|
}
|
|
1307
|
+
return Math.max(a.left, b.left) < Math.min(a.right, b.right);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function buildPaddingSpacingHandles(el, rect, cs) {
|
|
1311
|
+
var handles = [];
|
|
1312
|
+
var borderTop = readPx(cs.borderTopWidth);
|
|
1313
|
+
var borderRight = readPx(cs.borderRightWidth);
|
|
1314
|
+
var borderBottom = readPx(cs.borderBottomWidth);
|
|
1315
|
+
var borderLeft = readPx(cs.borderLeftWidth);
|
|
1316
|
+
var paddingTop = readPx(cs.paddingTop);
|
|
1317
|
+
var paddingRight = readPx(cs.paddingRight);
|
|
1318
|
+
var paddingBottom = readPx(cs.paddingBottom);
|
|
1319
|
+
var paddingLeft = readPx(cs.paddingLeft);
|
|
1320
|
+
var sx = chromeScaleX();
|
|
1321
|
+
var sy = chromeScaleY();
|
|
1322
|
+
var line = Math.max(1, chromeLineScale());
|
|
1323
|
+
var hLineWidth = Math.max(8, Math.min(24, rect.width * 0.18)) * sx;
|
|
1324
|
+
var vLineHeight = Math.max(8, Math.min(24, rect.height * 0.18)) * sy;
|
|
1325
|
+
var innerLeft = borderLeft;
|
|
1326
|
+
var innerTop = borderTop;
|
|
1327
|
+
var innerWidth = Math.max(1, rect.width - borderLeft - borderRight);
|
|
1328
|
+
var innerHeight = Math.max(1, rect.height - borderTop - borderBottom);
|
|
1329
|
+
if (paddingTop > 0) {
|
|
1330
|
+
handles.push(makeSpacingHandle({
|
|
1331
|
+
key: 'padding:top',
|
|
1332
|
+
kind: 'padding',
|
|
1333
|
+
property: 'paddingTop',
|
|
1334
|
+
oppositeProperty: 'paddingBottom',
|
|
1335
|
+
side: 'top',
|
|
1336
|
+
orientation: 'horizontal',
|
|
1337
|
+
value: paddingTop,
|
|
1338
|
+
region: { x: innerLeft, y: innerTop, width: innerWidth, height: paddingTop },
|
|
1339
|
+
line: {
|
|
1340
|
+
x: rect.width / 2 - hLineWidth / 2,
|
|
1341
|
+
y: innerTop + paddingTop / 2 - line / 2,
|
|
1342
|
+
width: hLineWidth,
|
|
1343
|
+
height: line,
|
|
1344
|
+
},
|
|
1345
|
+
}));
|
|
1346
|
+
}
|
|
1347
|
+
if (paddingBottom > 0) {
|
|
1348
|
+
handles.push(makeSpacingHandle({
|
|
1349
|
+
key: 'padding:bottom',
|
|
1350
|
+
kind: 'padding',
|
|
1351
|
+
property: 'paddingBottom',
|
|
1352
|
+
oppositeProperty: 'paddingTop',
|
|
1353
|
+
side: 'bottom',
|
|
1354
|
+
orientation: 'horizontal',
|
|
1355
|
+
value: paddingBottom,
|
|
1356
|
+
region: { x: innerLeft, y: rect.height - borderBottom - paddingBottom, width: innerWidth, height: paddingBottom },
|
|
1357
|
+
line: {
|
|
1358
|
+
x: rect.width / 2 - hLineWidth / 2,
|
|
1359
|
+
y: rect.height - borderBottom - paddingBottom / 2 - line / 2,
|
|
1360
|
+
width: hLineWidth,
|
|
1361
|
+
height: line,
|
|
1362
|
+
},
|
|
1363
|
+
}));
|
|
1364
|
+
}
|
|
1365
|
+
if (paddingLeft > 0) {
|
|
1366
|
+
handles.push(makeSpacingHandle({
|
|
1367
|
+
key: 'padding:left',
|
|
1368
|
+
kind: 'padding',
|
|
1369
|
+
property: 'paddingLeft',
|
|
1370
|
+
oppositeProperty: 'paddingRight',
|
|
1371
|
+
side: 'left',
|
|
1372
|
+
orientation: 'vertical',
|
|
1373
|
+
value: paddingLeft,
|
|
1374
|
+
region: { x: innerLeft, y: innerTop, width: paddingLeft, height: innerHeight },
|
|
1375
|
+
line: {
|
|
1376
|
+
x: innerLeft + paddingLeft / 2 - line / 2,
|
|
1377
|
+
y: rect.height / 2 - vLineHeight / 2,
|
|
1378
|
+
width: line,
|
|
1379
|
+
height: vLineHeight,
|
|
1380
|
+
},
|
|
1381
|
+
}));
|
|
1382
|
+
}
|
|
1383
|
+
if (paddingRight > 0) {
|
|
1384
|
+
handles.push(makeSpacingHandle({
|
|
1385
|
+
key: 'padding:right',
|
|
1386
|
+
kind: 'padding',
|
|
1387
|
+
property: 'paddingRight',
|
|
1388
|
+
oppositeProperty: 'paddingLeft',
|
|
1389
|
+
side: 'right',
|
|
1390
|
+
orientation: 'vertical',
|
|
1391
|
+
value: paddingRight,
|
|
1392
|
+
region: { x: rect.width - borderRight - paddingRight, y: innerTop, width: paddingRight, height: innerHeight },
|
|
1393
|
+
line: {
|
|
1394
|
+
x: rect.width - borderRight - paddingRight / 2 - line / 2,
|
|
1395
|
+
y: rect.height / 2 - vLineHeight / 2,
|
|
1396
|
+
width: line,
|
|
1397
|
+
height: vLineHeight,
|
|
1398
|
+
},
|
|
1399
|
+
}));
|
|
1400
|
+
}
|
|
1401
|
+
return handles.filter(Boolean);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function buildGapSpacingHandles(el, rect, cs) {
|
|
1405
|
+
var children = visibleLayoutChildren(el);
|
|
1406
|
+
if (children.length < 2) return [];
|
|
1407
|
+
var handles = [];
|
|
1408
|
+
var sx = chromeScaleX();
|
|
1409
|
+
var sy = chromeScaleY();
|
|
1410
|
+
var line = Math.max(1, chromeLineScale());
|
|
1411
|
+
var hLineWidth = 10 * sx;
|
|
1412
|
+
var vLineHeight = 10 * sy;
|
|
1413
|
+
var isFlex = cs.display === 'flex' || cs.display === 'inline-flex';
|
|
1414
|
+
var isGrid = cs.display === 'grid' || cs.display === 'inline-grid';
|
|
1415
|
+
if (!isFlex && !isGrid) return handles;
|
|
1416
|
+
var primaryAxis = isFlex && cs.flexDirection && cs.flexDirection.indexOf('column') === 0 ? 'y' : 'x';
|
|
1417
|
+
var childRects = children.map(function(child) {
|
|
1418
|
+
return childLocalRect(child, rect);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
function addAxisGaps(axis, property, groupKey) {
|
|
1422
|
+
var cssGap = readPx(cs[property]);
|
|
1423
|
+
if (cssGap <= 0) return;
|
|
1424
|
+
var sorted = childRects.slice().sort(function(a, b) {
|
|
1425
|
+
return axis === 'x' ? a.left - b.left : a.top - b.top;
|
|
1426
|
+
});
|
|
1427
|
+
var count = 0;
|
|
1428
|
+
for (var i = 0; i < sorted.length - 1; i += 1) {
|
|
1429
|
+
var a = sorted[i];
|
|
1430
|
+
var b = sorted[i + 1];
|
|
1431
|
+
if (!childRectsOverlap(a, b, axis)) continue;
|
|
1432
|
+
var gap = axis === 'x' ? b.left - a.right : b.top - a.bottom;
|
|
1433
|
+
if (gap <= 1) continue;
|
|
1434
|
+
if (axis === 'x') {
|
|
1435
|
+
var top = Math.max(a.top, b.top);
|
|
1436
|
+
var bottom = Math.min(a.bottom, b.bottom);
|
|
1437
|
+
var height = Math.max(1, bottom - top);
|
|
1438
|
+
handles.push(makeSpacingHandle({
|
|
1439
|
+
key: groupKey + ':' + count,
|
|
1440
|
+
groupKey: groupKey,
|
|
1441
|
+
kind: 'gap',
|
|
1442
|
+
property: property,
|
|
1443
|
+
orientation: 'vertical',
|
|
1444
|
+
value: cssGap,
|
|
1445
|
+
region: { x: a.right, y: top, width: gap, height: height },
|
|
1446
|
+
line: {
|
|
1447
|
+
x: a.right + gap / 2 - line / 2,
|
|
1448
|
+
y: top + height / 2 - vLineHeight / 2,
|
|
1449
|
+
width: line,
|
|
1450
|
+
height: vLineHeight,
|
|
1451
|
+
},
|
|
1452
|
+
}));
|
|
1453
|
+
} else {
|
|
1454
|
+
var left = Math.max(a.left, b.left);
|
|
1455
|
+
var right = Math.min(a.right, b.right);
|
|
1456
|
+
var width = Math.max(1, right - left);
|
|
1457
|
+
handles.push(makeSpacingHandle({
|
|
1458
|
+
key: groupKey + ':' + count,
|
|
1459
|
+
groupKey: groupKey,
|
|
1460
|
+
kind: 'gap',
|
|
1461
|
+
property: property,
|
|
1462
|
+
orientation: 'horizontal',
|
|
1463
|
+
value: cssGap,
|
|
1464
|
+
region: { x: left, y: a.bottom, width: width, height: gap },
|
|
1465
|
+
line: {
|
|
1466
|
+
x: left + width / 2 - hLineWidth / 2,
|
|
1467
|
+
y: a.bottom + gap / 2 - line / 2,
|
|
1468
|
+
width: hLineWidth,
|
|
1469
|
+
height: line,
|
|
1470
|
+
},
|
|
1471
|
+
}));
|
|
1472
|
+
}
|
|
1473
|
+
count += 1;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (primaryAxis === 'x') {
|
|
1478
|
+
addAxisGaps('x', 'columnGap', 'gap:column');
|
|
1479
|
+
if (isGrid) addAxisGaps('y', 'rowGap', 'gap:row');
|
|
1480
|
+
} else {
|
|
1481
|
+
addAxisGaps('y', 'rowGap', 'gap:row');
|
|
1482
|
+
if (isGrid) addAxisGaps('x', 'columnGap', 'gap:column');
|
|
1483
|
+
}
|
|
1484
|
+
return handles.filter(Boolean);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function buildSpacingHandles(el) {
|
|
1488
|
+
if (!el || !document.documentElement.contains(el)) return [];
|
|
1489
|
+
if (Math.abs(currentRotation(el)) > 0.01) return [];
|
|
1490
|
+
var children = visibleLayoutChildren(el);
|
|
1491
|
+
if (children.length === 0) return [];
|
|
1492
|
+
var rect = el.getBoundingClientRect();
|
|
1493
|
+
if (rect.width <= 0 || rect.height <= 0) return [];
|
|
857
1494
|
var cs = window.getComputedStyle(el);
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1495
|
+
return buildPaddingSpacingHandles(el, rect, cs).concat(buildGapSpacingHandles(el, rect, cs));
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function showSpacingBadgeForHandle(handle, value) {
|
|
1499
|
+
if (!selectedEl || !handle) {
|
|
1500
|
+
spacingBadge.style.display = 'none';
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
var rect = selectedEl.getBoundingClientRect();
|
|
1504
|
+
var x = rect.left + handle.region.x + handle.region.width / 2;
|
|
1505
|
+
var y = rect.top + handle.region.y + handle.region.height / 2;
|
|
1506
|
+
spacingBadge.textContent = String(clampSpacingValue(value));
|
|
1507
|
+
spacingBadge.style.display = 'block';
|
|
1508
|
+
spacingBadge.style.background = spacingColor(handle.kind);
|
|
1509
|
+
spacingBadge.style.left = x + 'px';
|
|
1510
|
+
spacingBadge.style.top = y + 'px';
|
|
1511
|
+
spacingBadge.style.transform = 'translate(-50%, -50%)';
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function renderSpacingHandle(handle, activeGroupKey) {
|
|
1515
|
+
if (!handle) return;
|
|
1516
|
+
spacingHandleStateByKey[handle.key] = handle;
|
|
1517
|
+
var highlighted = Boolean(activeGroupKey && handle.groupKey === activeGroupKey);
|
|
1518
|
+
var lineNode = document.createElement('span');
|
|
1519
|
+
lineNode.setAttribute('data-agent-native-spacing-line', handle.kind);
|
|
1520
|
+
lineNode.style.display = 'block';
|
|
1521
|
+
lineNode.style.left = handle.line.x + 'px';
|
|
1522
|
+
lineNode.style.top = handle.line.y + 'px';
|
|
1523
|
+
lineNode.style.width = Math.max(1, handle.line.width) + 'px';
|
|
1524
|
+
lineNode.style.height = Math.max(1, handle.line.height) + 'px';
|
|
1525
|
+
lineNode.style.background = spacingColor(handle.kind);
|
|
1526
|
+
spacingOverlay.appendChild(lineNode);
|
|
1527
|
+
|
|
1528
|
+
var regionNode = document.createElement('span');
|
|
1529
|
+
regionNode.setAttribute('data-agent-native-spacing-region', handle.kind);
|
|
1530
|
+
regionNode.setAttribute('data-orientation', handle.orientation);
|
|
1531
|
+
regionNode.setAttribute('data-spacing-key', handle.key);
|
|
1532
|
+
regionNode.style.display = 'block';
|
|
1533
|
+
regionNode.style.left = handle.region.x + 'px';
|
|
1534
|
+
regionNode.style.top = handle.region.y + 'px';
|
|
1535
|
+
regionNode.style.width = handle.region.width + 'px';
|
|
1536
|
+
regionNode.style.height = handle.region.height + 'px';
|
|
1537
|
+
regionNode.style.background = highlighted ? spacingFill(handle.kind, handle.orientation) : 'transparent';
|
|
1538
|
+
regionNode.style.outline = highlighted ? '1px solid ' + spacingColor(handle.kind) : '0';
|
|
1539
|
+
regionNode.style.outlineOffset = '-1px';
|
|
1540
|
+
regionNode.addEventListener('mouseenter', function() {
|
|
1541
|
+
hoveredSpacingHandleKey = handle.key;
|
|
1542
|
+
selectedSpacingHovered = true;
|
|
1543
|
+
updateSpacingOverlay(selectedEl);
|
|
1544
|
+
});
|
|
1545
|
+
regionNode.addEventListener('mouseleave', function() {
|
|
1546
|
+
if (spacingDrag) return;
|
|
1547
|
+
hoveredSpacingHandleKey = '';
|
|
1548
|
+
updateSpacingOverlay(selectedEl);
|
|
1549
|
+
});
|
|
1550
|
+
regionNode.addEventListener('mousedown', function(event) {
|
|
1551
|
+
startSpacingDrag(handle.key, event);
|
|
1552
|
+
}, true);
|
|
1553
|
+
spacingHandleNodesByKey[handle.key] = regionNode;
|
|
1554
|
+
spacingOverlay.appendChild(regionNode);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function updateSpacingOverlay(el) {
|
|
1558
|
+
if (el && el !== selectedEl) {
|
|
1559
|
+
hideSpacingOverlay();
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (!selectedEl || !document.documentElement.contains(selectedEl)) {
|
|
1563
|
+
hideSpacingOverlay();
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (!selectedSpacingHovered && !hoveredSpacingHandleKey && !spacingDrag) {
|
|
1567
|
+
hideSpacingOverlay();
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
var handles = buildSpacingHandles(selectedEl);
|
|
1571
|
+
if (handles.length === 0) {
|
|
1572
|
+
hideSpacingOverlay();
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
spacingOverlay.style.display = 'block';
|
|
1576
|
+
spacingOverlay.innerHTML = '';
|
|
1577
|
+
spacingHandleStateByKey = {};
|
|
1578
|
+
spacingHandleNodesByKey = {};
|
|
1579
|
+
var activeHandle =
|
|
1580
|
+
(spacingDrag && spacingDrag.handle) ||
|
|
1581
|
+
handles.find(function(handle) { return handle.key === hoveredSpacingHandleKey; }) ||
|
|
1582
|
+
null;
|
|
1583
|
+
var activeGroupKey = activeHandle ? activeHandle.groupKey : '';
|
|
1584
|
+
handles.forEach(function(handle) {
|
|
1585
|
+
renderSpacingHandle(handle, activeGroupKey);
|
|
1586
|
+
});
|
|
1587
|
+
if (activeHandle) {
|
|
1588
|
+
showSpacingBadgeForHandle(activeHandle, spacingDrag ? spacingDrag.currentValue : activeHandle.value);
|
|
1589
|
+
} else {
|
|
1590
|
+
spacingBadge.style.display = 'none';
|
|
1591
|
+
}
|
|
868
1592
|
}
|
|
869
1593
|
|
|
870
1594
|
function applyEditorChromeScale() {
|
|
@@ -874,7 +1598,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
874
1598
|
var line = chromeLineScale();
|
|
875
1599
|
highlightOverlay.style.borderWidth = (1.5 * line) + 'px';
|
|
876
1600
|
selectionOverlay.style.borderWidth = (1.5 * line) + 'px';
|
|
877
|
-
|
|
1601
|
+
if (selectedEl) updateSpacingOverlay(selectedEl);
|
|
878
1602
|
|
|
879
1603
|
selectionOverlay.querySelectorAll('[data-agent-native-edge-handle]').forEach(function(edge) {
|
|
880
1604
|
var pos = edge.getAttribute('data-agent-native-edge-handle');
|
|
@@ -913,6 +1637,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
913
1637
|
function positionOverlay(overlay, el) {
|
|
914
1638
|
if (!el || !document.documentElement.contains(el)) {
|
|
915
1639
|
overlay.style.display = 'none';
|
|
1640
|
+
if (overlay === selectionOverlay) clearComponentTag();
|
|
916
1641
|
return;
|
|
917
1642
|
}
|
|
918
1643
|
// For the selection overlay, prefer the CSS box + rotation transform so
|
|
@@ -936,7 +1661,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
936
1661
|
overlay.style.height = elH + 'px';
|
|
937
1662
|
overlay.style.transform = 'rotate(' + elRot + 'deg)';
|
|
938
1663
|
overlay.style.transformOrigin = '0 0';
|
|
939
|
-
|
|
1664
|
+
updateSpacingOverlay(el);
|
|
1665
|
+
updateComponentTag(el);
|
|
940
1666
|
return;
|
|
941
1667
|
}
|
|
942
1668
|
}
|
|
@@ -947,7 +1673,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
947
1673
|
overlay.style.width = rect.width + 'px';
|
|
948
1674
|
overlay.style.height = rect.height + 'px';
|
|
949
1675
|
overlay.style.transform = '';
|
|
950
|
-
if (overlay === selectionOverlay)
|
|
1676
|
+
if (overlay === selectionOverlay) {
|
|
1677
|
+
updateSpacingOverlay(el);
|
|
1678
|
+
updateComponentTag(el);
|
|
1679
|
+
}
|
|
951
1680
|
}
|
|
952
1681
|
|
|
953
1682
|
function refreshOverlays() {
|
|
@@ -1026,6 +1755,13 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1026
1755
|
);
|
|
1027
1756
|
}
|
|
1028
1757
|
|
|
1758
|
+
function dragEventNames(e) {
|
|
1759
|
+
var pointerGesture = e && e.type && e.type.indexOf('pointer') === 0;
|
|
1760
|
+
return pointerGesture
|
|
1761
|
+
? { move: 'pointermove', up: 'pointerup' }
|
|
1762
|
+
: { move: 'mousemove', up: 'mouseup' };
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1029
1765
|
function elementFromEditorPoint(clientX, clientY) {
|
|
1030
1766
|
var shieldPointerEvents = shieldOverlay.style.pointerEvents;
|
|
1031
1767
|
var selectionPointerEvents = selectionOverlay.style.pointerEvents;
|
|
@@ -1190,6 +1926,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1190
1926
|
function selectElementAtEvent(e) {
|
|
1191
1927
|
stopNativeInteraction(e);
|
|
1192
1928
|
blurActiveTextEditor();
|
|
1929
|
+
if (suppressNextShieldClick) {
|
|
1930
|
+
suppressNextShieldClick = false;
|
|
1931
|
+
if (suppressNextShieldClickTimer !== null) {
|
|
1932
|
+
clearTimeout(suppressNextShieldClickTimer);
|
|
1933
|
+
suppressNextShieldClickTimer = null;
|
|
1934
|
+
}
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1193
1937
|
// Suppress the first click in a double-click sequence — the dblclick
|
|
1194
1938
|
// handler (beginTextEditingFromEvent) will fire immediately after and a
|
|
1195
1939
|
// spurious element-select would cause inspector flicker.
|
|
@@ -1197,23 +1941,37 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1197
1941
|
var target = elementFromEditorPoint(e.clientX, e.clientY);
|
|
1198
1942
|
if (!target || target === document.body || target === document.documentElement) {
|
|
1199
1943
|
// Click on empty canvas: clear the current selection (matches Figma).
|
|
1200
|
-
|
|
1201
|
-
selectionOverlay.style.display = 'none';
|
|
1944
|
+
clearRuntimeSelection();
|
|
1202
1945
|
window.parent.postMessage({ type: 'clear-selection' }, '*');
|
|
1203
1946
|
return;
|
|
1204
1947
|
}
|
|
1948
|
+
selectedSpacingHovered = false;
|
|
1949
|
+
hoveredSpacingHandleKey = '';
|
|
1205
1950
|
selectedEl = selectionTargetForHit(target);
|
|
1206
1951
|
var info = getElementInfo(selectedEl);
|
|
1207
1952
|
positionOverlay(selectionOverlay, selectedEl);
|
|
1208
1953
|
window.parent.postMessage({ type: 'element-select', payload: info }, '*');
|
|
1209
1954
|
}
|
|
1210
1955
|
|
|
1956
|
+
function suppressNextShieldClickBriefly() {
|
|
1957
|
+
suppressNextShieldClick = true;
|
|
1958
|
+
if (suppressNextShieldClickTimer !== null) {
|
|
1959
|
+
clearTimeout(suppressNextShieldClickTimer);
|
|
1960
|
+
}
|
|
1961
|
+
suppressNextShieldClickTimer = setTimeout(function() {
|
|
1962
|
+
suppressNextShieldClick = false;
|
|
1963
|
+
suppressNextShieldClickTimer = null;
|
|
1964
|
+
}, 250);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1211
1967
|
function openContextMenuAtEvent(e) {
|
|
1212
1968
|
stopNativeInteraction(e);
|
|
1213
1969
|
blurActiveTextEditor();
|
|
1214
1970
|
var target = elementFromEditorPoint(e.clientX, e.clientY);
|
|
1215
1971
|
var info = null;
|
|
1216
1972
|
if (target) {
|
|
1973
|
+
selectedSpacingHovered = false;
|
|
1974
|
+
hoveredSpacingHandleKey = '';
|
|
1217
1975
|
selectedEl = selectionTargetForHit(target);
|
|
1218
1976
|
info = getElementInfo(selectedEl);
|
|
1219
1977
|
positionOverlay(selectionOverlay, selectedEl);
|
|
@@ -1319,6 +2077,81 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1319
2077
|
}, '*');
|
|
1320
2078
|
}
|
|
1321
2079
|
|
|
2080
|
+
function spacingValueFromPointer(handle, originValue, startX, startY, clientX, clientY) {
|
|
2081
|
+
var delta =
|
|
2082
|
+
handle.orientation === 'vertical' ? clientX - startX : clientY - startY;
|
|
2083
|
+
if (handle.kind === 'padding' && (handle.side === 'right' || handle.side === 'bottom')) {
|
|
2084
|
+
delta = -delta;
|
|
2085
|
+
}
|
|
2086
|
+
return clampSpacingValue(originValue + delta);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
function applySpacingDragValue(target, handle, value, mirrorOpposite) {
|
|
2090
|
+
if (!target || !handle) return;
|
|
2091
|
+
target.style[handle.property] = value + 'px';
|
|
2092
|
+
if (handle.kind === 'padding' && mirrorOpposite && handle.oppositeProperty) {
|
|
2093
|
+
target.style[handle.oppositeProperty] = value + 'px';
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function startSpacingDrag(key, e) {
|
|
2098
|
+
var handle = spacingHandleStateByKey[key];
|
|
2099
|
+
if (!selectedEl || !handle || isLayerInteractionBlocked(selectedEl)) return;
|
|
2100
|
+
e.preventDefault();
|
|
2101
|
+
e.stopPropagation();
|
|
2102
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2103
|
+
var events = dragEventNames(e);
|
|
2104
|
+
var dragEl = selectedEl;
|
|
2105
|
+
var originValue = handle.value;
|
|
2106
|
+
var startX = e.clientX;
|
|
2107
|
+
var startY = e.clientY;
|
|
2108
|
+
hoveredSpacingHandleKey = key;
|
|
2109
|
+
selectedSpacingHovered = true;
|
|
2110
|
+
spacingDrag = {
|
|
2111
|
+
handle: handle,
|
|
2112
|
+
currentValue: originValue,
|
|
2113
|
+
mirrorOpposite: !!e.altKey,
|
|
2114
|
+
};
|
|
2115
|
+
showSpacingBadgeForHandle(handle, originValue);
|
|
2116
|
+
|
|
2117
|
+
function onMove(ev) {
|
|
2118
|
+
if (!dragEl || !document.documentElement.contains(dragEl)) return;
|
|
2119
|
+
var nextValue = spacingValueFromPointer(handle, originValue, startX, startY, ev.clientX, ev.clientY);
|
|
2120
|
+
spacingDrag = {
|
|
2121
|
+
handle: handle,
|
|
2122
|
+
currentValue: nextValue,
|
|
2123
|
+
mirrorOpposite: !!ev.altKey,
|
|
2124
|
+
};
|
|
2125
|
+
applySpacingDragValue(dragEl, handle, nextValue, !!ev.altKey);
|
|
2126
|
+
positionOverlay(selectionOverlay, dragEl);
|
|
2127
|
+
showSpacingBadgeForHandle(handle, nextValue);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function onUp(ev) {
|
|
2131
|
+
document.removeEventListener(events.move, onMove, true);
|
|
2132
|
+
document.removeEventListener(events.up, onUp, true);
|
|
2133
|
+
if (!dragEl || !document.documentElement.contains(dragEl)) {
|
|
2134
|
+
spacingDrag = null;
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
var finalValue = spacingDrag ? spacingDrag.currentValue : originValue;
|
|
2138
|
+
var mirrorOpposite = spacingDrag ? spacingDrag.mirrorOpposite : !!ev.altKey;
|
|
2139
|
+
applySpacingDragValue(dragEl, handle, finalValue, mirrorOpposite);
|
|
2140
|
+
selectedEl = dragEl;
|
|
2141
|
+
spacingDrag = null;
|
|
2142
|
+
var styles = {};
|
|
2143
|
+
styles[handle.property] = finalValue + 'px';
|
|
2144
|
+
if (handle.kind === 'padding' && mirrorOpposite && handle.oppositeProperty) {
|
|
2145
|
+
styles[handle.oppositeProperty] = finalValue + 'px';
|
|
2146
|
+
}
|
|
2147
|
+
postVisualStyleChange(styles);
|
|
2148
|
+
positionOverlay(selectionOverlay, dragEl);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
document.addEventListener(events.move, onMove, true);
|
|
2152
|
+
document.addEventListener(events.up, onUp, true);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1322
2155
|
function postTextContentChange(el, value, html) {
|
|
1323
2156
|
window.parent.postMessage({
|
|
1324
2157
|
type: 'text-content-change',
|
|
@@ -1404,7 +2237,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1404
2237
|
}
|
|
1405
2238
|
|
|
1406
2239
|
function isOverlayElement(el) {
|
|
1407
|
-
|
|
2240
|
+
// Use closest() so that children of overlay elements (e.g. spacing-region
|
|
2241
|
+
// spans inside selectionOverlay that have pointer-events:auto via a CSS rule
|
|
2242
|
+
// and can therefore still be returned by elementFromPoint even when the
|
|
2243
|
+
// parent overlay has pointer-events:none set inline) are also treated as
|
|
2244
|
+
// overlay elements and never used as drag-drop anchor targets.
|
|
2245
|
+
return Boolean(
|
|
2246
|
+
el && el.closest && el.closest('[data-agent-native-edit-overlay]')
|
|
2247
|
+
);
|
|
1408
2248
|
}
|
|
1409
2249
|
|
|
1410
2250
|
function draggableElementChildren(parent) {
|
|
@@ -1418,9 +2258,40 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1418
2258
|
if (el === document.body || el === document.documentElement) return false;
|
|
1419
2259
|
var cs = window.getComputedStyle(el);
|
|
1420
2260
|
if (cs.position === 'absolute' || cs.position === 'fixed') return false;
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
2261
|
+
return true;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// keep in sync with EditPanel.tsx CONTAINER_TAGS/LEAF_TAGS (~line 1679)
|
|
2265
|
+
var BRIDGE_CONTAINER_TAGS = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'form', 'ul', 'ol', 'figure', 'fieldset', 'details', 'dialog', 'blockquote', 'table', 'tbody', 'thead', 'tr'];
|
|
2266
|
+
var BRIDGE_LEAF_TAGS = ['img', 'video', 'picture', 'audio', 'canvas', 'svg', 'path', 'input', 'textarea', 'select', 'br', 'hr', 'iframe'];
|
|
2267
|
+
var BRIDGE_TEXT_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'a', 'strong', 'em', 'label', 'li'];
|
|
2268
|
+
|
|
2269
|
+
function isContainerDropTarget(el) {
|
|
2270
|
+
if (!el || el === document.documentElement) return false;
|
|
2271
|
+
if (isOverlayElement(el) || isLayerInteractionBlocked(el)) return false;
|
|
2272
|
+
if (el === document.body) return true;
|
|
2273
|
+
var tag = (el.tagName || '').toLowerCase();
|
|
2274
|
+
// Reject leaf/text tags — they cannot accept children
|
|
2275
|
+
if (BRIDGE_LEAF_TAGS.indexOf(tag) !== -1 || BRIDGE_TEXT_TAGS.indexOf(tag) !== -1) return false;
|
|
2276
|
+
var cs = window.getComputedStyle(el);
|
|
2277
|
+
if (
|
|
2278
|
+
cs.display === 'flex' ||
|
|
2279
|
+
cs.display === 'inline-flex' ||
|
|
2280
|
+
cs.display === 'grid' ||
|
|
2281
|
+
cs.display === 'inline-grid'
|
|
2282
|
+
) {
|
|
2283
|
+
return true;
|
|
2284
|
+
}
|
|
2285
|
+
return BRIDGE_CONTAINER_TAGS.indexOf(tag) !== -1;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
function edgePlacementForRect(rect, axis, clientX, clientY) {
|
|
2289
|
+
var size = axis === 'x' ? rect.width : rect.height;
|
|
2290
|
+
if (!size) return null;
|
|
2291
|
+
var offset = axis === 'x' ? clientX - rect.left : clientY - rect.top;
|
|
2292
|
+
if (offset < size * 0.22) return 'before';
|
|
2293
|
+
if (offset > size * 0.78) return 'after';
|
|
2294
|
+
return null;
|
|
1424
2295
|
}
|
|
1425
2296
|
|
|
1426
2297
|
function parentFlowAxis(parent) {
|
|
@@ -1445,18 +2316,19 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1445
2316
|
var hit = elementFromEditorPoint(clientX, clientY);
|
|
1446
2317
|
if (
|
|
1447
2318
|
hit &&
|
|
1448
|
-
hit !== document.body &&
|
|
1449
2319
|
hit !== document.documentElement &&
|
|
1450
2320
|
hit !== el &&
|
|
1451
2321
|
!el.contains(hit) &&
|
|
1452
|
-
!hit.contains(el) &&
|
|
1453
2322
|
!isOverlayElement(hit)
|
|
1454
2323
|
) {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
2324
|
+
if (isContainerDropTarget(hit)) {
|
|
2325
|
+
var containerRect = hit.getBoundingClientRect();
|
|
2326
|
+
var edgeAxis = hit.parentElement ? parentFlowAxis(hit.parentElement) : parentFlowAxis(hit);
|
|
2327
|
+
var edgePlacement = edgePlacementForRect(containerRect, edgeAxis, clientX, clientY);
|
|
2328
|
+
if (!edgePlacement) {
|
|
2329
|
+
return { anchor: hit, placement: 'inside', axis: parentFlowAxis(hit) };
|
|
2330
|
+
}
|
|
2331
|
+
return { anchor: hit, placement: edgePlacement, axis: edgeAxis };
|
|
1460
2332
|
}
|
|
1461
2333
|
var hitParent = hit.parentElement;
|
|
1462
2334
|
if (hitParent) {
|
|
@@ -1503,6 +2375,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1503
2375
|
insertionGuide.style.display = 'block';
|
|
1504
2376
|
insertionGuide.style.background = 'var(--design-editor-accent-color)';
|
|
1505
2377
|
insertionGuide.style.border = '0';
|
|
2378
|
+
insertionGuide.style.borderRadius = '999px';
|
|
1506
2379
|
insertionGuide.style.boxShadow = '0 0 0 1px var(--design-editor-accent-color)';
|
|
1507
2380
|
if (target.placement === 'inside') {
|
|
1508
2381
|
insertionGuide.style.left = rect.left + 'px';
|
|
@@ -1511,6 +2384,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1511
2384
|
insertionGuide.style.height = rect.height + 'px';
|
|
1512
2385
|
insertionGuide.style.background = 'color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)';
|
|
1513
2386
|
insertionGuide.style.border = '2px solid var(--design-editor-accent-color)';
|
|
2387
|
+
insertionGuide.style.borderRadius = '4px';
|
|
1514
2388
|
insertionGuide.style.boxShadow = 'none';
|
|
1515
2389
|
return;
|
|
1516
2390
|
}
|
|
@@ -1578,6 +2452,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1578
2452
|
if (isLayerInteractionBlocked(selectedEl)) return;
|
|
1579
2453
|
e.preventDefault();
|
|
1580
2454
|
e.stopPropagation();
|
|
2455
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2456
|
+
var events = dragEventNames(e);
|
|
1581
2457
|
var originalSelectedEl = selectedEl;
|
|
1582
2458
|
var duplicatedForDrag = false;
|
|
1583
2459
|
if (e.altKey && selectedEl !== document.body && selectedEl !== document.documentElement) {
|
|
@@ -1595,16 +2471,96 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1595
2471
|
var reorderEl = selectedEl;
|
|
1596
2472
|
var currentTarget = reorderTargetForPoint(reorderEl, e.clientX, e.clientY);
|
|
1597
2473
|
showInsertionGuideFor(currentTarget);
|
|
2474
|
+
// Cross-screen drag state: true when the pointer is outside this iframe's
|
|
2475
|
+
// viewport bounds. The host frame renders the ghost + highlight and owns
|
|
2476
|
+
// the drop when this is true; the bridge suppresses its in-iframe reorder.
|
|
2477
|
+
var pointerOutsideIframe = false;
|
|
2478
|
+
var reorderSelector = getSelector(reorderEl);
|
|
2479
|
+
var reorderSourceId = getSourceId(reorderEl);
|
|
1598
2480
|
function onReorderMove(ev) {
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
2481
|
+
var vw = window.innerWidth;
|
|
2482
|
+
var vh = window.innerHeight;
|
|
2483
|
+
var cx = ev.clientX;
|
|
2484
|
+
var cy = ev.clientY;
|
|
2485
|
+
var outside = cx < 0 || cy < 0 || cx > vw || cy > vh;
|
|
2486
|
+
pointerOutsideIframe = outside;
|
|
2487
|
+
// Always notify the host frame so it can track the cursor position,
|
|
2488
|
+
// render the ghost, and highlight the target screen.
|
|
2489
|
+
window.parent.postMessage({
|
|
2490
|
+
type: 'agent-native:cross-screen-drag',
|
|
2491
|
+
phase: 'move',
|
|
2492
|
+
selector: reorderSelector,
|
|
2493
|
+
sourceId: reorderSourceId,
|
|
2494
|
+
iframeX: cx,
|
|
2495
|
+
iframeY: cy,
|
|
2496
|
+
viewportW: vw,
|
|
2497
|
+
viewportH: vh,
|
|
2498
|
+
}, '*');
|
|
2499
|
+
if (outside) {
|
|
2500
|
+
// Cursor left this iframe — hide the in-iframe insertion guide so
|
|
2501
|
+
// it does not render while the host shows a cross-screen drop target.
|
|
2502
|
+
hideInsertionGuide();
|
|
2503
|
+
showTransformBadge('Move layer', cx, cy);
|
|
2504
|
+
} else {
|
|
2505
|
+
// Cursor is inside this iframe — use existing in-iframe behavior.
|
|
2506
|
+
currentTarget = reorderTargetForPoint(reorderEl, cx, cy);
|
|
2507
|
+
showInsertionGuideFor(currentTarget);
|
|
2508
|
+
showTransformBadge(currentTarget ? 'Move layer' : 'Move', cx, cy);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
function onReorderEscape() {
|
|
2512
|
+
document.removeEventListener(events.move, onReorderMove, true);
|
|
2513
|
+
document.removeEventListener(events.up, onReorderUp, true);
|
|
2514
|
+
document.removeEventListener('keydown', onReorderKeyDown, true);
|
|
2515
|
+
hideTransformBadge();
|
|
2516
|
+
hideInsertionGuide();
|
|
2517
|
+
window.parent.postMessage({ type: 'agent-native:cross-screen-drag', phase: 'cancel' }, '*');
|
|
2518
|
+
// Revert any clone that was inserted for alt-drag.
|
|
2519
|
+
if (duplicatedForDrag && reorderEl && reorderEl !== originalSelectedEl) {
|
|
2520
|
+
if (reorderEl.parentElement) reorderEl.parentElement.removeChild(reorderEl);
|
|
2521
|
+
selectedEl = originalSelectedEl;
|
|
2522
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2523
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function onReorderKeyDown(ev) {
|
|
2527
|
+
if (ev.key === 'Escape') {
|
|
2528
|
+
ev.preventDefault();
|
|
2529
|
+
ev.stopPropagation();
|
|
2530
|
+
if (ev.stopImmediatePropagation) ev.stopImmediatePropagation();
|
|
2531
|
+
onReorderEscape();
|
|
2532
|
+
}
|
|
1602
2533
|
}
|
|
1603
|
-
function onReorderUp() {
|
|
1604
|
-
document.removeEventListener(
|
|
1605
|
-
document.removeEventListener(
|
|
2534
|
+
function onReorderUp(ev) {
|
|
2535
|
+
document.removeEventListener(events.move, onReorderMove, true);
|
|
2536
|
+
document.removeEventListener(events.up, onReorderUp, true);
|
|
2537
|
+
document.removeEventListener('keydown', onReorderKeyDown, true);
|
|
1606
2538
|
hideTransformBadge();
|
|
1607
2539
|
hideInsertionGuide();
|
|
2540
|
+
var vw = window.innerWidth;
|
|
2541
|
+
var vh = window.innerHeight;
|
|
2542
|
+
var cx = ev ? ev.clientX : 0;
|
|
2543
|
+
var cy = ev ? ev.clientY : 0;
|
|
2544
|
+
var outsideOnDrop = cx < 0 || cy < 0 || cx > vw || cy > vh;
|
|
2545
|
+
// Post the end message so the host can finalize a cross-screen drop.
|
|
2546
|
+
window.parent.postMessage({
|
|
2547
|
+
type: 'agent-native:cross-screen-drag',
|
|
2548
|
+
phase: 'end',
|
|
2549
|
+
selector: reorderSelector,
|
|
2550
|
+
sourceId: reorderSourceId,
|
|
2551
|
+
iframeX: cx,
|
|
2552
|
+
iframeY: cy,
|
|
2553
|
+
viewportW: vw,
|
|
2554
|
+
viewportH: vh,
|
|
2555
|
+
}, '*');
|
|
2556
|
+
// When the pointer is outside this iframe at release, the host owns the
|
|
2557
|
+
// move (cross-screen drop). Do NOT apply the in-iframe reorder so we
|
|
2558
|
+
// avoid a ghost element left in screen A's DOM.
|
|
2559
|
+
// Use outsideOnDrop only — pointerOutsideIframe is stale when the user
|
|
2560
|
+
// briefly exits the iframe and re-enters before releasing. The host
|
|
2561
|
+
// already clears cross-screen state on re-entry so checking the
|
|
2562
|
+
// momentary excursion flag here would wrongly drop the element nowhere.
|
|
2563
|
+
if (outsideOnDrop) return;
|
|
1608
2564
|
if (!currentTarget) {
|
|
1609
2565
|
// No valid drop target — clean up the clone if one was inserted so
|
|
1610
2566
|
// no ghost element is left in the DOM.
|
|
@@ -1630,9 +2586,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1630
2586
|
applyRuntimeReorder(reorderEl, currentTarget);
|
|
1631
2587
|
postVisualStructureChange(reorderEl, currentTarget, { prevParent: prevParent, prevNextSibling: prevNextSibling });
|
|
1632
2588
|
}
|
|
1633
|
-
|
|
1634
|
-
document.addEventListener(
|
|
1635
|
-
document.addEventListener(
|
|
2589
|
+
}
|
|
2590
|
+
document.addEventListener(events.move, onReorderMove, true);
|
|
2591
|
+
document.addEventListener(events.up, onReorderUp, true);
|
|
2592
|
+
document.addEventListener('keydown', onReorderKeyDown, true);
|
|
1636
2593
|
return;
|
|
1637
2594
|
}
|
|
1638
2595
|
ensurePositionable(selectedEl);
|
|
@@ -1659,8 +2616,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1659
2616
|
refreshOverlays();
|
|
1660
2617
|
}
|
|
1661
2618
|
function onUp() {
|
|
1662
|
-
document.removeEventListener(
|
|
1663
|
-
document.removeEventListener(
|
|
2619
|
+
document.removeEventListener(events.move, onMove, true);
|
|
2620
|
+
document.removeEventListener(events.up, onUp, true);
|
|
1664
2621
|
hideTransformBadge();
|
|
1665
2622
|
if (!dragEl) return;
|
|
1666
2623
|
if (duplicatedForDrag && !moved) {
|
|
@@ -1686,8 +2643,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1686
2643
|
}, '*');
|
|
1687
2644
|
}
|
|
1688
2645
|
}
|
|
1689
|
-
document.addEventListener(
|
|
1690
|
-
document.addEventListener(
|
|
2646
|
+
document.addEventListener(events.move, onMove, true);
|
|
2647
|
+
document.addEventListener(events.up, onUp, true);
|
|
1691
2648
|
}
|
|
1692
2649
|
|
|
1693
2650
|
function startResize(handle, e) {
|
|
@@ -1864,7 +2821,68 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1864
2821
|
document.addEventListener('mouseup', onUp, true);
|
|
1865
2822
|
}
|
|
1866
2823
|
|
|
2824
|
+
function clearPendingShieldDrag() {
|
|
2825
|
+
if (!pendingShieldDrag) return;
|
|
2826
|
+
document.removeEventListener(pendingShieldDrag.move, pendingShieldDrag.onMove, true);
|
|
2827
|
+
document.removeEventListener(pendingShieldDrag.up, pendingShieldDrag.onUp, true);
|
|
2828
|
+
pendingShieldDrag = null;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
function beginPotentialShieldDrag(e) {
|
|
2832
|
+
stopNativeInteraction(e);
|
|
2833
|
+
if (e.button !== 0 || activeTextEditEl) return;
|
|
2834
|
+
var events = dragEventNames(e);
|
|
2835
|
+
var hit = elementFromEditorPoint(e.clientX, e.clientY);
|
|
2836
|
+
if (!hit || hit === document.body || hit === document.documentElement) return;
|
|
2837
|
+
var dragTarget =
|
|
2838
|
+
selectedEl &&
|
|
2839
|
+
document.documentElement.contains(selectedEl) &&
|
|
2840
|
+
selectedEl.contains(hit)
|
|
2841
|
+
? selectedEl
|
|
2842
|
+
: selectionTargetForHit(hit);
|
|
2843
|
+
if (
|
|
2844
|
+
!dragTarget ||
|
|
2845
|
+
dragTarget === document.body ||
|
|
2846
|
+
dragTarget === document.documentElement ||
|
|
2847
|
+
isLayerInteractionBlocked(dragTarget)
|
|
2848
|
+
) {
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
var startX = e.clientX;
|
|
2852
|
+
var startY = e.clientY;
|
|
2853
|
+
var didStartDrag = false;
|
|
2854
|
+
function selectDragTarget() {
|
|
2855
|
+
selectedEl = dragTarget;
|
|
2856
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2857
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
2858
|
+
}
|
|
2859
|
+
function onMove(ev) {
|
|
2860
|
+
if (Math.hypot(ev.clientX - startX, ev.clientY - startY) <= 3) return;
|
|
2861
|
+
clearPendingShieldDrag();
|
|
2862
|
+
didStartDrag = true;
|
|
2863
|
+
selectDragTarget();
|
|
2864
|
+
suppressNextShieldClickBriefly();
|
|
2865
|
+
startMove(ev);
|
|
2866
|
+
}
|
|
2867
|
+
function onUp(ev) {
|
|
2868
|
+
clearPendingShieldDrag();
|
|
2869
|
+
if (didStartDrag) return;
|
|
2870
|
+
if (ev) stopNativeInteraction(ev);
|
|
2871
|
+
selectDragTarget();
|
|
2872
|
+
suppressNextShieldClickBriefly();
|
|
2873
|
+
}
|
|
2874
|
+
clearPendingShieldDrag();
|
|
2875
|
+
pendingShieldDrag = { move: events.move, up: events.up, onMove: onMove, onUp: onUp };
|
|
2876
|
+
document.addEventListener(events.move, onMove, true);
|
|
2877
|
+
document.addEventListener(events.up, onUp, true);
|
|
2878
|
+
}
|
|
2879
|
+
|
|
1867
2880
|
selectionOverlay.addEventListener('mousedown', function(e) {
|
|
2881
|
+
var spacingKey = e.target && e.target.getAttribute && e.target.getAttribute('data-spacing-key');
|
|
2882
|
+
if (spacingKey) {
|
|
2883
|
+
startSpacingDrag(spacingKey, e);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
1868
2886
|
var resizeHandle = e.target && e.target.getAttribute && e.target.getAttribute('data-agent-native-edit-handle');
|
|
1869
2887
|
if (!resizeHandle && e.target && e.target.getAttribute) {
|
|
1870
2888
|
resizeHandle = e.target.getAttribute('data-agent-native-edge-handle');
|
|
@@ -1881,6 +2899,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1881
2899
|
startMove(e);
|
|
1882
2900
|
}, true);
|
|
1883
2901
|
|
|
2902
|
+
shieldOverlay.addEventListener('pointerdown', beginPotentialShieldDrag, true);
|
|
2903
|
+
|
|
1884
2904
|
['pointerdown','pointerup','mousedown','mouseup','auxclick'].forEach(function(type) {
|
|
1885
2905
|
shieldOverlay.addEventListener(type, stopNativeInteraction, true);
|
|
1886
2906
|
});
|
|
@@ -2050,10 +3070,25 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2050
3070
|
hoveredEl = elementFromEditorPoint(e.clientX, e.clientY);
|
|
2051
3071
|
if (!hoveredEl) {
|
|
2052
3072
|
highlightOverlay.style.display = 'none';
|
|
3073
|
+
if (!spacingDrag) {
|
|
3074
|
+
selectedSpacingHovered = false;
|
|
3075
|
+
hoveredSpacingHandleKey = '';
|
|
3076
|
+
updateSpacingOverlay(selectedEl);
|
|
3077
|
+
}
|
|
2053
3078
|
hideMeasurements();
|
|
2054
3079
|
return;
|
|
2055
3080
|
}
|
|
2056
3081
|
if (hoveredEl && hoveredEl.closest('[data-agent-native-text-editing]')) return;
|
|
3082
|
+
if (!spacingDrag) {
|
|
3083
|
+
selectedSpacingHovered = Boolean(
|
|
3084
|
+
selectedEl &&
|
|
3085
|
+
hoveredEl &&
|
|
3086
|
+
(hoveredEl === selectedEl ||
|
|
3087
|
+
(selectedEl.contains && selectedEl.contains(hoveredEl))),
|
|
3088
|
+
);
|
|
3089
|
+
if (!selectedSpacingHovered) hoveredSpacingHandleKey = '';
|
|
3090
|
+
updateSpacingOverlay(selectedEl);
|
|
3091
|
+
}
|
|
2057
3092
|
if (hoveredEl === selectedEl) {
|
|
2058
3093
|
highlightOverlay.style.display = 'none';
|
|
2059
3094
|
} else {
|
|
@@ -2071,6 +3106,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2071
3106
|
shieldOverlay.addEventListener('pointerleave', function(e) {
|
|
2072
3107
|
stopNativeInteraction(e);
|
|
2073
3108
|
hoveredEl = null;
|
|
3109
|
+
if (!spacingDrag) {
|
|
3110
|
+
selectedSpacingHovered = false;
|
|
3111
|
+
hoveredSpacingHandleKey = '';
|
|
3112
|
+
updateSpacingOverlay(selectedEl);
|
|
3113
|
+
}
|
|
2074
3114
|
highlightOverlay.style.display = 'none';
|
|
2075
3115
|
hideMeasurements();
|
|
2076
3116
|
window.parent.postMessage({ type: 'element-hover', payload: null }, '*');
|
|
@@ -2130,6 +3170,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2130
3170
|
} catch (_err) {}
|
|
2131
3171
|
}
|
|
2132
3172
|
if (!target) return;
|
|
3173
|
+
selectedSpacingHovered = false;
|
|
3174
|
+
hoveredSpacingHandleKey = '';
|
|
2133
3175
|
selectedEl = target;
|
|
2134
3176
|
positionOverlay(selectionOverlay, target);
|
|
2135
3177
|
if (hoveredEl === selectedEl) highlightOverlay.style.display = 'none';
|
|
@@ -2230,6 +3272,41 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2230
3272
|
interface DesignCanvasProps {
|
|
2231
3273
|
content: string;
|
|
2232
3274
|
contentKey?: string;
|
|
3275
|
+
/**
|
|
3276
|
+
* The runtime source tier for this canvas.
|
|
3277
|
+
*
|
|
3278
|
+
* - `"inline"` (default) — HTML/Alpine `srcdoc` iframe; same-origin null
|
|
3279
|
+
* origin; all bridge scripts injected by DesignCanvas.
|
|
3280
|
+
* - `"localhost"` — `src=devServerUrl`; dev server is same-origin in most
|
|
3281
|
+
* setups; bridge trust: origin must match parent or be "null".
|
|
3282
|
+
* - `"fusion"` — `src=builderHostedUrl`; cross-origin Builder-hosted app;
|
|
3283
|
+
* bridge trust is relaxed to window-identity only (no origin check) so
|
|
3284
|
+
* the Builder-hosted iframe can communicate with the editor. The sandbox
|
|
3285
|
+
* grants `allow-same-origin` so the Builder app can reach its own resources.
|
|
3286
|
+
*
|
|
3287
|
+
* When omitted, DesignCanvas infers the tier from the content value:
|
|
3288
|
+
* a value that passes `getExternalPreviewUrl` is treated as `"localhost"`;
|
|
3289
|
+
* otherwise `"inline"`. Pass `sourceType="fusion"` explicitly when the
|
|
3290
|
+
* content URL is a Builder-hosted (cross-origin) app so the bridge security
|
|
3291
|
+
* model uses window-identity trust instead of same-origin trust.
|
|
3292
|
+
*/
|
|
3293
|
+
sourceType?: "inline" | "localhost" | "fusion";
|
|
3294
|
+
/**
|
|
3295
|
+
* Explicit Builder-hosted app URL for fusion source rendering.
|
|
3296
|
+
*
|
|
3297
|
+
* When `sourceType === "fusion"` and this prop is provided, the iframe uses
|
|
3298
|
+
* this URL as `src` regardless of what `content` contains. This lets the
|
|
3299
|
+
* caller hold the original inline HTML in `content` (for collab/history
|
|
3300
|
+
* purposes) while pointing the canvas at the migrated Builder-hosted app.
|
|
3301
|
+
*
|
|
3302
|
+
* When absent and `sourceType === "fusion"`, the component falls back to
|
|
3303
|
+
* the existing external-URL detection on `content` (i.e. if `content` is
|
|
3304
|
+
* itself a URL it is used as-is, which is the pattern when the branch URL
|
|
3305
|
+
* has been written into the design file content).
|
|
3306
|
+
*
|
|
3307
|
+
* For `"inline"` and `"localhost"` sources this prop is ignored.
|
|
3308
|
+
*/
|
|
3309
|
+
fusionUrl?: string;
|
|
2233
3310
|
zoom: number;
|
|
2234
3311
|
onZoomChange?: (zoom: number) => void;
|
|
2235
3312
|
deviceFrame: DeviceFrameType;
|
|
@@ -2322,6 +3399,55 @@ interface DesignCanvasProps {
|
|
|
2322
3399
|
* opened in a new tab by the iframe itself and never reach this callback.
|
|
2323
3400
|
*/
|
|
2324
3401
|
onPrototypeNavigate?: (screen: string, href: string) => void;
|
|
3402
|
+
/**
|
|
3403
|
+
* Motion tracks to load into the iframe's motion-preview bridge. Sent via
|
|
3404
|
+
* `motion-load-tracks` whenever this prop changes. When cleared
|
|
3405
|
+
* (`undefined` or `[]`) a `motion-preview-clear` message is sent to remove
|
|
3406
|
+
* any applied preview overrides.
|
|
3407
|
+
*
|
|
3408
|
+
* The MotionDock sends scrub ticks as `{ type: 'motion-preview', t,
|
|
3409
|
+
* durationMs }` directly from its `canvasIframeRef`. DesignCanvas only
|
|
3410
|
+
* needs the tracks so the bridge can interpolate values at each tick.
|
|
3411
|
+
*/
|
|
3412
|
+
motionTracks?: MotionTrackWire[];
|
|
3413
|
+
/**
|
|
3414
|
+
* Explicit iframe width in pixels. When provided it overrides the width
|
|
3415
|
+
* derived from `deviceFrame`, enabling per-breakpoint preview (e.g. Mobile
|
|
3416
|
+
* 390 / Tablet 768 / Desktop 1280 side-by-side frames in the overview).
|
|
3417
|
+
* The height still comes from `deviceFrame`; `deviceFrame="none"` keeps
|
|
3418
|
+
* 100% height.
|
|
3419
|
+
*/
|
|
3420
|
+
previewWidthPx?: number;
|
|
3421
|
+
/**
|
|
3422
|
+
* Shader-fill CSS preview to apply to a selected element inside the iframe.
|
|
3423
|
+
*
|
|
3424
|
+
* When set, the canvas sends a `shader-fill-preview` bridge message that
|
|
3425
|
+
* applies the CSS `background` value on the target element **without
|
|
3426
|
+
* persisting anything**. When cleared (`null` / `undefined`) a
|
|
3427
|
+
* `shader-fill-preview-clear` message is sent to restore the original
|
|
3428
|
+
* background.
|
|
3429
|
+
*
|
|
3430
|
+
* Preview-only — never writes to DB, Yjs, or source. Part of the §6.7
|
|
3431
|
+
* shader-fill PREVIEW path; the apply path remains gated until runtime
|
|
3432
|
+
* rendering + source-write + diff proof are all in place.
|
|
3433
|
+
*/
|
|
3434
|
+
shaderFillPreview?: {
|
|
3435
|
+
/** CSS selector for the target element (preferred over nodeId). */
|
|
3436
|
+
selector?: string;
|
|
3437
|
+
/** data-agent-native-node-id value for the target element. */
|
|
3438
|
+
nodeId?: string;
|
|
3439
|
+
/** The CSS `background` value returned by preview-shader-fill. */
|
|
3440
|
+
css: string;
|
|
3441
|
+
} | null;
|
|
3442
|
+
/**
|
|
3443
|
+
* Called when the user clicks the component-instance source tag (the
|
|
3444
|
+
* "ComponentName →" pill that floats above a selected component root).
|
|
3445
|
+
* The parent should invoke `open-component-source` with these params.
|
|
3446
|
+
*/
|
|
3447
|
+
onComponentSourceJump?: (params: {
|
|
3448
|
+
nodeId: string;
|
|
3449
|
+
componentName: string;
|
|
3450
|
+
}) => void;
|
|
2325
3451
|
}
|
|
2326
3452
|
|
|
2327
3453
|
function getExternalPreviewUrl(content: string): string | null {
|
|
@@ -2357,6 +3483,8 @@ export interface IframeContextMenuPayload {
|
|
|
2357
3483
|
export function DesignCanvas({
|
|
2358
3484
|
content,
|
|
2359
3485
|
contentKey,
|
|
3486
|
+
sourceType,
|
|
3487
|
+
fusionUrl,
|
|
2360
3488
|
zoom,
|
|
2361
3489
|
onZoomChange,
|
|
2362
3490
|
deviceFrame,
|
|
@@ -2396,6 +3524,10 @@ export function DesignCanvas({
|
|
|
2396
3524
|
commentContextId,
|
|
2397
3525
|
commentContextLabel,
|
|
2398
3526
|
onPrototypeNavigate,
|
|
3527
|
+
motionTracks,
|
|
3528
|
+
previewWidthPx,
|
|
3529
|
+
onComponentSourceJump,
|
|
3530
|
+
shaderFillPreview,
|
|
2399
3531
|
}: DesignCanvasProps) {
|
|
2400
3532
|
const t = useT();
|
|
2401
3533
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
@@ -2406,10 +3538,25 @@ export function DesignCanvas({
|
|
|
2406
3538
|
const [annotationPins, setAnnotationPins] = useState<CanvasPin[]>([]);
|
|
2407
3539
|
const [pinSubmitSignal, setPinSubmitSignal] = useState(0);
|
|
2408
3540
|
const isEmbeddedFrame = Boolean(embeddedFrame);
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
)
|
|
3541
|
+
// Resolve the URL to render in the iframe:
|
|
3542
|
+
// 1. When sourceType === "fusion" and fusionUrl is set, prefer the explicit
|
|
3543
|
+
// Builder-hosted URL over whatever is in `content` (which may still be the
|
|
3544
|
+
// original inline HTML).
|
|
3545
|
+
// 2. Otherwise fall back to the content-based URL detection (handles the case
|
|
3546
|
+
// where the branch URL has been written into the design file content, or
|
|
3547
|
+
// where the localhost URL is the file content).
|
|
3548
|
+
const externalPreviewUrl = useMemo(() => {
|
|
3549
|
+
if (sourceType === "fusion" && fusionUrl) {
|
|
3550
|
+
try {
|
|
3551
|
+
const url = new URL(fusionUrl);
|
|
3552
|
+
url.hash = "";
|
|
3553
|
+
return url.toString();
|
|
3554
|
+
} catch {
|
|
3555
|
+
// fall through to content detection below
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
return getExternalPreviewUrl(renderedContent);
|
|
3559
|
+
}, [fusionUrl, renderedContent, sourceType]);
|
|
2413
3560
|
zoomRef.current = zoom;
|
|
2414
3561
|
|
|
2415
3562
|
const queuedAnnotationPins = useMemo(
|
|
@@ -2462,6 +3609,8 @@ export function DesignCanvas({
|
|
|
2462
3609
|
isEmbeddedFrame ? "true" : "false",
|
|
2463
3610
|
);
|
|
2464
3611
|
const bridgeToInject =
|
|
3612
|
+
MOTION_PREVIEW_BRIDGE_SCRIPT +
|
|
3613
|
+
SHADER_FILL_PREVIEW_BRIDGE_SCRIPT +
|
|
2465
3614
|
TWEAK_BRIDGE_SCRIPT +
|
|
2466
3615
|
ZOOM_BRIDGE_SCRIPT +
|
|
2467
3616
|
NAV_BRIDGE_SCRIPT +
|
|
@@ -2492,14 +3641,27 @@ export function DesignCanvas({
|
|
|
2492
3641
|
// Listen for messages from the iframe
|
|
2493
3642
|
useEffect(() => {
|
|
2494
3643
|
function handleMessage(e: MessageEvent) {
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
3644
|
+
const iframeWindow = iframeRef.current?.contentWindow;
|
|
3645
|
+
// For fusion sources the Builder-hosted app is cross-origin, so the strict
|
|
3646
|
+
// `origin === parentOrigin` check can never match. We still require window
|
|
3647
|
+
// identity (the message must come from our own iframe window, not any
|
|
3648
|
+
// arbitrary cross-origin frame), AND we validate the message origin
|
|
3649
|
+
// against a Builder-host allowlist (the configured fusionUrl origin or the
|
|
3650
|
+
// *.builder.io family) before relaxing the origin check. If the origin is
|
|
3651
|
+
// not on the allowlist we keep the strict check so a hostile frame that
|
|
3652
|
+
// somehow shares our window reference still can't be trusted.
|
|
3653
|
+
const trusted =
|
|
3654
|
+
sourceType === "fusion"
|
|
3655
|
+
? iframeWindow != null &&
|
|
3656
|
+
e.source === iframeWindow &&
|
|
3657
|
+
isAllowedFusionOrigin(e.origin, fusionUrl)
|
|
3658
|
+
: isTrustedCanvasBridgeMessage({
|
|
3659
|
+
source: e.source,
|
|
3660
|
+
origin: e.origin,
|
|
3661
|
+
iframeWindow,
|
|
3662
|
+
parentOrigin: window.location.origin,
|
|
3663
|
+
});
|
|
3664
|
+
if (!trusted) {
|
|
2503
3665
|
return;
|
|
2504
3666
|
}
|
|
2505
3667
|
if (!e.data || !e.data.type) return;
|
|
@@ -2540,9 +3702,15 @@ export function DesignCanvas({
|
|
|
2540
3702
|
const placement = String(e.data.placement || "after");
|
|
2541
3703
|
const requestId =
|
|
2542
3704
|
typeof e.data.requestId === "string" ? e.data.requestId : undefined;
|
|
3705
|
+
const sourceId =
|
|
3706
|
+
typeof e.data.sourceId === "string" ? e.data.sourceId : undefined;
|
|
3707
|
+
const anchorSourceId =
|
|
3708
|
+
typeof e.data.anchorSourceId === "string"
|
|
3709
|
+
? e.data.anchorSourceId
|
|
3710
|
+
: undefined;
|
|
2543
3711
|
if (
|
|
2544
|
-
selector &&
|
|
2545
|
-
anchorSelector &&
|
|
3712
|
+
(selector || sourceId) &&
|
|
3713
|
+
(anchorSelector || anchorSourceId) &&
|
|
2546
3714
|
(placement === "before" ||
|
|
2547
3715
|
placement === "after" ||
|
|
2548
3716
|
placement === "inside")
|
|
@@ -2554,14 +3722,8 @@ export function DesignCanvas({
|
|
|
2554
3722
|
e.data.payload,
|
|
2555
3723
|
{
|
|
2556
3724
|
requestId,
|
|
2557
|
-
sourceId
|
|
2558
|
-
|
|
2559
|
-
? e.data.sourceId
|
|
2560
|
-
: undefined,
|
|
2561
|
-
anchorSourceId:
|
|
2562
|
-
typeof e.data.anchorSourceId === "string"
|
|
2563
|
-
? e.data.anchorSourceId
|
|
2564
|
-
: undefined,
|
|
3725
|
+
sourceId,
|
|
3726
|
+
anchorSourceId,
|
|
2565
3727
|
},
|
|
2566
3728
|
);
|
|
2567
3729
|
if (requestId) {
|
|
@@ -2663,6 +3825,16 @@ export function DesignCanvas({
|
|
|
2663
3825
|
);
|
|
2664
3826
|
return;
|
|
2665
3827
|
}
|
|
3828
|
+
if (e.data.type === "component-source-jump") {
|
|
3829
|
+
// The user clicked the component-instance tag ("ComponentName →").
|
|
3830
|
+
// Relay to the parent so it can invoke open-component-source.
|
|
3831
|
+
const nodeId = String(e.data.nodeId || "");
|
|
3832
|
+
const componentName = String(e.data.componentName || "");
|
|
3833
|
+
if (nodeId && componentName) {
|
|
3834
|
+
onComponentSourceJump?.({ nodeId, componentName });
|
|
3835
|
+
}
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
2666
3838
|
if (e.data.type === "embedded-canvas-wheel") {
|
|
2667
3839
|
if (!isEmbeddedFrame) return;
|
|
2668
3840
|
const iframe = iframeRef.current;
|
|
@@ -2748,7 +3920,10 @@ export function DesignCanvas({
|
|
|
2748
3920
|
onZoomChange,
|
|
2749
3921
|
deviceFrame,
|
|
2750
3922
|
onPrototypeNavigate,
|
|
3923
|
+
onComponentSourceJump,
|
|
2751
3924
|
isEmbeddedFrame,
|
|
3925
|
+
sourceType,
|
|
3926
|
+
fusionUrl,
|
|
2752
3927
|
]);
|
|
2753
3928
|
|
|
2754
3929
|
const replayIframeEditorState = useCallback(() => {
|
|
@@ -2786,14 +3961,43 @@ export function DesignCanvas({
|
|
|
2786
3961
|
: { type: "hover-element", selector: "", selectorCandidates: [] },
|
|
2787
3962
|
"*",
|
|
2788
3963
|
);
|
|
3964
|
+
// Re-send motion tracks so the preview bridge is ready after a reload.
|
|
3965
|
+
if (motionTracks && motionTracks.length > 0) {
|
|
3966
|
+
iframe.contentWindow?.postMessage(
|
|
3967
|
+
{ type: "motion-load-tracks", tracks: motionTracks },
|
|
3968
|
+
"*",
|
|
3969
|
+
);
|
|
3970
|
+
} else {
|
|
3971
|
+
iframe.contentWindow?.postMessage({ type: "motion-preview-clear" }, "*");
|
|
3972
|
+
}
|
|
3973
|
+
// Re-apply the shader-fill preview after a reload so the preview survives
|
|
3974
|
+
// screen switches. Preview-only — never writes to DB, Yjs, or source.
|
|
3975
|
+
if (shaderFillPreview) {
|
|
3976
|
+
iframe.contentWindow?.postMessage(
|
|
3977
|
+
{
|
|
3978
|
+
type: "shader-fill-preview",
|
|
3979
|
+
selector: shaderFillPreview.selector ?? "",
|
|
3980
|
+
nodeId: shaderFillPreview.nodeId ?? "",
|
|
3981
|
+
css: shaderFillPreview.css,
|
|
3982
|
+
},
|
|
3983
|
+
"*",
|
|
3984
|
+
);
|
|
3985
|
+
} else {
|
|
3986
|
+
iframe.contentWindow?.postMessage(
|
|
3987
|
+
{ type: "shader-fill-preview-clear" },
|
|
3988
|
+
"*",
|
|
3989
|
+
);
|
|
3990
|
+
}
|
|
2789
3991
|
}, [
|
|
2790
3992
|
hoveredSelector,
|
|
2791
3993
|
hoveredSelectorCandidates,
|
|
2792
3994
|
hiddenSelectors,
|
|
2793
3995
|
lockedSelectors,
|
|
3996
|
+
motionTracks,
|
|
2794
3997
|
scaleMode,
|
|
2795
3998
|
selectedSelector,
|
|
2796
3999
|
selectedSelectorCandidates,
|
|
4000
|
+
shaderFillPreview,
|
|
2797
4001
|
tweakValues,
|
|
2798
4002
|
]);
|
|
2799
4003
|
|
|
@@ -2816,6 +4020,44 @@ export function DesignCanvas({
|
|
|
2816
4020
|
);
|
|
2817
4021
|
}, [clearSelectionRequest]);
|
|
2818
4022
|
|
|
4023
|
+
// Sync motion tracks to the iframe bridge whenever they change.
|
|
4024
|
+
// When motionTracks is empty/undefined, clear any preview overrides so the
|
|
4025
|
+
// design returns to its authored state (no stale inline styles).
|
|
4026
|
+
useEffect(() => {
|
|
4027
|
+
const win = iframeRef.current?.contentWindow;
|
|
4028
|
+
if (!win) return;
|
|
4029
|
+
if (!motionTracks || motionTracks.length === 0) {
|
|
4030
|
+
win.postMessage({ type: "motion-preview-clear" }, "*");
|
|
4031
|
+
} else {
|
|
4032
|
+
win.postMessage(
|
|
4033
|
+
{ type: "motion-load-tracks", tracks: motionTracks },
|
|
4034
|
+
"*",
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
}, [motionTracks]);
|
|
4038
|
+
|
|
4039
|
+
// Sync shader-fill preview to the iframe whenever the prop changes.
|
|
4040
|
+
// When cleared (null / undefined) send a clear message so the bridge
|
|
4041
|
+
// restores the original background on the previously-patched element.
|
|
4042
|
+
// Preview-only — never writes to DB, Yjs, or source.
|
|
4043
|
+
useEffect(() => {
|
|
4044
|
+
const win = iframeRef.current?.contentWindow;
|
|
4045
|
+
if (!win) return;
|
|
4046
|
+
if (!shaderFillPreview) {
|
|
4047
|
+
win.postMessage({ type: "shader-fill-preview-clear" }, "*");
|
|
4048
|
+
} else {
|
|
4049
|
+
win.postMessage(
|
|
4050
|
+
{
|
|
4051
|
+
type: "shader-fill-preview",
|
|
4052
|
+
selector: shaderFillPreview.selector ?? "",
|
|
4053
|
+
nodeId: shaderFillPreview.nodeId ?? "",
|
|
4054
|
+
css: shaderFillPreview.css,
|
|
4055
|
+
},
|
|
4056
|
+
"*",
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
4059
|
+
}, [shaderFillPreview]);
|
|
4060
|
+
|
|
2819
4061
|
// Push the constant-size chrome scale into the iframe LIVE (CSS vars only) when
|
|
2820
4062
|
// overview zoom settles. This is intentionally separate from the srcdoc build so
|
|
2821
4063
|
// a scale change never rebuilds srcdoc / reloads the iframe (which flashes the
|
|
@@ -2843,6 +4085,61 @@ export function DesignCanvas({
|
|
|
2843
4085
|
[],
|
|
2844
4086
|
);
|
|
2845
4087
|
|
|
4088
|
+
/**
|
|
4089
|
+
* Send a motion-preview scrub tick to the iframe. `t` is the normalised
|
|
4090
|
+
* playhead position in [0, 1]. Tracks must have been loaded first via the
|
|
4091
|
+
* `motionTracks` prop (or an explicit `motion-load-tracks` message).
|
|
4092
|
+
* Preview-only — never writes to DB/Yjs/source.
|
|
4093
|
+
*/
|
|
4094
|
+
const sendMotionPreview = useCallback((t: number, durationMs?: number) => {
|
|
4095
|
+
const iframe = iframeRef.current;
|
|
4096
|
+
if (!iframe?.contentWindow) return;
|
|
4097
|
+
iframe.contentWindow.postMessage(
|
|
4098
|
+
{ type: "motion-preview", t: Math.max(0, Math.min(1, t)), durationMs },
|
|
4099
|
+
"*",
|
|
4100
|
+
);
|
|
4101
|
+
}, []);
|
|
4102
|
+
|
|
4103
|
+
/**
|
|
4104
|
+
* Clear all motion-preview inline-style overrides in the iframe and remove
|
|
4105
|
+
* the in-memory track list. Call when the Motion dock is closed.
|
|
4106
|
+
*/
|
|
4107
|
+
const clearMotionPreview = useCallback(() => {
|
|
4108
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
4109
|
+
{ type: "motion-preview-clear" },
|
|
4110
|
+
"*",
|
|
4111
|
+
);
|
|
4112
|
+
}, []);
|
|
4113
|
+
|
|
4114
|
+
/**
|
|
4115
|
+
* Send a shader-fill CSS preview to the iframe. Targets the element
|
|
4116
|
+
* identified by `selector` (preferred) or `nodeId`. Preview-only — the
|
|
4117
|
+
* bridge script restores the original background on clear.
|
|
4118
|
+
*/
|
|
4119
|
+
const sendShaderFillPreview = useCallback(
|
|
4120
|
+
(selector: string, nodeId: string, css: string) => {
|
|
4121
|
+
const win = iframeRef.current?.contentWindow;
|
|
4122
|
+
if (!win) return;
|
|
4123
|
+
win.postMessage(
|
|
4124
|
+
{ type: "shader-fill-preview", selector, nodeId, css },
|
|
4125
|
+
"*",
|
|
4126
|
+
);
|
|
4127
|
+
},
|
|
4128
|
+
[],
|
|
4129
|
+
);
|
|
4130
|
+
|
|
4131
|
+
/**
|
|
4132
|
+
* Clear the shader-fill preview in the iframe, restoring the original
|
|
4133
|
+
* background on the patched element. Call when the preview is dismissed
|
|
4134
|
+
* or when the selection changes to a different element.
|
|
4135
|
+
*/
|
|
4136
|
+
const clearShaderFillPreview = useCallback(() => {
|
|
4137
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
4138
|
+
{ type: "shader-fill-preview-clear" },
|
|
4139
|
+
"*",
|
|
4140
|
+
);
|
|
4141
|
+
}, []);
|
|
4142
|
+
|
|
2846
4143
|
const replacePreviewContent = useCallback(
|
|
2847
4144
|
(nextContent: string, selector?: string | null, candidates?: string[]) => {
|
|
2848
4145
|
const iframe = iframeRef.current;
|
|
@@ -2884,7 +4181,15 @@ export function DesignCanvas({
|
|
|
2884
4181
|
(window as any).__designCanvasSendStyle = sendStyleChange;
|
|
2885
4182
|
(window as any).__designCanvasReplaceContent = replacePreviewContent;
|
|
2886
4183
|
(window as any).__designCanvasDeleteElement = deleteRuntimeElement;
|
|
4184
|
+
(window as any).__designCanvasSendMotionPreview = sendMotionPreview;
|
|
4185
|
+
(window as any).__designCanvasClearMotionPreview = clearMotionPreview;
|
|
4186
|
+
// Shader-fill preview helpers (preview-only, §6.7 gating applies to apply).
|
|
4187
|
+
(window as any).__designCanvasSendShaderFillPreview = sendShaderFillPreview;
|
|
4188
|
+
(window as any).__designCanvasClearShaderFillPreview =
|
|
4189
|
+
clearShaderFillPreview;
|
|
2887
4190
|
return () => {
|
|
4191
|
+
// Identity-guard each delete so a stale unmounting instance never clobbers
|
|
4192
|
+
// a freshly mounted instance's bridge during a remount race.
|
|
2888
4193
|
if ((window as any).__designCanvasSendStyle === sendStyleChange) {
|
|
2889
4194
|
delete (window as any).__designCanvasSendStyle;
|
|
2890
4195
|
}
|
|
@@ -2898,12 +4203,38 @@ export function DesignCanvas({
|
|
|
2898
4203
|
) {
|
|
2899
4204
|
delete (window as any).__designCanvasDeleteElement;
|
|
2900
4205
|
}
|
|
4206
|
+
if (
|
|
4207
|
+
(window as any).__designCanvasSendMotionPreview === sendMotionPreview
|
|
4208
|
+
) {
|
|
4209
|
+
delete (window as any).__designCanvasSendMotionPreview;
|
|
4210
|
+
}
|
|
4211
|
+
if (
|
|
4212
|
+
(window as any).__designCanvasClearMotionPreview === clearMotionPreview
|
|
4213
|
+
) {
|
|
4214
|
+
delete (window as any).__designCanvasClearMotionPreview;
|
|
4215
|
+
}
|
|
4216
|
+
if (
|
|
4217
|
+
(window as any).__designCanvasSendShaderFillPreview ===
|
|
4218
|
+
sendShaderFillPreview
|
|
4219
|
+
) {
|
|
4220
|
+
delete (window as any).__designCanvasSendShaderFillPreview;
|
|
4221
|
+
}
|
|
4222
|
+
if (
|
|
4223
|
+
(window as any).__designCanvasClearShaderFillPreview ===
|
|
4224
|
+
clearShaderFillPreview
|
|
4225
|
+
) {
|
|
4226
|
+
delete (window as any).__designCanvasClearShaderFillPreview;
|
|
4227
|
+
}
|
|
2901
4228
|
};
|
|
2902
4229
|
}, [
|
|
2903
4230
|
deleteRuntimeElement,
|
|
2904
4231
|
registerRuntimeBridge,
|
|
2905
4232
|
replacePreviewContent,
|
|
2906
4233
|
sendStyleChange,
|
|
4234
|
+
sendMotionPreview,
|
|
4235
|
+
clearMotionPreview,
|
|
4236
|
+
sendShaderFillPreview,
|
|
4237
|
+
clearShaderFillPreview,
|
|
2907
4238
|
]);
|
|
2908
4239
|
|
|
2909
4240
|
// Device dimensions match real-world devices. iframes are replaced elements
|
|
@@ -2923,6 +4254,12 @@ export function DesignCanvas({
|
|
|
2923
4254
|
deviceDimensions[deviceFrame];
|
|
2924
4255
|
const embeddedFrameFluid = embeddedFrame?.fluid === true;
|
|
2925
4256
|
|
|
4257
|
+
// Per-breakpoint override: when previewWidthPx is set it takes priority over
|
|
4258
|
+
// the deviceFrame width so the caller can render the same source at an
|
|
4259
|
+
// explicit viewport width (e.g. 390 / 768 / 1280 side-by-side breakpoints).
|
|
4260
|
+
const resolvedWidth =
|
|
4261
|
+
previewWidthPx != null ? `${previewWidthPx}px` : iframeWidth;
|
|
4262
|
+
|
|
2926
4263
|
// Wrap the iframe in a positioned container so DrawOverlay /
|
|
2927
4264
|
// CanvasCommentPins can absolutely-position themselves on top of the
|
|
2928
4265
|
// iframe. The pin component anchors to `.design-canvas-iframe-wrapper`
|
|
@@ -2938,7 +4275,7 @@ export function DesignCanvas({
|
|
|
2938
4275
|
? embeddedFrameFluid
|
|
2939
4276
|
? "100%"
|
|
2940
4277
|
: embeddedFrame.viewportWidth
|
|
2941
|
-
:
|
|
4278
|
+
: resolvedWidth,
|
|
2942
4279
|
height: embeddedFrame
|
|
2943
4280
|
? embeddedFrameFluid
|
|
2944
4281
|
? "100%"
|
|
@@ -2958,6 +4295,12 @@ export function DesignCanvas({
|
|
|
2958
4295
|
: "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
2959
4296
|
}
|
|
2960
4297
|
data-design-preview-iframe
|
|
4298
|
+
data-design-source-type={
|
|
4299
|
+
sourceType ??
|
|
4300
|
+
(externalPreviewUrl
|
|
4301
|
+
? "localhost" // inferred — content is a URL
|
|
4302
|
+
: "inline")
|
|
4303
|
+
}
|
|
2961
4304
|
className="block h-full w-full border-0 bg-transparent"
|
|
2962
4305
|
title={t("designEditor.designPreview")}
|
|
2963
4306
|
/>
|