@agent-native/core 0.80.8 → 0.80.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +6 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +8 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +10 -2
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +10 -2
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +7 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +7 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +7 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +9 -2
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +6 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +6 -0
  13. package/corpus/core/docs/content/template-design.mdx +9 -2
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/cli/design-connect.ts +28 -2
  16. package/corpus/core/src/cli/mcp.ts +10 -0
  17. package/corpus/core/src/cli/skills.ts +103 -36
  18. package/corpus/core/src/client/error-format.ts +25 -0
  19. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
  20. package/corpus/core/src/client/guided-questions.tsx +32 -4
  21. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
  22. package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
  23. package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
  24. package/corpus/core/src/client/session-replay.ts +34 -0
  25. package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
  26. package/corpus/core/src/coding-tools/index.ts +1 -1
  27. package/corpus/core/src/mcp/index.ts +2 -0
  28. package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
  29. package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
  30. package/corpus/core/src/mcp-client/index.ts +9 -0
  31. package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
  32. package/corpus/core/src/onboarding/types.ts +7 -0
  33. package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
  34. package/corpus/core/src/server/agents-bundle.ts +35 -7
  35. package/corpus/core/src/styles/agent-native.css +5 -0
  36. package/corpus/core/src/templates/default/app/global.css +35 -35
  37. package/corpus/templates/analytics/AGENTS.md +6 -0
  38. package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
  39. package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
  40. package/corpus/templates/analytics/app/global.css +144 -40
  41. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
  45. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
  46. package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  48. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
  49. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
  50. package/corpus/templates/assets/app/global.css +99 -35
  51. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  52. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
  53. package/corpus/templates/assets/app/routes/library.tsx +4 -4
  54. package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
  55. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  56. package/corpus/templates/brain/app/global.css +63 -35
  57. package/corpus/templates/brain/app/routes/search.tsx +1 -1
  58. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  59. package/corpus/templates/brain/app/routes/sources.tsx +2 -2
  60. package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
  61. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
  62. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
  63. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
  64. package/corpus/templates/calendar/app/global.css +69 -35
  65. package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  66. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  67. package/corpus/templates/chat/app/global.css +35 -35
  68. package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  69. package/corpus/templates/clips/AGENTS.md +13 -0
  70. package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
  71. package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
  72. package/corpus/templates/clips/actions/view-screen.ts +2 -2
  73. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  74. package/corpus/templates/clips/app/global.css +47 -35
  75. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
  76. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
  77. package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
  79. package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
  80. package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
  81. package/corpus/templates/clips/desktop/src/app.tsx +290 -1
  82. package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
  83. package/corpus/templates/clips/desktop/src/styles.css +67 -52
  84. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
  87. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
  88. package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
  89. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
  90. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  91. package/corpus/templates/content/app/global.css +29 -15
  92. package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  93. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
  94. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  95. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
  96. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
  97. package/corpus/templates/design/AGENTS.md +20 -12
  98. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
  99. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  100. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  101. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  102. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  103. package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
  104. package/corpus/templates/design/actions/apply-design-state.ts +200 -0
  105. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  106. package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
  107. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  108. package/corpus/templates/design/actions/apply-visual-edit.ts +214 -8
  109. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  110. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  111. package/corpus/templates/design/actions/create-component.ts +447 -0
  112. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  113. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  114. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  115. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  116. package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
  117. package/corpus/templates/design/actions/generate-design.ts +6 -21
  118. package/corpus/templates/design/actions/generate-screens.ts +43 -2
  119. package/corpus/templates/design/actions/get-component-details.ts +242 -0
  120. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  121. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  122. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  123. package/corpus/templates/design/actions/get-design.ts +1 -0
  124. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  125. package/corpus/templates/design/actions/index-components.ts +258 -0
  126. package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
  127. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  128. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  129. package/corpus/templates/design/actions/list-design-states.ts +72 -0
  130. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  131. package/corpus/templates/design/actions/open-component-source.ts +237 -0
  132. package/corpus/templates/design/actions/present-design-variants.ts +355 -30
  133. package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
  134. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  135. package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
  136. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  137. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  138. package/corpus/templates/design/actions/run-design-audit.ts +421 -0
  139. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  140. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  141. package/corpus/templates/design/actions/view-screen.ts +75 -31
  142. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1738 -135
  143. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
  144. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
  145. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  146. package/corpus/templates/design/app/components/design/EditPanel.tsx +1873 -132
  147. package/corpus/templates/design/app/components/design/LayersPanel.tsx +384 -219
  148. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  149. package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
  150. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2103 -295
  151. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
  152. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
  153. package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
  154. package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
  155. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
  156. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  157. package/corpus/templates/design/app/components/design/index.ts +16 -1
  158. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +238 -137
  159. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +188 -14
  160. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +24 -5
  161. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +57 -1
  162. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  163. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  164. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
  165. package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
  166. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +12 -5
  167. package/corpus/templates/design/app/components/design/types.ts +9 -0
  168. package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
  169. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  170. package/corpus/templates/design/app/global.css +77 -49
  171. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  172. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  173. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -2
  174. package/corpus/templates/design/app/i18n/zh-TW.ts +34 -0
  175. package/corpus/templates/design/app/i18n-data.ts +314 -3
  176. package/corpus/templates/design/app/pages/DesignEditor.tsx +4929 -892
  177. package/corpus/templates/design/app/root.tsx +1 -1
  178. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  179. package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
  180. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  181. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
  182. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
  183. package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
  184. package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
  185. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  186. package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
  187. package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
  188. package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
  189. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  190. package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
  191. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  192. package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
  193. package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
  194. package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
  195. package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
  196. package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
  197. package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
  198. package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
  199. package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
  200. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
  201. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
  202. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
  203. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
  204. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
  205. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  206. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  207. package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
  208. package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
  209. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  210. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
  211. package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
  212. package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
  213. package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
  214. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
  215. package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
  216. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  217. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  218. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  219. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  220. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  221. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  222. package/corpus/templates/design/e2e/README.md +3 -3
  223. package/corpus/templates/design/e2e/global-setup.ts +11 -22
  224. package/corpus/templates/design/e2e/helpers.ts +128 -11
  225. package/corpus/templates/design/playwright.config.ts +9 -3
  226. package/corpus/templates/design/server/db/schema.ts +123 -0
  227. package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
  228. package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
  229. package/corpus/templates/design/server/plugins/db.ts +90 -0
  230. package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
  231. package/corpus/templates/design/shared/builder-app.ts +297 -0
  232. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  233. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  234. package/corpus/templates/design/shared/code-layer.ts +1141 -93
  235. package/corpus/templates/design/shared/component-model.ts +239 -0
  236. package/corpus/templates/design/shared/design-review.ts +275 -0
  237. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  238. package/corpus/templates/design/shared/design-state.ts +112 -0
  239. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  240. package/corpus/templates/design/shared/generation-session.ts +41 -0
  241. package/corpus/templates/design/shared/motion-compiler.ts +278 -0
  242. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  243. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  244. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  245. package/corpus/templates/design/shared/source-mode.ts +245 -0
  246. package/corpus/templates/dispatch/app/global.css +79 -35
  247. package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
  248. package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  249. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
  250. package/corpus/templates/forms/app/global.css +64 -35
  251. package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
  252. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
  253. package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
  254. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
  255. package/corpus/templates/macros/app/global.css +25 -9
  256. package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  257. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
  258. package/corpus/templates/mail/app/global.css +42 -36
  259. package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
  260. package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
  261. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
  262. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  263. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
  264. package/corpus/templates/plan/app/global.css +74 -42
  265. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  266. package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
  267. package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
  268. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
  269. package/corpus/templates/slides/app/global.css +49 -33
  270. package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  271. package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
  272. package/corpus/templates/videos/app/global.css +35 -35
  273. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  274. package/dist/cli/design-connect.d.ts.map +1 -1
  275. package/dist/cli/design-connect.js +28 -2
  276. package/dist/cli/design-connect.js.map +1 -1
  277. package/dist/cli/mcp.d.ts.map +1 -1
  278. package/dist/cli/mcp.js +10 -0
  279. package/dist/cli/mcp.js.map +1 -1
  280. package/dist/cli/skills.d.ts.map +1 -1
  281. package/dist/cli/skills.js +95 -33
  282. package/dist/cli/skills.js.map +1 -1
  283. package/dist/client/error-format.d.ts.map +1 -1
  284. package/dist/client/error-format.js +17 -0
  285. package/dist/client/error-format.js.map +1 -1
  286. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  287. package/dist/client/extensions/ExtensionViewer.js +3 -4
  288. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  289. package/dist/client/guided-questions.d.ts +4 -0
  290. package/dist/client/guided-questions.d.ts.map +1 -1
  291. package/dist/client/guided-questions.js +16 -7
  292. package/dist/client/guided-questions.js.map +1 -1
  293. package/dist/client/onboarding/OnboardingPanel.js +26 -1
  294. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  295. package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
  296. package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
  297. package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
  298. package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
  299. package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
  300. package/dist/client/resources/use-builtin-capabilities.js +2 -1
  301. package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
  302. package/dist/client/session-replay.d.ts.map +1 -1
  303. package/dist/client/session-replay.js +31 -0
  304. package/dist/client/session-replay.js.map +1 -1
  305. package/dist/client/sharing/ShareButton.d.ts +14 -0
  306. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  307. package/dist/client/sharing/ShareButton.js +36 -7
  308. package/dist/client/sharing/ShareButton.js.map +1 -1
  309. package/dist/coding-tools/index.js +1 -1
  310. package/dist/coding-tools/index.js.map +1 -1
  311. package/dist/mcp/index.d.ts +2 -0
  312. package/dist/mcp/index.d.ts.map +1 -1
  313. package/dist/mcp/index.js +1 -0
  314. package/dist/mcp/index.js.map +1 -1
  315. package/dist/mcp/screen-memory-stdio.d.ts +6 -0
  316. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
  317. package/dist/mcp/screen-memory-stdio.js +213 -0
  318. package/dist/mcp/screen-memory-stdio.js.map +1 -0
  319. package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
  320. package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
  321. package/dist/mcp-client/builtin-capabilities.js +10 -0
  322. package/dist/mcp-client/builtin-capabilities.js.map +1 -1
  323. package/dist/mcp-client/index.d.ts +1 -0
  324. package/dist/mcp-client/index.d.ts.map +1 -1
  325. package/dist/mcp-client/index.js +1 -0
  326. package/dist/mcp-client/index.js.map +1 -1
  327. package/dist/mcp-client/screen-memory-local.d.ts +61 -0
  328. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
  329. package/dist/mcp-client/screen-memory-local.js +336 -0
  330. package/dist/mcp-client/screen-memory-local.js.map +1 -0
  331. package/dist/observability/routes.d.ts +3 -3
  332. package/dist/onboarding/types.d.ts +7 -0
  333. package/dist/onboarding/types.d.ts.map +1 -1
  334. package/dist/onboarding/types.js.map +1 -1
  335. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  336. package/dist/server/agent-chat-plugin.js +26 -0
  337. package/dist/server/agent-chat-plugin.js.map +1 -1
  338. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  339. package/dist/server/agents-bundle.d.ts.map +1 -1
  340. package/dist/server/agents-bundle.js +22 -7
  341. package/dist/server/agents-bundle.js.map +1 -1
  342. package/dist/styles/agent-native.css +5 -0
  343. package/dist/templates/default/app/global.css +35 -35
  344. package/docs/content/locales/ar-SA/template-design.mdx +6 -0
  345. package/docs/content/locales/de-DE/template-design.mdx +8 -0
  346. package/docs/content/locales/es-ES/template-design.mdx +10 -2
  347. package/docs/content/locales/fr-FR/template-design.mdx +10 -2
  348. package/docs/content/locales/hi-IN/template-design.mdx +7 -0
  349. package/docs/content/locales/ja-JP/template-design.mdx +7 -0
  350. package/docs/content/locales/ko-KR/template-design.mdx +7 -0
  351. package/docs/content/locales/pt-BR/template-design.mdx +9 -2
  352. package/docs/content/locales/zh-CN/template-design.mdx +6 -0
  353. package/docs/content/locales/zh-TW/template-design.mdx +6 -0
  354. package/docs/content/template-design.mdx +9 -2
  355. package/package.json +1 -1
  356. package/src/templates/default/app/global.css +35 -35
  357. package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
  358. package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
  359. package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -342
