@agent-native/core 0.82.0 → 0.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/README.md +2 -2
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +34 -0
  4. package/corpus/core/README.md +2 -2
  5. package/corpus/core/docs/content/cloneable-saas.mdx +2 -2
  6. package/corpus/core/docs/content/faq.mdx +1 -1
  7. package/corpus/core/docs/content/locales/ar-SA/cloneable-saas.mdx +2 -2
  8. package/corpus/core/docs/content/locales/ar-SA/faq.mdx +1 -1
  9. package/corpus/core/docs/content/locales/ar-SA/template-chat.mdx +1 -1
  10. package/corpus/core/docs/content/locales/de-DE/cloneable-saas.mdx +2 -2
  11. package/corpus/core/docs/content/locales/de-DE/faq.mdx +1 -1
  12. package/corpus/core/docs/content/locales/de-DE/template-chat.mdx +1 -1
  13. package/corpus/core/docs/content/locales/es-ES/cloneable-saas.mdx +2 -2
  14. package/corpus/core/docs/content/locales/es-ES/faq.mdx +1 -1
  15. package/corpus/core/docs/content/locales/es-ES/template-chat.mdx +1 -1
  16. package/corpus/core/docs/content/locales/fr-FR/cloneable-saas.mdx +2 -2
  17. package/corpus/core/docs/content/locales/fr-FR/faq.mdx +1 -1
  18. package/corpus/core/docs/content/locales/fr-FR/template-chat.mdx +1 -1
  19. package/corpus/core/docs/content/locales/hi-IN/cloneable-saas.mdx +2 -2
  20. package/corpus/core/docs/content/locales/hi-IN/faq.mdx +1 -1
  21. package/corpus/core/docs/content/locales/hi-IN/template-chat.mdx +1 -1
  22. package/corpus/core/docs/content/locales/ja-JP/cloneable-saas.mdx +2 -2
  23. package/corpus/core/docs/content/locales/ja-JP/faq.mdx +1 -1
  24. package/corpus/core/docs/content/locales/ja-JP/template-chat.mdx +1 -1
  25. package/corpus/core/docs/content/locales/ko-KR/cloneable-saas.mdx +2 -2
  26. package/corpus/core/docs/content/locales/ko-KR/faq.mdx +1 -1
  27. package/corpus/core/docs/content/locales/ko-KR/template-chat.mdx +1 -1
  28. package/corpus/core/docs/content/locales/pt-BR/cloneable-saas.mdx +2 -2
  29. package/corpus/core/docs/content/locales/pt-BR/faq.mdx +1 -1
  30. package/corpus/core/docs/content/locales/pt-BR/template-chat.mdx +1 -1
  31. package/corpus/core/docs/content/locales/zh-CN/cloneable-saas.mdx +2 -2
  32. package/corpus/core/docs/content/locales/zh-CN/faq.mdx +1 -1
  33. package/corpus/core/docs/content/locales/zh-CN/template-chat.mdx +1 -1
  34. package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.mdx +2 -2
  35. package/corpus/core/docs/content/locales/zh-TW/faq.mdx +1 -1
  36. package/corpus/core/docs/content/locales/zh-TW/template-chat.mdx +1 -1
  37. package/corpus/core/docs/content/template-chat.mdx +1 -1
  38. package/corpus/core/package.json +1 -1
  39. package/corpus/core/src/agent/engine/builtin.ts +1 -1
  40. package/corpus/core/src/agent/engine/output-tokens.ts +1 -1
  41. package/corpus/core/src/agent/harness/acp-adapter.ts +28 -4
  42. package/corpus/core/src/agent/harness/ai-sdk-adapter.ts +3 -0
  43. package/corpus/core/src/agent/harness/translate.ts +3 -0
  44. package/corpus/core/src/agent/harness/types.ts +1 -0
  45. package/corpus/core/src/agent/model-config.ts +34 -13
  46. package/corpus/core/src/agent/production-agent.ts +87 -10
  47. package/corpus/core/src/agent/thread-data-builder.ts +29 -4
  48. package/corpus/core/src/agent/tool-call-journal.ts +109 -48
  49. package/corpus/core/src/agent/types.ts +6 -1
  50. package/corpus/core/src/cli/design-connect.ts +106 -0
  51. package/corpus/core/src/cli/gateway-helpers.ts +17 -0
  52. package/corpus/core/src/cli/recap.ts +0 -2
  53. package/corpus/core/src/cli/skills.ts +54 -16
  54. package/corpus/core/src/cli/workspace-dev.ts +29 -5
  55. package/corpus/core/src/client/AssistantChat.tsx +89 -42
  56. package/corpus/core/src/client/CommandMenu.tsx +34 -12
  57. package/corpus/core/src/client/DefaultSpinner.tsx +8 -1
  58. package/corpus/core/src/client/chat/message-components.tsx +49 -1
  59. package/corpus/core/src/client/chat/repo-helpers.ts +71 -0
  60. package/corpus/core/src/client/chat/run-recovery.tsx +15 -5
  61. package/corpus/core/src/client/components/ApiKeySettings.tsx +8 -8
  62. package/corpus/core/src/client/components/CodeRequiredDialog.tsx +26 -15
  63. package/corpus/core/src/client/components/MissingKeyCard.tsx +13 -6
  64. package/corpus/core/src/client/composer/TiptapComposer.tsx +4 -3
  65. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +0 -1
  66. package/corpus/core/src/client/guided-questions.tsx +21 -27
  67. package/corpus/core/src/client/integrations/IntegrationCard.tsx +1 -1
  68. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +1 -1
  69. package/corpus/core/src/client/onboarding/OnboardingBanner.tsx +2 -2
  70. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +23 -19
  71. package/corpus/core/src/client/resources/ResourceEditor.tsx +18 -10
  72. package/corpus/core/src/client/resources/ResourcesPanel.tsx +5 -1
  73. package/corpus/core/src/client/settings/SettingsPanel.tsx +8 -3
  74. package/corpus/core/src/client/settings/useBuilderStatus.ts +8 -41
  75. package/corpus/core/src/client/useProductionAgent.ts +6 -2
  76. package/corpus/core/src/deploy/build.ts +1 -1
  77. package/corpus/core/src/observability/traces.ts +3 -3
  78. package/corpus/core/src/onboarding/default-steps.ts +1 -1
  79. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +1 -1
  80. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +1 -1
  81. package/corpus/core/src/server/agent-chat-plugin.ts +177 -81
  82. package/corpus/core/src/server/core-routes-plugin.ts +7 -1
  83. package/corpus/core/src/server/open-route.ts +59 -40
  84. package/corpus/core/src/shared/reasoning-effort.ts +2 -0
  85. package/corpus/core/src/sharing/access.ts +22 -2
  86. package/corpus/core/src/sharing/registry.ts +18 -0
  87. package/corpus/core/src/styles/agent-native.css +104 -20
  88. package/corpus/core/src/styles/rich-markdown-editor.css +11 -10
  89. package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
  90. package/corpus/templates/analytics/app/components/dashboard/CumulativeNetChart.tsx +10 -10
  91. package/corpus/templates/analytics/app/components/dashboard/RevenueChart.tsx +10 -10
  92. package/corpus/templates/analytics/app/components/dashboard/RevenueComparisonChart.tsx +6 -5
  93. package/corpus/templates/analytics/app/components/dashboard/TimeSeriesChart.tsx +9 -9
  94. package/corpus/templates/analytics/app/global.css +4 -0
  95. package/corpus/templates/analytics/app/lib/chart-theme.ts +13 -0
  96. package/corpus/templates/analytics/app/pages/adhoc/_shared/KpiChart.tsx +22 -24
  97. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-charts-now-use-theme-aware-tooltip-and-grid-colors.md +6 -0
  98. package/corpus/templates/analytics/changelog/2026-06-30-fixed-ask-composer-spacing-so-the-chat-field-has-a-comfortab.md +6 -0
  99. package/corpus/templates/calendar/AGENTS.md +4 -0
  100. package/corpus/templates/calendar/actions/connect-google-calendar.ts +80 -0
  101. package/corpus/templates/calendar/changelog/2026-06-30-calendar-now-gives-the-agent-a-safe-google-connection-link-i.md +6 -0
  102. package/corpus/templates/calendar/server/plugins/agent-chat.ts +4 -0
  103. package/corpus/templates/content/.env.local.example +6 -0
  104. package/corpus/templates/content/AGENTS.md +10 -1
  105. package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
  106. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
  107. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
  108. package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
  109. package/corpus/templates/content/actions/_database-utils.ts +41 -0
  110. package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
  111. package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
  112. package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
  113. package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
  114. package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
  115. package/corpus/templates/content/actions/list-content-databases.ts +41 -33
  116. package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
  117. package/corpus/templates/content/actions/update-document.ts +93 -0
  118. package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
  119. package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
  120. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +5 -5
  121. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +45 -4
  122. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
  123. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
  124. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
  125. package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
  126. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
  127. package/corpus/templates/content/app/global.css +16 -8
  128. package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
  129. package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
  130. package/corpus/templates/content/app/i18n-data.ts +212 -0
  131. package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
  132. package/corpus/templates/content/app/root.tsx +236 -8
  133. package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
  134. package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
  135. package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
  136. package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
  137. package/corpus/templates/content/changelog/2026-06-30-editor-floating-toolbars-now-use-theme-aware-colors-in-light.md +6 -0
  138. package/corpus/templates/content/package.json +3 -1
  139. package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
  140. package/corpus/templates/content/scripts/dev-database.mjs +83 -0
  141. package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
  142. package/corpus/templates/content/server/db/schema.ts +27 -0
  143. package/corpus/templates/content/server/plugins/db.ts +33 -0
  144. package/corpus/templates/content/shared/api.ts +45 -0
  145. package/corpus/templates/content/shared/builder-mdx.ts +905 -5
  146. package/corpus/templates/content/shared/nfm-registry.ts +2 -0
  147. package/corpus/templates/content/shared/source-component-block.ts +141 -0
  148. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +6 -1
  149. package/corpus/templates/design/actions/add-localhost-screens.ts +63 -8
  150. package/corpus/templates/design/actions/apply-motion-edit.ts +12 -46
  151. package/corpus/templates/design/actions/list-design-native-assets.ts +2 -1
  152. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +49 -1
  153. package/corpus/templates/design/actions/navigate.ts +3 -12
  154. package/corpus/templates/design/actions/open-visual-edit.ts +341 -0
  155. package/corpus/templates/design/actions/view-screen.ts +1 -1
  156. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +1 -1
  157. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +350 -15
  158. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +71 -91
  159. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +25 -27
  160. package/corpus/templates/design/app/components/design/DeviceFrame.tsx +1 -1
  161. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +2 -2
  162. package/corpus/templates/design/app/components/design/EditPanel.tsx +1111 -570
  163. package/corpus/templates/design/app/components/design/LayersPanel.tsx +162 -32
  164. package/corpus/templates/design/app/components/design/MotionDock.tsx +268 -240
  165. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1193 -207
  166. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +1 -1
  167. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +81 -102
  168. package/corpus/templates/design/app/components/design/StatesPanel.tsx +6 -6
  169. package/corpus/templates/design/app/components/design/TokensPanel.tsx +11 -11
  170. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +9 -7
  171. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1225 -154
  172. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +130 -37
  173. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +24 -11
  174. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +184 -162
  175. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +137 -79
  176. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +13 -13
  177. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +8 -8
  178. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +2 -2
  179. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +4 -4
  180. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +3 -3
  181. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +13 -7
  182. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +14 -11
  183. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +4 -4
  184. package/corpus/templates/design/app/components/design/inspector/index.ts +2 -0
  185. package/corpus/templates/design/app/components/design/types.ts +19 -0
  186. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +61 -78
  187. package/corpus/templates/design/app/components/layout/Header.tsx +0 -1
  188. package/corpus/templates/design/app/components/layout/Layout.tsx +3 -2
  189. package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
  190. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +5 -4
  191. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  192. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +2 -2
  193. package/corpus/templates/design/app/global.css +56 -0
  194. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -5
  195. package/corpus/templates/design/app/hooks/use-navigation-state.ts +6 -7
  196. package/corpus/templates/design/app/hooks/use-question-flow.ts +3 -2
  197. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +6 -4
  198. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
  199. package/corpus/templates/design/app/i18n/zh-TW.ts +5 -14
  200. package/corpus/templates/design/app/i18n-data.ts +63 -188
  201. package/corpus/templates/design/app/lib/agent-chat.ts +13 -0
  202. package/corpus/templates/design/app/pages/DesignEditor.tsx +1755 -401
  203. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -5
  204. package/corpus/templates/design/app/pages/DesignSystems.tsx +2 -2
  205. package/corpus/templates/design/app/pages/Index.tsx +49 -26
  206. package/corpus/templates/design/app/pages/NotFound.tsx +25 -8
  207. package/corpus/templates/design/app/pages/VisualEdit.tsx +17 -5
  208. package/corpus/templates/design/app/routes/examples.tsx +1 -7
  209. package/corpus/templates/design/app/routes/templates.tsx +7 -4
  210. package/corpus/templates/design/changelog/2026-06-30-board-shapes-and-auto-layout-controls-are-clearer.md +6 -0
  211. package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
  212. package/corpus/templates/design/changelog/2026-06-30-component-selections-now-use-purple-component-chrome-and-sho.md +6 -0
  213. package/corpus/templates/design/changelog/2026-06-30-constraints-now-stay-tucked-into-a-compact-inspector-preview.md +6 -0
  214. package/corpus/templates/design/changelog/2026-06-30-creating-a-component-now-opens-in-a-compact-inspector-popove.md +6 -0
  215. package/corpus/templates/design/changelog/2026-06-30-design-cards-no-longer-show-legacy-project-type-badges.md +6 -0
  216. package/corpus/templates/design/changelog/2026-06-30-design-chat-now-keeps-new-turns-in-the-visible-design-agent-.md +6 -0
  217. package/corpus/templates/design/changelog/2026-06-30-design-editor-panels-now-keep-their-compact-text-sizing-cons.md +6 -0
  218. package/corpus/templates/design/changelog/2026-06-30-design-no-longer-shows-the-connected-local-code-warning-bann.md +6 -0
  219. package/corpus/templates/design/changelog/2026-06-30-design-now-opens-to-the-files-panel-unless-a-generation-is-a.md +6 -0
  220. package/corpus/templates/design/changelog/2026-06-30-design-opens-with-a-cleaner-empty-canvas-and-loading-preview.md +6 -0
  221. package/corpus/templates/design/changelog/2026-06-30-design-tools-use-simpler-less-decorative-icons.md +6 -0
  222. package/corpus/templates/design/changelog/2026-06-30-dragging-absolute-elements-no-longer-shows-coordinate-labels.md +6 -0
  223. package/corpus/templates/design/changelog/2026-06-30-fixed-all-screens-opening-too-zoomed-in-by-default.md +6 -0
  224. package/corpus/templates/design/changelog/2026-06-30-fixed-the-public-design-assets-sidebar-so-native-components-.md +6 -0
  225. package/corpus/templates/design/changelog/2026-06-30-fixed-the-screen-overview-board-so-empty-board-space-no-long.md +6 -0
  226. package/corpus/templates/design/changelog/2026-06-30-full-view-screens-can-now-scroll-while-remaining-editable.md +6 -0
  227. package/corpus/templates/design/changelog/2026-06-30-inspector-element-headers-show-the-selected-html-tag-and-the.md +6 -0
  228. package/corpus/templates/design/changelog/2026-06-30-inspector-inputs-stay-compact-across-desktop-breakpoints.md +6 -0
  229. package/corpus/templates/design/changelog/2026-06-30-layers-can-be-multi-selected-from-the-canvas-and-edited-toge.md +6 -0
  230. package/corpus/templates/design/changelog/2026-06-30-local-visual-edit-links-can-edit-localhost-frames-without-si.md +6 -0
  231. package/corpus/templates/design/changelog/2026-06-30-localhost-visual-edit-frames-can-now-be-selected-and-edited.md +6 -0
  232. package/corpus/templates/design/changelog/2026-06-30-marquee-selection-now-works-inside-screens-multi-selected-la.md +6 -0
  233. package/corpus/templates/design/changelog/2026-06-30-motion-is-now-available-from-the-persistent-left-rail-and-th.md +6 -0
  234. package/corpus/templates/design/changelog/2026-06-30-motion-timeline-collapse-and-reopen-animations-feel-smoother.md +6 -0
  235. package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +6 -0
  236. package/corpus/templates/design/changelog/2026-06-30-new-board-shapes-now-start-with-neutral-gray-styling.md +6 -0
  237. package/corpus/templates/design/changelog/2026-06-30-new-designs-open-into-a-steadier-loading-state-before-the-ed.md +6 -0
  238. package/corpus/templates/design/changelog/2026-06-30-new-designs-show-a-loading-state-during-handoff-and-keep-edi.md +6 -0
  239. package/corpus/templates/design/changelog/2026-06-30-public-visual-edit-design-links-now-open-directly-instead-of.md +6 -0
  240. package/corpus/templates/design/changelog/2026-06-30-removed-the-confusing-states-section-from-the-design-editor-.md +6 -0
  241. package/corpus/templates/design/changelog/2026-06-30-removed-the-templates-gallery-from-design-navigation.md +6 -0
  242. package/corpus/templates/design/changelog/2026-06-30-share-stays-visible-as-soon-as-editable-designs-load.md +6 -0
  243. package/corpus/templates/design/changelog/2026-06-30-signed-out-save-and-share-controls-use-simpler-labels-and-op.md +6 -0
  244. package/corpus/templates/design/changelog/2026-06-30-text-added-on-the-canvas-now-focuses-cleanly-with-the-right-.md +6 -0
  245. package/corpus/templates/design/changelog/2026-06-30-the-design-editor-loading-skeleton-is-simpler-and-its-bottom.md +6 -0
  246. package/corpus/templates/design/changelog/2026-06-30-the-design-editor-loading-skeleton-now-follows-the-current-t.md +6 -0
  247. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-no-longer-shows-child-layout-controls-o.md +6 -0
  248. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-no-longer-shows-the-ask-ai-section-abov.md +6 -0
  249. package/corpus/templates/design/changelog/2026-06-30-the-design-review-panel-no-longer-shows-a-separate-inline-au.md +6 -0
  250. package/corpus/templates/design/changelog/2026-06-30-the-empty-designs-page-now-focuses-on-a-single-primary-new-d.md +6 -0
  251. package/corpus/templates/design/changelog/2026-06-30-the-layers-panel-header-stays-as-layers-while-multiple-items.md +6 -0
  252. package/corpus/templates/design/changelog/2026-06-30-the-missing-design-screen-now-uses-a-neutral-back-button.md +6 -0
  253. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-now-closes-without-squeezing-the-canvas-.md +6 -0
  254. package/corpus/templates/design/changelog/2026-06-30-the-new-design-connect-ai-setup-now-lines-up-with-the-compos.md +6 -0
  255. package/corpus/templates/design/changelog/2026-06-30-the-screen-overview-board-now-blends-into-the-canvas-and-new.md +6 -0
  256. package/corpus/templates/design/changelog/2026-06-30-top-level-canvas-drops-now-show-screen-guides.md +6 -0
  257. package/corpus/templates/design/changelog/2026-06-30-top-level-canvas-layers-now-keep-a-transparent-backdrop-and-.md +6 -0
  258. package/corpus/templates/design/changelog/2026-06-30-top-level-canvas-layers-now-select-reliably-from-all-screens.md +6 -0
  259. package/corpus/templates/design/changelog/2026-06-30-visual-edit-can-open-and-refresh-localhost-screens-in-one-au.md +6 -0
  260. package/corpus/templates/design/e2e/helpers.ts +15 -4
  261. package/corpus/templates/design/server/db/index.ts +52 -0
  262. package/corpus/templates/design/server/plugins/agent-chat.ts +5 -0
  263. package/corpus/templates/design/server/plugins/auth.ts +1 -0
  264. package/corpus/templates/design/server/plugins/core-routes.ts +4 -0
  265. package/corpus/templates/design/shared/board-file.ts +380 -5
  266. package/corpus/templates/design/shared/board-objects.ts +2 -2
  267. package/corpus/templates/design/shared/code-layer.ts +50 -6
  268. package/corpus/templates/design/shared/motion-compiler.ts +27 -0
  269. package/corpus/templates/macros/app/components/DailyProgress.tsx +41 -37
  270. package/corpus/templates/macros/app/components/ExerciseCard.tsx +2 -2
  271. package/corpus/templates/macros/app/components/MealCard.tsx +2 -2
  272. package/corpus/templates/macros/app/components/VoiceDictation.tsx +1 -1
  273. package/corpus/templates/macros/app/components/WeeklyCaloriesChart.tsx +1 -1
  274. package/corpus/templates/macros/app/components/WeightCard.tsx +2 -2
  275. package/corpus/templates/macros/app/components/WeightTracker.tsx +1 -1
  276. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -1
  277. package/corpus/templates/macros/app/routes/_index.tsx +5 -5
  278. package/corpus/templates/macros/app/routes/analytics.tsx +1 -1
  279. package/corpus/templates/macros/changelog/2026-06-30-dashboard-loading-and-chart-surfaces-now-use-theme-aware-col.md +6 -0
  280. package/corpus/templates/mail/app/components/email/ComposeBubbleToolbar.tsx +14 -11
  281. package/corpus/templates/mail/app/global.css +3 -2
  282. package/corpus/templates/mail/changelog/2026-06-30-compose-floating-toolbars-now-use-theme-aware-colors-in-ligh.md +6 -0
  283. package/corpus/templates/plan/app/pages/PlansPage.tsx +2 -2
  284. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +3 -3
  285. package/corpus/templates/slides/app/global.css +10 -0
  286. package/corpus/templates/slides/app/pages/Index.tsx +55 -51
  287. package/corpus/templates/slides/changelog/2026-06-30-deck-thumbnails-collapse-cleanly-to-one-column-when-the-chat.md +6 -0
  288. package/dist/agent/engine/anthropic-engine.d.ts +2 -2
  289. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  290. package/dist/agent/engine/builder-engine.d.ts +2 -2
  291. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  292. package/dist/agent/engine/builtin.js +1 -1
  293. package/dist/agent/engine/builtin.js.map +1 -1
  294. package/dist/agent/engine/output-tokens.js +1 -1
  295. package/dist/agent/engine/output-tokens.js.map +1 -1
  296. package/dist/agent/harness/acp-adapter.d.ts +4 -1
  297. package/dist/agent/harness/acp-adapter.d.ts.map +1 -1
  298. package/dist/agent/harness/acp-adapter.js +19 -2
  299. package/dist/agent/harness/acp-adapter.js.map +1 -1
  300. package/dist/agent/harness/ai-sdk-adapter.d.ts.map +1 -1
  301. package/dist/agent/harness/ai-sdk-adapter.js +3 -0
  302. package/dist/agent/harness/ai-sdk-adapter.js.map +1 -1
  303. package/dist/agent/harness/translate.d.ts.map +1 -1
  304. package/dist/agent/harness/translate.js +3 -0
  305. package/dist/agent/harness/translate.js.map +1 -1
  306. package/dist/agent/harness/types.d.ts +1 -0
  307. package/dist/agent/harness/types.d.ts.map +1 -1
  308. package/dist/agent/harness/types.js.map +1 -1
  309. package/dist/agent/model-config.d.ts +21 -20
  310. package/dist/agent/model-config.d.ts.map +1 -1
  311. package/dist/agent/model-config.js +31 -13
  312. package/dist/agent/model-config.js.map +1 -1
  313. package/dist/agent/production-agent.d.ts.map +1 -1
  314. package/dist/agent/production-agent.js +85 -10
  315. package/dist/agent/production-agent.js.map +1 -1
  316. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  317. package/dist/agent/thread-data-builder.js +27 -6
  318. package/dist/agent/thread-data-builder.js.map +1 -1
  319. package/dist/agent/tool-call-journal.d.ts +11 -9
  320. package/dist/agent/tool-call-journal.d.ts.map +1 -1
  321. package/dist/agent/tool-call-journal.js +92 -40
  322. package/dist/agent/tool-call-journal.js.map +1 -1
  323. package/dist/agent/types.d.ts +5 -1
  324. package/dist/agent/types.d.ts.map +1 -1
  325. package/dist/agent/types.js.map +1 -1
  326. package/dist/cli/design-connect.d.ts.map +1 -1
  327. package/dist/cli/design-connect.js +84 -0
  328. package/dist/cli/design-connect.js.map +1 -1
  329. package/dist/cli/gateway-helpers.d.ts +4 -0
  330. package/dist/cli/gateway-helpers.d.ts.map +1 -1
  331. package/dist/cli/gateway-helpers.js +9 -0
  332. package/dist/cli/gateway-helpers.js.map +1 -1
  333. package/dist/cli/recap.d.ts.map +1 -1
  334. package/dist/cli/recap.js +0 -2
  335. package/dist/cli/recap.js.map +1 -1
  336. package/dist/cli/skills.d.ts.map +1 -1
  337. package/dist/cli/skills.js +54 -16
  338. package/dist/cli/skills.js.map +1 -1
  339. package/dist/cli/workspace-dev.d.ts.map +1 -1
  340. package/dist/cli/workspace-dev.js +33 -6
  341. package/dist/cli/workspace-dev.js.map +1 -1
  342. package/dist/client/AssistantChat.d.ts.map +1 -1
  343. package/dist/client/AssistantChat.js +72 -26
  344. package/dist/client/AssistantChat.js.map +1 -1
  345. package/dist/client/CommandMenu.d.ts +6 -2
  346. package/dist/client/CommandMenu.d.ts.map +1 -1
  347. package/dist/client/CommandMenu.js +25 -14
  348. package/dist/client/CommandMenu.js.map +1 -1
  349. package/dist/client/DefaultSpinner.d.ts.map +1 -1
  350. package/dist/client/DefaultSpinner.js +8 -1
  351. package/dist/client/DefaultSpinner.js.map +1 -1
  352. package/dist/client/chat/message-components.d.ts +7 -1
  353. package/dist/client/chat/message-components.d.ts.map +1 -1
  354. package/dist/client/chat/message-components.js +37 -1
  355. package/dist/client/chat/message-components.js.map +1 -1
  356. package/dist/client/chat/repo-helpers.d.ts +1 -0
  357. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  358. package/dist/client/chat/repo-helpers.js +59 -0
  359. package/dist/client/chat/repo-helpers.js.map +1 -1
  360. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  361. package/dist/client/chat/run-recovery.js +4 -4
  362. package/dist/client/chat/run-recovery.js.map +1 -1
  363. package/dist/client/components/ApiKeySettings.js +8 -8
  364. package/dist/client/components/ApiKeySettings.js.map +1 -1
  365. package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
  366. package/dist/client/components/CodeRequiredDialog.js +24 -15
  367. package/dist/client/components/CodeRequiredDialog.js.map +1 -1
  368. package/dist/client/components/MissingKeyCard.d.ts.map +1 -1
  369. package/dist/client/components/MissingKeyCard.js +11 -6
  370. package/dist/client/components/MissingKeyCard.js.map +1 -1
  371. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  372. package/dist/client/composer/TiptapComposer.js +4 -3
  373. package/dist/client/composer/TiptapComposer.js.map +1 -1
  374. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  375. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  376. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  377. package/dist/client/guided-questions.d.ts.map +1 -1
  378. package/dist/client/guided-questions.js +6 -6
  379. package/dist/client/guided-questions.js.map +1 -1
  380. package/dist/client/integrations/IntegrationCard.js +1 -1
  381. package/dist/client/integrations/IntegrationCard.js.map +1 -1
  382. package/dist/client/integrations/IntegrationsPanel.js +1 -1
  383. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  384. package/dist/client/onboarding/OnboardingBanner.js +2 -2
  385. package/dist/client/onboarding/OnboardingBanner.js.map +1 -1
  386. package/dist/client/onboarding/OnboardingPanel.js +23 -19
  387. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  388. package/dist/client/resources/ResourceEditor.d.ts.map +1 -1
  389. package/dist/client/resources/ResourceEditor.js +13 -10
  390. package/dist/client/resources/ResourceEditor.js.map +1 -1
  391. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  392. package/dist/client/resources/ResourcesPanel.js +2 -1
  393. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  394. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  395. package/dist/client/settings/SettingsPanel.js +8 -3
  396. package/dist/client/settings/SettingsPanel.js.map +1 -1
  397. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  398. package/dist/client/settings/useBuilderStatus.js +6 -38
  399. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  400. package/dist/client/useProductionAgent.d.ts +2 -1
  401. package/dist/client/useProductionAgent.d.ts.map +1 -1
  402. package/dist/client/useProductionAgent.js.map +1 -1
  403. package/dist/deploy/build.js +1 -1
  404. package/dist/deploy/build.js.map +1 -1
  405. package/dist/notifications/routes.d.ts +3 -3
  406. package/dist/observability/routes.d.ts +3 -3
  407. package/dist/observability/traces.d.ts.map +1 -1
  408. package/dist/observability/traces.js.map +1 -1
  409. package/dist/onboarding/default-steps.js +1 -1
  410. package/dist/onboarding/default-steps.js.map +1 -1
  411. package/dist/scripts/agent-engines/manage-agent-engine.js +1 -1
  412. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  413. package/dist/scripts/agent-engines/set-agent-engine.js +1 -1
  414. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  415. package/dist/server/agent-chat-plugin.d.ts +3 -0
  416. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  417. package/dist/server/agent-chat-plugin.js +134 -72
  418. package/dist/server/agent-chat-plugin.js.map +1 -1
  419. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  420. package/dist/server/core-routes-plugin.d.ts +3 -0
  421. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  422. package/dist/server/core-routes-plugin.js +4 -1
  423. package/dist/server/core-routes-plugin.js.map +1 -1
  424. package/dist/server/open-route.d.ts +9 -0
  425. package/dist/server/open-route.d.ts.map +1 -1
  426. package/dist/server/open-route.js +48 -38
  427. package/dist/server/open-route.js.map +1 -1
  428. package/dist/shared/reasoning-effort.js +3 -0
  429. package/dist/shared/reasoning-effort.js.map +1 -1
  430. package/dist/sharing/access.d.ts.map +1 -1
  431. package/dist/sharing/access.js +18 -2
  432. package/dist/sharing/access.js.map +1 -1
  433. package/dist/sharing/registry.d.ts +10 -0
  434. package/dist/sharing/registry.d.ts.map +1 -1
  435. package/dist/sharing/registry.js.map +1 -1
  436. package/dist/styles/agent-native.css +104 -20
  437. package/dist/styles/rich-markdown-editor.css +11 -10
  438. package/docs/content/cloneable-saas.mdx +2 -2
  439. package/docs/content/faq.mdx +1 -1
  440. package/docs/content/locales/ar-SA/cloneable-saas.mdx +2 -2
  441. package/docs/content/locales/ar-SA/faq.mdx +1 -1
  442. package/docs/content/locales/ar-SA/template-chat.mdx +1 -1
  443. package/docs/content/locales/de-DE/cloneable-saas.mdx +2 -2
  444. package/docs/content/locales/de-DE/faq.mdx +1 -1
  445. package/docs/content/locales/de-DE/template-chat.mdx +1 -1
  446. package/docs/content/locales/es-ES/cloneable-saas.mdx +2 -2
  447. package/docs/content/locales/es-ES/faq.mdx +1 -1
  448. package/docs/content/locales/es-ES/template-chat.mdx +1 -1
  449. package/docs/content/locales/fr-FR/cloneable-saas.mdx +2 -2
  450. package/docs/content/locales/fr-FR/faq.mdx +1 -1
  451. package/docs/content/locales/fr-FR/template-chat.mdx +1 -1
  452. package/docs/content/locales/hi-IN/cloneable-saas.mdx +2 -2
  453. package/docs/content/locales/hi-IN/faq.mdx +1 -1
  454. package/docs/content/locales/hi-IN/template-chat.mdx +1 -1
  455. package/docs/content/locales/ja-JP/cloneable-saas.mdx +2 -2
  456. package/docs/content/locales/ja-JP/faq.mdx +1 -1
  457. package/docs/content/locales/ja-JP/template-chat.mdx +1 -1
  458. package/docs/content/locales/ko-KR/cloneable-saas.mdx +2 -2
  459. package/docs/content/locales/ko-KR/faq.mdx +1 -1
  460. package/docs/content/locales/ko-KR/template-chat.mdx +1 -1
  461. package/docs/content/locales/pt-BR/cloneable-saas.mdx +2 -2
  462. package/docs/content/locales/pt-BR/faq.mdx +1 -1
  463. package/docs/content/locales/pt-BR/template-chat.mdx +1 -1
  464. package/docs/content/locales/zh-CN/cloneable-saas.mdx +2 -2
  465. package/docs/content/locales/zh-CN/faq.mdx +1 -1
  466. package/docs/content/locales/zh-CN/template-chat.mdx +1 -1
  467. package/docs/content/locales/zh-TW/cloneable-saas.mdx +2 -2
  468. package/docs/content/locales/zh-TW/faq.mdx +1 -1
  469. package/docs/content/locales/zh-TW/template-chat.mdx +1 -1
  470. package/docs/content/template-chat.mdx +1 -1
  471. package/package.json +1 -1
  472. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +0 -157
  473. package/corpus/templates/design/app/lib/design-templates.ts +0 -1305
  474. package/corpus/templates/design/app/pages/Templates.tsx +0 -279
