@agent-native/core 0.80.10 → 0.81.0

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 (267) 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/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +419 -19
  6. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  7. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  8. package/corpus/core/src/client/chat/index.ts +1 -0
  9. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  10. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  11. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  12. package/corpus/core/src/client/index.ts +1 -0
  13. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  15. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  17. package/corpus/core/src/db/client.ts +29 -2
  18. package/corpus/core/src/provider-api/index.ts +29 -0
  19. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  20. package/corpus/core/src/styles/agent-native.css +54 -0
  21. package/corpus/core/src/usage/store.ts +2 -2
  22. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  23. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  24. package/corpus/templates/analytics/app/global.css +8 -0
  25. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  26. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  27. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  29. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  30. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  31. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  36. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  38. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  39. package/corpus/templates/assets/AGENTS.md +4 -0
  40. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  42. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  43. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  44. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  46. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  47. package/corpus/templates/assets/app/global.css +15 -0
  48. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  49. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  50. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  51. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  52. package/corpus/templates/assets/app/routes/library.tsx +233 -52
  53. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  54. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  56. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  57. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  58. package/corpus/templates/assets/shared/api.ts +4 -0
  59. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  60. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  61. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  64. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  66. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  67. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  68. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  69. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  70. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  71. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  72. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  76. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  77. package/corpus/templates/design/AGENTS.md +27 -0
  78. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  79. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  80. package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
  81. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  82. package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
  83. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  84. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  85. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  86. package/corpus/templates/design/actions/delete-design.ts +20 -0
  87. package/corpus/templates/design/actions/export-zip.ts +23 -8
  88. package/corpus/templates/design/actions/generate-design.ts +53 -1
  89. package/corpus/templates/design/actions/get-component-details.ts +21 -26
  90. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  91. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  92. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  93. package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
  94. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  95. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  96. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  97. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  98. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  99. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  100. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  101. package/corpus/templates/design/actions/navigate.ts +18 -3
  102. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  103. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  104. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  105. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  106. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  107. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  108. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  109. package/corpus/templates/design/actions/run-design-audit.ts +74 -10
  110. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
  118. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
  120. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
  121. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
  122. package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
  123. package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
  124. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  125. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  126. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  127. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  128. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  129. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  131. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  132. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  133. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  134. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  136. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  137. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  138. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  139. package/corpus/templates/design/app/global.css +7 -0
  140. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  141. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  142. package/corpus/templates/design/app/i18n-data.ts +369 -0
  143. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
  145. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  146. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  171. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  172. package/corpus/templates/design/e2e/helpers.ts +168 -65
  173. package/corpus/templates/design/package.json +3 -1
  174. package/corpus/templates/design/server/db/schema.ts +23 -0
  175. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  176. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  177. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  178. package/corpus/templates/design/server/plugins/db.ts +39 -0
  179. package/corpus/templates/design/server/register-secrets.ts +38 -0
  180. package/corpus/templates/design/shared/board-file.ts +228 -0
  181. package/corpus/templates/design/shared/board-objects.ts +288 -0
  182. package/corpus/templates/design/shared/code-layer.ts +52 -0
  183. package/corpus/templates/design/shared/component-model.ts +35 -0
  184. package/corpus/templates/design/shared/motion-compiler.ts +137 -15
  185. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  186. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  187. package/corpus/templates/design/tsconfig.json +2 -1
  188. package/corpus/templates/slides/AGENTS.md +9 -0
  189. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  190. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  191. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  192. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  193. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  194. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  195. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  196. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  197. package/corpus/templates/slides/package.json +1 -0
  198. package/corpus/templates/slides/vite.config.ts +1 -0
  199. package/dist/agent/production-agent.d.ts.map +1 -1
  200. package/dist/agent/production-agent.js +94 -10
  201. package/dist/agent/production-agent.js.map +1 -1
  202. package/dist/cli/design-connect.d.ts +25 -0
  203. package/dist/cli/design-connect.d.ts.map +1 -1
  204. package/dist/cli/design-connect.js +357 -18
  205. package/dist/cli/design-connect.js.map +1 -1
  206. package/dist/client/AssistantChat.d.ts +14 -0
  207. package/dist/client/AssistantChat.d.ts.map +1 -1
  208. package/dist/client/AssistantChat.js +115 -22
  209. package/dist/client/AssistantChat.js.map +1 -1
  210. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  211. package/dist/client/ConnectBuilderCard.js +1 -0
  212. package/dist/client/ConnectBuilderCard.js.map +1 -1
  213. package/dist/client/chat/index.d.ts +1 -1
  214. package/dist/client/chat/index.d.ts.map +1 -1
  215. package/dist/client/chat/index.js +1 -1
  216. package/dist/client/chat/index.js.map +1 -1
  217. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  218. package/dist/client/chat/run-recovery.js +5 -5
  219. package/dist/client/chat/run-recovery.js.map +1 -1
  220. package/dist/client/chat-view-transition.d.ts +6 -0
  221. package/dist/client/chat-view-transition.d.ts.map +1 -1
  222. package/dist/client/chat-view-transition.js +23 -2
  223. package/dist/client/chat-view-transition.js.map +1 -1
  224. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  225. package/dist/client/composer/PromptComposer.js +3 -2
  226. package/dist/client/composer/PromptComposer.js.map +1 -1
  227. package/dist/client/index.d.ts +1 -1
  228. package/dist/client/index.d.ts.map +1 -1
  229. package/dist/client/index.js +1 -1
  230. package/dist/client/index.js.map +1 -1
  231. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  232. package/dist/client/org/OrgSwitcher.js +11 -4
  233. package/dist/client/org/OrgSwitcher.js.map +1 -1
  234. package/dist/client/sharing/ShareButton.js +3 -3
  235. package/dist/client/sharing/ShareButton.js.map +1 -1
  236. package/dist/client/sse-event-processor.d.ts.map +1 -1
  237. package/dist/client/sse-event-processor.js +10 -0
  238. package/dist/client/sse-event-processor.js.map +1 -1
  239. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  240. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  241. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  242. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  243. package/dist/collab/awareness.d.ts +2 -2
  244. package/dist/collab/awareness.d.ts.map +1 -1
  245. package/dist/collab/routes.d.ts +2 -2
  246. package/dist/db/client.d.ts +1 -0
  247. package/dist/db/client.d.ts.map +1 -1
  248. package/dist/db/client.js +28 -2
  249. package/dist/db/client.js.map +1 -1
  250. package/dist/notifications/routes.d.ts +3 -3
  251. package/dist/observability/routes.d.ts +3 -3
  252. package/dist/progress/routes.d.ts +1 -1
  253. package/dist/provider-api/index.d.ts +5 -4
  254. package/dist/provider-api/index.d.ts.map +1 -1
  255. package/dist/provider-api/index.js +28 -0
  256. package/dist/provider-api/index.js.map +1 -1
  257. package/dist/resources/handlers.d.ts +3 -3
  258. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  259. package/dist/server/core-routes-plugin.d.ts +25 -0
  260. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  261. package/dist/server/core-routes-plugin.js +43 -18
  262. package/dist/server/core-routes-plugin.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +54 -0
  265. package/dist/usage/store.js +2 -2
  266. package/dist/usage/store.js.map +1 -1
  267. 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,3204 +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