@@ -15,6 +15,249 @@ import { isTrustedCanvasBridgeMessage } from "./bridge-security";
15
15
  import { DeviceFrame } from "./DeviceFrame";
16
16
  import type { ElementInfo, DeviceFrameType } from "./types";
17
17
 
18
+ /**
19
+ * Allowlist check for Fusion (Builder-hosted) frame origins.
20
+ *
21
+ * Fusion frames are served cross-origin from the Builder-hosted app, so the
22
+ * strict `origin === parentOrigin` bridge check can never match. Before relaxing
23
+ * trust to window-identity only, we must confirm the message origin is actually
24
+ * a Builder host — the exact origin of the `fusionUrl` we were asked to render,
25
+ * or any `*.builder.io` host (plus the bare `builder.io`), over https. This
26
+ * prevents the relaxed-trust path from accepting messages from an arbitrary
27
+ * cross-origin frame that merely shares our iframe's window reference.
28
+ */
29
+ function isAllowedFusionOrigin(
30
+ origin: string,
31
+ fusionUrl: string | undefined,
32
+ ): boolean {
33
+ if (!origin || origin === "null") return false;
34
+ let host: string;
35
+ let protocol: string;
36
+ try {
37
+ const parsed = new URL(origin);
38
+ host = parsed.hostname.toLowerCase();
39
+ protocol = parsed.protocol;
40
+ } catch {
41
+ return false;
42
+ }
43
+ // Only allow secure (https) Builder origins.
44
+ if (protocol !== "https:") return false;
45
+ // Exact match against the configured fusion URL's origin.
46
+ if (fusionUrl) {
47
+ try {
48
+ if (new URL(fusionUrl).origin === origin) return true;
49
+ } catch {
50
+ // Malformed fusionUrl — fall through to the host-family allowlist.
51
+ }
52
+ }
53
+ // Builder host family: builder.io and any subdomain of it.
54
+ return host === "builder.io" || host.endsWith(".builder.io");
55
+ }
56
+
57
+ /**
58
+ * Wire shape for a single motion track sent via the `motion-load-tracks`
59
+ * postMessage. Matches the serialisable subset of `MotionTrack` from
60
+ * `shared/motion-timeline.ts` without requiring an import at the UI layer.
61
+ */
62
+ export interface MotionTrackWire {
63
+ targetNodeId: string;
64
+ property: string;
65
+ keyframes: Array<{ t: number; value: string; ease?: string }>;
66
+ }
67
+
68
+ /**
69
+ * Motion-preview bridge. Injected alongside the other bridge scripts so the
70
+ * MotionDock's scrubbing preview works in ALL editor modes without writing
71
+ * anything to the DB, Yjs state, or source files.
72
+ *
73
+ * Protocol (parent → iframe):
74
+ *
75
+ * { type: 'motion-load-tracks', tracks: MotionTrackWire[] }
76
+ * Load (or replace) the track list for this document. Each entry:
77
+ * { targetNodeId, property, keyframes: [{ t, value, ease? }] }
78
+ * where t ∈ [0, 1]. Sent whenever the active timeline changes.
79
+ *
80
+ * { type: 'motion-preview', t, durationMs }
81
+ * Seek all loaded tracks to normalised position t ∈ [0, 1] and apply
82
+ * the interpolated CSS property values as inline styles on the matching
83
+ * [data-agent-native-node-id="…"] elements. Never writes to storage.
84
+ *
85
+ * { type: 'motion-preview-clear' }
86
+ * Remove all motion-preview inline-style overrides and the in-memory
87
+ * track list. Called when the dock is closed or the timeline is
88
+ * discarded.
89
+ *
90
+ * Easing is deliberately simple: linear interpolation between keyframe
91
+ * values (CSS handles easing when the compiled CSS is actually applied;
92
+ * preview is a live visualisation, not a perfect recreation of the final
93
+ * animation).
94
+ */
95
+ const MOTION_PREVIEW_BRIDGE_SCRIPT = `
96
+ <script data-agent-native-motion-preview-bridge>
97
+ (function() {
98
+ // Track list loaded by 'motion-load-tracks'.
99
+ var loadedTracks = [];
100
+ // Map of nodeId -> [property, ...] we have touched, for cleanup.
101
+ var touchedProps = {};
102
+
103
+ function lerp(a, b, ratio) {
104
+ var numA = parseFloat(a);
105
+ var numB = parseFloat(b);
106
+ if (Number.isFinite(numA) && Number.isFinite(numB)) {
107
+ var suffix = a.replace(/^-?[\\d.]+/, '') || b.replace(/^-?[\\d.]+/, '');
108
+ return (numA + (numB - numA) * ratio).toFixed(4).replace(/\\.?0+$/, '') + suffix;
109
+ }
110
+ // Non-numeric: snap to b after the midpoint.
111
+ return ratio < 0.5 ? a : b;
112
+ }
113
+
114
+ function interpolate(keyframes, t) {
115
+ if (!keyframes || keyframes.length === 0) return '';
116
+ if (keyframes.length === 1) return keyframes[0].value;
117
+ // Find surrounding keyframes.
118
+ var prev = keyframes[0];
119
+ var next = keyframes[keyframes.length - 1];
120
+ for (var i = 0; i < keyframes.length - 1; i++) {
121
+ if (t >= keyframes[i].t && t <= keyframes[i + 1].t) {
122
+ prev = keyframes[i];
123
+ next = keyframes[i + 1];
124
+ break;
125
+ }
126
+ }
127
+ var span = next.t - prev.t;
128
+ if (span <= 0) return prev.value;
129
+ var ratio = Math.max(0, Math.min(1, (t - prev.t) / span));
130
+ return lerp(prev.value, next.value, ratio);
131
+ }
132
+
133
+ function applyPreview(t) {
134
+ for (var i = 0; i < loadedTracks.length; i++) {
135
+ var track = loadedTracks[i];
136
+ var el = document.querySelector('[data-agent-native-node-id="' + track.targetNodeId + '"]');
137
+ if (!el) continue;
138
+ var value = interpolate(track.keyframes, t);
139
+ if (value === '') continue;
140
+ el.style[track.property] = value;
141
+ if (!touchedProps[track.targetNodeId]) touchedProps[track.targetNodeId] = [];
142
+ if (touchedProps[track.targetNodeId].indexOf(track.property) === -1) {
143
+ touchedProps[track.targetNodeId].push(track.property);
144
+ }
145
+ }
146
+ }
147
+
148
+ function clearPreview() {
149
+ var nodeIds = Object.keys(touchedProps);
150
+ for (var i = 0; i < nodeIds.length; i++) {
151
+ var el = document.querySelector('[data-agent-native-node-id="' + nodeIds[i] + '"]');
152
+ if (!el) continue;
153
+ var props = touchedProps[nodeIds[i]];
154
+ for (var j = 0; j < props.length; j++) {
155
+ el.style[props[j]] = '';
156
+ }
157
+ }
158
+ touchedProps = {};
159
+ loadedTracks = [];
160
+ }
161
+
162
+ window.addEventListener('message', function(e) {
163
+ if (e.source !== window.parent) return;
164
+ if (!e.data || typeof e.data.type !== 'string') return;
165
+ if (e.data.type === 'motion-load-tracks') {
166
+ loadedTracks = Array.isArray(e.data.tracks) ? e.data.tracks : [];
167
+ touchedProps = {};
168
+ return;
169
+ }
170
+ if (e.data.type === 'motion-preview') {
171
+ var t = Number(e.data.t);
172
+ if (!Number.isFinite(t)) return;
173
+ t = Math.max(0, Math.min(1, t));
174
+ applyPreview(t);
175
+ return;
176
+ }
177
+ if (e.data.type === 'motion-preview-clear') {
178
+ clearPreview();
179
+ return;
180
+ }
181
+ });
182
+ })();
183
+ </script>
184
+ `;
185
+
186
+ /**
187
+ * Shader-fill preview bridge. ALWAYS injected alongside the other bridge
188
+ * scripts so the parent can apply a CSS gradient approximation of a shader
189
+ * fill to the currently-selected element **without** persisting anything.
190
+ *
191
+ * Protocol (parent → iframe):
192
+ *
193
+ * { type: 'shader-fill-preview', selector, nodeId, css }
194
+ * Apply `css` as the `background` inline style on the first element that
195
+ * matches `selector` (preferred) or `[data-agent-native-node-id="nodeId"]`.
196
+ * When both are absent, targets `document.body`. Stores the previous
197
+ * background value so it can be restored on clear.
198
+ * Preview-only — never writes to DB, Yjs, or source files.
199
+ *
200
+ * { type: 'shader-fill-preview-clear' }
201
+ * Remove the applied background override and restore the previous value.
202
+ * Called when the user discards the preview or switches selections.
203
+ */
204
+ const SHADER_FILL_PREVIEW_BRIDGE_SCRIPT = `
205
+ <script data-agent-native-shader-fill-preview-bridge>
206
+ (function() {
207
+ // Track the element we patched and its original background so we can undo.
208
+ var patchedEl = null;
209
+ var originalBackground = '';
210
+
211
+ function resolveTarget(selector, nodeId) {
212
+ if (selector) {
213
+ try {
214
+ var hit = document.querySelector(selector);
215
+ if (hit) return hit;
216
+ } catch (_err) {}
217
+ }
218
+ if (nodeId) {
219
+ var byId = document.querySelector('[data-agent-native-node-id="' + nodeId.replace(/"/g, '\\\\"') + '"]');
220
+ if (byId) return byId;
221
+ }
222
+ return document.body;
223
+ }
224
+
225
+ function applyPreview(selector, nodeId, css) {
226
+ // Clear any prior patch first so we don't stack patches.
227
+ clearPreview();
228
+ var el = resolveTarget(selector, nodeId);
229
+ if (!el) return;
230
+ originalBackground = el.style.background || '';
231
+ el.style.background = css || '';
232
+ patchedEl = el;
233
+ }
234
+
235
+ function clearPreview() {
236
+ if (!patchedEl) return;
237
+ patchedEl.style.background = originalBackground;
238
+ patchedEl = null;
239
+ originalBackground = '';
240
+ }
241
+
242
+ window.addEventListener('message', function(e) {
243
+ if (e.source !== window.parent) return;
244
+ if (!e.data || typeof e.data.type !== 'string') return;
245
+ if (e.data.type === 'shader-fill-preview') {
246
+ var selector = typeof e.data.selector === 'string' ? e.data.selector : '';
247
+ var nodeId = typeof e.data.nodeId === 'string' ? e.data.nodeId : '';
248
+ var css = typeof e.data.css === 'string' ? e.data.css : '';
249
+ applyPreview(selector, nodeId, css);
250
+ return;
251
+ }
252
+ if (e.data.type === 'shader-fill-preview-clear') {
253
+ clearPreview();
254
+ return;
255
+ }
256
+ });
257
+ })();
258
+ </script>
259
+ `;
260
+
18
261
  /**
19
262
  * Tweak-bridge script. ALWAYS injected so the parent's postMessage
20
263
  * (`tweak-values`) can update CSS custom properties on the iframe's :root
@@ -247,8 +490,44 @@ function createEditorBridgeThemeScript(vars: Record<string, string>) {
247
490
  const EDITOR_CHROME_BRIDGE_SCRIPT = `
248
491
  <script data-agent-native-editor-chrome-bridge>
249
492
  (function() {
250
- var textEditingEnabled = __TEXT_EDITING_ENABLED__;
493
+ var readOnly = __READ_ONLY__;
494
+ var textEditingEnabled = !readOnly && __TEXT_EDITING_ENABLED__;
251
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
+ }
252
531
 
253
532
  function escapeIdent(value) {
254
533
  if (window.CSS && typeof window.CSS.escape === 'function') {
@@ -321,6 +600,32 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
321
600
  );
322
601
  }
323
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
+
324
629
  function freshRuntimeNodeId(prefix) {
325
630
  var random = '';
326
631
  try {
@@ -356,7 +661,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
356
661
  if (stableOwnSelector) return stableOwnSelector;
357
662
 
358
663
  if (el.id) return '#' + escapeIdent(el.id);
359
- var stableAncestor = el.closest('[data-agent-native-node-id],[data-code-layer-id],[data-layer-id],[data-builder-id],[data-loc]');
664
+ var stableAncestor = closestStableSourceElement(el);
360
665
  if (stableAncestor && stableAncestor !== el) {
361
666
  var stableAncestorSelector = selectorPart(stableAncestor);
362
667
  if (stableAncestorSelector) {
@@ -379,7 +684,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
379
684
  ? window.getComputedStyle(el.parentElement)
380
685
  : null;
381
686
  var parentDisplay = parentStyles ? parentStyles.display : undefined;
382
- var sourceId = getSourceId(el) || getSelector(el);
687
+ var sourceBacked = hasStableOwnSource(el) || !!closestStableSourceElement(el);
688
+ var sourceId = sourceBacked ? (getSourceId(el) || getSelector(el)) : '';
383
689
  var parentLayout = parentStyles
384
690
  ? {
385
691
  display: parentStyles.display,
@@ -392,15 +698,24 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
392
698
  position: parentStyles.position,
393
699
  }
394
700
  : undefined;
395
- var capabilities = [
396
- {
397
- kind: 'deterministic-style-edit',
398
- label: 'deterministic-style-edit',
399
- confidence: 0.92,
400
- reason: 'Inline style can be patched and replayed through HMR/collab.',
401
- },
402
- ];
403
- if (el.classList && el.classList.length > 0) {
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) {
404
719
  capabilities.push({
405
720
  kind: 'deterministic-class-edit',
406
721
  label: 'deterministic-class-edit',
@@ -408,7 +723,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
408
723
  reason: 'Class tokens are visible on the selected element.',
409
724
  });
410
725
  }
411
- if (parentDisplay === 'flex' || parentDisplay === 'inline-flex' || parentDisplay === 'grid' || parentDisplay === 'inline-grid') {
726
+ if (sourceBacked && (parentDisplay === 'flex' || parentDisplay === 'inline-flex' || parentDisplay === 'grid' || parentDisplay === 'inline-grid')) {
412
727
  capabilities.push({
413
728
  kind: 'agent-structural-edit',
414
729
  label: 'agent-structural-edit',
@@ -416,6 +731,38 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
416
731
  reason: 'Parent layout context decides whether movement means gap, order, alignment, or wrapper structure.',
417
732
  });
418
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
+ }
419
766
  return {
420
767
  tagName: el.tagName.toLowerCase(),
421
768
  id: el.id || undefined,
@@ -433,9 +780,9 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
433
780
  lineHeight: cs.lineHeight,
434
781
  letterSpacing: cs.letterSpacing,
435
782
  textAlign: cs.textAlign,
436
- display: cs.display,
437
- overflow: cs.overflow,
438
- flexDirection: cs.flexDirection,
783
+ display: cs.display,
784
+ overflow: cs.overflow,
785
+ flexDirection: cs.flexDirection,
439
786
  justifyContent: cs.justifyContent,
440
787
  alignItems: cs.alignItems,
441
788
  alignSelf: cs.alignSelf,
@@ -491,6 +838,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
491
838
  confidence: capabilities.reduce(function(best, item) {
492
839
  return Math.max(best, item.confidence || 0);
493
840
  }, 0),
841
+ provenance: provenance,
494
842
  };
495
843
  }
496
844
 
@@ -501,12 +849,12 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
501
849
 
502
850
  var highlightOverlay = document.createElement('div');
503
851
  highlightOverlay.setAttribute('data-agent-native-edit-overlay', 'highlight');
504
- highlightOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:99999;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;';
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;';
505
853
  document.body.appendChild(highlightOverlay);
506
854
 
507
855
  var selectionOverlay = document.createElement('div');
508
856
  selectionOverlay.setAttribute('data-agent-native-edit-overlay', 'selection');
509
- selectionOverlay.style.cssText = 'position:fixed;pointer-events:auto;z-index:99998;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;cursor:move;';
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;';
510
858
  ['n','e','s','w'].forEach(function(pos) {
511
859
  var edge = document.createElement('span');
512
860
  edge.setAttribute('data-agent-native-edge-handle', pos);
@@ -542,7 +890,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
542
890
  var handle = document.createElement('span');
543
891
  handle.setAttribute('data-agent-native-edit-handle', pos);
544
892
  var cursor = pos === 'n' || pos === 's' ? 'ns-resize' : pos === 'e' || pos === 'w' ? 'ew-resize' : pos === 'nw' || pos === 'se' ? 'nwse-resize' : 'nesw-resize';
545
- handle.style.cssText = 'position:absolute;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 + ';';
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 + ';';
546
894
  if (pos.indexOf('n') !== -1) handle.style.top = '-4px';
547
895
  if (pos.indexOf('s') !== -1) handle.style.bottom = '-4px';
548
896
  if (pos.indexOf('w') !== -1) handle.style.left = '-4px';
@@ -567,10 +915,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
567
915
  if (pos.indexOf('e') !== -1) rotate.style.right = '-26px';
568
916
  selectionOverlay.appendChild(rotate);
569
917
  });
570
- var paddingOverlay = document.createElement('div');
571
- paddingOverlay.setAttribute('data-agent-native-padding-overlay', '');
572
- paddingOverlay.style.cssText = 'position:absolute;border:1px dashed var(--design-editor-accent-strong-color);border-radius:2px;pointer-events:none;';
573
- selectionOverlay.appendChild(paddingOverlay);
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);
574
922
  document.body.appendChild(selectionOverlay);
575
923
 
576
924
  var transformBadge = document.createElement('div');
@@ -578,6 +926,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
578
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);';
579
927
  document.body.appendChild(transformBadge);
580
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
+
581
934
  var insertionGuide = document.createElement('div');
582
935
  insertionGuide.setAttribute('data-agent-native-insertion-guide', '');
583
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);';
@@ -588,14 +941,118 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
588
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;';
589
942
  document.body.appendChild(measurementOverlay);
590
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
+
591
1027
  var selectedEl = null;
592
1028
  var hoveredEl = null;
593
1029
  var activeTextEditEl = null;
594
1030
  var textEditPointerState = null;
595
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;
596
1040
  var lockedSelectors = [];
597
1041
  var hiddenSelectors = [];
598
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
+
599
1056
  function matchesSelectorList(el, selectors) {
600
1057
  if (!el || !selectors || selectors.length === 0) return false;
601
1058
  for (var i = 0; i < selectors.length; i += 1) {
@@ -752,7 +1209,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
752
1209
  for (var i = 0; i < activeCandidates.length && !selectedEl; i += 1) {
753
1210
  try {
754
1211
  var match = document.querySelector(activeCandidates[i]);
755
- if (match && !isLayerInteractionBlocked(match)) selectedEl = match;
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
+ }
756
1220
  } catch (_err) {}
757
1221
  }
758
1222
  if (selectedEl) {
@@ -766,50 +1230,441 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
766
1230
  refreshOverlays();
767
1231
  }
768
1232
 
769
- function updatePaddingOverlay(el) {
770
- if (!el) {
771
- paddingOverlay.style.display = 'none';
772
- return;
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');
773
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 [];
774
1494
  var cs = window.getComputedStyle(el);
775
- var top = readPx(cs.paddingTop) + readPx(cs.borderTopWidth);
776
- var right = readPx(cs.paddingRight) + readPx(cs.borderRightWidth);
777
- var bottom = readPx(cs.paddingBottom) + readPx(cs.borderBottomWidth);
778
- var left = readPx(cs.paddingLeft) + readPx(cs.borderLeftWidth);
779
- var hasPadding = top > 0 || right > 0 || bottom > 0 || left > 0;
780
- paddingOverlay.style.display = hasPadding ? 'block' : 'none';
781
- paddingOverlay.style.top = top + 'px';
782
- paddingOverlay.style.right = right + 'px';
783
- paddingOverlay.style.bottom = bottom + 'px';
784
- paddingOverlay.style.left = left + 'px';
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
+ });
785
1635
  }
786
1636
 
787
1637
  function positionOverlay(overlay, el) {
788
1638
  if (!el || !document.documentElement.contains(el)) {
789
1639
  overlay.style.display = 'none';
1640
+ if (overlay === selectionOverlay) clearComponentTag();
790
1641
  return;
791
1642
  }
792
1643
  // For the selection overlay, prefer the CSS box + rotation transform so
793
1644
  // the handles hug the rotated element rather than its inflated AABB.
794
1645
  if (overlay === selectionOverlay) {
795
1646
  var elCs = window.getComputedStyle(el);
796
- var elLeft = readPx(el.style.left || elCs.left);
797
- var elTop = readPx(el.style.top || elCs.top);
798
- var elW = readPx(el.style.width || elCs.width);
799
- var elH = readPx(el.style.height || elCs.height);
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);
800
1651
  var elRot = currentRotation(el);
1652
+ var canUseLocalBox = Math.abs(elRot) > 0.01 && elLeft !== null && elTop !== null && elW !== null && elH !== null;
801
1653
  // Convert element-local left/top to viewport coords by walking to the
802
1654
  // nearest positioned ancestor (same reference frame as getBoundingClientRect).
803
- var parentRect = (el.offsetParent || document.documentElement).getBoundingClientRect();
804
- overlay.style.display = 'block';
805
- overlay.style.left = (parentRect.left + elLeft) + 'px';
806
- overlay.style.top = (parentRect.top + elTop) + 'px';
807
- overlay.style.width = elW + 'px';
808
- overlay.style.height = elH + 'px';
809
- overlay.style.transform = elRot ? 'rotate(' + elRot + 'deg)' : '';
810
- overlay.style.transformOrigin = '0 0';
811
- updatePaddingOverlay(el);
812
- return;
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
+ }
813
1668
  }
814
1669
  var rect = el.getBoundingClientRect();
815
1670
  overlay.style.display = 'block';
@@ -817,10 +1672,19 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
817
1672
  overlay.style.left = rect.left + 'px';
818
1673
  overlay.style.width = rect.width + 'px';
819
1674
  overlay.style.height = rect.height + 'px';
1675
+ overlay.style.transform = '';
1676
+ if (overlay === selectionOverlay) {
1677
+ updateSpacingOverlay(el);
1678
+ updateComponentTag(el);
1679
+ }
820
1680
  }
821
1681
 
822
1682
  function refreshOverlays() {
823
- if (hoveredEl) positionOverlay(highlightOverlay, hoveredEl);
1683
+ if (hoveredEl && hoveredEl !== selectedEl) {
1684
+ positionOverlay(highlightOverlay, hoveredEl);
1685
+ } else {
1686
+ highlightOverlay.style.display = 'none';
1687
+ }
824
1688
  if (selectedEl) positionOverlay(selectionOverlay, selectedEl);
825
1689
  }
826
1690
 
@@ -891,6 +1755,13 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
891
1755
  );
892
1756
  }
893
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
+
894
1765
  function elementFromEditorPoint(clientX, clientY) {
895
1766
  var shieldPointerEvents = shieldOverlay.style.pointerEvents;
896
1767
  var selectionPointerEvents = selectionOverlay.style.pointerEvents;
@@ -1055,6 +1926,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1055
1926
  function selectElementAtEvent(e) {
1056
1927
  stopNativeInteraction(e);
1057
1928
  blurActiveTextEditor();
1929
+ if (suppressNextShieldClick) {
1930
+ suppressNextShieldClick = false;
1931
+ if (suppressNextShieldClickTimer !== null) {
1932
+ clearTimeout(suppressNextShieldClickTimer);
1933
+ suppressNextShieldClickTimer = null;
1934
+ }
1935
+ return;
1936
+ }
1058
1937
  // Suppress the first click in a double-click sequence — the dblclick
1059
1938
  // handler (beginTextEditingFromEvent) will fire immediately after and a
1060
1939
  // spurious element-select would cause inspector flicker.
@@ -1062,26 +1941,40 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1062
1941
  var target = elementFromEditorPoint(e.clientX, e.clientY);
1063
1942
  if (!target || target === document.body || target === document.documentElement) {
1064
1943
  // Click on empty canvas: clear the current selection (matches Figma).
1065
- selectedEl = null;
1066
- selectionOverlay.style.display = 'none';
1944
+ clearRuntimeSelection();
1067
1945
  window.parent.postMessage({ type: 'clear-selection' }, '*');
1068
1946
  return;
1069
1947
  }
1070
- selectedEl = target;
1948
+ selectedSpacingHovered = false;
1949
+ hoveredSpacingHandleKey = '';
1950
+ selectedEl = selectionTargetForHit(target);
1071
1951
  var info = getElementInfo(selectedEl);
1072
1952
  positionOverlay(selectionOverlay, selectedEl);
1073
1953
  window.parent.postMessage({ type: 'element-select', payload: info }, '*');
1074
1954
  }
1075
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
+
1076
1967
  function openContextMenuAtEvent(e) {
1077
1968
  stopNativeInteraction(e);
1078
1969
  blurActiveTextEditor();
1079
1970
  var target = elementFromEditorPoint(e.clientX, e.clientY);
1080
1971
  var info = null;
1081
1972
  if (target) {
1082
- selectedEl = target;
1083
- info = getElementInfo(target);
1084
- positionOverlay(selectionOverlay, target);
1973
+ selectedSpacingHovered = false;
1974
+ hoveredSpacingHandleKey = '';
1975
+ selectedEl = selectionTargetForHit(target);
1976
+ info = getElementInfo(selectedEl);
1977
+ positionOverlay(selectionOverlay, selectedEl);
1085
1978
  window.parent.postMessage({ type: 'element-select', payload: info }, '*');
1086
1979
  }
1087
1980
  window.parent.postMessage({
@@ -1138,6 +2031,12 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1138
2031
  return Number.isFinite(num) ? num : 0;
1139
2032
  }
1140
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
+
1141
2040
  function currentRotation(el) {
1142
2041
  var transform = el.style.transform || window.getComputedStyle(el).transform || '';
1143
2042
  var match = transform.match(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/);
@@ -1178,6 +2077,81 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1178
2077
  }, '*');
1179
2078
  }
1180
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
+
1181
2155
  function postTextContentChange(el, value, html) {
1182
2156
  window.parent.postMessage({
1183
2157
  type: 'text-content-change',
@@ -1263,7 +2237,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1263
2237
  }
1264
2238
 
1265
2239
  function isOverlayElement(el) {
1266
- return Boolean(el && el.getAttribute && el.getAttribute('data-agent-native-edit-overlay'));
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
+ );
1267
2248
  }
1268
2249
 
1269
2250
  function draggableElementChildren(parent) {
@@ -1277,9 +2258,40 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1277
2258
  if (el === document.body || el === document.documentElement) return false;
1278
2259
  var cs = window.getComputedStyle(el);
1279
2260
  if (cs.position === 'absolute' || cs.position === 'fixed') return false;
1280
- var parent = el.parentElement;
1281
- if (parent === document.body && draggableElementChildren(parent).length <= 2) return false;
1282
- return draggableElementChildren(parent).filter(function(child) { return child !== el; }).length > 0;
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;
1283
2295
  }
1284
2296
 
1285
2297
  function parentFlowAxis(parent) {
@@ -1304,18 +2316,19 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1304
2316
  var hit = elementFromEditorPoint(clientX, clientY);
1305
2317
  if (
1306
2318
  hit &&
1307
- hit !== document.body &&
1308
2319
  hit !== document.documentElement &&
1309
2320
  hit !== el &&
1310
2321
  !el.contains(hit) &&
1311
- !hit.contains(el) &&
1312
2322
  !isOverlayElement(hit)
1313
2323
  ) {
1314
- var hitChildren = draggableElementChildren(hit).filter(function(child) {
1315
- return child !== el;
1316
- });
1317
- if (hitChildren.length === 0) {
1318
- return { anchor: hit, placement: 'inside', axis: parentFlowAxis(hit) };
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 };
1319
2332
  }
1320
2333
  var hitParent = hit.parentElement;
1321
2334
  if (hitParent) {
@@ -1362,6 +2375,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1362
2375
  insertionGuide.style.display = 'block';
1363
2376
  insertionGuide.style.background = 'var(--design-editor-accent-color)';
1364
2377
  insertionGuide.style.border = '0';
2378
+ insertionGuide.style.borderRadius = '999px';
1365
2379
  insertionGuide.style.boxShadow = '0 0 0 1px var(--design-editor-accent-color)';
1366
2380
  if (target.placement === 'inside') {
1367
2381
  insertionGuide.style.left = rect.left + 'px';
@@ -1370,6 +2384,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1370
2384
  insertionGuide.style.height = rect.height + 'px';
1371
2385
  insertionGuide.style.background = 'color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)';
1372
2386
  insertionGuide.style.border = '2px solid var(--design-editor-accent-color)';
2387
+ insertionGuide.style.borderRadius = '4px';
1373
2388
  insertionGuide.style.boxShadow = 'none';
1374
2389
  return;
1375
2390
  }
@@ -1437,6 +2452,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1437
2452
  if (isLayerInteractionBlocked(selectedEl)) return;
1438
2453
  e.preventDefault();
1439
2454
  e.stopPropagation();
2455
+ if (e.stopImmediatePropagation) e.stopImmediatePropagation();
2456
+ var events = dragEventNames(e);
1440
2457
  var originalSelectedEl = selectedEl;
1441
2458
  var duplicatedForDrag = false;
1442
2459
  if (e.altKey && selectedEl !== document.body && selectedEl !== document.documentElement) {
@@ -1454,16 +2471,96 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1454
2471
  var reorderEl = selectedEl;
1455
2472
  var currentTarget = reorderTargetForPoint(reorderEl, e.clientX, e.clientY);
1456
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);
1457
2480
  function onReorderMove(ev) {
1458
- currentTarget = reorderTargetForPoint(reorderEl, ev.clientX, ev.clientY);
1459
- showInsertionGuideFor(currentTarget);
1460
- showTransformBadge(currentTarget ? 'Move layer' : 'Move', ev.clientX, ev.clientY);
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
+ }
1461
2510
  }
1462
- function onReorderUp() {
1463
- document.removeEventListener('mousemove', onReorderMove, true);
1464
- document.removeEventListener('mouseup', onReorderUp, true);
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);
1465
2538
  hideTransformBadge();
1466
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;
1467
2564
  if (!currentTarget) {
1468
2565
  // No valid drop target — clean up the clone if one was inserted so
1469
2566
  // no ghost element is left in the DOM.
@@ -1489,9 +2586,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1489
2586
  applyRuntimeReorder(reorderEl, currentTarget);
1490
2587
  postVisualStructureChange(reorderEl, currentTarget, { prevParent: prevParent, prevNextSibling: prevNextSibling });
1491
2588
  }
1492
- }
1493
- document.addEventListener('mousemove', onReorderMove, true);
1494
- document.addEventListener('mouseup', onReorderUp, true);
2589
+ }
2590
+ document.addEventListener(events.move, onReorderMove, true);
2591
+ document.addEventListener(events.up, onReorderUp, true);
2592
+ document.addEventListener('keydown', onReorderKeyDown, true);
1495
2593
  return;
1496
2594
  }
1497
2595
  ensurePositionable(selectedEl);
@@ -1518,8 +2616,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1518
2616
  refreshOverlays();
1519
2617
  }
1520
2618
  function onUp() {
1521
- document.removeEventListener('mousemove', onMove, true);
1522
- document.removeEventListener('mouseup', onUp, true);
2619
+ document.removeEventListener(events.move, onMove, true);
2620
+ document.removeEventListener(events.up, onUp, true);
1523
2621
  hideTransformBadge();
1524
2622
  if (!dragEl) return;
1525
2623
  if (duplicatedForDrag && !moved) {
@@ -1545,8 +2643,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1545
2643
  }, '*');
1546
2644
  }
1547
2645
  }
1548
- document.addEventListener('mousemove', onMove, true);
1549
- document.addEventListener('mouseup', onUp, true);
2646
+ document.addEventListener(events.move, onMove, true);
2647
+ document.addEventListener(events.up, onUp, true);
1550
2648
  }
1551
2649
 
1552
2650
  function startResize(handle, e) {
@@ -1602,9 +2700,16 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1602
2700
  if (handle.indexOf('s') !== -1) height = origin.height + dy;
1603
2701
  // Apply Shift / scaleToolEnabled aspect-ratio lock BEFORE the min-size
1604
2702
  // clamp so the ratio is computed from unclamped values (bug fix).
1605
- if (ev.shiftKey && handle.length === 2) {
1606
- if (Math.abs(dx) > Math.abs(dy)) height = width / origin.ratio;
1607
- else width = height * origin.ratio;
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
+ }
1608
2713
  }
1609
2714
  if (scaleToolEnabled) {
1610
2715
  // Scale tool: enforce aspect ratio on all 8 handles, not just corners.
@@ -1617,9 +2722,22 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1617
2722
  else width = height * origin.ratio;
1618
2723
  }
1619
2724
  }
1620
- // Clamp after ratio correction.
1621
- width = Math.max(8, width);
1622
- height = Math.max(8, height);
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;
1623
2741
  // Re-anchor the pinned edge for w/n handles after aspect-ratio lock and
1624
2742
  // clamping so the opposite (e/s) edge stays fixed regardless of whether
1625
2743
  // the dimension change was driven by raw dx/dy or by the ratio lock.
@@ -1703,7 +2821,68 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1703
2821
  document.addEventListener('mouseup', onUp, true);
1704
2822
  }
1705
2823
 
2824
+ function clearPendingShieldDrag() {
2825
+ if (!pendingShieldDrag) return;
2826
+ document.removeEventListener(pendingShieldDrag.move, pendingShieldDrag.onMove, true);
2827
+ document.removeEventListener(pendingShieldDrag.up, pendingShieldDrag.onUp, true);
2828
+ pendingShieldDrag = null;
2829
+ }
2830
+
2831
+ function beginPotentialShieldDrag(e) {
2832
+ stopNativeInteraction(e);
2833
+ if (e.button !== 0 || activeTextEditEl) return;
2834
+ var events = dragEventNames(e);
2835
+ var hit = elementFromEditorPoint(e.clientX, e.clientY);
2836
+ if (!hit || hit === document.body || hit === document.documentElement) return;
2837
+ var dragTarget =
2838
+ selectedEl &&
2839
+ document.documentElement.contains(selectedEl) &&
2840
+ selectedEl.contains(hit)
2841
+ ? selectedEl
2842
+ : selectionTargetForHit(hit);
2843
+ if (
2844
+ !dragTarget ||
2845
+ dragTarget === document.body ||
2846
+ dragTarget === document.documentElement ||
2847
+ isLayerInteractionBlocked(dragTarget)
2848
+ ) {
2849
+ return;
2850
+ }
2851
+ var startX = e.clientX;
2852
+ var startY = e.clientY;
2853
+ var didStartDrag = false;
2854
+ function selectDragTarget() {
2855
+ selectedEl = dragTarget;
2856
+ positionOverlay(selectionOverlay, selectedEl);
2857
+ window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
2858
+ }
2859
+ function onMove(ev) {
2860
+ if (Math.hypot(ev.clientX - startX, ev.clientY - startY) <= 3) return;
2861
+ clearPendingShieldDrag();
2862
+ didStartDrag = true;
2863
+ selectDragTarget();
2864
+ suppressNextShieldClickBriefly();
2865
+ startMove(ev);
2866
+ }
2867
+ function onUp(ev) {
2868
+ clearPendingShieldDrag();
2869
+ if (didStartDrag) return;
2870
+ if (ev) stopNativeInteraction(ev);
2871
+ selectDragTarget();
2872
+ suppressNextShieldClickBriefly();
2873
+ }
2874
+ clearPendingShieldDrag();
2875
+ pendingShieldDrag = { move: events.move, up: events.up, onMove: onMove, onUp: onUp };
2876
+ document.addEventListener(events.move, onMove, true);
2877
+ document.addEventListener(events.up, onUp, true);
2878
+ }
2879
+
1706
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
+ }
1707
2886
  var resizeHandle = e.target && e.target.getAttribute && e.target.getAttribute('data-agent-native-edit-handle');
1708
2887
  if (!resizeHandle && e.target && e.target.getAttribute) {
1709
2888
  resizeHandle = e.target.getAttribute('data-agent-native-edge-handle');
@@ -1720,6 +2899,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1720
2899
  startMove(e);
1721
2900
  }, true);
1722
2901
 
2902
+ shieldOverlay.addEventListener('pointerdown', beginPotentialShieldDrag, true);
2903
+
1723
2904
  ['pointerdown','pointerup','mousedown','mouseup','auxclick'].forEach(function(type) {
1724
2905
  shieldOverlay.addEventListener(type, stopNativeInteraction, true);
1725
2906
  });
@@ -1735,6 +2916,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1735
2916
  document.addEventListener('keydown', function(e) {
1736
2917
  if (!shouldForwardDesignHotkey(e)) return;
1737
2918
  stopNativeInteraction(e);
2919
+ if (e.key === 'Escape') clearRuntimeSelection();
1738
2920
  window.parent.postMessage({
1739
2921
  type: 'design-hotkey',
1740
2922
  key: e.key,
@@ -1788,7 +2970,12 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1788
2970
  stopNativeInteraction(e);
1789
2971
  var target = findTextEditTarget(elementFromEditorPoint(e.clientX, e.clientY));
1790
2972
  if (!target || target.nodeType !== 1) return;
1791
- selectedEl = target;
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;
1792
2979
  var originalText = target.textContent || '';
1793
2980
  var originalHtml = target.innerHTML || '';
1794
2981
  var committed = false;
@@ -1801,6 +2988,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1801
2988
  setTextEditingPointerPassthrough(true);
1802
2989
  positionOverlay(selectionOverlay, target);
1803
2990
  window.parent.postMessage({ type: 'element-select', payload: getElementInfo(target) }, '*');
2991
+ window.parent.postMessage({ type: 'element-dblclick-text', payload: getElementInfo(target) }, '*');
1804
2992
  postTextEditingState(target, true);
1805
2993
 
1806
2994
  function finish(commit) {
@@ -1882,11 +3070,30 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1882
3070
  hoveredEl = elementFromEditorPoint(e.clientX, e.clientY);
1883
3071
  if (!hoveredEl) {
1884
3072
  highlightOverlay.style.display = 'none';
3073
+ if (!spacingDrag) {
3074
+ selectedSpacingHovered = false;
3075
+ hoveredSpacingHandleKey = '';
3076
+ updateSpacingOverlay(selectedEl);
3077
+ }
1885
3078
  hideMeasurements();
1886
3079
  return;
1887
3080
  }
1888
3081
  if (hoveredEl && hoveredEl.closest('[data-agent-native-text-editing]')) return;
1889
- positionOverlay(highlightOverlay, hoveredEl);
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
+ }
1890
3097
  if (e.altKey && selectedEl && hoveredEl && selectedEl !== hoveredEl) {
1891
3098
  showMeasurements(selectedEl, hoveredEl);
1892
3099
  } else {
@@ -1899,8 +3106,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1899
3106
  shieldOverlay.addEventListener('pointerleave', function(e) {
1900
3107
  stopNativeInteraction(e);
1901
3108
  hoveredEl = null;
3109
+ if (!spacingDrag) {
3110
+ selectedSpacingHovered = false;
3111
+ hoveredSpacingHandleKey = '';
3112
+ updateSpacingOverlay(selectedEl);
3113
+ }
1902
3114
  highlightOverlay.style.display = 'none';
1903
3115
  hideMeasurements();
3116
+ window.parent.postMessage({ type: 'element-hover', payload: null }, '*');
1904
3117
  }, true);
1905
3118
 
1906
3119
  window.addEventListener('keyup', function(e) {
@@ -1910,16 +3123,21 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1910
3123
  window.addEventListener('message', function(e) {
1911
3124
  if (e.source !== window.parent) return;
1912
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
+ }
1913
3135
  if (e.data.type === 'scale-tool-mode') {
1914
3136
  scaleToolEnabled = !!e.data.enabled;
1915
3137
  return;
1916
3138
  }
1917
3139
  if (e.data.type === 'clear-selection') {
1918
- selectedEl = null;
1919
- hoveredEl = null;
1920
- selectionOverlay.style.display = 'none';
1921
- highlightOverlay.style.display = 'none';
1922
- hideMeasurements();
3140
+ clearRuntimeSelection();
1923
3141
  return;
1924
3142
  }
1925
3143
  if (e.data.type === 'select-element') {
@@ -1952,10 +3170,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1952
3170
  } catch (_err) {}
1953
3171
  }
1954
3172
  if (!target) return;
3173
+ selectedSpacingHovered = false;
3174
+ hoveredSpacingHandleKey = '';
1955
3175
  selectedEl = target;
1956
- var selectedInfo = getElementInfo(target);
1957
3176
  positionOverlay(selectionOverlay, target);
1958
- window.parent.postMessage({ type: 'element-select', payload: selectedInfo }, '*');
3177
+ if (hoveredEl === selectedEl) highlightOverlay.style.display = 'none';
1959
3178
  return;
1960
3179
  }
1961
3180
  if (e.data.type === 'hover-element') {
@@ -1978,7 +3197,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
1978
3197
  }
1979
3198
  var hoverTarget = findRuntimeTarget(String(e.data.selector || ''), hoverCandidates);
1980
3199
  hoveredEl = hoverTarget;
1981
- if (hoveredEl && !isLayerInteractionBlocked(hoveredEl)) {
3200
+ if (hoveredEl && !isLayerInteractionBlocked(hoveredEl) && hoveredEl !== selectedEl) {
1982
3201
  positionOverlay(highlightOverlay, hoveredEl);
1983
3202
  } else {
1984
3203
  highlightOverlay.style.display = 'none';
@@ -2045,6 +3264,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
2045
3264
 
2046
3265
  window.addEventListener('scroll', refreshOverlays, true);
2047
3266
  window.addEventListener('resize', refreshOverlays);
3267
+ applyEditorChromeScale();
2048
3268
  })();
2049
3269
  </script>
2050
3270
  `;
@@ -2052,6 +3272,41 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
2052
3272
  interface DesignCanvasProps {
2053
3273
  content: string;
2054
3274
  contentKey?: string;
3275
+ /**
3276
+ * The runtime source tier for this canvas.
3277
+ *
3278
+ * - `"inline"` (default) — HTML/Alpine `srcdoc` iframe; same-origin null
3279
+ * origin; all bridge scripts injected by DesignCanvas.
3280
+ * - `"localhost"` — `src=devServerUrl`; dev server is same-origin in most
3281
+ * setups; bridge trust: origin must match parent or be "null".
3282
+ * - `"fusion"` — `src=builderHostedUrl`; cross-origin Builder-hosted app;
3283
+ * bridge trust is relaxed to window-identity only (no origin check) so
3284
+ * the Builder-hosted iframe can communicate with the editor. The sandbox
3285
+ * grants `allow-same-origin` so the Builder app can reach its own resources.
3286
+ *
3287
+ * When omitted, DesignCanvas infers the tier from the content value:
3288
+ * a value that passes `getExternalPreviewUrl` is treated as `"localhost"`;
3289
+ * otherwise `"inline"`. Pass `sourceType="fusion"` explicitly when the
3290
+ * content URL is a Builder-hosted (cross-origin) app so the bridge security
3291
+ * model uses window-identity trust instead of same-origin trust.
3292
+ */
3293
+ sourceType?: "inline" | "localhost" | "fusion";
3294
+ /**
3295
+ * Explicit Builder-hosted app URL for fusion source rendering.
3296
+ *
3297
+ * When `sourceType === "fusion"` and this prop is provided, the iframe uses
3298
+ * this URL as `src` regardless of what `content` contains. This lets the
3299
+ * caller hold the original inline HTML in `content` (for collab/history
3300
+ * purposes) while pointing the canvas at the migrated Builder-hosted app.
3301
+ *
3302
+ * When absent and `sourceType === "fusion"`, the component falls back to
3303
+ * the existing external-URL detection on `content` (i.e. if `content` is
3304
+ * itself a URL it is used as-is, which is the pattern when the branch URL
3305
+ * has been written into the design file content).
3306
+ *
3307
+ * For `"inline"` and `"localhost"` sources this prop is ignored.
3308
+ */
3309
+ fusionUrl?: string;
2055
3310
  zoom: number;