@@ -52,11 +52,14 @@ import {
52
52
  DesignCanvas,
53
53
  LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT,
54
54
  appendHitTestResponder,
55
+ type IframeContextMenuPayload,
56
+ type IframeHotkeyPayload,
55
57
  } from "./DesignCanvas";
56
58
  import {
57
59
  DEVICE_FRAME_VIEWPORTS,
58
60
  type DeviceFrameType,
59
61
  type ElementInfo,
62
+ type ElementSelectionIntent,
60
63
  } from "./types";
61
64
 
62
65
  // Re-export so consumers of MultiScreenCanvas can use the same script without
@@ -78,6 +81,7 @@ interface ScreenFile {
78
81
  height?: number;
79
82
  url?: string;
80
83
  previewUrl?: string;
84
+ bridgeUrl?: string;
81
85
  /**
82
86
  * When set, renders multiple side-by-side breakpoint frames (mobile-first,
83
87
  * §6.4). Each entry is a pixel width; the active breakpoint determines the
@@ -90,7 +94,7 @@ interface ScreenFile {
90
94
 
91
95
  type ScreenSourceType = "localhost" | "fusion" | "inline";
92
96
  type ScreenPreviewState = "live" | "snapshot" | "preview";
93
- type MultiScreenCanvasTool =
97
+ export type MultiScreenCanvasTool =
94
98
  | "move"
95
99
  | "frame"
96
100
  | "rect"
@@ -142,6 +146,7 @@ interface ScreenMetadata {
142
146
  height?: number;
143
147
  url?: string;
144
148
  previewUrl?: string;
149
+ bridgeUrl?: string;
145
150
  }
146
151
 
147
152
  interface DuplicateRequest {
@@ -213,6 +218,11 @@ interface MultiScreenCanvasProps {
213
218
  widthPx: number | undefined,
214
219
  ) => void;
215
220
  onSelectionChange?: (selectedIds: string[]) => void;
221
+ onLayerMarqueeSelectionChange?: (
222
+ selection: CanvasLayerMarqueeSelection[],
223
+ intent: ElementSelectionIntent,
224
+ ) => void;
225
+ selectedLayerSelectorGroupsByScreen?: Record<string, string[][]>;
216
226
  /**
217
227
  * Called when the user drags an element out of the active screen's iframe
218
228
  * and drops it onto a different screen. The bridge in the source iframe
@@ -231,6 +241,12 @@ interface MultiScreenCanvasProps {
231
241
  targetAnchorNodeId?: string;
232
242
  /** DOM insertion placement relative to the anchor node. */
233
243
  targetAnchorPlacement?: "before" | "after" | "inside";
244
+ /** Final drop point in logical overview canvas coordinates. */
245
+ targetCanvasPoint?: Point;
246
+ /** Final drop point in the destination iframe/content coordinate space. */
247
+ targetLocalPoint?: Point;
248
+ /** Pointer offset from the dragged element's top-left in source iframe px. */
249
+ sourcePointerOffset?: Point;
234
250
  }) => void;