-
103
- function lerp(a, b, ratio) {
104
- var numA = parseFloat(a);
105
- var numB = parseFloat(b);
106
- if (Number.isFinite(numA) && Number.isFinite(numB)) {
107
- var suffix = a.replace(/^-?[\\d.]+/, '') || b.replace(/^-?[\\d.]+/, '');
108
- return (numA + (numB - numA) * ratio).toFixed(4).replace(/\\.?0+$/, '') + suffix;
109
- }
110
- // Non-numeric: snap to b after the midpoint.
111
- return ratio < 0.5 ? a : b;
112
- }
113
-
114
- function interpolate(keyframes, t) {
115
- if (!keyframes || keyframes.length === 0) return '';
116
- if (keyframes.length === 1) return keyframes[0].value;
117
- // Find surrounding keyframes.
118
- var prev = keyframes[0];
119
- var next = keyframes[keyframes.length - 1];
120
- for (var i = 0; i < keyframes.length - 1; i++) {
121
- if (t >= keyframes[i].t && t <= keyframes[i + 1].t) {
122
- prev = keyframes[i];
123
- next = keyframes[i + 1];
124
- break;
125
- }
126
- }
127
- var span = next.t - prev.t;
128
- if (span <= 0) return prev.value;
129
- var ratio = Math.max(0, Math.min(1, (t - prev.t) / span));
130
- return lerp(prev.value, next.value, ratio);
131
- }
132
-
133
- function applyPreview(t) {
134
- for (var i = 0; i < loadedTracks.length; i++) {
135
- var track = loadedTracks[i];
136
- var el = document.querySelector('[data-agent-native-node-id="' + track.targetNodeId + '"]');
137
- if (!el) continue;
138
- var value = interpolate(track.keyframes, t);
139
- if (value === '') continue;
140
- el.style[track.property] = value;
141
- if (!touchedProps[track.targetNodeId]) touchedProps[track.targetNodeId] = [];
142
- if (touchedProps[track.targetNodeId].indexOf(track.property) === -1) {
143
- touchedProps[track.targetNodeId].push(track.property);
144
- }
145
- }
146
- }
147
-
148
- function clearPreview() {
149
- var nodeIds = Object.keys(touchedProps);
150
- for (var i = 0; i < nodeIds.length; i++) {
151
- var el = document.querySelector('[data-agent-native-node-id="' + nodeIds[i] + '"]');
152
- if (!el) continue;
153
- var props = touchedProps[nodeIds[i]];
154
- for (var j = 0; j < props.length; j++) {
155
- el.style[props[j]] = '';
156
- }
157
- }
158
- touchedProps = {};
159
- loadedTracks = [];
160
- }
161
-
162
- window.addEventListener('message', function(e) {
163
- if (e.source !== window.parent) return;
164
- if (!e.data || typeof e.data.type !== 'string') return;
165
- if (e.data.type === 'motion-load-tracks') {
166
- loadedTracks = Array.isArray(e.data.tracks) ? e.data.tracks : [];
167
- touchedProps = {};
168
- return;
169
- }
170
- if (e.data.type === 'motion-preview') {
171
- var t = Number(e.data.t);
172
- if (!Number.isFinite(t)) return;
173
- t = Math.max(0, Math.min(1, t));
174
- applyPreview(t);
175
- return;
176
- }
177
- if (e.data.type === 'motion-preview-clear') {
178
- clearPreview();
179
- return;
180
- }
181
- });
182
- })();
183
- </script>
184
- `;
185
-
186
- /**
187
- * Shader-fill preview bridge. ALWAYS injected alongside the other bridge
188
- * scripts so the parent can apply a CSS gradient approximation of a shader
189
- * fill to the currently-selected element **without** persisting anything.
190
- *
191
- * Protocol (parent → iframe):
192
- *
193
- * { type: 'shader-fill-preview', selector, nodeId, css }
194
- * Apply `css` as the `background` inline style on the first element that
195
- * matches `selector` (preferred) or `[data-agent-native-node-id="nodeId"]`.
196
- * When both are absent, targets `document.body`. Stores the previous
197
- * background value so it can be restored on clear.
198
- * Preview-only — never writes to DB, Yjs, or source files.
199
- *
200
- * { type: 'shader-fill-preview-clear' }
201
- * Remove the applied background override and restore the previous value.
202
- * Called when the user discards the preview or switches selections.
203
- */
204
- const SHADER_FILL_PREVIEW_BRIDGE_SCRIPT = `
205
- <script data-agent-native-shader-fill-preview-bridge>
206
- (function() {
207
- // Track the element we patched and its original background so we can undo.
208
- var patchedEl = null;
209
- var originalBackground = '';
210
-
211
- function resolveTarget(selector, nodeId) {
212
- if (selector) {
213
- try {
214
- var hit = document.querySelector(selector);
215
- if (hit) return hit;
216
- } catch (_err) {}
217
- }
218
- if (nodeId) {
219
- var byId = document.querySelector('[data-agent-native-node-id="' + nodeId.replace(/"/g, '\\\\"') + '"]');
220
- if (byId) return byId;
221
- }
222
- return document.body;
223
- }
224
-
225
- function applyPreview(selector, nodeId, css) {
226
- // Clear any prior patch first so we don't stack patches.
227
- clearPreview();
228
- var el = resolveTarget(selector, nodeId);
229
- if (!el) return;
230
- originalBackground = el.style.background || '';
231
- el.style.background = css || '';
232
- patchedEl = el;
233
- }
234
-
235
- function clearPreview() {
236
- if (!patchedEl) return;
237
- patchedEl.style.background = originalBackground;
238
- patchedEl = null;
239
- originalBackground = '';
240
- }
241
-
242
- window.addEventListener('message', function(e) {
243
- if (e.source !== window.parent) return;
244
- if (!e.data || typeof e.data.type !== 'string') return;
245
- if (e.data.type === 'shader-fill-preview') {
246
- var selector = typeof e.data.selector === 'string' ? e.data.selector : '';
247
- var nodeId = typeof e.data.nodeId === 'string' ? e.data.nodeId : '';
248
- var css = typeof e.data.css === 'string' ? e.data.css : '';
249
- applyPreview(selector, nodeId, css);
250
- return;
251
- }
252
- if (e.data.type === 'shader-fill-preview-clear') {
253
- clearPreview();
254
- return;
255
- }
256
- });
257
- })();
258
- </script>
259
- `;
260
-
261
- /**
262
- * Tweak-bridge script. ALWAYS injected so the parent's postMessage
263
- * (`tweak-values`) can update CSS custom properties on the iframe's :root
264
- * regardless of which editor mode is active. Without this the tweak panel
265
- * silently no-ops in the default Comment mode.
266
- */
267
- const TWEAK_BRIDGE_SCRIPT = `
268
- <script data-agent-native-tweak-bridge>
269
- (function() {
270
- window.addEventListener('message', function(e) {
271
- if (e.source !== window.parent) return;
272
- if (!e.data || e.data.type !== 'tweak-values') return;
273
- var root = document.documentElement;
274
- var vals = e.data.values || {};
275
- Object.keys(vals).forEach(function(k) {
276
- root.style.setProperty(k, vals[k]);
277
- });
278
- });
279
- })();
280
- </script>
281
- `;
282
-
283
- /**
284
- * Pinch-zoom bridge: forwards trackpad pinch / Cmd-Ctrl+scroll wheel events
285
- * from inside the iframe to the parent window. Wheel events don't naturally
286
- * bubble out of an iframe, so without this the user can only pinch in the
287
- * empty area around the canvas, not over the design itself.
288
- */
289
- const ZOOM_BRIDGE_SCRIPT = `
290
- <script data-agent-native-zoom-bridge>
291
- (function() {
292
- // Attach to documentElement (not window/document) so { passive: false }
293
- // is honored consistently and the browser doesn't natively pinch-zoom the
294
- // iframe's own document alongside the parent's zoom.
295
- var target = document.documentElement || document.body || document;
296
- function onWheel(e) {
297
- if (!(e.ctrlKey || e.metaKey)) return;
298
- e.preventDefault();
299
- try {
300
- window.parent.postMessage({
301
- type: 'pinch-zoom-wheel',
302
- deltaY: e.deltaY,
303
- clientX: e.clientX,
304
- clientY: e.clientY,
305
- }, '*');
306
- } catch (err) {}
307
- }
308
- target.addEventListener('wheel', onWheel, { passive: false, capture: true });
309
- })();
310
- </script>
311
- `;
312
-
313
- /**
314
- * Embedded overview bridge. A screen preview is a real iframe, so normal wheel
315
- * events never bubble to the overview canvas underneath. In embedded mode we
316
- * forward a bounded wheel payload to the parent so the existing canvas wheel
317
- * handler can pan/zoom exactly as if the pointer were over empty canvas.
318
- */
319
- const EMBEDDED_WHEEL_BRIDGE_SCRIPT = `
320
- <script data-agent-native-embedded-wheel-bridge>
321
- (function() {
322
- var enabled = __EMBEDDED_WHEEL_FORWARDING_ENABLED__;
323
- if (!enabled) return;
324
- function clamp(value, limit) {
325
- var number = Number(value) || 0;
326
- if (number > limit) return limit;
327
- if (number < -limit) return -limit;
328
- return number;
329
- }
330
- function onWheel(e) {
331
- e.preventDefault();
332
- e.stopPropagation();
333
- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
334
- try {
335
- window.parent.postMessage({
336
- type: 'embedded-canvas-wheel',
337
- deltaX: clamp(e.deltaX, 240),
338
- deltaY: clamp(e.deltaY, 240),
339
- deltaZ: clamp(e.deltaZ, 240),
340
- deltaMode: e.deltaMode,
341
- clientX: e.clientX,
342
- clientY: e.clientY,
343
- ctrlKey: !!e.ctrlKey,
344
- metaKey: !!e.metaKey,
345
- shiftKey: !!e.shiftKey,
346
- altKey: !!e.altKey,
347
- }, '*');
348
- } catch (err) {}
349
- }
350
- var target = document.documentElement || document.body || document;
351
- target.addEventListener('wheel', onWheel, { passive: false, capture: true });
352
- })();
353
- </script>
354
- `;
355
-
356
- /**
357
- * Navigation bridge. ALWAYS injected. A prototype lives in a `srcdoc` iframe,
358
- * so a plain `<a href="/pricing">` resolves the relative URL against the PARENT
359
- * app document and navigates the iframe to the Design app itself ("Design not
360
- * found"), nuking the prototype. We intercept link clicks + relative form
361
- * submits and route them to the parent instead:
362
- * - in-page anchors (`#...`) and `javascript:`/`@click` handlers: left alone
363
- * - external `http(s)`/`//` links: opened in a new tab by the parent
364
- * - internal/relative links (or an explicit `data-screen`): asked to switch
365
- * to the matching screen in a multi-screen design; otherwise a no-op so the
366
- * prototype never blows itself away.
367
- */
368
- const NAV_BRIDGE_SCRIPT = `
369
- <script data-agent-native-nav-bridge>
370
- (function() {
371
- function classify(href) {
372
- var h = (href || '').trim();
373
- if (!h) return null;
374
- var lower = h.toLowerCase();
375
- if (lower.charAt(0) === '#') return null;
376
- if (lower.indexOf('javascript:') === 0) return null;
377
- if (lower.indexOf('mailto:') === 0 || lower.indexOf('tel:') === 0) {
378
- return { external: true, href: h };
379
- }
380
- if (/^https?:\\/\\//i.test(h) || /^\\/\\//.test(h)) {
381
- return { external: true, href: h };
382
- }
383
- var screen = h.replace(/^\\.?\\//, '').split(/[?#]/)[0];
384
- return { external: false, href: h, screen: screen };
385
- }
386
- document.addEventListener('click', function(e) {
387
- var t = e.target;
388
- if (!t || !t.closest) return;
389
- var a = t.closest('a[href], [data-screen]');
390
- if (!a) return;
391
- var ds = a.getAttribute && a.getAttribute('data-screen');
392
- // In-page anchors ('#...') and empty hrefs must be handled in-document.
393
- // A srcdoc document resolves '#'/'' against the PARENT app URL, so the
394
- // browser's default action would navigate the iframe to the app itself.
395
- if (!ds) {
396
- var rawHref = a.getAttribute('href');
397
- if (rawHref != null) {
398
- var hh = rawHref.trim();
399
- if (hh === '' || hh.charAt(0) === '#') {
400
- e.preventDefault();
401
- var fid = hh.charAt(0) === '#' ? hh.slice(1) : '';
402
- var tgt = fid ? document.getElementById(fid) : null;
403
- if (tgt && tgt.scrollIntoView) {
404
- tgt.scrollIntoView({ behavior: 'smooth', block: 'start' });
405
- } else {
406
- window.scrollTo({ top: 0, behavior: 'smooth' });
407
- }
408
- return;
409
- }
410
- }
411
- }
412
- var info = ds
413
- ? { external: false, href: ds, screen: ds.replace(/^\\.?\\//, '').split(/[?#]/)[0] }
414
- : classify(a.getAttribute('href'));
415
- if (!info) return;
416
- if (info.external) {
417
- // Open external links in a new tab from the iframe itself (the sandbox
418
- // grants allow-popups), bound to this real user click. We deliberately do
419
- // NOT round-trip through the parent: a parent window.open() driven by
420
- // postMessage would let any script in here spawn popups without a gesture.
421
- try {
422
- a.setAttribute('target', '_blank');
423
- a.setAttribute('rel', 'noopener noreferrer');
424
- } catch (err) {}
425
- return; // allow the native click to proceed
426
- }
427
- e.preventDefault();
428
- try {
429
- window.parent.postMessage({
430
- type: 'prototype-navigate',
431
- href: info.href,
432
- screen: info.screen || '',
433
- }, '*');
434
- } catch (err) {}
435
- }, true);
436
- document.addEventListener('submit', function(e) {
437
- var f = e.target;
438
- if (!f || f.tagName !== 'FORM') return;
439
- var action = f.getAttribute('action') || '';
440
- if (/^https?:\\/\\//i.test(action)) return;
441
- e.preventDefault();
442
- }, true);
443
- })();
444
- </script>
445
- `;
446
-
447
- const EDITOR_BRIDGE_VAR_NAMES = [
448
- "--design-editor-accent-color",
449
- "--design-editor-accent-hover-color",
450
- "--design-editor-selection-color",
451
- "--design-editor-accent-strong-color",
452
- "--design-editor-accent-contrast-color",
453
- "--design-editor-measure-color",
454
- "--background",
455
- "--foreground",
456
- "--border",
457
- ];
458
-
459
- function readEditorBridgeThemeVars(): Record<string, string> {
460
- if (typeof window === "undefined") return {};
461
- const styles = window.getComputedStyle(document.documentElement);
462
- return Object.fromEntries(
463
- EDITOR_BRIDGE_VAR_NAMES.map((name) => [
464
- name,
465
- styles.getPropertyValue(name).trim(),
466
- ]).filter(([, value]) => value.length > 0),
467
- );
468
- }
469
-
470
- function createEditorBridgeThemeScript(vars: Record<string, string>) {
471
- const serializedVars = JSON.stringify(vars).replace(/</g, "\\u003c");
472
- return `
473
- <script data-agent-native-editor-theme>
474
- (function() {
475
- var vars = ${serializedVars};
476
- var root = document.documentElement;
477
- Object.keys(vars).forEach(function(name) {
478
- root.style.setProperty(name, vars[name]);
479
- });
480
- })();
481
- </script>
482
- `;
483
- }
484
-
485
- /**
486
- * Editor chrome bridge: blocks native iframe app interaction outside Interact
487
- * mode and replaces it with element hover/selection overlays. Double-click text
488
- * editing is enabled only while the editor is specifically in Edit mode.
489
- */
490
- const EDITOR_CHROME_BRIDGE_SCRIPT = `
491
- <script data-agent-native-editor-chrome-bridge>
492
- (function() {
493
- var readOnly = __READ_ONLY__;
494
- var textEditingEnabled = !readOnly && __TEXT_EDITING_ENABLED__;
495
- var scaleToolEnabled = false;
496
- var editorChromeScaleX = Math.max(0.05, Number(__EDITOR_CHROME_SCALE_X__) || 1);
497
- var editorChromeScaleY = Math.max(0.05, Number(__EDITOR_CHROME_SCALE_Y__) || editorChromeScaleX);
498
-
499
- // Ease the constant-size selection chrome to its new size when overview zoom
500
- // settles (parent posts set-editor-chrome-scale), matching the canvas chrome.
501
- // Only chrome-scale-driven props animate; the overlay's live position is excluded.
502
- (function () {
503
- var chromeTransitionStyle = document.createElement('style');
504
- chromeTransitionStyle.textContent =
505
- '[data-agent-native-edit-overlay="selection"]{transition:border-width 150ms ease-out}' +
506
- '[data-agent-native-edge-handle],[data-agent-native-edit-handle],[data-agent-native-rotate-handle]{transition:width 150ms ease-out,height 150ms ease-out,border-width 150ms ease-out,top 150ms ease-out,bottom 150ms ease-out,left 150ms ease-out,right 150ms ease-out}' +
507
- '[data-agent-native-spacing-line]{position:absolute;display:none;pointer-events:none;border-radius:999px}' +
508
- '[data-agent-native-spacing-region]{position:absolute;display:none;box-sizing:border-box;pointer-events:auto;background-size:6px 6px}' +
509
- '[data-agent-native-spacing-region][data-orientation="vertical"]{cursor:ew-resize}' +
510
- '[data-agent-native-spacing-region][data-orientation="horizontal"]{cursor:ns-resize}';
511
- (document.head || document.documentElement).appendChild(chromeTransitionStyle);
512
- })();
513
-
514
- function chromeScaleX() {
515
- return 1 / Math.max(0.05, editorChromeScaleX);
516
- }
517
-
518
- function chromeScaleY() {
519
- return 1 / Math.max(0.05, editorChromeScaleY);
520
- }
521
-
522
- function chromeLineScale() {
523
- return 1 / Math.max(0.05, Math.max(editorChromeScaleX, editorChromeScaleY));
524
- }
525
-
526
- function syncEditorChromeScaleVars() {
527
- document.documentElement.style.setProperty('--agent-native-editor-chrome-scale-x', String(chromeScaleX()));
528
- document.documentElement.style.setProperty('--agent-native-editor-chrome-scale-y', String(chromeScaleY()));
529
- document.documentElement.style.setProperty('--agent-native-editor-chrome-line-scale', String(chromeLineScale()));
530
- }
531
-
532
- function escapeIdent(value) {
533
- if (window.CSS && typeof window.CSS.escape === 'function') {
534
- return window.CSS.escape(value);
535
- }
536
- return String(value).replace(/[^a-zA-Z0-9_-]/g, '\\\\$&');
537
- }
538
-
539
- function escapeAttribute(value) {
540
- return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
541
- }
542
-
543
- function attributeSelector(el, name) {
544
- var value = el && el.getAttribute && el.getAttribute(name);
545
- return value ? '[' + name + '="' + escapeAttribute(value) + '"]' : '';
546
- }
547
-
548
- function classSelectorSuffix(el, maxCount) {
549
- if (!el || !el.classList) return '';
550
- return Array.prototype.slice.call(el.classList, 0, maxCount)
551
- .map(function(token) { return '.' + escapeIdent(token); })
552
- .join('');
553
- }
554
-
555
- function selectorPart(el) {
556
- if (!el || !el.tagName) return '';
557
- var stableSelector =
558
- attributeSelector(el, 'data-agent-native-node-id') ||
559
- attributeSelector(el, 'data-code-layer-id') ||
560
- attributeSelector(el, 'data-layer-id') ||
561
- attributeSelector(el, 'data-builder-id') ||
562
- attributeSelector(el, 'data-loc');
563
- if (stableSelector) return el.tagName.toLowerCase() + stableSelector;
564
- if (el.id) return '#' + escapeIdent(el.id);
565
- var part = el.tagName.toLowerCase() + (stableSelector || classSelectorSuffix(el, 2));
566
- var parent = el.parentElement;
567
- if (parent) {
568
- var sameTag = Array.prototype.filter.call(
569
- parent.children,
570
- function(child) { return child.tagName === el.tagName; }
571
- );
572
- if (sameTag.length > 1) {
573
- part += ':nth-of-type(' + (sameTag.indexOf(el) + 1) + ')';
574
- }
575
- }
576
- return part;
577
- }
578
-
579
- function selectorPath(el, stopEl) {
580
- var parts = [];
581
- var node = el;
582
- while (node && node.nodeType === 1) {
583
- if (node !== stopEl) parts.unshift(selectorPart(node));
584
- if (node === stopEl) break;
585
- node = node.parentElement;
586
- }
587
- return parts.slice(-5).join(' > ');
588
- }
589
-
590
- function getSourceId(el) {
591
- if (!el || !el.getAttribute) return '';
592
- return (
593
- el.getAttribute('data-agent-native-node-id') ||
594
- el.getAttribute('data-code-layer-id') ||
595
- el.getAttribute('data-layer-id') ||
596
- el.getAttribute('data-builder-id') ||
597
- el.getAttribute('data-loc') ||
598
- el.id ||
599
- ''
600
- );
601
- }
602
-
603
- function isDocumentRootElement(el) {
604
- return el === document.body || el === document.documentElement;
605
- }
606
-
607
- function closestStableSourceElement(el) {
608
- if (!el || !el.closest) return null;
609
- var stable = el.closest('[data-agent-native-node-id],[data-code-layer-id],[data-layer-id],[data-builder-id],[data-loc]');
610
- if (!stable || isDocumentRootElement(stable)) return null;
611
- return stable;
612
- }
613
-
614
- function hasStableOwnSource(el) {
615
- return !!(
616
- el &&
617
- !isDocumentRootElement(el) &&
618
- getSourceId(el)
619
- );
620
- }
621
-
622
- function selectionTargetForHit(hit) {
623
- if (!hit || isDocumentRootElement(hit)) return hit;
624
- if (selectedEl && hit !== selectedEl && selectedEl.contains(hit)) return hit;
625
- if (hasStableOwnSource(hit)) return hit;
626
- return closestStableSourceElement(hit) || hit;
627
- }
628
-
629
- function freshRuntimeNodeId(prefix) {
630
- var random = '';
631
- try {
632
- if (window.crypto && window.crypto.getRandomValues) {
633
- var bytes = new Uint32Array(2);
634
- window.crypto.getRandomValues(bytes);
635
- random = Array.prototype.map.call(bytes, function(part) {
636
- return part.toString(36);
637
- }).join('');
638
- }
639
- } catch (_err) {}
640
- if (!random) random = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
641
- return 'an-' + String(prefix || 'copy') + '-' + random;
642
- }
643
-
644
- function resetRuntimeStableIds(root) {
645
- if (!root || !root.querySelectorAll) return;
646
- var nodes = [root].concat(Array.prototype.slice.call(root.querySelectorAll('[data-agent-native-node-id]')));
647
- nodes.forEach(function(node, index) {
648
- if (node && node.setAttribute) {
649
- node.setAttribute('data-agent-native-node-id', freshRuntimeNodeId(index === 0 ? 'copy' : 'copy-child'));
650
- }
651
- });
652
- }
653
-
654
- function getSelector(el) {
655
- var stableOwnSelector =
656
- attributeSelector(el, 'data-agent-native-node-id') ||
657
- attributeSelector(el, 'data-code-layer-id') ||
658
- attributeSelector(el, 'data-layer-id') ||
659
- attributeSelector(el, 'data-builder-id') ||
660
- attributeSelector(el, 'data-loc');
661
- if (stableOwnSelector) return stableOwnSelector;
662
-
663
- if (el.id) return '#' + escapeIdent(el.id);
664
- var stableAncestor = closestStableSourceElement(el);
665
- if (stableAncestor && stableAncestor !== el) {
666
- var stableAncestorSelector = selectorPart(stableAncestor);
667
- if (stableAncestorSelector) {
668
- var descendantPath = selectorPath(el, stableAncestor);
669
- var descendantParts = descendantPath ? descendantPath.split(' > ') : [];
670
- if (descendantParts.length) {
671
- return stableAncestorSelector + ' > ' + descendantParts.join(' > ');
672
- }
673
- return stableAncestorSelector;
674
- }
675
- }
676
-
677
- return selectorPath(el);
678
- }
679
-
680
- function getElementInfo(el) {
681
- var cs = window.getComputedStyle(el);
682
- var rect = el.getBoundingClientRect();
683
- var parentStyles = el.parentElement
684
- ? window.getComputedStyle(el.parentElement)
685
- : null;
686
- var parentDisplay = parentStyles ? parentStyles.display : undefined;
687
- var sourceBacked = hasStableOwnSource(el) || !!closestStableSourceElement(el);
688
- var sourceId = sourceBacked ? (getSourceId(el) || getSelector(el)) : '';
689
- var parentLayout = parentStyles
690
- ? {
691
- display: parentStyles.display,
692
- flexDirection: parentStyles.flexDirection,
693
- alignItems: parentStyles.alignItems,
694
- justifyContent: parentStyles.justifyContent,
695
- gap: parentStyles.gap,
696
- gridTemplateColumns: parentStyles.gridTemplateColumns,
697
- gridTemplateRows: parentStyles.gridTemplateRows,
698
- position: parentStyles.position,
699
- }
700
- : undefined;
701
- var capabilities = sourceBacked
702
- ? [
703
- {
704
- kind: 'deterministic-style-edit',
705
- label: 'deterministic-style-edit',
706
- confidence: 0.92,
707
- reason: 'Inline style can be patched and replayed through HMR/collab.',
708
- },
709
- ]
710
- : [
711
- {
712
- kind: 'unsupported',
713
- label: 'runtime-only-element',
714
- confidence: 0.3,
715
- reason: 'This runtime node is not anchored to a source code layer.',
716
- },
717
- ];
718
- if (sourceBacked && el.classList && el.classList.length > 0) {
719
- capabilities.push({
720
- kind: 'deterministic-class-edit',
721
- label: 'deterministic-class-edit',
722
- confidence: 0.78,
723
- reason: 'Class tokens are visible on the selected element.',
724
- });
725
- }
726
- if (sourceBacked && (parentDisplay === 'flex' || parentDisplay === 'inline-flex' || parentDisplay === 'grid' || parentDisplay === 'inline-grid')) {
727
- capabilities.push({
728
- kind: 'agent-structural-edit',
729
- label: 'agent-structural-edit',
730
- confidence: 0.54,
731
- reason: 'Parent layout context decides whether movement means gap, order, alignment, or wrapper structure.',
732
- });
733
- }
734
- // --- provenance: read source-location attributes when present ---
735
- // These are emitted by connected apps via @vitejs/plugin-react jsxDEV or a
736
- // Babel source plugin. Cross-origin localhost iframes cannot be read (CSP /
737
- // same-origin policy), so this will be undefined in that case — expected.
738
- var provenance = undefined;
739
- var dataSourceFile = el.getAttribute('data-source-file');
740
- var dataSourceLine = el.getAttribute('data-source-line');
741
- var dataSourceColumn = el.getAttribute('data-source-column');
742
- var dataComponentName = el.getAttribute('data-component-name');
743
- var dataLoc = el.getAttribute('data-loc');
744
- // data-loc may encode "file:line:col" (Babel source plugin convention).
745
- // Only parse it when data-source-file is absent, to avoid double-reads.
746
- if (!dataSourceFile && dataLoc) {
747
- var lastColonIndex = dataLoc.lastIndexOf(':');
748
- var lastPart = lastColonIndex >= 0 ? dataLoc.slice(lastColonIndex + 1) : '';
749
- if (lastColonIndex >= 0 && /^\\d+$/.test(lastPart)) {
750
- var beforeLastPart = dataLoc.slice(0, lastColonIndex);
751
- var previousColonIndex = beforeLastPart.lastIndexOf(':');
752
- var previousPart = previousColonIndex >= 0 ? beforeLastPart.slice(previousColonIndex + 1) : '';
753
- var hasColumn = /^\\d+$/.test(previousPart);
754
- dataSourceFile = hasColumn ? beforeLastPart.slice(0, previousColonIndex) : beforeLastPart;
755
- dataSourceLine = hasColumn ? previousPart : lastPart;
756
- if (hasColumn) dataSourceColumn = lastPart;
757
- }
758
- }
759
- if (dataSourceFile || dataSourceLine || dataSourceColumn || dataComponentName) {
760
- provenance = {};
761
- if (dataSourceFile) provenance.sourceFile = dataSourceFile;
762
- if (dataSourceLine) { var ln = parseInt(dataSourceLine, 10); if (!isNaN(ln)) provenance.line = ln; }
763
- if (dataSourceColumn) { var col = parseInt(dataSourceColumn, 10); if (!isNaN(col)) provenance.column = col; }
764
- if (dataComponentName) provenance.component = dataComponentName;
765
- }
766
- return {
767
- tagName: el.tagName.toLowerCase(),
768
- id: el.id || undefined,
769
- sourceId: sourceId,
770
- selector: getSelector(el),
771
- classes: Array.from(el.classList),
772
- computedStyles: {
773
- color: cs.color,
774
- backgroundColor: cs.backgroundColor,
775
- backgroundImage: cs.backgroundImage,
776
- backgroundBlendMode: cs.backgroundBlendMode,
777
- fontSize: cs.fontSize,
778
- fontFamily: cs.fontFamily,
779
- fontWeight: cs.fontWeight,
780
- lineHeight: cs.lineHeight,
781
- letterSpacing: cs.letterSpacing,
782
- textAlign: cs.textAlign,
783
- display: cs.display,
784
- overflow: cs.overflow,
785
- flexDirection: cs.flexDirection,
786
- justifyContent: cs.justifyContent,
787
- alignItems: cs.alignItems,
788
- alignSelf: cs.alignSelf,
789
- flexGrow: cs.flexGrow,
790
- flexShrink: cs.flexShrink,
791
- flexBasis: cs.flexBasis,
792
- order: cs.order,
793
- gridColumn: cs.gridColumn,
794
- gridRow: cs.gridRow,
795
- position: cs.position,
796
- top: cs.top,
797
- right: cs.right,
798
- bottom: cs.bottom,
799
- left: cs.left,
800
- gap: cs.gap,
801
- width: cs.width,
802
- height: cs.height,
803
- opacity: cs.opacity,
804
- paddingTop: cs.paddingTop,
805
- paddingRight: cs.paddingRight,
806
- paddingBottom: cs.paddingBottom,
807
- paddingLeft: cs.paddingLeft,
808
- marginTop: cs.marginTop,
809
- marginRight: cs.marginRight,
810
- marginBottom: cs.marginBottom,
811
- marginLeft: cs.marginLeft,
812
- borderWidth: cs.borderWidth,
813
- borderStyle: cs.borderStyle,
814
- borderColor: cs.borderColor,
815
- borderRadius: cs.borderRadius,
816
- borderTopLeftRadius: cs.borderTopLeftRadius,
817
- borderTopRightRadius: cs.borderTopRightRadius,
818
- borderBottomRightRadius: cs.borderBottomRightRadius,
819
- borderBottomLeftRadius: cs.borderBottomLeftRadius,
820
- outlineWidth: cs.outlineWidth,
821
- outlineStyle: cs.outlineStyle,
822
- outlineColor: cs.outlineColor,
823
- outlineOffset: cs.outlineOffset,
824
- boxShadow: cs.boxShadow,
825
- textShadow: cs.textShadow,
826
- filter: cs.filter,
827
- mixBlendMode: cs.mixBlendMode,
828
- zIndex: cs.zIndex,
829
- },
830
- boundingRect: { x: rect.x + (window.scrollX || window.pageXOffset || 0), y: rect.y + (window.scrollY || window.pageYOffset || 0), width: rect.width, height: rect.height },
831
- textContent: el.textContent ? el.textContent.slice(0, 200) : undefined,
832
- htmlContent: el.innerHTML && el.innerHTML !== el.textContent ? el.innerHTML.slice(0, 4000) : undefined,
833
- isFlexContainer: cs.display === 'flex' || cs.display === 'inline-flex',
834
- isFlexChild: parentDisplay === 'flex' || parentDisplay === 'inline-flex',
835
- parentDisplay: parentDisplay,
836
- parentLayout: parentLayout,
837
- editCapabilities: capabilities,
838
- confidence: capabilities.reduce(function(best, item) {
839
- return Math.max(best, item.confidence || 0);
840
- }, 0),
841
- provenance: provenance,
842
- };
843
- }
844
-
845
- var shieldOverlay = document.createElement('div');
846
- shieldOverlay.setAttribute('data-agent-native-edit-overlay', 'shield');
847
- shieldOverlay.style.cssText = 'position:fixed;inset:0;z-index:99990;background:transparent;pointer-events:auto;touch-action:none;cursor:default;';
848
- document.body.appendChild(shieldOverlay);
849
-
850
- var highlightOverlay = document.createElement('div');
851
- highlightOverlay.setAttribute('data-agent-native-edit-overlay', 'highlight');
852
- 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;';
853
- document.body.appendChild(highlightOverlay);
854
-
855
- var selectionOverlay = document.createElement('div');
856
- selectionOverlay.setAttribute('data-agent-native-edit-overlay', 'selection');
857
- 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;';
858
- ['n','e','s','w'].forEach(function(pos) {
859
- var edge = document.createElement('span');
860
- edge.setAttribute('data-agent-native-edge-handle', pos);
861
- var cursor = pos === 'n' || pos === 's' ? 'ns-resize' : 'ew-resize';
862
- edge.style.cssText = 'position:absolute;pointer-events:auto;cursor:' + cursor + ';background:transparent;';
863
- if (pos === 'n') {
864
- edge.style.left = '0';
865
- edge.style.right = '0';
866
- edge.style.top = '-5px';
867
- edge.style.height = '10px';
868
- }
869
- if (pos === 's') {
870
- edge.style.left = '0';
871
- edge.style.right = '0';
872
- edge.style.bottom = '-5px';
873
- edge.style.height = '10px';
874
- }
875
- if (pos === 'e') {
876
- edge.style.top = '0';
877
- edge.style.bottom = '0';
878
- edge.style.right = '-5px';
879
- edge.style.width = '10px';
880
- }
881
- if (pos === 'w') {
882
- edge.style.top = '0';
883
- edge.style.bottom = '0';
884
- edge.style.left = '-5px';
885
- edge.style.width = '10px';
886
- }
887
- selectionOverlay.appendChild(edge);
888
- });
889
- ['nw','ne','se','sw'].forEach(function(pos) {
890
- var handle = document.createElement('span');
891
- handle.setAttribute('data-agent-native-edit-handle', pos);
892
- var cursor = pos === 'n' || pos === 's' ? 'ns-resize' : pos === 'e' || pos === 'w' ? 'ew-resize' : pos === 'nw' || pos === 'se' ? 'nwse-resize' : 'nesw-resize';
893
- 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 + ';';
894
- if (pos.indexOf('n') !== -1) handle.style.top = '-4px';
895
- if (pos.indexOf('s') !== -1) handle.style.bottom = '-4px';
896
- if (pos.indexOf('w') !== -1) handle.style.left = '-4px';
897
- if (pos.indexOf('e') !== -1) handle.style.right = '-4px';
898
- if (pos === 'n' || pos === 's') {
899
- handle.style.left = '50%';
900
- handle.style.transform = 'translateX(-50%)';
901
- }
902
- if (pos === 'e' || pos === 'w') {
903
- handle.style.top = '50%';
904
- handle.style.transform = 'translateY(-50%)';
905
- }
906
- selectionOverlay.appendChild(handle);
907
- });
908
- ['nw','ne','se','sw'].forEach(function(pos) {
909
- var rotate = document.createElement('span');
910
- rotate.setAttribute('data-agent-native-rotate-handle', pos);
911
- rotate.style.cssText = 'position:absolute;width:18px;height:18px;border-radius:999px;pointer-events:auto;cursor:grab;';
912
- if (pos.indexOf('n') !== -1) rotate.style.top = '-26px';
913
- if (pos.indexOf('s') !== -1) rotate.style.bottom = '-26px';
914
- if (pos.indexOf('w') !== -1) rotate.style.left = '-26px';
915
- if (pos.indexOf('e') !== -1) rotate.style.right = '-26px';
916
- selectionOverlay.appendChild(rotate);
917
- });
918
- var spacingOverlay = document.createElement('div');
919
- spacingOverlay.setAttribute('data-agent-native-spacing-overlay', '');
920
- spacingOverlay.style.cssText = 'position:absolute;inset:0;display:none;pointer-events:none;';
921
- selectionOverlay.appendChild(spacingOverlay);
922
- document.body.appendChild(selectionOverlay);
923
-
924
- var transformBadge = document.createElement('div');
925
- transformBadge.setAttribute('data-agent-native-transform-badge', '');
926
- transformBadge.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--background) / 0.96);color:hsl(var(--foreground));font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;padding:3px 5px;box-shadow:0 8px 20px color-mix(in srgb, hsl(var(--foreground)) 16%, transparent);';
927
- document.body.appendChild(transformBadge);
928
-
929
- var spacingBadge = document.createElement('div');
930
- spacingBadge.setAttribute('data-agent-native-spacing-badge', '');
931
- spacingBadge.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;border-radius:3px;color:white;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:700;padding:2px 4px;box-shadow:0 4px 14px rgba(0,0,0,0.18);';
932
- document.body.appendChild(spacingBadge);
933
-
934
- var insertionGuide = document.createElement('div');
935
- insertionGuide.setAttribute('data-agent-native-insertion-guide', '');
936
- insertionGuide.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;background:var(--design-editor-accent-color);border-radius:999px;box-shadow:0 0 0 1px var(--design-editor-accent-color);';
937
- document.body.appendChild(insertionGuide);
938
-
939
- var measurementOverlay = document.createElement('div');
940
- measurementOverlay.setAttribute('data-agent-native-measurement-overlay', '');
941
- measurementOverlay.style.cssText = 'position:fixed;inset:0;z-index:100001;display:none;pointer-events:none;color:var(--design-editor-measure-color);font:11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;';
942
- document.body.appendChild(measurementOverlay);
943
-
944
- // Component-instance tag: a small pill that floats above the selection
945
- // outline whenever the selected element carries a data-agent-native-component
946
- // attribute. Clicking it sends a 'component-source-jump' message to the
947
- // parent so the editor can invoke open-component-source.
948
- var componentTagOverlay = document.createElement('div');
949
- componentTagOverlay.setAttribute('data-agent-native-edit-overlay', 'component-tag');
950
- componentTagOverlay.style.cssText = [
951
- 'position:fixed',
952
- 'z-index:100002',
953
- 'display:none',
954
- 'pointer-events:auto',
955
- 'cursor:pointer',
956
- 'padding:2px 6px',
957
- 'border-radius:4px',
958
- 'font:11px/1.6 ui-sans-serif,system-ui,sans-serif',
959
- 'white-space:nowrap',
960
- 'user-select:none',
961
- '-webkit-user-select:none',
962
- 'background:var(--design-editor-accent-color)',
963
- 'color:var(--design-editor-accent-contrast-color)',
964
- 'box-shadow:0 1px 4px color-mix(in srgb,var(--design-editor-accent-color) 40%,transparent)',
965
- 'border:1px solid color-mix(in srgb,var(--design-editor-accent-strong-color) 60%,transparent)',
966
- 'outline:2px solid transparent',
967
- 'transition:opacity 0.1s',
968
- ].join(';') + ';';
969
- document.body.appendChild(componentTagOverlay);
970
-
971
- componentTagOverlay.addEventListener('click', function(e) {
972
- e.stopPropagation();
973
- e.preventDefault();
974
- var nodeId = componentTagOverlay.getAttribute('data-component-node-id') || '';
975
- var componentName = componentTagOverlay.getAttribute('data-component-name') || '';
976
- if (!nodeId || !componentName) return;
977
- try {
978
- window.parent.postMessage({
979
- type: 'component-source-jump',
980
- nodeId: nodeId,
981
- componentName: componentName,
982
- }, '*');
983
- } catch (_err) {}
984
- });
985
-
986
- function updateComponentTag(el) {
987
- if (!el) {
988
- clearComponentTag();
989
- return;
990
- }
991
- var compName = el.getAttribute && el.getAttribute('data-agent-native-component');
992
- if (!compName) {
993
- clearComponentTag();
994
- return;
995
- }
996
- var nodeId = (
997
- el.getAttribute('data-agent-native-node-id') ||
998
- el.getAttribute('data-code-layer-id') ||
999
- el.getAttribute('data-layer-id') ||
1000
- el.id ||
1001
- ''
1002
- );
1003
- componentTagOverlay.textContent = compName + ' →';
1004
- componentTagOverlay.setAttribute('data-component-node-id', nodeId);
1005
- componentTagOverlay.setAttribute('data-component-name', compName);
1006
-
1007
- var rect = el.getBoundingClientRect();
1008
- var tagHeight = 22;
1009
- var tagTop = rect.top - tagHeight - 4;
1010
- if (tagTop < 4) tagTop = rect.top + 4;
1011
- componentTagOverlay.style.display = 'block';
1012
- componentTagOverlay.style.left = rect.left + 'px';
1013
- componentTagOverlay.style.top = tagTop + 'px';
1014
- // Accent outline on the selection overlay to distinguish component roots.
1015
- selectionOverlay.style.outline = '2px solid var(--design-editor-accent-strong-color)';
1016
- selectionOverlay.style.outlineOffset = '2px';
1017
- }
1018
-
1019
- function clearComponentTag() {
1020
- componentTagOverlay.style.display = 'none';
1021
- componentTagOverlay.removeAttribute('data-component-node-id');
1022
- componentTagOverlay.removeAttribute('data-component-name');
1023
- selectionOverlay.style.outline = '';
1024
- selectionOverlay.style.outlineOffset = '';
1025
- }
1026
-
1027
- var selectedEl = null;
1028
- var hoveredEl = null;
1029
- var activeTextEditEl = null;
1030
- var textEditPointerState = null;
1031
- var pendingStructureMove = null;
1032
- var pendingShieldDrag = null;
1033
- var suppressNextShieldClick = false;
1034
- var suppressNextShieldClickTimer = null;
1035
- var selectedSpacingHovered = false;
1036
- var hoveredSpacingHandleKey = '';
1037
- var spacingHandleStateByKey = {};
1038
- var spacingHandleNodesByKey = {};
1039
- var spacingDrag = null;
1040
- var lockedSelectors = [];
1041
- var hiddenSelectors = [];
1042
-
1043
- function clearRuntimeSelection() {
1044
- selectedEl = null;
1045
- hoveredEl = null;
1046
- selectedSpacingHovered = false;
1047
- hoveredSpacingHandleKey = '';
1048
- spacingDrag = null;
1049
- selectionOverlay.style.display = 'none';
1050
- highlightOverlay.style.display = 'none';
1051
- hideSpacingOverlay();
1052
- hideMeasurements();
1053
- clearComponentTag();
1054
- }
1055
-
1056
- function matchesSelectorList(el, selectors) {
1057
- if (!el || !selectors || selectors.length === 0) return false;
1058
- for (var i = 0; i < selectors.length; i += 1) {
1059
- try {
1060
- if (el.matches(selectors[i]) || el.closest(selectors[i])) return true;
1061
- } catch (_err) {}
1062
- }
1063
- return false;
1064
- }
1065
-
1066
- function matchesExactSelectorList(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])) return true;
1071
- } catch (_err) {}
1072
- }
1073
- return false;
1074
- }
1075
-
1076
- function isLayerInteractionBlocked(el) {
1077
- return matchesSelectorList(el, lockedSelectors) || matchesSelectorList(el, hiddenSelectors);
1078
- }
1079
-
1080
- function applyHiddenSelectors() {
1081
- document.querySelectorAll('[data-agent-native-runtime-hidden]').forEach(function(el) {
1082
- var previous = el.getAttribute('data-agent-native-previous-display');
1083
- if (previous === null) {
1084
- el.style.removeProperty('display');
1085
- } else {
1086
- el.style.display = previous;
1087
- }
1088
- el.removeAttribute('data-agent-native-runtime-hidden');
1089
- el.removeAttribute('data-agent-native-previous-display');
1090
- });
1091
- hiddenSelectors.forEach(function(selector) {
1092
- try {
1093
- document.querySelectorAll(selector).forEach(function(el) {
1094
- if (!el.hasAttribute('data-agent-native-runtime-hidden')) {
1095
- el.setAttribute('data-agent-native-previous-display', el.style.display || '');
1096
- }
1097
- el.setAttribute('data-agent-native-runtime-hidden', 'true');
1098
- el.style.display = 'none';
1099
- });
1100
- } catch (_err) {}
1101
- });
1102
- }
1103
-
1104
- function replaceRuntimeDocument(html, preferredSelector, selectorCandidates) {
1105
- if (typeof html !== 'string') return;
1106
- if (activeTextEditEl) {
1107
- applyHiddenSelectors();
1108
- refreshOverlays();
1109
- return;
1110
- }
1111
- var parser = new DOMParser();
1112
- var nextDoc = parser.parseFromString(html, 'text/html');
1113
- if (!nextDoc || !nextDoc.body) return;
1114
-
1115
- var persistentNodes = Array.prototype.slice.call(
1116
- document.querySelectorAll('[data-agent-native-edit-overlay]'),
1117
- );
1118
- var activeSelector = preferredSelector || (selectedEl ? getSelector(selectedEl) : '');
1119
- var activeCandidates = [];
1120
- if (Array.isArray(selectorCandidates)) {
1121
- selectorCandidates.forEach(function(selector) {
1122
- if (typeof selector === 'string' && selector && activeCandidates.indexOf(selector) === -1) {
1123
- activeCandidates.push(selector);
1124
- }
1125
- });
1126
- }
1127
- if (activeSelector && activeCandidates.indexOf(activeSelector) === -1) {
1128
- activeCandidates.push(activeSelector);
1129
- }
1130
-
1131
- var nextHeadHtml = nextDoc.head ? nextDoc.head.innerHTML : '';
1132
- if (nextHeadHtml === document.head.innerHTML && activeCandidates.length > 0) {
1133
- var currentMatch = null;
1134
- var nextMatch = null;
1135
- var matchedSelector = '';
1136
- var fallbackCurrentMatch = null;
1137
- var fallbackSelector = '';
1138
- for (var matchIndex = 0; matchIndex < activeCandidates.length; matchIndex += 1) {
1139
- try {
1140
- var currentCandidate = document.querySelector(activeCandidates[matchIndex]);
1141
- var nextCandidate = nextDoc.querySelector(activeCandidates[matchIndex]);
1142
- if (currentCandidate && !fallbackCurrentMatch) {
1143
- fallbackCurrentMatch = currentCandidate;
1144
- fallbackSelector = activeCandidates[matchIndex];
1145
- }
1146
- if (currentCandidate && nextCandidate) {
1147
- currentMatch = currentCandidate;
1148
- nextMatch = nextCandidate;
1149
- matchedSelector = activeCandidates[matchIndex];
1150
- break;
1151
- }
1152
- } catch (_err) {
1153
- // Keep trying later aliases; bridge selectors can differ between
1154
- // runtime and DOMParser passes.
1155
- }
1156
- }
1157
- if (!currentMatch && fallbackCurrentMatch) {
1158
- currentMatch = fallbackCurrentMatch;
1159
- matchedSelector = fallbackSelector;
1160
- }
1161
- if (
1162
- currentMatch &&
1163
- currentMatch !== document.body &&
1164
- currentMatch !== document.documentElement &&
1165
- !isOverlayElement(currentMatch)
1166
- ) {
1167
- if (nextMatch) {
1168
- currentMatch.replaceWith(document.importNode(nextMatch, true));
1169
- } else if (currentMatch !== document.body && currentMatch !== document.documentElement) {
1170
- currentMatch.parentElement && currentMatch.parentElement.removeChild(currentMatch);
1171
- }
1172
- applyHiddenSelectors();
1173
- selectedEl = null;
1174
- if (nextMatch) {
1175
- try {
1176
- selectedEl = document.querySelector(matchedSelector);
1177
- } catch (_err) {}
1178
- }
1179
- hoveredEl = null;
1180
- if (selectedEl && !isLayerInteractionBlocked(selectedEl)) {
1181
- positionOverlay(selectionOverlay, selectedEl);
1182
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
1183
- } else {
1184
- selectionOverlay.style.display = 'none';
1185
- }
1186
- highlightOverlay.style.display = 'none';
1187
- hideMeasurements();
1188
- refreshOverlays();
1189
- return;
1190
- }
1191
- }
1192
- if (document.head.innerHTML !== nextHeadHtml) {
1193
- document.head.innerHTML = nextHeadHtml;
1194
- }
1195
- Array.prototype.slice.call(document.body.attributes).forEach(function(attribute) {
1196
- document.body.removeAttribute(attribute.name);
1197
- });
1198
- Array.prototype.slice.call(nextDoc.body.attributes).forEach(function(attribute) {
1199
- document.body.setAttribute(attribute.name, attribute.value);
1200
- });
1201
- document.body.innerHTML = nextDoc.body.innerHTML;
1202
- persistentNodes.forEach(function(node) {
1203
- document.body.appendChild(node);
1204
- });
1205
- applyHiddenSelectors();
1206
-
1207
- selectedEl = null;
1208
- hoveredEl = null;
1209
- for (var i = 0; i < activeCandidates.length && !selectedEl; i += 1) {
1210
- try {
1211
- var match = document.querySelector(activeCandidates[i]);
1212
- // Skip the editor's own injected overlay chrome and re-anchor to a
1213
- // source-backed element. A stale positional candidate like
1214
- // body > div:nth-of-type(6) can otherwise re-match an overlay div
1215
- // (the only direct div children of body at runtime), which then has
1216
- // no code-layer node and fails every edit.
1217
- if (match && !isLayerInteractionBlocked(match) && !isOverlayElement(match)) {
1218
- selectedEl = selectionTargetForHit(match) || match;
1219
- }
1220
- } catch (_err) {}
1221
- }
1222
- if (selectedEl) {
1223
- positionOverlay(selectionOverlay, selectedEl);
1224
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
1225
- } else {
1226
- selectionOverlay.style.display = 'none';
1227
- }
1228
- highlightOverlay.style.display = 'none';
1229
- hideMeasurements();
1230
- refreshOverlays();
1231
- }
1232
-
1233
- function hideSpacingOverlay() {
1234
- spacingOverlay.style.display = 'none';
1235
- spacingOverlay.innerHTML = '';
1236
- spacingHandleStateByKey = {};
1237
- spacingHandleNodesByKey = {};
1238
- if (!spacingDrag) spacingBadge.style.display = 'none';
1239
- }
1240
-
1241
- function visibleLayoutChildren(el) {
1242
- if (!el || !el.children) return [];
1243
- return Array.prototype.slice.call(el.children).filter(function(child) {
1244
- if (!child || child.nodeType !== 1 || isOverlayElement(child) || isLayerInteractionBlocked(child)) return false;
1245
- var cs = window.getComputedStyle(child);
1246
- if (cs.display === 'none' || cs.visibility === 'hidden' || cs.position === 'fixed') return false;
1247
- var rect = child.getBoundingClientRect();
1248
- return rect.width > 0 && rect.height > 0;
1249
- });
1250
- }
1251
-
1252
- function spacingColor(kind) {
1253
- return kind === 'gap' ? '#ff4fd8' : 'var(--design-editor-accent-color)';
1254
- }
1255
-
1256
- function spacingFill(kind, orientation) {
1257
- var tint = kind === 'gap' ? 'rgba(255, 79, 216, 0.28)' : 'rgba(46, 168, 255, 0.24)';
1258
- var stripe = kind === 'gap' ? 'rgba(255, 79, 216, 0.58)' : 'rgba(46, 168, 255, 0.52)';
1259
- var angle = orientation === 'vertical' ? '135deg' : '45deg';
1260
- return 'repeating-linear-gradient(' + angle + ', ' + stripe + ' 0 1px, ' + tint + ' 1px 4px, transparent 4px 7px)';
1261
- }
1262
-
1263
- function clampSpacingValue(value) {
1264
- var rounded = Math.round(value);
1265
- if (!Number.isFinite(rounded)) return 0;
1266
- return Math.max(0, Math.min(999, rounded));
1267
- }
1268
-
1269
- function makeSpacingHandle(config) {
1270
- var region = config.region;
1271
- if (!region || region.width <= 0 || region.height <= 0) return null;
1272
- return {
1273
- key: config.key,
1274
- groupKey: config.groupKey || config.key,
1275
- kind: config.kind,
1276
- property: config.property,
1277
- oppositeProperty: config.oppositeProperty || '',
1278
- side: config.side || '',
1279
- orientation: config.orientation,
1280
- value: clampSpacingValue(config.value),
1281
- region: {
1282
- x: Math.round(region.x),
1283
- y: Math.round(region.y),
1284
- width: Math.max(1, Math.round(region.width)),
1285
- height: Math.max(1, Math.round(region.height)),
1286
- },
1287
- line: config.line,
1288
- };
1289
- }
1290
-
1291
- function childLocalRect(child, containerRect) {
1292
- var rect = child.getBoundingClientRect();
1293
- return {
1294
- left: rect.left - containerRect.left,
1295
- top: rect.top - containerRect.top,
1296
- right: rect.right - containerRect.left,
1297
- bottom: rect.bottom - containerRect.top,
1298
- width: rect.width,
1299
- height: rect.height,
1300
- };
1301
- }
1302
-
1303
- function childRectsOverlap(a, b, axis) {
1304
- if (axis === 'x') {
1305
- return Math.max(a.top, b.top) < Math.min(a.bottom, b.bottom);
1306
- }
1307
- return Math.max(a.left, b.left) < Math.min(a.right, b.right);
1308
- }
1309
-
1310
- function buildPaddingSpacingHandles(el, rect, cs) {
1311
- var handles = [];
1312
- var borderTop = readPx(cs.borderTopWidth);
1313
- var borderRight = readPx(cs.borderRightWidth);
1314
- var borderBottom = readPx(cs.borderBottomWidth);
1315
- var borderLeft = readPx(cs.borderLeftWidth);
1316
- var paddingTop = readPx(cs.paddingTop);
1317
- var paddingRight = readPx(cs.paddingRight);
1318
- var paddingBottom = readPx(cs.paddingBottom);
1319
- var paddingLeft = readPx(cs.paddingLeft);
1320
- var sx = chromeScaleX();
1321
- var sy = chromeScaleY();
1322
- var line = Math.max(1, chromeLineScale());
1323
- var hLineWidth = Math.max(8, Math.min(24, rect.width * 0.18)) * sx;
1324
- var vLineHeight = Math.max(8, Math.min(24, rect.height * 0.18)) * sy;
1325
- var innerLeft = borderLeft;
1326
- var innerTop = borderTop;
1327
- var innerWidth = Math.max(1, rect.width - borderLeft - borderRight);
1328
- var innerHeight = Math.max(1, rect.height - borderTop - borderBottom);
1329
- if (paddingTop > 0) {
1330
- handles.push(makeSpacingHandle({
1331
- key: 'padding:top',
1332
- kind: 'padding',
1333
- property: 'paddingTop',
1334
- oppositeProperty: 'paddingBottom',
1335
- side: 'top',
1336
- orientation: 'horizontal',
1337
- value: paddingTop,
1338
- region: { x: innerLeft, y: innerTop, width: innerWidth, height: paddingTop },
1339
- line: {
1340
- x: rect.width / 2 - hLineWidth / 2,
1341
- y: innerTop + paddingTop / 2 - line / 2,
1342
- width: hLineWidth,
1343
- height: line,
1344
- },
1345
- }));
1346
- }
1347
- if (paddingBottom > 0) {
1348
- handles.push(makeSpacingHandle({
1349
- key: 'padding:bottom',
1350
- kind: 'padding',
1351
- property: 'paddingBottom',
1352
- oppositeProperty: 'paddingTop',
1353
- side: 'bottom',
1354
- orientation: 'horizontal',
1355
- value: paddingBottom,
1356
- region: { x: innerLeft, y: rect.height - borderBottom - paddingBottom, width: innerWidth, height: paddingBottom },
1357
- line: {
1358
- x: rect.width / 2 - hLineWidth / 2,
1359
- y: rect.height - borderBottom - paddingBottom / 2 - line / 2,
1360
- width: hLineWidth,
1361
- height: line,
1362
- },
1363
- }));
1364
- }
1365
- if (paddingLeft > 0) {
1366
- handles.push(makeSpacingHandle({
1367
- key: 'padding:left',
1368
- kind: 'padding',
1369
- property: 'paddingLeft',
1370
- oppositeProperty: 'paddingRight',
1371
- side: 'left',
1372
- orientation: 'vertical',
1373
- value: paddingLeft,
1374
- region: { x: innerLeft, y: innerTop, width: paddingLeft, height: innerHeight },
1375
- line: {
1376
- x: innerLeft + paddingLeft / 2 - line / 2,
1377
- y: rect.height / 2 - vLineHeight / 2,
1378
- width: line,
1379
- height: vLineHeight,
1380
- },
1381
- }));
1382
- }
1383
- if (paddingRight > 0) {
1384
- handles.push(makeSpacingHandle({
1385
- key: 'padding:right',
1386
- kind: 'padding',
1387
- property: 'paddingRight',
1388
- oppositeProperty: 'paddingLeft',
1389
- side: 'right',
1390
- orientation: 'vertical',
1391
- value: paddingRight,
1392
- region: { x: rect.width - borderRight - paddingRight, y: innerTop, width: paddingRight, height: innerHeight },
1393
- line: {
1394
- x: rect.width - borderRight - paddingRight / 2 - line / 2,
1395
- y: rect.height / 2 - vLineHeight / 2,
1396
- width: line,
1397
- height: vLineHeight,
1398
- },
1399
- }));
1400
- }
1401
- return handles.filter(Boolean);
1402
- }
1403
-
1404
- function buildGapSpacingHandles(el, rect, cs) {
1405
- var children = visibleLayoutChildren(el);
1406
- if (children.length < 2) return [];
1407
- var handles = [];
1408
- var sx = chromeScaleX();
1409
- var sy = chromeScaleY();
1410
- var line = Math.max(1, chromeLineScale());
1411
- var hLineWidth = 10 * sx;
1412
- var vLineHeight = 10 * sy;
1413
- var isFlex = cs.display === 'flex' || cs.display === 'inline-flex';
1414
- var isGrid = cs.display === 'grid' || cs.display === 'inline-grid';
1415
- if (!isFlex && !isGrid) return handles;
1416
- var primaryAxis = isFlex && cs.flexDirection && cs.flexDirection.indexOf('column') === 0 ? 'y' : 'x';
1417
- var childRects = children.map(function(child) {
1418
- return childLocalRect(child, rect);
1419
- });
1420
-
1421
- function addAxisGaps(axis, property, groupKey) {
1422
- var cssGap = readPx(cs[property]);
1423
- if (cssGap <= 0) return;
1424
- var sorted = childRects.slice().sort(function(a, b) {
1425
- return axis === 'x' ? a.left - b.left : a.top - b.top;
1426
- });
1427
- var count = 0;
1428
- for (var i = 0; i < sorted.length - 1; i += 1) {
1429
- var a = sorted[i];
1430
- var b = sorted[i + 1];
1431
- if (!childRectsOverlap(a, b, axis)) continue;
1432
- var gap = axis === 'x' ? b.left - a.right : b.top - a.bottom;
1433
- if (gap <= 1) continue;
1434
- if (axis === 'x') {
1435
- var top = Math.max(a.top, b.top);
1436
- var bottom = Math.min(a.bottom, b.bottom);
1437
- var height = Math.max(1, bottom - top);
1438
- handles.push(makeSpacingHandle({
1439
- key: groupKey + ':' + count,
1440
- groupKey: groupKey,
1441
- kind: 'gap',
1442
- property: property,
1443
- orientation: 'vertical',
1444
- value: cssGap,
1445
- region: { x: a.right, y: top, width: gap, height: height },
1446
- line: {
1447
- x: a.right + gap / 2 - line / 2,
1448
- y: top + height / 2 - vLineHeight / 2,
1449
- width: line,
1450
- height: vLineHeight,
1451
- },
1452
- }));
1453
- } else {
1454
- var left = Math.max(a.left, b.left);
1455
- var right = Math.min(a.right, b.right);
1456
- var width = Math.max(1, right - left);
1457
- handles.push(makeSpacingHandle({
1458
- key: groupKey + ':' + count,
1459
- groupKey: groupKey,
1460
- kind: 'gap',
1461
- property: property,
1462
- orientation: 'horizontal',
1463
- value: cssGap,
1464
- region: { x: left, y: a.bottom, width: width, height: gap },
1465
- line: {
1466
- x: left + width / 2 - hLineWidth / 2,
1467
- y: a.bottom + gap / 2 - line / 2,
1468
- width: hLineWidth,
1469
- height: line,
1470
- },
1471
- }));
1472
- }
1473
- count += 1;
1474
- }
1475
- }
1476
-
1477
- if (primaryAxis === 'x') {
1478
- addAxisGaps('x', 'columnGap', 'gap:column');
1479
- if (isGrid) addAxisGaps('y', 'rowGap', 'gap:row');
1480
- } else {
1481
- addAxisGaps('y', 'rowGap', 'gap:row');
1482
- if (isGrid) addAxisGaps('x', 'columnGap', 'gap:column');
1483
- }
1484
- return handles.filter(Boolean);
1485
- }
1486
-
1487
- function buildSpacingHandles(el) {
1488
- if (!el || !document.documentElement.contains(el)) return [];
1489
- if (Math.abs(currentRotation(el)) > 0.01) return [];
1490
- var children = visibleLayoutChildren(el);
1491
- if (children.length === 0) return [];
1492
- var rect = el.getBoundingClientRect();
1493
- if (rect.width <= 0 || rect.height <= 0) return [];
1494
- var cs = window.getComputedStyle(el);
1495
- return buildPaddingSpacingHandles(el, rect, cs).concat(buildGapSpacingHandles(el, rect, cs));
1496
- }
1497
-
1498
- function showSpacingBadgeForHandle(handle, value) {
1499
- if (!selectedEl || !handle) {
1500
- spacingBadge.style.display = 'none';
1501
- return;
1502
- }
1503
- var rect = selectedEl.getBoundingClientRect();
1504
- var x = rect.left + handle.region.x + handle.region.width / 2;
1505
- var y = rect.top + handle.region.y + handle.region.height / 2;
1506
- spacingBadge.textContent = String(clampSpacingValue(value));
1507
- spacingBadge.style.display = 'block';
1508
- spacingBadge.style.background = spacingColor(handle.kind);
1509
- spacingBadge.style.left = x + 'px';
1510
- spacingBadge.style.top = y + 'px';
1511
- spacingBadge.style.transform = 'translate(-50%, -50%)';
1512
- }
1513
-
1514
- function renderSpacingHandle(handle, activeGroupKey) {
1515
- if (!handle) return;
1516
- spacingHandleStateByKey[handle.key] = handle;
1517
- var highlighted = Boolean(activeGroupKey && handle.groupKey === activeGroupKey);
1518
- var lineNode = document.createElement('span');
1519
- lineNode.setAttribute('data-agent-native-spacing-line', handle.kind);
1520
- lineNode.style.display = 'block';
1521
- lineNode.style.left = handle.line.x + 'px';
1522
- lineNode.style.top = handle.line.y + 'px';
1523
- lineNode.style.width = Math.max(1, handle.line.width) + 'px';
1524
- lineNode.style.height = Math.max(1, handle.line.height) + 'px';
1525
- lineNode.style.background = spacingColor(handle.kind);
1526
- spacingOverlay.appendChild(lineNode);
1527
-
1528
- var regionNode = document.createElement('span');
1529
- regionNode.setAttribute('data-agent-native-spacing-region', handle.kind);
1530
- regionNode.setAttribute('data-orientation', handle.orientation);
1531
- regionNode.setAttribute('data-spacing-key', handle.key);
1532
- regionNode.style.display = 'block';
1533
- regionNode.style.left = handle.region.x + 'px';
1534
- regionNode.style.top = handle.region.y + 'px';
1535
- regionNode.style.width = handle.region.width + 'px';
1536
- regionNode.style.height = handle.region.height + 'px';
1537
- regionNode.style.background = highlighted ? spacingFill(handle.kind, handle.orientation) : 'transparent';
1538
- regionNode.style.outline = highlighted ? '1px solid ' + spacingColor(handle.kind) : '0';
1539
- regionNode.style.outlineOffset = '-1px';
1540
- regionNode.addEventListener('mouseenter', function() {
1541
- hoveredSpacingHandleKey = handle.key;
1542
- selectedSpacingHovered = true;
1543
- updateSpacingOverlay(selectedEl);
1544
- });
1545
- regionNode.addEventListener('mouseleave', function() {
1546
- if (spacingDrag) return;
1547
- hoveredSpacingHandleKey = '';
1548
- updateSpacingOverlay(selectedEl);
1549
- });
1550
- regionNode.addEventListener('mousedown', function(event) {
1551
- startSpacingDrag(handle.key, event);
1552
- }, true);
1553
- spacingHandleNodesByKey[handle.key] = regionNode;
1554
- spacingOverlay.appendChild(regionNode);
1555
- }
1556
-
1557
- function updateSpacingOverlay(el) {
1558
- if (el && el !== selectedEl) {
1559
- hideSpacingOverlay();
1560
- return;
1561
- }
1562
- if (!selectedEl || !document.documentElement.contains(selectedEl)) {
1563
- hideSpacingOverlay();
1564
- return;
1565
- }
1566
- if (!selectedSpacingHovered && !hoveredSpacingHandleKey && !spacingDrag) {
1567
- hideSpacingOverlay();
1568
- return;
1569
- }
1570
- var handles = buildSpacingHandles(selectedEl);
1571
- if (handles.length === 0) {
1572
- hideSpacingOverlay();
1573
- return;
1574
- }
1575
- spacingOverlay.style.display = 'block';
1576
- spacingOverlay.innerHTML = '';
1577
- spacingHandleStateByKey = {};
1578
- spacingHandleNodesByKey = {};
1579
- var activeHandle =
1580
- (spacingDrag && spacingDrag.handle) ||
1581
- handles.find(function(handle) { return handle.key === hoveredSpacingHandleKey; }) ||
1582
- null;
1583
- var activeGroupKey = activeHandle ? activeHandle.groupKey : '';
1584
- handles.forEach(function(handle) {
1585
- renderSpacingHandle(handle, activeGroupKey);
1586
- });
1587
- if (activeHandle) {
1588
- showSpacingBadgeForHandle(activeHandle, spacingDrag ? spacingDrag.currentValue : activeHandle.value);
1589
- } else {
1590
- spacingBadge.style.display = 'none';
1591
- }
1592
- }
1593
-
1594
- function applyEditorChromeScale() {
1595
- syncEditorChromeScaleVars();
1596
- var sx = chromeScaleX();
1597
- var sy = chromeScaleY();
1598
- var line = chromeLineScale();
1599
- highlightOverlay.style.borderWidth = (1.5 * line) + 'px';
1600
- selectionOverlay.style.borderWidth = (1.5 * line) + 'px';
1601
- if (selectedEl) updateSpacingOverlay(selectedEl);
1602
-
1603
- selectionOverlay.querySelectorAll('[data-agent-native-edge-handle]').forEach(function(edge) {
1604
- var pos = edge.getAttribute('data-agent-native-edge-handle');
1605
- if (pos === 'n' || pos === 's') {
1606
- edge.style.height = (10 * sy) + 'px';
1607
- edge.style[pos === 'n' ? 'top' : 'bottom'] = (-5 * sy) + 'px';
1608
- }
1609
- if (pos === 'e' || pos === 'w') {
1610
- edge.style.width = (10 * sx) + 'px';
1611
- edge.style[pos === 'w' ? 'left' : 'right'] = (-5 * sx) + 'px';
1612
- }
1613
- });
1614
-
1615
- selectionOverlay.querySelectorAll('[data-agent-native-edit-handle]').forEach(function(handle) {
1616
- var pos = handle.getAttribute('data-agent-native-edit-handle') || '';
1617
- handle.style.width = (7 * sx) + 'px';
1618
- handle.style.height = (7 * sy) + 'px';
1619
- handle.style.borderWidth = Math.max(1, 1 * line) + 'px';
1620
- if (pos.indexOf('n') !== -1) handle.style.top = (-4 * sy) + 'px';
1621
- if (pos.indexOf('s') !== -1) handle.style.bottom = (-4 * sy) + 'px';
1622
- if (pos.indexOf('w') !== -1) handle.style.left = (-4 * sx) + 'px';
1623
- if (pos.indexOf('e') !== -1) handle.style.right = (-4 * sx) + 'px';
1624
- });
1625
-
1626
- selectionOverlay.querySelectorAll('[data-agent-native-rotate-handle]').forEach(function(handle) {
1627
- var pos = handle.getAttribute('data-agent-native-rotate-handle') || '';
1628
- handle.style.width = (18 * sx) + 'px';
1629
- handle.style.height = (18 * sy) + 'px';
1630
- if (pos.indexOf('n') !== -1) handle.style.top = (-26 * sy) + 'px';
1631
- if (pos.indexOf('s') !== -1) handle.style.bottom = (-26 * sy) + 'px';
1632
- if (pos.indexOf('w') !== -1) handle.style.left = (-26 * sx) + 'px';
1633
- if (pos.indexOf('e') !== -1) handle.style.right = (-26 * sx) + 'px';
1634
- });
1635
- }
1636
-
1637
- function positionOverlay(overlay, el) {
1638
- if (!el || !document.documentElement.contains(el)) {
1639
- overlay.style.display = 'none';
1640
- if (overlay === selectionOverlay) clearComponentTag();
1641
- return;
1642
- }
1643
- // For the selection overlay, prefer the CSS box + rotation transform so
1644
- // the handles hug the rotated element rather than its inflated AABB.
1645
- if (overlay === selectionOverlay) {
1646
- var elCs = window.getComputedStyle(el);
1647
- var elLeft = readFinitePx(el.style.left || elCs.left);
1648
- var elTop = readFinitePx(el.style.top || elCs.top);
1649
- var elW = readFinitePx(el.style.width || elCs.width);
1650
- var elH = readFinitePx(el.style.height || elCs.height);
1651
- var elRot = currentRotation(el);
1652
- var canUseLocalBox = Math.abs(elRot) > 0.01 && elLeft !== null && elTop !== null && elW !== null && elH !== null;
1653
- // Convert element-local left/top to viewport coords by walking to the
1654
- // nearest positioned ancestor (same reference frame as getBoundingClientRect).
1655
- if (canUseLocalBox) {
1656
- var parentRect = (el.offsetParent || document.documentElement).getBoundingClientRect();
1657
- overlay.style.display = 'block';
1658
- overlay.style.left = (parentRect.left + elLeft) + 'px';
1659
- overlay.style.top = (parentRect.top + elTop) + 'px';
1660
- overlay.style.width = elW + 'px';
1661
- overlay.style.height = elH + 'px';
1662
- overlay.style.transform = 'rotate(' + elRot + 'deg)';
1663
- overlay.style.transformOrigin = '0 0';
1664
- updateSpacingOverlay(el);
1665
- updateComponentTag(el);
1666
- return;
1667
- }
1668
- }
1669
- var rect = el.getBoundingClientRect();
1670
- overlay.style.display = 'block';
1671
- overlay.style.top = rect.top + 'px';
1672
- overlay.style.left = rect.left + 'px';
1673
- overlay.style.width = rect.width + 'px';
1674
- overlay.style.height = rect.height + 'px';
1675
- overlay.style.transform = '';
1676
- if (overlay === selectionOverlay) {
1677
- updateSpacingOverlay(el);
1678
- updateComponentTag(el);
1679
- }
1680
- }
1681
-
1682
- function refreshOverlays() {
1683
- if (hoveredEl && hoveredEl !== selectedEl) {
1684
- positionOverlay(highlightOverlay, hoveredEl);
1685
- } else {
1686
- highlightOverlay.style.display = 'none';
1687
- }
1688
- if (selectedEl) positionOverlay(selectionOverlay, selectedEl);
1689
- }
1690
-
1691
- function hideMeasurements() {
1692
- measurementOverlay.style.display = 'none';
1693
- measurementOverlay.innerHTML = '';
1694
- }
1695
-
1696
- function addMeasurementLine(x1, y1, x2, y2, label) {
1697
- var horizontal = Math.abs(x2 - x1) >= Math.abs(y2 - y1);
1698
- var line = document.createElement('div');
1699
- var labelEl = document.createElement('div');
1700
- if (horizontal) {
1701
- var left = Math.min(x1, x2);
1702
- var width = Math.max(1, Math.abs(x2 - x1));
1703
- line.style.cssText = 'position:fixed;left:' + left + 'px;top:' + y1 + 'px;width:' + width + 'px;border-top:1px dashed var(--design-editor-measure-color);';
1704
- 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;';
1705
- } else {
1706
- var top = Math.min(y1, y2);
1707
- var height = Math.max(1, Math.abs(y2 - y1));
1708
- line.style.cssText = 'position:fixed;left:' + x1 + 'px;top:' + top + 'px;height:' + height + 'px;border-left:1px dashed var(--design-editor-measure-color);';
1709
- 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;';
1710
- }
1711
- labelEl.textContent = label;
1712
- measurementOverlay.appendChild(line);
1713
- measurementOverlay.appendChild(labelEl);
1714
- }
1715
-
1716
- function showMeasurements(a, b) {
1717
- if (!a || !b || a === b) {
1718
- hideMeasurements();
1719
- return;
1720
- }
1721
- var selectedRect = a.getBoundingClientRect();
1722
- var hoverRect = b.getBoundingClientRect();
1723
- measurementOverlay.innerHTML = '';
1724
- measurementOverlay.style.display = 'block';
1725
-
1726
- if (hoverRect.right <= selectedRect.left) {
1727
- var yLeft = Math.max(hoverRect.top, Math.min(hoverRect.bottom, selectedRect.top + selectedRect.height / 2));
1728
- addMeasurementLine(hoverRect.right, yLeft, selectedRect.left, yLeft, Math.round(selectedRect.left - hoverRect.right) + 'px');
1729
- return;
1730
- }
1731
- if (selectedRect.right <= hoverRect.left) {
1732
- var yRight = Math.max(selectedRect.top, Math.min(selectedRect.bottom, hoverRect.top + hoverRect.height / 2));
1733
- addMeasurementLine(selectedRect.right, yRight, hoverRect.left, yRight, Math.round(hoverRect.left - selectedRect.right) + 'px');
1734
- return;
1735
- }
1736
- if (hoverRect.bottom <= selectedRect.top) {
1737
- var xTop = Math.max(hoverRect.left, Math.min(hoverRect.right, selectedRect.left + selectedRect.width / 2));
1738
- addMeasurementLine(xTop, hoverRect.bottom, xTop, selectedRect.top, Math.round(selectedRect.top - hoverRect.bottom) + 'px');
1739
- return;
1740
- }
1741
- if (selectedRect.bottom <= hoverRect.top) {
1742
- var xBottom = Math.max(selectedRect.left, Math.min(selectedRect.right, hoverRect.left + hoverRect.width / 2));
1743
- addMeasurementLine(xBottom, selectedRect.bottom, xBottom, hoverRect.top, Math.round(hoverRect.top - selectedRect.bottom) + 'px');
1744
- return;
1745
- }
1746
- addMeasurementLine(
1747
- selectedRect.left + selectedRect.width / 2,
1748
- selectedRect.top + selectedRect.height / 2,
1749
- hoverRect.left + hoverRect.width / 2,
1750
- hoverRect.top + hoverRect.height / 2,
1751
- Math.round(Math.hypot(
1752
- hoverRect.left + hoverRect.width / 2 - (selectedRect.left + selectedRect.width / 2),
1753
- hoverRect.top + hoverRect.height / 2 - (selectedRect.top + selectedRect.height / 2)
1754
- )) + 'px'
1755
- );
1756
- }
1757
-
1758
- function dragEventNames(e) {
1759
- var pointerGesture = e && e.type && e.type.indexOf('pointer') === 0;
1760
- return pointerGesture
1761
- ? { move: 'pointermove', up: 'pointerup' }
1762
- : { move: 'mousemove', up: 'mouseup' };
1763
- }
1764
-
1765
- function elementFromEditorPoint(clientX, clientY) {
1766
- var shieldPointerEvents = shieldOverlay.style.pointerEvents;
1767
- var selectionPointerEvents = selectionOverlay.style.pointerEvents;
1768
- var highlightPointerEvents = highlightOverlay.style.pointerEvents;
1769
- shieldOverlay.style.pointerEvents = 'none';
1770
- selectionOverlay.style.pointerEvents = 'none';
1771
- highlightOverlay.style.pointerEvents = 'none';
1772
- var target = document.elementFromPoint(clientX, clientY);
1773
- shieldOverlay.style.pointerEvents = shieldPointerEvents;
1774
- selectionOverlay.style.pointerEvents = selectionPointerEvents;
1775
- highlightOverlay.style.pointerEvents = highlightPointerEvents;
1776
- if (!target || target.nodeType !== 1) return null;
1777
- if (isLayerInteractionBlocked(target)) return null;
1778
- return target;
1779
- }
1780
-
1781
- function stopNativeInteraction(e) {
1782
- e.preventDefault();
1783
- e.stopPropagation();
1784
- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
1785
- }
1786
-
1787
- function isEditorTypingTarget(target) {
1788
- if (!target || !target.closest) return false;
1789
- return !!target.closest('input, textarea, select, [contenteditable], [role="textbox"], [data-agent-native-text-editing]');
1790
- }
1791
-
1792
- function shouldForwardDesignHotkey(e) {
1793
- if (activeTextEditEl || isEditorTypingTarget(e.target) || e.isComposing) return false;
1794
- var key = e.key;
1795
- var normalized = key && key.length === 1 ? key.toLowerCase() : key;
1796
- var primary = e.metaKey || e.ctrlKey;
1797
- if (key === 'Escape' || key === 'Enter') return true;
1798
- // Forward Tab only when an element is actively selected so the iframe does
1799
- // not intercept Tab when the user is tabbing through browser UI with nothing
1800
- // selected (preserves native keyboard accessibility).
1801
- if (key === 'Tab') return !!selectedEl;
1802
- if (key === 'Delete' || key === 'Backspace') return !primary;
1803
- if (/^Arrow/.test(key || '')) return !e.altKey;
1804
- if (primary) {
1805
- return ['z','y','a','x','c','v','d','g','=','+','-','0',']','['].indexOf(normalized) !== -1 ||
1806
- e.code === 'Digit1' ||
1807
- e.code === 'Digit2' ||
1808
- key === '1' ||
1809
- key === '2';
1810
- }
1811
- if (e.shiftKey && (e.code === 'Digit1' || e.code === 'Digit2' || key === '1' || key === '2')) return true;
1812
- return !e.altKey && !e.shiftKey && ['v','f','r','t','p','h','c','k'].indexOf(normalized) !== -1;
1813
- }
1814
-
1815
- function blurActiveTextEditor() {
1816
- var active = document.activeElement;
1817
- if (
1818
- active &&
1819
- active.closest &&
1820
- active.closest('[data-agent-native-text-editing]') &&
1821
- typeof active.blur === 'function'
1822
- ) {
1823
- active.blur();
1824
- }
1825
- }
1826
-
1827
- function setTextEditingPointerPassthrough(enabled) {
1828
- if (enabled) {
1829
- if (!textEditPointerState) {
1830
- textEditPointerState = {
1831
- shield: shieldOverlay.style.pointerEvents,
1832
- selection: selectionOverlay.style.pointerEvents,
1833
- highlight: highlightOverlay.style.pointerEvents,
1834
- };
1835
- }
1836
- shieldOverlay.style.pointerEvents = 'none';
1837
- selectionOverlay.style.pointerEvents = 'none';
1838
- highlightOverlay.style.pointerEvents = 'none';
1839
- return;
1840
- }
1841
- if (!textEditPointerState) return;
1842
- shieldOverlay.style.pointerEvents = textEditPointerState.shield;
1843
- selectionOverlay.style.pointerEvents = textEditPointerState.selection;
1844
- highlightOverlay.style.pointerEvents = textEditPointerState.highlight;
1845
- textEditPointerState = null;
1846
- }
1847
-
1848
- function hasTextContent(el) {
1849
- return !!(el && el.textContent && el.textContent.trim().length > 0);
1850
- }
1851
-
1852
- function isInlineEditableDescendant(el) {
1853
- if (!el || !el.tagName) return false;
1854
- // Allowlist covers inline markup AND common block-level text containers
1855
- // (p, h1-h6, li, etc.) so that paragraphs with inline markup like
1856
- // <p>Hello <strong>world</strong></p> can be double-click edited.
1857
- return [
1858
- // Inline formatting
1859
- 'a',
1860
- 'abbr',
1861
- 'b',
1862
- 'br',
1863
- 'cite',
1864
- 'code',
1865
- 'em',
1866
- 'i',
1867
- 'mark',
1868
- 'small',
1869
- 'span',
1870
- 'strong',
1871
- 'sub',
1872
- 'sup',
1873
- 'time',
1874
- 'u',
1875
- 'wbr',
1876
- // Block-level text containers
1877
- 'p',
1878
- 'h1',
1879
- 'h2',
1880
- 'h3',
1881
- 'h4',
1882
- 'h5',
1883
- 'h6',
1884
- 'li',
1885
- 'ul',
1886
- 'ol',
1887
- 'dl',
1888
- 'dt',
1889
- 'dd',
1890
- 'label',
1891
- 'caption',
1892
- 'td',
1893
- 'th'
1894
- ].indexOf(el.tagName.toLowerCase()) !== -1;
1895
- }
1896
-
1897
- function hasOnlyInlineEditableChildren(el) {
1898
- if (!el || !hasTextContent(el)) return false;
1899
- var descendants = el.querySelectorAll ? el.querySelectorAll('*') : [];
1900
- for (var i = 0; i < descendants.length; i += 1) {
1901
- if (!isInlineEditableDescendant(descendants[i])) return false;
1902
- }
1903
- return true;
1904
- }
1905
-
1906
- function findTextEditTarget(hit) {
1907
- if (!hit || hit.nodeType !== 1 || hit === document.body || hit === document.documentElement) return null;
1908
- var selectedContainsHit = selectedEl && selectedEl.contains && selectedEl.contains(hit);
1909
- if (selectedContainsHit && hasOnlyInlineEditableChildren(selectedEl)) return selectedEl;
1910
-
1911
- var candidate = null;
1912
- var node = hit;
1913
- while (node && node.nodeType === 1 && node !== document.body && node !== document.documentElement) {
1914
- if (hasOnlyInlineEditableChildren(node)) {
1915
- candidate = node;
1916
- }
1917
- if (selectedEl && node === selectedEl) break;
1918
- node = node.parentElement;
1919
- }
1920
- // Return null (not the raw hit) when no text-editable ancestor is found.
1921
- // Falling back to the raw hit element makes non-text nodes like <img> or
1922
- // <canvas> contenteditable, which leaves the editor in a broken state.
1923
- return candidate || null;
1924
- }
1925
-
1926
- function selectElementAtEvent(e) {
1927
- stopNativeInteraction(e);
1928
- blurActiveTextEditor();
1929
- if (suppressNextShieldClick) {
1930
- suppressNextShieldClick = false;
1931
- if (suppressNextShieldClickTimer !== null) {
1932
- clearTimeout(suppressNextShieldClickTimer);
1933
- suppressNextShieldClickTimer = null;
1934
- }
1935
- return;
1936
- }
1937
- // Suppress the first click in a double-click sequence — the dblclick
1938
- // handler (beginTextEditingFromEvent) will fire immediately after and a
1939
- // spurious element-select would cause inspector flicker.
1940
- if (e.detail >= 2) return;
1941
- var target = elementFromEditorPoint(e.clientX, e.clientY);
1942
- if (!target || target === document.body || target === document.documentElement) {
1943
- // Click on empty canvas: clear the current selection (matches Figma).
1944
- clearRuntimeSelection();
1945
- window.parent.postMessage({ type: 'clear-selection' }, '*');
1946
- return;
1947
- }
1948
- selectedSpacingHovered = false;
1949
- hoveredSpacingHandleKey = '';
1950
- selectedEl = selectionTargetForHit(target);
1951
- var info = getElementInfo(selectedEl);
1952
- positionOverlay(selectionOverlay, selectedEl);
1953
- window.parent.postMessage({ type: 'element-select', payload: info }, '*');
1954
- }
1955
-
1956
- function suppressNextShieldClickBriefly() {
1957
- suppressNextShieldClick = true;
1958
- if (suppressNextShieldClickTimer !== null) {
1959
- clearTimeout(suppressNextShieldClickTimer);
1960
- }
1961
- suppressNextShieldClickTimer = setTimeout(function() {
1962
- suppressNextShieldClick = false;
1963
- suppressNextShieldClickTimer = null;
1964
- }, 250);
1965
- }
1966
-
1967
- function openContextMenuAtEvent(e) {
1968
- stopNativeInteraction(e);
1969
- blurActiveTextEditor();
1970
- var target = elementFromEditorPoint(e.clientX, e.clientY);
1971
- var info = null;
1972
- if (target) {
1973
- selectedSpacingHovered = false;
1974
- hoveredSpacingHandleKey = '';
1975
- selectedEl = selectionTargetForHit(target);
1976
- info = getElementInfo(selectedEl);
1977
- positionOverlay(selectionOverlay, selectedEl);
1978
- window.parent.postMessage({ type: 'element-select', payload: info }, '*');
1979
- }
1980
- window.parent.postMessage({
1981
- type: 'element-contextmenu',
1982
- clientX: e.clientX,
1983
- clientY: e.clientY,
1984
- payload: info
1985
- }, '*');
1986
- }
1987
-
1988
- function findRuntimeTarget(selector, selectorCandidates) {
1989
- var candidates = [];
1990
- if (Array.isArray(selectorCandidates)) {
1991
- selectorCandidates.forEach(function(candidate) {
1992
- if (typeof candidate === 'string' && candidate && candidates.indexOf(candidate) === -1) {
1993
- candidates.push(candidate);
1994
- }
1995
- });
1996
- }
1997
- if (selector && candidates.indexOf(selector) === -1) candidates.push(selector);
1998
- if (
1999
- selectedEl &&
2000
- document.documentElement.contains(selectedEl) &&
2001
- (candidates.length === 0 || matchesExactSelectorList(selectedEl, candidates))
2002
- ) {
2003
- return selectedEl;
2004
- }
2005
- for (var i = 0; i < candidates.length; i += 1) {
2006
- try {
2007
- var match = document.querySelector(candidates[i]);
2008
- if (match && !isLayerInteractionBlocked(match)) return match;
2009
- } catch (_err) {}
2010
- }
2011
- return null;
2012
- }
2013
-
2014
- function removeRuntimeTarget(selector, selectorCandidates) {
2015
- var target = findRuntimeTarget(selector, selectorCandidates);
2016
- if (!target || target === document.body || target === document.documentElement) return false;
2017
- if (target.parentElement) target.parentElement.removeChild(target);
2018
- if (selectedEl === target || !document.documentElement.contains(selectedEl)) {
2019
- selectedEl = null;
2020
- selectionOverlay.style.display = 'none';
2021
- }
2022
- hoveredEl = null;
2023
- highlightOverlay.style.display = 'none';
2024
- hideMeasurements();
2025
- refreshOverlays();
2026
- return true;
2027
- }
2028
-
2029
- function readPx(value) {
2030
- var num = parseFloat(value);
2031
- return Number.isFinite(num) ? num : 0;
2032
- }
2033
-
2034
- function readFinitePx(value) {
2035
- if (!value || value === 'auto') return null;
2036
- var num = parseFloat(value);
2037
- return Number.isFinite(num) ? num : null;
2038
- }
2039
-
2040
- function currentRotation(el) {
2041
- var transform = el.style.transform || window.getComputedStyle(el).transform || '';
2042
- var match = transform.match(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/);
2043
- if (match) return parseFloat(match[1]) || 0;
2044
- if (transform && transform !== 'none' && window.DOMMatrixReadOnly) {
2045
- try {
2046
- var matrix = new DOMMatrixReadOnly(transform);
2047
- return Math.round(Math.atan2(matrix.b, matrix.a) * 180 / Math.PI);
2048
- } catch (err) {}
2049
- }
2050
- return 0;
2051
- }
2052
-
2053
- function mergeRotation(el, degrees) {
2054
- var inline = el.style.transform || '';
2055
- var next = inline.match(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/)
2056
- ? inline.replace(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/, 'rotate(' + degrees + 'deg)')
2057
- : (inline && inline !== 'none' ? inline + ' ' : '') + 'rotate(' + degrees + 'deg)';
2058
- return next.trim();
2059
- }
2060
-
2061
- function ensurePositionable(el) {
2062
- var cs = window.getComputedStyle(el);
2063
- if (cs.position === 'static') {
2064
- el.style.position = 'relative';
2065
- if (!el.style.left) el.style.left = '0px';
2066
- if (!el.style.top) el.style.top = '0px';
2067
- }
2068
- }
2069
-
2070
- function postVisualStyleChange(styles) {
2071
- if (!selectedEl) return;
2072
- window.parent.postMessage({
2073
- type: 'visual-style-change',
2074
- selector: getSelector(selectedEl),
2075
- styles: styles,
2076
- payload: getElementInfo(selectedEl),
2077
- }, '*');
2078
- }
2079
-
2080
- function spacingValueFromPointer(handle, originValue, startX, startY, clientX, clientY) {
2081
- var delta =
2082
- handle.orientation === 'vertical' ? clientX - startX : clientY - startY;
2083
- if (handle.kind === 'padding' && (handle.side === 'right' || handle.side === 'bottom')) {
2084
- delta = -delta;
2085
- }
2086
- return clampSpacingValue(originValue + delta);
2087
- }
2088
-
2089
- function applySpacingDragValue(target, handle, value, mirrorOpposite) {
2090
- if (!target || !handle) return;
2091
- target.style[handle.property] = value + 'px';
2092
- if (handle.kind === 'padding' && mirrorOpposite && handle.oppositeProperty) {
2093
- target.style[handle.oppositeProperty] = value + 'px';
2094
- }
2095
- }
2096
-
2097
- function startSpacingDrag(key, e) {
2098
- var handle = spacingHandleStateByKey[key];
2099
- if (!selectedEl || !handle || isLayerInteractionBlocked(selectedEl)) return;
2100
- e.preventDefault();
2101
- e.stopPropagation();
2102
- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
2103
- var events = dragEventNames(e);
2104
- var dragEl = selectedEl;
2105
- var originValue = handle.value;
2106
- var startX = e.clientX;
2107
- var startY = e.clientY;
2108
- hoveredSpacingHandleKey = key;
2109
- selectedSpacingHovered = true;
2110
- spacingDrag = {
2111
- handle: handle,
2112
- currentValue: originValue,
2113
- mirrorOpposite: !!e.altKey,
2114
- };
2115
- showSpacingBadgeForHandle(handle, originValue);
2116
-
2117
- function onMove(ev) {
2118
- if (!dragEl || !document.documentElement.contains(dragEl)) return;
2119
- var nextValue = spacingValueFromPointer(handle, originValue, startX, startY, ev.clientX, ev.clientY);
2120
- spacingDrag = {
2121
- handle: handle,
2122
- currentValue: nextValue,
2123
- mirrorOpposite: !!ev.altKey,
2124
- };
2125
- applySpacingDragValue(dragEl, handle, nextValue, !!ev.altKey);
2126
- positionOverlay(selectionOverlay, dragEl);
2127
- showSpacingBadgeForHandle(handle, nextValue);
2128
- }
2129
-
2130
- function onUp(ev) {
2131
- document.removeEventListener(events.move, onMove, true);
2132
- document.removeEventListener(events.up, onUp, true);
2133
- if (!dragEl || !document.documentElement.contains(dragEl)) {
2134
- spacingDrag = null;
2135
- return;
2136
- }
2137
- var finalValue = spacingDrag ? spacingDrag.currentValue : originValue;
2138
- var mirrorOpposite = spacingDrag ? spacingDrag.mirrorOpposite : !!ev.altKey;
2139
- applySpacingDragValue(dragEl, handle, finalValue, mirrorOpposite);
2140
- selectedEl = dragEl;
2141
- spacingDrag = null;
2142
- var styles = {};
2143
- styles[handle.property] = finalValue + 'px';
2144
- if (handle.kind === 'padding' && mirrorOpposite && handle.oppositeProperty) {
2145
- styles[handle.oppositeProperty] = finalValue + 'px';
2146
- }
2147
- postVisualStyleChange(styles);
2148
- positionOverlay(selectionOverlay, dragEl);
2149
- }
2150
-
2151
- document.addEventListener(events.move, onMove, true);
2152
- document.addEventListener(events.up, onUp, true);
2153
- }
2154
-
2155
- function postTextContentChange(el, value, html) {
2156
- window.parent.postMessage({
2157
- type: 'text-content-change',
2158
- selector: getSelector(el),
2159
- value: value,
2160
- html: html,
2161
- payload: getElementInfo(el),
2162
- }, '*');
2163
- }
2164
-
2165
- function postTextEditingState(el, active) {
2166
- var selection = window.getSelection && window.getSelection();
2167
- window.parent.postMessage({
2168
- type: 'text-editing-state',
2169
- active: !!active,
2170
- selector: el ? getSelector(el) : '',
2171
- hasRange: !!(active && selection && selection.rangeCount > 0 && !selection.isCollapsed && selectionBelongsToElement(selection, el)),
2172
- }, '*');
2173
- }
2174
-
2175
- function insertPlainTextAtSelection(text) {
2176
- if (!text) return;
2177
- if (document.queryCommandSupported && document.queryCommandSupported('insertText')) {
2178
- document.execCommand('insertText', false, text);
2179
- return;
2180
- }
2181
- var selection = window.getSelection && window.getSelection();
2182
- if (!selection || selection.rangeCount === 0) return;
2183
- var range = selection.getRangeAt(0);
2184
- range.deleteContents();
2185
- var textNode = document.createTextNode(text);
2186
- range.insertNode(textNode);
2187
- range.setStartAfter(textNode);
2188
- range.setEndAfter(textNode);
2189
- selection.removeAllRanges();
2190
- selection.addRange(range);
2191
- }
2192
-
2193
- function selectionBelongsToElement(selection, el) {
2194
- if (!selection || !el || selection.rangeCount === 0) return false;
2195
- var range = selection.getRangeAt(0);
2196
- var ancestor = range.commonAncestorContainer;
2197
- var ancestorEl = ancestor && ancestor.nodeType === 1 ? ancestor : ancestor && ancestor.parentElement;
2198
- return !!(ancestorEl && (ancestorEl === el || el.contains(ancestorEl)));
2199
- }
2200
-
2201
- function applyTextRangeStyle(property, value) {
2202
- if (!activeTextEditEl || !property) return false;
2203
- var selection = window.getSelection && window.getSelection();
2204
- if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return false;
2205
- if (!selectionBelongsToElement(selection, activeTextEditEl)) return false;
2206
- var range = selection.getRangeAt(0);
2207
- var span = document.createElement('span');
2208
- span.style[property] = value;
2209
- if (!span.getAttribute('style')) return false;
2210
- try {
2211
- range.surroundContents(span);
2212
- } catch (err) {
2213
- var contents = range.extractContents();
2214
- span.appendChild(contents);
2215
- range.insertNode(span);
2216
- }
2217
- selection.removeAllRanges();
2218
- var nextRange = document.createRange();
2219
- nextRange.selectNodeContents(span);
2220
- selection.addRange(nextRange);
2221
- return true;
2222
- }
2223
-
2224
- function showTransformBadge(text, clientX, clientY) {
2225
- transformBadge.textContent = text;
2226
- transformBadge.style.display = 'block';
2227
- transformBadge.style.left = clientX + 12 + 'px';
2228
- transformBadge.style.top = clientY + 12 + 'px';
2229
- }
2230
-
2231
- function hideTransformBadge() {
2232
- transformBadge.style.display = 'none';
2233
- }
2234
-
2235
- function hideInsertionGuide() {
2236
- insertionGuide.style.display = 'none';
2237
- }
2238
-
2239
- function isOverlayElement(el) {
2240
- // Use closest() so that children of overlay elements (e.g. spacing-region
2241
- // spans inside selectionOverlay that have pointer-events:auto via a CSS rule
2242
- // and can therefore still be returned by elementFromPoint even when the
2243
- // parent overlay has pointer-events:none set inline) are also treated as
2244
- // overlay elements and never used as drag-drop anchor targets.
2245
- return Boolean(
2246
- el && el.closest && el.closest('[data-agent-native-edit-overlay]')
2247
- );
2248
- }
2249
-
2250
- function draggableElementChildren(parent) {
2251
- return Array.prototype.slice.call(parent.children).filter(function(child) {
2252
- return child.nodeType === 1 && !isOverlayElement(child) && !isLayerInteractionBlocked(child);
2253
- });
2254
- }
2255
-
2256
- function isFlowReorderCandidate(el) {
2257
- if (!el || !el.parentElement) return false;
2258
- if (el === document.body || el === document.documentElement) return false;
2259
- var cs = window.getComputedStyle(el);
2260
- if (cs.position === 'absolute' || cs.position === 'fixed') return false;
2261
- return true;
2262
- }
2263
-
2264
- // keep in sync with EditPanel.tsx CONTAINER_TAGS/LEAF_TAGS (~line 1679)
2265
- var BRIDGE_CONTAINER_TAGS = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'form', 'ul', 'ol', 'figure', 'fieldset', 'details', 'dialog', 'blockquote', 'table', 'tbody', 'thead', 'tr'];
2266
- var BRIDGE_LEAF_TAGS = ['img', 'video', 'picture', 'audio', 'canvas', 'svg', 'path', 'input', 'textarea', 'select', 'br', 'hr', 'iframe'];
2267
- var BRIDGE_TEXT_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'a', 'strong', 'em', 'label', 'li'];
2268
-
2269
- function isContainerDropTarget(el) {
2270
- if (!el || el === document.documentElement) return false;
2271
- if (isOverlayElement(el) || isLayerInteractionBlocked(el)) return false;
2272
- if (el === document.body) return true;
2273
- var tag = (el.tagName || '').toLowerCase();
2274
- // Reject leaf/text tags — they cannot accept children
2275
- if (BRIDGE_LEAF_TAGS.indexOf(tag) !== -1 || BRIDGE_TEXT_TAGS.indexOf(tag) !== -1) return false;
2276
- var cs = window.getComputedStyle(el);
2277
- if (
2278
- cs.display === 'flex' ||
2279
- cs.display === 'inline-flex' ||
2280
- cs.display === 'grid' ||
2281
- cs.display === 'inline-grid'
2282
- ) {
2283
- return true;
2284
- }
2285
- return BRIDGE_CONTAINER_TAGS.indexOf(tag) !== -1;
2286
- }
2287
-
2288
- function edgePlacementForRect(rect, axis, clientX, clientY) {
2289
- var size = axis === 'x' ? rect.width : rect.height;
2290
- if (!size) return null;
2291
- var offset = axis === 'x' ? clientX - rect.left : clientY - rect.top;
2292
- if (offset < size * 0.22) return 'before';
2293
- if (offset > size * 0.78) return 'after';
2294
- return null;
2295
- }
2296
-
2297
- function parentFlowAxis(parent) {
2298
- var cs = window.getComputedStyle(parent);
2299
- if (cs.display === 'flex' || cs.display === 'inline-flex') {
2300
- var isRow = cs.flexDirection && cs.flexDirection.indexOf('row') === 0;
2301
- // Wrapping row containers need Y-axis awareness for inter-row targeting;
2302
- // fall back to column-axis insertion so the heuristic picks the right row.
2303
- var wraps = cs.flexWrap === 'wrap' || cs.flexWrap === 'wrap-reverse';
2304
- if (isRow && !wraps) return 'x';
2305
- return 'y';
2306
- }
2307
- if (cs.display === 'grid' || cs.display === 'inline-grid') {
2308
- var cols = (cs.gridTemplateColumns || '').split(' ').filter(Boolean).length;
2309
- return cols > 1 ? 'x' : 'y';
2310
- }
2311
- return 'y';
2312
- }
2313
-
2314
- function reorderTargetForPoint(el, clientX, clientY) {
2315
- if (!el || !el.parentElement) return null;
2316
- var hit = elementFromEditorPoint(clientX, clientY);
2317
- if (
2318
- hit &&
2319
- hit !== document.documentElement &&
2320
- hit !== el &&
2321
- !el.contains(hit) &&
2322
- !isOverlayElement(hit)
2323
- ) {
2324
- if (isContainerDropTarget(hit)) {
2325
- var containerRect = hit.getBoundingClientRect();
2326
- var edgeAxis = hit.parentElement ? parentFlowAxis(hit.parentElement) : parentFlowAxis(hit);
2327
- var edgePlacement = edgePlacementForRect(containerRect, edgeAxis, clientX, clientY);
2328
- if (!edgePlacement) {
2329
- return { anchor: hit, placement: 'inside', axis: parentFlowAxis(hit) };
2330
- }
2331
- return { anchor: hit, placement: edgePlacement, axis: edgeAxis };
2332
- }
2333
- var hitParent = hit.parentElement;
2334
- if (hitParent) {
2335
- var hitAxis = parentFlowAxis(hitParent);
2336
- var hitRect = hit.getBoundingClientRect();
2337
- var hitCenter = hitAxis === 'x'
2338
- ? hitRect.left + hitRect.width / 2
2339
- : hitRect.top + hitRect.height / 2;
2340
- var hitPointer = hitAxis === 'x' ? clientX : clientY;
2341
- return {
2342
- anchor: hit,
2343
- placement: hitPointer < hitCenter ? 'before' : 'after',
2344
- axis: hitAxis,
2345
- };
2346
- }
2347
- }
2348
- var parent = el.parentElement;
2349
- var axis = parentFlowAxis(parent);
2350
- var siblings = draggableElementChildren(parent).filter(function(child) {
2351
- return child !== el;
2352
- });
2353
- if (!siblings.length) return null;
2354
- var beforeTarget = null;
2355
- for (var i = 0; i < siblings.length; i += 1) {
2356
- var rect = siblings[i].getBoundingClientRect();
2357
- var center = axis === 'x' ? rect.left + rect.width / 2 : rect.top + rect.height / 2;
2358
- var pointer = axis === 'x' ? clientX : clientY;
2359
- if (pointer < center) {
2360
- beforeTarget = siblings[i];
2361
- break;
2362
- }
2363
- }
2364
- var anchor = beforeTarget || siblings[siblings.length - 1];
2365
- var placement = beforeTarget ? 'before' : 'after';
2366
- return { anchor: anchor, placement: placement, axis: axis };
2367
- }
2368
-
2369
- function showInsertionGuideFor(target) {
2370
- if (!target || !target.anchor) {
2371
- hideInsertionGuide();
2372
- return;
2373
- }
2374
- var rect = target.anchor.getBoundingClientRect();
2375
- insertionGuide.style.display = 'block';
2376
- insertionGuide.style.background = 'var(--design-editor-accent-color)';
2377
- insertionGuide.style.border = '0';
2378
- insertionGuide.style.borderRadius = '999px';
2379
- insertionGuide.style.boxShadow = '0 0 0 1px var(--design-editor-accent-color)';
2380
- if (target.placement === 'inside') {
2381
- insertionGuide.style.left = rect.left + 'px';
2382
- insertionGuide.style.top = rect.top + 'px';
2383
- insertionGuide.style.width = rect.width + 'px';
2384
- insertionGuide.style.height = rect.height + 'px';
2385
- insertionGuide.style.background = 'color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)';
2386
- insertionGuide.style.border = '2px solid var(--design-editor-accent-color)';
2387
- insertionGuide.style.borderRadius = '4px';
2388
- insertionGuide.style.boxShadow = 'none';
2389
- return;
2390
- }
2391
- if (target.axis === 'x') {
2392
- var x = target.placement === 'before' ? rect.left : rect.right;
2393
- insertionGuide.style.left = x + 'px';
2394
- insertionGuide.style.top = rect.top + 'px';
2395
- insertionGuide.style.width = '2px';
2396
- insertionGuide.style.height = rect.height + 'px';
2397
- } else {
2398
- var y = target.placement === 'before' ? rect.top : rect.bottom;
2399
- insertionGuide.style.left = rect.left + 'px';
2400
- insertionGuide.style.top = y + 'px';
2401
- insertionGuide.style.width = rect.width + 'px';
2402
- insertionGuide.style.height = '2px';
2403
- }
2404
- }
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
+ `;
2405
89
 
2406
- function applyRuntimeReorder(el, target) {
2407
- if (!el || !target || !target.anchor || !target.anchor.parentElement) return;
2408
- if (target.placement === 'inside') {
2409
- target.anchor.appendChild(el);
2410
- return;
2411
- }
2412
- var parent = target.anchor.parentElement;
2413
- if (target.placement === 'before') {
2414
- parent.insertBefore(el, target.anchor);
2415
- } else {
2416
- parent.insertBefore(el, target.anchor.nextSibling);
2417
- }
2418
- }
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
+ `;
2419
103
 
2420
- function postVisualStructureChange(el, target, origin) {
2421
- if (!el || !target || !target.anchor) return;
2422
- var requestId = 'move-' + Date.now() + '-' + Math.random().toString(16).slice(2);
2423
- pendingStructureMove = { requestId: requestId, el: el, target: target, origin: origin || null };
2424
- window.parent.postMessage({
2425
- type: 'visual-structure-change',
2426
- requestId: requestId,
2427
- selector: getSelector(el),
2428
- sourceId: getSourceId(el),
2429
- anchorSelector: getSelector(target.anchor),
2430
- anchorSourceId: getSourceId(target.anchor),
2431
- placement: target.placement,
2432
- payload: getElementInfo(el),
2433
- }, '*');
2434
- }
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
+ `;
2435
117
 
2436
- function postVisualDuplicateChange(originalEl, cloneEl, target) {
2437
- if (!originalEl || !cloneEl) return;
2438
- window.parent.postMessage({
2439
- type: 'visual-duplicate-change',
2440
- selector: getSelector(originalEl),
2441
- sourceId: getSourceId(originalEl),
2442
- anchorSelector: target && target.anchor ? getSelector(target.anchor) : '',
2443
- anchorSourceId: target && target.anchor ? getSourceId(target.anchor) : '',
2444
- placement: target && target.placement ? target.placement : 'after',
2445
- cloneHtml: cloneEl.outerHTML,
2446
- payload: getElementInfo(cloneEl),
2447
- }, '*');
2448
- }
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
+ `;
2449
130
 
2450
- function startMove(e) {
2451
- if (!selectedEl) return;
2452
- if (isLayerInteractionBlocked(selectedEl)) return;
2453
- e.preventDefault();
2454
- e.stopPropagation();
2455
- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
2456
- var events = dragEventNames(e);
2457
- var originalSelectedEl = selectedEl;
2458
- var duplicatedForDrag = false;
2459
- if (e.altKey && selectedEl !== document.body && selectedEl !== document.documentElement) {
2460
- var clone = selectedEl.cloneNode(true);
2461
- resetRuntimeStableIds(clone);
2462
- selectedEl.parentElement.insertBefore(clone, selectedEl.nextSibling);
2463
- selectedEl = clone;
2464
- duplicatedForDrag = true;
2465
- positionOverlay(selectionOverlay, selectedEl);
2466
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2467
- }
2468
- if (isFlowReorderCandidate(selectedEl)) {
2469
- // Snapshot the element being reordered so a concurrent select-element or
2470
- // clear-selection postMessage cannot mutate the wrong element mid-drag.
2471
- var reorderEl = selectedEl;
2472
- var currentTarget = reorderTargetForPoint(reorderEl, e.clientX, e.clientY);
2473
- showInsertionGuideFor(currentTarget);
2474
- // Cross-screen drag state: true when the pointer is outside this iframe's
2475
- // viewport bounds. The host frame renders the ghost + highlight and owns
2476
- // the drop when this is true; the bridge suppresses its in-iframe reorder.
2477
- var pointerOutsideIframe = false;
2478
- var reorderSelector = getSelector(reorderEl);
2479
- var reorderSourceId = getSourceId(reorderEl);
2480
- function onReorderMove(ev) {
2481
- var vw = window.innerWidth;
2482
- var vh = window.innerHeight;
2483
- var cx = ev.clientX;
2484
- var cy = ev.clientY;
2485
- var outside = cx < 0 || cy < 0 || cx > vw || cy > vh;
2486
- pointerOutsideIframe = outside;
2487
- // Always notify the host frame so it can track the cursor position,
2488
- // render the ghost, and highlight the target screen.
2489
- window.parent.postMessage({
2490
- type: 'agent-native:cross-screen-drag',
2491
- phase: 'move',
2492
- selector: reorderSelector,
2493
- sourceId: reorderSourceId,
2494
- iframeX: cx,
2495
- iframeY: cy,
2496
- viewportW: vw,
2497
- viewportH: vh,
2498
- }, '*');
2499
- if (outside) {
2500
- // Cursor left this iframe — hide the in-iframe insertion guide so
2501
- // it does not render while the host shows a cross-screen drop target.
2502
- hideInsertionGuide();
2503
- showTransformBadge('Move layer', cx, cy);
2504
- } else {
2505
- // Cursor is inside this iframe — use existing in-iframe behavior.
2506
- currentTarget = reorderTargetForPoint(reorderEl, cx, cy);
2507
- showInsertionGuideFor(currentTarget);
2508
- showTransformBadge(currentTarget ? 'Move layer' : 'Move', cx, cy);
2509
- }
2510
- }
2511
- function onReorderEscape() {
2512
- document.removeEventListener(events.move, onReorderMove, true);
2513
- document.removeEventListener(events.up, onReorderUp, true);
2514
- document.removeEventListener('keydown', onReorderKeyDown, true);
2515
- hideTransformBadge();
2516
- hideInsertionGuide();
2517
- window.parent.postMessage({ type: 'agent-native:cross-screen-drag', phase: 'cancel' }, '*');
2518
- // Revert any clone that was inserted for alt-drag.
2519
- if (duplicatedForDrag && reorderEl && reorderEl !== originalSelectedEl) {
2520
- if (reorderEl.parentElement) reorderEl.parentElement.removeChild(reorderEl);
2521
- selectedEl = originalSelectedEl;
2522
- positionOverlay(selectionOverlay, selectedEl);
2523
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2524
- }
2525
- }
2526
- function onReorderKeyDown(ev) {
2527
- if (ev.key === 'Escape') {
2528
- ev.preventDefault();
2529
- ev.stopPropagation();
2530
- if (ev.stopImmediatePropagation) ev.stopImmediatePropagation();
2531
- onReorderEscape();
2532
- }
2533
- }
2534
- function onReorderUp(ev) {
2535
- document.removeEventListener(events.move, onReorderMove, true);
2536
- document.removeEventListener(events.up, onReorderUp, true);
2537
- document.removeEventListener('keydown', onReorderKeyDown, true);
2538
- hideTransformBadge();
2539
- hideInsertionGuide();
2540
- var vw = window.innerWidth;
2541
- var vh = window.innerHeight;
2542
- var cx = ev ? ev.clientX : 0;
2543
- var cy = ev ? ev.clientY : 0;
2544
- var outsideOnDrop = cx < 0 || cy < 0 || cx > vw || cy > vh;
2545
- // Post the end message so the host can finalize a cross-screen drop.
2546
- window.parent.postMessage({
2547
- type: 'agent-native:cross-screen-drag',
2548
- phase: 'end',
2549
- selector: reorderSelector,
2550
- sourceId: reorderSourceId,
2551
- iframeX: cx,
2552
- iframeY: cy,
2553
- viewportW: vw,
2554
- viewportH: vh,
2555
- }, '*');
2556
- // When the pointer is outside this iframe at release, the host owns the
2557
- // move (cross-screen drop). Do NOT apply the in-iframe reorder so we
2558
- // avoid a ghost element left in screen A's DOM.
2559
- // Use outsideOnDrop only — pointerOutsideIframe is stale when the user
2560
- // briefly exits the iframe and re-enters before releasing. The host
2561
- // already clears cross-screen state on re-entry so checking the
2562
- // momentary excursion flag here would wrongly drop the element nowhere.
2563
- if (outsideOnDrop) return;
2564
- if (!currentTarget) {
2565
- // No valid drop target — clean up the clone if one was inserted so
2566
- // no ghost element is left in the DOM.
2567
- if (duplicatedForDrag && reorderEl && reorderEl !== originalSelectedEl) {
2568
- if (reorderEl.parentElement) reorderEl.parentElement.removeChild(reorderEl);
2569
- selectedEl = originalSelectedEl;
2570
- positionOverlay(selectionOverlay, selectedEl);
2571
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2572
- }
2573
- return;
2574
- }
2575
- if (duplicatedForDrag) {
2576
- applyRuntimeReorder(reorderEl, currentTarget);
2577
- postVisualDuplicateChange(originalSelectedEl, reorderEl, currentTarget);
2578
- } else {
2579
- // Capture the pre-drag DOM anchor so we can revert if the parent
2580
- // reports applied===false on the structure-ack.
2581
- var prevParent = reorderEl.parentElement;
2582
- var prevNextSibling = reorderEl.nextSibling;
2583
- // Optimistically apply the reorder in the DOM for immediate
2584
- // visual feedback; the visual-structure-ack handler will confirm
2585
- // or revert once the parent processes the change.
2586
- applyRuntimeReorder(reorderEl, currentTarget);
2587
- postVisualStructureChange(reorderEl, currentTarget, { prevParent: prevParent, prevNextSibling: prevNextSibling });
2588
- }
2589
- }
2590
- document.addEventListener(events.move, onReorderMove, true);
2591
- document.addEventListener(events.up, onReorderUp, true);
2592
- document.addEventListener('keydown', onReorderKeyDown, true);
2593
- return;
2594
- }
2595
- ensurePositionable(selectedEl);
2596
- var cs = window.getComputedStyle(selectedEl);
2597
- var originLeft = readPx(selectedEl.style.left || cs.left);
2598
- var originTop = readPx(selectedEl.style.top || cs.top);
2599
- var startX = e.clientX;
2600
- var startY = e.clientY;
2601
- // Snapshot the element being moved so that a concurrent select-element or
2602
- // clear-selection postMessage cannot swap selectedEl mid-drag and cause
2603
- // mutations on the wrong element or a null-deref in onUp.
2604
- var dragEl = selectedEl;
2605
- var moved = false;
2606
- var DRAG_THRESHOLD = 3;
2607
- function onMove(ev) {
2608
- if (!moved && Math.hypot(ev.clientX - startX, ev.clientY - startY) > DRAG_THRESHOLD) {
2609
- moved = true;
2610
- }
2611
- var nextLeft = originLeft + ev.clientX - startX;
2612
- var nextTop = originTop + ev.clientY - startY;
2613
- dragEl.style.left = Math.round(nextLeft) + 'px';
2614
- dragEl.style.top = Math.round(nextTop) + 'px';
2615
- showTransformBadge('X ' + Math.round(nextLeft) + ' Y ' + Math.round(nextTop), ev.clientX, ev.clientY);
2616
- refreshOverlays();
2617
- }
2618
- function onUp() {
2619
- document.removeEventListener(events.move, onMove, true);
2620
- document.removeEventListener(events.up, onUp, true);
2621
- hideTransformBadge();
2622
- if (!dragEl) return;
2623
- if (duplicatedForDrag && !moved) {
2624
- // Alt-click with no real drag — remove the premature clone and restore the original selection.
2625
- if (dragEl.parentElement) dragEl.parentElement.removeChild(dragEl);
2626
- selectedEl = originalSelectedEl;
2627
- positionOverlay(selectionOverlay, selectedEl);
2628
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2629
- return;
2630
- }
2631
- if (duplicatedForDrag) {
2632
- postVisualDuplicateChange(originalSelectedEl, dragEl);
2633
- } else {
2634
- window.parent.postMessage({
2635
- type: 'visual-style-change',
2636
- selector: getSelector(dragEl),
2637
- styles: {
2638
- position: dragEl.style.position,
2639
- left: dragEl.style.left,
2640
- top: dragEl.style.top,
2641
- },
2642
- payload: getElementInfo(dragEl),
2643
- }, '*');
2644
- }
2645
- }
2646
- document.addEventListener(events.move, onMove, true);
2647
- document.addEventListener(events.up, onUp, true);
2648
- }
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
+ `;
2649
143
 
2650
- function startResize(handle, e) {
2651
- if (!selectedEl) return;
2652
- if (isLayerInteractionBlocked(selectedEl)) return;
2653
- e.preventDefault();
2654
- e.stopPropagation();
2655
- ensurePositionable(selectedEl);
2656
- var cs = window.getComputedStyle(selectedEl);
2657
- // Bug fix: use CSS width/height (not getBoundingClientRect) for the resize
2658
- // origin dimensions so that rotated elements don't use the inflated
2659
- // axis-aligned bounding box as the starting size.
2660
- var originW = readPx(selectedEl.style.width || cs.width);
2661
- var originH = readPx(selectedEl.style.height || cs.height);
2662
- var origin = {
2663
- left: readPx(selectedEl.style.left || cs.left),
2664
- top: readPx(selectedEl.style.top || cs.top),
2665
- width: originW,
2666
- height: originH,
2667
- ratio: originW / Math.max(1, originH),
2668
- };
2669
- var startX = e.clientX;
2670
- var startY = e.clientY;
2671
- // Snapshot the element so a concurrent clear-selection postMessage cannot
2672
- // cause a null-deref in onMove/onUp.
2673
- var resizeEl = selectedEl;
2674
- // Capture the element rotation once at drag-start so per-move projection is
2675
- // cheap and consistent even if the transform changes during the drag.
2676
- var resizeTheta = currentRotation(resizeEl) * Math.PI / 180;
2677
- function nextRect(ev) {
2678
- var screenDx = ev.clientX - startX;
2679
- var screenDy = ev.clientY - startY;
2680
- // Project the screen-space pointer delta into the element's local
2681
- // (un-rotated) coordinate frame so handles behave relative to the
2682
- // visible rotated box rather than screen axes.
2683
- var cosT = Math.cos(resizeTheta);
2684
- var sinT = Math.sin(resizeTheta);
2685
- var dx = screenDx * cosT + screenDy * sinT;
2686
- var dy = -screenDx * sinT + screenDy * cosT;
2687
- var left = origin.left;
2688
- var top = origin.top;
2689
- var width = origin.width;
2690
- var height = origin.height;
2691
- if (handle.indexOf('w') !== -1) {
2692
- left = origin.left + dx;
2693
- width = origin.width - dx;
2694
- }
2695
- if (handle.indexOf('e') !== -1) width = origin.width + dx;
2696
- if (handle.indexOf('n') !== -1) {
2697
- top = origin.top + dy;
2698
- height = origin.height - dy;
2699
- }
2700
- if (handle.indexOf('s') !== -1) height = origin.height + dy;
2701
- // Apply Shift / scaleToolEnabled aspect-ratio lock BEFORE the min-size
2702
- // clamp so the ratio is computed from unclamped values (bug fix).
2703
- if (ev.shiftKey) {
2704
- // Shift locks aspect ratio for ALL 8 handles (corners and edges).
2705
- if (handle === 'e' || handle === 'w') {
2706
- height = width / origin.ratio;
2707
- } else if (handle === 'n' || handle === 's') {
2708
- width = height * origin.ratio;
2709
- } else if (handle.length === 2) {
2710
- if (Math.abs(dx) > Math.abs(dy)) height = width / origin.ratio;
2711
- else width = height * origin.ratio;
2712
- }
2713
- }
2714
- if (scaleToolEnabled) {
2715
- // Scale tool: enforce aspect ratio on all 8 handles, not just corners.
2716
- if (handle === 'e' || handle === 'w') {
2717
- height = width / origin.ratio;
2718
- } else if (handle === 'n' || handle === 's') {
2719
- width = height * origin.ratio;
2720
- } else if (handle.length === 2) {
2721
- if (Math.abs(dx) > Math.abs(dy)) height = width / origin.ratio;
2722
- else width = height * origin.ratio;
2723
- }
2724
- }
2725
- // Clamp to minimum size.
2726
- var clampedW = Math.max(8, width);
2727
- var clampedH = Math.max(8, height);
2728
- // After clamping, re-apply the ratio if Shift or scale tool is active so
2729
- // the clamped dimension doesn't silently break the locked aspect ratio.
2730
- if (ev.shiftKey || scaleToolEnabled) {
2731
- if (clampedW !== width) {
2732
- // Width was clamped; re-derive height from the clamped width.
2733
- clampedH = Math.max(8, clampedW / origin.ratio);
2734
- } else if (clampedH !== height) {
2735
- // Height was clamped; re-derive width from the clamped height.
2736
- clampedW = Math.max(8, clampedH * origin.ratio);
2737
- }
2738
- }
2739
- width = clampedW;
2740
- height = clampedH;
2741
- // Re-anchor the pinned edge for w/n handles after aspect-ratio lock and
2742
- // clamping so the opposite (e/s) edge stays fixed regardless of whether
2743
- // the dimension change was driven by raw dx/dy or by the ratio lock.
2744
- if (handle.indexOf('w') !== -1) left = origin.left + (origin.width - width);
2745
- if (handle.indexOf('n') !== -1) top = origin.top + (origin.height - height);
2746
- if (ev.altKey) {
2747
- if (handle.indexOf('w') !== -1 || handle.indexOf('e') !== -1) left = origin.left - (width - origin.width) / 2;
2748
- if (handle.indexOf('n') !== -1 || handle.indexOf('s') !== -1) top = origin.top - (height - origin.height) / 2;
2749
- }
2750
- return { left: left, top: top, width: width, height: height };
2751
- }
2752
- function onMove(ev) {
2753
- if (!resizeEl) return;
2754
- var rect = nextRect(ev);
2755
- resizeEl.style.left = Math.round(rect.left) + 'px';
2756
- resizeEl.style.top = Math.round(rect.top) + 'px';
2757
- resizeEl.style.width = Math.round(rect.width) + 'px';
2758
- resizeEl.style.height = Math.round(rect.height) + 'px';
2759
- showTransformBadge(Math.round(rect.width) + ' x ' + Math.round(rect.height), ev.clientX, ev.clientY);
2760
- refreshOverlays();
2761
- }
2762
- function onUp() {
2763
- document.removeEventListener('mousemove', onMove, true);
2764
- document.removeEventListener('mouseup', onUp, true);
2765
- hideTransformBadge();
2766
- if (!resizeEl) return;
2767
- window.parent.postMessage({
2768
- type: 'visual-style-change',
2769
- selector: getSelector(resizeEl),
2770
- styles: {
2771
- position: resizeEl.style.position,
2772
- left: resizeEl.style.left,
2773
- top: resizeEl.style.top,
2774
- width: resizeEl.style.width,
2775
- height: resizeEl.style.height,
2776
- },
2777
- payload: getElementInfo(resizeEl),
2778
- }, '*');
2779
- }
2780
- document.addEventListener('mousemove', onMove, true);
2781
- document.addEventListener('mouseup', onUp, true);
2782
- }
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
+ `;
2783
157
 
2784
- function startRotate(e) {
2785
- if (!selectedEl) return;
2786
- if (isLayerInteractionBlocked(selectedEl)) return;
2787
- e.preventDefault();
2788
- e.stopPropagation();
2789
- // getBoundingClientRect is correct here we only need the element center
2790
- // for angle math, and the element's visual position is what we want.
2791
- var rect = selectedEl.getBoundingClientRect();
2792
- var center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
2793
- var originAngle = Math.atan2(e.clientY - center.y, e.clientX - center.x) * 180 / Math.PI;
2794
- var originRotation = currentRotation(selectedEl);
2795
- // Snapshot so a concurrent clear-selection postMessage cannot cause a
2796
- // null-deref in onMove/onUp.
2797
- var rotateEl = selectedEl;
2798
- function onMove(ev) {
2799
- if (!rotateEl) return;
2800
- var pointerAngle = Math.atan2(ev.clientY - center.y, ev.clientX - center.x) * 180 / Math.PI;
2801
- var next = originRotation + pointerAngle - originAngle;
2802
- if (ev.shiftKey) next = Math.round(next / 15) * 15;
2803
- next = Math.round(next);
2804
- rotateEl.style.transform = mergeRotation(rotateEl, next);
2805
- showTransformBadge(next + 'deg', ev.clientX, ev.clientY);
2806
- refreshOverlays();
2807
- }
2808
- function onUp() {
2809
- document.removeEventListener('mousemove', onMove, true);
2810
- document.removeEventListener('mouseup', onUp, true);
2811
- hideTransformBadge();
2812
- if (!rotateEl) return;
2813
- window.parent.postMessage({
2814
- type: 'visual-style-change',
2815
- selector: getSelector(rotateEl),
2816
- styles: { transform: rotateEl.style.transform },
2817
- payload: getElementInfo(rotateEl),
2818
- }, '*');
2819
- }
2820
- document.addEventListener('mousemove', onMove, true);
2821
- document.addEventListener('mouseup', onUp, true);
2822
- }
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
+ `;
2823
184
 
2824
- function clearPendingShieldDrag() {
2825
- if (!pendingShieldDrag) return;
2826
- document.removeEventListener(pendingShieldDrag.move, pendingShieldDrag.onMove, true);
2827
- document.removeEventListener(pendingShieldDrag.up, pendingShieldDrag.onUp, true);
2828
- pendingShieldDrag = null;
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
+ );
2829
197
  }
2830
-
2831
- function beginPotentialShieldDrag(e) {
2832
- stopNativeInteraction(e);
2833
- if (e.button !== 0 || activeTextEditEl) return;
2834
- var events = dragEventNames(e);
2835
- var hit = elementFromEditorPoint(e.clientX, e.clientY);
2836
- if (!hit || hit === document.body || hit === document.documentElement) return;
2837
- var dragTarget =
2838
- selectedEl &&
2839
- document.documentElement.contains(selectedEl) &&
2840
- selectedEl.contains(hit)
2841
- ? selectedEl
2842
- : selectionTargetForHit(hit);
2843
- if (
2844
- !dragTarget ||
2845
- dragTarget === document.body ||
2846
- dragTarget === document.documentElement ||
2847
- isLayerInteractionBlocked(dragTarget)
2848
- ) {
2849
- return;
2850
- }
2851
- var startX = e.clientX;
2852
- var startY = e.clientY;
2853
- var didStartDrag = false;
2854
- function selectDragTarget() {
2855
- selectedEl = dragTarget;
2856
- positionOverlay(selectionOverlay, selectedEl);
2857
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2858
- }
2859
- function onMove(ev) {
2860
- if (Math.hypot(ev.clientX - startX, ev.clientY - startY) <= 3) return;
2861
- clearPendingShieldDrag();
2862
- didStartDrag = true;
2863
- selectDragTarget();
2864
- suppressNextShieldClickBriefly();
2865
- startMove(ev);
2866
- }
2867
- function onUp(ev) {
2868
- clearPendingShieldDrag();
2869
- if (didStartDrag) return;
2870
- if (ev) stopNativeInteraction(ev);
2871
- selectDragTarget();
2872
- suppressNextShieldClickBriefly();
2873
- }
2874
- clearPendingShieldDrag();
2875
- pendingShieldDrag = { move: events.move, up: events.up, onMove: onMove, onUp: onUp };
2876
- document.addEventListener(events.move, onMove, true);
2877
- document.addEventListener(events.up, onUp, true);
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
+ );
2878
203
  }
