@agent-native/core 0.80.10 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +419 -19
  6. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  7. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  8. package/corpus/core/src/client/chat/index.ts +1 -0
  9. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  10. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  11. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  12. package/corpus/core/src/client/index.ts +1 -0
  13. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  15. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  17. package/corpus/core/src/db/client.ts +29 -2
  18. package/corpus/core/src/provider-api/index.ts +29 -0
  19. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  20. package/corpus/core/src/styles/agent-native.css +54 -0
  21. package/corpus/core/src/usage/store.ts +2 -2
  22. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  23. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  24. package/corpus/templates/analytics/app/global.css +8 -0
  25. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  26. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  27. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  29. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  30. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  31. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  36. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  38. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  39. package/corpus/templates/assets/AGENTS.md +4 -0
  40. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  42. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  43. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  44. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  46. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  47. package/corpus/templates/assets/app/global.css +15 -0
  48. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  49. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  50. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  51. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  52. package/corpus/templates/assets/app/routes/library.tsx +233 -52
  53. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  54. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  56. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  57. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  58. package/corpus/templates/assets/shared/api.ts +4 -0
  59. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  60. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  61. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  64. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  66. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  67. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  68. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  69. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  70. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  71. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  72. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  76. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  77. package/corpus/templates/design/AGENTS.md +27 -0
  78. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  79. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  80. package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
  81. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  82. package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
  83. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  84. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  85. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  86. package/corpus/templates/design/actions/delete-design.ts +20 -0
  87. package/corpus/templates/design/actions/export-zip.ts +23 -8
  88. package/corpus/templates/design/actions/generate-design.ts +53 -1
  89. package/corpus/templates/design/actions/get-component-details.ts +21 -26
  90. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  91. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  92. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  93. package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
  94. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  95. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  96. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  97. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  98. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  99. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  100. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  101. package/corpus/templates/design/actions/navigate.ts +18 -3
  102. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  103. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  104. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  105. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  106. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  107. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  108. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  109. package/corpus/templates/design/actions/run-design-audit.ts +74 -10
  110. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
  118. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
  120. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
  121. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
  122. package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
  123. package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
  124. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  125. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  126. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  127. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  128. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  129. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  131. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  132. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  133. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  134. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  136. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  137. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  138. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  139. package/corpus/templates/design/app/global.css +7 -0
  140. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  141. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  142. package/corpus/templates/design/app/i18n-data.ts +369 -0
  143. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
  145. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  146. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  171. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  172. package/corpus/templates/design/e2e/helpers.ts +168 -65
  173. package/corpus/templates/design/package.json +3 -1
  174. package/corpus/templates/design/server/db/schema.ts +23 -0
  175. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  176. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  177. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  178. package/corpus/templates/design/server/plugins/db.ts +39 -0
  179. package/corpus/templates/design/server/register-secrets.ts +38 -0
  180. package/corpus/templates/design/shared/board-file.ts +228 -0
  181. package/corpus/templates/design/shared/board-objects.ts +288 -0
  182. package/corpus/templates/design/shared/code-layer.ts +52 -0
  183. package/corpus/templates/design/shared/component-model.ts +35 -0
  184. package/corpus/templates/design/shared/motion-compiler.ts +137 -15
  185. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  186. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  187. package/corpus/templates/design/tsconfig.json +2 -1
  188. package/corpus/templates/slides/AGENTS.md +9 -0
  189. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  190. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  191. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  192. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  193. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  194. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  195. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  196. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  197. package/corpus/templates/slides/package.json +1 -0
  198. package/corpus/templates/slides/vite.config.ts +1 -0
  199. package/dist/agent/production-agent.d.ts.map +1 -1
  200. package/dist/agent/production-agent.js +94 -10
  201. package/dist/agent/production-agent.js.map +1 -1
  202. package/dist/cli/design-connect.d.ts +25 -0
  203. package/dist/cli/design-connect.d.ts.map +1 -1
  204. package/dist/cli/design-connect.js +357 -18
  205. package/dist/cli/design-connect.js.map +1 -1
  206. package/dist/client/AssistantChat.d.ts +14 -0
  207. package/dist/client/AssistantChat.d.ts.map +1 -1
  208. package/dist/client/AssistantChat.js +115 -22
  209. package/dist/client/AssistantChat.js.map +1 -1
  210. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  211. package/dist/client/ConnectBuilderCard.js +1 -0
  212. package/dist/client/ConnectBuilderCard.js.map +1 -1
  213. package/dist/client/chat/index.d.ts +1 -1
  214. package/dist/client/chat/index.d.ts.map +1 -1
  215. package/dist/client/chat/index.js +1 -1
  216. package/dist/client/chat/index.js.map +1 -1
  217. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  218. package/dist/client/chat/run-recovery.js +5 -5
  219. package/dist/client/chat/run-recovery.js.map +1 -1
  220. package/dist/client/chat-view-transition.d.ts +6 -0
  221. package/dist/client/chat-view-transition.d.ts.map +1 -1
  222. package/dist/client/chat-view-transition.js +23 -2
  223. package/dist/client/chat-view-transition.js.map +1 -1
  224. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  225. package/dist/client/composer/PromptComposer.js +3 -2
  226. package/dist/client/composer/PromptComposer.js.map +1 -1
  227. package/dist/client/index.d.ts +1 -1
  228. package/dist/client/index.d.ts.map +1 -1
  229. package/dist/client/index.js +1 -1
  230. package/dist/client/index.js.map +1 -1
  231. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  232. package/dist/client/org/OrgSwitcher.js +11 -4
  233. package/dist/client/org/OrgSwitcher.js.map +1 -1
  234. package/dist/client/sharing/ShareButton.js +3 -3
  235. package/dist/client/sharing/ShareButton.js.map +1 -1
  236. package/dist/client/sse-event-processor.d.ts.map +1 -1
  237. package/dist/client/sse-event-processor.js +10 -0
  238. package/dist/client/sse-event-processor.js.map +1 -1
  239. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  240. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  241. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  242. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  243. package/dist/collab/awareness.d.ts +2 -2
  244. package/dist/collab/awareness.d.ts.map +1 -1
  245. package/dist/collab/routes.d.ts +2 -2
  246. package/dist/db/client.d.ts +1 -0
  247. package/dist/db/client.d.ts.map +1 -1
  248. package/dist/db/client.js +28 -2
  249. package/dist/db/client.js.map +1 -1
  250. package/dist/notifications/routes.d.ts +3 -3
  251. package/dist/observability/routes.d.ts +3 -3
  252. package/dist/progress/routes.d.ts +1 -1
  253. package/dist/provider-api/index.d.ts +5 -4
  254. package/dist/provider-api/index.d.ts.map +1 -1
  255. package/dist/provider-api/index.js +28 -0
  256. package/dist/provider-api/index.js.map +1 -1
  257. package/dist/resources/handlers.d.ts +3 -3
  258. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  259. package/dist/server/core-routes-plugin.d.ts +25 -0
  260. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  261. package/dist/server/core-routes-plugin.js +43 -18
  262. package/dist/server/core-routes-plugin.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +54 -0
  265. package/dist/usage/store.js +2 -2
  266. package/dist/usage/store.js.map +1 -1
  267. package/package.json +1 -1