235
251
  // ── Board file (new model) ───────────────────────────────────────────────
236
252
  /**
@@ -264,16 +280,51 @@ interface MultiScreenCanvasProps {
264
280
  * Pass `canEditDesign` from DesignEditor. Defaults to false.
265
281
  */
266
282
  boardEditMode?: boolean;
283
+ /**
284
+ * When true the board is the active surface (activeFileId === boardFileId),
285
+ * so the board <DesignCanvas> owns the global window runtime bridge
286
+ * (`registerRuntimeBridge={boardIsActive}`). This mirrors how the active
287
+ * screen owns the bridge in single/overview mode: at most one surface
288
+ * registers the global `window.__designCanvas*` helpers at a time
289
+ * (active screen XOR active board, since `activeFileId` is exclusive), so
290
+ * in-place ops — delete removal, begin-text-edit — reach the board exactly
291
+ * like a screen. DesignEditor passes `activeFileId === boardFileId`.
292
+ * Defaults to false.
293
+ */
294
+ boardIsActive?: boolean;
267
295
  /**
268
296
  * Called when the user selects an element on the board surface.
269
297
  * DesignEditor should set boardFileId as the active file and push the
270
298
  * selection to the inspector.
271
299
  */
272
- onBoardElementSelect?: (info: ElementInfo) => void;
300
+ onBoardElementSelect?: (
301
+ info: ElementInfo,
302
+ intent?: ElementSelectionIntent,
303
+ ) => void;
304
+ onBoardElementMarqueeSelect?: (
305
+ infos: ElementInfo[],
306
+ intent?: ElementSelectionIntent,
307
+ ) => void;
273
308
  /**
274
309
  * Called when the user hovers an element on the board surface.
275
310
  */
276
311
  onBoardElementHover?: (info: ElementInfo | null) => void;