204
+ return html + LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT;
205
+ }
2879
206
 
2880
- selectionOverlay.addEventListener('mousedown', function(e) {
2881
- var spacingKey = e.target && e.target.getAttribute && e.target.getAttribute('data-spacing-key');
2882
- if (spacingKey) {
2883
- startSpacingDrag(spacingKey, e);
2884
- return;
2885
- }
2886
- var resizeHandle = e.target && e.target.getAttribute && e.target.getAttribute('data-agent-native-edit-handle');
2887
- if (!resizeHandle && e.target && e.target.getAttribute) {
2888
- resizeHandle = e.target.getAttribute('data-agent-native-edge-handle');
2889
- }
2890
- if (resizeHandle) {
2891
- startResize(resizeHandle, e);
2892
- return;
2893
- }
2894
- var rotateHandle = e.target && e.target.getAttribute && e.target.getAttribute('data-agent-native-rotate-handle');
2895
- if (rotateHandle) {
2896
- startRotate(e);
2897
- return;
2898
- }
2899
- startMove(e);
2900
- }, true);
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
+ ];
2901
218
 
2902
- shieldOverlay.addEventListener('pointerdown', beginPotentialShieldDrag, true);
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
+ }
2903
229
 
2904
- ['pointerdown','pointerup','mousedown','mouseup','auxclick'].forEach(function(type) {
2905
- shieldOverlay.addEventListener(type, stopNativeInteraction, 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]);
2906
239
  });