@@ -48,7 +48,20 @@ import { prettyScreenName } from "@/lib/screen-names";
48
48
  import { cn } from "@/lib/utils";
49
49
 
50
50
  import { canvasPrimitiveReactStyle } from "./canvas-primitive-style";
51
- import { DEVICE_FRAME_VIEWPORTS, type DeviceFrameType } from "./types";
51
+ import {
52
+ DesignCanvas,
53
+ LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT,
54
+ appendHitTestResponder,
55
+ } from "./DesignCanvas";
56
+ import {
57
+ DEVICE_FRAME_VIEWPORTS,
58
+ type DeviceFrameType,
59
+ type ElementInfo,
60
+ } from "./types";
61
+
62
+ // Re-export so consumers of MultiScreenCanvas can use the same script without
63
+ // importing DesignCanvas directly.
64
+ export { LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT };
52
65
 
53
66
  interface ScreenFile {
54
67
  id: string;
@@ -204,15 +217,112 @@ interface MultiScreenCanvasProps {
204
217
  * Called when the user drags an element out of the active screen's iframe
205
218
  * and drops it onto a different screen. The bridge in the source iframe
206
219
  * posts { type:"agent-native:cross-screen-drag" } messages; the host
207
- * translates them to board coords, finds the target frame, and calls this
208
- * prop with the resolved ids.
220
+ * translates them to board coords, finds the target frame, runs a hit-test
221
+ * in the target iframe (50ms timeout), and calls this prop with the resolved
222
+ * ids and anchor placement.
209
223
  */
210
224
  onCrossScreenElementDrop?: (args: {
211
225
  sourceSelector: string;
212
226
  sourceNodeId?: string;
213
227
  sourceScreenId: string;
214
228
  targetScreenId: string;
229
+ /** data-agent-native-node-id of the deepest container at the drop point
230
+ * inside the target screen iframe (undefined when hit-test timed out). */
231
+ targetAnchorNodeId?: string;
232
+ /** DOM insertion placement relative to the anchor node. */
233
+ targetAnchorPlacement?: "before" | "after" | "inside";
215
234
  }) => void;
235
+ // ── Board file (new model) ───────────────────────────────────────────────
236
+ /**
237
+ * The id of the reserved "__board__.html" design file.
238
+ * When provided, a full-surface board <DesignCanvas> is rendered below
239
+ * the screen iframes so board elements are editable through the bridge.
240
+ */
241
+ boardFileId?: string;
242
+ /**
243
+ * The current HTML content of the board file.
244
+ * Passed as `content` to the board <DesignCanvas> instance.
245
+ */
246
+ boardFileContent?: string;
247
+ /**
248
+ * The logical geometry of the board iframe in canvas coordinates.
249
+ * Should be { x:0, y:0, width:totalSurfaceWidth, height:totalSurfaceHeight }.
250
+ * Used to translate cross-screen-drag coords when the source is the board.
251
+ */
252
+ boardFrameGeometry?: FrameGeometry;
253
+ /**
254
+ * Called when a draft primitive is committed outside all screen frames
255
+ * (and there is more than one screen). The caller should append the
256
+ * primitive into the board file's HTML content.
257
+ *
258
+ * Replaces the legacy onCreateBoardObject.
259
+ */
260
+ onBoardDrawPrimitive?: (primitive: CanvasPrimitiveInsert) => boolean | string;
261
+ // ── Board edit callbacks (active-target model) ───────────────────────────
262
+ /**
263
+ * When true the board <DesignCanvas> is in edit mode.
264
+ * Pass `canEditDesign` from DesignEditor. Defaults to false.
265
+ */
266
+ boardEditMode?: boolean;
267
+ /**
268
+ * Called when the user selects an element on the board surface.
269
+ * DesignEditor should set boardFileId as the active file and push the
270
+ * selection to the inspector.
271
+ */
272
+ onBoardElementSelect?: (info: ElementInfo) => void;
273
+ /**
274
+ * Called when the user hovers an element on the board surface.
275
+ */
276
+ onBoardElementHover?: (info: ElementInfo | null) => void;
277
+ /**
278
+ * Called when a drag / reorder / reparent / drop-into-container or delete
279
+ * occurs on a board element. Target file is boardFileId.
280
+ */
281
+ onBoardVisualStructureChange?: (
282
+ selector: string,
283
+ anchorSelector: string,
284
+ placement: "before" | "after" | "inside",
285
+ info?: ElementInfo,
286
+ details?: {
287
+ sourceId?: string;
288
+ anchorSourceId?: string;
289
+ requestId?: string;
290
+ },
291
+ ) => boolean | void;
292
+ /**
293
+ * Called when a style property changes on a board element.
294
+ * Target file is boardFileId.
295
+ */
296
+ onBoardVisualStyleChange?: (
297
+ selector: string,
298
+ styles: Record<string, string>,
299
+ info?: ElementInfo,
300
+ ) => void;
301
+ /**
302
+ * Called when an alt-drag clone is created on the board surface.
303
+ * Target file is boardFileId.
304
+ */
305
+ onBoardVisualDuplicateChange?: (
306
+ selector: string,
307
+ cloneHtml: string,
308
+ info?: ElementInfo,
309
+ details?: {
310
+ sourceId?: string;
311
+ anchorSelector?: string;
312
+ anchorSourceId?: string;
313
+ placement?: "before" | "after" | "inside";
314
+ },
315
+ ) => boolean | void;
316
+ /**
317
+ * Called when inline text is edited on a board element.
318
+ * Target file is boardFileId.
319
+ */
320
+ onBoardTextContentChange?: (
321
+ selector: string,
322
+ value: string,
323
+ info?: ElementInfo,
324
+ details?: { html?: string },
325
+ ) => void;
216
326
  }
217
327
 
218
328
  /**
@@ -224,7 +334,7 @@ const SCREEN_WIDTH = OVERVIEW_FRAME_WIDTH;
224
334
  const SCREEN_HEIGHT = 640;
225
335
  const SCREEN_CARD_HEIGHT = SCREEN_HEIGHT + 26;
226
336
  const SCREEN_GAP = 56;
227
- const SURFACE_PADDING = 240;
337
+ export const SURFACE_PADDING = 240;
228
338
  const DUPLICATE_DRAG_THRESHOLD = 6;
229
339
  const DRAG_THRESHOLD = 3;
230
340
  const FRAME_LABEL_HEIGHT = 28;
@@ -555,6 +665,17 @@ export function MultiScreenCanvas({
555
665
  onActiveBreakpointChange,
556
666
  onSelectionChange,
557
667
  onCrossScreenElementDrop,
668
+ boardFileId,
669
+ boardFileContent,
670
+ boardFrameGeometry,
671
+ onBoardDrawPrimitive,
672
+ boardEditMode = false,
673
+ onBoardElementSelect,
674
+ onBoardElementHover,
675
+ onBoardVisualStructureChange,
676
+ onBoardVisualStyleChange,
677
+ onBoardVisualDuplicateChange,
678
+ onBoardTextContentChange,
558
679
  }: MultiScreenCanvasProps) {
559
680
  const t = useT();
560
681
  const surfaceRef = useRef<HTMLDivElement>(null);
@@ -628,7 +749,15 @@ export function MultiScreenCanvas({
628
749
  selector: string;
629
750
  sourceId?: string;
630
751
  } | null>(null);
752
+ /** Board-space point from the last cross-screen-drag "move" message. */
753
+ const crossScreenLastBoardPointRef = useRef<{ x: number; y: number } | null>(
754
+ null,
755
+ );
631
756
  const onCrossScreenElementDropRef = useRef(onCrossScreenElementDrop);
757
+ const onBoardDrawPrimitiveRef = useRef(onBoardDrawPrimitive);
758
+ // Ref wrapper for finishDrag so callbacks declared before finishDrag can
759
+ // reference it via the ref without hitting the const TDZ.
760
+ const finishDragRef = useRef<() => void>(() => {});
632
761
  const suppressNextPick = useRef(false);
633
762
  const feedbackTimerRef = useRef<number | null>(null);
634
763
  const pendingWheelGestureRef = useRef<PendingWheelGesture | null>(null);
@@ -668,6 +797,10 @@ export function MultiScreenCanvas({
668
797
  onCrossScreenElementDropRef.current = onCrossScreenElementDrop;
669
798
  }, [onCrossScreenElementDrop]);
670
799
 
800
+ useEffect(() => {
801
+ onBoardDrawPrimitiveRef.current = onBoardDrawPrimitive;
802
+ }, [onBoardDrawPrimitive]);
803
+
671
804
  useEffect(() => {
672
805
  screensRef.current = screens;
673
806
  }, [screens]);
@@ -1083,22 +1216,37 @@ export function MultiScreenCanvas({
1083
1216
  // Board math mirrors primitiveLocalToBoardRect:
1084
1217
  // boardX = frame.x + iframeX * (frame.width / metadata.width)
1085
1218
  // boardY = frame.y + iframeY * (frame.height / metadata.height)
1086
- const sourceScreen = screensRef.current.find(
1087
- (s) => s.id === sourceScreenId,
1088
- );
1089
- const sourceGeometry = frameGeometryRef.current[sourceScreenId];
1090
- if (!sourceScreen || !sourceGeometry) {
1091
- clearCrossScreenDrag();
1092
- return;
1219
+
1220
+ let boardX: number;
1221
+ let boardY: number;
1222
+
1223
+ if (sourceScreenId === boardFileId && boardFrameGeometry) {
1224
+ // The board iframe is pixel-exact: 1 iframe pixel == 1 canvas unit.
1225
+ // iframeX/iframeY are already in canvas space (no scale needed).
1226
+ boardX = boardFrameGeometry.x + iframeX;
1227
+ boardY = boardFrameGeometry.y + iframeY;
1228
+ } else {
1229
+ const sourceScreen = screensRef.current.find(
1230
+ (s) => s.id === sourceScreenId,
1231
+ );
1232
+ const sourceGeometry = frameGeometryRef.current[sourceScreenId];
1233
+ if (!sourceScreen || !sourceGeometry) {
1234
+ clearCrossScreenDrag();
1235
+ return;
1236
+ }
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);
1242
+ boardX = sourceGeometry.x + iframeX * scaleX;
1243
+ boardY = sourceGeometry.y + iframeY * scaleY;
1093
1244
  }
1094
- const sourceMetadata = getResolvedMetadata(sourceScreen);
1095
- const scaleX = sourceGeometry.width / Math.max(1, sourceMetadata.width);
1096
- const scaleY =
1097
- sourceGeometry.height / Math.max(1, sourceMetadata.height);
1098
- const boardX = sourceGeometry.x + iframeX * scaleX;
1099
- const boardY = sourceGeometry.y + iframeY * scaleY;
1100
1245
  const boardPoint = { x: boardX, y: boardY };
1101
1246
 
1247
+ // Remember the latest board-space position for use in the "end" handler.
1248
+ crossScreenLastBoardPointRef.current = boardPoint;
1249
+
1102
1250
  setCrossScreenGhost({ boardX, boardY });
1103
1251
 
1104
1252
  // Find which screen frame the board point falls in.
@@ -1124,18 +1272,91 @@ export function MultiScreenCanvas({
1124
1272
  selector: msg.selector ?? "",
1125
1273
  sourceId: msg.sourceId,
1126
1274
  };
1275
+ const lastBoardPoint = crossScreenLastBoardPointRef.current;
1127
1276
  clearCrossScreenDrag();
1277
+ crossScreenLastBoardPointRef.current = null;
1128
1278
  // Guard: require at least one stable identifier (selector or sourceId)
1129
1279
  // so the drop handler in DesignEditor can resolve the node. An empty
1130
1280
  // selector AND a missing sourceId would produce nodeAttrId="" and
1131
1281
  // moveNodeBetweenDocuments would fail with a generic error toast.
1132
1282
  const hasIdentifier = !!(payload.selector || payload.sourceId);
1133
1283
  if (candidate && hasIdentifier && sourceScreenId) {
1134
- onCrossScreenElementDropRef.current?.({
1135
- sourceSelector: payload.selector,
1136
- sourceNodeId: payload.sourceId,
1137
- sourceScreenId,
1138
- targetScreenId: candidate.id,
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
+ });
1139
1360
  });
1140
1361
  }
1141
1362
  }
@@ -1147,6 +1368,8 @@ export function MultiScreenCanvas({
1147
1368
  };
1148
1369
  }, [
1149
1370
  activeId,
1371
+ boardFileId,
1372
+ boardFrameGeometry,
1150
1373
  getFrameEntryAtPoint,
1151
1374
  getResolvedMetadata,
1152
1375
  onCrossScreenElementDrop,
@@ -1202,6 +1425,7 @@ export function MultiScreenCanvas({
1202
1425
  (
1203
1426
  handleMouseMove: (ev: MouseEvent) => void,
1204
1427
  handleMouseUp: (ev: MouseEvent) => void,
1428
+ handleCancel?: () => void,
1205
1429
  ) => {
1206
1430
  dragCleanup.current?.();
1207
1431
  const restorePreviewPointerEvents = mutePreviewIframePointerEvents(
@@ -1218,8 +1442,13 @@ export function MultiScreenCanvas({
1218
1442
  ev.preventDefault();
1219
1443
  handleMouseUp(ev);
1220
1444
  };
1221
- const cleanupOnBlur = () =>
1445
+ const cleanupOnBlur = () => {
1446
+ if (handleCancel) {
1447
+ handleCancel();
1448
+ return;
1449
+ }
1222
1450
  handleMouseUp(lastMouseEvent ?? new MouseEvent("mouseup"));
1451
+ };
1223
1452
  dragCleanup.current = () => {
1224
1453
  window.removeEventListener("mousemove", move);
1225
1454
  window.removeEventListener("mouseup", up);
@@ -1323,6 +1552,10 @@ export function MultiScreenCanvas({
1323
1552
  dragCleanup.current?.();
1324
1553
  }, []);
1325
1554
 
1555
+ // Keep the finishDragRef in sync so board-object callbacks declared before
1556
+ // finishDrag can call it via the ref without a TDZ forward-reference issue.
1557
+ finishDragRef.current = finishDrag;
1558
+
1326
1559
  const cancelActiveDrag = useCallback(() => {
1327
1560
  let cancelled = false;
1328
1561
  const state = dragState.current;
@@ -1475,6 +1708,7 @@ export function MultiScreenCanvas({
1475
1708
  ),
1476
1709
  )
1477
1710
  .map((entry) => entry.id);
1711
+
1478
1712
  updateSelectedIds(() =>
1479
1713
  state.additive
1480
1714
  ? dedupeIds([...state.baseSelectedIds, ...hitIds])
@@ -1541,17 +1775,18 @@ export function MultiScreenCanvas({
1541
1775
 
1542
1776
  // Fallback: if no frame contains the center (shape drawn outside all
1543
1777
  // frames), use the nearest frame so the shape still persists rather than
1544
- // becoming a lost draft primitive. With a single screen this is always
1545
- // that screen; with multiple screens it's the visually closest one.
1778
+ // becoming a lost draft primitive.
1779
+ //
1780
+ // Exception: when there is MORE THAN ONE screen, a draft whose center
1781
+ // falls outside all frames becomes a board object (floating on the
1782
+ // infinite canvas surface), not a primitive inside any screen. In that
1783
+ // case return undefined so the caller can route to onCreateBoardObject.
1784
+ // With a single screen there is no meaningful "outside", so we always
1785
+ // absorb the draft into the only available frame (existing behaviour).
1546
1786
  if (entries.length === 0) return undefined;
1547
1787
  if (entries.length === 1) return entries[0];
1548
- return entries.slice().sort((a, b) => {
1549
- const ca = getFrameCenter(a.geometry);
1550
- const cb = getFrameCenter(b.geometry);
1551
- const da = Math.hypot(draftCenter.x - ca.x, draftCenter.y - ca.y);
1552
- const db = Math.hypot(draftCenter.x - cb.x, draftCenter.y - cb.y);
1553
- return da - db;
1554
- })[0];
1788
+ // Multiple screens: draft drawn outside all frames → board object.
1789
+ return undefined;
1555
1790
  },
1556
1791
  [getCurrentFrameEntries],
1557
1792
  );
@@ -1562,7 +1797,39 @@ export function MultiScreenCanvas({
1562
1797
  preferredFrameId?: string,
1563
1798
  ): PersistedDraftPrimitive | null => {
1564
1799
  const targetFrame = getTargetFrameForDraft(draft, preferredFrameId);
1565
- if (!targetFrame || !onCreatePrimitive) return null;
1800
+
1801
+ // When the draft center is outside ALL frames (and screens.length > 1),
1802
+ // getTargetFrameForDraft returns undefined. Route to onBoardDrawPrimitive
1803
+ // so the board file captures the new element.
1804
+ if (!targetFrame) {
1805
+ const handler = onBoardDrawPrimitiveRef.current;
1806
+ if (handler) {
1807
+ // Convert the draft into a board-space CanvasPrimitiveInsert.
1808
+ // The board uses a 1:1 coordinate mapping (no frame scaling needed).
1809
+ const boardPrimitive = draftPrimitiveToInsert(draft, {
1810
+ x: 0,
1811
+ y: 0,
1812
+ width: 1,
1813
+ height: 1,
1814
+ });
1815
+ const persisted = handler(boardPrimitive);
1816
+ if (!persisted) return null;
1817
+ // Return a sentinel PersistedDraftPrimitive so the caller can remove
1818
+ // the draft. The sentinel frameId "__board__" is detected downstream.
1819
+ return {
1820
+ frameId: "__board__",
1821
+ nodeId:
1822
+ (typeof persisted === "string"
1823
+ ? persisted
1824
+ : boardPrimitive.nodeId) ?? draft.id,
1825
+ };
1826
+ }
1827
+ return null;
1828
+ }
1829
+
1830
+ if (!onCreatePrimitive) {
1831
+ return null;
1832
+ }
1566
1833
  const targetScreen = screens.find(
1567
1834
  (screen) => screen.id === targetFrame.id,
1568
1835
  );
@@ -1580,7 +1847,9 @@ export function MultiScreenCanvas({
1580
1847
  targetMetadata,
1581
1848
  );
1582
1849
  const persisted = onCreatePrimitive(targetFrame.id, localPrimitive);
1583
- if (!persisted) return null;
1850
+ if (!persisted) {
1851
+ return null;
1852
+ }
1584
1853
  return {
1585
1854
  frameId: targetFrame.id,
1586
1855
  nodeId:
@@ -1606,7 +1875,9 @@ export function MultiScreenCanvas({
1606
1875
  );
1607
1876
  updateSelectedDraftIds(() => []);
1608
1877
  updateSelectedIds(() => []);
1609
- onPrimitiveCreated?.(persisted.frameId, persisted.nodeId);
1878
+ if (persisted.frameId !== "__board__") {
1879
+ onPrimitiveCreated?.(persisted.frameId, persisted.nodeId);
1880
+ }
1610
1881
  return;
1611
1882
  }
1612
1883
 
@@ -1623,6 +1894,51 @@ export function MultiScreenCanvas({
1623
1894
  ],
1624
1895
  );
1625
1896
 
1897
+ const retryPersistedDraftPrimitives = useCallback(() => {
1898
+ const drafts = draftPrimitivesRef.current;
1899
+ if (drafts.length === 0 || !onCreatePrimitive) return;
1900
+
1901
+ const persistedByDraftId = new Map<string, PersistedDraftPrimitive>();
1902
+ drafts.forEach((draft) => {
1903
+ const persisted = persistDraftPrimitive(draft);
1904
+ if (persisted) persistedByDraftId.set(draft.id, persisted);
1905
+ });
1906
+ if (persistedByDraftId.size === 0) return;
1907
+
1908
+ const selectedDraftIds = selectedDraftIdsRef.current;
1909
+ updateDraftPrimitives((current) =>
1910
+ current.filter((draft) => !persistedByDraftId.has(draft.id)),
1911
+ );
1912
+ updateSelectedDraftIds((current) =>
1913
+ current.filter((id) => !persistedByDraftId.has(id)),
1914
+ );
1915
+ updateSelectedIds(() => []);
1916
+
1917
+ const selectedPersisted = selectedDraftIds
1918
+ .map((id) => persistedByDraftId.get(id))
1919
+ .filter((entry): entry is PersistedDraftPrimitive => Boolean(entry));
1920
+ const persistedEntries =
1921
+ selectedPersisted.length > 0
1922
+ ? selectedPersisted
1923
+ : Array.from(persistedByDraftId.values());
1924
+ const lastPersisted = persistedEntries[persistedEntries.length - 1];
1925
+ // Do not call onPrimitiveCreated for board objects (sentinel frameId).
1926
+ if (lastPersisted && lastPersisted.frameId !== "__board__") {
1927
+ onPrimitiveCreated?.(lastPersisted.frameId, lastPersisted.nodeId);
1928
+ }
1929
+ }, [
1930
+ onCreatePrimitive,
1931
+ onPrimitiveCreated,
1932
+ persistDraftPrimitive,
1933
+ updateDraftPrimitives,
1934
+ updateSelectedDraftIds,
1935
+ updateSelectedIds,
1936
+ ]);
1937
+
1938
+ useEffect(() => {
1939
+ retryPersistedDraftPrimitives();
1940
+ }, [frameGeometry, retryPersistedDraftPrimitives, screens]);
1941
+
1626
1942
  const clearActivePenPath = useCallback(() => {
1627
1943
  setActivePenPath(null);
1628
1944
  setPenGesturePreview(null);
@@ -1784,7 +2100,18 @@ export function MultiScreenCanvas({
1784
2100
  finishDrag();
1785
2101
  };
1786
2102
 
1787
- installDragListeners(handleMouseMove, handleMouseUp);
2103
+ const cancelPenGesture = () => {
2104
+ const state = dragState.current;
2105
+ if (state?.type === "pen-node") {
2106
+ setActivePenPath(state.pathBefore);
2107
+ }
2108
+ setPenGesturePreview(null);
2109
+ setPenPointer(null);
2110
+ setPenCloseHover(false);
2111
+ finishDrag();
2112
+ };
2113
+
2114
+ installDragListeners(handleMouseMove, handleMouseUp, cancelPenGesture);
1788
2115
  },
1789
2116
  [
1790
2117
  finishDrag,
@@ -1804,15 +2131,12 @@ export function MultiScreenCanvas({
1804
2131
 
1805
2132
  const originCanvas = getCanvasPoint(e.clientX, e.clientY);
1806
2133
  const originFrameId = getFrameEntryAtPoint(originCanvas)?.id;
1807
- // B1 fix: seed a zero-size preview at the click origin instead of
1808
- // getDraftGeometryForTool(tool, origin, origin) which returns the
1809
- // default 160x120 size and causes a visible flash before the drag delta
1810
- // applies. The real size only matters at mouseup (createDraftPrimitive)
1811
- // which already handles the "no drag" click-to-place case correctly.
1812
- const initialGeometry =
1813
- tool === "pen" || tool === "line" || tool === "arrow"
1814
- ? getDraftGeometryForTool(tool, originCanvas, originCanvas)
1815
- : { x: originCanvas.x, y: originCanvas.y, width: 0, height: 0 };
2134
+ const initialGeometry = getDraftPreviewGeometryForTool(
2135
+ tool,
2136
+ originCanvas,
2137
+ originCanvas,
2138
+ false,
2139
+ );
1816
2140
  const initialPoints =
1817
2141
  tool === "pen"
1818
2142
  ? [originCanvas]
@@ -1871,10 +2195,11 @@ export function MultiScreenCanvas({
1871
2195
 
1872
2196
  setCreationPreview({
1873
2197
  tool,
1874
- geometry: getDraftGeometryForTool(
2198
+ geometry: getDraftPreviewGeometryForTool(
1875
2199
  tool,
1876
2200
  state.originCanvas,
1877
2201
  nextCanvas,
2202
+ state.hasMoved,
1878
2203
  ),
1879
2204
  points:
1880
2205
  state.tool === "line" || state.tool === "arrow"
@@ -3395,7 +3720,7 @@ export function MultiScreenCanvas({
3395
3720
  <div
3396
3721
  ref={surfaceRef}
3397
3722
  className="relative h-full w-full select-none overflow-hidden"
3398
- onMouseDown={handleMouseDown}
3723
+ onMouseDownCapture={handleMouseDown}
3399
3724
  onMouseMove={handleMouseMove}
3400
3725
  style={{ cursor: surfaceCursor, touchAction: "none" }}
3401
3726
  >
@@ -3429,6 +3754,55 @@ export function MultiScreenCanvas({
3429
3754
  transformOrigin: "top left",
3430
3755
  }}
3431
3756
  >
3757
+ {boardFileId &&
3758
+ boardFileContent !== undefined &&
3759
+ (() => {
3760
+ const boardGeo = boardFrameGeometry ?? {
3761
+ x: 0,
3762
+ y: 0,
3763
+ width: 8192,
3764
+ height: 8192,
3765
+ };
3766
+ // Clamp board canvas to the DesignCanvas max safe dimension.
3767
+ const boardW = Math.min(boardGeo.width, 16384);
3768
+ const boardH = Math.min(boardGeo.height, 16384);
3769
+ return (
3770
+ // Overflow-hidden wrapper so the board iframe never bleeds outside
3771
+ // its declared logical surface. z-index 0 keeps it below screen
3772
+ // iframes (which have their own stacking context above this).
3773
+ <div
3774
+ style={{
3775
+ position: "absolute",
3776
+ left: SURFACE_PADDING,
3777
+ top: SURFACE_PADDING,
3778
+ width: boardW,
3779
+ height: boardH,
3780
+ overflow: "hidden",
3781
+ pointerEvents: "auto",
3782
+ // Board sits behind all screen iframes.
3783
+ zIndex: 0,
3784
+ }}
3785
+ >
3786
+ <DesignCanvas
3787
+ content={boardFileContent}
3788
+ contentKey={boardFileId}
3789
+ zoom={100}
3790
+ deviceFrame="none"
3791
+ editMode={boardEditMode}
3792
+ interactMode={false}
3793
+ registerRuntimeBridge={false}
3794
+ onElementSelect={onBoardElementSelect ?? (() => {})}
3795
+ onElementHover={onBoardElementHover ?? (() => {})}
3796
+ onVisualStructureChange={onBoardVisualStructureChange}
3797
+ onVisualStyleChange={onBoardVisualStyleChange}
3798
+ onVisualDuplicateChange={onBoardVisualDuplicateChange}
3799
+ onTextContentChange={onBoardTextContentChange}
3800
+ tweakValues={{}}
3801
+ />
3802
+ </div>
3803
+ );
3804
+ })()}
3805
+
3432
3806
  {canvasFrames.map(({ screen, metadata, geometry }) => {
3433
3807
  return (
3434
3808
  <Screen
@@ -3575,6 +3949,14 @@ export function MultiScreenCanvas({
3575
3949
  ))}
3576
3950
  </div>
3577
3951
 
3952
+ {penActive || creationToolActive ? (
3953
+ <div
3954
+ data-canvas-creation-shield
3955
+ className="pointer-events-auto absolute inset-0 z-30 cursor-crosshair"
3956
+ aria-hidden="true"
3957
+ />
3958
+ ) : null}
3959
+
3578
3960
  {marquee ? (
3579
3961
  <span
3580
3962
  className="pointer-events-none absolute z-40 border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-selection-color)]"
@@ -3731,6 +4113,7 @@ function DraftPrimitiveLayer({
3731
4113
  return (
3732
4114
  <button
3733
4115
  data-frame-shell
4116
+ data-screen-shell
3734
4117
  type="button"
3735
4118
  className={cn(
3736
4119
  "group/artboard pointer-events-auto absolute block overflow-visible text-left outline-none",
@@ -4109,17 +4492,31 @@ const Screen = memo(function Screen({
4109
4492
  (directlyHovered || isDirectlyHovered) &&
4110
4493
  !creationToolActive &&
4111
4494
  !canvasGestureActive;
4495
+ const suppressFrameChromeForChild =
4496
+ hasHoveredChild && !directlyHovered && !isDirectlyHovered;
4112
4497
  const emphasized = isSelected || frameDirectlyHovered;
4113
4498
  const fullViewVisible = emphasized || showFullView;
4114
4499
  const activeOrEmphasized = isActive || emphasized;
4115
4500
  const selectionOutlined = isSelected && !groupSelected;
4116
4501
  const showHoverChrome =
4117
- frameDirectlyHovered && !isSelected && !groupSelected && !hasHoveredChild;
4502
+ frameDirectlyHovered &&
4503
+ !isSelected &&
4504
+ !groupSelected &&
4505
+ !suppressFrameChromeForChild;
4118
4506
  const screenContentInteractive =
4119
4507
  Boolean(screenContent) &&
4120
4508
  !penActive &&
4121
4509
  !creationToolActive &&
4122
4510
  !canvasGestureActive;
4511
+ // Memoize the srcdoc with the hit-test responder injected so we don't
4512
+ // rebuild the string every render (that would reload the iframe).
4513
+ // Keyed only on screen.content; the hit-test script itself is constant.
4514
+ const srcdocWithHitTest = useMemo(
4515
+ () => appendHitTestResponder(screen.content),
4516
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4517
+ [screen.content],
4518
+ );
4519
+
4123
4520
  const updateDirectHover = useCallback((next: boolean) => {
4124
4521
  setDirectlyHovered((current) => (current === next ? current : next));
4125
4522
  }, []);
@@ -4148,6 +4545,7 @@ const Screen = memo(function Screen({
4148
4545
  return (
4149
4546
  <div
4150
4547
  data-frame-shell
4548
+ data-screen-shell
4151
4549
  className="group/frame pointer-events-auto absolute"
4152
4550
  style={{
4153
4551
  left: SURFACE_PADDING + geometry.x,
@@ -4338,17 +4736,15 @@ const Screen = memo(function Screen({
4338
4736
  <span
4339
4737
  data-screen-content
4340
4738
  className={cn(
4341
- "relative block h-full w-full overflow-hidden rounded-lg border bg-white shadow-2xl transition-colors",
4342
- showHoverChrome
4343
- ? "border-[var(--design-editor-accent-color)]"
4344
- : "border-border",
4739
+ "relative block h-full w-full overflow-hidden rounded-[inherit] bg-white shadow-2xl ring-1 ring-inset ring-border transition-colors",
4345
4740
  )}
4346
4741
  style={{ pointerEvents: screenContentInteractive ? "auto" : "none" }}
4347
4742
  >
4348
4743
  {screenContent ?? (
4349
4744
  <iframe
4745
+ data-screen-iframe-id={screen.id}
4350
4746
  src={previewUrl}
4351
- srcDoc={previewUrl ? undefined : screen.content}
4747
+ srcDoc={previewUrl ? undefined : srcdocWithHitTest}
4352
4748
  sandbox="allow-scripts"
4353
4749
  loading="lazy"
4354
4750
  className="pointer-events-none border-0"
@@ -4377,7 +4773,7 @@ const Screen = memo(function Screen({
4377
4773
  )}
4378
4774
  {creationToolActive ? (
4379
4775
  <span
4380
- className="absolute inset-0 z-20 cursor-crosshair"
4776
+ className="pointer-events-auto absolute inset-0 z-20 cursor-crosshair"
4381
4777
  aria-hidden="true"
4382
4778
  />
4383
4779
  ) : null}
@@ -4388,8 +4784,20 @@ const Screen = memo(function Screen({
4388
4784
  aria-hidden="true"
4389
4785
  />
4390
4786
  ) : null}
4391
- <span className="pointer-events-none absolute inset-0 rounded-[7px] border border-black/5" />
4392
4787
  </span>
4788
+ <span
4789
+ data-screen-hover-outline
4790
+ className={cn(
4791
+ "pointer-events-none absolute inset-0 z-10 rounded-[inherit] border border-[var(--design-editor-accent-color)] transition-opacity",
4792
+ showHoverChrome ? "opacity-100" : "opacity-0",
4793
+ )}
4794
+ style={{
4795
+ borderWidth: 1.5 * chromeScale,
4796
+ transition: getChromeBorderTransition(chromeSettling),
4797
+ }}
4798
+ aria-hidden="true"
4799
+ />
4800
+ <span className="pointer-events-none absolute inset-0 rounded-[inherit] border border-black/5" />
4393
4801
  <ResizeHandles
4394
4802
  active={false}
4395
4803
  enabled={
@@ -4417,6 +4825,7 @@ const Screen = memo(function Screen({
4417
4825
  screen={screen}
4418
4826
  primaryGeometry={geometry}
4419
4827
  previewUrl={previewUrl}
4828
+ srcdocWithHitTest={srcdocWithHitTest}
4420
4829
  activeBreakpointWidth={screen.activeBreakpointWidth}
4421
4830
  penActive={penActive}
4422
4831
  creationToolActive={creationToolActive}
@@ -4465,6 +4874,7 @@ function BreakpointPreviewRow({
4465
4874
  screen,
4466
4875
  primaryGeometry,
4467
4876
  previewUrl,
4877
+ srcdocWithHitTest,
4468
4878
  activeBreakpointWidth,
4469
4879
  penActive,
4470
4880
  creationToolActive,
@@ -4475,6 +4885,13 @@ function BreakpointPreviewRow({
4475
4885
  screen: ScreenFile;
4476
4886
  primaryGeometry: FrameGeometry;
4477
4887
  previewUrl: string | undefined;
4888
+ /**
4889
+ * The primary screen's srcdoc with the lightweight hit-test responder already
4890
+ * injected (memoised in the parent Screen component). Passed down so
4891
+ * breakpoint sub-iframes carry the same responder and can be found via
4892
+ * [data-screen-iframe-id] by the cross-screen drop-into-container handler.
4893
+ */
4894
+ srcdocWithHitTest: string;
4478
4895
  activeBreakpointWidth: number | undefined;
4479
4896
  penActive: boolean;
4480
4897
  creationToolActive: boolean;
@@ -4563,8 +4980,9 @@ function BreakpointPreviewRow({
4563
4980
  style={{ width: frameWidth, height: frameHeight }}
4564
4981
  >
4565
4982
  <iframe
4983
+ data-screen-iframe-id={screen.id}
4566
4984
  src={previewUrl}
4567
- srcDoc={previewUrl ? undefined : screen.content}
4985
+ srcDoc={previewUrl ? undefined : srcdocWithHitTest}
4568
4986
  sandbox="allow-scripts"
4569
4987
  loading="lazy"
4570
4988
  className="pointer-events-none border-0"
@@ -5240,6 +5658,23 @@ function isDraftPrimitive(
5240
5658
  return Boolean(value);
5241
5659
  }
5242
5660
 
5661
+ export function getDraftPreviewGeometryForTool(
5662
+ tool: DraftCreationTool,
5663
+ start: Point,
5664
+ end: Point,
5665
+ hasMoved: boolean,
5666
+ ): FrameGeometry {
5667
+ if (tool === "pen" || tool === "line" || tool === "arrow") {
5668
+ return getDraftGeometryForTool(tool, start, end);
5669
+ }
5670
+
5671
+ if (!hasMoved) {
5672
+ return { x: start.x, y: start.y, width: 0, height: 0 };
5673
+ }
5674
+
5675
+ return getDraftGeometryForTool(tool, start, end);
5676
+ }
5677
+
5243
5678
  function getDraftGeometryForTool(
5244
5679
  tool: DraftCreationTool,
5245
5680
  start: Point,