312
+ onBoardElementClear?: () => void;
313
+ onBoardElementDblClickText?: (info: ElementInfo) => void;
314
+ onBoardIframeHotkey?: (event: IframeHotkeyPayload) => void;
315
+ onBoardIframeContextMenu?: (event: IframeContextMenuPayload) => void;
316
+ onBoardTextEditingStateChange?: (state: {
317
+ active: boolean;
318
+ selector?: string;
319
+ hasRange?: boolean;
320
+ }) => void;
321
+ boardClearSelectionRequest?: number;
322
+ boardSelectedSelector?: string | null;
323
+ boardSelectedSelectorCandidates?: string[];
324
+ boardHoveredSelector?: string | null;
325
+ boardHoveredSelectorCandidates?: string[];
326
+ boardLockedSelectors?: string[];
327
+ boardHiddenSelectors?: string[];
277
328
  /**
278
329
  * Called when a drag / reorder / reparent / drop-into-container or delete
279
330
  * occurs on a board element. Target file is boardFileId.
@@ -391,6 +442,245 @@ export function isDirectScreenHoverTarget(
391
442
  return !!element && !element.closest("[data-screen-content]");
392
443
  }
393
444
 
445
+ export function shouldBoardSurfaceCapturePointerEvents(args: {
446
+ tool: MultiScreenCanvasTool | string;
447
+ gestureActive?: boolean;
448
+ }) {
449
+ if (args.gestureActive) return false;
450
+ const tool = normalizeCanvasTool(args.tool as MultiScreenCanvasTool);
451
+ return (
452
+ !getDraftCreationTool(tool) &&
453
+ tool !== "hand" &&
454
+ tool !== "comment" &&
455
+ tool !== "draw"
456
+ );
457
+ }
458
+
459
+ export function getBoardSurfaceLayerStyle(args: {
460
+ geometry: FrameGeometry;
461
+ interactive: boolean;
462
+ }): CSSProperties {
463
+ return {
464
+ position: "absolute",
465
+ left: SURFACE_PADDING + args.geometry.x,
466
+ top: SURFACE_PADDING + args.geometry.y,
467
+ width: args.geometry.width,
468
+ height: args.geometry.height,
469
+ overflow: "hidden",
470
+ pointerEvents: args.interactive ? "auto" : "none",
471
+ background: "transparent",
472
+ zIndex: 0,
473
+ };
474
+ }
475
+
476
+ export function hasBoardSurfaceContent(html: string | undefined) {
477
+ if (!html) return false;
478
+ const bodyMatch = html.match(/<body\b[^>]*>([\s\S]*?)<\/body>/i);
479
+ const content = bodyMatch?.[1] ?? html;
480
+ return content.replace(/<!--[\s\S]*?-->/g, "").trim().length > 0;
481
+ }
482
+
483
+ const BOARD_SURFACE_RENDER_STYLE = `<style data-agent-native-board-surface-render>html,body{background:transparent!important;background-color:transparent!important;background-image:none!important;}body{margin:0!important;position:relative;overflow:visible;}body>:not([data-agent-native-node-id]):not(style):not(script),body>[data-agent-native-node-id]:not([data-an-primitive]):has([data-agent-native-node-id]),body>[data-agent-native-node-id="body"],body>[data-agent-native-node-id="Body"],body>[data-agent-native-layer-name="body"],body>[data-agent-native-layer-name="Body"],body>[data-agent-native-layer-name="<body>"]{background:transparent!important;background-color:transparent!important;background-image:none!important;box-shadow:none!important;}[data-agent-native-board-backdrop-candidate="true"]{display:none!important;pointer-events:none!important;}</style>`;
484
+ const BOARD_SURFACE_BACKGROUND = "hsl(0 0% 10%)";
485
+
486
+ const BOARD_SURFACE_BACKDROP_MIN_EDGE_PX = 2400;
487
+ const BOARD_SURFACE_BACKDROP_MIN_AREA_PX = 8_000_000;
488
+ const HTML_VOID_TAGS = new Set([
489
+ "area",
490
+ "base",
491
+ "br",
492
+ "col",
493
+ "embed",
494
+ "hr",
495
+ "img",
496
+ "input",
497
+ "link",
498
+ "meta",
499
+ "param",
500
+ "source",
501
+ "track",
502
+ "wbr",
503
+ ]);
504
+ const HTML_TAG_RE = /<!--[\s\S]*?-->|<\/?([a-zA-Z][\w:-]*)([^<>]*?)\/?>/g;
505
+
506
+ function getHtmlAttributeValue(tag: string, name: string) {
507
+ const match = tag.match(
508
+ new RegExp(
509
+ `\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'=<>]+))`,
510
+ "i",
511
+ ),
512
+ );
513
+ return match?.[1] ?? match?.[2] ?? match?.[3] ?? "";
514
+ }
515
+
516
+ function getCssDeclarationValue(style: string, name: string) {
517
+ const match = style.match(
518
+ new RegExp(`(?:^|;)\\s*${name}\\s*:\\s*([^;]+)`, "i"),
519
+ );
520
+ return match?.[1]?.trim() ?? "";
521
+ }
522
+
523
+ function getCssPixelValue(style: string, name: string) {
524
+ const value = getCssDeclarationValue(style, name);
525
+ const match = value.match(/^(-?\d+(?:\.\d+)?)px$/i);
526
+ if (!match?.[1]) return null;
527
+ const parsed = Number(match[1]);
528
+ return Number.isFinite(parsed) ? parsed : null;
529
+ }
530
+
531
+ function parseCssColor(value: string) {
532
+ const trimmed = value.trim().toLowerCase();
533
+ if (!trimmed || trimmed === "transparent") return null;
534
+ const rgb = trimmed.match(
535
+ /^rgba?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)(?:\s*,\s*(\d+(?:\.\d+)?))?\s*\)$/,
536
+ );
537
+ if (rgb?.[1] && rgb[2] && rgb[3]) {
538
+ const alpha = rgb[4] === undefined ? 1 : Number(rgb[4]);
539
+ if (alpha <= 0) return null;
540
+ return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])] as const;
541
+ }
542
+ const hex = trimmed.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
543
+ if (!hex?.[1]) return null;
544
+ const raw = hex[1];
545
+ const full =
546
+ raw.length === 3
547
+ ? raw
548
+ .split("")
549
+ .map((part) => part + part)
550
+ .join("")
551
+ : raw;
552
+ return [
553
+ Number.parseInt(full.slice(0, 2), 16),
554
+ Number.parseInt(full.slice(2, 4), 16),
555
+ Number.parseInt(full.slice(4, 6), 16),
556
+ ] as const;
557
+ }
558
+
559
+ function isNeutralBackdropColor(value: string) {
560
+ const color = parseCssColor(value);
561
+ if (!color) return false;
562
+ const max = Math.max(...color);
563
+ const min = Math.min(...color);
564
+ return min >= 180 && max - min <= 24;
565
+ }
566
+
567
+ function isAccidentalBoardBackdropTag(tag: string) {
568
+ if (
569
+ getHtmlAttributeValue(tag, "data-agent-native-board-backdrop-candidate")
570
+ ) {
571
+ return false;
572
+ }
573
+ const primitive = getHtmlAttributeValue(
574
+ tag,
575
+ "data-an-primitive",
576
+ ).toLowerCase();
577
+ if (primitive !== "rectangle" && primitive !== "rect") return false;
578
+ const style = getHtmlAttributeValue(tag, "style");
579
+ if (!style) return false;
580
+ const width = getCssPixelValue(style, "width");
581
+ const height = getCssPixelValue(style, "height");
582
+ if (width === null || height === null) return false;
583
+ if (
584
+ width < BOARD_SURFACE_BACKDROP_MIN_EDGE_PX ||
585
+ height < BOARD_SURFACE_BACKDROP_MIN_EDGE_PX ||
586
+ width * height < BOARD_SURFACE_BACKDROP_MIN_AREA_PX
587
+ ) {
588
+ return false;
589
+ }
590
+ const background =
591
+ getCssDeclarationValue(style, "background-color") ||
592
+ getCssDeclarationValue(style, "background");
593
+ return isNeutralBackdropColor(background);
594
+ }
595
+
596
+ function markAccidentalBoardBackdropCandidates(html: string) {
597
+ return html.replace(HTML_TAG_RE, (tag: string, tagName?: string) => {
598
+ if (!tagName || tag.startsWith("</")) return tag;
599
+ if (!isAccidentalBoardBackdropTag(tag)) return tag;
600
+ return tag.replace(
601
+ /\/?>$/,
602
+ (ending) => ` data-agent-native-board-backdrop-candidate="true"${ending}`,
603
+ );
604
+ });
605
+ }
606
+
607
+ function findLastHtmlStackTagIndex(
608
+ stack: Array<{ tagName: string; nodeId: string }>,
609
+ tagName: string,
610
+ ) {
611
+ for (let i = stack.length - 1; i >= 0; i--) {
612
+ if (stack[i]?.tagName === tagName) return i;
613
+ }
614
+ return -1;
615
+ }
616
+
617
+ function getCurrentLayerParentNodeId(
618
+ stack: Array<{ tagName: string; nodeId: string }>,
619
+ ) {
620
+ for (let i = stack.length - 1; i >= 0; i--) {
621
+ const nodeId = stack[i]?.nodeId;
622
+ if (nodeId) return nodeId;
623
+ }
624
+ return "body";
625
+ }
626
+
627
+ export function getBoardSurfaceRenderContent(html: string) {
628
+ if (!html) return html;
629
+ const renderHtml = markAccidentalBoardBackdropCandidates(html);
630
+ if (renderHtml.includes("data-agent-native-board-surface-render")) {
631
+ return renderHtml;
632
+ }
633
+ if (/<\/head>/i.test(html)) {
634
+ return renderHtml.replace(
635
+ /<\/head>/i,
636
+ `${BOARD_SURFACE_RENDER_STYLE}</head>`,
637
+ );
638
+ }
639
+ if (/<body\b/i.test(html)) {
640
+ return renderHtml.replace(/<body\b/i, `${BOARD_SURFACE_RENDER_STYLE}<body`);
641
+ }
642
+ return `${BOARD_SURFACE_RENDER_STYLE}${renderHtml}`;
643
+ }
644
+
645
+ export function getBoardContentLayerSignature(html: string) {
646
+ const layers: string[] = [];
647
+ const stack: Array<{ tagName: string; nodeId: string }> = [];
648
+ const childCountsByParent = new Map<string, number>();
649
+
650
+ for (const match of html.matchAll(HTML_TAG_RE)) {
651
+ const token = match[0];
652
+ const tagName = match[1]?.toLowerCase();
653
+ if (!tagName) continue;
654
+
655
+ if (token.startsWith("</")) {
656
+ const index = findLastHtmlStackTagIndex(stack, tagName);
657
+ if (index >= 0) stack.splice(index);
658
+ continue;
659
+ }
660
+
661
+ const nodeId = getHtmlAttributeValue(token, "data-agent-native-node-id");
662
+ if (nodeId) {
663
+ const parentNodeId = getCurrentLayerParentNodeId(stack);
664
+ const childIndex = childCountsByParent.get(parentNodeId) ?? 0;
665
+ childCountsByParent.set(parentNodeId, childIndex + 1);
666
+ layers.push(`${nodeId}<${parentNodeId}#${childIndex}`);
667
+ }
668
+
669
+ const selfClosing = token.endsWith("/>") || HTML_VOID_TAGS.has(tagName);
670
+ if (!selfClosing) stack.push({ tagName, nodeId });
671
+ }
672
+
673
+ return `${layers.length}:${hashString(layers.join("\n"))}`;
674
+ }
675
+
676
+ export function getBoardContentKey(args: {
677
+ boardFileId: string;
678
+ boardFileContent: string;
679
+ boardIsActive: boolean;
680
+ }) {
681
+ return `${args.boardFileId}:layers:${getBoardContentLayerSignature(args.boardFileContent)}`;
682
+ }
683
+
394
684
  function getChromeHandleTransition(chromeSettling: boolean) {
395
685
  return chromeSettling
396
686
  ? CHROME_HANDLE_SETTLE_TRANSITION
@@ -454,6 +744,152 @@ export interface Point {
454
744
  y: number;
455
745
  }
456
746
 
747
+ export type CrossScreenDropPlacement = "before" | "after" | "inside";
748
+ export type CrossScreenDropAxis = "x" | "y";
749
+
750
+ export interface CrossScreenHitTestAnchorRect {
751
+ left: number;
752
+ top: number;
753
+ width: number;
754
+ height: number;
755
+ }
756
+
757
+ export interface CrossScreenHitTestResult {
758
+ anchorNodeId?: string;
759
+ placement?: CrossScreenDropPlacement;
760
+ axis?: CrossScreenDropAxis;
761
+ anchorRect?: CrossScreenHitTestAnchorRect;
762
+ }
763
+
764
+ interface CrossScreenDragElementRect {
765
+ left: number;
766
+ top: number;
767
+ width: number;
768
+ height: number;
769
+ }
770
+
771
+ function isFinitePoint(value: unknown): value is Point {
772
+ if (!value || typeof value !== "object") return false;
773
+ const point = value as Record<string, unknown>;
774
+ return Number.isFinite(point.x) && Number.isFinite(point.y);
775
+ }
776
+
777
+ interface CanvasLayerMarqueeCandidate {
778
+ screenId: string;
779
+ info: ElementInfo;
780
+ geometry: FrameGeometry;
781
+ frameGeometry: FrameGeometry;
782
+ }
783
+
784
+ export interface CanvasLayerMarqueeSelection {
785
+ screenId: string;
786
+ info: ElementInfo;
787
+ }
788
+
789
+ export interface CrossScreenDropGuide {
790
+ placement: CrossScreenDropPlacement;
791
+ axis: CrossScreenDropAxis;
792
+ boardRect: FrameGeometry;
793
+ }
794
+
795
+ function isCrossScreenDropPlacement(
796
+ value: unknown,
797
+ ): value is CrossScreenDropPlacement {
798
+ return value === "before" || value === "after" || value === "inside";
799
+ }
800
+
801
+ function isCrossScreenDropAxis(value: unknown): value is CrossScreenDropAxis {
802
+ return value === "x" || value === "y";
803
+ }
804
+
805
+ function isCrossScreenHitTestAnchorRect(
806
+ value: unknown,
807
+ ): value is CrossScreenHitTestAnchorRect {
808
+ if (!value || typeof value !== "object") return false;
809
+ const rect = value as Record<string, unknown>;
810
+ return (
811
+ Number.isFinite(rect.left) &&
812
+ Number.isFinite(rect.top) &&
813
+ Number.isFinite(rect.width) &&
814
+ Number.isFinite(rect.height)
815
+ );
816
+ }
817
+
818
+ export function getCrossScreenDropGuideForHitTest(args: {
819
+ hit: CrossScreenHitTestResult;
820
+ targetGeometry: FrameGeometry;
821
+ targetMetadata: { width: number; height: number };
822
+ }): CrossScreenDropGuide | null {
823
+ const rect = args.hit.anchorRect;
824
+ if (!rect) return null;
825
+ const placement = args.hit.placement ?? "inside";
826
+ const axis = args.hit.axis ?? "y";
827
+ const scaleX =
828
+ args.targetGeometry.width / Math.max(1, args.targetMetadata.width);
829
+ const scaleY =
830
+ args.targetGeometry.height / Math.max(1, args.targetMetadata.height);
831
+ return {
832
+ placement,
833
+ axis,
834
+ boardRect: {
835
+ x: args.targetGeometry.x + rect.left * scaleX,
836
+ y: args.targetGeometry.y + rect.top * scaleY,
837
+ width: Math.max(1, rect.width * scaleX),
838
+ height: Math.max(1, rect.height * scaleY),
839
+ },
840
+ };
841
+ }
842
+
843
+ export function getCrossScreenDropGuideStyle(args: {
844
+ guide: CrossScreenDropGuide;
845
+ pan: Point;
846
+ scale: number;
847
+ }): CSSProperties {
848
+ const { boardRect, placement, axis } = args.guide;
849
+ const left = args.pan.x + (SURFACE_PADDING + boardRect.x) * args.scale;
850
+ const top = args.pan.y + (SURFACE_PADDING + boardRect.y) * args.scale;
851
+ const width = Math.max(1, boardRect.width * args.scale);
852
+ const height = Math.max(1, boardRect.height * args.scale);
853
+
854
+ if (placement === "inside") {
855
+ return {
856
+ left,
857
+ top,
858
+ width,
859
+ height,
860
+ border: "2px solid var(--design-editor-accent-color)",
861
+ background:
862
+ "color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)",
863
+ borderRadius: 2,
864
+ boxShadow: "none",
865
+ };
866
+ }
867
+
868
+ if (axis === "x") {
869
+ const x = placement === "before" ? left : left + width;
870
+ return {
871
+ left: x - 1,
872
+ top,
873
+ width: 2,
874
+ height: Math.max(8, height),
875
+ background: "var(--design-editor-accent-color)",
876
+ borderRadius: 999,
877
+ boxShadow: "0 0 0 1px var(--design-editor-accent-color)",
878
+ };
879
+ }
880
+
881
+ const y = placement === "before" ? top : top + height;
882
+ return {
883
+ left,
884
+ top: y - 1,
885
+ width: Math.max(8, width),
886
+ height: 2,
887
+ background: "var(--design-editor-accent-color)",
888
+ borderRadius: 999,
889
+ boxShadow: "0 0 0 1px var(--design-editor-accent-color)",
890
+ };
891
+ }
892
+
457
893
  export type DraftPrimitiveKind =
458
894
  | "frame"
459
895
  | "rectangle"
@@ -498,7 +934,6 @@ interface DraftPrimitiveInput {
498
934
  points?: Point[];
499
935
  moved: boolean;
500
936
  toolProps?: CanvasToolProps;
501
- fallbackText: string;
502
937
  }
503
938
 
504
939
  interface MarqueeRect {
@@ -664,14 +1099,30 @@ export function MultiScreenCanvas({
664
1099
  onAddBreakpoint,
665
1100
  onActiveBreakpointChange,
666
1101
  onSelectionChange,
1102
+ onLayerMarqueeSelectionChange,
1103
+ selectedLayerSelectorGroupsByScreen = {},
667
1104
  onCrossScreenElementDrop,
668
1105
  boardFileId,
669
1106
  boardFileContent,
670
1107
  boardFrameGeometry,
671
1108
  onBoardDrawPrimitive,
672
1109
  boardEditMode = false,
1110
+ boardIsActive = false,
673
1111
  onBoardElementSelect,
1112
+ onBoardElementMarqueeSelect,
674
1113
  onBoardElementHover,
1114
+ onBoardElementClear,
1115
+ onBoardElementDblClickText,
1116
+ onBoardIframeHotkey,
1117
+ onBoardIframeContextMenu,
1118
+ onBoardTextEditingStateChange,
1119
+ boardClearSelectionRequest,
1120
+ boardSelectedSelector,
1121
+ boardSelectedSelectorCandidates,
1122
+ boardHoveredSelector,
1123
+ boardHoveredSelectorCandidates,
1124
+ boardLockedSelectors,
1125
+ boardHiddenSelectors,
675
1126
  onBoardVisualStructureChange,
676
1127
  onBoardVisualStyleChange,
677
1128
  onBoardVisualDuplicateChange,
@@ -742,13 +1193,21 @@ export function MultiScreenCanvas({
742
1193
  useState<CrossScreenDragGhost | null>(null);
743
1194
  const [crossScreenTarget, setCrossScreenTarget] =
744
1195
  useState<CrossScreenDragTarget | null>(null);
1196
+ const [crossScreenDropGuide, setCrossScreenDropGuide] =
1197
+ useState<CrossScreenDropGuide | null>(null);
1198
+ const [crossScreenSourceIsBoard, setCrossScreenSourceIsBoard] =
1199
+ useState(false);
745
1200
  /** Ref kept in sync with state so the message handler can read without closures. */
746
1201
  const crossScreenTargetRef = useRef<CrossScreenDragTarget | null>(null);
1202
+ const crossScreenHitTestSeqRef = useRef(0);
1203
+ const crossScreenPreviewTargetIdRef = useRef<string | null>(null);
747
1204
  /** The most-recent drag message payload — kept for use in the "end" handler. */
748
1205
  const crossScreenDragMsgRef = useRef<{
749
1206
  selector: string;
750
1207
  sourceId?: string;
1208
+ sourcePointerOffset?: Point;
751
1209
  } | null>(null);
1210
+ const crossScreenParentDragCleanupRef = useRef<(() => void) | null>(null);
752
1211
  /** Board-space point from the last cross-screen-drag "move" message. */