2907
-
2908
- shieldOverlay.addEventListener('click', selectElementAtEvent, true);
2909
- shieldOverlay.addEventListener('contextmenu', openContextMenuAtEvent, true);
2910
- selectionOverlay.addEventListener('contextmenu', openContextMenuAtEvent, true);
2911
- document.addEventListener('contextmenu', function(e) {
2912
- if (isOverlayElement(e.target)) return;
2913
- openContextMenuAtEvent(e);
2914
- }, true);
2915
-
2916
- document.addEventListener('keydown', function(e) {
2917
- if (!shouldForwardDesignHotkey(e)) return;
2918
- stopNativeInteraction(e);
2919
- if (e.key === 'Escape') clearRuntimeSelection();
2920
- window.parent.postMessage({
2921
- type: 'design-hotkey',
2922
- key: e.key,
2923
- code: e.code,
2924
- metaKey: !!e.metaKey,
2925
- ctrlKey: !!e.ctrlKey,
2926
- shiftKey: !!e.shiftKey,
2927
- altKey: !!e.altKey,
2928
- repeat: !!e.repeat
2929
- }, '*');
2930
- }, true);
2931
-
2932
- function placeTextCaretFromPoint(target, clientX, clientY) {
2933
- try {
2934
- var range = null;
2935
- if (document.caretRangeFromPoint) {
2936
- range = document.caretRangeFromPoint(clientX, clientY);
2937
- } else if (document.caretPositionFromPoint) {
2938
- var position = document.caretPositionFromPoint(clientX, clientY);
2939
- if (position) {
2940
- range = document.createRange();
2941
- range.setStart(position.offsetNode, position.offset);
2942
- }
2943
- }
2944
- if (!range) {
2945
- range = document.createRange();
2946
- range.selectNodeContents(target);
2947
- range.collapse(false);
2948
- }
2949
- var selection = window.getSelection();
2950
- selection.removeAllRanges();
2951
- selection.addRange(range);
2952
- } catch (err) {
2953
- try {
2954
- var fallbackRange = document.createRange();
2955
- fallbackRange.selectNodeContents(target);
2956
- fallbackRange.collapse(false);
2957
- var fallbackSelection = window.getSelection();
2958
- fallbackSelection.removeAllRanges();
2959
- fallbackSelection.addRange(fallbackRange);
2960
- } catch (_err) {}
2961
- }
2962
- }
2963
-
2964
- function beginTextEditingFromEvent(e) {
2965
- if (activeTextEditEl && e.target && activeTextEditEl.contains(e.target)) return;
2966
- if (!textEditingEnabled) {
2967
- stopNativeInteraction(e);
2968
- return;
2969
- }
2970
- stopNativeInteraction(e);
2971
- var target = findTextEditTarget(elementFromEditorPoint(e.clientX, e.clientY));
2972
- if (!target || target.nodeType !== 1) return;
2973
- // Anchor the selection identity to the nearest source-backed element. Text
2974
- // editing still operates on the actual target text node, but a later
2975
- // style edit posts from selectedEl, so it must point at a patchable
2976
- // code-layer node rather than a runtime-only descendant (which would emit a
2977
- // brittle body > div:nth-of-type(...) selector that never resolves).
2978
- selectedEl = selectionTargetForHit(target) || target;
2979
- var originalText = target.textContent || '';
2980
- var originalHtml = target.innerHTML || '';
2981
- var committed = false;
2982
- activeTextEditEl = target;
2983
- target.setAttribute('contenteditable', 'true');
2984
- target.setAttribute('data-agent-native-text-editing', 'true');
2985
- target.style.cursor = 'text';
2986
- target.style.outline = '1.5px solid var(--design-editor-accent-color)';
2987
- target.style.outlineOffset = '2px';
2988
- setTextEditingPointerPassthrough(true);
2989
- positionOverlay(selectionOverlay, target);
2990
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(target) }, '*');
2991
- window.parent.postMessage({ type: 'element-dblclick-text', payload: getElementInfo(target) }, '*');
2992
- postTextEditingState(target, true);
2993
-
2994
- function finish(commit) {
2995
- if (committed) return;
2996
- committed = true;
2997
- target.removeEventListener('blur', onBlur, true);
2998
- target.removeEventListener('keydown', onKeyDown, true);
2999
- target.removeEventListener('paste', onPaste, true);
3000
- target.removeEventListener('keyup', onSelectionChange, true);
3001
- target.removeEventListener('mouseup', onSelectionChange, true);
3002
- document.removeEventListener('selectionchange', onSelectionChange);
3003
- target.removeAttribute('contenteditable');
3004
- target.removeAttribute('data-agent-native-text-editing');
3005
- target.style.cursor = '';
3006
- target.style.outline = '';
3007
- target.style.outlineOffset = '';
3008
- setTextEditingPointerPassthrough(false);
3009
- if (activeTextEditEl === target) activeTextEditEl = null;
3010
- postTextEditingState(target, false);
3011
- if (!commit) {
3012
- target.innerHTML = originalHtml;
3013
- refreshOverlays();
3014
- return;
3015
- }
3016
- var next = target.textContent || '';
3017
- var nextHtml = target.innerHTML || '';
3018
- refreshOverlays();
3019
- if (next !== originalText || nextHtml !== originalHtml) {
3020
- postTextContentChange(target, next, nextHtml);
3021
- }
3022
- }
3023
-
3024
- function onBlur() {
3025
- finish(true);
3026
- }
3027
-
3028
- function onKeyDown(ev) {
3029
- if (ev.key === 'Escape') {
3030
- ev.preventDefault();
3031
- finish(true);
3032
- target.blur();
3033
- return;
3034
- }
3035
- if (ev.key === 'Enter' && !ev.shiftKey) {
3036
- ev.preventDefault();
3037
- finish(true);
3038
- target.blur();
3039
- }
3040
- }
3041
-
3042
- function onPaste(ev) {
3043
- ev.preventDefault();
3044
- insertPlainTextAtSelection((ev.clipboardData && ev.clipboardData.getData('text/plain')) || '');
3045
- }
3046
-
3047
- function onSelectionChange() {
3048
- postTextEditingState(target, true);
3049
- }
3050
-
3051
- target.addEventListener('blur', onBlur, true);
3052
- target.addEventListener('keydown', onKeyDown, true);
3053
- target.addEventListener('paste', onPaste, true);
3054
- target.addEventListener('keyup', onSelectionChange, true);
3055
- target.addEventListener('mouseup', onSelectionChange, true);
3056
- document.addEventListener('selectionchange', onSelectionChange);
3057
- target.focus();
3058
- placeTextCaretFromPoint(target, e.clientX, e.clientY);
3059
- }
3060
-
3061
- shieldOverlay.addEventListener('dblclick', beginTextEditingFromEvent, true);
3062
- selectionOverlay.addEventListener('dblclick', beginTextEditingFromEvent, true);
3063
- document.addEventListener('dblclick', function(e) {
3064
- if (isOverlayElement(e.target)) return;
3065
- beginTextEditingFromEvent(e);
3066
- }, true);
3067
-
3068
- shieldOverlay.addEventListener('pointermove', function(e) {
3069
- stopNativeInteraction(e);
3070
- hoveredEl = elementFromEditorPoint(e.clientX, e.clientY);
3071
- if (!hoveredEl) {
3072
- highlightOverlay.style.display = 'none';
3073
- if (!spacingDrag) {
3074
- selectedSpacingHovered = false;
3075
- hoveredSpacingHandleKey = '';
3076
- updateSpacingOverlay(selectedEl);
3077
- }
3078
- hideMeasurements();
3079
- return;
3080
- }
3081
- if (hoveredEl && hoveredEl.closest('[data-agent-native-text-editing]')) return;
3082
- if (!spacingDrag) {
3083
- selectedSpacingHovered = Boolean(
3084
- selectedEl &&
3085
- hoveredEl &&
3086
- (hoveredEl === selectedEl ||
3087
- (selectedEl.contains && selectedEl.contains(hoveredEl))),
3088
- );
3089
- if (!selectedSpacingHovered) hoveredSpacingHandleKey = '';
3090
- updateSpacingOverlay(selectedEl);
3091
- }
3092
- if (hoveredEl === selectedEl) {
3093
- highlightOverlay.style.display = 'none';
3094
- } else {
3095
- positionOverlay(highlightOverlay, hoveredEl);
3096
- }
3097
- if (e.altKey && selectedEl && hoveredEl && selectedEl !== hoveredEl) {
3098
- showMeasurements(selectedEl, hoveredEl);
3099
- } else {
3100
- hideMeasurements();
3101
- }
3102
- var info = getElementInfo(hoveredEl);
3103
- window.parent.postMessage({ type: 'element-hover', payload: info }, '*');
3104
- }, true);
3105
-
3106
- shieldOverlay.addEventListener('pointerleave', function(e) {
3107
- stopNativeInteraction(e);
3108
- hoveredEl = null;
3109
- if (!spacingDrag) {
3110
- selectedSpacingHovered = false;
3111
- hoveredSpacingHandleKey = '';
3112
- updateSpacingOverlay(selectedEl);
3113
- }
3114
- highlightOverlay.style.display = 'none';
3115
- hideMeasurements();
3116
- window.parent.postMessage({ type: 'element-hover', payload: null }, '*');
3117
- }, true);
3118
-
3119
- window.addEventListener('keyup', function(e) {
3120
- if (e.key === 'Alt') hideMeasurements();
3121
- }, true);
3122
-
3123
- window.addEventListener('message', function(e) {
3124
- if (e.source !== window.parent) return;
3125
- if (!e.data) return;
3126
- if (e.data.type === 'set-editor-chrome-scale') {
3127
- // Live-update the constant-size chrome scale WITHOUT rebuilding srcdoc.
3128
- // Rebuilding srcdoc reloads the iframe and flashes the content white.
3129
- editorChromeScaleX = Math.max(0.05, Number(e.data.scaleX) || 1);
3130
- editorChromeScaleY = Math.max(0.05, Number(e.data.scaleY) || editorChromeScaleX);
3131
- applyEditorChromeScale();
3132
- if (selectedEl || hoveredEl) refreshOverlays();
3133
- return;
3134
- }
3135
- if (e.data.type === 'scale-tool-mode') {
3136
- scaleToolEnabled = !!e.data.enabled;
3137
- return;
3138
- }
3139
- if (e.data.type === 'clear-selection') {
3140
- clearRuntimeSelection();
3141
- return;
3142
- }
3143
- if (e.data.type === 'select-element') {
3144
- var candidates = [];
3145
- if (Array.isArray(e.data.selectorCandidates)) {
3146
- e.data.selectorCandidates.forEach(function(selector) {
3147
- if (typeof selector === 'string' && selector && candidates.indexOf(selector) === -1) {
3148
- candidates.push(selector);
3149
- }
3150
- });
3151
- }
3152
- if (e.data.selector && candidates.indexOf(String(e.data.selector)) === -1) {
3153
- candidates.push(String(e.data.selector));
3154
- }
3155
- var target =
3156
- selectedEl &&
3157
- document.documentElement.contains(selectedEl) &&
3158
- matchesExactSelectorList(selectedEl, candidates)
3159
- ? selectedEl
3160
- : null;
3161
- for (var i = 0; i < candidates.length && !target; i += 1) {
3162
- try {
3163
- var matches = document.querySelectorAll(candidates[i]);
3164
- for (var j = 0; j < matches.length; j += 1) {
3165
- if (!isLayerInteractionBlocked(matches[j])) {
3166
- target = matches[j];
3167
- break;
3168
- }
3169
- }
3170
- } catch (_err) {}
3171
- }
3172
- if (!target) return;
3173
- selectedSpacingHovered = false;
3174
- hoveredSpacingHandleKey = '';
3175
- selectedEl = target;
3176
- positionOverlay(selectionOverlay, target);
3177
- if (hoveredEl === selectedEl) highlightOverlay.style.display = 'none';
3178
- return;
3179
- }
3180
- if (e.data.type === 'hover-element') {
3181
- var hoverCandidates = [];
3182
- if (Array.isArray(e.data.selectorCandidates)) {
3183
- e.data.selectorCandidates.forEach(function(selector) {
3184
- if (typeof selector === 'string' && selector && hoverCandidates.indexOf(selector) === -1) {
3185
- hoverCandidates.push(selector);
3186
- }
3187
- });
3188
- }
3189
- if (e.data.selector && hoverCandidates.indexOf(String(e.data.selector)) === -1) {
3190
- hoverCandidates.push(String(e.data.selector));
3191
- }
3192
- if (hoverCandidates.length === 0) {
3193
- hoveredEl = null;
3194
- highlightOverlay.style.display = 'none';
3195
- hideMeasurements();
3196
- return;
3197
- }
3198
- var hoverTarget = findRuntimeTarget(String(e.data.selector || ''), hoverCandidates);
3199
- hoveredEl = hoverTarget;
3200
- if (hoveredEl && !isLayerInteractionBlocked(hoveredEl) && hoveredEl !== selectedEl) {
3201
- positionOverlay(highlightOverlay, hoveredEl);
3202
- } else {
3203
- highlightOverlay.style.display = 'none';
3204
- hideMeasurements();
3205
- }
3206
- return;
3207
- }
3208
- if (e.data.type === 'layer-states') {
3209
- lockedSelectors = Array.isArray(e.data.lockedSelectors) ? e.data.lockedSelectors.filter(function(item) { return typeof item === 'string'; }) : [];
3210
- hiddenSelectors = Array.isArray(e.data.hiddenSelectors) ? e.data.hiddenSelectors.filter(function(item) { return typeof item === 'string'; }) : [];
3211
- if (selectedEl && isLayerInteractionBlocked(selectedEl)) {
3212
- selectedEl = null;
3213
- selectionOverlay.style.display = 'none';
3214
- }
3215
- if (hoveredEl && isLayerInteractionBlocked(hoveredEl)) {
3216
- hoveredEl = null;
3217
- highlightOverlay.style.display = 'none';
3218
- }
3219
- applyHiddenSelectors();
3220
- return;
3221
- }
3222
- if (e.data.type === 'visual-structure-ack') {
3223
- if (!pendingStructureMove || e.data.requestId !== pendingStructureMove.requestId) return;
3224
- var move = pendingStructureMove;
3225
- pendingStructureMove = null;
3226
- if (e.data.applied) {
3227
- if (move.el && move.el.isConnected) {
3228
- applyRuntimeReorder(move.el, move.target);
3229
- selectedEl = move.el;
3230
- positionOverlay(selectionOverlay, selectedEl);
3231
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
3232
- }
3233
- } else {
3234
- // Revert the optimistic reorder to its pre-drag position.
3235
- if (move.el && move.el.isConnected && move.origin && move.origin.prevParent && move.origin.prevParent.isConnected) {
3236
- move.origin.prevParent.insertBefore(move.el, move.origin.prevNextSibling);
3237
- selectedEl = move.el;
3238
- positionOverlay(selectionOverlay, selectedEl);
3239
- window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
3240
- }
3241
- }
3242
- return;
3243
- }
3244
- if (e.data.type === 'replace-document-content') {
3245
- replaceRuntimeDocument(e.data.content, e.data.selectedSelector, e.data.selectorCandidates);
3246
- return;
3247
- }
3248
- if (e.data.type === 'delete-element') {
3249
- removeRuntimeTarget(e.data.selector, e.data.selectorCandidates);
3250
- return;
3251
- }
3252
- if (e.data.type !== 'style-change') return;
3253
- var sel = e.data.selector;
3254
- var prop = e.data.property;
3255
- var val = e.data.value;
3256
- var el = sel ? document.querySelector(sel) : null;
3257
- if (activeTextEditEl && el === activeTextEditEl && applyTextRangeStyle(prop, val)) {
3258
- postTextContentChange(activeTextEditEl, activeTextEditEl.textContent || '', activeTextEditEl.innerHTML || '');
3259
- refreshOverlays();
3260
- return;
3261
- }
3262
- if (el) el.style[prop] = val;
3263
- });
3264
-
3265
- window.addEventListener('scroll', refreshOverlays, true);
3266
- window.addEventListener('resize', refreshOverlays);
3267
- applyEditorChromeScale();
3268
240
  })();
