@agent-native/core 0.80.9 → 0.80.11
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 +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/skills.ts +45 -24
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +145 -38
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- 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 +441 -0
- package/corpus/templates/design/actions/apply-design-state.ts +213 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +413 -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/delete-design.ts +20 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/get-component-details.ts +251 -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 +277 -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 +90 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +242 -0
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +260 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +187 -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 +436 -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 +1550 -82
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +649 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1788 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +114 -33
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1071 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1053 -99
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +766 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +581 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +578 -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 +32 -0
- package/corpus/templates/design/app/i18n-data.ts +280 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +3087 -490
- 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-design-token-edits-now-stay-visible-in-previews-and-token-li.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/global-setup.ts +94 -2
- 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 +274 -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 +349 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- 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/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- 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/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.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 +8 -8
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -15,6 +15,259 @@ 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
|
+
// Map of nodeId -> property -> original inline style value.
|
|
103
|
+
var originalInlineValues = {};
|
|
104
|
+
|
|
105
|
+
function lerp(a, b, ratio) {
|
|
106
|
+
var numA = parseFloat(a);
|
|
107
|
+
var numB = parseFloat(b);
|
|
108
|
+
if (Number.isFinite(numA) && Number.isFinite(numB)) {
|
|
109
|
+
var suffix = a.replace(/^-?[\\d.]+/, '') || b.replace(/^-?[\\d.]+/, '');
|
|
110
|
+
return (numA + (numB - numA) * ratio).toFixed(4).replace(/\\.?0+$/, '') + suffix;
|
|
111
|
+
}
|
|
112
|
+
// Non-numeric: snap to b after the midpoint.
|
|
113
|
+
return ratio < 0.5 ? a : b;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function interpolate(keyframes, t) {
|
|
117
|
+
if (!keyframes || keyframes.length === 0) return '';
|
|
118
|
+
if (keyframes.length === 1) return keyframes[0].value;
|
|
119
|
+
// Find surrounding keyframes.
|
|
120
|
+
var prev = keyframes[0];
|
|
121
|
+
var next = keyframes[keyframes.length - 1];
|
|
122
|
+
for (var i = 0; i < keyframes.length - 1; i++) {
|
|
123
|
+
if (t >= keyframes[i].t && t <= keyframes[i + 1].t) {
|
|
124
|
+
prev = keyframes[i];
|
|
125
|
+
next = keyframes[i + 1];
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
var span = next.t - prev.t;
|
|
130
|
+
if (span <= 0) return prev.value;
|
|
131
|
+
var ratio = Math.max(0, Math.min(1, (t - prev.t) / span));
|
|
132
|
+
return lerp(prev.value, next.value, ratio);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function applyPreview(t) {
|
|
136
|
+
for (var i = 0; i < loadedTracks.length; i++) {
|
|
137
|
+
var track = loadedTracks[i];
|
|
138
|
+
var el = document.querySelector('[data-agent-native-node-id="' + track.targetNodeId + '"]');
|
|
139
|
+
if (!el) continue;
|
|
140
|
+
var value = interpolate(track.keyframes, t);
|
|
141
|
+
if (value === '') continue;
|
|
142
|
+
if (!originalInlineValues[track.targetNodeId]) originalInlineValues[track.targetNodeId] = {};
|
|
143
|
+
if (!(track.property in originalInlineValues[track.targetNodeId])) {
|
|
144
|
+
originalInlineValues[track.targetNodeId][track.property] = el.style[track.property] || '';
|
|
145
|
+
}
|
|
146
|
+
el.style[track.property] = value;
|
|
147
|
+
if (!touchedProps[track.targetNodeId]) touchedProps[track.targetNodeId] = [];
|
|
148
|
+
if (touchedProps[track.targetNodeId].indexOf(track.property) === -1) {
|
|
149
|
+
touchedProps[track.targetNodeId].push(track.property);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function clearPreview() {
|
|
155
|
+
var nodeIds = Object.keys(touchedProps);
|
|
156
|
+
for (var i = 0; i < nodeIds.length; i++) {
|
|
157
|
+
var el = document.querySelector('[data-agent-native-node-id="' + nodeIds[i] + '"]');
|
|
158
|
+
if (!el) continue;
|
|
159
|
+
var props = touchedProps[nodeIds[i]];
|
|
160
|
+
for (var j = 0; j < props.length; j++) {
|
|
161
|
+
var originals = originalInlineValues[nodeIds[i]] || {};
|
|
162
|
+
el.style[props[j]] = Object.prototype.hasOwnProperty.call(originals, props[j])
|
|
163
|
+
? originals[props[j]]
|
|
164
|
+
: '';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
touchedProps = {};
|
|
168
|
+
originalInlineValues = {};
|
|
169
|
+
loadedTracks = [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
window.addEventListener('message', function(e) {
|
|
173
|
+
if (e.source !== window.parent) return;
|
|
174
|
+
if (!e.data || typeof e.data.type !== 'string') return;
|
|
175
|
+
if (e.data.type === 'motion-load-tracks') {
|
|
176
|
+
clearPreview();
|
|
177
|
+
loadedTracks = Array.isArray(e.data.tracks) ? e.data.tracks : [];
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (e.data.type === 'motion-preview') {
|
|
181
|
+
var t = Number(e.data.t);
|
|
182
|
+
if (!Number.isFinite(t)) return;
|
|
183
|
+
t = Math.max(0, Math.min(1, t));
|
|
184
|
+
applyPreview(t);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (e.data.type === 'motion-preview-clear') {
|
|
188
|
+
clearPreview();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
})();
|
|
193
|
+
</script>
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Shader-fill preview bridge. ALWAYS injected alongside the other bridge
|
|
198
|
+
* scripts so the parent can apply a CSS gradient approximation of a shader
|
|
199
|
+
* fill to the currently-selected element **without** persisting anything.
|
|
200
|
+
*
|
|
201
|
+
* Protocol (parent → iframe):
|
|
202
|
+
*
|
|
203
|
+
* { type: 'shader-fill-preview', selector, nodeId, css }
|
|
204
|
+
* Apply `css` as the `background` inline style on the first element that
|
|
205
|
+
* matches `selector` (preferred) or `[data-agent-native-node-id="nodeId"]`.
|
|
206
|
+
* When both are absent, targets `document.body`. Stores the previous
|
|
207
|
+
* background value so it can be restored on clear.
|
|
208
|
+
* Preview-only — never writes to DB, Yjs, or source files.
|
|
209
|
+
*
|
|
210
|
+
* { type: 'shader-fill-preview-clear' }
|
|
211
|
+
* Remove the applied background override and restore the previous value.
|
|
212
|
+
* Called when the user discards the preview or switches selections.
|
|
213
|
+
*/
|
|
214
|
+
const SHADER_FILL_PREVIEW_BRIDGE_SCRIPT = `
|
|
215
|
+
<script data-agent-native-shader-fill-preview-bridge>
|
|
216
|
+
(function() {
|
|
217
|
+
// Track the element we patched and its original background so we can undo.
|
|
218
|
+
var patchedEl = null;
|
|
219
|
+
var originalBackground = '';
|
|
220
|
+
|
|
221
|
+
function resolveTarget(selector, nodeId) {
|
|
222
|
+
if (selector) {
|
|
223
|
+
try {
|
|
224
|
+
var hit = document.querySelector(selector);
|
|
225
|
+
if (hit) return hit;
|
|
226
|
+
} catch (_err) {}
|
|
227
|
+
}
|
|
228
|
+
if (nodeId) {
|
|
229
|
+
var byId = document.querySelector('[data-agent-native-node-id="' + nodeId.replace(/"/g, '\\\\"') + '"]');
|
|
230
|
+
if (byId) return byId;
|
|
231
|
+
}
|
|
232
|
+
return document.body;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function applyPreview(selector, nodeId, css) {
|
|
236
|
+
// Clear any prior patch first so we don't stack patches.
|
|
237
|
+
clearPreview();
|
|
238
|
+
var el = resolveTarget(selector, nodeId);
|
|
239
|
+
if (!el) return;
|
|
240
|
+
originalBackground = el.style.background || '';
|
|
241
|
+
el.style.background = css || '';
|
|
242
|
+
patchedEl = el;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function clearPreview() {
|
|
246
|
+
if (!patchedEl) return;
|
|
247
|
+
patchedEl.style.background = originalBackground;
|
|
248
|
+
patchedEl = null;
|
|
249
|
+
originalBackground = '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
window.addEventListener('message', function(e) {
|
|
253
|
+
if (e.source !== window.parent) return;
|
|
254
|
+
if (!e.data || typeof e.data.type !== 'string') return;
|
|
255
|
+
if (e.data.type === 'shader-fill-preview') {
|
|
256
|
+
var selector = typeof e.data.selector === 'string' ? e.data.selector : '';
|
|
257
|
+
var nodeId = typeof e.data.nodeId === 'string' ? e.data.nodeId : '';
|
|
258
|
+
var css = typeof e.data.css === 'string' ? e.data.css : '';
|
|
259
|
+
applyPreview(selector, nodeId, css);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (e.data.type === 'shader-fill-preview-clear') {
|
|
263
|
+
clearPreview();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
})();
|
|
268
|
+
</script>
|
|
269
|
+
`;
|
|
270
|
+
|
|
18
271
|
/**
|
|
19
272
|
* Tweak-bridge script. ALWAYS injected so the parent's postMessage
|
|
20
273
|
* (`tweak-values`) can update CSS custom properties on the iframe's :root
|
|
@@ -260,7 +513,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
260
513
|
var chromeTransitionStyle = document.createElement('style');
|
|
261
514
|
chromeTransitionStyle.textContent =
|
|
262
515
|
'[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}'
|
|
516
|
+
'[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}' +
|
|
517
|
+
'[data-agent-native-spacing-line]{position:absolute;display:none;pointer-events:none;border-radius:999px}' +
|
|
518
|
+
'[data-agent-native-spacing-region]{position:absolute;display:none;box-sizing:border-box;pointer-events:auto;background-size:6px 6px}' +
|
|
519
|
+
'[data-agent-native-spacing-region][data-orientation="vertical"]{cursor:ew-resize}' +
|
|
520
|
+
'[data-agent-native-spacing-region][data-orientation="horizontal"]{cursor:ns-resize}';
|
|
264
521
|
(document.head || document.documentElement).appendChild(chromeTransitionStyle);
|
|
265
522
|
})();
|
|
266
523
|
|
|
@@ -484,6 +741,38 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
484
741
|
reason: 'Parent layout context decides whether movement means gap, order, alignment, or wrapper structure.',
|
|
485
742
|
});
|
|
486
743
|
}
|
|
744
|
+
// --- provenance: read source-location attributes when present ---
|
|
745
|
+
// These are emitted by connected apps via @vitejs/plugin-react jsxDEV or a
|
|
746
|
+
// Babel source plugin. Cross-origin localhost iframes cannot be read (CSP /
|
|
747
|
+
// same-origin policy), so this will be undefined in that case — expected.
|
|
748
|
+
var provenance = undefined;
|
|
749
|
+
var dataSourceFile = el.getAttribute('data-source-file');
|
|
750
|
+
var dataSourceLine = el.getAttribute('data-source-line');
|
|
751
|
+
var dataSourceColumn = el.getAttribute('data-source-column');
|
|
752
|
+
var dataComponentName = el.getAttribute('data-component-name');
|
|
753
|
+
var dataLoc = el.getAttribute('data-loc');
|
|
754
|
+
// data-loc may encode "file:line:col" (Babel source plugin convention).
|
|
755
|
+
// Only parse it when data-source-file is absent, to avoid double-reads.
|
|
756
|
+
if (!dataSourceFile && dataLoc) {
|
|
757
|
+
var lastColonIndex = dataLoc.lastIndexOf(':');
|
|
758
|
+
var lastPart = lastColonIndex >= 0 ? dataLoc.slice(lastColonIndex + 1) : '';
|
|
759
|
+
if (lastColonIndex >= 0 && /^\\d+$/.test(lastPart)) {
|
|
760
|
+
var beforeLastPart = dataLoc.slice(0, lastColonIndex);
|
|
761
|
+
var previousColonIndex = beforeLastPart.lastIndexOf(':');
|
|
762
|
+
var previousPart = previousColonIndex >= 0 ? beforeLastPart.slice(previousColonIndex + 1) : '';
|
|
763
|
+
var hasColumn = /^\\d+$/.test(previousPart);
|
|
764
|
+
dataSourceFile = hasColumn ? beforeLastPart.slice(0, previousColonIndex) : beforeLastPart;
|
|
765
|
+
dataSourceLine = hasColumn ? previousPart : lastPart;
|
|
766
|
+
if (hasColumn) dataSourceColumn = lastPart;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (dataSourceFile || dataSourceLine || dataSourceColumn || dataComponentName) {
|
|
770
|
+
provenance = {};
|
|
771
|
+
if (dataSourceFile) provenance.sourceFile = dataSourceFile;
|
|
772
|
+
if (dataSourceLine) { var ln = parseInt(dataSourceLine, 10); if (!isNaN(ln)) provenance.line = ln; }
|
|
773
|
+
if (dataSourceColumn) { var col = parseInt(dataSourceColumn, 10); if (!isNaN(col)) provenance.column = col; }
|
|
774
|
+
if (dataComponentName) provenance.component = dataComponentName;
|
|
775
|
+
}
|
|
487
776
|
return {
|
|
488
777
|
tagName: el.tagName.toLowerCase(),
|
|
489
778
|
id: el.id || undefined,
|
|
@@ -559,6 +848,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
559
848
|
confidence: capabilities.reduce(function(best, item) {
|
|
560
849
|
return Math.max(best, item.confidence || 0);
|
|
561
850
|
}, 0),
|
|
851
|
+
provenance: provenance,
|
|
562
852
|
};
|
|
563
853
|
}
|
|
564
854
|
|
|
@@ -635,10 +925,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
635
925
|
if (pos.indexOf('e') !== -1) rotate.style.right = '-26px';
|
|
636
926
|
selectionOverlay.appendChild(rotate);
|
|
637
927
|
});
|
|
638
|
-
var
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
selectionOverlay.appendChild(
|
|
928
|
+
var spacingOverlay = document.createElement('div');
|
|
929
|
+
spacingOverlay.setAttribute('data-agent-native-spacing-overlay', '');
|
|
930
|
+
spacingOverlay.style.cssText = 'position:absolute;inset:0;display:none;pointer-events:none;';
|
|
931
|
+
selectionOverlay.appendChild(spacingOverlay);
|
|
642
932
|
document.body.appendChild(selectionOverlay);
|
|
643
933
|
|
|
644
934
|
var transformBadge = document.createElement('div');
|
|
@@ -646,6 +936,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
646
936
|
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
937
|
document.body.appendChild(transformBadge);
|
|
648
938
|
|
|
939
|
+
var spacingBadge = document.createElement('div');
|
|
940
|
+
spacingBadge.setAttribute('data-agent-native-spacing-badge', '');
|
|
941
|
+
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);';
|
|
942
|
+
document.body.appendChild(spacingBadge);
|
|
943
|
+
|
|
649
944
|
var insertionGuide = document.createElement('div');
|
|
650
945
|
insertionGuide.setAttribute('data-agent-native-insertion-guide', '');
|
|
651
946
|
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 +951,116 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
656
951
|
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
952
|
document.body.appendChild(measurementOverlay);
|
|
658
953
|
|
|
954
|
+
// Component-instance tag: a small pill that floats above the selection
|
|
955
|
+
// outline whenever the selected element carries a data-agent-native-component
|
|
956
|
+
// attribute. Clicking it sends a 'component-source-jump' message to the
|
|
957
|
+
// parent so the editor can invoke open-component-source.
|
|
958
|
+
var componentTagOverlay = document.createElement('div');
|
|
959
|
+
componentTagOverlay.setAttribute('data-agent-native-edit-overlay', 'component-tag');
|
|
960
|
+
componentTagOverlay.style.cssText = [
|
|
961
|
+
'position:fixed',
|
|
962
|
+
'z-index:100002',
|
|
963
|
+
'display:none',
|
|
964
|
+
'pointer-events:auto',
|
|
965
|
+
'cursor:pointer',
|
|
966
|
+
'padding:2px 6px',
|
|
967
|
+
'border-radius:4px',
|
|
968
|
+
'font:11px/1.6 ui-sans-serif,system-ui,sans-serif',
|
|
969
|
+
'white-space:nowrap',
|
|
970
|
+
'user-select:none',
|
|
971
|
+
'-webkit-user-select:none',
|
|
972
|
+
'background:var(--design-editor-accent-color)',
|
|
973
|
+
'color:var(--design-editor-accent-contrast-color)',
|
|
974
|
+
'box-shadow:0 1px 4px color-mix(in srgb,var(--design-editor-accent-color) 40%,transparent)',
|
|
975
|
+
'border:1px solid color-mix(in srgb,var(--design-editor-accent-strong-color) 60%,transparent)',
|
|
976
|
+
'outline:2px solid transparent',
|
|
977
|
+
'transition:opacity 0.1s',
|
|
978
|
+
].join(';') + ';';
|
|
979
|
+
document.body.appendChild(componentTagOverlay);
|
|
980
|
+
|
|
981
|
+
componentTagOverlay.addEventListener('click', function(e) {
|
|
982
|
+
e.stopPropagation();
|
|
983
|
+
e.preventDefault();
|
|
984
|
+
var nodeId = componentTagOverlay.getAttribute('data-component-node-id') || '';
|
|
985
|
+
var componentName = componentTagOverlay.getAttribute('data-component-name') || '';
|
|
986
|
+
if (!nodeId || !componentName) return;
|
|
987
|
+
try {
|
|
988
|
+
window.parent.postMessage({
|
|
989
|
+
type: 'component-source-jump',
|
|
990
|
+
nodeId: nodeId,
|
|
991
|
+
componentName: componentName,
|
|
992
|
+
}, '*');
|
|
993
|
+
} catch (_err) {}
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
function updateComponentTag(el) {
|
|
997
|
+
if (!el) {
|
|
998
|
+
clearComponentTag();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
var compName = el.getAttribute && el.getAttribute('data-agent-native-component');
|
|
1002
|
+
if (!compName) {
|
|
1003
|
+
clearComponentTag();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
var nodeId = (
|
|
1007
|
+
el.getAttribute('data-agent-native-node-id') ||
|
|
1008
|
+
el.getAttribute('data-code-layer-id') ||
|
|
1009
|
+
el.getAttribute('data-layer-id') ||
|
|
1010
|
+
el.id ||
|
|
1011
|
+
''
|
|
1012
|
+
);
|
|
1013
|
+
componentTagOverlay.textContent = compName + ' →';
|
|
1014
|
+
componentTagOverlay.setAttribute('data-component-node-id', nodeId);
|
|
1015
|
+
componentTagOverlay.setAttribute('data-component-name', compName);
|
|
1016
|
+
|
|
1017
|
+
var rect = el.getBoundingClientRect();
|
|
1018
|
+
var tagHeight = 22;
|
|
1019
|
+
var tagTop = rect.top - tagHeight - 4;
|
|
1020
|
+
if (tagTop < 4) tagTop = rect.top + 4;
|
|
1021
|
+
componentTagOverlay.style.display = 'block';
|
|
1022
|
+
componentTagOverlay.style.left = rect.left + 'px';
|
|
1023
|
+
componentTagOverlay.style.top = tagTop + 'px';
|
|
1024
|
+
// Accent outline on the selection overlay to distinguish component roots.
|
|
1025
|
+
selectionOverlay.style.outline = '2px solid var(--design-editor-accent-strong-color)';
|
|
1026
|
+
selectionOverlay.style.outlineOffset = '2px';
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function clearComponentTag() {
|
|
1030
|
+
componentTagOverlay.style.display = 'none';
|
|
1031
|
+
componentTagOverlay.removeAttribute('data-component-node-id');
|
|
1032
|
+
componentTagOverlay.removeAttribute('data-component-name');
|
|
1033
|
+
selectionOverlay.style.outline = '';
|
|
1034
|
+
selectionOverlay.style.outlineOffset = '';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
659
1037
|
var selectedEl = null;
|
|
660
1038
|
var hoveredEl = null;
|
|
661
1039
|
var activeTextEditEl = null;
|
|
662
1040
|
var textEditPointerState = null;
|
|
663
1041
|
var pendingStructureMove = null;
|
|
1042
|
+
var pendingShieldDrag = null;
|
|
1043
|
+
var suppressNextShieldClick = false;
|
|
1044
|
+
var suppressNextShieldClickTimer = null;
|
|
1045
|
+
var selectedSpacingHovered = false;
|
|
1046
|
+
var hoveredSpacingHandleKey = '';
|
|
1047
|
+
var spacingHandleStateByKey = {};
|
|
1048
|
+
var spacingHandleNodesByKey = {};
|
|
1049
|
+
var spacingDrag = null;
|
|
664
1050
|
var lockedSelectors = [];
|
|
665
1051
|
var hiddenSelectors = [];
|
|
666
1052
|
|
|
667
1053
|
function clearRuntimeSelection() {
|
|
668
1054
|
selectedEl = null;
|
|
669
1055
|
hoveredEl = null;
|
|
1056
|
+
selectedSpacingHovered = false;
|
|
1057
|
+
hoveredSpacingHandleKey = '';
|
|
1058
|
+
spacingDrag = null;
|
|
670
1059
|
selectionOverlay.style.display = 'none';
|
|
671
1060
|
highlightOverlay.style.display = 'none';
|
|
1061
|
+
hideSpacingOverlay();
|
|
672
1062
|
hideMeasurements();
|
|
1063
|
+
clearComponentTag();
|
|
673
1064
|
}
|
|
674
1065
|
|
|
675
1066
|
function matchesSelectorList(el, selectors) {
|
|
@@ -693,6 +1084,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
693
1084
|
}
|
|
694
1085
|
|
|
695
1086
|
function isLayerInteractionBlocked(el) {
|
|
1087
|
+
if (!el) return false;
|
|
1088
|
+
if (el.closest && el.closest('[data-agent-native-locked="true"], [data-agent-native-hidden="true"]')) {
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
696
1091
|
return matchesSelectorList(el, lockedSelectors) || matchesSelectorList(el, hiddenSelectors);
|
|
697
1092
|
}
|
|
698
1093
|
|
|
@@ -849,22 +1244,365 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
849
1244
|
refreshOverlays();
|
|
850
1245
|
}
|
|
851
1246
|
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1247
|
+
function hideSpacingOverlay() {
|
|
1248
|
+
spacingOverlay.style.display = 'none';
|
|
1249
|
+
spacingOverlay.innerHTML = '';
|
|
1250
|
+
spacingHandleStateByKey = {};
|
|
1251
|
+
spacingHandleNodesByKey = {};
|
|
1252
|
+
if (!spacingDrag) spacingBadge.style.display = 'none';
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function visibleLayoutChildren(el) {
|
|
1256
|
+
if (!el || !el.children) return [];
|
|
1257
|
+
return Array.prototype.slice.call(el.children).filter(function(child) {
|
|
1258
|
+
if (!child || child.nodeType !== 1 || isOverlayElement(child) || isLayerInteractionBlocked(child)) return false;
|
|
1259
|
+
var cs = window.getComputedStyle(child);
|
|
1260
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.position === 'fixed') return false;
|
|
1261
|
+
var rect = child.getBoundingClientRect();
|
|
1262
|
+
return rect.width > 0 && rect.height > 0;
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function spacingColor(kind) {
|
|
1267
|
+
return kind === 'gap' ? '#ff4fd8' : 'var(--design-editor-accent-color)';
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function spacingFill(kind, orientation) {
|
|
1271
|
+
var tint = kind === 'gap' ? 'rgba(255, 79, 216, 0.28)' : 'rgba(46, 168, 255, 0.24)';
|
|
1272
|
+
var stripe = kind === 'gap' ? 'rgba(255, 79, 216, 0.58)' : 'rgba(46, 168, 255, 0.52)';
|
|
1273
|
+
var angle = orientation === 'vertical' ? '135deg' : '45deg';
|
|
1274
|
+
return 'repeating-linear-gradient(' + angle + ', ' + stripe + ' 0 1px, ' + tint + ' 1px 4px, transparent 4px 7px)';
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function clampSpacingValue(value) {
|
|
1278
|
+
var rounded = Math.round(value);
|
|
1279
|
+
if (!Number.isFinite(rounded)) return 0;
|
|
1280
|
+
return Math.max(0, Math.min(999, rounded));
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function makeSpacingHandle(config) {
|
|
1284
|
+
var region = config.region;
|
|
1285
|
+
if (!region || region.width <= 0 || region.height <= 0) return null;
|
|
1286
|
+
return {
|
|
1287
|
+
key: config.key,
|
|
1288
|
+
groupKey: config.groupKey || config.key,
|
|
1289
|
+
kind: config.kind,
|
|
1290
|
+
property: config.property,
|
|
1291
|
+
oppositeProperty: config.oppositeProperty || '',
|
|
1292
|
+
side: config.side || '',
|
|
1293
|
+
orientation: config.orientation,
|
|
1294
|
+
value: clampSpacingValue(config.value),
|
|
1295
|
+
region: {
|
|
1296
|
+
x: Math.round(region.x),
|
|
1297
|
+
y: Math.round(region.y),
|
|
1298
|
+
width: Math.max(1, Math.round(region.width)),
|
|
1299
|
+
height: Math.max(1, Math.round(region.height)),
|
|
1300
|
+
},
|
|
1301
|
+
line: config.line,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function childLocalRect(child, containerRect) {
|
|
1306
|
+
var rect = child.getBoundingClientRect();
|
|
1307
|
+
return {
|
|
1308
|
+
left: rect.left - containerRect.left,
|
|
1309
|
+
top: rect.top - containerRect.top,
|
|
1310
|
+
right: rect.right - containerRect.left,
|
|
1311
|
+
bottom: rect.bottom - containerRect.top,
|
|
1312
|
+
width: rect.width,
|
|
1313
|
+
height: rect.height,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function childRectsOverlap(a, b, axis) {
|
|
1318
|
+
if (axis === 'x') {
|
|
1319
|
+
return Math.max(a.top, b.top) < Math.min(a.bottom, b.bottom);
|
|
856
1320
|
}
|
|
1321
|
+
return Math.max(a.left, b.left) < Math.min(a.right, b.right);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function buildPaddingSpacingHandles(el, rect, cs) {
|
|
1325
|
+
var handles = [];
|
|
1326
|
+
var borderTop = readPx(cs.borderTopWidth);
|
|
1327
|
+
var borderRight = readPx(cs.borderRightWidth);
|
|
1328
|
+
var borderBottom = readPx(cs.borderBottomWidth);
|
|
1329
|
+
var borderLeft = readPx(cs.borderLeftWidth);
|
|
1330
|
+
var paddingTop = readPx(cs.paddingTop);
|
|
1331
|
+
var paddingRight = readPx(cs.paddingRight);
|
|
1332
|
+
var paddingBottom = readPx(cs.paddingBottom);
|
|
1333
|
+
var paddingLeft = readPx(cs.paddingLeft);
|
|
1334
|
+
var sx = chromeScaleX();
|
|
1335
|
+
var sy = chromeScaleY();
|
|
1336
|
+
var line = Math.max(1, chromeLineScale());
|
|
1337
|
+
var hLineWidth = Math.max(8, Math.min(24, rect.width * 0.18)) * sx;
|
|
1338
|
+
var vLineHeight = Math.max(8, Math.min(24, rect.height * 0.18)) * sy;
|
|
1339
|
+
var innerLeft = borderLeft;
|
|
1340
|
+
var innerTop = borderTop;
|
|
1341
|
+
var innerWidth = Math.max(1, rect.width - borderLeft - borderRight);
|
|
1342
|
+
var innerHeight = Math.max(1, rect.height - borderTop - borderBottom);
|
|
1343
|
+
if (paddingTop > 0) {
|
|
1344
|
+
handles.push(makeSpacingHandle({
|
|
1345
|
+
key: 'padding:top',
|
|
1346
|
+
kind: 'padding',
|
|
1347
|
+
property: 'paddingTop',
|
|
1348
|
+
oppositeProperty: 'paddingBottom',
|
|
1349
|
+
side: 'top',
|
|
1350
|
+
orientation: 'horizontal',
|
|
1351
|
+
value: paddingTop,
|
|
1352
|
+
region: { x: innerLeft, y: innerTop, width: innerWidth, height: paddingTop },
|
|
1353
|
+
line: {
|
|
1354
|
+
x: rect.width / 2 - hLineWidth / 2,
|
|
1355
|
+
y: innerTop + paddingTop / 2 - line / 2,
|
|
1356
|
+
width: hLineWidth,
|
|
1357
|
+
height: line,
|
|
1358
|
+
},
|
|
1359
|
+
}));
|
|
1360
|
+
}
|
|
1361
|
+
if (paddingBottom > 0) {
|
|
1362
|
+
handles.push(makeSpacingHandle({
|
|
1363
|
+
key: 'padding:bottom',
|
|
1364
|
+
kind: 'padding',
|
|
1365
|
+
property: 'paddingBottom',
|
|
1366
|
+
oppositeProperty: 'paddingTop',
|
|
1367
|
+
side: 'bottom',
|
|
1368
|
+
orientation: 'horizontal',
|
|
1369
|
+
value: paddingBottom,
|
|
1370
|
+
region: { x: innerLeft, y: rect.height - borderBottom - paddingBottom, width: innerWidth, height: paddingBottom },
|
|
1371
|
+
line: {
|
|
1372
|
+
x: rect.width / 2 - hLineWidth / 2,
|
|
1373
|
+
y: rect.height - borderBottom - paddingBottom / 2 - line / 2,
|
|
1374
|
+
width: hLineWidth,
|
|
1375
|
+
height: line,
|
|
1376
|
+
},
|
|
1377
|
+
}));
|
|
1378
|
+
}
|
|
1379
|
+
if (paddingLeft > 0) {
|
|
1380
|
+
handles.push(makeSpacingHandle({
|
|
1381
|
+
key: 'padding:left',
|
|
1382
|
+
kind: 'padding',
|
|
1383
|
+
property: 'paddingLeft',
|
|
1384
|
+
oppositeProperty: 'paddingRight',
|
|
1385
|
+
side: 'left',
|
|
1386
|
+
orientation: 'vertical',
|
|
1387
|
+
value: paddingLeft,
|
|
1388
|
+
region: { x: innerLeft, y: innerTop, width: paddingLeft, height: innerHeight },
|
|
1389
|
+
line: {
|
|
1390
|
+
x: innerLeft + paddingLeft / 2 - line / 2,
|
|
1391
|
+
y: rect.height / 2 - vLineHeight / 2,
|
|
1392
|
+
width: line,
|
|
1393
|
+
height: vLineHeight,
|
|
1394
|
+
},
|
|
1395
|
+
}));
|
|
1396
|
+
}
|
|
1397
|
+
if (paddingRight > 0) {
|
|
1398
|
+
handles.push(makeSpacingHandle({
|
|
1399
|
+
key: 'padding:right',
|
|
1400
|
+
kind: 'padding',
|
|
1401
|
+
property: 'paddingRight',
|
|
1402
|
+
oppositeProperty: 'paddingLeft',
|
|
1403
|
+
side: 'right',
|
|
1404
|
+
orientation: 'vertical',
|
|
1405
|
+
value: paddingRight,
|
|
1406
|
+
region: { x: rect.width - borderRight - paddingRight, y: innerTop, width: paddingRight, height: innerHeight },
|
|
1407
|
+
line: {
|
|
1408
|
+
x: rect.width - borderRight - paddingRight / 2 - line / 2,
|
|
1409
|
+
y: rect.height / 2 - vLineHeight / 2,
|
|
1410
|
+
width: line,
|
|
1411
|
+
height: vLineHeight,
|
|
1412
|
+
},
|
|
1413
|
+
}));
|
|
1414
|
+
}
|
|
1415
|
+
return handles.filter(Boolean);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function buildGapSpacingHandles(el, rect, cs) {
|
|
1419
|
+
var children = visibleLayoutChildren(el);
|
|
1420
|
+
if (children.length < 2) return [];
|
|
1421
|
+
var handles = [];
|
|
1422
|
+
var sx = chromeScaleX();
|
|
1423
|
+
var sy = chromeScaleY();
|
|
1424
|
+
var line = Math.max(1, chromeLineScale());
|
|
1425
|
+
var hLineWidth = 10 * sx;
|
|
1426
|
+
var vLineHeight = 10 * sy;
|
|
1427
|
+
var isFlex = cs.display === 'flex' || cs.display === 'inline-flex';
|
|
1428
|
+
var isGrid = cs.display === 'grid' || cs.display === 'inline-grid';
|
|
1429
|
+
if (!isFlex && !isGrid) return handles;
|
|
1430
|
+
var primaryAxis = isFlex && cs.flexDirection && cs.flexDirection.indexOf('column') === 0 ? 'y' : 'x';
|
|
1431
|
+
var childRects = children.map(function(child) {
|
|
1432
|
+
return childLocalRect(child, rect);
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
function addAxisGaps(axis, property, groupKey) {
|
|
1436
|
+
var cssGap = readPx(cs[property]);
|
|
1437
|
+
if (cssGap <= 0) return;
|
|
1438
|
+
var sorted = childRects.slice().sort(function(a, b) {
|
|
1439
|
+
return axis === 'x' ? a.left - b.left : a.top - b.top;
|
|
1440
|
+
});
|
|
1441
|
+
var count = 0;
|
|
1442
|
+
for (var i = 0; i < sorted.length - 1; i += 1) {
|
|
1443
|
+
var a = sorted[i];
|
|
1444
|
+
var b = sorted[i + 1];
|
|
1445
|
+
if (!childRectsOverlap(a, b, axis)) continue;
|
|
1446
|
+
var gap = axis === 'x' ? b.left - a.right : b.top - a.bottom;
|
|
1447
|
+
if (gap <= 1) continue;
|
|
1448
|
+
if (axis === 'x') {
|
|
1449
|
+
var top = Math.max(a.top, b.top);
|
|
1450
|
+
var bottom = Math.min(a.bottom, b.bottom);
|
|
1451
|
+
var height = Math.max(1, bottom - top);
|
|
1452
|
+
handles.push(makeSpacingHandle({
|
|
1453
|
+
key: groupKey + ':' + count,
|
|
1454
|
+
groupKey: groupKey,
|
|
1455
|
+
kind: 'gap',
|
|
1456
|
+
property: property,
|
|
1457
|
+
orientation: 'vertical',
|
|
1458
|
+
value: cssGap,
|
|
1459
|
+
region: { x: a.right, y: top, width: gap, height: height },
|
|
1460
|
+
line: {
|
|
1461
|
+
x: a.right + gap / 2 - line / 2,
|
|
1462
|
+
y: top + height / 2 - vLineHeight / 2,
|
|
1463
|
+
width: line,
|
|
1464
|
+
height: vLineHeight,
|
|
1465
|
+
},
|
|
1466
|
+
}));
|
|
1467
|
+
} else {
|
|
1468
|
+
var left = Math.max(a.left, b.left);
|
|
1469
|
+
var right = Math.min(a.right, b.right);
|
|
1470
|
+
var width = Math.max(1, right - left);
|
|
1471
|
+
handles.push(makeSpacingHandle({
|
|
1472
|
+
key: groupKey + ':' + count,
|
|
1473
|
+
groupKey: groupKey,
|
|
1474
|
+
kind: 'gap',
|
|
1475
|
+
property: property,
|
|
1476
|
+
orientation: 'horizontal',
|
|
1477
|
+
value: cssGap,
|
|
1478
|
+
region: { x: left, y: a.bottom, width: width, height: gap },
|
|
1479
|
+
line: {
|
|
1480
|
+
x: left + width / 2 - hLineWidth / 2,
|
|
1481
|
+
y: a.bottom + gap / 2 - line / 2,
|
|
1482
|
+
width: hLineWidth,
|
|
1483
|
+
height: line,
|
|
1484
|
+
},
|
|
1485
|
+
}));
|
|
1486
|
+
}
|
|
1487
|
+
count += 1;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (primaryAxis === 'x') {
|
|
1492
|
+
addAxisGaps('x', 'columnGap', 'gap:column');
|
|
1493
|
+
if (isGrid) addAxisGaps('y', 'rowGap', 'gap:row');
|
|
1494
|
+
} else {
|
|
1495
|
+
addAxisGaps('y', 'rowGap', 'gap:row');
|
|
1496
|
+
if (isGrid) addAxisGaps('x', 'columnGap', 'gap:column');
|
|
1497
|
+
}
|
|
1498
|
+
return handles.filter(Boolean);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function buildSpacingHandles(el) {
|
|
1502
|
+
if (!el || !document.documentElement.contains(el)) return [];
|
|
1503
|
+
if (Math.abs(currentRotation(el)) > 0.01) return [];
|
|
1504
|
+
var children = visibleLayoutChildren(el);
|
|
1505
|
+
if (children.length === 0) return [];
|
|
1506
|
+
var rect = el.getBoundingClientRect();
|
|
1507
|
+
if (rect.width <= 0 || rect.height <= 0) return [];
|
|
857
1508
|
var cs = window.getComputedStyle(el);
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1509
|
+
return buildPaddingSpacingHandles(el, rect, cs).concat(buildGapSpacingHandles(el, rect, cs));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function showSpacingBadgeForHandle(handle, value) {
|
|
1513
|
+
if (!selectedEl || !handle) {
|
|
1514
|
+
spacingBadge.style.display = 'none';
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
var rect = selectedEl.getBoundingClientRect();
|
|
1518
|
+
var x = rect.left + handle.region.x + handle.region.width / 2;
|
|
1519
|
+
var y = rect.top + handle.region.y + handle.region.height / 2;
|
|
1520
|
+
spacingBadge.textContent = String(clampSpacingValue(value));
|
|
1521
|
+
spacingBadge.style.display = 'block';
|
|
1522
|
+
spacingBadge.style.background = spacingColor(handle.kind);
|
|
1523
|
+
spacingBadge.style.left = x + 'px';
|
|
1524
|
+
spacingBadge.style.top = y + 'px';
|
|
1525
|
+
spacingBadge.style.transform = 'translate(-50%, -50%)';
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function renderSpacingHandle(handle, activeGroupKey) {
|
|
1529
|
+
if (!handle) return;
|
|
1530
|
+
spacingHandleStateByKey[handle.key] = handle;
|
|
1531
|
+
var highlighted = Boolean(activeGroupKey && handle.groupKey === activeGroupKey);
|
|
1532
|
+
var lineNode = document.createElement('span');
|
|
1533
|
+
lineNode.setAttribute('data-agent-native-spacing-line', handle.kind);
|
|
1534
|
+
lineNode.style.display = 'block';
|
|
1535
|
+
lineNode.style.left = handle.line.x + 'px';
|
|
1536
|
+
lineNode.style.top = handle.line.y + 'px';
|
|
1537
|
+
lineNode.style.width = Math.max(1, handle.line.width) + 'px';
|
|
1538
|
+
lineNode.style.height = Math.max(1, handle.line.height) + 'px';
|
|
1539
|
+
lineNode.style.background = spacingColor(handle.kind);
|
|
1540
|
+
spacingOverlay.appendChild(lineNode);
|
|
1541
|
+
|
|
1542
|
+
var regionNode = document.createElement('span');
|
|
1543
|
+
regionNode.setAttribute('data-agent-native-spacing-region', handle.kind);
|
|
1544
|
+
regionNode.setAttribute('data-orientation', handle.orientation);
|
|
1545
|
+
regionNode.setAttribute('data-spacing-key', handle.key);
|
|
1546
|
+
regionNode.style.display = 'block';
|
|
1547
|
+
regionNode.style.left = handle.region.x + 'px';
|
|
1548
|
+
regionNode.style.top = handle.region.y + 'px';
|
|
1549
|
+
regionNode.style.width = handle.region.width + 'px';
|
|
1550
|
+
regionNode.style.height = handle.region.height + 'px';
|
|
1551
|
+
regionNode.style.background = highlighted ? spacingFill(handle.kind, handle.orientation) : 'transparent';
|
|
1552
|
+
regionNode.style.outline = highlighted ? '1px solid ' + spacingColor(handle.kind) : '0';
|
|
1553
|
+
regionNode.style.outlineOffset = '-1px';
|
|
1554
|
+
regionNode.addEventListener('mouseenter', function() {
|
|
1555
|
+
hoveredSpacingHandleKey = handle.key;
|
|
1556
|
+
selectedSpacingHovered = true;
|
|
1557
|
+
updateSpacingOverlay(selectedEl);
|
|
1558
|
+
});
|
|
1559
|
+
regionNode.addEventListener('mouseleave', function() {
|
|
1560
|
+
if (spacingDrag) return;
|
|
1561
|
+
hoveredSpacingHandleKey = '';
|
|
1562
|
+
updateSpacingOverlay(selectedEl);
|
|
1563
|
+
});
|
|
1564
|
+
regionNode.addEventListener('mousedown', function(event) {
|
|
1565
|
+
startSpacingDrag(handle.key, event);
|
|
1566
|
+
}, true);
|
|
1567
|
+
spacingHandleNodesByKey[handle.key] = regionNode;
|
|
1568
|
+
spacingOverlay.appendChild(regionNode);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function updateSpacingOverlay(el) {
|
|
1572
|
+
if (el && el !== selectedEl) {
|
|
1573
|
+
hideSpacingOverlay();
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (!selectedEl || !document.documentElement.contains(selectedEl)) {
|
|
1577
|
+
hideSpacingOverlay();
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
if (!selectedSpacingHovered && !hoveredSpacingHandleKey && !spacingDrag) {
|
|
1581
|
+
hideSpacingOverlay();
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
var handles = buildSpacingHandles(selectedEl);
|
|
1585
|
+
if (handles.length === 0) {
|
|
1586
|
+
hideSpacingOverlay();
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
spacingOverlay.style.display = 'block';
|
|
1590
|
+
spacingOverlay.innerHTML = '';
|
|
1591
|
+
spacingHandleStateByKey = {};
|
|
1592
|
+
spacingHandleNodesByKey = {};
|
|
1593
|
+
var activeHandle =
|
|
1594
|
+
(spacingDrag && spacingDrag.handle) ||
|
|
1595
|
+
handles.find(function(handle) { return handle.key === hoveredSpacingHandleKey; }) ||
|
|
1596
|
+
null;
|
|
1597
|
+
var activeGroupKey = activeHandle ? activeHandle.groupKey : '';
|
|
1598
|
+
handles.forEach(function(handle) {
|
|
1599
|
+
renderSpacingHandle(handle, activeGroupKey);
|
|
1600
|
+
});
|
|
1601
|
+
if (activeHandle) {
|
|
1602
|
+
showSpacingBadgeForHandle(activeHandle, spacingDrag ? spacingDrag.currentValue : activeHandle.value);
|
|
1603
|
+
} else {
|
|
1604
|
+
spacingBadge.style.display = 'none';
|
|
1605
|
+
}
|
|
868
1606
|
}
|
|
869
1607
|
|
|
870
1608
|
function applyEditorChromeScale() {
|
|
@@ -874,7 +1612,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
874
1612
|
var line = chromeLineScale();
|
|
875
1613
|
highlightOverlay.style.borderWidth = (1.5 * line) + 'px';
|
|
876
1614
|
selectionOverlay.style.borderWidth = (1.5 * line) + 'px';
|
|
877
|
-
|
|
1615
|
+
if (selectedEl) updateSpacingOverlay(selectedEl);
|
|
878
1616
|
|
|
879
1617
|
selectionOverlay.querySelectorAll('[data-agent-native-edge-handle]').forEach(function(edge) {
|
|
880
1618
|
var pos = edge.getAttribute('data-agent-native-edge-handle');
|
|
@@ -913,6 +1651,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
913
1651
|
function positionOverlay(overlay, el) {
|
|
914
1652
|
if (!el || !document.documentElement.contains(el)) {
|
|
915
1653
|
overlay.style.display = 'none';
|
|
1654
|
+
if (overlay === selectionOverlay) clearComponentTag();
|
|
916
1655
|
return;
|
|
917
1656
|
}
|
|
918
1657
|
// For the selection overlay, prefer the CSS box + rotation transform so
|
|
@@ -936,7 +1675,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
936
1675
|
overlay.style.height = elH + 'px';
|
|
937
1676
|
overlay.style.transform = 'rotate(' + elRot + 'deg)';
|
|
938
1677
|
overlay.style.transformOrigin = '0 0';
|
|
939
|
-
|
|
1678
|
+
updateSpacingOverlay(el);
|
|
1679
|
+
updateComponentTag(el);
|
|
940
1680
|
return;
|
|
941
1681
|
}
|
|
942
1682
|
}
|
|
@@ -947,7 +1687,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
947
1687
|
overlay.style.width = rect.width + 'px';
|
|
948
1688
|
overlay.style.height = rect.height + 'px';
|
|
949
1689
|
overlay.style.transform = '';
|
|
950
|
-
if (overlay === selectionOverlay)
|
|
1690
|
+
if (overlay === selectionOverlay) {
|
|
1691
|
+
updateSpacingOverlay(el);
|
|
1692
|
+
updateComponentTag(el);
|
|
1693
|
+
}
|
|
951
1694
|
}
|
|
952
1695
|
|
|
953
1696
|
function refreshOverlays() {
|
|
@@ -1026,6 +1769,13 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1026
1769
|
);
|
|
1027
1770
|
}
|
|
1028
1771
|
|
|
1772
|
+
function dragEventNames(e) {
|
|
1773
|
+
var pointerGesture = e && e.type && e.type.indexOf('pointer') === 0;
|
|
1774
|
+
return pointerGesture
|
|
1775
|
+
? { move: 'pointermove', up: 'pointerup' }
|
|
1776
|
+
: { move: 'mousemove', up: 'mouseup' };
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1029
1779
|
function elementFromEditorPoint(clientX, clientY) {
|
|
1030
1780
|
var shieldPointerEvents = shieldOverlay.style.pointerEvents;
|
|
1031
1781
|
var selectionPointerEvents = selectionOverlay.style.pointerEvents;
|
|
@@ -1190,6 +1940,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1190
1940
|
function selectElementAtEvent(e) {
|
|
1191
1941
|
stopNativeInteraction(e);
|
|
1192
1942
|
blurActiveTextEditor();
|
|
1943
|
+
if (suppressNextShieldClick) {
|
|
1944
|
+
suppressNextShieldClick = false;
|
|
1945
|
+
if (suppressNextShieldClickTimer !== null) {
|
|
1946
|
+
clearTimeout(suppressNextShieldClickTimer);
|
|
1947
|
+
suppressNextShieldClickTimer = null;
|
|
1948
|
+
}
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1193
1951
|
// Suppress the first click in a double-click sequence — the dblclick
|
|
1194
1952
|
// handler (beginTextEditingFromEvent) will fire immediately after and a
|
|
1195
1953
|
// spurious element-select would cause inspector flicker.
|
|
@@ -1197,27 +1955,53 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1197
1955
|
var target = elementFromEditorPoint(e.clientX, e.clientY);
|
|
1198
1956
|
if (!target || target === document.body || target === document.documentElement) {
|
|
1199
1957
|
// Click on empty canvas: clear the current selection (matches Figma).
|
|
1200
|
-
|
|
1201
|
-
selectionOverlay.style.display = 'none';
|
|
1958
|
+
clearRuntimeSelection();
|
|
1202
1959
|
window.parent.postMessage({ type: 'clear-selection' }, '*');
|
|
1203
1960
|
return;
|
|
1204
1961
|
}
|
|
1962
|
+
selectedSpacingHovered = false;
|
|
1963
|
+
hoveredSpacingHandleKey = '';
|
|
1205
1964
|
selectedEl = selectionTargetForHit(target);
|
|
1965
|
+
if (!selectedEl || isLayerInteractionBlocked(selectedEl)) {
|
|
1966
|
+
selectedEl = null;
|
|
1967
|
+
selectionOverlay.style.display = 'none';
|
|
1968
|
+
clearComponentTag();
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1206
1971
|
var info = getElementInfo(selectedEl);
|
|
1207
1972
|
positionOverlay(selectionOverlay, selectedEl);
|
|
1208
1973
|
window.parent.postMessage({ type: 'element-select', payload: info }, '*');
|
|
1209
1974
|
}
|
|
1210
1975
|
|
|
1976
|
+
function suppressNextShieldClickBriefly() {
|
|
1977
|
+
suppressNextShieldClick = true;
|
|
1978
|
+
if (suppressNextShieldClickTimer !== null) {
|
|
1979
|
+
clearTimeout(suppressNextShieldClickTimer);
|
|
1980
|
+
}
|
|
1981
|
+
suppressNextShieldClickTimer = setTimeout(function() {
|
|
1982
|
+
suppressNextShieldClick = false;
|
|
1983
|
+
suppressNextShieldClickTimer = null;
|
|
1984
|
+
}, 250);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1211
1987
|
function openContextMenuAtEvent(e) {
|
|
1212
1988
|
stopNativeInteraction(e);
|
|
1213
1989
|
blurActiveTextEditor();
|
|
1214
1990
|
var target = elementFromEditorPoint(e.clientX, e.clientY);
|
|
1215
1991
|
var info = null;
|
|
1216
1992
|
if (target) {
|
|
1993
|
+
selectedSpacingHovered = false;
|
|
1994
|
+
hoveredSpacingHandleKey = '';
|
|
1217
1995
|
selectedEl = selectionTargetForHit(target);
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1996
|
+
if (selectedEl && !isLayerInteractionBlocked(selectedEl)) {
|
|
1997
|
+
info = getElementInfo(selectedEl);
|
|
1998
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
1999
|
+
window.parent.postMessage({ type: 'element-select', payload: info }, '*');
|
|
2000
|
+
} else {
|
|
2001
|
+
selectedEl = null;
|
|
2002
|
+
selectionOverlay.style.display = 'none';
|
|
2003
|
+
clearComponentTag();
|
|
2004
|
+
}
|
|
1221
2005
|
}
|
|
1222
2006
|
window.parent.postMessage({
|
|
1223
2007
|
type: 'element-contextmenu',
|
|
@@ -1319,6 +2103,81 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1319
2103
|
}, '*');
|
|
1320
2104
|
}
|
|
1321
2105
|
|
|
2106
|
+
function spacingValueFromPointer(handle, originValue, startX, startY, clientX, clientY) {
|
|
2107
|
+
var delta =
|
|
2108
|
+
handle.orientation === 'vertical' ? clientX - startX : clientY - startY;
|
|
2109
|
+
if (handle.kind === 'padding' && (handle.side === 'right' || handle.side === 'bottom')) {
|
|
2110
|
+
delta = -delta;
|
|
2111
|
+
}
|
|
2112
|
+
return clampSpacingValue(originValue + delta);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
function applySpacingDragValue(target, handle, value, mirrorOpposite) {
|
|
2116
|
+
if (!target || !handle) return;
|
|
2117
|
+
target.style[handle.property] = value + 'px';
|
|
2118
|
+
if (handle.kind === 'padding' && mirrorOpposite && handle.oppositeProperty) {
|
|
2119
|
+
target.style[handle.oppositeProperty] = value + 'px';
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function startSpacingDrag(key, e) {
|
|
2124
|
+
var handle = spacingHandleStateByKey[key];
|
|
2125
|
+
if (!selectedEl || !handle || isLayerInteractionBlocked(selectedEl)) return;
|
|
2126
|
+
e.preventDefault();
|
|
2127
|
+
e.stopPropagation();
|
|
2128
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2129
|
+
var events = dragEventNames(e);
|
|
2130
|
+
var dragEl = selectedEl;
|
|
2131
|
+
var originValue = handle.value;
|
|
2132
|
+
var startX = e.clientX;
|
|
2133
|
+
var startY = e.clientY;
|
|
2134
|
+
hoveredSpacingHandleKey = key;
|
|
2135
|
+
selectedSpacingHovered = true;
|
|
2136
|
+
spacingDrag = {
|
|
2137
|
+
handle: handle,
|
|
2138
|
+
currentValue: originValue,
|
|
2139
|
+
mirrorOpposite: !!e.altKey,
|
|
2140
|
+
};
|
|
2141
|
+
showSpacingBadgeForHandle(handle, originValue);
|
|
2142
|
+
|
|
2143
|
+
function onMove(ev) {
|
|
2144
|
+
if (!dragEl || !document.documentElement.contains(dragEl)) return;
|
|
2145
|
+
var nextValue = spacingValueFromPointer(handle, originValue, startX, startY, ev.clientX, ev.clientY);
|
|
2146
|
+
spacingDrag = {
|
|
2147
|
+
handle: handle,
|
|
2148
|
+
currentValue: nextValue,
|
|
2149
|
+
mirrorOpposite: !!ev.altKey,
|
|
2150
|
+
};
|
|
2151
|
+
applySpacingDragValue(dragEl, handle, nextValue, !!ev.altKey);
|
|
2152
|
+
positionOverlay(selectionOverlay, dragEl);
|
|
2153
|
+
showSpacingBadgeForHandle(handle, nextValue);
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function onUp(ev) {
|
|
2157
|
+
document.removeEventListener(events.move, onMove, true);
|
|
2158
|
+
document.removeEventListener(events.up, onUp, true);
|
|
2159
|
+
if (!dragEl || !document.documentElement.contains(dragEl)) {
|
|
2160
|
+
spacingDrag = null;
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
var finalValue = spacingDrag ? spacingDrag.currentValue : originValue;
|
|
2164
|
+
var mirrorOpposite = spacingDrag ? spacingDrag.mirrorOpposite : !!ev.altKey;
|
|
2165
|
+
applySpacingDragValue(dragEl, handle, finalValue, mirrorOpposite);
|
|
2166
|
+
selectedEl = dragEl;
|
|
2167
|
+
spacingDrag = null;
|
|
2168
|
+
var styles = {};
|
|
2169
|
+
styles[handle.property] = finalValue + 'px';
|
|
2170
|
+
if (handle.kind === 'padding' && mirrorOpposite && handle.oppositeProperty) {
|
|
2171
|
+
styles[handle.oppositeProperty] = finalValue + 'px';
|
|
2172
|
+
}
|
|
2173
|
+
postVisualStyleChange(styles);
|
|
2174
|
+
positionOverlay(selectionOverlay, dragEl);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
document.addEventListener(events.move, onMove, true);
|
|
2178
|
+
document.addEventListener(events.up, onUp, true);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
1322
2181
|
function postTextContentChange(el, value, html) {
|
|
1323
2182
|
window.parent.postMessage({
|
|
1324
2183
|
type: 'text-content-change',
|
|
@@ -1365,6 +2224,21 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1365
2224
|
return !!(ancestorEl && (ancestorEl === el || el.contains(ancestorEl)));
|
|
1366
2225
|
}
|
|
1367
2226
|
|
|
2227
|
+
function normalizeCssPropertyName(property) {
|
|
2228
|
+
var prop = String(property || '').trim();
|
|
2229
|
+
if (!prop) return '';
|
|
2230
|
+
if (prop.indexOf('--') === 0) return prop;
|
|
2231
|
+
return prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
function applyInlineStyleProperty(el, property, value) {
|
|
2235
|
+
if (!el || !property) return false;
|
|
2236
|
+
var cssProperty = normalizeCssPropertyName(property);
|
|
2237
|
+
if (!cssProperty) return false;
|
|
2238
|
+
el.style.setProperty(cssProperty, String(value));
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
1368
2242
|
function applyTextRangeStyle(property, value) {
|
|
1369
2243
|
if (!activeTextEditEl || !property) return false;
|
|
1370
2244
|
var selection = window.getSelection && window.getSelection();
|
|
@@ -1372,7 +2246,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1372
2246
|
if (!selectionBelongsToElement(selection, activeTextEditEl)) return false;
|
|
1373
2247
|
var range = selection.getRangeAt(0);
|
|
1374
2248
|
var span = document.createElement('span');
|
|
1375
|
-
span
|
|
2249
|
+
applyInlineStyleProperty(span, property, value);
|
|
1376
2250
|
if (!span.getAttribute('style')) return false;
|
|
1377
2251
|
try {
|
|
1378
2252
|
range.surroundContents(span);
|
|
@@ -1404,7 +2278,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1404
2278
|
}
|
|
1405
2279
|
|
|
1406
2280
|
function isOverlayElement(el) {
|
|
1407
|
-
|
|
2281
|
+
// Use closest() so that children of overlay elements (e.g. spacing-region
|
|
2282
|
+
// spans inside selectionOverlay that have pointer-events:auto via a CSS rule
|
|
2283
|
+
// and can therefore still be returned by elementFromPoint even when the
|
|
2284
|
+
// parent overlay has pointer-events:none set inline) are also treated as
|
|
2285
|
+
// overlay elements and never used as drag-drop anchor targets.
|
|
2286
|
+
return Boolean(
|
|
2287
|
+
el && el.closest && el.closest('[data-agent-native-edit-overlay]')
|
|
2288
|
+
);
|
|
1408
2289
|
}
|
|
1409
2290
|
|
|
1410
2291
|
function draggableElementChildren(parent) {
|
|
@@ -1418,9 +2299,40 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1418
2299
|
if (el === document.body || el === document.documentElement) return false;
|
|
1419
2300
|
var cs = window.getComputedStyle(el);
|
|
1420
2301
|
if (cs.position === 'absolute' || cs.position === 'fixed') return false;
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
2302
|
+
return true;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// keep in sync with EditPanel.tsx CONTAINER_TAGS/LEAF_TAGS (~line 1679)
|
|
2306
|
+
var BRIDGE_CONTAINER_TAGS = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'form', 'ul', 'ol', 'figure', 'fieldset', 'details', 'dialog', 'blockquote', 'table', 'tbody', 'thead', 'tr'];
|
|
2307
|
+
var BRIDGE_LEAF_TAGS = ['img', 'video', 'picture', 'audio', 'canvas', 'svg', 'path', 'input', 'textarea', 'select', 'br', 'hr', 'iframe'];
|
|
2308
|
+
var BRIDGE_TEXT_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'a', 'strong', 'em', 'label', 'li'];
|
|
2309
|
+
|
|
2310
|
+
function isContainerDropTarget(el) {
|
|
2311
|
+
if (!el || el === document.documentElement) return false;
|
|
2312
|
+
if (isOverlayElement(el) || isLayerInteractionBlocked(el)) return false;
|
|
2313
|
+
if (el === document.body) return true;
|
|
2314
|
+
var tag = (el.tagName || '').toLowerCase();
|
|
2315
|
+
// Reject leaf/text tags — they cannot accept children
|
|
2316
|
+
if (BRIDGE_LEAF_TAGS.indexOf(tag) !== -1 || BRIDGE_TEXT_TAGS.indexOf(tag) !== -1) return false;
|
|
2317
|
+
var cs = window.getComputedStyle(el);
|
|
2318
|
+
if (
|
|
2319
|
+
cs.display === 'flex' ||
|
|
2320
|
+
cs.display === 'inline-flex' ||
|
|
2321
|
+
cs.display === 'grid' ||
|
|
2322
|
+
cs.display === 'inline-grid'
|
|
2323
|
+
) {
|
|
2324
|
+
return true;
|
|
2325
|
+
}
|
|
2326
|
+
return BRIDGE_CONTAINER_TAGS.indexOf(tag) !== -1;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function edgePlacementForRect(rect, axis, clientX, clientY) {
|
|
2330
|
+
var size = axis === 'x' ? rect.width : rect.height;
|
|
2331
|
+
if (!size) return null;
|
|
2332
|
+
var offset = axis === 'x' ? clientX - rect.left : clientY - rect.top;
|
|
2333
|
+
if (offset < size * 0.22) return 'before';
|
|
2334
|
+
if (offset > size * 0.78) return 'after';
|
|
2335
|
+
return null;
|
|
1424
2336
|
}
|
|
1425
2337
|
|
|
1426
2338
|
function parentFlowAxis(parent) {
|
|
@@ -1445,18 +2357,19 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1445
2357
|
var hit = elementFromEditorPoint(clientX, clientY);
|
|
1446
2358
|
if (
|
|
1447
2359
|
hit &&
|
|
1448
|
-
hit !== document.body &&
|
|
1449
2360
|
hit !== document.documentElement &&
|
|
1450
2361
|
hit !== el &&
|
|
1451
2362
|
!el.contains(hit) &&
|
|
1452
|
-
!hit.contains(el) &&
|
|
1453
2363
|
!isOverlayElement(hit)
|
|
1454
2364
|
) {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
2365
|
+
if (isContainerDropTarget(hit)) {
|
|
2366
|
+
var containerRect = hit.getBoundingClientRect();
|
|
2367
|
+
var edgeAxis = hit.parentElement ? parentFlowAxis(hit.parentElement) : parentFlowAxis(hit);
|
|
2368
|
+
var edgePlacement = edgePlacementForRect(containerRect, edgeAxis, clientX, clientY);
|
|
2369
|
+
if (!edgePlacement) {
|
|
2370
|
+
return { anchor: hit, placement: 'inside', axis: parentFlowAxis(hit) };
|
|
2371
|
+
}
|
|
2372
|
+
return { anchor: hit, placement: edgePlacement, axis: edgeAxis };
|
|
1460
2373
|
}
|
|
1461
2374
|
var hitParent = hit.parentElement;
|
|
1462
2375
|
if (hitParent) {
|
|
@@ -1503,6 +2416,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1503
2416
|
insertionGuide.style.display = 'block';
|
|
1504
2417
|
insertionGuide.style.background = 'var(--design-editor-accent-color)';
|
|
1505
2418
|
insertionGuide.style.border = '0';
|
|
2419
|
+
insertionGuide.style.borderRadius = '999px';
|
|
1506
2420
|
insertionGuide.style.boxShadow = '0 0 0 1px var(--design-editor-accent-color)';
|
|
1507
2421
|
if (target.placement === 'inside') {
|
|
1508
2422
|
insertionGuide.style.left = rect.left + 'px';
|
|
@@ -1511,6 +2425,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1511
2425
|
insertionGuide.style.height = rect.height + 'px';
|
|
1512
2426
|
insertionGuide.style.background = 'color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)';
|
|
1513
2427
|
insertionGuide.style.border = '2px solid var(--design-editor-accent-color)';
|
|
2428
|
+
insertionGuide.style.borderRadius = '2px';
|
|
1514
2429
|
insertionGuide.style.boxShadow = 'none';
|
|
1515
2430
|
return;
|
|
1516
2431
|
}
|
|
@@ -1578,6 +2493,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1578
2493
|
if (isLayerInteractionBlocked(selectedEl)) return;
|
|
1579
2494
|
e.preventDefault();
|
|
1580
2495
|
e.stopPropagation();
|
|
2496
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2497
|
+
var events = dragEventNames(e);
|
|
1581
2498
|
var originalSelectedEl = selectedEl;
|
|
1582
2499
|
var duplicatedForDrag = false;
|
|
1583
2500
|
if (e.altKey && selectedEl !== document.body && selectedEl !== document.documentElement) {
|
|
@@ -1595,16 +2512,96 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1595
2512
|
var reorderEl = selectedEl;
|
|
1596
2513
|
var currentTarget = reorderTargetForPoint(reorderEl, e.clientX, e.clientY);
|
|
1597
2514
|
showInsertionGuideFor(currentTarget);
|
|
2515
|
+
// Cross-screen drag state: true when the pointer is outside this iframe's
|
|
2516
|
+
// viewport bounds. The host frame renders the ghost + highlight and owns
|
|
2517
|
+
// the drop when this is true; the bridge suppresses its in-iframe reorder.
|
|
2518
|
+
var pointerOutsideIframe = false;
|
|
2519
|
+
var reorderSelector = getSelector(reorderEl);
|
|
2520
|
+
var reorderSourceId = getSourceId(reorderEl);
|
|
1598
2521
|
function onReorderMove(ev) {
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
2522
|
+
var vw = window.innerWidth;
|
|
2523
|
+
var vh = window.innerHeight;
|
|
2524
|
+
var cx = ev.clientX;
|
|
2525
|
+
var cy = ev.clientY;
|
|
2526
|
+
var outside = cx < 0 || cy < 0 || cx > vw || cy > vh;
|
|
2527
|
+
pointerOutsideIframe = outside;
|
|
2528
|
+
// Always notify the host frame so it can track the cursor position,
|
|
2529
|
+
// render the ghost, and highlight the target screen.
|
|
2530
|
+
window.parent.postMessage({
|
|
2531
|
+
type: 'agent-native:cross-screen-drag',
|
|
2532
|
+
phase: 'move',
|
|
2533
|
+
selector: reorderSelector,
|
|
2534
|
+
sourceId: reorderSourceId,
|
|
2535
|
+
iframeX: cx,
|
|
2536
|
+
iframeY: cy,
|
|
2537
|
+
viewportW: vw,
|
|
2538
|
+
viewportH: vh,
|
|
2539
|
+
}, '*');
|
|
2540
|
+
if (outside) {
|
|
2541
|
+
// Cursor left this iframe — hide the in-iframe insertion guide so
|
|
2542
|
+
// it does not render while the host shows a cross-screen drop target.
|
|
2543
|
+
hideInsertionGuide();
|
|
2544
|
+
showTransformBadge('Move layer', cx, cy);
|
|
2545
|
+
} else {
|
|
2546
|
+
// Cursor is inside this iframe — use existing in-iframe behavior.
|
|
2547
|
+
currentTarget = reorderTargetForPoint(reorderEl, cx, cy);
|
|
2548
|
+
showInsertionGuideFor(currentTarget);
|
|
2549
|
+
showTransformBadge(currentTarget ? 'Move layer' : 'Move', cx, cy);
|
|
2550
|
+
}
|
|
1602
2551
|
}
|
|
1603
|
-
function
|
|
1604
|
-
document.removeEventListener(
|
|
1605
|
-
document.removeEventListener(
|
|
2552
|
+
function onReorderEscape() {
|
|
2553
|
+
document.removeEventListener(events.move, onReorderMove, true);
|
|
2554
|
+
document.removeEventListener(events.up, onReorderUp, true);
|
|
2555
|
+
document.removeEventListener('keydown', onReorderKeyDown, true);
|
|
2556
|
+
hideTransformBadge();
|
|
2557
|
+
hideInsertionGuide();
|
|
2558
|
+
window.parent.postMessage({ type: 'agent-native:cross-screen-drag', phase: 'cancel' }, '*');
|
|
2559
|
+
// Revert any clone that was inserted for alt-drag.
|
|
2560
|
+
if (duplicatedForDrag && reorderEl && reorderEl !== originalSelectedEl) {
|
|
2561
|
+
if (reorderEl.parentElement) reorderEl.parentElement.removeChild(reorderEl);
|
|
2562
|
+
selectedEl = originalSelectedEl;
|
|
2563
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2564
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
function onReorderKeyDown(ev) {
|
|
2568
|
+
if (ev.key === 'Escape') {
|
|
2569
|
+
ev.preventDefault();
|
|
2570
|
+
ev.stopPropagation();
|
|
2571
|
+
if (ev.stopImmediatePropagation) ev.stopImmediatePropagation();
|
|
2572
|
+
onReorderEscape();
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
function onReorderUp(ev) {
|
|
2576
|
+
document.removeEventListener(events.move, onReorderMove, true);
|
|
2577
|
+
document.removeEventListener(events.up, onReorderUp, true);
|
|
2578
|
+
document.removeEventListener('keydown', onReorderKeyDown, true);
|
|
1606
2579
|
hideTransformBadge();
|
|
1607
2580
|
hideInsertionGuide();
|
|
2581
|
+
var vw = window.innerWidth;
|
|
2582
|
+
var vh = window.innerHeight;
|
|
2583
|
+
var cx = ev ? ev.clientX : 0;
|
|
2584
|
+
var cy = ev ? ev.clientY : 0;
|
|
2585
|
+
var outsideOnDrop = cx < 0 || cy < 0 || cx > vw || cy > vh;
|
|
2586
|
+
// Post the end message so the host can finalize a cross-screen drop.
|
|
2587
|
+
window.parent.postMessage({
|
|
2588
|
+
type: 'agent-native:cross-screen-drag',
|
|
2589
|
+
phase: 'end',
|
|
2590
|
+
selector: reorderSelector,
|
|
2591
|
+
sourceId: reorderSourceId,
|
|
2592
|
+
iframeX: cx,
|
|
2593
|
+
iframeY: cy,
|
|
2594
|
+
viewportW: vw,
|
|
2595
|
+
viewportH: vh,
|
|
2596
|
+
}, '*');
|
|
2597
|
+
// When the pointer is outside this iframe at release, the host owns the
|
|
2598
|
+
// move (cross-screen drop). Do NOT apply the in-iframe reorder so we
|
|
2599
|
+
// avoid a ghost element left in screen A's DOM.
|
|
2600
|
+
// Use outsideOnDrop only — pointerOutsideIframe is stale when the user
|
|
2601
|
+
// briefly exits the iframe and re-enters before releasing. The host
|
|
2602
|
+
// already clears cross-screen state on re-entry so checking the
|
|
2603
|
+
// momentary excursion flag here would wrongly drop the element nowhere.
|
|
2604
|
+
if (outsideOnDrop) return;
|
|
1608
2605
|
if (!currentTarget) {
|
|
1609
2606
|
// No valid drop target — clean up the clone if one was inserted so
|
|
1610
2607
|
// no ghost element is left in the DOM.
|
|
@@ -1630,9 +2627,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1630
2627
|
applyRuntimeReorder(reorderEl, currentTarget);
|
|
1631
2628
|
postVisualStructureChange(reorderEl, currentTarget, { prevParent: prevParent, prevNextSibling: prevNextSibling });
|
|
1632
2629
|
}
|
|
1633
|
-
|
|
1634
|
-
document.addEventListener(
|
|
1635
|
-
document.addEventListener(
|
|
2630
|
+
}
|
|
2631
|
+
document.addEventListener(events.move, onReorderMove, true);
|
|
2632
|
+
document.addEventListener(events.up, onReorderUp, true);
|
|
2633
|
+
document.addEventListener('keydown', onReorderKeyDown, true);
|
|
1636
2634
|
return;
|
|
1637
2635
|
}
|
|
1638
2636
|
ensurePositionable(selectedEl);
|
|
@@ -1659,8 +2657,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1659
2657
|
refreshOverlays();
|
|
1660
2658
|
}
|
|
1661
2659
|
function onUp() {
|
|
1662
|
-
document.removeEventListener(
|
|
1663
|
-
document.removeEventListener(
|
|
2660
|
+
document.removeEventListener(events.move, onMove, true);
|
|
2661
|
+
document.removeEventListener(events.up, onUp, true);
|
|
1664
2662
|
hideTransformBadge();
|
|
1665
2663
|
if (!dragEl) return;
|
|
1666
2664
|
if (duplicatedForDrag && !moved) {
|
|
@@ -1686,8 +2684,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1686
2684
|
}, '*');
|
|
1687
2685
|
}
|
|
1688
2686
|
}
|
|
1689
|
-
document.addEventListener(
|
|
1690
|
-
document.addEventListener(
|
|
2687
|
+
document.addEventListener(events.move, onMove, true);
|
|
2688
|
+
document.addEventListener(events.up, onUp, true);
|
|
1691
2689
|
}
|
|
1692
2690
|
|
|
1693
2691
|
function startResize(handle, e) {
|
|
@@ -1864,7 +2862,68 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1864
2862
|
document.addEventListener('mouseup', onUp, true);
|
|
1865
2863
|
}
|
|
1866
2864
|
|
|
2865
|
+
function clearPendingShieldDrag() {
|
|
2866
|
+
if (!pendingShieldDrag) return;
|
|
2867
|
+
document.removeEventListener(pendingShieldDrag.move, pendingShieldDrag.onMove, true);
|
|
2868
|
+
document.removeEventListener(pendingShieldDrag.up, pendingShieldDrag.onUp, true);
|
|
2869
|
+
pendingShieldDrag = null;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
function beginPotentialShieldDrag(e) {
|
|
2873
|
+
stopNativeInteraction(e);
|
|
2874
|
+
if (e.button !== 0 || activeTextEditEl) return;
|
|
2875
|
+
var events = dragEventNames(e);
|
|
2876
|
+
var hit = elementFromEditorPoint(e.clientX, e.clientY);
|
|
2877
|
+
if (!hit || hit === document.body || hit === document.documentElement) return;
|
|
2878
|
+
var dragTarget =
|
|
2879
|
+
selectedEl &&
|
|
2880
|
+
document.documentElement.contains(selectedEl) &&
|
|
2881
|
+
selectedEl.contains(hit)
|
|
2882
|
+
? selectedEl
|
|
2883
|
+
: selectionTargetForHit(hit);
|
|
2884
|
+
if (
|
|
2885
|
+
!dragTarget ||
|
|
2886
|
+
dragTarget === document.body ||
|
|
2887
|
+
dragTarget === document.documentElement ||
|
|
2888
|
+
isLayerInteractionBlocked(dragTarget)
|
|
2889
|
+
) {
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
var startX = e.clientX;
|
|
2893
|
+
var startY = e.clientY;
|
|
2894
|
+
var didStartDrag = false;
|
|
2895
|
+
function selectDragTarget() {
|
|
2896
|
+
selectedEl = dragTarget;
|
|
2897
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2898
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
2899
|
+
}
|
|
2900
|
+
function onMove(ev) {
|
|
2901
|
+
if (Math.hypot(ev.clientX - startX, ev.clientY - startY) <= 3) return;
|
|
2902
|
+
clearPendingShieldDrag();
|
|
2903
|
+
didStartDrag = true;
|
|
2904
|
+
selectDragTarget();
|
|
2905
|
+
suppressNextShieldClickBriefly();
|
|
2906
|
+
startMove(ev);
|
|
2907
|
+
}
|
|
2908
|
+
function onUp(ev) {
|
|
2909
|
+
clearPendingShieldDrag();
|
|
2910
|
+
if (didStartDrag) return;
|
|
2911
|
+
if (ev) stopNativeInteraction(ev);
|
|
2912
|
+
selectDragTarget();
|
|
2913
|
+
suppressNextShieldClickBriefly();
|
|
2914
|
+
}
|
|
2915
|
+
clearPendingShieldDrag();
|
|
2916
|
+
pendingShieldDrag = { move: events.move, up: events.up, onMove: onMove, onUp: onUp };
|
|
2917
|
+
document.addEventListener(events.move, onMove, true);
|
|
2918
|
+
document.addEventListener(events.up, onUp, true);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
1867
2921
|
selectionOverlay.addEventListener('mousedown', function(e) {
|
|
2922
|
+
var spacingKey = e.target && e.target.getAttribute && e.target.getAttribute('data-spacing-key');
|
|
2923
|
+
if (spacingKey) {
|
|
2924
|
+
startSpacingDrag(spacingKey, e);
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
1868
2927
|
var resizeHandle = e.target && e.target.getAttribute && e.target.getAttribute('data-agent-native-edit-handle');
|
|
1869
2928
|
if (!resizeHandle && e.target && e.target.getAttribute) {
|
|
1870
2929
|
resizeHandle = e.target.getAttribute('data-agent-native-edge-handle');
|
|
@@ -1881,10 +2940,23 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1881
2940
|
startMove(e);
|
|
1882
2941
|
}, true);
|
|
1883
2942
|
|
|
2943
|
+
shieldOverlay.addEventListener('pointerdown', beginPotentialShieldDrag, true);
|
|
2944
|
+
|
|
1884
2945
|
['pointerdown','pointerup','mousedown','mouseup','auxclick'].forEach(function(type) {
|
|
1885
2946
|
shieldOverlay.addEventListener(type, stopNativeInteraction, true);
|
|
1886
2947
|
});
|
|
1887
2948
|
|
|
2949
|
+
function stopBlockedLayerInteraction(e) {
|
|
2950
|
+
if (isOverlayElement(e.target)) return;
|
|
2951
|
+
var target = e.target && e.target.nodeType === 1 ? e.target : null;
|
|
2952
|
+
if (!target || !isLayerInteractionBlocked(target)) return;
|
|
2953
|
+
stopNativeInteraction(e);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
['pointerdown','pointerup','mousedown','mouseup','click','auxclick'].forEach(function(type) {
|
|
2957
|
+
document.addEventListener(type, stopBlockedLayerInteraction, true);
|
|
2958
|
+
});
|
|
2959
|
+
|
|
1888
2960
|
shieldOverlay.addEventListener('click', selectElementAtEvent, true);
|
|
1889
2961
|
shieldOverlay.addEventListener('contextmenu', openContextMenuAtEvent, true);
|
|
1890
2962
|
selectionOverlay.addEventListener('contextmenu', openContextMenuAtEvent, true);
|
|
@@ -2050,10 +3122,25 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2050
3122
|
hoveredEl = elementFromEditorPoint(e.clientX, e.clientY);
|
|
2051
3123
|
if (!hoveredEl) {
|
|
2052
3124
|
highlightOverlay.style.display = 'none';
|
|
3125
|
+
if (!spacingDrag) {
|
|
3126
|
+
selectedSpacingHovered = false;
|
|
3127
|
+
hoveredSpacingHandleKey = '';
|
|
3128
|
+
updateSpacingOverlay(selectedEl);
|
|
3129
|
+
}
|
|
2053
3130
|
hideMeasurements();
|
|
2054
3131
|
return;
|
|
2055
3132
|
}
|
|
2056
3133
|
if (hoveredEl && hoveredEl.closest('[data-agent-native-text-editing]')) return;
|
|
3134
|
+
if (!spacingDrag) {
|
|
3135
|
+
selectedSpacingHovered = Boolean(
|
|
3136
|
+
selectedEl &&
|
|
3137
|
+
hoveredEl &&
|
|
3138
|
+
(hoveredEl === selectedEl ||
|
|
3139
|
+
(selectedEl.contains && selectedEl.contains(hoveredEl))),
|
|
3140
|
+
);
|
|
3141
|
+
if (!selectedSpacingHovered) hoveredSpacingHandleKey = '';
|
|
3142
|
+
updateSpacingOverlay(selectedEl);
|
|
3143
|
+
}
|
|
2057
3144
|
if (hoveredEl === selectedEl) {
|
|
2058
3145
|
highlightOverlay.style.display = 'none';
|
|
2059
3146
|
} else {
|
|
@@ -2071,6 +3158,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2071
3158
|
shieldOverlay.addEventListener('pointerleave', function(e) {
|
|
2072
3159
|
stopNativeInteraction(e);
|
|
2073
3160
|
hoveredEl = null;
|
|
3161
|
+
if (!spacingDrag) {
|
|
3162
|
+
selectedSpacingHovered = false;
|
|
3163
|
+
hoveredSpacingHandleKey = '';
|
|
3164
|
+
updateSpacingOverlay(selectedEl);
|
|
3165
|
+
}
|
|
2074
3166
|
highlightOverlay.style.display = 'none';
|
|
2075
3167
|
hideMeasurements();
|
|
2076
3168
|
window.parent.postMessage({ type: 'element-hover', payload: null }, '*');
|
|
@@ -2083,6 +3175,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2083
3175
|
window.addEventListener('message', function(e) {
|
|
2084
3176
|
if (e.source !== window.parent) return;
|
|
2085
3177
|
if (!e.data) return;
|
|
3178
|
+
if (e.data.payload && typeof e.data.payload === 'object') {
|
|
3179
|
+
Object.keys(e.data.payload).forEach(function(key) {
|
|
3180
|
+
if (e.data[key] === undefined) e.data[key] = e.data.payload[key];
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
2086
3183
|
if (e.data.type === 'set-editor-chrome-scale') {
|
|
2087
3184
|
// Live-update the constant-size chrome scale WITHOUT rebuilding srcdoc.
|
|
2088
3185
|
// Rebuilding srcdoc reloads the iframe and flashes the content white.
|
|
@@ -2130,6 +3227,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2130
3227
|
} catch (_err) {}
|
|
2131
3228
|
}
|
|
2132
3229
|
if (!target) return;
|
|
3230
|
+
selectedSpacingHovered = false;
|
|
3231
|
+
hoveredSpacingHandleKey = '';
|
|
2133
3232
|
selectedEl = target;
|
|
2134
3233
|
positionOverlay(selectionOverlay, target);
|
|
2135
3234
|
if (hoveredEl === selectedEl) highlightOverlay.style.display = 'none';
|
|
@@ -2169,6 +3268,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2169
3268
|
if (selectedEl && isLayerInteractionBlocked(selectedEl)) {
|
|
2170
3269
|
selectedEl = null;
|
|
2171
3270
|
selectionOverlay.style.display = 'none';
|
|
3271
|
+
clearComponentTag();
|
|
2172
3272
|
}
|
|
2173
3273
|
if (hoveredEl && isLayerInteractionBlocked(hoveredEl)) {
|
|
2174
3274
|
hoveredEl = null;
|
|
@@ -2200,7 +3300,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2200
3300
|
return;
|
|
2201
3301
|
}
|
|
2202
3302
|
if (e.data.type === 'replace-document-content') {
|
|
2203
|
-
replaceRuntimeDocument(
|
|
3303
|
+
replaceRuntimeDocument(
|
|
3304
|
+
e.data.content,
|
|
3305
|
+
e.data.forceFullDocument ? '' : e.data.selectedSelector,
|
|
3306
|
+
e.data.forceFullDocument ? [] : e.data.selectorCandidates
|
|
3307
|
+
);
|
|
2204
3308
|
return;
|
|
2205
3309
|
}
|
|
2206
3310
|
if (e.data.type === 'delete-element') {
|
|
@@ -2211,13 +3315,63 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2211
3315
|
var sel = e.data.selector;
|
|
2212
3316
|
var prop = e.data.property;
|
|
2213
3317
|
var val = e.data.value;
|
|
2214
|
-
var
|
|
2215
|
-
if (
|
|
3318
|
+
var candidatesForStyle = Array.isArray(e.data.selectorCandidates) ? e.data.selectorCandidates : [];
|
|
3319
|
+
if (sel && candidatesForStyle.indexOf(String(sel)) === -1) candidatesForStyle.push(String(sel));
|
|
3320
|
+
if (e.data.nodeId) {
|
|
3321
|
+
candidatesForStyle.push('[data-agent-native-node-id="' + String(e.data.nodeId).replace(/"/g, '\\\\"') + '"]');
|
|
3322
|
+
}
|
|
3323
|
+
var el = findRuntimeTarget(String(sel || ''), candidatesForStyle);
|
|
3324
|
+
if (prop && activeTextEditEl && el === activeTextEditEl && applyTextRangeStyle(prop, val)) {
|
|
2216
3325
|
postTextContentChange(activeTextEditEl, activeTextEditEl.textContent || '', activeTextEditEl.innerHTML || '');
|
|
2217
3326
|
refreshOverlays();
|
|
2218
3327
|
return;
|
|
2219
3328
|
}
|
|
2220
|
-
if (el)
|
|
3329
|
+
if (!el) return;
|
|
3330
|
+
var didPatchDom = false;
|
|
3331
|
+
var attributeOverrides = e.data.attributeOverrides;
|
|
3332
|
+
if (attributeOverrides && typeof attributeOverrides === 'object' && !Array.isArray(attributeOverrides)) {
|
|
3333
|
+
Object.keys(attributeOverrides).forEach(function(name) {
|
|
3334
|
+
if (!/^(?!on)[a-zA-Z][a-zA-Z0-9:_.-]*$/i.test(name)) return;
|
|
3335
|
+
var nextValue = attributeOverrides[name];
|
|
3336
|
+
if (nextValue === null || nextValue === undefined || nextValue === false) {
|
|
3337
|
+
el.removeAttribute(name);
|
|
3338
|
+
} else {
|
|
3339
|
+
el.setAttribute(name, String(nextValue));
|
|
3340
|
+
}
|
|
3341
|
+
didPatchDom = true;
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
var classEdit = e.data.classEdit;
|
|
3345
|
+
if (classEdit && typeof classEdit === 'object') {
|
|
3346
|
+
var currentClass = (el.getAttribute('class') || '').trim().split(/\\s+/).filter(Boolean);
|
|
3347
|
+
var nextClass = currentClass.slice();
|
|
3348
|
+
if (classEdit.operation === 'replace' && classEdit.from && classEdit.to) {
|
|
3349
|
+
var replaced = false;
|
|
3350
|
+
nextClass = currentClass.map(function(token) {
|
|
3351
|
+
if (token === classEdit.from) {
|
|
3352
|
+
replaced = true;
|
|
3353
|
+
return String(classEdit.to);
|
|
3354
|
+
}
|
|
3355
|
+
return token;
|
|
3356
|
+
});
|
|
3357
|
+
if (!replaced && nextClass.indexOf(String(classEdit.to)) === -1) nextClass.push(String(classEdit.to));
|
|
3358
|
+
didPatchDom = true;
|
|
3359
|
+
} else if (classEdit.operation === 'add' && classEdit.className) {
|
|
3360
|
+
if (nextClass.indexOf(String(classEdit.className)) === -1) nextClass.push(String(classEdit.className));
|
|
3361
|
+
didPatchDom = true;
|
|
3362
|
+
} else if (classEdit.operation === 'remove' && classEdit.className) {
|
|
3363
|
+
nextClass = currentClass.filter(function(token) { return token !== String(classEdit.className); });
|
|
3364
|
+
didPatchDom = true;
|
|
3365
|
+
}
|
|
3366
|
+
if (didPatchDom) el.setAttribute('class', nextClass.join(' '));
|
|
3367
|
+
}
|
|
3368
|
+
if (prop && typeof prop === 'string') {
|
|
3369
|
+
applyInlineStyleProperty(el, prop, val);
|
|
3370
|
+
didPatchDom = true;
|
|
3371
|
+
}
|
|
3372
|
+
if (didPatchDom) {
|
|
3373
|
+
refreshOverlays();
|
|
3374
|
+
}
|
|
2221
3375
|
});
|
|
2222
3376
|
|
|
2223
3377
|
window.addEventListener('scroll', refreshOverlays, true);
|
|
@@ -2230,6 +3384,41 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2230
3384
|
interface DesignCanvasProps {
|
|
2231
3385
|
content: string;
|
|
2232
3386
|
contentKey?: string;
|
|
3387
|
+
/**
|
|
3388
|
+
* The runtime source tier for this canvas.
|
|
3389
|
+
*
|
|
3390
|
+
* - `"inline"` (default) — HTML/Alpine `srcdoc` iframe; same-origin null
|
|
3391
|
+
* origin; all bridge scripts injected by DesignCanvas.
|
|
3392
|
+
* - `"localhost"` — `src=devServerUrl`; dev server is same-origin in most
|
|
3393
|
+
* setups; bridge trust: origin must match parent or be "null".
|
|
3394
|
+
* - `"fusion"` — `src=builderHostedUrl`; cross-origin Builder-hosted app;
|
|
3395
|
+
* bridge trust is relaxed to window-identity only (no origin check) so
|
|
3396
|
+
* the Builder-hosted iframe can communicate with the editor. The sandbox
|
|
3397
|
+
* grants `allow-same-origin` so the Builder app can reach its own resources.
|
|
3398
|
+
*
|
|
3399
|
+
* When omitted, DesignCanvas infers the tier from the content value:
|
|
3400
|
+
* a value that passes `getExternalPreviewUrl` is treated as `"localhost"`;
|
|
3401
|
+
* otherwise `"inline"`. Pass `sourceType="fusion"` explicitly when the
|
|
3402
|
+
* content URL is a Builder-hosted (cross-origin) app so the bridge security
|
|
3403
|
+
* model uses window-identity trust instead of same-origin trust.
|
|
3404
|
+
*/
|
|
3405
|
+
sourceType?: "inline" | "localhost" | "fusion";
|
|
3406
|
+
/**
|
|
3407
|
+
* Explicit Builder-hosted app URL for fusion source rendering.
|
|
3408
|
+
*
|
|
3409
|
+
* When `sourceType === "fusion"` and this prop is provided, the iframe uses
|
|
3410
|
+
* this URL as `src` regardless of what `content` contains. This lets the
|
|
3411
|
+
* caller hold the original inline HTML in `content` (for collab/history
|
|
3412
|
+
* purposes) while pointing the canvas at the migrated Builder-hosted app.
|
|
3413
|
+
*
|
|
3414
|
+
* When absent and `sourceType === "fusion"`, the component falls back to
|
|
3415
|
+
* the existing external-URL detection on `content` (i.e. if `content` is
|
|
3416
|
+
* itself a URL it is used as-is, which is the pattern when the branch URL
|
|
3417
|
+
* has been written into the design file content).
|
|
3418
|
+
*
|
|
3419
|
+
* For `"inline"` and `"localhost"` sources this prop is ignored.
|
|
3420
|
+
*/
|
|
3421
|
+
fusionUrl?: string;
|
|
2233
3422
|
zoom: number;
|
|
2234
3423
|
onZoomChange?: (zoom: number) => void;
|
|
2235
3424
|
deviceFrame: DeviceFrameType;
|
|
@@ -2322,6 +3511,55 @@ interface DesignCanvasProps {
|
|
|
2322
3511
|
* opened in a new tab by the iframe itself and never reach this callback.
|
|
2323
3512
|
*/
|
|
2324
3513
|
onPrototypeNavigate?: (screen: string, href: string) => void;
|
|
3514
|
+
/**
|
|
3515
|
+
* Motion tracks to load into the iframe's motion-preview bridge. Sent via
|
|
3516
|
+
* `motion-load-tracks` whenever this prop changes. When cleared
|
|
3517
|
+
* (`undefined` or `[]`) a `motion-preview-clear` message is sent to remove
|
|
3518
|
+
* any applied preview overrides.
|
|
3519
|
+
*
|
|
3520
|
+
* The MotionDock sends scrub ticks as `{ type: 'motion-preview', t,
|
|
3521
|
+
* durationMs }` directly from its `canvasIframeRef`. DesignCanvas only
|
|
3522
|
+
* needs the tracks so the bridge can interpolate values at each tick.
|
|
3523
|
+
*/
|
|
3524
|
+
motionTracks?: MotionTrackWire[];
|
|
3525
|
+
/**
|
|
3526
|
+
* Explicit iframe width in pixels. When provided it overrides the width
|
|
3527
|
+
* derived from `deviceFrame`, enabling per-breakpoint preview (e.g. Mobile
|
|
3528
|
+
* 390 / Tablet 768 / Desktop 1280 side-by-side frames in the overview).
|
|
3529
|
+
* The height still comes from `deviceFrame`; `deviceFrame="none"` keeps
|
|
3530
|
+
* 100% height.
|
|
3531
|
+
*/
|
|
3532
|
+
previewWidthPx?: number;
|
|
3533
|
+
/**
|
|
3534
|
+
* Shader-fill CSS preview to apply to a selected element inside the iframe.
|
|
3535
|
+
*
|
|
3536
|
+
* When set, the canvas sends a `shader-fill-preview` bridge message that
|
|
3537
|
+
* applies the CSS `background` value on the target element **without
|
|
3538
|
+
* persisting anything**. When cleared (`null` / `undefined`) a
|
|
3539
|
+
* `shader-fill-preview-clear` message is sent to restore the original
|
|
3540
|
+
* background.
|
|
3541
|
+
*
|
|
3542
|
+
* Preview-only — never writes to DB, Yjs, or source. Part of the §6.7
|
|
3543
|
+
* shader-fill PREVIEW path; the apply path remains gated until runtime
|
|
3544
|
+
* rendering + source-write + diff proof are all in place.
|
|
3545
|
+
*/
|
|
3546
|
+
shaderFillPreview?: {
|
|
3547
|
+
/** CSS selector for the target element (preferred over nodeId). */
|
|
3548
|
+
selector?: string;
|
|
3549
|
+
/** data-agent-native-node-id value for the target element. */
|
|
3550
|
+
nodeId?: string;
|
|
3551
|
+
/** The CSS `background` value returned by preview-shader-fill. */
|
|
3552
|
+
css: string;
|
|
3553
|
+
} | null;
|
|
3554
|
+
/**
|
|
3555
|
+
* Called when the user clicks the component-instance source tag (the
|
|
3556
|
+
* "ComponentName →" pill that floats above a selected component root).
|
|
3557
|
+
* The parent should invoke `open-component-source` with these params.
|
|
3558
|
+
*/
|
|
3559
|
+
onComponentSourceJump?: (params: {
|
|
3560
|
+
nodeId: string;
|
|
3561
|
+
componentName: string;
|
|
3562
|
+
}) => void;
|
|
2325
3563
|
}
|
|
2326
3564
|
|
|
2327
3565
|
function getExternalPreviewUrl(content: string): string | null {
|
|
@@ -2357,6 +3595,8 @@ export interface IframeContextMenuPayload {
|
|
|
2357
3595
|
export function DesignCanvas({
|
|
2358
3596
|
content,
|
|
2359
3597
|
contentKey,
|
|
3598
|
+
sourceType,
|
|
3599
|
+
fusionUrl,
|
|
2360
3600
|
zoom,
|
|
2361
3601
|
onZoomChange,
|
|
2362
3602
|
deviceFrame,
|
|
@@ -2396,6 +3636,10 @@ export function DesignCanvas({
|
|
|
2396
3636
|
commentContextId,
|
|
2397
3637
|
commentContextLabel,
|
|
2398
3638
|
onPrototypeNavigate,
|
|
3639
|
+
motionTracks,
|
|
3640
|
+
previewWidthPx,
|
|
3641
|
+
onComponentSourceJump,
|
|
3642
|
+
shaderFillPreview,
|
|
2399
3643
|
}: DesignCanvasProps) {
|
|
2400
3644
|
const t = useT();
|
|
2401
3645
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
@@ -2406,10 +3650,25 @@ export function DesignCanvas({
|
|
|
2406
3650
|
const [annotationPins, setAnnotationPins] = useState<CanvasPin[]>([]);
|
|
2407
3651
|
const [pinSubmitSignal, setPinSubmitSignal] = useState(0);
|
|
2408
3652
|
const isEmbeddedFrame = Boolean(embeddedFrame);
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
)
|
|
3653
|
+
// Resolve the URL to render in the iframe:
|
|
3654
|
+
// 1. When sourceType === "fusion" and fusionUrl is set, prefer the explicit
|
|
3655
|
+
// Builder-hosted URL over whatever is in `content` (which may still be the
|
|
3656
|
+
// original inline HTML).
|
|
3657
|
+
// 2. Otherwise fall back to the content-based URL detection (handles the case
|
|
3658
|
+
// where the branch URL has been written into the design file content, or
|
|
3659
|
+
// where the localhost URL is the file content).
|
|
3660
|
+
const externalPreviewUrl = useMemo(() => {
|
|
3661
|
+
if (sourceType === "fusion" && fusionUrl) {
|
|
3662
|
+
try {
|
|
3663
|
+
const url = new URL(fusionUrl);
|
|
3664
|
+
url.hash = "";
|
|
3665
|
+
return url.toString();
|
|
3666
|
+
} catch {
|
|
3667
|
+
// fall through to content detection below
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
return getExternalPreviewUrl(renderedContent);
|
|
3671
|
+
}, [fusionUrl, renderedContent, sourceType]);
|
|
2413
3672
|
zoomRef.current = zoom;
|
|
2414
3673
|
|
|
2415
3674
|
const queuedAnnotationPins = useMemo(
|
|
@@ -2443,7 +3702,8 @@ export function DesignCanvas({
|
|
|
2443
3702
|
});
|
|
2444
3703
|
|
|
2445
3704
|
// Build the srcdoc. The tweak bridge ALWAYS goes in so the panel works
|
|
2446
|
-
// outside Edit mode. The editor chrome bridge is omitted
|
|
3705
|
+
// outside Edit mode. The editor chrome bridge is omitted for Interact and
|
|
3706
|
+
// read-only surfaces so preview/app users can interact with the app normally.
|
|
2447
3707
|
const srcdoc = useMemo(() => {
|
|
2448
3708
|
if (externalPreviewUrl) return undefined;
|
|
2449
3709
|
const editorChromeBridge =
|
|
@@ -2462,6 +3722,8 @@ export function DesignCanvas({
|
|
|
2462
3722
|
isEmbeddedFrame ? "true" : "false",
|
|
2463
3723
|
);
|
|
2464
3724
|
const bridgeToInject =
|
|
3725
|
+
MOTION_PREVIEW_BRIDGE_SCRIPT +
|
|
3726
|
+
SHADER_FILL_PREVIEW_BRIDGE_SCRIPT +
|
|
2465
3727
|
TWEAK_BRIDGE_SCRIPT +
|
|
2466
3728
|
ZOOM_BRIDGE_SCRIPT +
|
|
2467
3729
|
NAV_BRIDGE_SCRIPT +
|
|
@@ -2492,14 +3754,27 @@ export function DesignCanvas({
|
|
|
2492
3754
|
// Listen for messages from the iframe
|
|
2493
3755
|
useEffect(() => {
|
|
2494
3756
|
function handleMessage(e: MessageEvent) {
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
3757
|
+
const iframeWindow = iframeRef.current?.contentWindow;
|
|
3758
|
+
// For fusion sources the Builder-hosted app is cross-origin, so the strict
|
|
3759
|
+
// `origin === parentOrigin` check can never match. We still require window
|
|
3760
|
+
// identity (the message must come from our own iframe window, not any
|
|
3761
|
+
// arbitrary cross-origin frame), AND we validate the message origin
|
|
3762
|
+
// against a Builder-host allowlist (the configured fusionUrl origin or the
|
|
3763
|
+
// *.builder.io family) before relaxing the origin check. If the origin is
|
|
3764
|
+
// not on the allowlist we keep the strict check so a hostile frame that
|
|
3765
|
+
// somehow shares our window reference still can't be trusted.
|
|
3766
|
+
const trusted =
|
|
3767
|
+
sourceType === "fusion"
|
|
3768
|
+
? iframeWindow != null &&
|
|
3769
|
+
e.source === iframeWindow &&
|
|
3770
|
+
isAllowedFusionOrigin(e.origin, fusionUrl)
|
|
3771
|
+
: isTrustedCanvasBridgeMessage({
|
|
3772
|
+
source: e.source,
|
|
3773
|
+
origin: e.origin,
|
|
3774
|
+
iframeWindow,
|
|
3775
|
+
parentOrigin: window.location.origin,
|
|
3776
|
+
});
|
|
3777
|
+
if (!trusted) {
|
|
2503
3778
|
return;
|
|
2504
3779
|
}
|
|
2505
3780
|
if (!e.data || !e.data.type) return;
|
|
@@ -2540,9 +3815,15 @@ export function DesignCanvas({
|
|
|
2540
3815
|
const placement = String(e.data.placement || "after");
|
|
2541
3816
|
const requestId =
|
|
2542
3817
|
typeof e.data.requestId === "string" ? e.data.requestId : undefined;
|
|
3818
|
+
const sourceId =
|
|
3819
|
+
typeof e.data.sourceId === "string" ? e.data.sourceId : undefined;
|
|
3820
|
+
const anchorSourceId =
|
|
3821
|
+
typeof e.data.anchorSourceId === "string"
|
|
3822
|
+
? e.data.anchorSourceId
|
|
3823
|
+
: undefined;
|
|
2543
3824
|
if (
|
|
2544
|
-
selector &&
|
|
2545
|
-
anchorSelector &&
|
|
3825
|
+
(selector || sourceId) &&
|
|
3826
|
+
(anchorSelector || anchorSourceId) &&
|
|
2546
3827
|
(placement === "before" ||
|
|
2547
3828
|
placement === "after" ||
|
|
2548
3829
|
placement === "inside")
|
|
@@ -2554,14 +3835,8 @@ export function DesignCanvas({
|
|
|
2554
3835
|
e.data.payload,
|
|
2555
3836
|
{
|
|
2556
3837
|
requestId,
|
|
2557
|
-
sourceId
|
|
2558
|
-
|
|
2559
|
-
? e.data.sourceId
|
|
2560
|
-
: undefined,
|
|
2561
|
-
anchorSourceId:
|
|
2562
|
-
typeof e.data.anchorSourceId === "string"
|
|
2563
|
-
? e.data.anchorSourceId
|
|
2564
|
-
: undefined,
|
|
3838
|
+
sourceId,
|
|
3839
|
+
anchorSourceId,
|
|
2565
3840
|
},
|
|
2566
3841
|
);
|
|
2567
3842
|
if (requestId) {
|
|
@@ -2663,6 +3938,16 @@ export function DesignCanvas({
|
|
|
2663
3938
|
);
|
|
2664
3939
|
return;
|
|
2665
3940
|
}
|
|
3941
|
+
if (e.data.type === "component-source-jump") {
|
|
3942
|
+
// The user clicked the component-instance tag ("ComponentName →").
|
|
3943
|
+
// Relay to the parent so it can invoke open-component-source.
|
|
3944
|
+
const nodeId = String(e.data.nodeId || "");
|
|
3945
|
+
const componentName = String(e.data.componentName || "");
|
|
3946
|
+
if (nodeId && componentName) {
|
|
3947
|
+
onComponentSourceJump?.({ nodeId, componentName });
|
|
3948
|
+
}
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
2666
3951
|
if (e.data.type === "embedded-canvas-wheel") {
|
|
2667
3952
|
if (!isEmbeddedFrame) return;
|
|
2668
3953
|
const iframe = iframeRef.current;
|
|
@@ -2748,7 +4033,10 @@ export function DesignCanvas({
|
|
|
2748
4033
|
onZoomChange,
|
|
2749
4034
|
deviceFrame,
|
|
2750
4035
|
onPrototypeNavigate,
|
|
4036
|
+
onComponentSourceJump,
|
|
2751
4037
|
isEmbeddedFrame,
|
|
4038
|
+
sourceType,
|
|
4039
|
+
fusionUrl,
|
|
2752
4040
|
]);
|
|
2753
4041
|
|
|
2754
4042
|
const replayIframeEditorState = useCallback(() => {
|
|
@@ -2786,14 +4074,43 @@ export function DesignCanvas({
|
|
|
2786
4074
|
: { type: "hover-element", selector: "", selectorCandidates: [] },
|
|
2787
4075
|
"*",
|
|
2788
4076
|
);
|
|
4077
|
+
// Re-send motion tracks so the preview bridge is ready after a reload.
|
|
4078
|
+
if (motionTracks && motionTracks.length > 0) {
|
|
4079
|
+
iframe.contentWindow?.postMessage(
|
|
4080
|
+
{ type: "motion-load-tracks", tracks: motionTracks },
|
|
4081
|
+
"*",
|
|
4082
|
+
);
|
|
4083
|
+
} else {
|
|
4084
|
+
iframe.contentWindow?.postMessage({ type: "motion-preview-clear" }, "*");
|
|
4085
|
+
}
|
|
4086
|
+
// Re-apply the shader-fill preview after a reload so the preview survives
|
|
4087
|
+
// screen switches. Preview-only — never writes to DB, Yjs, or source.
|
|
4088
|
+
if (shaderFillPreview) {
|
|
4089
|
+
iframe.contentWindow?.postMessage(
|
|
4090
|
+
{
|
|
4091
|
+
type: "shader-fill-preview",
|
|
4092
|
+
selector: shaderFillPreview.selector ?? "",
|
|
4093
|
+
nodeId: shaderFillPreview.nodeId ?? "",
|
|
4094
|
+
css: shaderFillPreview.css,
|
|
4095
|
+
},
|
|
4096
|
+
"*",
|
|
4097
|
+
);
|
|
4098
|
+
} else {
|
|
4099
|
+
iframe.contentWindow?.postMessage(
|
|
4100
|
+
{ type: "shader-fill-preview-clear" },
|
|
4101
|
+
"*",
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
2789
4104
|
}, [
|
|
2790
4105
|
hoveredSelector,
|
|
2791
4106
|
hoveredSelectorCandidates,
|
|
2792
4107
|
hiddenSelectors,
|
|
2793
4108
|
lockedSelectors,
|
|
4109
|
+
motionTracks,
|
|
2794
4110
|
scaleMode,
|
|
2795
4111
|
selectedSelector,
|
|
2796
4112
|
selectedSelectorCandidates,
|
|
4113
|
+
shaderFillPreview,
|
|
2797
4114
|
tweakValues,
|
|
2798
4115
|
]);
|
|
2799
4116
|
|
|
@@ -2816,6 +4133,44 @@ export function DesignCanvas({
|
|
|
2816
4133
|
);
|
|
2817
4134
|
}, [clearSelectionRequest]);
|
|
2818
4135
|
|
|
4136
|
+
// Sync motion tracks to the iframe bridge whenever they change.
|
|
4137
|
+
// When motionTracks is empty/undefined, clear any preview overrides so the
|
|
4138
|
+
// design returns to its authored state (no stale inline styles).
|
|
4139
|
+
useEffect(() => {
|
|
4140
|
+
const win = iframeRef.current?.contentWindow;
|
|
4141
|
+
if (!win) return;
|
|
4142
|
+
if (!motionTracks || motionTracks.length === 0) {
|
|
4143
|
+
win.postMessage({ type: "motion-preview-clear" }, "*");
|
|
4144
|
+
} else {
|
|
4145
|
+
win.postMessage(
|
|
4146
|
+
{ type: "motion-load-tracks", tracks: motionTracks },
|
|
4147
|
+
"*",
|
|
4148
|
+
);
|
|
4149
|
+
}
|
|
4150
|
+
}, [motionTracks]);
|
|
4151
|
+
|
|
4152
|
+
// Sync shader-fill preview to the iframe whenever the prop changes.
|
|
4153
|
+
// When cleared (null / undefined) send a clear message so the bridge
|
|
4154
|
+
// restores the original background on the previously-patched element.
|
|
4155
|
+
// Preview-only — never writes to DB, Yjs, or source.
|
|
4156
|
+
useEffect(() => {
|
|
4157
|
+
const win = iframeRef.current?.contentWindow;
|
|
4158
|
+
if (!win) return;
|
|
4159
|
+
if (!shaderFillPreview) {
|
|
4160
|
+
win.postMessage({ type: "shader-fill-preview-clear" }, "*");
|
|
4161
|
+
} else {
|
|
4162
|
+
win.postMessage(
|
|
4163
|
+
{
|
|
4164
|
+
type: "shader-fill-preview",
|
|
4165
|
+
selector: shaderFillPreview.selector ?? "",
|
|
4166
|
+
nodeId: shaderFillPreview.nodeId ?? "",
|
|
4167
|
+
css: shaderFillPreview.css,
|
|
4168
|
+
},
|
|
4169
|
+
"*",
|
|
4170
|
+
);
|
|
4171
|
+
}
|
|
4172
|
+
}, [shaderFillPreview]);
|
|
4173
|
+
|
|
2819
4174
|
// Push the constant-size chrome scale into the iframe LIVE (CSS vars only) when
|
|
2820
4175
|
// overview zoom settles. This is intentionally separate from the srcdoc build so
|
|
2821
4176
|
// a scale change never rebuilds srcdoc / reloads the iframe (which flashes the
|
|
@@ -2832,17 +4187,84 @@ export function DesignCanvas({
|
|
|
2832
4187
|
}, [editorChromeScaleX, editorChromeScaleY]);
|
|
2833
4188
|
|
|
2834
4189
|
const sendStyleChange = useCallback(
|
|
2835
|
-
(
|
|
4190
|
+
(
|
|
4191
|
+
selector: string,
|
|
4192
|
+
property: string,
|
|
4193
|
+
value: string,
|
|
4194
|
+
options?: { selectorCandidates?: string[]; nodeId?: string | null },
|
|
4195
|
+
) => {
|
|
2836
4196
|
const iframe = iframeRef.current;
|
|
2837
4197
|
if (!iframe?.contentWindow) return;
|
|
2838
4198
|
iframe.contentWindow.postMessage(
|
|
2839
|
-
{
|
|
4199
|
+
{
|
|
4200
|
+
type: "style-change",
|
|
4201
|
+
selector,
|
|
4202
|
+
property,
|
|
4203
|
+
value,
|
|
4204
|
+
selectorCandidates: options?.selectorCandidates ?? [],
|
|
4205
|
+
nodeId: options?.nodeId ?? "",
|
|
4206
|
+
},
|
|
4207
|
+
"*",
|
|
4208
|
+
);
|
|
4209
|
+
},
|
|
4210
|
+
[],
|
|
4211
|
+
);
|
|
4212
|
+
|
|
4213
|
+
/**
|
|
4214
|
+
* Send a motion-preview scrub tick to the iframe. `t` is the normalised
|
|
4215
|
+
* playhead position in [0, 1]. Tracks must have been loaded first via the
|
|
4216
|
+
* `motionTracks` prop (or an explicit `motion-load-tracks` message).
|
|
4217
|
+
* Preview-only — never writes to DB/Yjs/source.
|
|
4218
|
+
*/
|
|
4219
|
+
const sendMotionPreview = useCallback((t: number, durationMs?: number) => {
|
|
4220
|
+
const iframe = iframeRef.current;
|
|
4221
|
+
if (!iframe?.contentWindow) return;
|
|
4222
|
+
iframe.contentWindow.postMessage(
|
|
4223
|
+
{ type: "motion-preview", t: Math.max(0, Math.min(1, t)), durationMs },
|
|
4224
|
+
"*",
|
|
4225
|
+
);
|
|
4226
|
+
}, []);
|
|
4227
|
+
|
|
4228
|
+
/**
|
|
4229
|
+
* Clear all motion-preview inline-style overrides in the iframe and remove
|
|
4230
|
+
* the in-memory track list. Call when the Motion dock is closed.
|
|
4231
|
+
*/
|
|
4232
|
+
const clearMotionPreview = useCallback(() => {
|
|
4233
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
4234
|
+
{ type: "motion-preview-clear" },
|
|
4235
|
+
"*",
|
|
4236
|
+
);
|
|
4237
|
+
}, []);
|
|
4238
|
+
|
|
4239
|
+
/**
|
|
4240
|
+
* Send a shader-fill CSS preview to the iframe. Targets the element
|
|
4241
|
+
* identified by `selector` (preferred) or `nodeId`. Preview-only — the
|
|
4242
|
+
* bridge script restores the original background on clear.
|
|
4243
|
+
*/
|
|
4244
|
+
const sendShaderFillPreview = useCallback(
|
|
4245
|
+
(selector: string, nodeId: string, css: string) => {
|
|
4246
|
+
const win = iframeRef.current?.contentWindow;
|
|
4247
|
+
if (!win) return;
|
|
4248
|
+
win.postMessage(
|
|
4249
|
+
{ type: "shader-fill-preview", selector, nodeId, css },
|
|
2840
4250
|
"*",
|
|
2841
4251
|
);
|
|
2842
4252
|
},
|
|
2843
4253
|
[],
|
|
2844
4254
|
);
|
|
2845
4255
|
|
|
4256
|
+
/**
|
|
4257
|
+
* Clear the shader-fill preview in the iframe, restoring the original
|
|
4258
|
+
* background on the patched element. Call when the preview is dismissed
|
|
4259
|
+
* or when the selection changes to a different element.
|
|
4260
|
+
*/
|
|
4261
|
+
const clearShaderFillPreview = useCallback(() => {
|
|
4262
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
4263
|
+
{ type: "shader-fill-preview-clear" },
|
|
4264
|
+
"*",
|
|
4265
|
+
);
|
|
4266
|
+
}, []);
|
|
4267
|
+
|
|
2846
4268
|
const replacePreviewContent = useCallback(
|
|
2847
4269
|
(nextContent: string, selector?: string | null, candidates?: string[]) => {
|
|
2848
4270
|
const iframe = iframeRef.current;
|
|
@@ -2884,7 +4306,15 @@ export function DesignCanvas({
|
|
|
2884
4306
|
(window as any).__designCanvasSendStyle = sendStyleChange;
|
|
2885
4307
|
(window as any).__designCanvasReplaceContent = replacePreviewContent;
|
|
2886
4308
|
(window as any).__designCanvasDeleteElement = deleteRuntimeElement;
|
|
4309
|
+
(window as any).__designCanvasSendMotionPreview = sendMotionPreview;
|
|
4310
|
+
(window as any).__designCanvasClearMotionPreview = clearMotionPreview;
|
|
4311
|
+
// Shader-fill preview helpers (preview-only, §6.7 gating applies to apply).
|
|
4312
|
+
(window as any).__designCanvasSendShaderFillPreview = sendShaderFillPreview;
|
|
4313
|
+
(window as any).__designCanvasClearShaderFillPreview =
|
|
4314
|
+
clearShaderFillPreview;
|
|
2887
4315
|
return () => {
|
|
4316
|
+
// Identity-guard each delete so a stale unmounting instance never clobbers
|
|
4317
|
+
// a freshly mounted instance's bridge during a remount race.
|
|
2888
4318
|
if ((window as any).__designCanvasSendStyle === sendStyleChange) {
|
|
2889
4319
|
delete (window as any).__designCanvasSendStyle;
|
|
2890
4320
|
}
|
|
@@ -2898,12 +4328,38 @@ export function DesignCanvas({
|
|
|
2898
4328
|
) {
|
|
2899
4329
|
delete (window as any).__designCanvasDeleteElement;
|
|
2900
4330
|
}
|
|
4331
|
+
if (
|
|
4332
|
+
(window as any).__designCanvasSendMotionPreview === sendMotionPreview
|
|
4333
|
+
) {
|
|
4334
|
+
delete (window as any).__designCanvasSendMotionPreview;
|
|
4335
|
+
}
|
|
4336
|
+
if (
|
|
4337
|
+
(window as any).__designCanvasClearMotionPreview === clearMotionPreview
|
|
4338
|
+
) {
|
|
4339
|
+
delete (window as any).__designCanvasClearMotionPreview;
|
|
4340
|
+
}
|
|
4341
|
+
if (
|
|
4342
|
+
(window as any).__designCanvasSendShaderFillPreview ===
|
|
4343
|
+
sendShaderFillPreview
|
|
4344
|
+
) {
|
|
4345
|
+
delete (window as any).__designCanvasSendShaderFillPreview;
|
|
4346
|
+
}
|
|
4347
|
+
if (
|
|
4348
|
+
(window as any).__designCanvasClearShaderFillPreview ===
|
|
4349
|
+
clearShaderFillPreview
|
|
4350
|
+
) {
|
|
4351
|
+
delete (window as any).__designCanvasClearShaderFillPreview;
|
|
4352
|
+
}
|
|
2901
4353
|
};
|
|
2902
4354
|
}, [
|
|
2903
4355
|
deleteRuntimeElement,
|
|
2904
4356
|
registerRuntimeBridge,
|
|
2905
4357
|
replacePreviewContent,
|
|
2906
4358
|
sendStyleChange,
|
|
4359
|
+
sendMotionPreview,
|
|
4360
|
+
clearMotionPreview,
|
|
4361
|
+
sendShaderFillPreview,
|
|
4362
|
+
clearShaderFillPreview,
|
|
2907
4363
|
]);
|
|
2908
4364
|
|
|
2909
4365
|
// Device dimensions match real-world devices. iframes are replaced elements
|
|
@@ -2923,6 +4379,12 @@ export function DesignCanvas({
|
|
|
2923
4379
|
deviceDimensions[deviceFrame];
|
|
2924
4380
|
const embeddedFrameFluid = embeddedFrame?.fluid === true;
|
|
2925
4381
|
|
|
4382
|
+
// Per-breakpoint override: when previewWidthPx is set it takes priority over
|
|
4383
|
+
// the deviceFrame width so the caller can render the same source at an
|
|
4384
|
+
// explicit viewport width (e.g. 390 / 768 / 1280 side-by-side breakpoints).
|
|
4385
|
+
const resolvedWidth =
|
|
4386
|
+
previewWidthPx != null ? `${previewWidthPx}px` : iframeWidth;
|
|
4387
|
+
|
|
2926
4388
|
// Wrap the iframe in a positioned container so DrawOverlay /
|
|
2927
4389
|
// CanvasCommentPins can absolutely-position themselves on top of the
|
|
2928
4390
|
// iframe. The pin component anchors to `.design-canvas-iframe-wrapper`
|
|
@@ -2938,7 +4400,7 @@ export function DesignCanvas({
|
|
|
2938
4400
|
? embeddedFrameFluid
|
|
2939
4401
|
? "100%"
|
|
2940
4402
|
: embeddedFrame.viewportWidth
|
|
2941
|
-
:
|
|
4403
|
+
: resolvedWidth,
|
|
2942
4404
|
height: embeddedFrame
|
|
2943
4405
|
? embeddedFrameFluid
|
|
2944
4406
|
? "100%"
|
|
@@ -2958,6 +4420,12 @@ export function DesignCanvas({
|
|
|
2958
4420
|
: "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
2959
4421
|
}
|
|
2960
4422
|
data-design-preview-iframe
|
|
4423
|
+
data-design-source-type={
|
|
4424
|
+
sourceType ??
|
|
4425
|
+
(externalPreviewUrl
|
|
4426
|
+
? "localhost" // inferred — content is a URL
|
|
4427
|
+
: "inline")
|
|
4428
|
+
}
|
|
2961
4429
|
className="block h-full w-full border-0 bg-transparent"
|
|
2962
4430
|
title={t("designEditor.designPreview")}
|
|
2963
4431
|
/>
|