@agent-native/core 0.80.11 → 0.81.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +419 -19
  5. package/corpus/core/src/client/AgentPanel.tsx +33 -1
  6. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  7. package/corpus/core/src/client/chat/index.ts +1 -0
  8. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  9. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  10. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  11. package/corpus/core/src/client/index.ts +1 -0
  12. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  13. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  14. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  15. package/corpus/core/src/db/client.ts +29 -2
  16. package/corpus/core/src/provider-api/index.ts +29 -0
  17. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  18. package/corpus/core/src/styles/agent-native.css +54 -0
  19. package/corpus/core/src/usage/store.ts +2 -2
  20. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  21. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  22. package/corpus/templates/analytics/app/global.css +8 -0
  23. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  24. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  25. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  26. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  27. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  28. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  29. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  34. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  35. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  36. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  37. package/corpus/templates/assets/app/global.css +15 -0
  38. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  39. package/corpus/templates/assets/app/routes/library.tsx +90 -16
  40. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  41. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  42. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  43. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  44. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  46. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  47. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  48. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  49. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  50. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  51. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  52. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  53. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  54. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  56. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  57. package/corpus/templates/design/AGENTS.md +27 -0
  58. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  59. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  60. package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
  61. package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
  62. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  63. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  64. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  65. package/corpus/templates/design/actions/export-zip.ts +23 -8
  66. package/corpus/templates/design/actions/generate-design.ts +53 -1
  67. package/corpus/templates/design/actions/get-component-details.ts +5 -19
  68. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  69. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  70. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  71. package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
  72. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  73. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  74. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  75. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  76. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  77. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  78. package/corpus/templates/design/actions/navigate.ts +18 -3
  79. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  80. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  81. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  82. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  83. package/corpus/templates/design/actions/run-design-audit.ts +53 -4
  84. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  85. package/corpus/templates/design/actions/view-screen.ts +1 -1
  86. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  87. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
  88. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  89. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
  90. package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
  91. package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
  92. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  93. package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
  94. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
  95. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
  96. package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
  97. package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
  98. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  99. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  100. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  101. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  102. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  103. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  104. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  105. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  106. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  107. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  108. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  109. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  110. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  111. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  112. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  113. package/corpus/templates/design/app/global.css +7 -0
  114. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  115. package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
  116. package/corpus/templates/design/app/i18n-data.ts +358 -0
  117. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  118. package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
  119. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  120. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  121. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  122. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  123. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  124. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  125. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  126. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  127. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  128. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  129. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  134. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  144. package/corpus/templates/design/e2e/helpers.ts +168 -65
  145. package/corpus/templates/design/package.json +3 -1
  146. package/corpus/templates/design/server/db/schema.ts +23 -0
  147. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  148. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  149. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  150. package/corpus/templates/design/server/plugins/db.ts +39 -0
  151. package/corpus/templates/design/server/register-secrets.ts +38 -0
  152. package/corpus/templates/design/shared/board-file.ts +228 -0
  153. package/corpus/templates/design/shared/board-objects.ts +288 -0
  154. package/corpus/templates/design/shared/code-layer.ts +52 -0
  155. package/corpus/templates/design/shared/motion-compiler.ts +58 -7
  156. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  157. package/corpus/templates/design/tsconfig.json +2 -1
  158. package/corpus/templates/slides/AGENTS.md +9 -0
  159. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  160. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  161. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  162. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  163. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  164. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  165. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  166. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  167. package/corpus/templates/slides/package.json +1 -0
  168. package/corpus/templates/slides/vite.config.ts +1 -0
  169. package/dist/cli/design-connect.d.ts +25 -0
  170. package/dist/cli/design-connect.d.ts.map +1 -1
  171. package/dist/cli/design-connect.js +357 -18
  172. package/dist/cli/design-connect.js.map +1 -1
  173. package/dist/client/AgentPanel.d.ts +1 -1
  174. package/dist/client/AgentPanel.d.ts.map +1 -1
  175. package/dist/client/AgentPanel.js +18 -3
  176. package/dist/client/AgentPanel.js.map +1 -1
  177. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  178. package/dist/client/ConnectBuilderCard.js +1 -0
  179. package/dist/client/ConnectBuilderCard.js.map +1 -1
  180. package/dist/client/chat/index.d.ts +1 -1
  181. package/dist/client/chat/index.d.ts.map +1 -1
  182. package/dist/client/chat/index.js +1 -1
  183. package/dist/client/chat/index.js.map +1 -1
  184. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  185. package/dist/client/chat/run-recovery.js +5 -5
  186. package/dist/client/chat/run-recovery.js.map +1 -1
  187. package/dist/client/chat-view-transition.d.ts +6 -0
  188. package/dist/client/chat-view-transition.d.ts.map +1 -1
  189. package/dist/client/chat-view-transition.js +23 -2
  190. package/dist/client/chat-view-transition.js.map +1 -1
  191. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  192. package/dist/client/composer/PromptComposer.js +3 -2
  193. package/dist/client/composer/PromptComposer.js.map +1 -1
  194. package/dist/client/index.d.ts +1 -1
  195. package/dist/client/index.d.ts.map +1 -1
  196. package/dist/client/index.js +1 -1
  197. package/dist/client/index.js.map +1 -1
  198. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  199. package/dist/client/org/OrgSwitcher.js +11 -4
  200. package/dist/client/org/OrgSwitcher.js.map +1 -1
  201. package/dist/client/sharing/ShareButton.js +3 -3
  202. package/dist/client/sharing/ShareButton.js.map +1 -1
  203. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  204. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  205. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  206. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  207. package/dist/collab/routes.d.ts +1 -1
  208. package/dist/db/client.d.ts +1 -0
  209. package/dist/db/client.d.ts.map +1 -1
  210. package/dist/db/client.js +28 -2
  211. package/dist/db/client.js.map +1 -1
  212. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  213. package/dist/notifications/routes.d.ts +2 -2
  214. package/dist/observability/routes.d.ts +8 -8
  215. package/dist/progress/routes.d.ts +1 -1
  216. package/dist/provider-api/index.d.ts +5 -4
  217. package/dist/provider-api/index.d.ts.map +1 -1
  218. package/dist/provider-api/index.js +28 -0
  219. package/dist/provider-api/index.js.map +1 -1
  220. package/dist/resources/handlers.d.ts +3 -3
  221. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  222. package/dist/server/core-routes-plugin.d.ts +25 -0
  223. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  224. package/dist/server/core-routes-plugin.js +43 -18
  225. package/dist/server/core-routes-plugin.js.map +1 -1
  226. package/dist/server/transcribe-voice.d.ts +1 -1
  227. package/dist/styles/agent-native.css +54 -0
  228. package/dist/usage/store.js +2 -2
  229. package/dist/usage/store.js.map +1 -1
  230. package/package.json +1 -1
@@ -11,6 +11,14 @@ import {
11
11
  type CanvasPin,
12
12
  } from "@/components/visual-editor";
13
13
 
14
+ import { editorChromeBridgeScript } from "../../../.generated/bridge/editor-chrome.generated";
15
+ import { embeddedWheelBridgeScript } from "../../../.generated/bridge/embedded-wheel.generated";
16
+ import { hitTestBridgeScript } from "../../../.generated/bridge/hit-test.generated";
17
+ import { motionPreviewBridgeScript } from "../../../.generated/bridge/motion-preview.generated";
18
+ import { navBridgeScript } from "../../../.generated/bridge/nav.generated";
19
+ import { shaderFillPreviewBridgeScript } from "../../../.generated/bridge/shader-fill-preview.generated";
20
+ import { tweakBridgeScript } from "../../../.generated/bridge/tweak.generated";
21
+ import { zoomBridgeScript } from "../../../.generated/bridge/zoom.generated";
14
22
  import { isTrustedCanvasBridgeMessage } from "./bridge-security";
15
23
  import { DeviceFrame } from "./DeviceFrame";
16
24
  import type { ElementInfo, DeviceFrameType } from "./types";