753
1212
  const crossScreenLastBoardPointRef = useRef<{ x: number; y: number } | null>(
754
1213
  null,
@@ -770,6 +1229,21 @@ export function MultiScreenCanvas({
770
1229
  const [chromeSettling, setChromeSettling] = useState(false);
771
1230
  const previousPreviewDeviceFrameRef = useRef(previewDeviceFrame);
772
1231
 
1232
+ const claimKeyboardFocus = useCallback(() => {
1233
+ const surface = surfaceRef.current;
1234
+ if (!surface) return;
1235
+ const active = document.activeElement;
1236
+ if (
1237
+ active instanceof HTMLElement &&
1238
+ active !== surface &&
1239
+ !surface.contains(active) &&
1240
+ isEditableHotkeyTarget(active)
1241
+ ) {
1242
+ active.blur();
1243
+ }
1244
+ surface.focus({ preventScroll: true });
1245
+ }, []);
1246
+
773
1247
  const getResolvedMetadata = useCallback(
774
1248
  (screen: ScreenFile) =>
775
1249
  resolveScreenMetadata(
@@ -1144,26 +1618,279 @@ export function MultiScreenCanvas({
1144
1618
  useEffect(() => {
1145
1619
  if (!onCrossScreenElementDrop) return;
1146
1620
 
1621
+ const clearCrossScreenDropGuide = () => {
1622
+ crossScreenHitTestSeqRef.current += 1;
1623
+ setCrossScreenDropGuide(null);
1624
+ };
1625
+
1626
+ const postHitTestPreviewClear = (targetId: string | null | undefined) => {
1627
+ if (!targetId) return;
1628
+ const targetIframe = surfaceRef.current?.querySelector<HTMLIFrameElement>(
1629
+ `[data-screen-iframe-id="${CSS.escape(targetId)}"]`,
1630
+ );
1631
+ targetIframe?.contentWindow?.postMessage(
1632
+ { type: "agent-native:hit-test-preview-clear" },
1633
+ "*",
1634
+ );
1635
+ };
1636
+
1637
+ const clearCrossScreenPreviewGuide = (
1638
+ targetId?: string | null | undefined,
1639
+ ) => {
1640
+ const id = targetId ?? crossScreenPreviewTargetIdRef.current;
1641
+ postHitTestPreviewClear(id);
1642
+ if (!targetId || targetId === crossScreenPreviewTargetIdRef.current) {
1643
+ crossScreenPreviewTargetIdRef.current = null;
1644
+ }
1645
+ };
1646
+
1647
+ const stopParentCrossScreenDrag = () => {
1648
+ crossScreenParentDragCleanupRef.current?.();
1649
+ crossScreenParentDragCleanupRef.current = null;
1650
+ };
1651
+
1147
1652
  const clearCrossScreenDrag = () => {
1653
+ stopParentCrossScreenDrag();
1654
+ clearCrossScreenPreviewGuide();
1148
1655
  setCrossScreenGhost(null);
1149
1656
  setCrossScreenTarget(null);
1657
+ setCrossScreenSourceIsBoard(false);
1658
+ clearCrossScreenDropGuide();
1150
1659
  crossScreenTargetRef.current = null;
1151
1660
  crossScreenDragMsgRef.current = null;
1152
1661
  };
1153
1662
 
1663
+ const runHitTest = (
1664
+ candidate: CrossScreenDragTarget,
1665
+ boardPoint: Point,
1666
+ options: { preview?: boolean } = {},
1667
+ ): Promise<CrossScreenHitTestResult> => {
1668
+ const targetScreen = screensRef.current.find(
1669
+ (s) => s.id === candidate.id,
1670
+ );
1671
+ if (!targetScreen) return Promise.resolve({});
1672
+ const targetGeometry = candidate.geometry;
1673
+ const targetIframe = surfaceRef.current?.querySelector<HTMLIFrameElement>(
1674
+ `[data-screen-iframe-id="${CSS.escape(candidate.id)}"]`,
1675
+ );
1676
+ const targetContentWindow = targetIframe?.contentWindow;
1677
+ if (!targetContentWindow) return Promise.resolve({});
1678
+ const targetViewportWidth =
1679
+ targetIframe.clientWidth || getResolvedMetadata(targetScreen).width;
1680
+ const targetViewportHeight =
1681
+ targetIframe.clientHeight || getResolvedMetadata(targetScreen).height;
1682
+ const scaleX = targetViewportWidth / Math.max(1, targetGeometry.width);
1683
+ const scaleY = targetViewportHeight / Math.max(1, targetGeometry.height);
1684
+ const localX = (boardPoint.x - targetGeometry.x) * scaleX;
1685
+ const localY = (boardPoint.y - targetGeometry.y) * scaleY;
1686
+
1687
+ const correlationId = `hit-${Date.now()}-${Math.random()
1688
+ .toString(36)
1689
+ .slice(2, 6)}`;
1690
+
1691
+ return new Promise((resolve) => {
1692
+ const timer = window.setTimeout(() => {
1693
+ window.removeEventListener("message", hitListener);
1694
+ resolve({});
1695
+ }, 50);
1696
+
1697
+ const hitListener = (ev: MessageEvent) => {
1698
+ if (
1699
+ !ev.data ||
1700
+ ev.data.type !== "agent-native:hit-test-result" ||
1701
+ ev.data.correlationId !== correlationId
1702
+ ) {
1703
+ return;
1704
+ }
1705
+ window.clearTimeout(timer);
1706
+ window.removeEventListener("message", hitListener);
1707
+ resolve({
1708
+ anchorNodeId:
1709
+ typeof ev.data.anchorNodeId === "string"
1710
+ ? ev.data.anchorNodeId
1711
+ : undefined,
1712
+ placement: isCrossScreenDropPlacement(ev.data.placement)
1713
+ ? ev.data.placement
1714
+ : undefined,
1715
+ axis: isCrossScreenDropAxis(ev.data.axis)
1716
+ ? ev.data.axis
1717
+ : undefined,
1718
+ anchorRect: isCrossScreenHitTestAnchorRect(ev.data.anchorRect)
1719
+ ? ev.data.anchorRect
1720
+ : undefined,
1721
+ });
1722
+ };
1723
+ window.addEventListener("message", hitListener);
1724
+
1725
+ targetContentWindow.postMessage(
1726
+ {
1727
+ type: "agent-native:hit-test",
1728
+ correlationId,
1729
+ x: localX,
1730
+ y: localY,
1731
+ preview: options.preview === true,
1732
+ },
1733
+ "*",
1734
+ );
1735
+ if (options.preview) {
1736
+ crossScreenPreviewTargetIdRef.current = candidate.id;
1737
+ }
1738
+ });
1739
+ };
1740
+
1741
+ const getTargetLocalPoint = (
1742
+ candidate: CrossScreenDragTarget,
1743
+ boardPoint: Point,
1744
+ ): Point | null => {
1745
+ if (candidate.id === boardFileId) return boardPoint;
1746
+ const targetScreen = screensRef.current.find(
1747
+ (s) => s.id === candidate.id,
1748
+ );
1749
+ if (!targetScreen) return null;
1750
+ const targetIframe = surfaceRef.current?.querySelector<HTMLIFrameElement>(
1751
+ `[data-screen-iframe-id="${CSS.escape(candidate.id)}"]`,
1752
+ );
1753
+ const targetViewportWidth =
1754
+ targetIframe?.clientWidth || getResolvedMetadata(targetScreen).width;
1755
+ const targetViewportHeight =
1756
+ targetIframe?.clientHeight || getResolvedMetadata(targetScreen).height;
1757
+ const scaleX =
1758
+ targetViewportWidth / Math.max(1, candidate.geometry.width);
1759
+ const scaleY =
1760
+ targetViewportHeight / Math.max(1, candidate.geometry.height);
1761
+ return {
1762
+ x: (boardPoint.x - candidate.geometry.x) * scaleX,
1763
+ y: (boardPoint.y - candidate.geometry.y) * scaleY,
1764
+ };
1765
+ };
1766
+
1767
+ const requestCrossScreenDropGuide = (
1768
+ candidate: CrossScreenDragTarget,
1769
+ boardPoint: Point,
1770
+ sourceIsBoard: boolean,
1771
+ ) => {
1772
+ const requestSeq = ++crossScreenHitTestSeqRef.current;
1773
+ void runHitTest(candidate, boardPoint, { preview: sourceIsBoard }).then(
1774
+ (hit) => {
1775
+ if (crossScreenHitTestSeqRef.current !== requestSeq) return;
1776
+ if (crossScreenTargetRef.current?.id !== candidate.id) return;
1777
+ const targetScreen = screensRef.current.find(
1778
+ (s) => s.id === candidate.id,
1779
+ );
1780
+ const guide = targetScreen
1781
+ ? getCrossScreenDropGuideForHitTest({
1782
+ hit,
1783
+ targetGeometry: candidate.geometry,
1784
+ targetMetadata: getResolvedMetadata(targetScreen),
1785
+ })
1786
+ : null;
1787
+ setCrossScreenDropGuide(sourceIsBoard ? null : guide);
1788
+ },
1789
+ );
1790
+ };
1791
+
1792
+ const updateCrossScreenTargetFromBoardPoint = (
1793
+ boardPoint: Point,
1794
+ sourceScreenId: string,
1795
+ ) => {
1796
+ crossScreenLastBoardPointRef.current = boardPoint;
1797
+ const sourceIsBoard = sourceScreenId === boardFileId;
1798
+ setCrossScreenSourceIsBoard(sourceIsBoard);
1799
+ setCrossScreenGhost(
1800
+ sourceIsBoard ? null : { boardX: boardPoint.x, boardY: boardPoint.y },
1801
+ );
1802
+ const target = getFrameEntryAtPoint(boardPoint);
1803
+ if (target && target.id !== sourceScreenId) {
1804
+ const nextTarget = { id: target.id, geometry: target.geometry };
1805
+ if (crossScreenTargetRef.current?.id !== nextTarget.id) {
1806
+ clearCrossScreenPreviewGuide();
1807
+ }
1808
+ crossScreenTargetRef.current = nextTarget;
1809
+ setCrossScreenTarget(nextTarget);
1810
+ requestCrossScreenDropGuide(nextTarget, boardPoint, sourceIsBoard);
1811
+ } else {
1812
+ clearCrossScreenPreviewGuide();
1813
+ crossScreenTargetRef.current = null;
1814
+ setCrossScreenTarget(null);
1815
+ clearCrossScreenDropGuide();
1816
+ }
1817
+ };
1818
+
1819
+ const finalizeCrossScreenDrop = (
1820
+ sourceScreenId: string,
1821
+ candidate: CrossScreenDragTarget | null,
1822
+ payload: {
1823
+ selector: string;
1824
+ sourceId?: string;
1825
+ sourcePointerOffset?: Point;
1826
+ },
1827
+ lastBoardPoint: Point | null,
1828
+ ) => {
1829
+ clearCrossScreenDrag();
1830
+ crossScreenLastBoardPointRef.current = null;
1831
+ const hasIdentifier = !!(payload.selector || payload.sourceId);
1832
+ if (!hasIdentifier || !sourceScreenId) return;
1833
+ if (!lastBoardPoint) return;
1834
+ const targetCandidate =
1835
+ candidate ??
1836
+ (boardFileId && sourceScreenId !== boardFileId && boardFrameGeometry
1837
+ ? { id: boardFileId, geometry: boardFrameGeometry }
1838
+ : null);
1839
+ if (!targetCandidate) return;
1840
+
1841
+ if (targetCandidate.id === boardFileId) {
1842
+ onCrossScreenElementDropRef.current?.({
1843
+ sourceSelector: payload.selector,
1844
+ sourceNodeId: payload.sourceId,
1845
+ sourceScreenId,
1846
+ targetScreenId: targetCandidate.id,
1847
+ targetCanvasPoint: lastBoardPoint,
1848
+ targetLocalPoint: lastBoardPoint,
1849
+ sourcePointerOffset: payload.sourcePointerOffset,
1850
+ });
1851
+ return;
1852
+ }
1853
+
1854
+ void runHitTest(targetCandidate, lastBoardPoint).then(
1855
+ ({ anchorNodeId, placement }) => {
1856
+ const targetAnchorPlacement = isCrossScreenDropPlacement(placement)
1857
+ ? placement
1858
+ : undefined;
1859
+ const targetLocalPoint = getTargetLocalPoint(
1860
+ targetCandidate,
1861
+ lastBoardPoint,
1862
+ );
1863
+ onCrossScreenElementDropRef.current?.({
1864
+ sourceSelector: payload.selector,
1865
+ sourceNodeId: payload.sourceId,
1866
+ sourceScreenId,
1867
+ targetScreenId: targetCandidate.id,
1868
+ targetAnchorNodeId: anchorNodeId,
1869
+ targetAnchorPlacement,
1870
+ targetCanvasPoint: lastBoardPoint,
1871
+ targetLocalPoint: targetLocalPoint ?? undefined,
1872
+ sourcePointerOffset: payload.sourcePointerOffset,
1873
+ });
1874
+ },
1875
+ );
1876
+ };
1877
+
1154
1878
  const handleMessage = (event: MessageEvent) => {
1155
1879
  if (!event.data || event.data.type !== "agent-native:cross-screen-drag") {
1156
1880
  return;
1157
1881
  }
1158
1882
  const msg = event.data as {
1159
1883
  type: string;
1160
- phase: "move" | "end" | "cancel";
1884
+ phase: "start" | "move" | "end" | "cancel";
1885
+ screenId?: string;
1161
1886
  selector?: string;
1162
1887
  sourceId?: string;
1163
1888
  iframeX?: number;
1164
1889
  iframeY?: number;
1165
1890
  viewportW?: number;
1166
1891
  viewportH?: number;
1892
+ elementRect?: CrossScreenDragElementRect;
1893
+ pointerOffset?: Point;
1167
1894
  };
1168
1895
 
1169
1896
  if (msg.phase === "cancel") {
@@ -1171,9 +1898,13 @@ export function MultiScreenCanvas({
1171
1898
  return;
1172
1899
  }
1173
1900
 
1174
- // The drag originates from the active screen (only the active screen has
1175
- // a live interactive iframe posting these messages).
1176
- const sourceScreenId = activeId;
1901
+ // Prefer the iframe-supplied source id. In overview, activeId can point
1902
+ // at a different screen than the layer currently being dragged.
1903
+ const sourceScreenId =
1904
+ msg.screenId &&
1905
+ (msg.screenId === boardFileId || frameGeometryRef.current[msg.screenId])
1906
+ ? msg.screenId
1907
+ : activeId;
1177
1908
  if (!sourceScreenId) {
1178
1909
  // Always clear visual state (ghost + highlight) when we have no active
1179
1910
  // screen to attribute the drag to — regardless of the phase. Without
@@ -1183,6 +1914,69 @@ export function MultiScreenCanvas({
1183
1914
  return;
1184
1915
  }
1185
1916
 
1917
+ if (msg.phase === "start") {
1918
+ setCrossScreenSourceIsBoard(sourceScreenId === boardFileId);
1919
+ crossScreenDragMsgRef.current = {
1920
+ selector: msg.selector ?? "",
1921
+ sourceId: msg.sourceId,
1922
+ sourcePointerOffset: isFinitePoint(msg.pointerOffset)
1923
+ ? msg.pointerOffset
1924
+ : undefined,
1925
+ };
1926
+ stopParentCrossScreenDrag();
1927
+ const restorePreviewPointerEvents = mutePreviewIframePointerEvents(
1928
+ surfaceRef.current,
1929
+ );
1930
+ let didCleanup = false;
1931
+ const activateParentDrag = (ev: MouseEvent) => {
1932
+ ev.preventDefault();
1933
+ updateCrossScreenTargetFromBoardPoint(
1934
+ getCanvasPoint(ev.clientX, ev.clientY),
1935
+ sourceScreenId,
1936
+ );
1937
+ };
1938
+ const handleParentMouseMove = (ev: MouseEvent) => {
1939
+ activateParentDrag(ev);
1940
+ };
1941
+ const handleParentMouseUp = (ev: MouseEvent) => {
1942
+ activateParentDrag(ev);
1943
+ const candidate = crossScreenTargetRef.current;
1944
+ const payload = crossScreenDragMsgRef.current ?? {
1945
+ selector: msg.selector ?? "",
1946
+ sourceId: msg.sourceId,
1947
+ sourcePointerOffset: isFinitePoint(msg.pointerOffset)
1948
+ ? msg.pointerOffset
1949
+ : undefined,
1950
+ };
1951
+ const lastBoardPoint = crossScreenLastBoardPointRef.current;
1952
+ finalizeCrossScreenDrop(
1953
+ sourceScreenId,
1954
+ candidate,
1955
+ payload,
1956
+ lastBoardPoint,
1957
+ );
1958
+ };
1959
+ const handleParentWindowBlur = () => {
1960
+ clearCrossScreenDrag();
1961
+ };
1962
+ const cleanup = () => {
1963
+ if (didCleanup) return;
1964
+ didCleanup = true;
1965
+ window.removeEventListener("mousemove", handleParentMouseMove, true);
1966
+ window.removeEventListener("mouseup", handleParentMouseUp, true);
1967
+ window.removeEventListener("blur", handleParentWindowBlur, true);
1968
+ restorePreviewPointerEvents();
1969
+ if (crossScreenParentDragCleanupRef.current === cleanup) {
1970
+ crossScreenParentDragCleanupRef.current = null;
1971
+ }
1972
+ };
1973
+ crossScreenParentDragCleanupRef.current = cleanup;
1974
+ window.addEventListener("mousemove", handleParentMouseMove, true);
1975
+ window.addEventListener("mouseup", handleParentMouseUp, true);
1976
+ window.addEventListener("blur", handleParentWindowBlur, true);
1977
+ return;
1978
+ }
1979
+
1186
1980
  if (msg.phase === "move") {
1187
1981
  const { iframeX, iframeY, viewportW, viewportH, selector, sourceId } =
1188
1982
  msg;
@@ -1199,23 +1993,36 @@ export function MultiScreenCanvas({
1199
1993
  crossScreenDragMsgRef.current = {
1200
1994
  selector: selector ?? "",
1201
1995
  sourceId,
1996
+ sourcePointerOffset: isFinitePoint(msg.pointerOffset)
1997
+ ? msg.pointerOffset
1998
+ : (crossScreenDragMsgRef.current?.sourcePointerOffset ?? undefined),
1202
1999
  };
1203
2000
 
1204
- // Pointer is inside the source iframe — let the bridge handle it.
1205
- if (
2001
+ const pointerInsideSourceIframe =
1206
2002
  iframeX >= 0 &&
1207
2003
  iframeY >= 0 &&
1208
2004
  iframeX <= viewportW &&
1209
- iframeY <= viewportH
1210
- ) {
1211
- clearCrossScreenDrag();
2005
+ iframeY <= viewportH;
2006
+ const sourceIsBoard = sourceScreenId === boardFileId;
2007
+ // Regular screen iframes are finite artboards, so an in-bounds pointer
2008
+ // means the source bridge should keep handling the drag. The board
2009
+ // iframe is different: it spans the whole overview canvas, including
2010
+ // every screen frame, so board-origin drags must still be checked
2011
+ // against screen drop targets while technically "inside" the source.
2012
+ if (pointerInsideSourceIframe && !sourceIsBoard) {
2013
+ clearCrossScreenPreviewGuide();
2014
+ setCrossScreenGhost(null);
2015
+ setCrossScreenTarget(null);
2016
+ setCrossScreenSourceIsBoard(false);
2017
+ crossScreenTargetRef.current = null;
2018
+ crossScreenLastBoardPointRef.current = null;
2019
+ clearCrossScreenDropGuide();
1212
2020
  return;
1213
2021
  }
1214
2022
 
1215
- // Translate iframe coords → board coords using source frame geometry.
1216
- // Board math mirrors primitiveLocalToBoardRect:
1217
- // boardX = frame.x + iframeX * (frame.width / metadata.width)
1218
- // boardY = frame.y + iframeY * (frame.height / metadata.height)
2023
+ // Translate iframe coords → board coords using the live embedded
2024
+ // viewport from the bridge. In overview, the iframe viewport may be the
2025
+ // frame geometry rather than the screen metadata width.
1219
2026
 
1220
2027
  let boardX: number;
1221
2028
  let boardY: number;
@@ -1234,31 +2041,13 @@ export function MultiScreenCanvas({
1234
2041
  clearCrossScreenDrag();
1235
2042
  return;
1236
2043
  }
1237
- const sourceMetadata = getResolvedMetadata(sourceScreen);
1238
- const scaleX =
1239
- sourceGeometry.width / Math.max(1, sourceMetadata.width);
1240
- const scaleY =
1241
- sourceGeometry.height / Math.max(1, sourceMetadata.height);
2044
+ const scaleX = sourceGeometry.width / Math.max(1, viewportW);
2045
+ const scaleY = sourceGeometry.height / Math.max(1, viewportH);
1242
2046
  boardX = sourceGeometry.x + iframeX * scaleX;
1243
2047
  boardY = sourceGeometry.y + iframeY * scaleY;
1244
2048
  }
1245
2049
  const boardPoint = { x: boardX, y: boardY };
1246
-
1247
- // Remember the latest board-space position for use in the "end" handler.
1248
- crossScreenLastBoardPointRef.current = boardPoint;
1249
-
1250
- setCrossScreenGhost({ boardX, boardY });
1251
-
1252
- // Find which screen frame the board point falls in.
1253
- const target = getFrameEntryAtPoint(boardPoint);
1254
- if (target && target.id !== sourceScreenId) {
1255
- const nextTarget = { id: target.id, geometry: target.geometry };
1256
- crossScreenTargetRef.current = nextTarget;
1257
- setCrossScreenTarget(nextTarget);
1258
- } else {
1259
- crossScreenTargetRef.current = null;
1260
- setCrossScreenTarget(null);
1261
- }
2050
+ updateCrossScreenTargetFromBoardPoint(boardPoint, sourceScreenId);
1262
2051
  return;
1263
2052
  }
1264
2053
 
@@ -1271,99 +2060,23 @@ export function MultiScreenCanvas({
1271
2060
  const payload = crossScreenDragMsgRef.current ?? {
1272
2061
  selector: msg.selector ?? "",
1273
2062
  sourceId: msg.sourceId,
2063
+ sourcePointerOffset: isFinitePoint(msg.pointerOffset)
2064
+ ? msg.pointerOffset
2065
+ : undefined,
1274
2066
  };
1275
2067
  const lastBoardPoint = crossScreenLastBoardPointRef.current;
1276
- clearCrossScreenDrag();
1277
- crossScreenLastBoardPointRef.current = null;
1278
- // Guard: require at least one stable identifier (selector or sourceId)
1279
- // so the drop handler in DesignEditor can resolve the node. An empty
1280
- // selector AND a missing sourceId would produce nodeAttrId="" and
1281
- // moveNodeBetweenDocuments would fail with a generic error toast.
1282
- const hasIdentifier = !!(payload.selector || payload.sourceId);
1283
- if (candidate && hasIdentifier && sourceScreenId) {
1284
- // Translate the board-space drop point to the target iframe's local
1285
- // coordinate space, then run a lightweight hit-test (postMessage +
1286
- // 50 ms timeout) to find the deepest container at the drop site.
1287
- const runHitTest = (): Promise<{
1288
- anchorNodeId?: string;
1289
- placement?: "before" | "after" | "inside";
1290
- }> => {
1291
- if (!lastBoardPoint) return Promise.resolve({});
1292
- const targetScreen = screensRef.current.find(
1293
- (s) => s.id === candidate.id,
1294
- );
1295
- if (!targetScreen) return Promise.resolve({});
1296
- const targetGeometry = candidate.geometry;
1297
- const targetMetadata = getResolvedMetadata(targetScreen);
1298
- // Board → target iframe local coords
1299
- const scaleX =
1300
- targetMetadata.width / Math.max(1, targetGeometry.width);
1301
- const scaleY =
1302
- targetMetadata.height / Math.max(1, targetGeometry.height);
1303
- const localX = (lastBoardPoint.x - targetGeometry.x) * scaleX;
1304
- const localY = (lastBoardPoint.y - targetGeometry.y) * scaleY;
1305
-
1306
- const targetIframe =
1307
- surfaceRef.current?.querySelector<HTMLIFrameElement>(
1308
- `[data-screen-iframe-id="${CSS.escape(candidate.id)}"]`,
1309
- );
1310
- const targetContentWindow = targetIframe?.contentWindow;
1311
- if (!targetContentWindow) return Promise.resolve({});
1312
-
1313
- const correlationId = `hit-${Date.now()}-${Math.random()
1314
- .toString(36)
1315
- .slice(2, 6)}`;
1316
-
1317
- return new Promise((resolve) => {
1318
- const timer = window.setTimeout(() => {
1319
- window.removeEventListener("message", hitListener);
1320
- resolve({});
1321
- }, 50);
1322
-
1323
- const hitListener = (ev: MessageEvent) => {
1324
- if (
1325
- !ev.data ||
1326
- ev.data.type !== "agent-native:hit-test-result" ||
1327
- ev.data.correlationId !== correlationId
1328
- )
1329
- return;
1330
- window.clearTimeout(timer);
1331
- window.removeEventListener("message", hitListener);
1332
- resolve({
1333
- anchorNodeId: ev.data.anchorNodeId ?? undefined,
1334
- placement: ev.data.placement ?? undefined,
1335
- });
1336
- };
1337
- window.addEventListener("message", hitListener);
1338
-
1339
- targetContentWindow.postMessage(
1340
- {
1341
- type: "agent-native:hit-test",
1342
- correlationId,
1343
- x: localX,
1344
- y: localY,
1345
- },
1346
- "*",
1347
- );
1348
- });
1349
- };
1350
-
1351
- void runHitTest().then(({ anchorNodeId, placement }) => {
1352
- onCrossScreenElementDropRef.current?.({
1353
- sourceSelector: payload.selector,
1354
- sourceNodeId: payload.sourceId,
1355
- sourceScreenId,
1356
- targetScreenId: candidate.id,
1357
- targetAnchorNodeId: anchorNodeId,
1358
- targetAnchorPlacement: placement,
1359
- });
1360
- });
1361
- }
2068
+ finalizeCrossScreenDrop(
2069
+ sourceScreenId,
2070
+ candidate,
2071
+ payload,
2072
+ lastBoardPoint,
2073
+ );
1362
2074
  }
1363
2075
  };
1364
2076
 
1365
2077
  window.addEventListener("message", handleMessage);
1366
2078
  return () => {
2079
+ stopParentCrossScreenDrag();
1367
2080
  window.removeEventListener("message", handleMessage);
1368
2081
  };
1369
2082
  }, [
@@ -1371,6 +2084,7 @@ export function MultiScreenCanvas({
1371
2084
  boardFileId,
1372
2085
  boardFrameGeometry,
1373
2086
  getFrameEntryAtPoint,
2087
+ getCanvasPoint,
1374
2088
  getResolvedMetadata,
1375
2089
  onCrossScreenElementDrop,
1376
2090
  ]);
@@ -1463,6 +2177,110 @@ export function MultiScreenCanvas({
1463
2177
  [],
1464
2178
  );
1465
2179
 
2180
+ const requestSelectableElementInfos = useCallback(
2181
+ (screenId: string): Promise<ElementInfo[]> => {
2182
+ const targetIframe = surfaceRef.current?.querySelector<HTMLIFrameElement>(
2183
+ `[data-screen-iframe-id="${CSS.escape(screenId)}"]`,
2184
+ );
2185
+ const targetContentWindow = targetIframe?.contentWindow;
2186
+ if (!targetContentWindow) return Promise.resolve([]);
2187
+ const correlationId = `rects-${Date.now()}-${Math.random()
2188
+ .toString(36)
2189
+ .slice(2, 6)}`;
2190
+ return new Promise((resolve) => {
2191
+ const timer = window.setTimeout(() => {
2192
+ window.removeEventListener("message", listener);
2193
+ resolve([]);
2194
+ }, 80);
2195
+ const listener = (event: MessageEvent) => {
2196
+ if (
2197
+ !event.data ||
2198
+ event.data.type !== "agent-native:selectable-rects-result" ||
2199
+ event.data.correlationId !== correlationId
2200
+ ) {
2201
+ return;
2202
+ }
2203
+ window.clearTimeout(timer);
2204
+ window.removeEventListener("message", listener);
2205
+ const payload: unknown[] = Array.isArray(event.data.payload)
2206
+ ? event.data.payload
2207
+ : [];
2208
+ resolve(
2209
+ payload.filter((item): item is ElementInfo => {
2210
+ if (!item || typeof item !== "object") return false;
2211
+ const candidate = item as Partial<ElementInfo>;
2212
+ return (
2213
+ typeof candidate.tagName === "string" &&
2214
+ !!candidate.boundingRect &&
2215
+ typeof candidate.boundingRect.width === "number" &&
2216
+ typeof candidate.boundingRect.height === "number"
2217
+ );
2218
+ }),
2219
+ );
2220
+ };
2221
+ window.addEventListener("message", listener);
2222
+ targetContentWindow.postMessage(
2223
+ { type: "agent-native:collect-selectable-rects", correlationId },
2224
+ "*",
2225
+ );
2226
+ });
2227
+ },
2228
+ [],
2229
+ );
2230
+
2231
+ const collectLayerMarqueeCandidates = useCallback(async () => {
2232
+ const frameCandidates = await Promise.all(
2233
+ getCurrentFrameEntries().map(async (entry) => {
2234
+ const screen = screensRef.current.find((item) => item.id === entry.id);
2235
+ if (!screen) return [] as CanvasLayerMarqueeCandidate[];
2236
+ const iframe = surfaceRef.current?.querySelector<HTMLIFrameElement>(
2237
+ `[data-screen-iframe-id="${CSS.escape(entry.id)}"]`,
2238
+ );
2239
+ const metadata = getResolvedMetadata(screen);
2240
+ const viewportWidth = iframe?.clientWidth || metadata.width;
2241
+ const viewportHeight = iframe?.clientHeight || metadata.height;
2242
+ const scaleX = entry.geometry.width / Math.max(1, viewportWidth);
2243
+ const scaleY = entry.geometry.height / Math.max(1, viewportHeight);
2244
+ const infos = await requestSelectableElementInfos(entry.id);
2245
+ return infos.map((info) => ({
2246
+ screenId: entry.id,
2247
+ info,
2248
+ geometry: {
2249
+ x: entry.geometry.x + info.boundingRect.x * scaleX,
2250
+ y: entry.geometry.y + info.boundingRect.y * scaleY,
2251
+ width: info.boundingRect.width * scaleX,
2252
+ height: info.boundingRect.height * scaleY,
2253
+ },
2254
+ frameGeometry: entry.geometry,
2255
+ }));
2256
+ }),
2257
+ );
2258
+ const boardCandidates =
2259
+ boardFileId && boardFrameGeometry
2260
+ ? await (async () => {
2261
+ const infos = await requestSelectableElementInfos(boardFileId);
2262
+ return infos.map((info) => ({
2263
+ screenId: boardFileId,
2264
+ info,
2265
+ geometry: {
2266
+ x: boardFrameGeometry.x + info.boundingRect.x,
2267
+ y: boardFrameGeometry.y + info.boundingRect.y,
2268
+ width: info.boundingRect.width,
2269
+ height: info.boundingRect.height,
2270
+ },
2271
+ frameGeometry: boardFrameGeometry,
2272
+ }));
2273
+ })()
2274
+ : [];
2275
+ return [...frameCandidates.flat(), ...boardCandidates];
2276
+ }, [
2277
+ boardFileId,
2278
+ boardFrameGeometry,
2279
+ getCurrentFrameEntries,
2280
+ getResolvedMetadata,
2281
+ requestSelectableElementInfos,
2282
+ ]);
2283
+
1466
2284
  const scheduleFeedbackClear = useCallback(() => {
1467
2285
  if (feedbackTimerRef.current !== null) {
1468
2286
  window.clearTimeout(feedbackTimerRef.current);
@@ -1656,6 +2474,30 @@ export function MultiScreenCanvas({
1656
2474
  e.preventDefault();
1657
2475
  e.stopPropagation();
1658
2476
  const originCanvas = getCanvasPoint(e.clientX, e.clientY);
2477
+ let latestRect = normalizeRectFromPoints(originCanvas, originCanvas);
2478
+ let layerCandidates: CanvasLayerMarqueeCandidate[] = [];
2479
+ const reportLayerSelection = (rect: MarqueeRect) => {
2480
+ const state = dragState.current;
2481
+ if (!state || state.type !== "marquee") return;
2482
+ const selection = layerCandidates
2483
+ .filter((candidate) =>
2484
+ rotatedRectIntersects(
2485
+ rect,
2486
+ getSelectableBounds(candidate.geometry),
2487
+ getFrameCenter(candidate.frameGeometry),
2488
+ candidate.frameGeometry.rotation ?? 0,
2489
+ ),
2490
+ )
2491
+ .map((candidate) => ({
2492
+ screenId: candidate.screenId,
2493
+ info: candidate.info,
2494
+ }));
2495
+ onLayerMarqueeSelectionChange?.(selection, {
2496
+ source: "marquee",
2497
+ additive: state.additive,
2498
+ shiftKey: state.additive,
2499
+ });
2500
+ };
1659
2501
  dragState.current = {
1660
2502
  type: "marquee",
1661
2503
  originClient: { x: e.clientX, y: e.clientY },
@@ -1669,14 +2511,25 @@ export function MultiScreenCanvas({
1669
2511
  if (!e.shiftKey) {
1670
2512
  updateSelectedIds(() => []);
1671
2513
  updateSelectedDraftIds(() => []);
2514
+ onLayerMarqueeSelectionChange?.([], {
2515
+ source: "marquee",
2516
+ additive: false,
2517
+ shiftKey: false,
2518
+ });
1672
2519
  }
1673
2520
  setIsDragging(true);
2521
+ void collectLayerMarqueeCandidates().then((candidates) => {
2522
+ if (dragState.current?.type !== "marquee") return;
2523
+ layerCandidates = candidates;
2524
+ reportLayerSelection(latestRect);
2525
+ });
1674
2526
 
1675
2527
  const handleMouseMove = (ev: MouseEvent) => {
1676
2528
  const state = dragState.current;
1677
2529
  if (!state || state.type !== "marquee") return;
1678
2530
  const nextPoint = getCanvasPoint(ev.clientX, ev.clientY);
1679
2531
  const rect = normalizeRectFromPoints(state.originCanvas, nextPoint);
2532
+ latestRect = rect;
1680
2533
  if (
1681
2534
  !state.hasMoved &&
1682
2535
  Math.hypot(
@@ -1719,6 +2572,7 @@ export function MultiScreenCanvas({
1719
2572
  ? dedupeIds([...state.baseSelectedDraftIds, ...hitDraftIds])
1720
2573
  : hitDraftIds,
1721
2574
  );
2575
+ reportLayerSelection(rect);
1722
2576
  };
1723
2577
 
1724
2578
  const handleMouseUp = () => {
@@ -1726,6 +2580,11 @@ export function MultiScreenCanvas({
1726
2580
  if (state?.type === "marquee" && !state.hasMoved && !state.additive) {
1727
2581
  updateSelectedIds(() => []);
1728
2582
  updateSelectedDraftIds(() => []);
2583
+ onLayerMarqueeSelectionChange?.([], {
2584
+ source: "marquee",
2585
+ additive: false,
2586
+ shiftKey: false,
2587
+ });
1729
2588
  }
1730
2589
  finishDrag();
1731
2590
  };
@@ -1733,11 +2592,13 @@ export function MultiScreenCanvas({
1733
2592
  installDragListeners(handleMouseMove, handleMouseUp);
1734
2593
  },
1735
2594
  [
2595
+ collectLayerMarqueeCandidates,
1736
2596
  finishDrag,
1737
2597
  getCanvasPoint,
1738
2598
  getCurrentDraftEntries,
1739
2599
  getCurrentFrameEntries,
1740
2600
  installDragListeners,
2601
+ onLayerMarqueeSelectionChange,
1741
2602
  updateSelectedDraftIds,
1742
2603
  updateSelectedIds,
1743
2604
  ],
@@ -1940,6 +2801,7 @@ export function MultiScreenCanvas({
1940
2801
  }, [frameGeometry, retryPersistedDraftPrimitives, screens]);
1941
2802
 
1942
2803
  const clearActivePenPath = useCallback(() => {
2804
+ activePenPathRef.current = null;
1943
2805
  setActivePenPath(null);
1944
2806
  setPenGesturePreview(null);
1945
2807
  setPenPointer(null);
@@ -1965,6 +2827,25 @@ export function MultiScreenCanvas({
1965
2827
  [clearActivePenPath, commitDraftPrimitive, onActiveToolChange, toolProps],
1966
2828
  );
1967
2829
 
2830
+ const undoActivePenPathSegment = useCallback(() => {
2831
+ const path = activePenPathRef.current;
2832
+ if (!path) return false;
2833
+
2834
+ const remainingNodes = path.nodes.slice(0, -1);
2835
+ if (remainingNodes.length === 0) {
2836
+ clearActivePenPath();
2837
+ return true;
2838
+ }
2839
+
2840
+ const nextPath: PenPath = { nodes: remainingNodes, closed: false };
2841
+ activePenPathRef.current = nextPath;
2842
+ setActivePenPath(nextPath);
2843
+ setPenGesturePreview(null);
2844
+ setPenPointer(null);
2845
+ setPenCloseHover(false);
2846
+ return true;
2847
+ }, [clearActivePenPath]);
2848
+
1968
2849
  const getPenAnchorPoint = useCallback(
1969
2850
  (
1970
2851
  clientX: number,
@@ -2256,7 +3137,6 @@ export function MultiScreenCanvas({
2256
3137
  points,
2257
3138
  moved: releaseMoved,
2258
3139
  toolProps,
2259
- fallbackText: t("designEditor.tools.text"),
2260
3140
  });
2261
3141
  commitDraftPrimitive(nextDraft, state.originFrameId);
2262
3142
  if (activeTool === undefined) {
@@ -2284,7 +3164,6 @@ export function MultiScreenCanvas({
2284
3164
  installDragListeners,
2285
3165
  onActiveToolChange,
2286
3166
  onCreateScreenFrame,
2287
- t,
2288
3167
  toolProps,
2289
3168
  ],
2290
3169
  );
@@ -2365,14 +3244,6 @@ export function MultiScreenCanvas({
2365
3244
  }),
2366
3245
  );
2367
3246
  setAlignmentGuides(snap.guides);
2368
- const primary = state.originDrafts[state.primaryId].geometry;
2369
- showTransformFeedback(
2370
- `X ${Math.round(primary.x + dx + snap.dx)} Y ${Math.round(
2371
- primary.y + dy + snap.dy,
2372
- )}`,
2373
- ev.clientX,
2374
- ev.clientY,
2375
- );
2376
3247
 
2377
3248
  // Primitive drop-into-container detection: check if the dragged draft
2378
3249
  // is hovering over a committed container primitive on any screen.
@@ -2475,7 +3346,6 @@ export function MultiScreenCanvas({
2475
3346
  getCurrentCanvasEntries,
2476
3347
  installDragListeners,
2477
3348
  persistDraftPrimitive,
2478
- showTransformFeedback,
2479
3349
  updateDraftPrimitives,
2480
3350
  updatePrimitiveDropTarget,
2481
3351
  updateSelectedDraftIds,
@@ -2717,14 +3587,6 @@ export function MultiScreenCanvas({
2717
3587
  return next;
2718
3588
  });
2719
3589
  setAlignmentGuides(snap.guides);
2720
- const primary = state.originFrames[state.primaryId];
2721
- showTransformFeedback(
2722
- `X ${Math.round(primary.x + dx + snap.dx)} Y ${Math.round(
2723
- primary.y + dy + snap.dy,
2724
- )}`,
2725
- ev.clientX,
2726
- ev.clientY,
2727
- );
2728
3590
 
2729
3591
  // When all dragged ids are committed primitive nodeIds (not screen
2730
3592
  // frames), check for a container primitive drop target to highlight.
@@ -2790,7 +3652,6 @@ export function MultiScreenCanvas({
2790
3652
  getCurrentFrameEntries,
2791
3653
  installDragListeners,
2792
3654
  onPick,
2793
- showTransformFeedback,
2794
3655
  updateFrameGeometry,
2795
3656
  updatePrimitiveDropTarget,
2796
3657
  updateSelectedDraftIds,
@@ -3188,6 +4049,7 @@ export function MultiScreenCanvas({
3188
4049
 
3189
4050
  const handleMouseDown = useCallback(
3190
4051
  (e: React.MouseEvent) => {
4052
+ claimKeyboardFocus();
3191
4053
  // Clear any stale pick-suppression left over from a prior resize/rotate/move
3192
4054
  // gesture that never received its trailing frame click — otherwise it would
3193
4055
  // silently swallow this unrelated interaction.
@@ -3222,6 +4084,7 @@ export function MultiScreenCanvas({
3222
4084
  beginMarquee,
3223
4085
  beginPan,
3224
4086
  beginPenNodeCreation,
4087
+ claimKeyboardFocus,
3225
4088
  localActiveTool,
3226
4089
  ],
3227
4090
  );
@@ -3615,15 +4478,21 @@ export function MultiScreenCanvas({
3615
4478
  useEffect(() => {
3616
4479
  const handleKeyDown = (event: KeyboardEvent) => {
3617
4480
  const path = activePenPathRef.current;
3618
- if (
3619
- !path ||
3620
- event.metaKey ||
3621
- event.ctrlKey ||
3622
- isEditableHotkeyTarget(event.target)
3623
- ) {
4481
+ if (!path || isEditableHotkeyTarget(event.target)) {
3624
4482
  return;
3625
4483
  }
3626
4484
 
4485
+ const primaryKey = event.metaKey || event.ctrlKey;
4486
+ if (primaryKey && !event.shiftKey && event.key.toLowerCase() === "z") {
4487
+ event.preventDefault();
4488
+ event.stopPropagation();
4489
+ event.stopImmediatePropagation();
4490
+ undoActivePenPathSegment();
4491
+ return;
4492
+ }
4493
+
4494
+ if (primaryKey) return;
4495
+
3627
4496
  if (event.key === "Enter") {
3628
4497
  event.preventDefault();
3629
4498
  event.stopPropagation();
@@ -3644,14 +4513,7 @@ export function MultiScreenCanvas({
3644
4513
  event.preventDefault();
3645
4514
  event.stopPropagation();
3646
4515
  event.stopImmediatePropagation();
3647
- const remainingNodes = path.nodes.slice(0, -1);
3648
- if (remainingNodes.length === 0) {
3649
- clearActivePenPath();
3650
- return;
3651
- }
3652
- setActivePenPath({ nodes: remainingNodes, closed: false });
3653
- setPenPointer(null);
3654
- setPenCloseHover(false);
4516
+ undoActivePenPathSegment();
3655
4517
  }
3656
4518
  };
3657
4519
 
@@ -3659,7 +4521,7 @@ export function MultiScreenCanvas({
3659
4521
  return () => {
3660
4522
  window.removeEventListener("keydown", handleKeyDown, true);
3661
4523
  };
3662
- }, [clearActivePenPath, finishPenPath]);
4524
+ }, [clearActivePenPath, finishPenPath, undoActivePenPathSegment]);
3663
4525
 
3664
4526
  useEffect(() => {
3665
4527
  const tool = normalizeCanvasTool(activeTool ?? localActiveTool);
@@ -3719,6 +4581,10 @@ export function MultiScreenCanvas({
3719
4581
  const penActive = effectiveTool === "pen";
3720
4582
  const creationToolActive = Boolean(getDraftCreationTool(effectiveTool));
3721
4583
  const canvasGestureActive = isDragging || isPanning;
4584
+ const boardSurfaceInteractive = shouldBoardSurfaceCapturePointerEvents({
4585
+ tool: effectiveTool,
4586
+ gestureActive: canvasGestureActive,
4587
+ });
3722
4588
  const displayedPenPath = penGesturePreview
3723
4589
  ? penGesturePreview
3724
4590
  : activePenPath && penPointer && activePenPath.nodes.length > 0
@@ -3786,10 +4652,14 @@ export function MultiScreenCanvas({
3786
4652
  selectedDraftEntries.length === 1 && !selectedDraftGroupBounds
3787
4653
  ? selectedDraftEntries[0]
3788
4654
  : null;
4655
+ const rootSelectedEntryCount =
4656
+ selectedFrameEntries.length + selectedDraftEntries.length;
4657
+ const showPassiveRootSelectionBoxes = rootSelectedEntryCount > 1;
3789
4658
  return (
3790
4659
  <div
3791
4660
  ref={surfaceRef}
3792
- className="relative h-full w-full select-none overflow-hidden"
4661
+ tabIndex={-1}
4662
+ className="relative h-full w-full select-none overflow-hidden outline-none"
3793
4663
  onMouseDownCapture={handleMouseDown}
3794
4664
  onMouseMove={handleMouseMove}
3795
4665
  style={{ cursor: surfaceCursor, touchAction: "none" }}
@@ -3826,6 +4696,7 @@ export function MultiScreenCanvas({
3826
4696
  >
3827
4697
  {boardFileId &&
3828
4698
  boardFileContent !== undefined &&
4699
+ hasBoardSurfaceContent(boardFileContent) &&
3829
4700
  (() => {
3830
4701
  const boardGeo = boardFrameGeometry ?? {
3831
4702
  x: 0,
@@ -3833,40 +4704,87 @@ export function MultiScreenCanvas({
3833
4704
  width: 8192,
3834
4705
  height: 8192,
3835
4706
  };
3836
- // Clamp board canvas to the DesignCanvas max safe dimension.
3837
- const boardW = Math.min(boardGeo.width, 16384);
3838
- const boardH = Math.min(boardGeo.height, 16384);
4707
+ const boardW = boardGeo.width;
4708
+ const boardH = boardGeo.height;
4709
+ const boardContentKey = getBoardContentKey({
4710
+ boardFileId,
4711
+ boardFileContent,
4712
+ boardIsActive,
4713
+ });
4714
+ const boardRenderContent =
4715
+ getBoardSurfaceRenderContent(boardFileContent);
3839
4716
  return (
3840
4717
  // Overflow-hidden wrapper so the board iframe never bleeds outside
3841
4718
  // its declared logical surface. z-index 0 keeps it below screen
3842
4719
  // iframes (which have their own stacking context above this).
3843
4720
  <div
3844
- style={{
3845
- position: "absolute",
3846
- left: SURFACE_PADDING,
3847
- top: SURFACE_PADDING,
3848
- width: boardW,
3849
- height: boardH,
3850
- overflow: "hidden",
3851
- pointerEvents: "auto",
3852
- // Board sits behind all screen iframes.
3853
- zIndex: 0,
3854
- }}
4721
+ className="[&_.design-canvas-iframe-wrapper]:shadow-none [&_.design-canvas-iframe-wrapper]:ring-0"
4722
+ style={getBoardSurfaceLayerStyle({
4723
+ geometry: boardGeo,
4724
+ interactive: boardSurfaceInteractive,
4725
+ })}
3855
4726
  >
3856
4727
  <DesignCanvas
3857
- content={boardFileContent}
3858
- contentKey={boardFileId}
4728
+ content={boardRenderContent}
4729
+ contentKey={boardContentKey}
4730
+ screenId={boardFileId}
3859
4731
  zoom={100}
3860
4732
  deviceFrame="none"
4733
+ boardSurface
4734
+ embeddedFrameBackground={BOARD_SURFACE_BACKGROUND}
4735
+ embeddedFrame={{
4736
+ viewportWidth: Math.max(1, Math.round(boardW)),
4737
+ viewportHeight: Math.max(1, Math.round(boardH)),
4738
+ displayWidth: Math.max(1, Math.round(boardW)),
4739
+ displayHeight: Math.max(1, Math.round(boardH)),
4740
+ fluid: true,
4741
+ contentOffsetX: -boardGeo.x,
4742
+ contentOffsetY: -boardGeo.y,
4743
+ }}
4744
+ editorChromeScaleX={canvasZoom / 100}
4745
+ editorChromeScaleY={canvasZoom / 100}
3861
4746
  editMode={boardEditMode}
3862
4747
  interactMode={false}
3863
- registerRuntimeBridge={false}
4748
+ scaleMode={boardIsActive && effectiveTool === "scale"}
4749
+ clearSelectionRequest={boardClearSelectionRequest}
4750
+ selectedSelector={
4751
+ boardIsActive ? (boardSelectedSelector ?? null) : null
4752
+ }
4753
+ selectedSelectorCandidates={
4754
+ boardIsActive ? (boardSelectedSelectorCandidates ?? []) : []
4755
+ }
4756
+ selectedSelectorGroups={
4757
+ selectedLayerSelectorGroupsByScreen[boardFileId] ?? []
4758
+ }
4759
+ hoveredSelector={boardHoveredSelector ?? null}
4760
+ hoveredSelectorCandidates={
4761
+ boardHoveredSelectorCandidates ?? []
4762
+ }
4763
+ lockedSelectors={boardLockedSelectors ?? []}
4764
+ hiddenSelectors={boardHiddenSelectors ?? []}
4765
+ // Board owns the global window runtime bridge only when it is
4766
+ // the active surface (activeFileId === boardFileId). This is
4767
+ // the XOR counterpart to the active screen's
4768
+ // registerRuntimeBridge={screenIsActive}: since activeFileId
4769
+ // is exclusive, the board and any screen can never both
4770
+ // register at once, so window.__designCanvas* in-place ops
4771
+ // (delete removal, begin-text-edit) target the board exactly
4772
+ // like a screen. editMode stays always-editable so a board
4773
+ // element can still be clicked to select it before the board
4774
+ // becomes active.
4775
+ registerRuntimeBridge={boardIsActive}
3864
4776
  onElementSelect={onBoardElementSelect ?? (() => {})}
4777
+ onElementMarqueeSelect={onBoardElementMarqueeSelect}
3865
4778
  onElementHover={onBoardElementHover ?? (() => {})}
4779
+ onClearSelection={onBoardElementClear}
4780
+ onIframeHotkey={onBoardIframeHotkey}
4781
+ onIframeContextMenu={onBoardIframeContextMenu}
3866
4782
  onVisualStructureChange={onBoardVisualStructureChange}
3867
4783
  onVisualStyleChange={onBoardVisualStyleChange}
3868
4784
  onVisualDuplicateChange={onBoardVisualDuplicateChange}
3869
4785
  onTextContentChange={onBoardTextContentChange}
4786
+ onTextEditingStateChange={onBoardTextEditingStateChange}
4787
+ onElementDblClickText={onBoardElementDblClickText}
3870
4788
  tweakValues={{}}
3871
4789
  />
3872
4790
  </div>
@@ -3946,6 +4864,28 @@ export function MultiScreenCanvas({
3946
4864
  />
3947
4865
  ) : null}
3948
4866
 
4867
+ {showPassiveRootSelectionBoxes
4868
+ ? selectedFrameEntries.map((entry) => (
4869
+ <PassiveSelectionBox
4870
+ key={`selected-frame-${entry.id}`}
4871
+ geometry={entry.geometry}
4872
+ chromeScale={chromeScale}
4873
+ chromeSettling={chromeSettling}
4874
+ />
4875
+ ))
4876
+ : null}
4877
+
4878
+ {showPassiveRootSelectionBoxes
4879
+ ? selectedDraftEntries.map((entry) => (
4880
+ <PassiveSelectionBox
4881
+ key={`selected-draft-${entry.id}`}
4882
+ geometry={entry.geometry}
4883
+ chromeScale={chromeScale}
4884
+ chromeSettling={chromeSettling}
4885
+ />
4886
+ ))
4887
+ : null}
4888
+
3949
4889
  {displayedPenPath ? (
3950
4890
  <PenPathOverlay path={displayedPenPath} closeHover={penCloseHover} />
3951
4891
  ) : null}
@@ -4083,7 +5023,7 @@ export function MultiScreenCanvas({
4083
5023
  }}
4084
5024
  >
4085
5025
  <div className="flex h-full w-full items-start justify-between rounded-lg bg-muted/20 p-2">
4086
- <span className="max-w-[190px] truncate text-[11px] font-medium text-foreground">
5026
+ <span className="max-w-[190px] truncate !text-[11px] font-medium text-foreground">
4087
5027
  {duplicatePreview.display}
4088
5028
  </span>
4089
5029
  <span className="flex items-center gap-1 rounded-md border border-border bg-background px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm">
@@ -4101,37 +5041,29 @@ export function MultiScreenCanvas({
4101
5041
  {transformBadge ? (
4102
5042
  <div
4103
5043
  data-transform-badge
4104
- className="pointer-events-none fixed z-50 rounded border border-border bg-background/95 px-1.5 py-0.5 font-mono text-[11px] leading-5 text-foreground shadow-lg backdrop-blur"
5044
+ className="pointer-events-none fixed z-50 rounded border border-border bg-background/95 px-1.5 py-0.5 font-mono !text-[11px] leading-5 text-foreground shadow-lg backdrop-blur"
4105
5045
  style={{ left: transformBadge.x, top: transformBadge.y }}
4106
5046
  >
4107
5047
  {transformBadge.text}
4108
5048
  </div>
4109
5049
  ) : null}
4110
5050
 
4111
- {/* Cross-screen element drag: highlight the candidate target screen frame.
4112
- Uses the same style as the primitiveDropTarget overlay (2px accent border
4113
- + 14% accent fill) so it is visually consistent. */}
4114
- {crossScreenTarget ? (
5051
+ {crossScreenDropGuide ? (
4115
5052
  <span
4116
- data-cross-screen-drop-target
4117
- className="pointer-events-none absolute z-40 rounded-sm"
4118
- style={{
4119
- // Surface position = pan + (SURFACE_PADDING + canvasCoord) * scale
4120
- left:
4121
- pan.x + (SURFACE_PADDING + crossScreenTarget.geometry.x) * scale,
4122
- top:
4123
- pan.y + (SURFACE_PADDING + crossScreenTarget.geometry.y) * scale,
4124
- width: Math.max(1, crossScreenTarget.geometry.width * scale),
4125
- height: Math.max(1, crossScreenTarget.geometry.height * scale),
4126
- border: "2px solid var(--design-editor-accent-color)",
4127
- background:
4128
- "color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)",
4129
- }}
5053
+ data-cross-screen-drop-guide
5054
+ className="pointer-events-none absolute z-50 rounded-sm shadow-[0_0_0_1px_var(--design-editor-accent-contrast-color)]"
5055
+ style={getCrossScreenDropGuideStyle({
5056
+ guide: crossScreenDropGuide,
5057
+ pan,
5058
+ scale,
5059
+ })}
4130
5060
  />
4131
5061
  ) : null}
4132
5062
 
4133
5063
  {/* Cross-screen element drag: small ghost following the board-space cursor. */}
4134
- {crossScreenGhost ? (
5064
+ {crossScreenGhost &&
5065
+ !crossScreenSourceIsBoard &&
5066
+ !crossScreenDropGuide ? (
4135
5067
  <span
4136
5068
  data-cross-screen-drag-ghost
4137
5069
  className="pointer-events-none absolute z-50 rounded border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-color)]/20 shadow"
@@ -4680,7 +5612,7 @@ const Screen = memo(function Screen({
4680
5612
  <span
4681
5613
  data-frame-title
4682
5614
  className={cn(
4683
- "min-w-0 truncate text-[11px] font-medium",
5615
+ "min-w-0 truncate !text-[11px] font-medium",
4684
5616
  emphasized
4685
5617
  ? "text-[var(--design-editor-accent-color)]"
4686
5618
  : activeOrEmphasized
@@ -5026,7 +5958,7 @@ function BreakpointPreviewRow({
5026
5958
  />
5027
5959
  <span
5028
5960
  className={cn(
5029
- "truncate text-[11px] font-medium",
5961
+ "truncate !text-[11px] font-medium",
5030
5962
  isActive
5031
5963
  ? "text-[var(--design-editor-accent-color)]"
5032
5964
  : "text-muted-foreground",
@@ -5134,16 +6066,63 @@ function GroupSelectionBox({
5134
6066
  chromeScale={chromeScale}
5135
6067
  chromeSettling={chromeSettling}
5136
6068
  showRotate={false}
6069
+ filled
5137
6070
  onStartResize={onStartResize}
5138
6071
  onStartRotate={() => {}}
5139
6072
  />
5140
6073
  );
5141
6074
  }
5142
6075
 
6076
+ function PassiveSelectionBox({
6077
+ geometry,
6078
+ chromeScale,
6079
+ chromeSettling,
6080
+ }: {
6081
+ geometry: FrameGeometry;
6082
+ chromeScale: number;
6083
+ chromeSettling: boolean;
6084
+ }) {
6085
+ return (
6086
+ <div
6087
+ data-passive-frame-selection-box
6088
+ className="pointer-events-none absolute border border-[var(--design-editor-accent-color)]"
6089
+ style={{
6090
+ left: SURFACE_PADDING + geometry.x,
6091
+ top: SURFACE_PADDING + geometry.y,
6092
+ width: geometry.width,
6093
+ height: geometry.height,
6094
+ borderRadius: 13 * chromeScale,
6095
+ borderWidth: 1.5 * chromeScale,
6096
+ transition: getSelectionBoxTransition(chromeSettling),
6097
+ transform: geometry.rotation
6098
+ ? `rotate(${geometry.rotation}deg)`
6099
+ : undefined,
6100
+ transformOrigin: `${geometry.width / 2}px ${geometry.height / 2}px`,
6101
+ zIndex: 999_999,
6102
+ }}
6103
+ >
6104
+ {CORNER_RESIZE_HANDLE_CONFIGS.map((config) => (
6105
+ <span
6106
+ key={config.handle}
6107
+ data-passive-resize-handle={config.handle}
6108
+ className="pointer-events-none absolute z-20 rounded-[2px] border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-contrast-color)] shadow"
6109
+ style={cornerHandleStyle(
6110
+ config.handle,
6111
+ config.cursor,
6112
+ chromeScale,
6113
+ chromeSettling,
6114
+ )}
6115
+ />
6116
+ ))}
6117
+ </div>
6118
+ );
6119
+ }
6120
+
5143
6121
  function SelectionBox({
5144
6122
  geometry,
5145
6123
  chromeScale,
5146
6124
  chromeSettling,
6125
+ filled = false,
5147
6126
  showRotate = true,
5148
6127
  onStartResize,
5149
6128
  onStartRotate,
@@ -5151,6 +6130,7 @@ function SelectionBox({
5151
6130
  geometry: FrameGeometry;
5152
6131
  chromeScale: number;
5153
6132
  chromeSettling: boolean;
6133
+ filled?: boolean;
5154
6134
  showRotate?: boolean;
5155
6135
  onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
5156
6136
  onStartRotate: (e: React.MouseEvent) => void;
@@ -5165,6 +6145,9 @@ function SelectionBox({
5165
6145
  top: SURFACE_PADDING + geometry.y,
5166
6146
  width: geometry.width,
5167
6147
  height: geometry.height,
6148
+ background: filled
6149
+ ? "var(--design-editor-selection-color)"
6150
+ : "transparent",
5168
6151
  borderRadius: 13 * chromeScale,
5169
6152
  borderWidth: 1.5 * chromeScale,
5170
6153
  transition: getSelectionBoxTransition(chromeSettling),
@@ -5807,7 +6790,6 @@ function createDraftPrimitive({
5807
6790
  points,
5808
6791
  moved,
5809
6792
  toolProps,
5810
- fallbackText,
5811
6793
  }: DraftPrimitiveInput): DraftPrimitive {
5812
6794
  const id = createDraftId(tool);
5813
6795
  const geometry = moved
@@ -5839,7 +6821,7 @@ function createDraftPrimitive({
5839
6821
  id,
5840
6822
  kind: "text",
5841
6823
  geometry,
5842
- text: toolProps?.text ?? fallbackText,
6824
+ text: toolProps?.text ?? "",
5843
6825
  fill: toolProps?.fill,
5844
6826
  stroke: toolProps?.stroke,
5845
6827
  autoSize: !moved,
@@ -6365,17 +7347,21 @@ export function parsePrimitivesFromScreen(
6365
7347
  // Validity: must have non-zero size
6366
7348
  if (width <= 0 || height <= 0) return;
6367
7349
 
6368
- // Container check:
6369
- // - Must be a div (not svg/path/circle/polygon/etc.)
6370
- // - Must NOT be an ellipse (border-radius:50%)
6371
- // - NOT a text primitive (but text divs could be containers in principle;
6372
- // we exclude them by checking for display:inline-block autoSize pattern)
7350
+ // Only explicit auto-layout primitives should accept drops. Plain absolute
7351
+ // rectangles are visual layers, not flow containers; treating every div as a
7352
+ // container made top-level rectangles disappear into each other on drag.
6373
7353
  const isDiv = tag === "div";
6374
7354
  const isEllipse =
6375
7355
  style.borderRadius === "50%" ||
6376
7356
  style.borderRadius === "50% 50% 50% 50%";
6377
7357
  const isTextAutoSize = style.display === "inline-block";
6378
- const isContainer = isDiv && !isEllipse && !isTextAutoSize;
7358
+ const isAutoLayout =
7359
+ style.display === "flex" ||
7360
+ style.display === "inline-flex" ||
7361
+ style.display === "grid" ||
7362
+ style.display === "inline-grid";
7363
+ const isContainer =
7364
+ isDiv && !isEllipse && !isTextAutoSize && isAutoLayout;
6379
7365
 
6380
7366
  result.push({
6381
7367
  nodeId,