2056
3311
  onZoomChange?: (zoom: number) => void;
2057
3312
  deviceFrame: DeviceFrameType;
@@ -2060,12 +3315,17 @@ interface DesignCanvasProps {
2060
3315
  viewportHeight: number;
2061
3316
  displayWidth: number;
2062
3317
  displayHeight: number;
3318
+ fluid?: boolean;
2063
3319
  };
3320
+ editorChromeScaleX?: number;
3321
+ editorChromeScaleY?: number;
2064
3322
  editMode: boolean;
2065
3323
  interactMode: boolean;
3324
+ readOnly?: boolean;
2066
3325
  scaleMode?: boolean;
2067
3326
  onElementSelect: (info: ElementInfo) => void;
2068
- onElementHover: (info: ElementInfo) => void;
3327
+ onElementHover: (info: ElementInfo | null) => void;
3328
+ onClearSelection?: () => void;
2069
3329
  onVisualStyleChange?: (
2070
3330
  selector: string,
2071
3331
  styles: Record<string, string>,
@@ -2082,6 +3342,7 @@ interface DesignCanvasProps {
2082
3342
  selector?: string;
2083
3343
  hasRange?: boolean;
2084
3344
  }) => void;
3345
+ onElementDblClickText?: (info: ElementInfo) => void;
2085
3346
  onIframeHotkey?: (event: IframeHotkeyPayload) => void;