3269
241
  </script>
3270
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
+ `;
3271
255
 
3272
256
  interface DesignCanvasProps {
3273
257
  content: string;
@@ -3590,20 +574,26 @@ export function DesignCanvas({
3590
574
  });
3591
575
 
3592
576
  // Build the srcdoc. The tweak bridge ALWAYS goes in so the panel works
3593
- // outside Edit mode. The editor chrome bridge is omitted only for Interact.
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.
3594
585
  const srcdoc = useMemo(() => {
3595
586
  if (externalPreviewUrl) return undefined;
3596
- const editorChromeBridge =
3597
- interactMode || readOnly
3598
- ? ""
3599
- : createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
3600
- EDITOR_CHROME_BRIDGE_SCRIPT.replace(
3601
- "__READ_ONLY__",
3602
- readOnly ? "true" : "false",
3603
- )
3604
- .replace("__TEXT_EDITING_ENABLED__", editMode ? "true" : "false")
3605
- .replace("__EDITOR_CHROME_SCALE_X__", String(editorChromeScaleX))
3606
- .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));
3607
597
  const embeddedWheelBridge = EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
3608
598
  "__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
3609
599
  isEmbeddedFrame ? "true" : "false",
@@ -3628,13 +618,13 @@ export function DesignCanvas({
3628
618
  // baked chrome scale. Live zoom updates flow through the set-editor-chrome-scale
3629
619
  // postMessage above. Including them here rebuilds srcdoc on every zoom commit,
3630
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.
3631
622
  // eslint-disable-next-line react-hooks/exhaustive-deps
3632
623
  }, [
3633
624
  editMode,
3634
625
  externalPreviewUrl,
3635
626
  interactMode,
3636
627
  isEmbeddedFrame,
3637
- readOnly,
3638
628
  renderedContent,
3639
629
  ]);
3640
630
 
@@ -4073,12 +1063,62 @@ export function DesignCanvas({
4073
1063
  );
4074
1064
  }, [editorChromeScaleX, editorChromeScaleY]);
4075
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
+
4076
1104
  const sendStyleChange = useCallback(
4077
- (selector: string, property: string, value: string) => {
1105
+ (
1106
+ selector: string,
1107
+ property: string,
1108
+ value: string,
1109
+ options?: { selectorCandidates?: string[]; nodeId?: string | null },
1110
+ ) => {
4078
1111
  const iframe = iframeRef.current;
4079
1112
  if (!iframe?.contentWindow) return;
4080
1113
  iframe.contentWindow.postMessage(
4081
- { type: "style-change", selector, property, value },
1114
+ {
1115
+ type: "style-change",
1116
+ selector,
1117
+ property,
1118
+ value,
1119
+ selectorCandidates: options?.selectorCandidates ?? [],
1120
+ nodeId: options?.nodeId ?? "",
1121
+ },
4082
1122
  "*",
4083
1123
  );
4084
1124
  },
@@ -4187,6 +1227,9 @@ export function DesignCanvas({
4187
1227
  (window as any).__designCanvasSendShaderFillPreview = sendShaderFillPreview;
4188
1228
  (window as any).__designCanvasClearShaderFillPreview =
4189
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;
4190
1233
  return () => {
4191
1234
  // Identity-guard each delete so a stale unmounting instance never clobbers
4192
1235
  // a freshly mounted instance's bridge during a remount race.
@@ -4225,6 +1268,9 @@ export function DesignCanvas({
4225
1268
  ) {
4226
1269
  delete (window as any).__designCanvasClearShaderFillPreview;
4227
1270
  }
1271
+ if ((window as any).__designCanvasBeginTextEdit === beginTextEdit) {
1272
+ delete (window as any).__designCanvasBeginTextEdit;
1273
+ }
4228
1274
  };
4229
1275
  }, [
4230
1276
  deleteRuntimeElement,
@@ -4235,6 +1281,7 @@ export function DesignCanvas({
4235
1281
  clearMotionPreview,
4236
1282
  sendShaderFillPreview,
4237
1283
  clearShaderFillPreview,
1284
+ beginTextEdit,
4238
1285
  ]);
4239
1286
 
4240
1287
  // Device dimensions match real-world devices. iframes are replaced elements