@@ -70,3316 +78,180 @@ export interface MotionTrackWire {
70
78
  * MotionDock's scrubbing preview works in ALL editor modes without writing
71
79
  * anything to the DB, Yjs state, or source files.
72
80
  *
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
-
271
- /**
272
- * Tweak-bridge script. ALWAYS injected so the parent's postMessage
273
- * (`tweak-values`) can update CSS custom properties on the iframe's :root
274
- * regardless of which editor mode is active. Without this the tweak panel
275
- * silently no-ops in the default Comment mode.
276
- */
277
- const TWEAK_BRIDGE_SCRIPT = `
278
- <script data-agent-native-tweak-bridge>
279
- (function() {
280
- window.addEventListener('message', function(e) {
281
- if (e.source !== window.parent) return;
282
- if (!e.data || e.data.type !== 'tweak-values') return;
283
- var root = document.documentElement;
284
- var vals = e.data.values || {};
285
- Object.keys(vals).forEach(function(k) {
286
- root.style.setProperty(k, vals[k]);
287
- });
288
- });
289
- })();
290
- </script>
291
- `;
292
-
293
- /**
294
- * Pinch-zoom bridge: forwards trackpad pinch / Cmd-Ctrl+scroll wheel events
295
- * from inside the iframe to the parent window. Wheel events don't naturally
296
- * bubble out of an iframe, so without this the user can only pinch in the
297
- * empty area around the canvas, not over the design itself.
298
- */
299
- const ZOOM_BRIDGE_SCRIPT = `
300
- <script data-agent-native-zoom-bridge>
301
- (function() {
302
- // Attach to documentElement (not window/document) so { passive: false }
303
- // is honored consistently and the browser doesn't natively pinch-zoom the
304
- // iframe's own document alongside the parent's zoom.
305
- var target = document.documentElement || document.body || document;
306
- function onWheel(e) {
307
- if (!(e.ctrlKey || e.metaKey)) return;
308
- e.preventDefault();
309
- try {
310
- window.parent.postMessage({
311
- type: 'pinch-zoom-wheel',
312
- deltaY: e.deltaY,
313
- clientX: e.clientX,
314
- clientY: e.clientY,
315
- }, '*');
316
- } catch (err) {}
317
- }
318
- target.addEventListener('wheel', onWheel, { passive: false, capture: true });
319
- })();
320
- </script>
321
- `;
322
-
323
- /**
324
- * Embedded overview bridge. A screen preview is a real iframe, so normal wheel
325
- * events never bubble to the overview canvas underneath. In embedded mode we
326
- * forward a bounded wheel payload to the parent so the existing canvas wheel
327
- * handler can pan/zoom exactly as if the pointer were over empty canvas.
328
- */
329
- const EMBEDDED_WHEEL_BRIDGE_SCRIPT = `
330
- <script data-agent-native-embedded-wheel-bridge>
331
- (function() {
332
- var enabled = __EMBEDDED_WHEEL_FORWARDING_ENABLED__;
333
- if (!enabled) return;
334
- function clamp(value, limit) {
335
- var number = Number(value) || 0;
336
- if (number > limit) return limit;
337
- if (number < -limit) return -limit;
338
- return number;
339
- }
340
- function onWheel(e) {
341
- e.preventDefault();
342
- e.stopPropagation();
343
- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
344
- try {
345
- window.parent.postMessage({
346
- type: 'embedded-canvas-wheel',
347
- deltaX: clamp(e.deltaX, 240),
348
- deltaY: clamp(e.deltaY, 240),
349
- deltaZ: clamp(e.deltaZ, 240),
350
- deltaMode: e.deltaMode,
351
- clientX: e.clientX,
352
- clientY: e.clientY,
353
- ctrlKey: !!e.ctrlKey,
354
- metaKey: !!e.metaKey,
355
- shiftKey: !!e.shiftKey,
356
- altKey: !!e.altKey,
357
- }, '*');
358
- } catch (err) {}
359
- }
360
- var target = document.documentElement || document.body || document;
361
- target.addEventListener('wheel', onWheel, { passive: false, capture: true });
362
- })();
363
- </script>
364
- `;
365
-
366
- /**
367
- * Navigation bridge. ALWAYS injected. A prototype lives in a `srcdoc` iframe,
368
- * so a plain `<a href="/pricing">` resolves the relative URL against the PARENT
369
- * app document and navigates the iframe to the Design app itself ("Design not
370
- * found"), nuking the prototype. We intercept link clicks + relative form
371
- * submits and route them to the parent instead:
372
- * - in-page anchors (`#...`) and `javascript:`/`@click` handlers: left alone
373
- * - external `http(s)`/`//` links: opened in a new tab by the parent
374
- * - internal/relative links (or an explicit `data-screen`): asked to switch
375
- * to the matching screen in a multi-screen design; otherwise a no-op so the
376
- * prototype never blows itself away.
377
- */
378
- const NAV_BRIDGE_SCRIPT = `
379
- <script data-agent-native-nav-bridge>
380
- (function() {
381
- function classify(href) {
382
- var h = (href || '').trim();
383
- if (!h) return null;
384
- var lower = h.toLowerCase();
385
- if (lower.charAt(0) === '#') return null;
386
- if (lower.indexOf('javascript:') === 0) return null;
387
- if (lower.indexOf('mailto:') === 0 || lower.indexOf('tel:') === 0) {
388
- return { external: true, href: h };
389
- }
390
- if (/^https?:\\/\\//i.test(h) || /^\\/\\//.test(h)) {
391
- return { external: true, href: h };
392
- }
393
- var screen = h.replace(/^\\.?\\//, '').split(/[?#]/)[0];
394
- return { external: false, href: h, screen: screen };
395
- }
396
- document.addEventListener('click', function(e) {
397
- var t = e.target;
398
- if (!t || !t.closest) return;
399
- var a = t.closest('a[href], [data-screen]');
400
- if (!a) return;
401
- var ds = a.getAttribute && a.getAttribute('data-screen');
402
- // In-page anchors ('#...') and empty hrefs must be handled in-document.
403
- // A srcdoc document resolves '#'/'' against the PARENT app URL, so the
404
- // browser's default action would navigate the iframe to the app itself.
405
- if (!ds) {
406
- var rawHref = a.getAttribute('href');
407
- if (rawHref != null) {
408
- var hh = rawHref.trim();
409
- if (hh === '' || hh.charAt(0) === '#') {
410
- e.preventDefault();
411
- var fid = hh.charAt(0) === '#' ? hh.slice(1) : '';
412
- var tgt = fid ? document.getElementById(fid) : null;
413
- if (tgt && tgt.scrollIntoView) {
414
- tgt.scrollIntoView({ behavior: 'smooth', block: 'start' });
415
- } else {
416
- window.scrollTo({ top: 0, behavior: 'smooth' });
417
- }
418
- return;
419
- }
420
- }
421
- }
422
- var info = ds
423
- ? { external: false, href: ds, screen: ds.replace(/^\\.?\\//, '').split(/[?#]/)[0] }
424
- : classify(a.getAttribute('href'));
425
- if (!info) return;
426
- if (info.external) {
427
- // Open external links in a new tab from the iframe itself (the sandbox
428
- // grants allow-popups), bound to this real user click. We deliberately do
429
- // NOT round-trip through the parent: a parent window.open() driven by
430
- // postMessage would let any script in here spawn popups without a gesture.
431
- try {
432
- a.setAttribute('target', '_blank');
433
- a.setAttribute('rel', 'noopener noreferrer');
434
- } catch (err) {}
435
- return; // allow the native click to proceed
436
- }
437
- e.preventDefault();
438
- try {
439
- window.parent.postMessage({
440
- type: 'prototype-navigate',
441
- href: info.href,
442
- screen: info.screen || '',
443
- }, '*');
444
- } catch (err) {}
445
- }, true);
446
- document.addEventListener('submit', function(e) {
447
- var f = e.target;
448
- if (!f || f.tagName !== 'FORM') return;
449
- var action = f.getAttribute('action') || '';
450
- if (/^https?:\\/\\//i.test(action)) return;
451
- e.preventDefault();
452
- }, true);
453
- })();
454
- </script>
455
- `;
456
-
457
- const EDITOR_BRIDGE_VAR_NAMES = [
458
- "--design-editor-accent-color",
459
- "--design-editor-accent-hover-color",
460
- "--design-editor-selection-color",
461
- "--design-editor-accent-strong-color",
462
- "--design-editor-accent-contrast-color",
463
- "--design-editor-measure-color",
464
- "--background",
465
- "--foreground",
466
- "--border",
467
- ];
468
-
469
- function readEditorBridgeThemeVars(): Record<string, string> {
470
- if (typeof window === "undefined") return {};
471
- const styles = window.getComputedStyle(document.documentElement);
472
- return Object.fromEntries(
473
- EDITOR_BRIDGE_VAR_NAMES.map((name) => [
474
- name,
475
- styles.getPropertyValue(name).trim(),
476
- ]).filter(([, value]) => value.length > 0),
477
- );
478
- }
479
-
480
- function createEditorBridgeThemeScript(vars: Record<string, string>) {
481
- const serializedVars = JSON.stringify(vars).replace(/</g, "\\u003c");
482
- return `
483
- <script data-agent-native-editor-theme>
484
- (function() {
485
- var vars = ${serializedVars};
486
- var root = document.documentElement;
487
- Object.keys(vars).forEach(function(name) {
488
- root.style.setProperty(name, vars[name]);
489
- });
490
- })();
491
- </script>
492
- `;
493
- }
494
-
495
- /**
496
- * Editor chrome bridge: blocks native iframe app interaction outside Interact
497
- * mode and replaces it with element hover/selection overlays. Double-click text
498
- * editing is enabled only while the editor is specifically in Edit mode.
499
- */
500
- const EDITOR_CHROME_BRIDGE_SCRIPT = `
501
- <script data-agent-native-editor-chrome-bridge>
502
- (function() {
503
- var readOnly = __READ_ONLY__;
504
- var textEditingEnabled = !readOnly && __TEXT_EDITING_ENABLED__;
505
- var scaleToolEnabled = false;
506
- var editorChromeScaleX = Math.max(0.05, Number(__EDITOR_CHROME_SCALE_X__) || 1);
507
- var editorChromeScaleY = Math.max(0.05, Number(__EDITOR_CHROME_SCALE_Y__) || editorChromeScaleX);
508
-
509
- // Ease the constant-size selection chrome to its new size when overview zoom
510
- // settles (parent posts set-editor-chrome-scale), matching the canvas chrome.
511
- // Only chrome-scale-driven props animate; the overlay's live position is excluded.
512
- (function () {
513
- var chromeTransitionStyle = document.createElement('style');
514
- chromeTransitionStyle.textContent =
515
- '[data-agent-native-edit-overlay="selection"]{transition:border-width 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}';
521
- (document.head || document.documentElement).appendChild(chromeTransitionStyle);
522
- })();
523
-
524
- function chromeScaleX() {
525
- return 1 / Math.max(0.05, editorChromeScaleX);
526
- }
527
-
528
- function chromeScaleY() {
529
- return 1 / Math.max(0.05, editorChromeScaleY);
530
- }
531
-
532
- function chromeLineScale() {
533
- return 1 / Math.max(0.05, Math.max(editorChromeScaleX, editorChromeScaleY));
534
- }
535
-
536
- function syncEditorChromeScaleVars() {
537
- document.documentElement.style.setProperty('--agent-native-editor-chrome-scale-x', String(chromeScaleX()));
538
- document.documentElement.style.setProperty('--agent-native-editor-chrome-scale-y', String(chromeScaleY()));
539
- document.documentElement.style.setProperty('--agent-native-editor-chrome-line-scale', String(chromeLineScale()));
540
- }
541
-
542
- function escapeIdent(value) {
543
- if (window.CSS && typeof window.CSS.escape === 'function') {
544
- return window.CSS.escape(value);
545
- }
546
- return String(value).replace(/[^a-zA-Z0-9_-]/g, '\\\\$&');
547
- }
548
-
549
- function escapeAttribute(value) {
550
- return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
551
- }
552
-
553
- function attributeSelector(el, name) {
554
- var value = el && el.getAttribute && el.getAttribute(name);
555
- return value ? '[' + name + '="' + escapeAttribute(value) + '"]' : '';
556
- }
557
-
558
- function classSelectorSuffix(el, maxCount) {
559
- if (!el || !el.classList) return '';
560
- return Array.prototype.slice.call(el.classList, 0, maxCount)
561
- .map(function(token) { return '.' + escapeIdent(token); })
562
- .join('');
563
- }
564
-
565
- function selectorPart(el) {
566
- if (!el || !el.tagName) return '';
567
- var stableSelector =
568
- attributeSelector(el, 'data-agent-native-node-id') ||
569
- attributeSelector(el, 'data-code-layer-id') ||
570
- attributeSelector(el, 'data-layer-id') ||
571
- attributeSelector(el, 'data-builder-id') ||
572
- attributeSelector(el, 'data-loc');
573
- if (stableSelector) return el.tagName.toLowerCase() + stableSelector;
574
- if (el.id) return '#' + escapeIdent(el.id);
575
- var part = el.tagName.toLowerCase() + (stableSelector || classSelectorSuffix(el, 2));
576
- var parent = el.parentElement;
577
- if (parent) {
578
- var sameTag = Array.prototype.filter.call(
579
- parent.children,
580
- function(child) { return child.tagName === el.tagName; }
581
- );
582
- if (sameTag.length > 1) {
583
- part += ':nth-of-type(' + (sameTag.indexOf(el) + 1) + ')';
584
- }
585
- }
586
- return part;
587
- }
588
-
589
- function selectorPath(el, stopEl) {
590
- var parts = [];
591
- var node = el;
592
- while (node && node.nodeType === 1) {
593
- if (node !== stopEl) parts.unshift(selectorPart(node));
594
- if (node === stopEl) break;
595
- node = node.parentElement;
596
- }
597
- return parts.slice(-5).join(' > ');
598
- }
599
-
600
- function getSourceId(el) {
601
- if (!el || !el.getAttribute) return '';
602
- return (
603
- el.getAttribute('data-agent-native-node-id') ||
604
- el.getAttribute('data-code-layer-id') ||
605
- el.getAttribute('data-layer-id') ||
606
- el.getAttribute('data-builder-id') ||
607
- el.getAttribute('data-loc') ||
608
- el.id ||
609
- ''
610
- );
611
- }
612
-
613
- function isDocumentRootElement(el) {
614
- return el === document.body || el === document.documentElement;
615
- }
616
-
617
- function closestStableSourceElement(el) {
618
- if (!el || !el.closest) return null;
619
- var stable = el.closest('[data-agent-native-node-id],[data-code-layer-id],[data-layer-id],[data-builder-id],[data-loc]');
620
- if (!stable || isDocumentRootElement(stable)) return null;
621
- return stable;
622
- }
623
-
624
- function hasStableOwnSource(el) {
625
- return !!(
626
- el &&
627
- !isDocumentRootElement(el) &&
628
- getSourceId(el)
629
- );
630
- }
631
-
632
- function selectionTargetForHit(hit) {
633
- if (!hit || isDocumentRootElement(hit)) return hit;
634
- if (selectedEl && hit !== selectedEl && selectedEl.contains(hit)) return hit;
635
- if (hasStableOwnSource(hit)) return hit;
636
- return closestStableSourceElement(hit) || hit;
637
- }
638
-
639
- function freshRuntimeNodeId(prefix) {
640
- var random = '';
641
- try {
642
- if (window.crypto && window.crypto.getRandomValues) {
643
- var bytes = new Uint32Array(2);
644
- window.crypto.getRandomValues(bytes);
645
- random = Array.prototype.map.call(bytes, function(part) {
646
- return part.toString(36);
647
- }).join('');
648
- }
649
- } catch (_err) {}
650
- if (!random) random = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
651
- return 'an-' + String(prefix || 'copy') + '-' + random;
652
- }
653
-
654
- function resetRuntimeStableIds(root) {
655
- if (!root || !root.querySelectorAll) return;
656
- var nodes = [root].concat(Array.prototype.slice.call(root.querySelectorAll('[data-agent-native-node-id]')));
657
- nodes.forEach(function(node, index) {
658
- if (node && node.setAttribute) {
659
- node.setAttribute('data-agent-native-node-id', freshRuntimeNodeId(index === 0 ? 'copy' : 'copy-child'));
660
- }
661
- });
662
- }
663
-
664
- function getSelector(el) {
665
- var stableOwnSelector =
666
- attributeSelector(el, 'data-agent-native-node-id') ||
667
- attributeSelector(el, 'data-code-layer-id') ||
668
- attributeSelector(el, 'data-layer-id') ||
669
- attributeSelector(el, 'data-builder-id') ||
670
- attributeSelector(el, 'data-loc');
671
- if (stableOwnSelector) return stableOwnSelector;
672
-
673
- if (el.id) return '#' + escapeIdent(el.id);
674
- var stableAncestor = closestStableSourceElement(el);
675
- if (stableAncestor && stableAncestor !== el) {
676
- var stableAncestorSelector = selectorPart(stableAncestor);
677
- if (stableAncestorSelector) {
678
- var descendantPath = selectorPath(el, stableAncestor);
679
- var descendantParts = descendantPath ? descendantPath.split(' > ') : [];
680
- if (descendantParts.length) {
681
- return stableAncestorSelector + ' > ' + descendantParts.join(' > ');
682
- }
683
- return stableAncestorSelector;
684
- }
685
- }
686
-
687
- return selectorPath(el);
688
- }
689
-
690
- function getElementInfo(el) {
691
- var cs = window.getComputedStyle(el);
692
- var rect = el.getBoundingClientRect();
693
- var parentStyles = el.parentElement
694
- ? window.getComputedStyle(el.parentElement)
695
- : null;
696
- var parentDisplay = parentStyles ? parentStyles.display : undefined;
697
- var sourceBacked = hasStableOwnSource(el) || !!closestStableSourceElement(el);
698
- var sourceId = sourceBacked ? (getSourceId(el) || getSelector(el)) : '';
699
- var parentLayout = parentStyles
700
- ? {
701
- display: parentStyles.display,
702
- flexDirection: parentStyles.flexDirection,
703
- alignItems: parentStyles.alignItems,
704
- justifyContent: parentStyles.justifyContent,
705
- gap: parentStyles.gap,
706
- gridTemplateColumns: parentStyles.gridTemplateColumns,
707
- gridTemplateRows: parentStyles.gridTemplateRows,
708
- position: parentStyles.position,
709
- }
710
- : undefined;
711
- var capabilities = sourceBacked
712
- ? [
713
- {
714
- kind: 'deterministic-style-edit',
715
- label: 'deterministic-style-edit',
716
- confidence: 0.92,
717
- reason: 'Inline style can be patched and replayed through HMR/collab.',
718
- },
719
- ]
720
- : [
721
- {
722
- kind: 'unsupported',
723
- label: 'runtime-only-element',
724
- confidence: 0.3,
725
- reason: 'This runtime node is not anchored to a source code layer.',
726
- },
727
- ];
728
- if (sourceBacked && el.classList && el.classList.length > 0) {
729
- capabilities.push({
730
- kind: 'deterministic-class-edit',
731
- label: 'deterministic-class-edit',
732
- confidence: 0.78,
733
- reason: 'Class tokens are visible on the selected element.',
734
- });
735
- }
736
- if (sourceBacked && (parentDisplay === 'flex' || parentDisplay === 'inline-flex' || parentDisplay === 'grid' || parentDisplay === 'inline-grid')) {
737
- capabilities.push({
738
- kind: 'agent-structural-edit',
739
- label: 'agent-structural-edit',
740
- confidence: 0.54,
741
- reason: 'Parent layout context decides whether movement means gap, order, alignment, or wrapper structure.',
742
- });
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
- }
776
- return {
777
- tagName: el.tagName.toLowerCase(),
778
- id: el.id || undefined,
779
- sourceId: sourceId,
780
- selector: getSelector(el),
781
- classes: Array.from(el.classList),
782
- computedStyles: {
783
- color: cs.color,
784
- backgroundColor: cs.backgroundColor,
785
- backgroundImage: cs.backgroundImage,
786
- backgroundBlendMode: cs.backgroundBlendMode,
787
- fontSize: cs.fontSize,
788
- fontFamily: cs.fontFamily,
789
- fontWeight: cs.fontWeight,
790
- lineHeight: cs.lineHeight,
791
- letterSpacing: cs.letterSpacing,
792
- textAlign: cs.textAlign,
793
- display: cs.display,
794
- overflow: cs.overflow,
795
- flexDirection: cs.flexDirection,
796
- justifyContent: cs.justifyContent,
797
- alignItems: cs.alignItems,
798
- alignSelf: cs.alignSelf,
799
- flexGrow: cs.flexGrow,
800
- flexShrink: cs.flexShrink,
801
- flexBasis: cs.flexBasis,
802
- order: cs.order,
803
- gridColumn: cs.gridColumn,
804
- gridRow: cs.gridRow,
805
- position: cs.position,
806
- top: cs.top,
807
- right: cs.right,
808
- bottom: cs.bottom,
809
- left: cs.left,
810
- gap: cs.gap,
811
- width: cs.width,
812
- height: cs.height,
813
- opacity: cs.opacity,
814
- paddingTop: cs.paddingTop,
815
- paddingRight: cs.paddingRight,
816
- paddingBottom: cs.paddingBottom,
817
- paddingLeft: cs.paddingLeft,
818
- marginTop: cs.marginTop,
819
- marginRight: cs.marginRight,
820
- marginBottom: cs.marginBottom,
821
- marginLeft: cs.marginLeft,
822
- borderWidth: cs.borderWidth,
823
- borderStyle: cs.borderStyle,
824
- borderColor: cs.borderColor,
825
- borderRadius: cs.borderRadius,
826
- borderTopLeftRadius: cs.borderTopLeftRadius,
827
- borderTopRightRadius: cs.borderTopRightRadius,
828
- borderBottomRightRadius: cs.borderBottomRightRadius,
829
- borderBottomLeftRadius: cs.borderBottomLeftRadius,
830
- outlineWidth: cs.outlineWidth,
831
- outlineStyle: cs.outlineStyle,
832
- outlineColor: cs.outlineColor,
833
- outlineOffset: cs.outlineOffset,
834
- boxShadow: cs.boxShadow,
835
- textShadow: cs.textShadow,
836
- filter: cs.filter,
837
- mixBlendMode: cs.mixBlendMode,
838
- zIndex: cs.zIndex,
839
- },
840
- boundingRect: { x: rect.x + (window.scrollX || window.pageXOffset || 0), y: rect.y + (window.scrollY || window.pageYOffset || 0), width: rect.width, height: rect.height },
841
- textContent: el.textContent ? el.textContent.slice(0, 200) : undefined,
842
- htmlContent: el.innerHTML && el.innerHTML !== el.textContent ? el.innerHTML.slice(0, 4000) : undefined,
843
- isFlexContainer: cs.display === 'flex' || cs.display === 'inline-flex',
844
- isFlexChild: parentDisplay === 'flex' || parentDisplay === 'inline-flex',
845
- parentDisplay: parentDisplay,
846
- parentLayout: parentLayout,
847
- editCapabilities: capabilities,
848
- confidence: capabilities.reduce(function(best, item) {
849
- return Math.max(best, item.confidence || 0);
850
- }, 0),
851
- provenance: provenance,
852
- };
853
- }
854
-
855
- var shieldOverlay = document.createElement('div');
856
- shieldOverlay.setAttribute('data-agent-native-edit-overlay', 'shield');
857
- shieldOverlay.style.cssText = 'position:fixed;inset:0;z-index:99990;background:transparent;pointer-events:auto;touch-action:none;cursor:default;';
858
- document.body.appendChild(shieldOverlay);
859
-
860
- var highlightOverlay = document.createElement('div');
861
- highlightOverlay.setAttribute('data-agent-native-edit-overlay', 'highlight');
862
- highlightOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:99997;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;';
863
- document.body.appendChild(highlightOverlay);
864
-
865
- var selectionOverlay = document.createElement('div');
866
- selectionOverlay.setAttribute('data-agent-native-edit-overlay', 'selection');
867
- selectionOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:99998;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;cursor:default;';
868
- ['n','e','s','w'].forEach(function(pos) {
869
- var edge = document.createElement('span');
870
- edge.setAttribute('data-agent-native-edge-handle', pos);
871
- var cursor = pos === 'n' || pos === 's' ? 'ns-resize' : 'ew-resize';
872
- edge.style.cssText = 'position:absolute;pointer-events:auto;cursor:' + cursor + ';background:transparent;';
873
- if (pos === 'n') {
874
- edge.style.left = '0';
875
- edge.style.right = '0';
876
- edge.style.top = '-5px';
877
- edge.style.height = '10px';
878
- }
879
- if (pos === 's') {
880
- edge.style.left = '0';
881
- edge.style.right = '0';
882
- edge.style.bottom = '-5px';
883
- edge.style.height = '10px';
884
- }
885
- if (pos === 'e') {
886
- edge.style.top = '0';
887
- edge.style.bottom = '0';
888
- edge.style.right = '-5px';
889
- edge.style.width = '10px';
890
- }
891
- if (pos === 'w') {
892
- edge.style.top = '0';
893
- edge.style.bottom = '0';
894
- edge.style.left = '-5px';
895
- edge.style.width = '10px';
896
- }
897
- selectionOverlay.appendChild(edge);
898
- });
899
- ['nw','ne','se','sw'].forEach(function(pos) {
900
- var handle = document.createElement('span');
901
- handle.setAttribute('data-agent-native-edit-handle', pos);
902
- var cursor = pos === 'n' || pos === 's' ? 'ns-resize' : pos === 'e' || pos === 'w' ? 'ew-resize' : pos === 'nw' || pos === 'se' ? 'nwse-resize' : 'nesw-resize';
903
- handle.style.cssText = 'position:absolute;z-index:1;width:7px;height:7px;border:1px solid var(--design-editor-accent-color);background:var(--design-editor-accent-contrast-color);box-sizing:border-box;border-radius:1px;pointer-events:auto;cursor:' + cursor + ';';
904
- if (pos.indexOf('n') !== -1) handle.style.top = '-4px';
905
- if (pos.indexOf('s') !== -1) handle.style.bottom = '-4px';
906
- if (pos.indexOf('w') !== -1) handle.style.left = '-4px';
907
- if (pos.indexOf('e') !== -1) handle.style.right = '-4px';
908
- if (pos === 'n' || pos === 's') {
909
- handle.style.left = '50%';
910
- handle.style.transform = 'translateX(-50%)';
911
- }
912
- if (pos === 'e' || pos === 'w') {
913
- handle.style.top = '50%';
914
- handle.style.transform = 'translateY(-50%)';
915
- }
916
- selectionOverlay.appendChild(handle);
917
- });
918
- ['nw','ne','se','sw'].forEach(function(pos) {
919
- var rotate = document.createElement('span');
920
- rotate.setAttribute('data-agent-native-rotate-handle', pos);
921
- rotate.style.cssText = 'position:absolute;width:18px;height:18px;border-radius:999px;pointer-events:auto;cursor:grab;';
922
- if (pos.indexOf('n') !== -1) rotate.style.top = '-26px';
923
- if (pos.indexOf('s') !== -1) rotate.style.bottom = '-26px';
924
- if (pos.indexOf('w') !== -1) rotate.style.left = '-26px';
925
- if (pos.indexOf('e') !== -1) rotate.style.right = '-26px';
926
- selectionOverlay.appendChild(rotate);
927
- });
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);
932
- document.body.appendChild(selectionOverlay);
933
-
934
- var transformBadge = document.createElement('div');
935
- transformBadge.setAttribute('data-agent-native-transform-badge', '');
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);';
937
- document.body.appendChild(transformBadge);
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
-
944
- var insertionGuide = document.createElement('div');
945
- insertionGuide.setAttribute('data-agent-native-insertion-guide', '');
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);';
947
- document.body.appendChild(insertionGuide);
948
-
949
- var measurementOverlay = document.createElement('div');
950
- measurementOverlay.setAttribute('data-agent-native-measurement-overlay', '');
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;';
952
- document.body.appendChild(measurementOverlay);
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
-
1037
- var selectedEl = null;
1038
- var hoveredEl = null;
1039
- var activeTextEditEl = null;
1040
- var textEditPointerState = null;
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;
1050
- var lockedSelectors = [];
1051
- var hiddenSelectors = [];
1052
-
1053
- function clearRuntimeSelection() {
1054
- selectedEl = null;
1055
- hoveredEl = null;
1056
- selectedSpacingHovered = false;
1057
- hoveredSpacingHandleKey = '';
1058
- spacingDrag = null;
1059
- selectionOverlay.style.display = 'none';
1060
- highlightOverlay.style.display = 'none';
1061
- hideSpacingOverlay();
1062
- hideMeasurements();
1063
- clearComponentTag();
1064
- }
1065
-
1066
- function matchesSelectorList(el, selectors) {
1067
- if (!el || !selectors || selectors.length === 0) return false;
1068
- for (var i = 0; i < selectors.length; i += 1) {
1069
- try {
1070
- if (el.matches(selectors[i]) || el.closest(selectors[i])) return true;
1071
- } catch (_err) {}
1072
- }
1073
- return false;
1074
- }
1075
-
1076
- function matchesExactSelectorList(el, selectors) {
1077
- if (!el || !selectors || selectors.length === 0) return false;
1078
- for (var i = 0; i < selectors.length; i += 1) {
1079
- try {
1080
- if (el.matches(selectors[i])) return true;
1081
- } catch (_err) {}
1082
- }
1083
- return false;
1084
- }
1085
-
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
- }
1091
- return matchesSelectorList(el, lockedSelectors) || matchesSelectorList(el, hiddenSelectors);
1092
- }
1093
-
1094
- function applyHiddenSelectors() {
1095
- document.querySelectorAll('[data-agent-native-runtime-hidden]').forEach(function(el) {
1096
- var previous = el.getAttribute('data-agent-native-previous-display');
1097
- if (previous === null) {
1098
- el.style.removeProperty('display');
1099
- } else {
1100
- el.style.display = previous;
1101
- }
1102
- el.removeAttribute('data-agent-native-runtime-hidden');
1103
- el.removeAttribute('data-agent-native-previous-display');
1104
- });
1105
- hiddenSelectors.forEach(function(selector) {
1106
- try {
1107
- document.querySelectorAll(selector).forEach(function(el) {
1108
- if (!el.hasAttribute('data-agent-native-runtime-hidden')) {
1109
- el.setAttribute('data-agent-native-previous-display', el.style.display || '');
1110
- }
1111
- el.setAttribute('data-agent-native-runtime-hidden', 'true');
1112
- el.style.display = 'none';
1113
- });
1114
- } catch (_err) {}
1115
- });
1116
- }
1117
-
1118
- function replaceRuntimeDocument(html, preferredSelector, selectorCandidates) {
1119
- if (typeof html !== 'string') return;
1120
- if (activeTextEditEl) {
1121
- applyHiddenSelectors();
1122
- refreshOverlays();
1123
- return;
1124
- }
1125
- var parser = new DOMParser();
1126
- var nextDoc = parser.parseFromString(html, 'text/html');
1127
- if (!nextDoc || !nextDoc.body) return;
1128
-
1129
- var persistentNodes = Array.prototype.slice.call(
1130
- document.querySelectorAll('[data-agent-native-edit-overlay]'),
1131
- );
1132
- var activeSelector = preferredSelector || (selectedEl ? getSelector(selectedEl) : '');
1133
- var activeCandidates = [];
1134
- if (Array.isArray(selectorCandidates)) {
1135
- selectorCandidates.forEach(function(selector) {
1136
- if (typeof selector === 'string' && selector && activeCandidates.indexOf(selector) === -1) {
1137
- activeCandidates.push(selector);
1138
- }
1139
- });
1140
- }
1141
- if (activeSelector && activeCandidates.indexOf(activeSelector) === -1) {
1142
- activeCandidates.push(activeSelector);
1143
- }
1144
-
1145
- var nextHeadHtml = nextDoc.head ? nextDoc.head.innerHTML : '';
1146
- if (nextHeadHtml === document.head.innerHTML && activeCandidates.length > 0) {
1147
- var currentMatch = null;
1148
- var nextMatch = null;
1149
- var matchedSelector = '';
1150
- var fallbackCurrentMatch = null;
1151
- var fallbackSelector = '';
1152
- for (var matchIndex = 0; matchIndex < activeCandidates.length; matchIndex += 1) {
1153
- try {
1154
- var currentCandidate = document.querySelector(activeCandidates[matchIndex]);
1155
- var nextCandidate = nextDoc.querySelector(activeCandidates[matchIndex]);
1156
- if (currentCandidate && !fallbackCurrentMatch) {
1157
- fallbackCurrentMatch = currentCandidate;
1158
- fallbackSelector = activeCandidates[matchIndex];
1159
- }
1160
- if (currentCandidate && nextCandidate) {
1161
- currentMatch = currentCandidate;
1162
- nextMatch = nextCandidate;
1163
- matchedSelector = activeCandidates[matchIndex];
1164
- break;
1165
- }
1166
- } catch (_err) {
1167
- // Keep trying later aliases; bridge selectors can differ between
1168
- // runtime and DOMParser passes.
1169
- }
1170
- }
1171
- if (!currentMatch && fallbackCurrentMatch) {
1172
- currentMatch = fallbackCurrentMatch;
1173
- matchedSelector = fallbackSelector;
1174
- }
1175
- if (
1176
- currentMatch &&
1177
- currentMatch !== document.body &&
1178
- currentMatch !== document.documentElement &&
1179
- !isOverlayElement(currentMatch)
1180
- ) {
1181
- if (nextMatch) {
1182
- currentMatch.replaceWith(document.importNode(nextMatch, true));
1183
- } else if (currentMatch !== document.body && currentMatch !== document.documentElement) {
1184
- currentMatch.parentElement && currentMatch.parentElement.removeChild(currentMatch);
1185
- }
1186
- applyHiddenSelectors();
1187
- selectedEl = null;
1188
- if (nextMatch) {
1189
- try {
1190
- selectedEl = document.querySelector(matchedSelector);
1191
- } catch (_err) {}
1192
- }
1193
- hoveredEl = null;
1194
- if (selectedEl && !isLayerInteractionBlocked(selectedEl)) {
1195
- positionOverlay(selectionOverlay, selectedEl);
1196
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
1197
- } else {
1198
- selectionOverlay.style.display = 'none';
1199
- }
1200
- highlightOverlay.style.display = 'none';
1201
- hideMeasurements();
1202
- refreshOverlays();
1203
- return;
1204
- }
1205
- }
1206
- if (document.head.innerHTML !== nextHeadHtml) {
1207
- document.head.innerHTML = nextHeadHtml;
1208
- }
1209
- Array.prototype.slice.call(document.body.attributes).forEach(function(attribute) {
1210
- document.body.removeAttribute(attribute.name);
1211
- });
1212
- Array.prototype.slice.call(nextDoc.body.attributes).forEach(function(attribute) {
1213
- document.body.setAttribute(attribute.name, attribute.value);
1214
- });
1215
- document.body.innerHTML = nextDoc.body.innerHTML;
1216
- persistentNodes.forEach(function(node) {
1217
- document.body.appendChild(node);
1218
- });
1219
- applyHiddenSelectors();
1220
-
1221
- selectedEl = null;
1222
- hoveredEl = null;
1223
- for (var i = 0; i < activeCandidates.length && !selectedEl; i += 1) {
1224
- try {
1225
- var match = document.querySelector(activeCandidates[i]);
1226
- // Skip the editor's own injected overlay chrome and re-anchor to a
1227
- // source-backed element. A stale positional candidate like
1228
- // body > div:nth-of-type(6) can otherwise re-match an overlay div
1229
- // (the only direct div children of body at runtime), which then has
1230
- // no code-layer node and fails every edit.
1231
- if (match && !isLayerInteractionBlocked(match) && !isOverlayElement(match)) {
1232
- selectedEl = selectionTargetForHit(match) || match;
1233
- }
1234
- } catch (_err) {}
1235
- }
1236
- if (selectedEl) {
1237
- positionOverlay(selectionOverlay, selectedEl);
1238
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
1239
- } else {
1240
- selectionOverlay.style.display = 'none';
1241
- }
1242
- highlightOverlay.style.display = 'none';
1243
- hideMeasurements();
1244
- refreshOverlays();
1245
- }
1246
-
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);
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 [];
1508
- var cs = window.getComputedStyle(el);
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
- }
1606
- }
1607
-
1608
- function applyEditorChromeScale() {
1609
- syncEditorChromeScaleVars();
1610
- var sx = chromeScaleX();
1611
- var sy = chromeScaleY();
1612
- var line = chromeLineScale();
1613
- highlightOverlay.style.borderWidth = (1.5 * line) + 'px';
1614
- selectionOverlay.style.borderWidth = (1.5 * line) + 'px';
1615
- if (selectedEl) updateSpacingOverlay(selectedEl);
1616
-
1617
- selectionOverlay.querySelectorAll('[data-agent-native-edge-handle]').forEach(function(edge) {
1618
- var pos = edge.getAttribute('data-agent-native-edge-handle');
1619
- if (pos === 'n' || pos === 's') {
1620
- edge.style.height = (10 * sy) + 'px';
1621
- edge.style[pos === 'n' ? 'top' : 'bottom'] = (-5 * sy) + 'px';
1622
- }
1623
- if (pos === 'e' || pos === 'w') {
1624
- edge.style.width = (10 * sx) + 'px';
1625
- edge.style[pos === 'w' ? 'left' : 'right'] = (-5 * sx) + 'px';
1626
- }
1627
- });
1628
-
1629
- selectionOverlay.querySelectorAll('[data-agent-native-edit-handle]').forEach(function(handle) {
1630
- var pos = handle.getAttribute('data-agent-native-edit-handle') || '';
1631
- handle.style.width = (7 * sx) + 'px';
1632
- handle.style.height = (7 * sy) + 'px';
1633
- handle.style.borderWidth = Math.max(1, 1 * line) + 'px';
1634
- if (pos.indexOf('n') !== -1) handle.style.top = (-4 * sy) + 'px';
1635
- if (pos.indexOf('s') !== -1) handle.style.bottom = (-4 * sy) + 'px';
1636
- if (pos.indexOf('w') !== -1) handle.style.left = (-4 * sx) + 'px';
1637
- if (pos.indexOf('e') !== -1) handle.style.right = (-4 * sx) + 'px';
1638
- });
1639
-
1640
- selectionOverlay.querySelectorAll('[data-agent-native-rotate-handle]').forEach(function(handle) {
1641
- var pos = handle.getAttribute('data-agent-native-rotate-handle') || '';
1642
- handle.style.width = (18 * sx) + 'px';
1643
- handle.style.height = (18 * sy) + 'px';
1644
- if (pos.indexOf('n') !== -1) handle.style.top = (-26 * sy) + 'px';
1645
- if (pos.indexOf('s') !== -1) handle.style.bottom = (-26 * sy) + 'px';
1646
- if (pos.indexOf('w') !== -1) handle.style.left = (-26 * sx) + 'px';
1647
- if (pos.indexOf('e') !== -1) handle.style.right = (-26 * sx) + 'px';
1648
- });
1649
- }
1650
-
1651
- function positionOverlay(overlay, el) {
1652
- if (!el || !document.documentElement.contains(el)) {
1653
- overlay.style.display = 'none';
1654
- if (overlay === selectionOverlay) clearComponentTag();
1655
- return;
1656
- }
1657
- // For the selection overlay, prefer the CSS box + rotation transform so
1658
- // the handles hug the rotated element rather than its inflated AABB.
1659
- if (overlay === selectionOverlay) {
1660
- var elCs = window.getComputedStyle(el);
1661
- var elLeft = readFinitePx(el.style.left || elCs.left);
1662
- var elTop = readFinitePx(el.style.top || elCs.top);
1663
- var elW = readFinitePx(el.style.width || elCs.width);
1664
- var elH = readFinitePx(el.style.height || elCs.height);
1665
- var elRot = currentRotation(el);
1666
- var canUseLocalBox = Math.abs(elRot) > 0.01 && elLeft !== null && elTop !== null && elW !== null && elH !== null;
1667
- // Convert element-local left/top to viewport coords by walking to the
1668
- // nearest positioned ancestor (same reference frame as getBoundingClientRect).
1669
- if (canUseLocalBox) {
1670
- var parentRect = (el.offsetParent || document.documentElement).getBoundingClientRect();
1671
- overlay.style.display = 'block';
1672
- overlay.style.left = (parentRect.left + elLeft) + 'px';
1673
- overlay.style.top = (parentRect.top + elTop) + 'px';
1674
- overlay.style.width = elW + 'px';
1675
- overlay.style.height = elH + 'px';
1676
- overlay.style.transform = 'rotate(' + elRot + 'deg)';
1677
- overlay.style.transformOrigin = '0 0';
1678
- updateSpacingOverlay(el);
1679
- updateComponentTag(el);
1680
- return;
1681
- }
1682
- }
1683
- var rect = el.getBoundingClientRect();
1684
- overlay.style.display = 'block';
1685
- overlay.style.top = rect.top + 'px';
1686
- overlay.style.left = rect.left + 'px';
1687
- overlay.style.width = rect.width + 'px';
1688
- overlay.style.height = rect.height + 'px';
1689
- overlay.style.transform = '';
1690
- if (overlay === selectionOverlay) {
1691
- updateSpacingOverlay(el);
1692
- updateComponentTag(el);
1693
- }
1694
- }
1695
-
1696
- function refreshOverlays() {
1697
- if (hoveredEl && hoveredEl !== selectedEl) {
1698
- positionOverlay(highlightOverlay, hoveredEl);
1699
- } else {
1700
- highlightOverlay.style.display = 'none';
1701
- }
1702
- if (selectedEl) positionOverlay(selectionOverlay, selectedEl);
1703
- }
1704
-
1705
- function hideMeasurements() {
1706
- measurementOverlay.style.display = 'none';
1707
- measurementOverlay.innerHTML = '';
1708
- }
1709
-
1710
- function addMeasurementLine(x1, y1, x2, y2, label) {
1711
- var horizontal = Math.abs(x2 - x1) >= Math.abs(y2 - y1);
1712
- var line = document.createElement('div');
1713
- var labelEl = document.createElement('div');
1714
- if (horizontal) {
1715
- var left = Math.min(x1, x2);
1716
- var width = Math.max(1, Math.abs(x2 - x1));
1717
- line.style.cssText = 'position:fixed;left:' + left + 'px;top:' + y1 + 'px;width:' + width + 'px;border-top:1px dashed var(--design-editor-measure-color);';
1718
- labelEl.style.cssText = 'position:fixed;left:' + (left + width / 2) + 'px;top:' + (y1 - 9) + 'px;transform:translateX(-50%);border-radius:3px;background:var(--design-editor-measure-color);color:white;padding:1px 4px;';
1719
- } else {
1720
- var top = Math.min(y1, y2);
1721
- var height = Math.max(1, Math.abs(y2 - y1));
1722
- line.style.cssText = 'position:fixed;left:' + x1 + 'px;top:' + top + 'px;height:' + height + 'px;border-left:1px dashed var(--design-editor-measure-color);';
1723
- labelEl.style.cssText = 'position:fixed;left:' + (x1 + 5) + 'px;top:' + (top + height / 2) + 'px;transform:translateY(-50%);border-radius:3px;background:var(--design-editor-measure-color);color:white;padding:1px 4px;';
1724
- }
1725
- labelEl.textContent = label;
1726
- measurementOverlay.appendChild(line);
1727
- measurementOverlay.appendChild(labelEl);
1728
- }
1729
-
1730
- function showMeasurements(a, b) {
1731
- if (!a || !b || a === b) {
1732
- hideMeasurements();
1733
- return;
1734
- }
1735
- var selectedRect = a.getBoundingClientRect();
1736
- var hoverRect = b.getBoundingClientRect();
1737
- measurementOverlay.innerHTML = '';
1738
- measurementOverlay.style.display = 'block';
1739
-
1740
- if (hoverRect.right <= selectedRect.left) {
1741
- var yLeft = Math.max(hoverRect.top, Math.min(hoverRect.bottom, selectedRect.top + selectedRect.height / 2));
1742
- addMeasurementLine(hoverRect.right, yLeft, selectedRect.left, yLeft, Math.round(selectedRect.left - hoverRect.right) + 'px');
1743
- return;
1744
- }
1745
- if (selectedRect.right <= hoverRect.left) {
1746
- var yRight = Math.max(selectedRect.top, Math.min(selectedRect.bottom, hoverRect.top + hoverRect.height / 2));
1747
- addMeasurementLine(selectedRect.right, yRight, hoverRect.left, yRight, Math.round(hoverRect.left - selectedRect.right) + 'px');
1748
- return;
1749
- }
1750
- if (hoverRect.bottom <= selectedRect.top) {
1751
- var xTop = Math.max(hoverRect.left, Math.min(hoverRect.right, selectedRect.left + selectedRect.width / 2));
1752
- addMeasurementLine(xTop, hoverRect.bottom, xTop, selectedRect.top, Math.round(selectedRect.top - hoverRect.bottom) + 'px');
1753
- return;
1754
- }
1755
- if (selectedRect.bottom <= hoverRect.top) {
1756
- var xBottom = Math.max(selectedRect.left, Math.min(selectedRect.right, hoverRect.left + hoverRect.width / 2));
1757
- addMeasurementLine(xBottom, selectedRect.bottom, xBottom, hoverRect.top, Math.round(hoverRect.top - selectedRect.bottom) + 'px');
1758
- return;
1759
- }
1760
- addMeasurementLine(
1761
- selectedRect.left + selectedRect.width / 2,
1762
- selectedRect.top + selectedRect.height / 2,
1763
- hoverRect.left + hoverRect.width / 2,
1764
- hoverRect.top + hoverRect.height / 2,
1765
- Math.round(Math.hypot(
1766
- hoverRect.left + hoverRect.width / 2 - (selectedRect.left + selectedRect.width / 2),
1767
- hoverRect.top + hoverRect.height / 2 - (selectedRect.top + selectedRect.height / 2)
1768
- )) + 'px'
1769
- );
1770
- }
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
-
1779
- function elementFromEditorPoint(clientX, clientY) {
1780
- var shieldPointerEvents = shieldOverlay.style.pointerEvents;
1781
- var selectionPointerEvents = selectionOverlay.style.pointerEvents;
1782
- var highlightPointerEvents = highlightOverlay.style.pointerEvents;
1783
- shieldOverlay.style.pointerEvents = 'none';
1784
- selectionOverlay.style.pointerEvents = 'none';
1785
- highlightOverlay.style.pointerEvents = 'none';
1786
- var target = document.elementFromPoint(clientX, clientY);
1787
- shieldOverlay.style.pointerEvents = shieldPointerEvents;
1788
- selectionOverlay.style.pointerEvents = selectionPointerEvents;
1789
- highlightOverlay.style.pointerEvents = highlightPointerEvents;
1790
- if (!target || target.nodeType !== 1) return null;
1791
- if (isLayerInteractionBlocked(target)) return null;
1792
- return target;
1793
- }
1794
-
1795
- function stopNativeInteraction(e) {
1796
- e.preventDefault();
1797
- e.stopPropagation();
1798
- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
1799
- }
1800
-
1801
- function isEditorTypingTarget(target) {
1802
- if (!target || !target.closest) return false;
1803
- return !!target.closest('input, textarea, select, [contenteditable], [role="textbox"], [data-agent-native-text-editing]');
1804
- }
1805
-
1806
- function shouldForwardDesignHotkey(e) {
1807
- if (activeTextEditEl || isEditorTypingTarget(e.target) || e.isComposing) return false;
1808
- var key = e.key;
1809
- var normalized = key && key.length === 1 ? key.toLowerCase() : key;
1810
- var primary = e.metaKey || e.ctrlKey;
1811
- if (key === 'Escape' || key === 'Enter') return true;
1812
- // Forward Tab only when an element is actively selected so the iframe does
1813
- // not intercept Tab when the user is tabbing through browser UI with nothing
1814
- // selected (preserves native keyboard accessibility).
1815
- if (key === 'Tab') return !!selectedEl;
1816
- if (key === 'Delete' || key === 'Backspace') return !primary;
1817
- if (/^Arrow/.test(key || '')) return !e.altKey;
1818
- if (primary) {
1819
- return ['z','y','a','x','c','v','d','g','=','+','-','0',']','['].indexOf(normalized) !== -1 ||
1820
- e.code === 'Digit1' ||
1821
- e.code === 'Digit2' ||
1822
- key === '1' ||
1823
- key === '2';
1824
- }
1825
- if (e.shiftKey && (e.code === 'Digit1' || e.code === 'Digit2' || key === '1' || key === '2')) return true;
1826
- return !e.altKey && !e.shiftKey && ['v','f','r','t','p','h','c','k'].indexOf(normalized) !== -1;
1827
- }
1828
-
1829
- function blurActiveTextEditor() {
1830
- var active = document.activeElement;
1831
- if (
1832
- active &&
1833
- active.closest &&
1834
- active.closest('[data-agent-native-text-editing]') &&
1835
- typeof active.blur === 'function'
1836
- ) {
1837
- active.blur();
1838
- }
1839
- }
1840
-
1841
- function setTextEditingPointerPassthrough(enabled) {
1842
- if (enabled) {
1843
- if (!textEditPointerState) {
1844
- textEditPointerState = {
1845
- shield: shieldOverlay.style.pointerEvents,
1846
- selection: selectionOverlay.style.pointerEvents,
1847
- highlight: highlightOverlay.style.pointerEvents,
1848
- };
1849
- }
1850
- shieldOverlay.style.pointerEvents = 'none';
1851
- selectionOverlay.style.pointerEvents = 'none';
1852
- highlightOverlay.style.pointerEvents = 'none';
1853
- return;
1854
- }
1855
- if (!textEditPointerState) return;
1856
- shieldOverlay.style.pointerEvents = textEditPointerState.shield;
1857
- selectionOverlay.style.pointerEvents = textEditPointerState.selection;
1858
- highlightOverlay.style.pointerEvents = textEditPointerState.highlight;
1859
- textEditPointerState = null;
1860
- }
1861
-
1862
- function hasTextContent(el) {
1863
- return !!(el && el.textContent && el.textContent.trim().length > 0);
1864
- }
1865
-
1866
- function isInlineEditableDescendant(el) {
1867
- if (!el || !el.tagName) return false;
1868
- // Allowlist covers inline markup AND common block-level text containers
1869
- // (p, h1-h6, li, etc.) so that paragraphs with inline markup like
1870
- // <p>Hello <strong>world</strong></p> can be double-click edited.
1871
- return [
1872
- // Inline formatting
1873
- 'a',
1874
- 'abbr',
1875
- 'b',
1876
- 'br',
1877
- 'cite',
1878
- 'code',
1879
- 'em',
1880
- 'i',
1881
- 'mark',
1882
- 'small',
1883
- 'span',
1884
- 'strong',
1885
- 'sub',
1886
- 'sup',
1887
- 'time',
1888
- 'u',
1889
- 'wbr',
1890
- // Block-level text containers
1891
- 'p',
1892
- 'h1',
1893
- 'h2',
1894
- 'h3',
1895
- 'h4',
1896
- 'h5',
1897
- 'h6',
1898
- 'li',
1899
- 'ul',
1900
- 'ol',
1901
- 'dl',
1902
- 'dt',
1903
- 'dd',
1904
- 'label',
1905
- 'caption',
1906
- 'td',
1907
- 'th'
1908
- ].indexOf(el.tagName.toLowerCase()) !== -1;
1909
- }
1910
-
1911
- function hasOnlyInlineEditableChildren(el) {
1912
- if (!el || !hasTextContent(el)) return false;
1913
- var descendants = el.querySelectorAll ? el.querySelectorAll('*') : [];
1914
- for (var i = 0; i < descendants.length; i += 1) {
1915
- if (!isInlineEditableDescendant(descendants[i])) return false;
1916
- }
1917
- return true;
1918
- }
1919
-
1920
- function findTextEditTarget(hit) {
1921
- if (!hit || hit.nodeType !== 1 || hit === document.body || hit === document.documentElement) return null;
1922
- var selectedContainsHit = selectedEl && selectedEl.contains && selectedEl.contains(hit);
1923
- if (selectedContainsHit && hasOnlyInlineEditableChildren(selectedEl)) return selectedEl;
1924
-
1925
- var candidate = null;
1926
- var node = hit;
1927
- while (node && node.nodeType === 1 && node !== document.body && node !== document.documentElement) {
1928
- if (hasOnlyInlineEditableChildren(node)) {
1929
- candidate = node;
1930
- }
1931
- if (selectedEl && node === selectedEl) break;
1932
- node = node.parentElement;
1933
- }
1934
- // Return null (not the raw hit) when no text-editable ancestor is found.
1935
- // Falling back to the raw hit element makes non-text nodes like <img> or
1936
- // <canvas> contenteditable, which leaves the editor in a broken state.
1937
- return candidate || null;
1938
- }
1939
-
1940
- function selectElementAtEvent(e) {
1941
- stopNativeInteraction(e);
1942
- blurActiveTextEditor();
1943
- if (suppressNextShieldClick) {
1944
- suppressNextShieldClick = false;
1945
- if (suppressNextShieldClickTimer !== null) {
1946
- clearTimeout(suppressNextShieldClickTimer);
1947
- suppressNextShieldClickTimer = null;
1948
- }
1949
- return;
1950
- }
1951
- // Suppress the first click in a double-click sequence — the dblclick
1952
- // handler (beginTextEditingFromEvent) will fire immediately after and a
1953
- // spurious element-select would cause inspector flicker.
1954
- if (e.detail >= 2) return;
1955
- var target = elementFromEditorPoint(e.clientX, e.clientY);
1956
- if (!target || target === document.body || target === document.documentElement) {
1957
- // Click on empty canvas: clear the current selection (matches Figma).
1958
- clearRuntimeSelection();
1959
- window.parent.postMessage({ type: 'clear-selection' }, '*');
1960
- return;
1961
- }
1962
- selectedSpacingHovered = false;
1963
- hoveredSpacingHandleKey = '';
1964
- selectedEl = selectionTargetForHit(target);
1965
- if (!selectedEl || isLayerInteractionBlocked(selectedEl)) {
1966
- selectedEl = null;
1967
- selectionOverlay.style.display = 'none';
1968
- clearComponentTag();
1969
- return;
1970
- }
1971
- var info = getElementInfo(selectedEl);
1972
- positionOverlay(selectionOverlay, selectedEl);
1973
- window.parent.postMessage({ type: 'element-select', payload: info }, '*');
1974
- }
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
-
1987
- function openContextMenuAtEvent(e) {
1988
- stopNativeInteraction(e);
1989
- blurActiveTextEditor();
1990
- var target = elementFromEditorPoint(e.clientX, e.clientY);
1991
- var info = null;
1992
- if (target) {
1993
- selectedSpacingHovered = false;
1994
- hoveredSpacingHandleKey = '';
1995
- selectedEl = selectionTargetForHit(target);
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
- }
2005
- }
2006
- window.parent.postMessage({
2007
- type: 'element-contextmenu',
2008
- clientX: e.clientX,
2009
- clientY: e.clientY,
2010
- payload: info
2011
- }, '*');
2012
- }
2013
-
2014
- function findRuntimeTarget(selector, selectorCandidates) {
2015
- var candidates = [];
2016
- if (Array.isArray(selectorCandidates)) {
2017
- selectorCandidates.forEach(function(candidate) {
2018
- if (typeof candidate === 'string' && candidate && candidates.indexOf(candidate) === -1) {
2019
- candidates.push(candidate);
2020
- }
2021
- });
2022
- }
2023
- if (selector && candidates.indexOf(selector) === -1) candidates.push(selector);
2024
- if (
2025
- selectedEl &&
2026
- document.documentElement.contains(selectedEl) &&
2027
- (candidates.length === 0 || matchesExactSelectorList(selectedEl, candidates))
2028
- ) {
2029
- return selectedEl;
2030
- }
2031
- for (var i = 0; i < candidates.length; i += 1) {
2032
- try {
2033
- var match = document.querySelector(candidates[i]);
2034
- if (match && !isLayerInteractionBlocked(match)) return match;
2035
- } catch (_err) {}
2036
- }
2037
- return null;
2038
- }
2039
-
2040
- function removeRuntimeTarget(selector, selectorCandidates) {
2041
- var target = findRuntimeTarget(selector, selectorCandidates);
2042
- if (!target || target === document.body || target === document.documentElement) return false;
2043
- if (target.parentElement) target.parentElement.removeChild(target);
2044
- if (selectedEl === target || !document.documentElement.contains(selectedEl)) {
2045
- selectedEl = null;
2046
- selectionOverlay.style.display = 'none';
2047
- }
2048
- hoveredEl = null;
2049
- highlightOverlay.style.display = 'none';
2050
- hideMeasurements();
2051
- refreshOverlays();
2052
- return true;
2053
- }
2054
-
2055
- function readPx(value) {
2056
- var num = parseFloat(value);
2057
- return Number.isFinite(num) ? num : 0;
2058
- }
2059
-
2060
- function readFinitePx(value) {
2061
- if (!value || value === 'auto') return null;
2062
- var num = parseFloat(value);
2063
- return Number.isFinite(num) ? num : null;
2064
- }
2065
-
2066
- function currentRotation(el) {
2067
- var transform = el.style.transform || window.getComputedStyle(el).transform || '';
2068
- var match = transform.match(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/);
2069
- if (match) return parseFloat(match[1]) || 0;
2070
- if (transform && transform !== 'none' && window.DOMMatrixReadOnly) {
2071
- try {
2072
- var matrix = new DOMMatrixReadOnly(transform);
2073
- return Math.round(Math.atan2(matrix.b, matrix.a) * 180 / Math.PI);
2074
- } catch (err) {}
2075
- }
2076
- return 0;
2077
- }
2078
-
2079
- function mergeRotation(el, degrees) {
2080
- var inline = el.style.transform || '';
2081
- var next = inline.match(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/)
2082
- ? inline.replace(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/, 'rotate(' + degrees + 'deg)')
2083
- : (inline && inline !== 'none' ? inline + ' ' : '') + 'rotate(' + degrees + 'deg)';
2084
- return next.trim();
2085
- }
2086
-
2087
- function ensurePositionable(el) {
2088
- var cs = window.getComputedStyle(el);
2089
- if (cs.position === 'static') {
2090
- el.style.position = 'relative';
2091
- if (!el.style.left) el.style.left = '0px';
2092
- if (!el.style.top) el.style.top = '0px';
2093
- }
2094
- }
2095
-
2096
- function postVisualStyleChange(styles) {
2097
- if (!selectedEl) return;
2098
- window.parent.postMessage({
2099
- type: 'visual-style-change',
2100
- selector: getSelector(selectedEl),
2101
- styles: styles,
2102
- payload: getElementInfo(selectedEl),
2103
- }, '*');
2104
- }
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
-
2181
- function postTextContentChange(el, value, html) {
2182
- window.parent.postMessage({
2183
- type: 'text-content-change',
2184
- selector: getSelector(el),
2185
- value: value,
2186
- html: html,
2187
- payload: getElementInfo(el),
2188
- }, '*');
2189
- }
2190
-
2191
- function postTextEditingState(el, active) {
2192
- var selection = window.getSelection && window.getSelection();
2193
- window.parent.postMessage({
2194
- type: 'text-editing-state',
2195
- active: !!active,
2196
- selector: el ? getSelector(el) : '',
2197
- hasRange: !!(active && selection && selection.rangeCount > 0 && !selection.isCollapsed && selectionBelongsToElement(selection, el)),
2198
- }, '*');
2199
- }
2200
-
2201
- function insertPlainTextAtSelection(text) {
2202
- if (!text) return;
2203
- if (document.queryCommandSupported && document.queryCommandSupported('insertText')) {
2204
- document.execCommand('insertText', false, text);
2205
- return;
2206
- }
2207
- var selection = window.getSelection && window.getSelection();
2208
- if (!selection || selection.rangeCount === 0) return;
2209
- var range = selection.getRangeAt(0);
2210
- range.deleteContents();
2211
- var textNode = document.createTextNode(text);
2212
- range.insertNode(textNode);
2213
- range.setStartAfter(textNode);
2214
- range.setEndAfter(textNode);
2215
- selection.removeAllRanges();
2216
- selection.addRange(range);
2217
- }
2218
-
2219
- function selectionBelongsToElement(selection, el) {
2220
- if (!selection || !el || selection.rangeCount === 0) return false;
2221
- var range = selection.getRangeAt(0);
2222
- var ancestor = range.commonAncestorContainer;
2223
- var ancestorEl = ancestor && ancestor.nodeType === 1 ? ancestor : ancestor && ancestor.parentElement;
2224
- return !!(ancestorEl && (ancestorEl === el || el.contains(ancestorEl)));
2225
- }
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
-
2242
- function applyTextRangeStyle(property, value) {
2243
- if (!activeTextEditEl || !property) return false;
2244
- var selection = window.getSelection && window.getSelection();
2245
- if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return false;
2246
- if (!selectionBelongsToElement(selection, activeTextEditEl)) return false;
2247
- var range = selection.getRangeAt(0);
2248
- var span = document.createElement('span');
2249
- applyInlineStyleProperty(span, property, value);
2250
- if (!span.getAttribute('style')) return false;
2251
- try {
2252
- range.surroundContents(span);
2253
- } catch (err) {
2254
- var contents = range.extractContents();
2255
- span.appendChild(contents);
2256
- range.insertNode(span);
2257
- }
2258
- selection.removeAllRanges();
2259
- var nextRange = document.createRange();
2260
- nextRange.selectNodeContents(span);
2261
- selection.addRange(nextRange);
2262
- return true;
2263
- }
2264
-
2265
- function showTransformBadge(text, clientX, clientY) {
2266
- transformBadge.textContent = text;
2267
- transformBadge.style.display = 'block';
2268
- transformBadge.style.left = clientX + 12 + 'px';
2269
- transformBadge.style.top = clientY + 12 + 'px';
2270
- }
2271
-
2272
- function hideTransformBadge() {
2273
- transformBadge.style.display = 'none';
2274
- }
2275
-
2276
- function hideInsertionGuide() {
2277
- insertionGuide.style.display = 'none';
2278
- }
2279
-
2280
- function isOverlayElement(el) {
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
- );
2289
- }
2290
-
2291
- function draggableElementChildren(parent) {
2292
- return Array.prototype.slice.call(parent.children).filter(function(child) {
2293
- return child.nodeType === 1 && !isOverlayElement(child) && !isLayerInteractionBlocked(child);
2294
- });
2295
- }
2296
-
2297
- function isFlowReorderCandidate(el) {
2298
- if (!el || !el.parentElement) return false;
2299
- if (el === document.body || el === document.documentElement) return false;
2300
- var cs = window.getComputedStyle(el);
2301
- if (cs.position === 'absolute' || cs.position === 'fixed') return false;
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;
2336
- }
2337
-
2338
- function parentFlowAxis(parent) {
2339
- var cs = window.getComputedStyle(parent);
2340
- if (cs.display === 'flex' || cs.display === 'inline-flex') {
2341
- var isRow = cs.flexDirection && cs.flexDirection.indexOf('row') === 0;
2342
- // Wrapping row containers need Y-axis awareness for inter-row targeting;
2343
- // fall back to column-axis insertion so the heuristic picks the right row.
2344
- var wraps = cs.flexWrap === 'wrap' || cs.flexWrap === 'wrap-reverse';
2345
- if (isRow && !wraps) return 'x';
2346
- return 'y';
2347
- }
2348
- if (cs.display === 'grid' || cs.display === 'inline-grid') {
2349
- var cols = (cs.gridTemplateColumns || '').split(' ').filter(Boolean).length;
2350
- return cols > 1 ? 'x' : 'y';
2351
- }
2352
- return 'y';
2353
- }
2354
-
2355
- function reorderTargetForPoint(el, clientX, clientY) {
2356
- if (!el || !el.parentElement) return null;
2357
- var hit = elementFromEditorPoint(clientX, clientY);
2358
- if (
2359
- hit &&
2360
- hit !== document.documentElement &&
2361
- hit !== el &&
2362
- !el.contains(hit) &&
2363
- !isOverlayElement(hit)
2364
- ) {
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 };
2373
- }
2374
- var hitParent = hit.parentElement;
2375
- if (hitParent) {
2376
- var hitAxis = parentFlowAxis(hitParent);
2377
- var hitRect = hit.getBoundingClientRect();
2378
- var hitCenter = hitAxis === 'x'
2379
- ? hitRect.left + hitRect.width / 2
2380
- : hitRect.top + hitRect.height / 2;
2381
- var hitPointer = hitAxis === 'x' ? clientX : clientY;
2382
- return {
2383
- anchor: hit,
2384
- placement: hitPointer < hitCenter ? 'before' : 'after',
2385
- axis: hitAxis,
2386
- };
2387
- }
2388
- }
2389
- var parent = el.parentElement;
2390
- var axis = parentFlowAxis(parent);
2391
- var siblings = draggableElementChildren(parent).filter(function(child) {
2392
- return child !== el;
2393
- });
2394
- if (!siblings.length) return null;
2395
- var beforeTarget = null;
2396
- for (var i = 0; i < siblings.length; i += 1) {
2397
- var rect = siblings[i].getBoundingClientRect();
2398
- var center = axis === 'x' ? rect.left + rect.width / 2 : rect.top + rect.height / 2;
2399
- var pointer = axis === 'x' ? clientX : clientY;
2400
- if (pointer < center) {
2401
- beforeTarget = siblings[i];
2402
- break;
2403
- }
2404
- }
2405
- var anchor = beforeTarget || siblings[siblings.length - 1];
2406
- var placement = beforeTarget ? 'before' : 'after';
2407
- return { anchor: anchor, placement: placement, axis: axis };
2408
- }
2409
-
2410
- function showInsertionGuideFor(target) {
2411
- if (!target || !target.anchor) {
2412
- hideInsertionGuide();
2413
- return;
2414
- }
2415
- var rect = target.anchor.getBoundingClientRect();
2416
- insertionGuide.style.display = 'block';
2417
- insertionGuide.style.background = 'var(--design-editor-accent-color)';
2418
- insertionGuide.style.border = '0';
2419
- insertionGuide.style.borderRadius = '999px';
2420
- insertionGuide.style.boxShadow = '0 0 0 1px var(--design-editor-accent-color)';
2421
- if (target.placement === 'inside') {
2422
- insertionGuide.style.left = rect.left + 'px';
2423
- insertionGuide.style.top = rect.top + 'px';
2424
- insertionGuide.style.width = rect.width + 'px';
2425
- insertionGuide.style.height = rect.height + 'px';
2426
- insertionGuide.style.background = 'color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)';
2427
- insertionGuide.style.border = '2px solid var(--design-editor-accent-color)';
2428
- insertionGuide.style.borderRadius = '2px';
2429
- insertionGuide.style.boxShadow = 'none';
2430
- return;
2431
- }
2432
- if (target.axis === 'x') {
2433
- var x = target.placement === 'before' ? rect.left : rect.right;
2434
- insertionGuide.style.left = x + 'px';
2435
- insertionGuide.style.top = rect.top + 'px';
2436
- insertionGuide.style.width = '2px';
2437
- insertionGuide.style.height = rect.height + 'px';
2438
- } else {
2439
- var y = target.placement === 'before' ? rect.top : rect.bottom;
2440
- insertionGuide.style.left = rect.left + 'px';
2441
- insertionGuide.style.top = y + 'px';
2442
- insertionGuide.style.width = rect.width + 'px';
2443
- insertionGuide.style.height = '2px';
2444
- }
2445
- }
81
+ * Source: app/components/design/bridge/motion-preview.bridge.ts
82
+ * Compiled: .generated/bridge/motion-preview.generated.ts (run bridge/codegen.ts to update)
83
+ */
84
+ const MOTION_PREVIEW_BRIDGE_SCRIPT = `
85
+ <script data-agent-native-motion-preview-bridge>
86
+ ${motionPreviewBridgeScript}
87
+ </script>
88
+ `;
2446
89
 
2447
- function applyRuntimeReorder(el, target) {
2448
- if (!el || !target || !target.anchor || !target.anchor.parentElement) return;
2449
- if (target.placement === 'inside') {
2450
- target.anchor.appendChild(el);
2451
- return;
2452
- }
2453
- var parent = target.anchor.parentElement;
2454
- if (target.placement === 'before') {
2455
- parent.insertBefore(el, target.anchor);
2456
- } else {
2457
- parent.insertBefore(el, target.anchor.nextSibling);
2458
- }
2459
- }
90
+ /**
91
+ * Shader-fill preview bridge. ALWAYS injected alongside the other bridge
92
+ * scripts so the parent can apply a CSS gradient approximation of a shader
93
+ * fill to the currently-selected element without persisting anything.
94
+ *
95
+ * Source: app/components/design/bridge/shader-fill-preview.bridge.ts
96
+ * Compiled: .generated/bridge/shader-fill-preview.generated.ts (run bridge/codegen.ts to update)
97
+ */
98
+ const SHADER_FILL_PREVIEW_BRIDGE_SCRIPT = `
99
+ <script data-agent-native-shader-fill-preview-bridge>
100
+ ${shaderFillPreviewBridgeScript}
101
+ </script>
102
+ `;
2460
103
 
2461
- function postVisualStructureChange(el, target, origin) {
2462
- if (!el || !target || !target.anchor) return;
2463
- var requestId = 'move-' + Date.now() + '-' + Math.random().toString(16).slice(2);
2464
- pendingStructureMove = { requestId: requestId, el: el, target: target, origin: origin || null };
2465
- window.parent.postMessage({
2466
- type: 'visual-structure-change',
2467
- requestId: requestId,
2468
- selector: getSelector(el),
2469
- sourceId: getSourceId(el),
2470
- anchorSelector: getSelector(target.anchor),
2471
- anchorSourceId: getSourceId(target.anchor),
2472
- placement: target.placement,
2473
- payload: getElementInfo(el),
2474
- }, '*');
2475
- }
104
+ /**
105
+ * Tweak bridge. ALWAYS injected so the parent's postMessage (`tweak-values`)
106
+ * can update CSS custom properties on the iframe's :root regardless of which
107
+ * editor mode is active.
108
+ *
109
+ * Source: app/components/design/bridge/tweak.bridge.ts
110
+ * Compiled: .generated/bridge/tweak.generated.ts (run bridge/codegen.ts to update)
111
+ */
112
+ const TWEAK_BRIDGE_SCRIPT = `
113
+ <script data-agent-native-tweak-bridge>
114
+ ${tweakBridgeScript}
115
+ </script>
116
+ `;
2476
117
 
2477
- function postVisualDuplicateChange(originalEl, cloneEl, target) {
2478
- if (!originalEl || !cloneEl) return;
2479
- window.parent.postMessage({
2480
- type: 'visual-duplicate-change',
2481
- selector: getSelector(originalEl),
2482
- sourceId: getSourceId(originalEl),
2483
- anchorSelector: target && target.anchor ? getSelector(target.anchor) : '',
2484
- anchorSourceId: target && target.anchor ? getSourceId(target.anchor) : '',
2485
- placement: target && target.placement ? target.placement : 'after',
2486
- cloneHtml: cloneEl.outerHTML,
2487
- payload: getElementInfo(cloneEl),
2488
- }, '*');
2489
- }
118
+ /**
119
+ * Pinch-zoom bridge: forwards trackpad pinch / Cmd-Ctrl+scroll wheel events
120
+ * from inside the iframe to the parent window.
121
+ *
122
+ * Source: app/components/design/bridge/zoom.bridge.ts
123
+ * Compiled: .generated/bridge/zoom.generated.ts (run bridge/codegen.ts to update)
124
+ */
125
+ const ZOOM_BRIDGE_SCRIPT = `
126
+ <script data-agent-native-zoom-bridge>
127
+ ${zoomBridgeScript}
128
+ </script>
129
+ `;
2490
130
 
2491
- function startMove(e) {
2492
- if (!selectedEl) return;
2493
- if (isLayerInteractionBlocked(selectedEl)) return;
2494
- e.preventDefault();
2495
- e.stopPropagation();
2496
- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
2497
- var events = dragEventNames(e);
2498
- var originalSelectedEl = selectedEl;
2499
- var duplicatedForDrag = false;
2500
- if (e.altKey && selectedEl !== document.body && selectedEl !== document.documentElement) {
2501
- var clone = selectedEl.cloneNode(true);
2502
- resetRuntimeStableIds(clone);
2503
- selectedEl.parentElement.insertBefore(clone, selectedEl.nextSibling);
2504
- selectedEl = clone;
2505
- duplicatedForDrag = true;
2506
- positionOverlay(selectionOverlay, selectedEl);
2507
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2508
- }
2509
- if (isFlowReorderCandidate(selectedEl)) {
2510
- // Snapshot the element being reordered so a concurrent select-element or
2511
- // clear-selection postMessage cannot mutate the wrong element mid-drag.
2512
- var reorderEl = selectedEl;
2513
- var currentTarget = reorderTargetForPoint(reorderEl, e.clientX, e.clientY);
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);
2521
- function onReorderMove(ev) {
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
- }
2551
- }
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);
2579
- hideTransformBadge();
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;
2605
- if (!currentTarget) {
2606
- // No valid drop target — clean up the clone if one was inserted so
2607
- // no ghost element is left in the DOM.
2608
- if (duplicatedForDrag && reorderEl && reorderEl !== originalSelectedEl) {
2609
- if (reorderEl.parentElement) reorderEl.parentElement.removeChild(reorderEl);
2610
- selectedEl = originalSelectedEl;
2611
- positionOverlay(selectionOverlay, selectedEl);
2612
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2613
- }
2614
- return;
2615
- }
2616
- if (duplicatedForDrag) {
2617
- applyRuntimeReorder(reorderEl, currentTarget);
2618
- postVisualDuplicateChange(originalSelectedEl, reorderEl, currentTarget);
2619
- } else {
2620
- // Capture the pre-drag DOM anchor so we can revert if the parent
2621
- // reports applied===false on the structure-ack.
2622
- var prevParent = reorderEl.parentElement;
2623
- var prevNextSibling = reorderEl.nextSibling;
2624
- // Optimistically apply the reorder in the DOM for immediate
2625
- // visual feedback; the visual-structure-ack handler will confirm
2626
- // or revert once the parent processes the change.
2627
- applyRuntimeReorder(reorderEl, currentTarget);
2628
- postVisualStructureChange(reorderEl, currentTarget, { prevParent: prevParent, prevNextSibling: prevNextSibling });
2629
- }
2630
- }
2631
- document.addEventListener(events.move, onReorderMove, true);
2632
- document.addEventListener(events.up, onReorderUp, true);
2633
- document.addEventListener('keydown', onReorderKeyDown, true);
2634
- return;
2635
- }
2636
- ensurePositionable(selectedEl);
2637
- var cs = window.getComputedStyle(selectedEl);
2638
- var originLeft = readPx(selectedEl.style.left || cs.left);
2639
- var originTop = readPx(selectedEl.style.top || cs.top);
2640
- var startX = e.clientX;
2641
- var startY = e.clientY;
2642
- // Snapshot the element being moved so that a concurrent select-element or
2643
- // clear-selection postMessage cannot swap selectedEl mid-drag and cause
2644
- // mutations on the wrong element or a null-deref in onUp.
2645
- var dragEl = selectedEl;
2646
- var moved = false;
2647
- var DRAG_THRESHOLD = 3;
2648
- function onMove(ev) {
2649
- if (!moved && Math.hypot(ev.clientX - startX, ev.clientY - startY) > DRAG_THRESHOLD) {
2650
- moved = true;
2651
- }
2652
- var nextLeft = originLeft + ev.clientX - startX;
2653
- var nextTop = originTop + ev.clientY - startY;
2654
- dragEl.style.left = Math.round(nextLeft) + 'px';
2655
- dragEl.style.top = Math.round(nextTop) + 'px';
2656
- showTransformBadge('X ' + Math.round(nextLeft) + ' Y ' + Math.round(nextTop), ev.clientX, ev.clientY);
2657
- refreshOverlays();
2658
- }
2659
- function onUp() {
2660
- document.removeEventListener(events.move, onMove, true);
2661
- document.removeEventListener(events.up, onUp, true);
2662
- hideTransformBadge();
2663
- if (!dragEl) return;
2664
- if (duplicatedForDrag && !moved) {
2665
- // Alt-click with no real drag — remove the premature clone and restore the original selection.
2666
- if (dragEl.parentElement) dragEl.parentElement.removeChild(dragEl);
2667
- selectedEl = originalSelectedEl;
2668
- positionOverlay(selectionOverlay, selectedEl);
2669
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2670
- return;
2671
- }
2672
- if (duplicatedForDrag) {
2673
- postVisualDuplicateChange(originalSelectedEl, dragEl);
2674
- } else {
2675
- window.parent.postMessage({
2676
- type: 'visual-style-change',
2677
- selector: getSelector(dragEl),
2678
- styles: {
2679
- position: dragEl.style.position,
2680
- left: dragEl.style.left,
2681
- top: dragEl.style.top,
2682
- },
2683
- payload: getElementInfo(dragEl),
2684
- }, '*');
2685
- }
2686
- }
2687
- document.addEventListener(events.move, onMove, true);
2688
- document.addEventListener(events.up, onUp, true);
2689
- }
131
+ /**
132
+ * Embedded overview bridge. Forwards wheel events from embedded screen iframes
133
+ * to the parent canvas handler so pan/zoom works over design content.
134
+ *
135
+ * Source: app/components/design/bridge/embedded-wheel.bridge.ts
136
+ * Compiled: .generated/bridge/embedded-wheel.generated.ts (run bridge/codegen.ts to update)
137
+ */
138
+ const EMBEDDED_WHEEL_BRIDGE_SCRIPT = `
139
+ <script data-agent-native-embedded-wheel-bridge>
140
+ ${embeddedWheelBridgeScript}
141
+ </script>
142
+ `;
2690
143
 
2691
- function startResize(handle, e) {
2692
- if (!selectedEl) return;
2693
- if (isLayerInteractionBlocked(selectedEl)) return;
2694
- e.preventDefault();
2695
- e.stopPropagation();
2696
- ensurePositionable(selectedEl);
2697
- var cs = window.getComputedStyle(selectedEl);
2698
- // Bug fix: use CSS width/height (not getBoundingClientRect) for the resize
2699
- // origin dimensions so that rotated elements don't use the inflated
2700
- // axis-aligned bounding box as the starting size.
2701
- var originW = readPx(selectedEl.style.width || cs.width);
2702
- var originH = readPx(selectedEl.style.height || cs.height);
2703
- var origin = {
2704
- left: readPx(selectedEl.style.left || cs.left),
2705
- top: readPx(selectedEl.style.top || cs.top),
2706
- width: originW,
2707
- height: originH,
2708
- ratio: originW / Math.max(1, originH),
2709
- };
2710
- var startX = e.clientX;
2711
- var startY = e.clientY;
2712
- // Snapshot the element so a concurrent clear-selection postMessage cannot
2713
- // cause a null-deref in onMove/onUp.
2714
- var resizeEl = selectedEl;
2715
- // Capture the element rotation once at drag-start so per-move projection is
2716
- // cheap and consistent even if the transform changes during the drag.
2717
- var resizeTheta = currentRotation(resizeEl) * Math.PI / 180;
2718
- function nextRect(ev) {
2719
- var screenDx = ev.clientX - startX;
2720
- var screenDy = ev.clientY - startY;
2721
- // Project the screen-space pointer delta into the element's local
2722
- // (un-rotated) coordinate frame so handles behave relative to the
2723
- // visible rotated box rather than screen axes.
2724
- var cosT = Math.cos(resizeTheta);
2725
- var sinT = Math.sin(resizeTheta);
2726
- var dx = screenDx * cosT + screenDy * sinT;
2727
- var dy = -screenDx * sinT + screenDy * cosT;
2728
- var left = origin.left;
2729
- var top = origin.top;
2730
- var width = origin.width;
2731
- var height = origin.height;
2732
- if (handle.indexOf('w') !== -1) {
2733
- left = origin.left + dx;
2734
- width = origin.width - dx;
2735
- }
2736
- if (handle.indexOf('e') !== -1) width = origin.width + dx;
2737
- if (handle.indexOf('n') !== -1) {
2738
- top = origin.top + dy;
2739
- height = origin.height - dy;
2740
- }
2741
- if (handle.indexOf('s') !== -1) height = origin.height + dy;
2742
- // Apply Shift / scaleToolEnabled aspect-ratio lock BEFORE the min-size
2743
- // clamp so the ratio is computed from unclamped values (bug fix).
2744
- if (ev.shiftKey) {
2745
- // Shift locks aspect ratio for ALL 8 handles (corners and edges).
2746
- if (handle === 'e' || handle === 'w') {
2747
- height = width / origin.ratio;
2748
- } else if (handle === 'n' || handle === 's') {
2749
- width = height * origin.ratio;
2750
- } else if (handle.length === 2) {
2751
- if (Math.abs(dx) > Math.abs(dy)) height = width / origin.ratio;
2752
- else width = height * origin.ratio;
2753
- }
2754
- }
2755
- if (scaleToolEnabled) {
2756
- // Scale tool: enforce aspect ratio on all 8 handles, not just corners.
2757
- if (handle === 'e' || handle === 'w') {
2758
- height = width / origin.ratio;
2759
- } else if (handle === 'n' || handle === 's') {
2760
- width = height * origin.ratio;
2761
- } else if (handle.length === 2) {
2762
- if (Math.abs(dx) > Math.abs(dy)) height = width / origin.ratio;
2763
- else width = height * origin.ratio;
2764
- }
2765
- }
2766
- // Clamp to minimum size.
2767
- var clampedW = Math.max(8, width);
2768
- var clampedH = Math.max(8, height);
2769
- // After clamping, re-apply the ratio if Shift or scale tool is active so
2770
- // the clamped dimension doesn't silently break the locked aspect ratio.
2771
- if (ev.shiftKey || scaleToolEnabled) {
2772
- if (clampedW !== width) {
2773
- // Width was clamped; re-derive height from the clamped width.
2774
- clampedH = Math.max(8, clampedW / origin.ratio);
2775
- } else if (clampedH !== height) {
2776
- // Height was clamped; re-derive width from the clamped height.
2777
- clampedW = Math.max(8, clampedH * origin.ratio);
2778
- }
2779
- }
2780
- width = clampedW;
2781
- height = clampedH;
2782
- // Re-anchor the pinned edge for w/n handles after aspect-ratio lock and
2783
- // clamping so the opposite (e/s) edge stays fixed regardless of whether
2784
- // the dimension change was driven by raw dx/dy or by the ratio lock.
2785
- if (handle.indexOf('w') !== -1) left = origin.left + (origin.width - width);
2786
- if (handle.indexOf('n') !== -1) top = origin.top + (origin.height - height);
2787
- if (ev.altKey) {
2788
- if (handle.indexOf('w') !== -1 || handle.indexOf('e') !== -1) left = origin.left - (width - origin.width) / 2;
2789
- if (handle.indexOf('n') !== -1 || handle.indexOf('s') !== -1) top = origin.top - (height - origin.height) / 2;
2790
- }
2791
- return { left: left, top: top, width: width, height: height };
2792
- }
2793
- function onMove(ev) {
2794
- if (!resizeEl) return;
2795
- var rect = nextRect(ev);
2796
- resizeEl.style.left = Math.round(rect.left) + 'px';
2797
- resizeEl.style.top = Math.round(rect.top) + 'px';
2798
- resizeEl.style.width = Math.round(rect.width) + 'px';
2799
- resizeEl.style.height = Math.round(rect.height) + 'px';
2800
- showTransformBadge(Math.round(rect.width) + ' x ' + Math.round(rect.height), ev.clientX, ev.clientY);
2801
- refreshOverlays();
2802
- }
2803
- function onUp() {
2804
- document.removeEventListener('mousemove', onMove, true);
2805
- document.removeEventListener('mouseup', onUp, true);
2806
- hideTransformBadge();
2807
- if (!resizeEl) return;
2808
- window.parent.postMessage({
2809
- type: 'visual-style-change',
2810
- selector: getSelector(resizeEl),
2811
- styles: {
2812
- position: resizeEl.style.position,
2813
- left: resizeEl.style.left,
2814
- top: resizeEl.style.top,
2815
- width: resizeEl.style.width,
2816
- height: resizeEl.style.height,
2817
- },
2818
- payload: getElementInfo(resizeEl),
2819
- }, '*');
2820
- }
2821
- document.addEventListener('mousemove', onMove, true);
2822
- document.addEventListener('mouseup', onUp, true);
2823
- }
144
+ /**
145
+ * Navigation bridge. ALWAYS injected. Intercepts link clicks and relative form
146
+ * submits inside prototype iframes so they route to the parent instead of
147
+ * navigating the iframe to the Design app itself.
148
+ *
149
+ * Source: app/components/design/bridge/nav.bridge.ts
150
+ * Compiled: .generated/bridge/nav.generated.ts (run bridge/codegen.ts to update)
151
+ */
152
+ const NAV_BRIDGE_SCRIPT = `
153
+ <script data-agent-native-nav-bridge>
154
+ ${navBridgeScript}
155
+ </script>
156
+ `;
2824
157
 
2825
- function startRotate(e) {
2826
- if (!selectedEl) return;
2827
- if (isLayerInteractionBlocked(selectedEl)) return;
2828
- e.preventDefault();
2829
- e.stopPropagation();
2830
- // getBoundingClientRect is correct here we only need the element center
2831
- // for angle math, and the element's visual position is what we want.
2832
- var rect = selectedEl.getBoundingClientRect();
2833
- var center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
2834
- var originAngle = Math.atan2(e.clientY - center.y, e.clientX - center.x) * 180 / Math.PI;
2835
- var originRotation = currentRotation(selectedEl);
2836
- // Snapshot so a concurrent clear-selection postMessage cannot cause a
2837
- // null-deref in onMove/onUp.
2838
- var rotateEl = selectedEl;
2839
- function onMove(ev) {
2840
- if (!rotateEl) return;
2841
- var pointerAngle = Math.atan2(ev.clientY - center.y, ev.clientX - center.x) * 180 / Math.PI;
2842
- var next = originRotation + pointerAngle - originAngle;
2843
- if (ev.shiftKey) next = Math.round(next / 15) * 15;
2844
- next = Math.round(next);
2845
- rotateEl.style.transform = mergeRotation(rotateEl, next);
2846
- showTransformBadge(next + 'deg', ev.clientX, ev.clientY);
2847
- refreshOverlays();
2848
- }
2849
- function onUp() {
2850
- document.removeEventListener('mousemove', onMove, true);
2851
- document.removeEventListener('mouseup', onUp, true);
2852
- hideTransformBadge();
2853
- if (!rotateEl) return;
2854
- window.parent.postMessage({
2855
- type: 'visual-style-change',
2856
- selector: getSelector(rotateEl),
2857
- styles: { transform: rotateEl.style.transform },
2858
- payload: getElementInfo(rotateEl),
2859
- }, '*');
2860
- }
2861
- document.addEventListener('mousemove', onMove, true);
2862
- document.addEventListener('mouseup', onUp, true);
2863
- }
158
+ /**
159
+ * Lightweight hit-test bridge: injected into every screen srcdoc iframe so
160
+ * MultiScreenCanvas can ask "what container is under this point?" during a
161
+ * cross-screen drag without needing to synthesise DOM events.
162
+ *
163
+ * Protocol (parent iframe via postMessage):
164
+ * { type: 'agent-native:hit-test', correlationId: string, x: number, y: number }
165
+ * where x/y are in this iframe's viewport coordinate space.
166
+ *
167
+ * Reply (iframe window.parent):
168
+ * { type: 'agent-native:hit-test-result', correlationId: string,
169
+ * anchorNodeId: string, placement: 'before'|'after'|'inside' }
170
+ *
171
+ * Reads DOM only — no mutations, no event interception. The container-drop and
172
+ * placement logic is intentionally kept in sync with the corresponding helpers
173
+ * inside editor-chrome.bridge.ts (search for "// keep in sync with
174
+ * hit-test.bridge.ts" comments there).
175
+ *
176
+ * Source: app/components/design/bridge/hit-test.bridge.ts
177
+ * Compiled: .generated/bridge/hit-test.generated.ts (run bridge/codegen.ts to update)
178
+ */
179
+ export const LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT = `
180
+ <script data-agent-native-hit-test-bridge>
181
+ ${hitTestBridgeScript}
182
+ </script>
183
+ `;
2864
184
 
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;
185
+ /**
186
+ * Append the LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT into a complete HTML string.
187
+ * Injects before </body> when present, before </html> as a fallback, or appends
188
+ * at the end. This is the seam MultiScreenCanvas uses to add the hit-test
189
+ * responder to each screen srcdoc without rebuilding the entire document.
190
+ */
191
+ export function appendHitTestResponder(html: string): string {
192
+ if (html.includes("</body>")) {
193
+ return html.replace(
194
+ "</body>", // i18n-ignore generated iframe HTML marker
195
+ LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT + "</body>", // i18n-ignore generated iframe HTML injection
196
+ );
2870
197
  }
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);
198
+ if (html.includes("</html>")) {
199
+ return html.replace(
200
+ "</html>", // i18n-ignore generated iframe HTML marker
201
+ LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT + "</html>", // i18n-ignore generated iframe HTML injection
202
+ );
2919
203
  }
204
+ return html + LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT;
205
+ }
2920
206
 
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
- }
2927
- var resizeHandle = e.target && e.target.getAttribute && e.target.getAttribute('data-agent-native-edit-handle');
2928
- if (!resizeHandle && e.target && e.target.getAttribute) {
2929
- resizeHandle = e.target.getAttribute('data-agent-native-edge-handle');
2930
- }
2931
- if (resizeHandle) {
2932
- startResize(resizeHandle, e);
2933
- return;
2934
- }
2935
- var rotateHandle = e.target && e.target.getAttribute && e.target.getAttribute('data-agent-native-rotate-handle');
2936
- if (rotateHandle) {
2937
- startRotate(e);
2938
- return;
2939
- }
2940
- startMove(e);
2941
- }, true);
2942
-
2943
- shieldOverlay.addEventListener('pointerdown', beginPotentialShieldDrag, true);
2944
-
2945
- ['pointerdown','pointerup','mousedown','mouseup','auxclick'].forEach(function(type) {
2946
- shieldOverlay.addEventListener(type, stopNativeInteraction, true);
2947
- });
207
+ const EDITOR_BRIDGE_VAR_NAMES = [
208
+ "--design-editor-accent-color",
209
+ "--design-editor-accent-hover-color",
210
+ "--design-editor-selection-color",
211
+ "--design-editor-accent-strong-color",
212
+ "--design-editor-accent-contrast-color",
213
+ "--design-editor-measure-color",
214
+ "--background",
215
+ "--foreground",
216
+ "--border",
217
+ ];
2948
218
 
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
- }
219
+ function readEditorBridgeThemeVars(): Record<string, string> {
220
+ if (typeof window === "undefined") return {};
221
+ const styles = window.getComputedStyle(document.documentElement);
222
+ return Object.fromEntries(
223
+ EDITOR_BRIDGE_VAR_NAMES.map((name) => [
224
+ name,
225
+ styles.getPropertyValue(name).trim(),
226
+ ]).filter(([, value]) => value.length > 0),
227
+ );
228
+ }
2955
229
 
2956
- ['pointerdown','pointerup','mousedown','mouseup','click','auxclick'].forEach(function(type) {
2957
- document.addEventListener(type, stopBlockedLayerInteraction, true);
230
+ function createEditorBridgeThemeScript(vars: Record<string, string>) {
231
+ const serializedVars = JSON.stringify(vars).replace(/</g, "\\u003c");
232
+ return `
233
+ <script data-agent-native-editor-theme>
234
+ (function() {
235
+ var vars = ${serializedVars};
236
+ var root = document.documentElement;
237
+ Object.keys(vars).forEach(function(name) {
238
+ root.style.setProperty(name, vars[name]);
2958
239
  });
2959
-
2960
- shieldOverlay.addEventListener('click', selectElementAtEvent, true);
2961
- shieldOverlay.addEventListener('contextmenu', openContextMenuAtEvent, true);
2962
- selectionOverlay.addEventListener('contextmenu', openContextMenuAtEvent, true);
2963
- document.addEventListener('contextmenu', function(e) {
2964
- if (isOverlayElement(e.target)) return;
2965
- openContextMenuAtEvent(e);
2966
- }, true);
2967
-
2968
- document.addEventListener('keydown', function(e) {
2969
- if (!shouldForwardDesignHotkey(e)) return;
2970
- stopNativeInteraction(e);
2971
- if (e.key === 'Escape') clearRuntimeSelection();
2972
- window.parent.postMessage({
2973
- type: 'design-hotkey',
2974
- key: e.key,
2975
- code: e.code,
2976
- metaKey: !!e.metaKey,
2977
- ctrlKey: !!e.ctrlKey,
2978
- shiftKey: !!e.shiftKey,
2979
- altKey: !!e.altKey,
2980
- repeat: !!e.repeat
2981
- }, '*');
2982
- }, true);
2983
-
2984
- function placeTextCaretFromPoint(target, clientX, clientY) {
2985
- try {
2986
- var range = null;
2987
- if (document.caretRangeFromPoint) {
2988
- range = document.caretRangeFromPoint(clientX, clientY);
2989
- } else if (document.caretPositionFromPoint) {
2990
- var position = document.caretPositionFromPoint(clientX, clientY);
2991
- if (position) {
2992
- range = document.createRange();
2993
- range.setStart(position.offsetNode, position.offset);
2994
- }
2995
- }
2996
- if (!range) {
2997
- range = document.createRange();
2998
- range.selectNodeContents(target);
2999
- range.collapse(false);
3000
- }
3001
- var selection = window.getSelection();
3002
- selection.removeAllRanges();
3003
- selection.addRange(range);
3004
- } catch (err) {
3005
- try {
3006
- var fallbackRange = document.createRange();
3007
- fallbackRange.selectNodeContents(target);
3008
- fallbackRange.collapse(false);
3009
- var fallbackSelection = window.getSelection();
3010
- fallbackSelection.removeAllRanges();
3011
- fallbackSelection.addRange(fallbackRange);
3012
- } catch (_err) {}
3013
- }
3014
- }
3015
-
3016
- function beginTextEditingFromEvent(e) {
3017
- if (activeTextEditEl && e.target && activeTextEditEl.contains(e.target)) return;
3018
- if (!textEditingEnabled) {
3019
- stopNativeInteraction(e);
3020
- return;
3021
- }
3022
- stopNativeInteraction(e);
3023
- var target = findTextEditTarget(elementFromEditorPoint(e.clientX, e.clientY));
3024
- if (!target || target.nodeType !== 1) return;
3025
- // Anchor the selection identity to the nearest source-backed element. Text
3026
- // editing still operates on the actual target text node, but a later
3027
- // style edit posts from selectedEl, so it must point at a patchable
3028
- // code-layer node rather than a runtime-only descendant (which would emit a
3029
- // brittle body > div:nth-of-type(...) selector that never resolves).
3030
- selectedEl = selectionTargetForHit(target) || target;
3031
- var originalText = target.textContent || '';
3032
- var originalHtml = target.innerHTML || '';
3033
- var committed = false;
3034
- activeTextEditEl = target;
3035
- target.setAttribute('contenteditable', 'true');
3036
- target.setAttribute('data-agent-native-text-editing', 'true');
3037
- target.style.cursor = 'text';
3038
- target.style.outline = '1.5px solid var(--design-editor-accent-color)';
3039
- target.style.outlineOffset = '2px';
3040
- setTextEditingPointerPassthrough(true);
3041
- positionOverlay(selectionOverlay, target);
3042
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(target) }, '*');
3043
- window.parent.postMessage({ type: 'element-dblclick-text', payload: getElementInfo(target) }, '*');
3044
- postTextEditingState(target, true);
3045
-
3046
- function finish(commit) {
3047
- if (committed) return;
3048
- committed = true;
3049
- target.removeEventListener('blur', onBlur, true);
3050
- target.removeEventListener('keydown', onKeyDown, true);
3051
- target.removeEventListener('paste', onPaste, true);
3052
- target.removeEventListener('keyup', onSelectionChange, true);
3053
- target.removeEventListener('mouseup', onSelectionChange, true);
3054
- document.removeEventListener('selectionchange', onSelectionChange);
3055
- target.removeAttribute('contenteditable');
3056
- target.removeAttribute('data-agent-native-text-editing');
3057
- target.style.cursor = '';
3058
- target.style.outline = '';
3059
- target.style.outlineOffset = '';
3060
- setTextEditingPointerPassthrough(false);
3061
- if (activeTextEditEl === target) activeTextEditEl = null;
3062
- postTextEditingState(target, false);
3063
- if (!commit) {
3064
- target.innerHTML = originalHtml;
3065
- refreshOverlays();
3066
- return;
3067
- }
3068
- var next = target.textContent || '';
3069
- var nextHtml = target.innerHTML || '';
3070
- refreshOverlays();
3071
- if (next !== originalText || nextHtml !== originalHtml) {
3072
- postTextContentChange(target, next, nextHtml);
3073
- }
3074
- }
3075
-
3076
- function onBlur() {
3077
- finish(true);
3078
- }
3079
-
3080
- function onKeyDown(ev) {
3081
- if (ev.key === 'Escape') {
3082
- ev.preventDefault();
3083
- finish(true);
3084
- target.blur();
3085
- return;
3086
- }
3087
- if (ev.key === 'Enter' && !ev.shiftKey) {
3088
- ev.preventDefault();
3089
- finish(true);
3090
- target.blur();
3091
- }
3092
- }
3093
-
3094
- function onPaste(ev) {
3095
- ev.preventDefault();
3096
- insertPlainTextAtSelection((ev.clipboardData && ev.clipboardData.getData('text/plain')) || '');
3097
- }
3098
-
3099
- function onSelectionChange() {
3100
- postTextEditingState(target, true);
3101
- }
3102
-
3103
- target.addEventListener('blur', onBlur, true);
3104
- target.addEventListener('keydown', onKeyDown, true);
3105
- target.addEventListener('paste', onPaste, true);
3106
- target.addEventListener('keyup', onSelectionChange, true);
3107
- target.addEventListener('mouseup', onSelectionChange, true);
3108
- document.addEventListener('selectionchange', onSelectionChange);
3109
- target.focus();
3110
- placeTextCaretFromPoint(target, e.clientX, e.clientY);
3111
- }
3112
-
3113
- shieldOverlay.addEventListener('dblclick', beginTextEditingFromEvent, true);
3114
- selectionOverlay.addEventListener('dblclick', beginTextEditingFromEvent, true);
3115
- document.addEventListener('dblclick', function(e) {
3116
- if (isOverlayElement(e.target)) return;
3117
- beginTextEditingFromEvent(e);
3118
- }, true);
3119
-
3120
- shieldOverlay.addEventListener('pointermove', function(e) {
3121
- stopNativeInteraction(e);
3122
- hoveredEl = elementFromEditorPoint(e.clientX, e.clientY);
3123
- if (!hoveredEl) {
3124
- highlightOverlay.style.display = 'none';
3125
- if (!spacingDrag) {
3126
- selectedSpacingHovered = false;
3127
- hoveredSpacingHandleKey = '';
3128
- updateSpacingOverlay(selectedEl);
3129
- }
3130
- hideMeasurements();
3131
- return;
3132
- }
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
- }
3144
- if (hoveredEl === selectedEl) {
3145
- highlightOverlay.style.display = 'none';
3146
- } else {
3147
- positionOverlay(highlightOverlay, hoveredEl);
3148
- }
3149
- if (e.altKey && selectedEl && hoveredEl && selectedEl !== hoveredEl) {
3150
- showMeasurements(selectedEl, hoveredEl);
3151
- } else {
3152
- hideMeasurements();
3153
- }
3154
- var info = getElementInfo(hoveredEl);
3155
- window.parent.postMessage({ type: 'element-hover', payload: info }, '*');
3156
- }, true);
3157
-
3158
- shieldOverlay.addEventListener('pointerleave', function(e) {
3159
- stopNativeInteraction(e);
3160
- hoveredEl = null;
3161
- if (!spacingDrag) {
3162
- selectedSpacingHovered = false;
3163
- hoveredSpacingHandleKey = '';
3164
- updateSpacingOverlay(selectedEl);
3165
- }
3166
- highlightOverlay.style.display = 'none';
3167
- hideMeasurements();
3168
- window.parent.postMessage({ type: 'element-hover', payload: null }, '*');
3169
- }, true);
3170
-
3171
- window.addEventListener('keyup', function(e) {
3172
- if (e.key === 'Alt') hideMeasurements();
3173
- }, true);
3174
-
3175
- window.addEventListener('message', function(e) {
3176
- if (e.source !== window.parent) return;
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
- }
3183
- if (e.data.type === 'set-editor-chrome-scale') {
3184
- // Live-update the constant-size chrome scale WITHOUT rebuilding srcdoc.
3185
- // Rebuilding srcdoc reloads the iframe and flashes the content white.
3186
- editorChromeScaleX = Math.max(0.05, Number(e.data.scaleX) || 1);
3187
- editorChromeScaleY = Math.max(0.05, Number(e.data.scaleY) || editorChromeScaleX);
3188
- applyEditorChromeScale();
3189
- if (selectedEl || hoveredEl) refreshOverlays();
3190
- return;
3191
- }
3192
- if (e.data.type === 'scale-tool-mode') {
3193
- scaleToolEnabled = !!e.data.enabled;
3194
- return;
3195
- }
3196
- if (e.data.type === 'clear-selection') {
3197
- clearRuntimeSelection();
3198
- return;
3199
- }
3200
- if (e.data.type === 'select-element') {
3201
- var candidates = [];
3202
- if (Array.isArray(e.data.selectorCandidates)) {
3203
- e.data.selectorCandidates.forEach(function(selector) {
3204
- if (typeof selector === 'string' && selector && candidates.indexOf(selector) === -1) {
3205
- candidates.push(selector);
3206
- }
3207
- });
3208
- }
3209
- if (e.data.selector && candidates.indexOf(String(e.data.selector)) === -1) {
3210
- candidates.push(String(e.data.selector));
3211
- }
3212
- var target =
3213
- selectedEl &&
3214
- document.documentElement.contains(selectedEl) &&
3215
- matchesExactSelectorList(selectedEl, candidates)
3216
- ? selectedEl
3217
- : null;
3218
- for (var i = 0; i < candidates.length && !target; i += 1) {
3219
- try {
3220
- var matches = document.querySelectorAll(candidates[i]);
3221
- for (var j = 0; j < matches.length; j += 1) {
3222
- if (!isLayerInteractionBlocked(matches[j])) {
3223
- target = matches[j];
3224
- break;
3225
- }
3226
- }
3227
- } catch (_err) {}
3228
- }
3229
- if (!target) return;
3230
- selectedSpacingHovered = false;
3231
- hoveredSpacingHandleKey = '';
3232
- selectedEl = target;
3233
- positionOverlay(selectionOverlay, target);
3234
- if (hoveredEl === selectedEl) highlightOverlay.style.display = 'none';
3235
- return;
3236
- }
3237
- if (e.data.type === 'hover-element') {
3238
- var hoverCandidates = [];
3239
- if (Array.isArray(e.data.selectorCandidates)) {
3240
- e.data.selectorCandidates.forEach(function(selector) {
3241
- if (typeof selector === 'string' && selector && hoverCandidates.indexOf(selector) === -1) {
3242
- hoverCandidates.push(selector);
3243
- }
3244
- });
3245
- }
3246
- if (e.data.selector && hoverCandidates.indexOf(String(e.data.selector)) === -1) {
3247
- hoverCandidates.push(String(e.data.selector));
3248
- }
3249
- if (hoverCandidates.length === 0) {
3250
- hoveredEl = null;
3251
- highlightOverlay.style.display = 'none';
3252
- hideMeasurements();
3253
- return;
3254
- }
3255
- var hoverTarget = findRuntimeTarget(String(e.data.selector || ''), hoverCandidates);
3256
- hoveredEl = hoverTarget;
3257
- if (hoveredEl && !isLayerInteractionBlocked(hoveredEl) && hoveredEl !== selectedEl) {
3258
- positionOverlay(highlightOverlay, hoveredEl);
3259
- } else {
3260
- highlightOverlay.style.display = 'none';
3261
- hideMeasurements();
3262
- }
3263
- return;
3264
- }
3265
- if (e.data.type === 'layer-states') {
3266
- lockedSelectors = Array.isArray(e.data.lockedSelectors) ? e.data.lockedSelectors.filter(function(item) { return typeof item === 'string'; }) : [];
3267
- hiddenSelectors = Array.isArray(e.data.hiddenSelectors) ? e.data.hiddenSelectors.filter(function(item) { return typeof item === 'string'; }) : [];
3268
- if (selectedEl && isLayerInteractionBlocked(selectedEl)) {
3269
- selectedEl = null;
3270
- selectionOverlay.style.display = 'none';
3271
- clearComponentTag();
3272
- }
3273
- if (hoveredEl && isLayerInteractionBlocked(hoveredEl)) {
3274
- hoveredEl = null;
3275
- highlightOverlay.style.display = 'none';
3276
- }
3277
- applyHiddenSelectors();
3278
- return;
3279
- }
3280
- if (e.data.type === 'visual-structure-ack') {
3281
- if (!pendingStructureMove || e.data.requestId !== pendingStructureMove.requestId) return;
3282
- var move = pendingStructureMove;
3283
- pendingStructureMove = null;
3284
- if (e.data.applied) {
3285
- if (move.el && move.el.isConnected) {
3286
- applyRuntimeReorder(move.el, move.target);
3287
- selectedEl = move.el;
3288
- positionOverlay(selectionOverlay, selectedEl);
3289
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
3290
- }
3291
- } else {
3292
- // Revert the optimistic reorder to its pre-drag position.
3293
- if (move.el && move.el.isConnected && move.origin && move.origin.prevParent && move.origin.prevParent.isConnected) {
3294
- move.origin.prevParent.insertBefore(move.el, move.origin.prevNextSibling);
3295
- selectedEl = move.el;
3296
- positionOverlay(selectionOverlay, selectedEl);
3297
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
3298
- }
3299
- }
3300
- return;
3301
- }
3302
- if (e.data.type === 'replace-document-content') {
3303
- replaceRuntimeDocument(
3304
- e.data.content,
3305
- e.data.forceFullDocument ? '' : e.data.selectedSelector,
3306
- e.data.forceFullDocument ? [] : e.data.selectorCandidates
3307
- );
3308
- return;
3309
- }
3310
- if (e.data.type === 'delete-element') {
3311
- removeRuntimeTarget(e.data.selector, e.data.selectorCandidates);
3312
- return;
3313
- }
3314
- if (e.data.type !== 'style-change') return;
3315
- var sel = e.data.selector;
3316
- var prop = e.data.property;
3317
- var val = e.data.value;
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)) {
3325
- postTextContentChange(activeTextEditEl, activeTextEditEl.textContent || '', activeTextEditEl.innerHTML || '');
3326
- refreshOverlays();
3327
- return;
3328
- }
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
- }
3375
- });
3376
-
3377
- window.addEventListener('scroll', refreshOverlays, true);
3378
- window.addEventListener('resize', refreshOverlays);
3379
- applyEditorChromeScale();
3380
240
  })();