2086
3347
  onIframeContextMenu?: (event: IframeContextMenuPayload) => void;
2087
3348
  onVisualStructureChange?: (
@@ -2120,6 +3381,7 @@ interface DesignCanvasProps {
2120
3381
  lockedSelectors?: string[];
2121
3382
  hiddenSelectors?: string[];
2122
3383
  clearSelectionRequest?: number;
3384
+ registerRuntimeBridge?: boolean;
2123
3385
  /** Called when the user exits pin mode. */
2124
3386
  onExitPinMode?: () => void;
2125
3387
  /** Stable id of the open design (used for pin scoping + agent prompt). */
@@ -2137,6 +3399,55 @@ interface DesignCanvasProps {
2137
3399
  * opened in a new tab by the iframe itself and never reach this callback.
2138
3400
  */
2139
3401
  onPrototypeNavigate?: (screen: string, href: string) => void;
3402
+ /**
3403
+ * Motion tracks to load into the iframe's motion-preview bridge. Sent via
3404
+ * `motion-load-tracks` whenever this prop changes. When cleared
3405
+ * (`undefined` or `[]`) a `motion-preview-clear` message is sent to remove
3406
+ * any applied preview overrides.
3407
+ *
3408
+ * The MotionDock sends scrub ticks as `{ type: 'motion-preview', t,
3409
+ * durationMs }` directly from its `canvasIframeRef`. DesignCanvas only
3410
+ * needs the tracks so the bridge can interpolate values at each tick.
3411
+ */
3412
+ motionTracks?: MotionTrackWire[];
3413
+ /**
3414
+ * Explicit iframe width in pixels. When provided it overrides the width
3415
+ * derived from `deviceFrame`, enabling per-breakpoint preview (e.g. Mobile
3416
+ * 390 / Tablet 768 / Desktop 1280 side-by-side frames in the overview).
3417
+ * The height still comes from `deviceFrame`; `deviceFrame="none"` keeps
3418
+ * 100% height.
3419
+ */
3420
+ previewWidthPx?: number;
3421
+ /**
3422
+ * Shader-fill CSS preview to apply to a selected element inside the iframe.
3423
+ *
3424
+ * When set, the canvas sends a `shader-fill-preview` bridge message that
3425
+ * applies the CSS `background` value on the target element **without
3426
+ * persisting anything**. When cleared (`null` / `undefined`) a
3427
+ * `shader-fill-preview-clear` message is sent to restore the original
3428
+ * background.
3429
+ *
3430
+ * Preview-only — never writes to DB, Yjs, or source. Part of the §6.7
3431
+ * shader-fill PREVIEW path; the apply path remains gated until runtime
3432
+ * rendering + source-write + diff proof are all in place.
3433
+ */
3434
+ shaderFillPreview?: {
3435
+ /** CSS selector for the target element (preferred over nodeId). */
3436
+ selector?: string;
3437
+ /** data-agent-native-node-id value for the target element. */
3438
+ nodeId?: string;
3439
+ /** The CSS `background` value returned by preview-shader-fill. */
3440
+ css: string;
3441
+ } | null;
3442
+ /**
3443
+ * Called when the user clicks the component-instance source tag (the
3444
+ * "ComponentName →" pill that floats above a selected component root).
3445
+ * The parent should invoke `open-component-source` with these params.
3446
+ */
3447
+ onComponentSourceJump?: (params: {
3448
+ nodeId: string;
3449
+ componentName: string;
3450
+ }) => void;
2140
3451
  }
2141
3452
 
2142
3453
  function getExternalPreviewUrl(content: string): string | null {
@@ -2172,19 +3483,26 @@ export interface IframeContextMenuPayload {
2172
3483
  export function DesignCanvas({
2173
3484
  content,
2174
3485
  contentKey,
3486
+ sourceType,
3487
+ fusionUrl,
2175
3488
  zoom,
2176
3489
  onZoomChange,
2177
3490
  deviceFrame,
2178
3491
  embeddedFrame,
3492
+ editorChromeScaleX = 1,
3493
+ editorChromeScaleY = editorChromeScaleX,
2179
3494
  editMode,
2180
3495
  interactMode,
3496
+ readOnly = false,
2181
3497
  scaleMode = false,
2182
3498
  clearSelectionRequest,
2183
3499
  onElementSelect,
2184
3500
  onElementHover,
3501
+ onClearSelection,
2185
3502
  onVisualStyleChange,
2186
3503
  onTextContentChange,
2187
3504
  onTextEditingStateChange,
3505
+ onElementDblClickText,
2188
3506
  onIframeHotkey,
2189
3507
  onIframeContextMenu,
2190
3508
  onVisualStructureChange,
@@ -2200,11 +3518,16 @@ export function DesignCanvas({
2200
3518
  lockedSelectors = [],
2201
3519
  hiddenSelectors = [],
2202
3520
  onExitPinMode,
3521
+ registerRuntimeBridge = true,
2203
3522
  designId,
2204
3523
  designTitle,
2205
3524
  commentContextId,
2206
3525
  commentContextLabel,
2207
3526
  onPrototypeNavigate,
3527
+ motionTracks,
3528
+ previewWidthPx,
3529
+ onComponentSourceJump,
3530
+ shaderFillPreview,
2208
3531
  }: DesignCanvasProps) {
2209
3532
  const t = useT();
2210
3533
  const iframeRef = useRef<HTMLIFrameElement>(null);
@@ -2215,10 +3538,25 @@ export function DesignCanvas({
2215
3538
  const [annotationPins, setAnnotationPins] = useState<CanvasPin[]>([]);
2216
3539
  const [pinSubmitSignal, setPinSubmitSignal] = useState(0);
2217
3540
  const isEmbeddedFrame = Boolean(embeddedFrame);
2218
- const externalPreviewUrl = useMemo(
2219
- () => getExternalPreviewUrl(renderedContent),
2220
- [renderedContent],
2221
- );
3541
+ // Resolve the URL to render in the iframe:
3542
+ // 1. When sourceType === "fusion" and fusionUrl is set, prefer the explicit
3543
+ // Builder-hosted URL over whatever is in `content` (which may still be the
3544
+ // original inline HTML).
3545
+ // 2. Otherwise fall back to the content-based URL detection (handles the case
3546
+ // where the branch URL has been written into the design file content, or
3547
+ // where the localhost URL is the file content).
3548
+ const externalPreviewUrl = useMemo(() => {
3549
+ if (sourceType === "fusion" && fusionUrl) {
3550
+ try {
3551
+ const url = new URL(fusionUrl);
3552
+ url.hash = "";
3553
+ return url.toString();
3554
+ } catch {
3555
+ // fall through to content detection below
3556
+ }
3557
+ }
3558
+ return getExternalPreviewUrl(renderedContent);
3559
+ }, [fusionUrl, renderedContent, sourceType]);
2222
3560
  zoomRef.current = zoom;
2223
3561
 
2224
3562
  const queuedAnnotationPins = useMemo(
@@ -2255,18 +3593,24 @@ export function DesignCanvas({
2255
3593
  // outside Edit mode. The editor chrome bridge is omitted only for Interact.
2256
3594
  const srcdoc = useMemo(() => {
2257
3595
  if (externalPreviewUrl) return undefined;
2258
- const editorChromeBridge = interactMode
2259
- ? ""
2260
- : createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
2261
- EDITOR_CHROME_BRIDGE_SCRIPT.replace(
2262
- "__TEXT_EDITING_ENABLED__",
2263
- editMode ? "true" : "false",
2264
- );
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));
2265
3607
  const embeddedWheelBridge = EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
2266
3608
  "__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
2267
3609
  isEmbeddedFrame ? "true" : "false",
2268
3610
  );
2269
3611
  const bridgeToInject =
3612
+ MOTION_PREVIEW_BRIDGE_SCRIPT +
3613
+ SHADER_FILL_PREVIEW_BRIDGE_SCRIPT +
2270
3614
  TWEAK_BRIDGE_SCRIPT +
2271
3615
  ZOOM_BRIDGE_SCRIPT +
2272
3616
  NAV_BRIDGE_SCRIPT +
@@ -2280,28 +3624,51 @@ export function DesignCanvas({
2280
3624
  }
2281
3625
  // No body/html tags — wrap it
2282
3626
  return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${renderedContent}${bridgeToInject}</body></html>`;
3627
+ // editorChromeScaleX/Y are intentionally NOT deps: they only seed the initial
3628
+ // baked chrome scale. Live zoom updates flow through the set-editor-chrome-scale
3629
+ // postMessage above. Including them here rebuilds srcdoc on every zoom commit,
3630
+ // which reloads the iframe and flashes the screen content white.
3631
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2283
3632
  }, [
2284
3633
  editMode,
2285
3634
  externalPreviewUrl,
2286
3635
  interactMode,
2287
3636
  isEmbeddedFrame,
3637
+ readOnly,
2288
3638
  renderedContent,
2289
3639
  ]);
2290
3640
 
2291
3641
  // Listen for messages from the iframe
2292
3642
  useEffect(() => {
2293
3643
  function handleMessage(e: MessageEvent) {
2294
- if (
2295
- !isTrustedCanvasBridgeMessage({
2296
- source: e.source,
2297
- origin: e.origin,
2298
- iframeWindow: iframeRef.current?.contentWindow,
2299
- parentOrigin: window.location.origin,
2300
- })
2301
- ) {
3644
+ const iframeWindow = iframeRef.current?.contentWindow;
3645
+ // For fusion sources the Builder-hosted app is cross-origin, so the strict
3646
+ // `origin === parentOrigin` check can never match. We still require window
3647
+ // identity (the message must come from our own iframe window, not any
3648
+ // arbitrary cross-origin frame), AND we validate the message origin
3649
+ // against a Builder-host allowlist (the configured fusionUrl origin or the
3650
+ // *.builder.io family) before relaxing the origin check. If the origin is
3651
+ // not on the allowlist we keep the strict check so a hostile frame that
3652
+ // somehow shares our window reference still can't be trusted.
3653
+ const trusted =
3654
+ sourceType === "fusion"
3655
+ ? iframeWindow != null &&
3656
+ e.source === iframeWindow &&
3657
+ isAllowedFusionOrigin(e.origin, fusionUrl)
3658
+ : isTrustedCanvasBridgeMessage({
3659
+ source: e.source,
3660
+ origin: e.origin,
3661
+ iframeWindow,
3662
+ parentOrigin: window.location.origin,
3663
+ });
3664
+ if (!trusted) {
2302
3665
  return;
2303
3666
  }
2304
3667
  if (!e.data || !e.data.type) return;
3668
+ if (e.data.type === "clear-selection") {
3669
+ onClearSelection?.();
3670
+ return;
3671
+ }
2305
3672
  if (e.data.type === "element-select") {
2306
3673
  onElementSelect(e.data.payload);
2307
3674
  }
@@ -2335,9 +3702,15 @@ export function DesignCanvas({
2335
3702
  const placement = String(e.data.placement || "after");
2336
3703
  const requestId =
2337
3704
  typeof e.data.requestId === "string" ? e.data.requestId : undefined;
3705
+ const sourceId =
3706
+ typeof e.data.sourceId === "string" ? e.data.sourceId : undefined;
3707
+ const anchorSourceId =
3708
+ typeof e.data.anchorSourceId === "string"
3709
+ ? e.data.anchorSourceId
3710
+ : undefined;
2338
3711
  if (
2339
- selector &&
2340
- anchorSelector &&
3712
+ (selector || sourceId) &&
3713
+ (anchorSelector || anchorSourceId) &&
2341
3714
  (placement === "before" ||
2342
3715
  placement === "after" ||
2343
3716
  placement === "inside")
@@ -2349,14 +3722,8 @@ export function DesignCanvas({
2349
3722
  e.data.payload,
2350
3723
  {
2351
3724
  requestId,
2352
- sourceId:
2353
- typeof e.data.sourceId === "string"
2354
- ? e.data.sourceId
2355
- : undefined,
2356
- anchorSourceId:
2357
- typeof e.data.anchorSourceId === "string"
2358
- ? e.data.anchorSourceId
2359
- : undefined,
3725
+ sourceId,
3726
+ anchorSourceId,
2360
3727
  },
2361
3728
  );
2362
3729
  if (requestId) {
@@ -2409,6 +3776,10 @@ export function DesignCanvas({
2409
3776
  });
2410
3777
  return;
2411
3778
  }
3779
+ if (e.data.type === "element-dblclick-text") {
3780
+ onElementDblClickText?.(e.data.payload);
3781
+ return;
3782
+ }
2412
3783
  if (e.data.type === "design-hotkey") {
2413
3784
  onIframeHotkey?.({
2414
3785
  key: String(e.data.key || ""),
@@ -2454,6 +3825,16 @@ export function DesignCanvas({
2454
3825
  );
2455
3826
  return;
2456
3827
  }
3828
+ if (e.data.type === "component-source-jump") {
3829
+ // The user clicked the component-instance tag ("ComponentName →").
3830
+ // Relay to the parent so it can invoke open-component-source.
3831
+ const nodeId = String(e.data.nodeId || "");
3832
+ const componentName = String(e.data.componentName || "");
3833
+ if (nodeId && componentName) {
3834
+ onComponentSourceJump?.({ nodeId, componentName });
3835
+ }
3836
+ return;
3837
+ }
2457
3838
  if (e.data.type === "embedded-canvas-wheel") {
2458
3839
  if (!isEmbeddedFrame) return;
2459
3840
  const iframe = iframeRef.current;
@@ -2527,9 +3908,11 @@ export function DesignCanvas({
2527
3908
  }, [
2528
3909
  onElementSelect,
2529
3910
  onElementHover,
3911
+ onClearSelection,
2530
3912
  onVisualStyleChange,
2531
3913
  onTextContentChange,
2532
3914
  onTextEditingStateChange,
3915
+ onElementDblClickText,
2533
3916
  onIframeHotkey,
2534
3917
  onIframeContextMenu,
2535
3918
  onVisualStructureChange,
@@ -2537,7 +3920,10 @@ export function DesignCanvas({
2537
3920
  onZoomChange,
2538
3921
  deviceFrame,
2539
3922
  onPrototypeNavigate,
3923
+ onComponentSourceJump,
2540
3924
  isEmbeddedFrame,
3925
+ sourceType,
3926
+ fusionUrl,
2541
3927
  ]);
2542
3928
 
2543
3929
  const replayIframeEditorState = useCallback(() => {
@@ -2575,14 +3961,43 @@ export function DesignCanvas({
2575
3961
  : { type: "hover-element", selector: "", selectorCandidates: [] },
2576
3962
  "*",
2577
3963
  );
3964
+ // Re-send motion tracks so the preview bridge is ready after a reload.
3965
+ if (motionTracks && motionTracks.length > 0) {
3966
+ iframe.contentWindow?.postMessage(
3967
+ { type: "motion-load-tracks", tracks: motionTracks },
3968
+ "*",
3969
+ );
3970
+ } else {
3971
+ iframe.contentWindow?.postMessage({ type: "motion-preview-clear" }, "*");
3972
+ }
3973
+ // Re-apply the shader-fill preview after a reload so the preview survives
3974
+ // screen switches. Preview-only — never writes to DB, Yjs, or source.
3975
+ if (shaderFillPreview) {
3976
+ iframe.contentWindow?.postMessage(
3977
+ {
3978
+ type: "shader-fill-preview",
3979
+ selector: shaderFillPreview.selector ?? "",
3980
+ nodeId: shaderFillPreview.nodeId ?? "",
3981
+ css: shaderFillPreview.css,
3982
+ },
3983
+ "*",
3984
+ );
3985
+ } else {
3986
+ iframe.contentWindow?.postMessage(
3987
+ { type: "shader-fill-preview-clear" },
3988
+ "*",
3989
+ );
3990
+ }
2578
3991
  }, [
2579
3992
  hoveredSelector,
2580
3993
  hoveredSelectorCandidates,
2581
3994
  hiddenSelectors,
2582
3995
  lockedSelectors,
3996
+ motionTracks,
2583
3997
  scaleMode,
2584
3998
  selectedSelector,
2585
3999
  selectedSelectorCandidates,
4000
+ shaderFillPreview,
2586
4001
  tweakValues,
2587
4002
  ]);
2588
4003
 
@@ -2605,6 +4020,59 @@ export function DesignCanvas({
2605
4020
  );
2606
4021
  }, [clearSelectionRequest]);
2607
4022
 
4023
+ // Sync motion tracks to the iframe bridge whenever they change.
4024
+ // When motionTracks is empty/undefined, clear any preview overrides so the
4025
+ // design returns to its authored state (no stale inline styles).
4026
+ useEffect(() => {
4027
+ const win = iframeRef.current?.contentWindow;
4028
+ if (!win) return;
4029
+ if (!motionTracks || motionTracks.length === 0) {
4030
+ win.postMessage({ type: "motion-preview-clear" }, "*");
4031
+ } else {
4032
+ win.postMessage(
4033
+ { type: "motion-load-tracks", tracks: motionTracks },
4034
+ "*",
4035
+ );
4036
+ }
4037
+ }, [motionTracks]);
4038
+
4039
+ // Sync shader-fill preview to the iframe whenever the prop changes.
4040
+ // When cleared (null / undefined) send a clear message so the bridge
4041
+ // restores the original background on the previously-patched element.
4042
+ // Preview-only — never writes to DB, Yjs, or source.
4043
+ useEffect(() => {
4044
+ const win = iframeRef.current?.contentWindow;
4045
+ if (!win) return;
4046
+ if (!shaderFillPreview) {
4047
+ win.postMessage({ type: "shader-fill-preview-clear" }, "*");
4048
+ } else {
4049
+ win.postMessage(
4050
+ {
4051
+ type: "shader-fill-preview",
4052
+ selector: shaderFillPreview.selector ?? "",
4053
+ nodeId: shaderFillPreview.nodeId ?? "",
4054
+ css: shaderFillPreview.css,
4055
+ },
4056
+ "*",
4057
+ );
4058
+ }
4059
+ }, [shaderFillPreview]);
4060
+
4061
+ // Push the constant-size chrome scale into the iframe LIVE (CSS vars only) when
4062
+ // overview zoom settles. This is intentionally separate from the srcdoc build so
4063
+ // a scale change never rebuilds srcdoc / reloads the iframe (which flashes the
4064
+ // content white). The baked __EDITOR_CHROME_SCALE__ values cover first paint.
4065
+ useEffect(() => {
4066
+ iframeRef.current?.contentWindow?.postMessage(
4067
+ {
4068
+ type: "set-editor-chrome-scale",
4069
+ scaleX: editorChromeScaleX,
4070
+ scaleY: editorChromeScaleY,
4071
+ },
4072
+ "*",
4073
+ );
4074
+ }, [editorChromeScaleX, editorChromeScaleY]);
4075
+
2608
4076
  const sendStyleChange = useCallback(
2609
4077
  (selector: string, property: string, value: string) => {
2610
4078
  const iframe = iframeRef.current;
@@ -2617,6 +4085,61 @@ export function DesignCanvas({
2617
4085
  [],
2618
4086
  );
2619
4087
 
4088
+ /**
4089
+ * Send a motion-preview scrub tick to the iframe. `t` is the normalised
4090
+ * playhead position in [0, 1]. Tracks must have been loaded first via the
4091
+ * `motionTracks` prop (or an explicit `motion-load-tracks` message).
4092
+ * Preview-only — never writes to DB/Yjs/source.
4093
+ */
4094
+ const sendMotionPreview = useCallback((t: number, durationMs?: number) => {
4095
+ const iframe = iframeRef.current;
4096
+ if (!iframe?.contentWindow) return;
4097
+ iframe.contentWindow.postMessage(
4098
+ { type: "motion-preview", t: Math.max(0, Math.min(1, t)), durationMs },
4099
+ "*",
4100
+ );
4101
+ }, []);
4102
+
4103
+ /**
4104
+ * Clear all motion-preview inline-style overrides in the iframe and remove
4105
+ * the in-memory track list. Call when the Motion dock is closed.
4106
+ */
4107
+ const clearMotionPreview = useCallback(() => {
4108
+ iframeRef.current?.contentWindow?.postMessage(
4109
+ { type: "motion-preview-clear" },
4110
+ "*",
4111
+ );
4112
+ }, []);
4113
+
4114
+ /**
4115
+ * Send a shader-fill CSS preview to the iframe. Targets the element
4116
+ * identified by `selector` (preferred) or `nodeId`. Preview-only — the
4117
+ * bridge script restores the original background on clear.
4118
+ */
4119
+ const sendShaderFillPreview = useCallback(
4120
+ (selector: string, nodeId: string, css: string) => {
4121
+ const win = iframeRef.current?.contentWindow;
4122
+ if (!win) return;
4123
+ win.postMessage(
4124
+ { type: "shader-fill-preview", selector, nodeId, css },
4125
+ "*",
4126
+ );
4127
+ },
4128
+ [],
4129
+ );
4130
+
4131
+ /**
4132
+ * Clear the shader-fill preview in the iframe, restoring the original
4133
+ * background on the patched element. Call when the preview is dismissed
4134
+ * or when the selection changes to a different element.
4135
+ */
4136
+ const clearShaderFillPreview = useCallback(() => {
4137
+ iframeRef.current?.contentWindow?.postMessage(
4138
+ { type: "shader-fill-preview-clear" },
4139
+ "*",
4140
+ );
4141
+ }, []);
4142
+
2620
4143
  const replacePreviewContent = useCallback(
2621
4144
  (nextContent: string, selector?: string | null, candidates?: string[]) => {
2622
4145
  const iframe = iframeRef.current;
@@ -2654,15 +4177,65 @@ export function DesignCanvas({
2654
4177
 
2655
4178
  // Expose iframe runtime mutations for the editor orchestrator.
2656
4179
  useEffect(() => {
4180
+ if (!registerRuntimeBridge) return;
2657
4181
  (window as any).__designCanvasSendStyle = sendStyleChange;
2658
4182
  (window as any).__designCanvasReplaceContent = replacePreviewContent;
2659
4183
  (window as any).__designCanvasDeleteElement = deleteRuntimeElement;
4184
+ (window as any).__designCanvasSendMotionPreview = sendMotionPreview;
4185
+ (window as any).__designCanvasClearMotionPreview = clearMotionPreview;
4186
+ // Shader-fill preview helpers (preview-only, §6.7 gating applies to apply).
4187
+ (window as any).__designCanvasSendShaderFillPreview = sendShaderFillPreview;
4188
+ (window as any).__designCanvasClearShaderFillPreview =
4189
+ clearShaderFillPreview;
2660
4190
  return () => {
2661
- delete (window as any).__designCanvasSendStyle;
2662
- delete (window as any).__designCanvasReplaceContent;
2663
- delete (window as any).__designCanvasDeleteElement;
4191
+ // Identity-guard each delete so a stale unmounting instance never clobbers
4192
+ // a freshly mounted instance's bridge during a remount race.
4193
+ if ((window as any).__designCanvasSendStyle === sendStyleChange) {
4194
+ delete (window as any).__designCanvasSendStyle;
4195
+ }
4196
+ if (
4197
+ (window as any).__designCanvasReplaceContent === replacePreviewContent
4198
+ ) {
4199
+ delete (window as any).__designCanvasReplaceContent;
4200
+ }
4201
+ if (
4202
+ (window as any).__designCanvasDeleteElement === deleteRuntimeElement
4203
+ ) {
4204
+ delete (window as any).__designCanvasDeleteElement;
4205
+ }
4206
+ if (
4207
+ (window as any).__designCanvasSendMotionPreview === sendMotionPreview
4208
+ ) {
4209
+ delete (window as any).__designCanvasSendMotionPreview;
4210
+ }
4211
+ if (
4212
+ (window as any).__designCanvasClearMotionPreview === clearMotionPreview
4213
+ ) {
4214
+ delete (window as any).__designCanvasClearMotionPreview;
4215
+ }
4216
+ if (
4217
+ (window as any).__designCanvasSendShaderFillPreview ===
4218
+ sendShaderFillPreview
4219
+ ) {
4220
+ delete (window as any).__designCanvasSendShaderFillPreview;
4221
+ }
4222
+ if (
4223
+ (window as any).__designCanvasClearShaderFillPreview ===
4224
+ clearShaderFillPreview
4225
+ ) {
4226
+ delete (window as any).__designCanvasClearShaderFillPreview;
4227
+ }
2664
4228
  };
2665
- }, [deleteRuntimeElement, replacePreviewContent, sendStyleChange]);
4229
+ }, [
4230
+ deleteRuntimeElement,
4231
+ registerRuntimeBridge,
4232
+ replacePreviewContent,
4233
+ sendStyleChange,
4234
+ sendMotionPreview,
4235
+ clearMotionPreview,
4236
+ sendShaderFillPreview,
4237
+ clearShaderFillPreview,
4238
+ ]);
2666
4239
 
2667
4240
  // Device dimensions match real-world devices. iframes are replaced elements
2668
4241
  // with an intrinsic 300×150 size, so `aspect-ratio` + `height: auto` doesn't
@@ -2679,6 +4252,13 @@ export function DesignCanvas({
2679
4252
 
2680
4253
  const { width: iframeWidth, height: iframeHeight } =
2681
4254
  deviceDimensions[deviceFrame];
4255
+ const embeddedFrameFluid = embeddedFrame?.fluid === true;
4256
+
4257
+ // Per-breakpoint override: when previewWidthPx is set it takes priority over
4258
+ // the deviceFrame width so the caller can render the same source at an
4259
+ // explicit viewport width (e.g. 390 / 768 / 1280 side-by-side breakpoints).
4260
+ const resolvedWidth =
4261
+ previewWidthPx != null ? `${previewWidthPx}px` : iframeWidth;
2682
4262
 
2683
4263
  // Wrap the iframe in a positioned container so DrawOverlay /
2684
4264
  // CanvasCommentPins can absolutely-position themselves on top of the
@@ -2691,9 +4271,15 @@ export function DesignCanvas({
2691
4271
  <div
2692
4272
  className="design-canvas-iframe-wrapper relative inline-block ring-1 ring-border/60 shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_8px_24px_-12px_rgba(0,0,0,0.45)]"
2693
4273
  style={{
2694
- width: embeddedFrame ? embeddedFrame.viewportWidth : iframeWidth,
4274
+ width: embeddedFrame
4275
+ ? embeddedFrameFluid
4276
+ ? "100%"
4277
+ : embeddedFrame.viewportWidth
4278
+ : resolvedWidth,
2695
4279
  height: embeddedFrame
2696
- ? embeddedFrame.viewportHeight
4280
+ ? embeddedFrameFluid
4281
+ ? "100%"
4282
+ : embeddedFrame.viewportHeight
2697
4283
  : deviceFrame === "none"
2698
4284
  ? "100%"
2699
4285
  : (iframeHeight ?? undefined),
@@ -2709,6 +4295,12 @@ export function DesignCanvas({
2709
4295
  : "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
2710
4296
  }
2711
4297
  data-design-preview-iframe
4298
+ data-design-source-type={
4299
+ sourceType ??
4300
+ (externalPreviewUrl
4301
+ ? "localhost" // inferred — content is a URL
4302
+ : "inline")
4303
+ }
2712
4304
  className="block h-full w-full border-0 bg-transparent"
2713
4305
  title={t("designEditor.designPreview")}
2714
4306
  />
@@ -2771,6 +4363,17 @@ export function DesignCanvas({
2771
4363
  );
2772
4364
 
2773
4365
  if (embeddedFrame) {
4366
+ if (embeddedFrameFluid) {
4367
+ return (
4368
+ <div
4369
+ ref={scrollContainerRef}
4370
+ className="relative h-full w-full overflow-hidden"
4371
+ >
4372
+ {iframeElement}
4373
+ </div>
4374
+ );
4375
+ }
4376
+
2774
4377
  const scaleX =
2775
4378
  embeddedFrame.displayWidth / Math.max(1, embeddedFrame.viewportWidth);
2776
4379
  const scaleY =