3381
241
  </script>
3382
242
  `;
243
+ }
244
+
245
+ /**
246
+ * Editor chrome bridge: blocks native iframe app interaction outside Interact
247
+ * mode and replaces it with element hover/selection overlays. Double-click text
248
+ * editing is enabled only while the editor is specifically in Edit mode.
249
+ */
250
+ const EDITOR_CHROME_BRIDGE_SCRIPT = `
251
+ <script data-agent-native-editor-chrome-bridge>
252
+ ${editorChromeBridgeScript}
253
+ </script>
254
+ `;
3383
255
 
3384
256
  interface DesignCanvasProps {
3385
257
  content: string;
@@ -3702,21 +574,26 @@ export function DesignCanvas({
3702
574
  });
3703
575
 
3704
576
  // Build the srcdoc. The tweak bridge ALWAYS goes in so the panel works
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.
577
+ // outside Edit mode. The editor chrome bridge is omitted for Interact mode
578
+ // so preview/app users can interact with the app normally.
579
+ //
580
+ // readOnly is intentionally NOT a dep here: the bridge is injected whenever
581
+ // !interactMode and the initial __READ_ONLY__ placeholder is baked from the
582
+ // *first* render only. After that, live readOnly changes flow through the
583
+ // set-read-only postMessage (see the useEffect below) so switching the active
584
+ // surface (board ↔ screen) never rebuilds srcdoc / reloads the iframe.
3707
585
  const srcdoc = useMemo(() => {
3708
586
  if (externalPreviewUrl) return undefined;
3709
- const editorChromeBridge =
3710
- interactMode || readOnly
3711
- ? ""
3712
- : createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
3713
- EDITOR_CHROME_BRIDGE_SCRIPT.replace(
3714
- "__READ_ONLY__",
3715
- readOnly ? "true" : "false",
3716
- )
3717
- .replace("__TEXT_EDITING_ENABLED__", editMode ? "true" : "false")
3718
- .replace("__EDITOR_CHROME_SCALE_X__", String(editorChromeScaleX))
3719
- .replace("__EDITOR_CHROME_SCALE_Y__", String(editorChromeScaleY));
587
+ const editorChromeBridge = interactMode
588
+ ? ""
589
+ : createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
590
+ EDITOR_CHROME_BRIDGE_SCRIPT.replace(
591
+ "__READ_ONLY__",
592
+ readOnly ? "true" : "false",
593
+ )
594
+ .replace("__TEXT_EDITING_ENABLED__", editMode ? "true" : "false")
595
+ .replace("__EDITOR_CHROME_SCALE_X__", String(editorChromeScaleX))
596
+ .replace("__EDITOR_CHROME_SCALE_Y__", String(editorChromeScaleY));
3720
597
  const embeddedWheelBridge = EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
3721
598
  "__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
3722
599
  isEmbeddedFrame ? "true" : "false",
@@ -3741,13 +618,13 @@ export function DesignCanvas({
3741
618
  // baked chrome scale. Live zoom updates flow through the set-editor-chrome-scale
3742
619
  // postMessage above. Including them here rebuilds srcdoc on every zoom commit,
3743
620
  // which reloads the iframe and flashes the screen content white.
621
+ // readOnly is intentionally NOT a dep: live changes flow through set-read-only.
3744
622
  // eslint-disable-next-line react-hooks/exhaustive-deps
3745
623
  }, [
3746
624
  editMode,
3747
625
  externalPreviewUrl,
3748
626
  interactMode,
3749
627
  isEmbeddedFrame,
3750
- readOnly,
3751
628
  renderedContent,
3752
629
  ]);
3753
630
 
@@ -4186,6 +1063,44 @@ export function DesignCanvas({
4186
1063
  );
4187
1064
  }, [editorChromeScaleX, editorChromeScaleY]);
4188
1065
 
1066
+ // Sync readOnly to the bridge IN-PLACE via postMessage so switching the active
1067
+ // surface (board ↔ screen) does not rebuild srcdoc / reload the iframe.
1068
+ // The initial baked __READ_ONLY__ placeholder covers first paint; subsequent
1069
+ // changes arrive here. Also re-send on iframe load so the bridge is in sync
1070
+ // after any content-key-driven reload.
1071
+ const readOnlyRef = useRef(readOnly);
1072
+ readOnlyRef.current = readOnly;
1073
+ useEffect(() => {
1074
+ const iframe = iframeRef.current;
1075
+ if (!iframe) return;
1076
+ function sendReadOnly() {
1077
+ iframe!.contentWindow?.postMessage(
1078
+ { type: "set-read-only", readOnly: readOnlyRef.current },
1079
+ "*",
1080
+ );
1081
+ }
1082
+ sendReadOnly();
1083
+ iframe.addEventListener("load", sendReadOnly);
1084
+ return () => iframe.removeEventListener("load", sendReadOnly);
1085
+ // Only re-run when readOnly changes; iframe identity is stable.
1086
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1087
+ }, [readOnly]);
1088
+
1089
+ /**
1090
+ * Trigger immediate text-editing mode for a specific node inside the iframe,
1091
+ * identified by its data-agent-native-node-id value. Call this after
1092
+ * programmatically creating a TEXT primitive so the user can type right away
1093
+ * without a second click.
1094
+ *
1095
+ * Posts { type: 'begin-text-edit', nodeId } to the iframe bridge.
1096
+ * The bridge enters the same contenteditable text-editing path used on dblclick.
1097
+ */
1098
+ const beginTextEdit = useCallback((nodeId: string) => {
1099
+ const iframe = iframeRef.current;
1100
+ if (!iframe?.contentWindow || !nodeId) return;
1101
+ iframe.contentWindow.postMessage({ type: "begin-text-edit", nodeId }, "*");
1102
+ }, []);
1103
+
4189
1104
  const sendStyleChange = useCallback(
4190
1105
  (
4191
1106
  selector: string,
@@ -4312,6 +1227,9 @@ export function DesignCanvas({
4312
1227
  (window as any).__designCanvasSendShaderFillPreview = sendShaderFillPreview;
4313
1228
  (window as any).__designCanvasClearShaderFillPreview =
4314
1229
  clearShaderFillPreview;
1230
+ // Imperative text-edit entry — call after creating a TEXT primitive so the
1231
+ // user can type immediately without a second click.
1232
+ (window as any).__designCanvasBeginTextEdit = beginTextEdit;
4315
1233
  return () => {
4316
1234
  // Identity-guard each delete so a stale unmounting instance never clobbers
4317
1235
  // a freshly mounted instance's bridge during a remount race.
@@ -4350,6 +1268,9 @@ export function DesignCanvas({
4350
1268
  ) {
4351
1269
  delete (window as any).__designCanvasClearShaderFillPreview;
4352
1270
  }
1271
+ if ((window as any).__designCanvasBeginTextEdit === beginTextEdit) {
1272
+ delete (window as any).__designCanvasBeginTextEdit;
1273
+ }
4353
1274
  };
4354
1275
  }, [
4355
1276
  deleteRuntimeElement,
@@ -4360,6 +1281,7 @@ export function DesignCanvas({
4360
1281
  clearMotionPreview,
4361
1282
  sendShaderFillPreview,
4362
1283
  clearShaderFillPreview,
1284
+ beginTextEdit,
4363
1285
  ]);
4364
1286
 
4365
1287
  // Device dimensions match real-world devices. iframes are replaced elements