@agent-native/core 0.169.1 → 0.171.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 (463) hide show
  1. package/agent-native.eject.json +4 -1
  2. package/bin/agent-native.js +23 -5
  3. package/corpus/README.md +1 -1
  4. package/corpus/templates/analytics/.agents/skills/secrets/SKILL.md +11 -4
  5. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  6. package/corpus/templates/analytics/app/i18n-data.ts +28 -0
  7. package/corpus/templates/analytics/app/pages/Agents.tsx +25 -1
  8. package/corpus/templates/analytics/app/routes/$.tsx +10 -0
  9. package/corpus/templates/analytics/app/routes.ts +3 -1
  10. package/corpus/templates/analytics/changelog/2026-08-22-fixed-the-agent-native-templates-first-party-dashboard-panel.md +6 -0
  11. package/corpus/templates/analytics/changelog/2026-08-22-org-admin-panels-no-longer-report-a-database-error-as-a-per.md +9 -0
  12. package/corpus/templates/analytics/changelog/2026-08-22-slack-requests-report-invalid-inputs.md +6 -0
  13. package/corpus/templates/analytics/changelog/2026-08-22-unknown-analytics-links-now-return-a-real-not-found-status.md +6 -0
  14. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +2 -1
  15. package/corpus/templates/analytics/docs/schemas/tracked-events.md +2 -0
  16. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +29 -7
  17. package/corpus/templates/analytics/server/handlers/slack.ts +59 -5
  18. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +11 -6
  19. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +132 -2
  20. package/corpus/templates/analytics/server/lib/slack.ts +4 -2
  21. package/corpus/templates/analytics/vitest.config.ts +6 -1
  22. package/corpus/templates/assets/.agents/skills/secrets/SKILL.md +11 -4
  23. package/corpus/templates/assets/app/routes/library.tsx +5 -3
  24. package/corpus/templates/assets/app/routes.ts +3 -1
  25. package/corpus/templates/assets/changelog/2026-08-22-library-page-loads-again-instead-of-crashing-on-open.md +6 -0
  26. package/corpus/templates/assets/vitest.config.ts +6 -1
  27. package/corpus/templates/brain/.agents/skills/secrets/SKILL.md +11 -4
  28. package/corpus/templates/brain/app/components/ui/pointer-lock.ts +1 -0
  29. package/corpus/templates/brain/app/routes/sources.tsx +8 -3
  30. package/corpus/templates/brain/changelog/2026-08-22-tuning-or-adding-a-source-no-longer-leaves-the-sources-page.md +6 -0
  31. package/corpus/templates/brain/vitest.config.ts +1 -0
  32. package/corpus/templates/calendar/.agents/skills/secrets/SKILL.md +11 -4
  33. package/corpus/templates/calendar/actions/list-booking-links.ts +83 -4
  34. package/corpus/templates/calendar/actions/update-booking-link.ts +4 -3
  35. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +11 -3
  36. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +1 -1
  37. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +103 -44
  38. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +28 -24
  39. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +9 -2
  40. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +47 -58
  41. package/corpus/templates/calendar/app/hooks/use-booking-links.ts +5 -1
  42. package/corpus/templates/calendar/app/lib/event-layout.ts +52 -26
  43. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +126 -98
  44. package/corpus/templates/calendar/app/pages/CalendarView.tsx +1 -0
  45. package/corpus/templates/calendar/app/pages/Settings.tsx +88 -81
  46. package/corpus/templates/calendar/app/routes.ts +3 -1
  47. package/corpus/templates/calendar/changelog/2026-08-22-booking-link-visibility-respects-shared-access.md +6 -0
  48. package/corpus/templates/calendar/changelog/2026-08-22-clicking-an-out-of-office-event-s-marker-no-longer-creates-a.md +6 -0
  49. package/corpus/templates/calendar/changelog/2026-08-22-event-detail-views-now-show-your-calendar-timezone.md +6 -0
  50. package/corpus/templates/calendar/changelog/2026-08-22-google-calendar-can-now-connect-with-the-shared-google-oauth.md +6 -0
  51. package/corpus/templates/calendar/changelog/2026-08-22-overlapping-events-now-show-each-others-boundaries.md +6 -0
  52. package/corpus/templates/calendar/changelog/2026-08-22-public-booking-pages-now-offer-time-slots-from-the-host-s-we.md +6 -0
  53. package/corpus/templates/calendar/server/handlers/bookings.ts +5 -7
  54. package/corpus/templates/calendar/server/handlers/google-auth.ts +10 -2
  55. package/corpus/templates/calendar/server/lib/google-calendar.ts +139 -14
  56. package/corpus/templates/calendar/server/onboarding.ts +85 -0
  57. package/corpus/templates/calendar/server/plugins/agent-chat.ts +2 -0
  58. package/corpus/templates/calendar/shared/api.ts +9 -1
  59. package/corpus/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
  60. package/corpus/templates/chat/app/routes.ts +3 -1
  61. package/corpus/templates/chat/vitest.config.ts +8 -0
  62. package/corpus/templates/clips/.agents/skills/secrets/SKILL.md +11 -4
  63. package/corpus/templates/clips/actions/get-meeting.ts +9 -4
  64. package/corpus/templates/clips/actions/list-recordings.ts +16 -3
  65. package/corpus/templates/clips/actions/search-meetings.ts +1 -0
  66. package/corpus/templates/clips/actions/view-screen.ts +8 -1
  67. package/corpus/templates/clips/app/components/meetings/meeting-history-row.tsx +32 -1
  68. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +1 -1
  69. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +29 -1
  70. package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
  71. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +33 -4
  72. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +1 -0
  73. package/corpus/templates/clips/app/routes/access-request.approve.tsx +16 -9
  74. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +17 -7
  75. package/corpus/templates/clips/app/routes/share.$shareId.tsx +17 -7
  76. package/corpus/templates/clips/changelog/2026-08-21-camera-bubble-black-circle.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-08-22-screen-recordings-respect-the-microphone-toggle.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-08-22-the-recordings-library-no-longer-fails-right-after-server-start.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-08-23-the-agent-can-see-your-library-right-after-server-start.md +6 -0
  80. package/corpus/templates/clips/chrome-extension/src/background.ts +1 -2
  81. package/corpus/templates/clips/chrome-extension/src/native-transcription.ts +39 -7
  82. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +21 -25
  83. package/corpus/templates/clips/desktop/src/app.tsx +53 -24
  84. package/corpus/templates/clips/desktop/src/lib/bubble-playback.ts +68 -0
  85. package/corpus/templates/clips/desktop/src/lib/recorder.ts +3 -0
  86. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +72 -14
  87. package/corpus/templates/clips/desktop/src/lib/updater.ts +8 -3
  88. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +162 -20
  89. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -6
  90. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +150 -13
  91. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +12 -0
  92. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +28 -3
  93. package/corpus/templates/clips/server/lib/emails.ts +17 -33
  94. package/corpus/templates/content/.agents/skills/secrets/SKILL.md +11 -4
  95. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +140 -15
  96. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +12 -14
  97. package/corpus/templates/content/app/components/ui/pointer-lock.ts +1 -0
  98. package/corpus/templates/content/app/hooks/use-content-database.ts +18 -2
  99. package/corpus/templates/content/app/hooks/use-local-storage.ts +38 -22
  100. package/corpus/templates/content/app/i18n-data.ts +25 -0
  101. package/corpus/templates/content/app/routes.ts +3 -1
  102. package/corpus/templates/content/changelog/2026-08-22-code-blocks-inserted-from-the-slash-menu-no-longer-get-stuck.md +6 -0
  103. package/corpus/templates/content/changelog/2026-08-22-the-sidebar-page-tree-no-longer-flashes-empty-after-creating-a-page.md +6 -0
  104. package/corpus/templates/content/docs/solutions/2026-08-21-content-mermaid-rendering-shape.md +233 -0
  105. package/corpus/templates/content/server/plugins/agent-chat.ts +9 -2
  106. package/corpus/templates/content/vitest.config.ts +7 -1
  107. package/corpus/templates/crm/.agents/skills/secrets/SKILL.md +11 -4
  108. package/corpus/templates/design/.agents/skills/secrets/SKILL.md +11 -4
  109. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1383 -88
  110. package/corpus/templates/design/actions/generate-design.ts +98 -47
  111. package/corpus/templates/design/actions/get-design-system.ts +6 -1
  112. package/corpus/templates/design/actions/refresh-design-system-with-builder.ts +146 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +70 -26
  114. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +284 -136
  115. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2057 -141
  116. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +6 -2
  117. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +4 -4
  118. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +10 -1
  119. package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +19 -2
  120. package/corpus/templates/design/app/components/design/multi-screen/types.ts +9 -2
  121. package/corpus/templates/design/app/components/layout/Header.tsx +15 -0
  122. package/corpus/templates/design/app/components/layout/Layout.tsx +7 -2
  123. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -4
  124. package/corpus/templates/design/app/i18n-data.ts +1 -0
  125. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +12 -0
  126. package/corpus/templates/design/app/lib/design-system-data.ts +68 -0
  127. package/corpus/templates/design/app/pages/DesignEditor.tsx +77 -35
  128. package/corpus/templates/design/app/pages/DesignSystems.tsx +136 -42
  129. package/corpus/templates/design/app/pages/Index.tsx +2 -0
  130. package/corpus/templates/design/app/pages/Templates.tsx +0 -10
  131. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +10 -0
  132. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +50 -0
  133. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +1 -1
  134. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +10 -8
  135. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +6 -6
  136. package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +1 -1
  137. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +18 -7
  138. package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +4 -90
  139. package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +2 -1
  140. package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +4 -3
  141. package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +12 -2
  142. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +9 -2
  143. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +1 -1
  144. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +1 -1
  145. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +1 -1
  146. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +1 -1
  147. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +11 -1
  148. package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +11 -19
  149. package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +6 -6
  150. package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +1 -1
  151. package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +12 -15
  152. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -59
  153. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +8 -0
  154. package/corpus/templates/design/app/pages/design-editor/types.ts +3 -3
  155. package/corpus/templates/design/app/routes.ts +3 -1
  156. package/corpus/templates/design/changelog/2026-08-20-canvas-edits-patch-the-frame-instead-of-rebuilding-it.md +6 -0
  157. package/corpus/templates/design/changelog/2026-08-21-builder-design-system-imports-reconcile-after-indexing.md +6 -0
  158. package/corpus/templates/design/changelog/2026-08-21-draw-mode-needs-shift-y.md +6 -0
  159. package/corpus/templates/design/changelog/2026-08-21-eyedropper-shortcut.md +6 -0
  160. package/corpus/templates/design/changelog/2026-08-21-figma-style-alignment-guides-and-snapping.md +6 -0
  161. package/corpus/templates/design/changelog/2026-08-21-group-transforms-keep-the-selection.md +6 -0
  162. package/corpus/templates/design/changelog/2026-08-21-inspector-fields-keep-focus-on-enter.md +6 -0
  163. package/corpus/templates/design/changelog/2026-08-21-pen-alt-keeps-the-incoming-handle.md +6 -0
  164. package/corpus/templates/design/changelog/2026-08-21-pen-on-the-board-creates-one-vector.md +6 -0
  165. package/corpus/templates/design/changelog/2026-08-21-scale-a-multi-selection-from-the-group-handles.md +6 -0
  166. package/corpus/templates/design/changelog/2026-08-21-scale-resizes-text-with-the-object.md +6 -0
  167. package/corpus/templates/design/changelog/2026-08-21-selecting-another-object-keeps-the-scale-tool-armed.md +6 -0
  168. package/corpus/templates/design/changelog/2026-08-21-the-shape-tool-remembers-the-shape-you-picked.md +6 -0
  169. package/corpus/templates/design/changelog/2026-08-21-vectors-drawn-in-a-frame-land-where-you-drew-them.md +6 -0
  170. package/corpus/templates/design/changelog/2026-08-22-multi-screen-generation-no-longer-loses-a-saved-screens-layo.md +6 -0
  171. package/corpus/templates/design/changelog/2026-08-22-rename-and-search-fields-are-labeled-for-screen-readers.md +6 -0
  172. package/corpus/templates/design/package.json +2 -0
  173. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +311 -0
  174. package/corpus/templates/design/shared/canvas-math.ts +612 -206
  175. package/corpus/templates/design/shared/pen-path.ts +37 -0
  176. package/corpus/templates/design/vitest.config.ts +7 -1
  177. package/corpus/templates/dispatch/.agents/skills/secrets/SKILL.md +11 -4
  178. package/corpus/templates/dispatch/actions/list-workspace-connections.ts +34 -3
  179. package/corpus/templates/dispatch/app/routes/integrations.tsx +55 -21
  180. package/corpus/templates/dispatch/app/routes.ts +3 -1
  181. package/corpus/templates/factory/.agents/skills/secrets/SKILL.md +11 -4
  182. package/corpus/templates/factory/.env.example +2 -7
  183. package/corpus/templates/factory/AGENTS.md +3 -0
  184. package/corpus/templates/factory/README.md +5 -3
  185. package/corpus/templates/factory/actions/create-factory.ts +16 -3
  186. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +2 -0
  187. package/corpus/templates/factory/actions/get-triage-item.ts +2 -0
  188. package/corpus/templates/factory/actions/list-factory-audit.ts +116 -36
  189. package/corpus/templates/factory/actions/list-factory-automations.ts +2 -1
  190. package/corpus/templates/factory/actions/list-triage-items.ts +22 -19
  191. package/corpus/templates/factory/actions/run-factory-automation.ts +7 -3
  192. package/corpus/templates/factory/actions/save-factory-automation.ts +5 -2
  193. package/corpus/templates/factory/actions/save-triage-config.ts +2 -8
  194. package/corpus/templates/factory/actions/start-builder-for-item.ts +21 -0
  195. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +229 -297
  196. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +397 -247
  197. package/corpus/templates/factory/app/components/factory/FactorySourceSettingsGroup.tsx +33 -0
  198. package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +202 -162
  199. package/corpus/templates/factory/app/i18n/en-US.ts +32 -4
  200. package/corpus/templates/factory/app/routes/factory.tsx +84 -11
  201. package/corpus/templates/factory/app/routes.ts +3 -1
  202. package/corpus/templates/factory/changelog/2026-08-21-activity-runs-use-automation-display-names.md +6 -0
  203. package/corpus/templates/factory/changelog/2026-08-21-create-and-settings-source-cards.md +6 -0
  204. package/corpus/templates/factory/changelog/2026-08-21-keep-automations-visible-while-running.md +6 -0
  205. package/corpus/templates/factory/changelog/2026-08-21-nested-factory-automations-can-poll.md +6 -0
  206. package/corpus/templates/factory/changelog/2026-08-21-settings-sticky-unsaved-bar.md +6 -0
  207. package/corpus/templates/factory/server/connectors/credentials.ts +10 -5
  208. package/corpus/templates/factory/server/lib/factory-audit-report.ts +412 -0
  209. package/corpus/templates/factory/server/lib/factory-config-reconcile.ts +321 -0
  210. package/corpus/templates/factory/server/lib/factory-scope.ts +25 -2
  211. package/corpus/templates/factory/server/lib/require-factory-automation.ts +13 -4
  212. package/corpus/templates/factory/server/plugins/factory-migrations.ts +148 -4
  213. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +4 -6
  214. package/corpus/templates/factory/vitest.config.ts +17 -2
  215. package/corpus/templates/forms/.agents/skills/secrets/SKILL.md +11 -4
  216. package/corpus/templates/forms/app/routes/_app.settings.tsx +4 -1
  217. package/corpus/templates/forms/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
  218. package/corpus/templates/forms/server/lib/public-form-ssr.ts +11 -1
  219. package/corpus/templates/forms/vitest.config.ts +1 -0
  220. package/corpus/templates/macros/.agents/skills/secrets/SKILL.md +11 -4
  221. package/corpus/templates/macros/app/routes.ts +3 -1
  222. package/corpus/templates/macros/vitest.config.ts +6 -1
  223. package/corpus/templates/mail/.agents/skills/secrets/SKILL.md +11 -4
  224. package/corpus/templates/mail/actions/manage-gmail-filters.ts +10 -2
  225. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -37
  226. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +14 -2
  227. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +66 -36
  228. package/corpus/templates/mail/app/hooks/use-google-auth.ts +2 -0
  229. package/corpus/templates/mail/app/lib/google-oauth-setup.ts +17 -0
  230. package/corpus/templates/mail/app/pages/InboxPage.tsx +69 -9
  231. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  232. package/corpus/templates/mail/app/routes/$view.$threadId.tsx +23 -1
  233. package/corpus/templates/mail/app/routes/$view.tsx +47 -1
  234. package/corpus/templates/mail/app/routes.ts +3 -1
  235. package/corpus/templates/mail/changelog/2026-08-22-gmail-can-now-connect-with-the-shared-google-oauth.md +6 -0
  236. package/corpus/templates/mail/changelog/2026-08-22-gmail-filters-now-shows-the-real-error.md +6 -0
  237. package/corpus/templates/mail/changelog/2026-08-22-unknown-mail-urls-now-show-the-404-page.md +6 -0
  238. package/corpus/templates/mail/server/handlers/google-auth.ts +11 -2
  239. package/corpus/templates/mail/server/lib/google-auth.ts +92 -12
  240. package/corpus/templates/mail/server/onboarding.ts +66 -18
  241. package/corpus/templates/mail/vitest.config.ts +6 -1
  242. package/corpus/templates/plan/.agents/skills/secrets/SKILL.md +11 -4
  243. package/corpus/templates/plan/app/routes/settings.tsx +4 -1
  244. package/corpus/templates/plan/app/routes.ts +3 -1
  245. package/corpus/templates/plan/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
  246. package/corpus/templates/plan/netlify.toml +1 -1
  247. package/corpus/templates/slides/.agents/skills/secrets/SKILL.md +11 -4
  248. package/corpus/templates/slides/actions/patch-deck.ts +79 -10
  249. package/corpus/templates/slides/actions/update-slide.ts +11 -1
  250. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +14 -4
  251. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +10 -52
  252. package/corpus/templates/slides/app/components/editor/GoogleDocImportHint.tsx +8 -53
  253. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +9 -75
  254. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +8 -60
  255. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +10 -2
  256. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +19 -7
  257. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +49 -36
  258. package/corpus/templates/slides/app/routes/access-request.approve.tsx +16 -9
  259. package/corpus/templates/slides/app/routes.ts +3 -1
  260. package/corpus/templates/slides/changelog/2026-08-22-already-uploaded-chat-images-are-no-longer-dropped.md +6 -0
  261. package/corpus/templates/slides/changelog/2026-08-22-google-drive-and-slides-can-now-connect-with-the-shared-google-oauth.md +6 -0
  262. package/corpus/templates/slides/changelog/2026-08-22-multi-slide-repairs-now-report-slides-that-still-overflow.md +6 -0
  263. package/corpus/templates/slides/changelog/2026-08-22-update-slide-now-reports-which-batched-edit-was-skipped.md +6 -0
  264. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +52 -1
  265. package/corpus/templates/slides/server/lib/chat-attachments.ts +29 -3
  266. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +66 -16
  267. package/corpus/templates/slides/server/lib/slide-content-patch.ts +13 -2
  268. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -2
  269. package/corpus/templates/slides/tsconfig.json +5 -0
  270. package/corpus/templates/slides/vitest.config.ts +6 -1
  271. package/corpus/templates/tasks/.agents/skills/secrets/SKILL.md +11 -4
  272. package/corpus/templates/tasks/app/routes/settings.tsx +4 -1
  273. package/corpus/templates/tasks/app/routes.ts +3 -1
  274. package/corpus/templates/tasks/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
  275. package/corpus/templates/tasks/vitest.config.ts +7 -1
  276. package/dist/action.d.ts +91 -0
  277. package/dist/action.js +50 -0
  278. package/dist/agent/abort-reasons.d.ts +38 -0
  279. package/dist/agent/abort-reasons.js +49 -0
  280. package/dist/agent/engine/ai-sdk-engine.js +8 -5
  281. package/dist/agent/engine/anthropic-engine.js +7 -5
  282. package/dist/agent/engine/first-event-timeout.d.ts +42 -17
  283. package/dist/agent/engine/first-event-timeout.js +67 -25
  284. package/dist/agent/production-agent.d.ts +23 -0
  285. package/dist/agent/production-agent.js +107 -102
  286. package/dist/agent/run-loop-with-resume.d.ts +2 -28
  287. package/dist/agent/run-loop-with-resume.js +25 -40
  288. package/dist/agent/run-manager.js +5 -4
  289. package/dist/agent/run-store.d.ts +55 -0
  290. package/dist/agent/run-store.js +124 -1
  291. package/dist/agent/tool-search.js +2 -1
  292. package/dist/agent/types.d.ts +9 -4
  293. package/dist/app-config/auth.d.ts +1 -0
  294. package/dist/app-config/auth.js +7 -0
  295. package/dist/app-config/configuration-error.d.ts +11 -0
  296. package/dist/app-config/configuration-error.js +14 -0
  297. package/dist/app-config/index.d.ts +1 -0
  298. package/dist/app-config/index.js +1 -0
  299. package/dist/app-config/integrations.d.ts +1 -0
  300. package/dist/app-config/integrations.js +10 -0
  301. package/dist/app-config/plugins.d.ts +34 -0
  302. package/dist/app-config/plugins.js +39 -0
  303. package/dist/app-config/run-lifecycle-invariants.d.ts +18 -9
  304. package/dist/app-config/run-lifecycle-invariants.js +18 -25
  305. package/dist/app-config/schema.d.ts +17 -0
  306. package/dist/app-config/schema.js +2 -0
  307. package/dist/chat-threads/schema-migrations.js +20 -0
  308. package/dist/chat-threads/store.js +21 -4
  309. package/dist/cli/native-dependencies.d.ts +4 -0
  310. package/dist/cli/native-dependencies.js +15 -0
  311. package/dist/client/EnvironmentBadge.d.ts +1 -1
  312. package/dist/client/EnvironmentBadge.js +40 -5
  313. package/dist/client/MultiTabAssistantChat.d.ts +1 -1
  314. package/dist/client/MultiTabAssistantChat.js +35 -14
  315. package/dist/client/agent-chat/index.d.ts +1 -1
  316. package/dist/client/agent-chat/index.js +1 -1
  317. package/dist/client/agent-page/use-jobs.d.ts +2 -1
  318. package/dist/client/agent-page/use-jobs.js +3 -5
  319. package/dist/client/agent-sidebar-state.js +3 -1
  320. package/dist/client/blocks/library/code-tabs.js +5 -0
  321. package/dist/client/blocks/library/code.js +1 -0
  322. package/dist/client/chat-first/apps-rail.js +2 -2
  323. package/dist/client/chat-model-groups.d.ts +12 -0
  324. package/dist/client/chat-model-groups.js +3 -2
  325. package/dist/client/integrations/GoogleProductLogo.d.ts +5 -0
  326. package/dist/client/integrations/GoogleProductLogo.js +45 -0
  327. package/dist/client/integrations/index.d.ts +2 -0
  328. package/dist/client/integrations/index.js +2 -0
  329. package/dist/client/integrations/workspace-provider-oauth.d.ts +8 -0
  330. package/dist/client/integrations/workspace-provider-oauth.js +13 -0
  331. package/dist/client/resources/mcp-integration-catalog.js +0 -29
  332. package/dist/client/settings/SettingsTabsPage.js +10 -6
  333. package/dist/client/sharing/ShareButton.js +11 -2
  334. package/dist/client/use-chat-models.d.ts +6 -0
  335. package/dist/client/use-chat-models.js +10 -0
  336. package/dist/client/use-db-sync.js +27 -9
  337. package/dist/collab/routes.d.ts +1 -1
  338. package/dist/collab/struct-routes.d.ts +1 -1
  339. package/dist/connections/catalog.d.ts +111 -9
  340. package/dist/connections/catalog.js +86 -30
  341. package/dist/db/create-get-db.js +18 -0
  342. package/dist/db/ddl-guard.d.ts +0 -4
  343. package/dist/db/ddl-guard.js +46 -0
  344. package/dist/deploy/build.d.ts +19 -0
  345. package/dist/deploy/build.js +190 -16
  346. package/dist/deploy/function-bundle.d.ts +45 -0
  347. package/dist/deploy/function-bundle.js +337 -0
  348. package/dist/deploy/workspace-deploy.js +19 -1
  349. package/dist/eject/workspace-connections.js +4 -0
  350. package/dist/email-catalog/registry.d.ts +4 -0
  351. package/dist/email-catalog/registry.js +79 -14
  352. package/dist/extensions/content-patch.js +14 -2
  353. package/dist/integrations/plugin.d.ts +10 -0
  354. package/dist/integrations/plugin.js +40 -6
  355. package/dist/jobs/actions/run-automation-now.d.ts +2 -1
  356. package/dist/jobs/actions/run-automation-now.js +11 -5
  357. package/dist/jobs/frontmatter.js +53 -2
  358. package/dist/jobs/run-now.d.ts +11 -1
  359. package/dist/jobs/run-now.js +42 -7
  360. package/dist/jobs/scheduler.d.ts +1 -0
  361. package/dist/jobs/scheduler.js +2 -1
  362. package/dist/localization/core-messages/ar-SA.js +3 -0
  363. package/dist/localization/core-messages/de-DE.js +3 -0
  364. package/dist/localization/core-messages/en-US.d.ts +3 -0
  365. package/dist/localization/core-messages/en-US.js +3 -0
  366. package/dist/localization/core-messages/es-ES.js +3 -0
  367. package/dist/localization/core-messages/fr-FR.js +3 -0
  368. package/dist/localization/core-messages/hi-IN.js +3 -0
  369. package/dist/localization/core-messages/ja-JP.js +3 -0
  370. package/dist/localization/core-messages/ko-KR.js +3 -0
  371. package/dist/localization/core-messages/pt-BR.js +3 -0
  372. package/dist/localization/core-messages/zh-CN.js +3 -0
  373. package/dist/localization/core-messages/zh-TW.js +3 -0
  374. package/dist/localization/core-messages.d.ts +3 -0
  375. package/dist/localization/default-messages.d.ts +2 -5
  376. package/dist/localization/default-messages.js +2 -5
  377. package/dist/mcp/build-server.d.ts +9 -0
  378. package/dist/mcp/build-server.js +37 -8
  379. package/dist/mcp-client/oauth-client.d.ts +7 -0
  380. package/dist/mcp-client/oauth-client.js +12 -1
  381. package/dist/oauth-tokens/index.d.ts +1 -1
  382. package/dist/oauth-tokens/index.js +1 -1
  383. package/dist/oauth-tokens/lifecycle.d.ts +11 -0
  384. package/dist/oauth-tokens/lifecycle.js +17 -0
  385. package/dist/oauth-tokens/store.js +7 -0
  386. package/dist/observability/posthog-ai.d.ts +15 -1
  387. package/dist/observability/posthog-ai.js +24 -12
  388. package/dist/observability/traces.js +158 -28
  389. package/dist/onboarding/app-profile.js +2 -2
  390. package/dist/onboarding/plugin.js +10 -2
  391. package/dist/onboarding/types.d.ts +2 -0
  392. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  393. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  394. package/dist/provider-api/index.js +2 -0
  395. package/dist/resources/handlers.d.ts +2 -2
  396. package/dist/resources/store.js +13 -4
  397. package/dist/scripts/agent-engines/list-agent-engines.js +29 -3
  398. package/dist/secrets/register-framework-secrets.js +1 -1
  399. package/dist/server/action-discovery.js +5 -0
  400. package/dist/server/agent-chat/action-filters-a2a.d.ts +27 -3
  401. package/dist/server/agent-chat/action-filters-a2a.js +48 -9
  402. package/dist/server/agent-chat-plugin.js +36 -9
  403. package/dist/server/agent-discovery.js +43 -20
  404. package/dist/server/auth.js +18 -16
  405. package/dist/server/beta-opt-out-html.js +45 -4
  406. package/dist/server/better-auth-instance.d.ts +2 -1
  407. package/dist/server/better-auth-instance.js +37 -7
  408. package/dist/server/builder-design-systems.d.ts +4 -1
  409. package/dist/server/builder-design-systems.js +87 -6
  410. package/dist/server/builder-oauth.d.ts +1 -7
  411. package/dist/server/builder-oauth.js +80 -163
  412. package/dist/server/core-routes-plugin.d.ts +29 -0
  413. package/dist/server/core-routes-plugin.js +100 -20
  414. package/dist/server/default-plugins.d.ts +18 -0
  415. package/dist/server/default-plugins.js +22 -0
  416. package/dist/server/edge.d.ts +1 -0
  417. package/dist/server/edge.js +1 -0
  418. package/dist/server/framework-request-handler.js +31 -3
  419. package/dist/server/google-oauth.d.ts +3 -0
  420. package/dist/server/google-oauth.js +3 -0
  421. package/dist/server/index.d.ts +4 -4
  422. package/dist/server/index.js +3 -3
  423. package/dist/server/onboarding-html.js +37 -3
  424. package/dist/server/realtime-token.d.ts +1 -1
  425. package/dist/server/workspace-provider-oauth.d.ts +6 -1
  426. package/dist/server/workspace-provider-oauth.js +72 -20
  427. package/dist/shared/agent-sidebar-url.d.ts +1 -0
  428. package/dist/shared/agent-sidebar-url.js +1 -0
  429. package/dist/shared/environment-lanes.d.ts +2 -0
  430. package/dist/shared/environment-lanes.js +3 -1
  431. package/dist/shared/index.d.ts +3 -1
  432. package/dist/shared/index.js +3 -1
  433. package/dist/shared/surface-visibility.d.ts +31 -0
  434. package/dist/shared/surface-visibility.js +57 -0
  435. package/dist/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
  436. package/dist/templates/chat/app/routes.ts +3 -1
  437. package/dist/templates/chat/vitest.config.ts +8 -0
  438. package/dist/templates/default/.agents/skills/secrets/SKILL.md +11 -4
  439. package/dist/templates/default/app/routes.ts +3 -1
  440. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +11 -4
  441. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +11 -4
  442. package/dist/tracking/registry.d.ts +11 -0
  443. package/dist/tracking/registry.js +5 -2
  444. package/dist/triggers/actions.js +10 -3
  445. package/dist/usage/store.js +23 -3
  446. package/dist/user-profile/actions/get-auth-methods.js +18 -5
  447. package/dist/vite/client.js +33 -7
  448. package/docs/content/actions-access-control.mdx +43 -4
  449. package/docs/content/actions-defining.mdx +16 -0
  450. package/docs/content/authentication.mdx +40 -21
  451. package/docs/content/environment-variables.mdx +1 -0
  452. package/docs/content/template-clips-capture-everywhere.mdx +29 -30
  453. package/docs/content/template-clips-developers.mdx +4 -4
  454. package/docs/content/template-clips.mdx +3 -1
  455. package/package.json +2 -2
  456. package/src/eject/workspace-connections.ts +4 -0
  457. package/src/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
  458. package/src/templates/chat/app/routes.ts +3 -1
  459. package/src/templates/chat/vitest.config.ts +8 -0
  460. package/src/templates/default/.agents/skills/secrets/SKILL.md +11 -4
  461. package/src/templates/default/app/routes.ts +3 -1
  462. package/src/templates/headless/.agents/skills/secrets/SKILL.md +11 -4
  463. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +11 -4
@@ -40,6 +40,11 @@ declare var __DESIGN_CANVAS_CONTENT_OFFSET_Y__: number;
40
40
  declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
41
41
  declare var __LIVE_REFLOW_ENABLED__: boolean;
42
42
  declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
43
+ /** Head inner HTML of the document this srcdoc was built from. The live head
44
+ * cannot supply it: a blocking script src (the Tailwind runtime) has already
45
+ * injected into it by the time this bridge runs, and adopting that as the
46
+ * source baseline makes the first diff delete the compiled stylesheet. */
47
+ declare var __INITIAL_SOURCE_HEAD__: string;
43
48
 
44
49
  (function () {
45
50
  // Idempotency guard: replace-document-content / srcdoc rebuilds can end up
@@ -323,7 +328,10 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
323
328
  * it, and the re-inserted `<script src>` cannot rebuild it because innerHTML
324
329
  * never executes scripts. The screen then renders unstyled for good.
325
330
  */
326
- var lastSourceHeadHtml: string | null = null;
331
+ var lastSourceHeadHtml: string | null =
332
+ typeof __INITIAL_SOURCE_HEAD__ === "string"
333
+ ? __INITIAL_SOURCE_HEAD__ || null
334
+ : null;
327
335
 
328
336
  /**
329
337
  * Swaps only the nodes the previous source head contributed. Assigning
@@ -332,6 +340,45 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
332
340
  * `<script src>` re-inserted in its place can never rebuild it, because
333
341
  * innerHTML does not execute scripts.
334
342
  */
343
+ /**
344
+ * Stable identity for the head nodes a source document owns and can change
345
+ * in place. Deliberately narrow: anything without a signature is only ever
346
+ * matched by exact `outerHTML`, so a runtime-injected node is never a
347
+ * replacement target.
348
+ */
349
+ function headNodeSignature(node: Element): string {
350
+ var tag = node.tagName.toLowerCase();
351
+ if (tag === "title" || tag === "base") return tag;
352
+ if (tag === "style") {
353
+ var attrs = node.attributes;
354
+ for (var i = 0; i < attrs.length; i += 1) {
355
+ var name = attrs[i]!.name;
356
+ if (name.indexOf("data-agent-native-") === 0) return "style:" + name;
357
+ }
358
+ return "";
359
+ }
360
+ if (tag === "meta") {
361
+ var meta = ["name", "property", "http-equiv", "charset"];
362
+ for (var m = 0; m < meta.length; m += 1) {
363
+ if (node.hasAttribute(meta[m]!)) {
364
+ return "meta:" + meta[m] + "=" + node.getAttribute(meta[m]!);
365
+ }
366
+ }
367
+ return "";
368
+ }
369
+ return "";
370
+ }
371
+
372
+ function findHeadNodeBySignature(signature: string): Element | null {
373
+ var children = document.head ? document.head.children : null;
374
+ if (!children) return null;
375
+ for (var i = 0; i < children.length; i += 1) {
376
+ var candidate = children[i]!;
377
+ if (headNodeSignature(candidate) === signature) return candidate;
378
+ }
379
+ return null;
380
+ }
381
+
335
382
  function replaceSourceHeadNodes(
336
383
  previousSourceHtml: string | null,
337
384
  nextSourceHtml: string,
@@ -344,6 +391,15 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
344
391
  var key = node.outerHTML;
345
392
  staleCounts[key] = (staleCounts[key] || 0) + 1;
346
393
  });
394
+ // A node the next head still carries is not stale. Removing and
395
+ // re-inserting it cancels an async script that has not finished loading,
396
+ // and the replacement is inert — innerHTML-built scripts never execute.
397
+ var retained = document.createElement("head");
398
+ retained.innerHTML = nextSourceHtml || "";
399
+ Array.prototype.forEach.call(retained.children, function (node: Element) {
400
+ var key = node.outerHTML;
401
+ if (staleCounts[key]) staleCounts[key] -= 1;
402
+ });
347
403
  Array.prototype.slice.call(document.head.children).forEach(function (
348
404
  node: Element,
349
405
  ) {
@@ -354,8 +410,39 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
354
410
  });
355
411
  var next = document.createElement("head");
356
412
  next.innerHTML = nextSourceHtml || "";
413
+ var present: Record<string, number> = {};
414
+ Array.prototype.forEach.call(
415
+ document.head.children,
416
+ function (node: Element) {
417
+ var key = node.outerHTML;
418
+ present[key] = (present[key] || 0) + 1;
419
+ },
420
+ );
357
421
  var anchor = document.head.firstChild;
358
422
  Array.prototype.slice.call(next.children).forEach(function (node: Element) {
423
+ // The seed pass below passes a null previous head, so every node it
424
+ // carries would otherwise be inserted on top of the identical one the
425
+ // srcdoc already rendered — two copies of the managed stylesheet.
426
+ var key = node.outerHTML;
427
+ if (present[key]) {
428
+ present[key] -= 1;
429
+ return;
430
+ }
431
+ // Still the seed pass: with no previous head to diff against, an
432
+ // existing node whose CONTENT changed cannot be matched by outerHTML.
433
+ // Left alone it survives alongside its replacement, and because new
434
+ // nodes are prepended the stale one wins the cascade. Match it by
435
+ // identity instead and swap in place. Unmatched live nodes stay put —
436
+ // they are what the page's own runtime injected.
437
+ var signature =
438
+ previousSourceHtml === null ? headNodeSignature(node) : "";
439
+ if (signature) {
440
+ var existing = findHeadNodeBySignature(signature);
441
+ if (existing) {
442
+ document.head.replaceChild(document.importNode(node, true), existing);
443
+ return;
444
+ }
445
+ }
359
446
  document.head.insertBefore(document.importNode(node, true), anchor);
360
447
  });
361
448
  }
@@ -2484,6 +2571,25 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
2484
2571
  "position:fixed;z-index:100000;display:none;pointer-events:none;border-radius:3px;color:white;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:700;padding:2px 4px;box-shadow:0 4px 14px rgba(0,0,0,0.18);";
2485
2572
  document.body.appendChild(spacingBadge);
2486
2573
 
2574
+ // Figma's constraint indicator: dashed lines running from the dragged
2575
+ // element to the frame edges it is pinned to. Distinct from the snap
2576
+ // guides above — this shows how the element will REFLOW when its parent
2577
+ // resizes, not what it is currently aligned with.
2578
+ var constraintGuideLayer = document.createElement("div");
2579
+ constraintGuideLayer.setAttribute(
2580
+ "data-agent-native-edit-overlay",
2581
+ "constraint-guide",
2582
+ );
2583
+ constraintGuideLayer.style.cssText =
2584
+ "position:fixed;inset:0;z-index:99999;display:none;pointer-events:none;";
2585
+ document.body.appendChild(constraintGuideLayer);
2586
+
2587
+ var sizeBadge = document.createElement("div");
2588
+ sizeBadge.setAttribute("data-agent-native-edit-overlay", "size-badge");
2589
+ sizeBadge.style.cssText =
2590
+ "position:fixed;z-index:100000;display:none;pointer-events:none;border-radius:3px;background:var(--design-editor-accent-color);color:var(--design-editor-accent-contrast-color);font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;padding:2px 4px;white-space:nowrap;";
2591
+ document.body.appendChild(sizeBadge);
2592
+
2487
2593
  var insertionGuide = document.createElement("div");
2488
2594
  insertionGuide.setAttribute("data-agent-native-insertion-guide", "");
2489
2595
  insertionGuide.setAttribute(
@@ -2494,24 +2600,18 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
2494
2600
  "position:fixed;z-index:100000;display:none;pointer-events:none;background:var(--design-editor-accent-color);border-radius:999px;box-shadow:0 0 0 1px var(--design-editor-accent-color);";
2495
2601
  document.body.appendChild(insertionGuide);
2496
2602
 
2497
- // Alignment/smart-guide lines shown while dragging (and resizing) an
2498
- // element inside the iframe — Figma-style snap-to-sibling guides. Two
2499
- // shared singleton divs (one per axis) are repositioned per-frame rather
2500
- // than pooled, matching the insertionGuide convention above. Tagged as an
2603
+ // Alignment and spacing guides shown while dragging (and resizing) an
2604
+ // element inside the iframe — Figma-style snap-to-sibling guides. One
2605
+ // container whose children are rebuilt per drag tick, since a snapped
2606
+ // position can sit on any number of guide lines at once. Tagged as an
2501
2607
  // edit-overlay so elementFromEditorPoint/isOverlayElement never treat a
2502
2608
  // guide line as a hit-test or drop target. Color matches the overview
2503
- // canvas's alignment guides (bg-destructive/90) translated to raw CSS.
2504
- var snapGuideV = document.createElement("div");
2505
- snapGuideV.setAttribute("data-agent-native-edit-overlay", "snap-guide");
2506
- snapGuideV.style.cssText =
2507
- "position:fixed;z-index:100000;display:none;pointer-events:none;width:1px;background:hsl(var(--destructive) / 0.9);";
2508
- document.body.appendChild(snapGuideV);
2509
-
2510
- var snapGuideH = document.createElement("div");
2511
- snapGuideH.setAttribute("data-agent-native-edit-overlay", "snap-guide");
2512
- snapGuideH.style.cssText =
2513
- "position:fixed;z-index:100000;display:none;pointer-events:none;height:1px;background:hsl(var(--destructive) / 0.9);";
2514
- document.body.appendChild(snapGuideH);
2609
+ // canvas's alignment guides, in the forwarded measure colour.
2610
+ var snapGuideLayer = document.createElement("div");
2611
+ snapGuideLayer.setAttribute("data-agent-native-edit-overlay", "snap-guide");
2612
+ snapGuideLayer.style.cssText =
2613
+ "position:fixed;inset:0;z-index:100000;display:none;pointer-events:none;";
2614
+ document.body.appendChild(snapGuideLayer);
2515
2615
 
2516
2616
  var measurementOverlay = document.createElement("div");
2517
2617
  measurementOverlay.setAttribute("data-agent-native-measurement-overlay", "");
@@ -2705,6 +2805,7 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
2705
2805
 
2706
2806
  function hideSelectionOverlay(): void {
2707
2807
  selectionOverlay.style.display = "none";
2808
+ hideSizeBadge();
2708
2809
  hideSpacingOverlay();
2709
2810
  hideParentAutoLayoutOverlay();
2710
2811
  clearComponentTag();
@@ -3024,7 +3125,9 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
3024
3125
  );
3025
3126
  handle.setAttribute("data-corner", pos);
3026
3127
  handle.style.cssText =
3027
- "position:absolute;z-index:1;width:7px;height:7px;border:1px solid var(--design-editor-accent-color);background:var(--design-editor-accent-contrast-color);box-sizing:border-box;border-radius:1px;pointer-events:none;";
3128
+ "position:absolute;z-index:1;width:7px;height:7px;border:1px solid var(--design-editor-accent-color);background:var(--design-editor-accent-contrast-color);box-sizing:border-box;border-radius:1px;pointer-events:auto;cursor:" +
3129
+ (pos === "nw" || pos === "se" ? "nwse-resize" : "nesw-resize") +
3130
+ ";";
3028
3131
  if (pos.indexOf("n") !== -1) handle.style.top = "-4px";
3029
3132
  if (pos.indexOf("s") !== -1) handle.style.bottom = "-4px";
3030
3133
  if (pos.indexOf("w") !== -1) handle.style.left = "-4px";
@@ -3061,6 +3164,8 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
3061
3164
  )
3062
3165
  .forEach(function (handle) {
3063
3166
  var pos = handle.getAttribute("data-corner") || "";
3167
+ // A viewer must not get four dead 7px squares over their content.
3168
+ handle.style.pointerEvents = readOnly ? "none" : "auto";
3064
3169
  handle.style.width = 7 * sx + "px";
3065
3170
  handle.style.height = 7 * sy + "px";
3066
3171
  handle.style.borderWidth = 1 * line + "px";
@@ -3200,6 +3305,513 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
3200
3305
  });
3201
3306
  }
3202
3307
 
3308
+ interface MorphContext {
3309
+ keyed: Map<string, Element>;
3310
+ nextKeys: Set<string>;
3311
+ /** Keys whose live element cannot be reused because its tag changed, so
3312
+ * the unkeyed probe must treat it as doomed even though the key lives on
3313
+ * in the next document. */
3314
+ obsolete: Set<string>;
3315
+ }
3316
+
3317
+ /**
3318
+ * What the SOURCE document declared for one element, as of the last time the
3319
+ * source was applied to it.
3320
+ *
3321
+ * The morph needs this because the live DOM is authored by two writers: the
3322
+ * design source, and whatever runtime the prototype loads (Alpine above all).
3323
+ * Only the source's own contributions may be reconciled away — an x-show
3324
+ * `display:none`, an `:class` token or an x-for clone has no counterpart in
3325
+ * source and must survive an unrelated edit. Inferring that from directive
3326
+ * names cannot work: it misses plugins, and it cannot tell an authored class
3327
+ * the user just deleted from a class Alpine computed.
3328
+ */
3329
+ interface SourceMeta {
3330
+ attrs: string[];
3331
+ className: string;
3332
+ style: string;
3333
+ }
3334
+
3335
+ function sourceMetaFor(element: Element): SourceMeta | undefined {
3336
+ return (element as Element & { __anSourceMeta?: SourceMeta })
3337
+ .__anSourceMeta;
3338
+ }
3339
+
3340
+ function isSourceOwned(node: Node): boolean {
3341
+ return (node as Node & { __anSource?: boolean }).__anSource === true;
3342
+ }
3343
+
3344
+ function recordSourceOwnership(node: Node): void {
3345
+ (node as Node & { __anSource?: boolean }).__anSource = true;
3346
+ if (node.nodeType !== 1) return;
3347
+ var element = node as Element;
3348
+ var names: string[] = [];
3349
+ for (var i = 0; i < element.attributes.length; i += 1) {
3350
+ names.push(element.attributes[i]!.name);
3351
+ }
3352
+ (element as Element & { __anSourceMeta?: SourceMeta }).__anSourceMeta = {
3353
+ attrs: names,
3354
+ className: element.getAttribute("class") ?? "",
3355
+ style: element.getAttribute("style") ?? "",
3356
+ };
3357
+ }
3358
+
3359
+ function recordSourceSubtree(root: Node): void {
3360
+ if (
3361
+ root.nodeType === 1 &&
3362
+ (root as Element).hasAttribute("data-agent-native-edit-overlay")
3363
+ ) {
3364
+ return;
3365
+ }
3366
+ recordSourceOwnership(root);
3367
+ if (root.nodeType !== 1) return;
3368
+ var template = templateContentOf(root as Element);
3369
+ if (template) {
3370
+ var held = template.childNodes;
3371
+ for (var t = 0; t < held.length; t += 1) recordSourceSubtree(held[t]!);
3372
+ return;
3373
+ }
3374
+ var children = (root as Element).childNodes;
3375
+ for (var i = 0; i < children.length; i += 1) {
3376
+ recordSourceSubtree(children[i]!);
3377
+ }
3378
+ }
3379
+
3380
+ /** A template's authored children live in `.content`, not as child nodes, so
3381
+ * a walk over childNodes silently ignores every edit inside an x-if/x-for
3382
+ * template until Alpine instantiates the stale markup. */
3383
+ function templateContentOf(element: Element): DocumentFragment | null {
3384
+ if (element.nodeName !== "TEMPLATE") return null;
3385
+ return (element as HTMLTemplateElement).content ?? null;
3386
+ }
3387
+
3388
+ function scopedMorphContext(
3389
+ liveRoot: ParentNode,
3390
+ nextRoot: ParentNode,
3391
+ ): MorphContext {
3392
+ var keyed = new Map<string, Element>();
3393
+ liveRoot.querySelectorAll("[data-agent-native-node-id]").forEach(function (
3394
+ element: Element,
3395
+ ) {
3396
+ if (!isSourceOwned(element)) return;
3397
+ var key = element.getAttribute("data-agent-native-node-id");
3398
+ if (key && !keyed.has(key)) keyed.set(key, element);
3399
+ });
3400
+ var nextKeys = new Set<string>();
3401
+ nextRoot.querySelectorAll("[data-agent-native-node-id]").forEach(function (
3402
+ element: Element,
3403
+ ) {
3404
+ var key = element.getAttribute("data-agent-native-node-id");
3405
+ if (key) nextKeys.add(key);
3406
+ });
3407
+ return { keyed: keyed, nextKeys: nextKeys, obsolete: new Set<string>() };
3408
+ }
3409
+
3410
+ /**
3411
+ * Seeds ownership from the document the srcdoc was built from: this runs
3412
+ * inline at the end of body during parsing, before any deferred script, so
3413
+ * Alpine and every other deferred runtime has yet to render.
3414
+ *
3415
+ * Two things it cannot see. The head is already contaminated — a blocking
3416
+ * script src such as the Tailwind runtime has injected there — which is why
3417
+ * the head baseline is baked in at build time instead. And DOM appended by
3418
+ * the design's OWN synchronous body scripts, which ran before this point, is
3419
+ * indistinguishable from authored markup and will be treated as source. Our
3420
+ * injected bridges are exempt: everything they add carries
3421
+ * data-agent-native-edit-overlay, which recordSourceSubtree skips.
3422
+ */
3423
+ function captureInitialSourceOwnership(): void {
3424
+ if (document.body) recordSourceSubtree(document.body);
3425
+ }
3426
+
3427
+ function classTokens(value: string): string[] {
3428
+ return value.split(/\s+/).filter(function (token) {
3429
+ return token.length > 0;
3430
+ });
3431
+ }
3432
+
3433
+ /** Keeps class tokens the runtime added while applying the source's edit. */
3434
+ function applyClassAttribute(
3435
+ live: Element,
3436
+ previousSource: string,
3437
+ nextSource: string,
3438
+ ): void {
3439
+ var previous = classTokens(previousSource);
3440
+ var current = classTokens(live.getAttribute("class") ?? "");
3441
+ var result = classTokens(nextSource);
3442
+ for (var i = 0; i < current.length; i += 1) {
3443
+ var token = current[i]!;
3444
+ if (previous.indexOf(token) !== -1) continue;
3445
+ if (result.indexOf(token) === -1) result.push(token);
3446
+ }
3447
+ var value = result.join(" ");
3448
+ if ((live.getAttribute("class") ?? "") === value) return;
3449
+ if (value) live.setAttribute("class", value);
3450
+ else live.removeAttribute("class");
3451
+ }
3452
+
3453
+ /** Parsed through a real CSSStyleDeclaration so quoted values and
3454
+ * `!important` survive a round trip that a split on ";" would corrupt. */
3455
+ var styleProbe: HTMLElement | null = null;
3456
+
3457
+ function styleDeclarations(value: string): Array<[string, string, string]> {
3458
+ var probe = styleProbe || (styleProbe = document.createElement("div"));
3459
+ probe.style.cssText = value || "";
3460
+ var out: Array<[string, string, string]> = [];
3461
+ for (var i = 0; i < probe.style.length; i += 1) {
3462
+ var property = probe.style.item(i);
3463
+ out.push([
3464
+ property,
3465
+ probe.style.getPropertyValue(property),
3466
+ probe.style.getPropertyPriority(property),
3467
+ ]);
3468
+ }
3469
+ return out;
3470
+ }
3471
+
3472
+ /** Property-level counterpart to applyClassAttribute: a property the source
3473
+ * never declared belongs to the runtime (x-show writes display). */
3474
+ function applyStyleAttribute(
3475
+ live: Element,
3476
+ previousSource: string,
3477
+ nextSource: string,
3478
+ ): void {
3479
+ var previousOwned: Record<string, string> = {};
3480
+ styleDeclarations(previousSource).forEach(function (entry) {
3481
+ previousOwned[entry[0]] = entry[1];
3482
+ });
3483
+ var nextOwned: Record<string, true> = {};
3484
+ styleDeclarations(nextSource).forEach(function (entry) {
3485
+ nextOwned[entry[0]] = true;
3486
+ });
3487
+ var target = document.createElement("div");
3488
+ target.style.cssText = nextSource || "";
3489
+ styleDeclarations(live.getAttribute("style") ?? "").forEach(
3490
+ function (entry) {
3491
+ if (nextOwned[entry[0]]) return;
3492
+ var wasSource = Object.prototype.hasOwnProperty.call(
3493
+ previousOwned,
3494
+ entry[0],
3495
+ );
3496
+ // Source declared this property and the live value still matches, so
3497
+ // it is the source's to drop. A live value that has diverged is the
3498
+ // runtime's — x-show writing display over an authored one — and
3499
+ // dropping it un-hides the element.
3500
+ if (wasSource && previousOwned[entry[0]] === entry[1]) return;
3501
+ target.style.setProperty(entry[0], entry[1], entry[2]);
3502
+ },
3503
+ );
3504
+ var value = target.style.cssText;
3505
+ if ((live.getAttribute("style") ?? "") === value) return;
3506
+ if (value) live.setAttribute("style", value);
3507
+ else live.removeAttribute("style");
3508
+ }
3509
+
3510
+ /** Form controls carry live state in properties the attributes do not
3511
+ * mirror. Gated on the DEFAULT changing, so a value the user typed is never
3512
+ * overwritten by an unrelated edit. */
3513
+ function morphFormState(live: Element, next: Element): void {
3514
+ if (live.nodeName === "INPUT") {
3515
+ var input = live as HTMLInputElement;
3516
+ var nextChecked = next.hasAttribute("checked");
3517
+ if (input.defaultChecked !== nextChecked) {
3518
+ input.defaultChecked = nextChecked;
3519
+ input.checked = nextChecked;
3520
+ }
3521
+ var nextValue = next.getAttribute("value");
3522
+ if (nextValue !== null && input.defaultValue !== nextValue) {
3523
+ input.defaultValue = nextValue;
3524
+ input.value = nextValue;
3525
+ }
3526
+ return;
3527
+ }
3528
+ if (live.nodeName === "TEXTAREA") {
3529
+ var area = live as HTMLTextAreaElement;
3530
+ var nextText = next.textContent ?? "";
3531
+ if (area.defaultValue !== nextText) {
3532
+ area.defaultValue = nextText;
3533
+ area.value = nextText;
3534
+ }
3535
+ return;
3536
+ }
3537
+ if (live.nodeName === "OPTION") {
3538
+ var option = live as HTMLOptionElement;
3539
+ var nextSelected = next.hasAttribute("selected");
3540
+ if (option.defaultSelected !== nextSelected) {
3541
+ option.defaultSelected = nextSelected;
3542
+ option.selected = nextSelected;
3543
+ }
3544
+ }
3545
+ }
3546
+
3547
+ /** x-text and x-html hand their entire child list to the runtime; whatever
3548
+ * the source still carries inside them is pre-hydration fallback, not
3549
+ * content to reconcile. */
3550
+ function declaresRuntimeChildren(element: Element): boolean {
3551
+ return (
3552
+ element.hasAttribute("x-text") ||
3553
+ element.hasAttribute("x-html") ||
3554
+ element.hasAttribute("v-text") ||
3555
+ element.hasAttribute("v-html")
3556
+ );
3557
+ }
3558
+
3559
+ function scopeDirectiveChanged(live: Element, next: Element): boolean {
3560
+ return (
3561
+ (live.getAttribute("x-data") ?? "") !==
3562
+ (next.getAttribute("x-data") ?? "")
3563
+ );
3564
+ }
3565
+
3566
+ function morphNodeKey(node: Node): string | null {
3567
+ if (node.nodeType !== 1) return null;
3568
+ return (node as Element).getAttribute("data-agent-native-node-id");
3569
+ }
3570
+
3571
+ function morphAttributes(live: Element, next: Element): void {
3572
+ var meta = sourceMetaFor(live);
3573
+ var previousAttrs = meta ? meta.attrs : [];
3574
+ var previousClass = meta ? meta.className : "";
3575
+ var previousStyle = meta ? meta.style : "";
3576
+ var nextNames: string[] = [];
3577
+
3578
+ var nextAttrs = next.attributes;
3579
+ for (var i = 0; i < nextAttrs.length; i += 1) {
3580
+ var attr = nextAttrs[i]!;
3581
+ nextNames.push(attr.name);
3582
+ if (attr.name === "class") {
3583
+ applyClassAttribute(live, previousClass, attr.value);
3584
+ continue;
3585
+ }
3586
+ if (attr.name === "style") {
3587
+ applyStyleAttribute(live, previousStyle, attr.value);
3588
+ continue;
3589
+ }
3590
+ // Alpine strips x-cloak the moment it initialises a tree. Source still
3591
+ // carries it, and putting it back re-hides an element that is running.
3592
+ if (attr.name === "x-cloak" && !live.hasAttribute("x-cloak")) continue;
3593
+ if (live.getAttribute(attr.name) === attr.value) continue;
3594
+ // A namespaced attribute (xlink:href inside an SVG) set through the
3595
+ // plain setter becomes an inert same-named attribute the renderer
3596
+ // ignores.
3597
+ if (attr.namespaceURI) {
3598
+ live.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
3599
+ } else {
3600
+ live.setAttribute(attr.name, attr.value);
3601
+ }
3602
+ }
3603
+
3604
+ for (var j = 0; j < previousAttrs.length; j += 1) {
3605
+ var name = previousAttrs[j]!;
3606
+ if (nextNames.indexOf(name) !== -1) continue;
3607
+ if (name === "class") {
3608
+ applyClassAttribute(live, previousClass, "");
3609
+ continue;
3610
+ }
3611
+ if (name === "style") {
3612
+ applyStyleAttribute(live, previousStyle, "");
3613
+ continue;
3614
+ }
3615
+ live.removeAttribute(name);
3616
+ }
3617
+
3618
+ (live as Element & { __anSourceMeta?: SourceMeta }).__anSourceMeta = {
3619
+ attrs: nextNames,
3620
+ className: next.getAttribute("class") ?? "",
3621
+ style: next.getAttribute("style") ?? "",
3622
+ };
3623
+ }
3624
+
3625
+ function morphChildren(
3626
+ live: Element | DocumentFragment,
3627
+ next: Element | DocumentFragment,
3628
+ context: MorphContext,
3629
+ ): void {
3630
+ var cursor = live.firstChild;
3631
+ var nextChild = next.firstChild;
3632
+ while (nextChild) {
3633
+ var key = morphNodeKey(nextChild);
3634
+ var reuse: Node | null = null;
3635
+ if (key) {
3636
+ var candidate = context.keyed.get(key) ?? null;
3637
+ // A keyed node that already contains this parent cannot be moved
3638
+ // inside itself; recreate it instead of building a cycle. A candidate
3639
+ // whose tag changed cannot be morphed into the new one either — the
3640
+ // iframe would keep a div where source now says button.
3641
+ if (
3642
+ candidate &&
3643
+ !candidate.contains(live) &&
3644
+ candidate.nodeName === nextChild.nodeName &&
3645
+ candidate.namespaceURI === (nextChild as Element).namespaceURI
3646
+ ) {
3647
+ reuse = candidate;
3648
+ // Claim it: authored markup can repeat an id, and reusing one live
3649
+ // element for both would move the same node twice and drop one.
3650
+ context.keyed.delete(key);
3651
+ } else if (candidate) {
3652
+ context.obsolete.add(key);
3653
+ }
3654
+ } else {
3655
+ // Skip live siblings that cannot be this source child: runtime output,
3656
+ // and keyed nodes already destined to disappear. Stopping at one would
3657
+ // import a fresh copy of the unchanged node behind it and destroy the
3658
+ // original's listeners and state.
3659
+ var probe: Node | null = cursor;
3660
+ while (probe) {
3661
+ var probeKey = morphNodeKey(probe);
3662
+ if (probeKey) {
3663
+ if (
3664
+ context.nextKeys.has(probeKey) &&
3665
+ !context.obsolete.has(probeKey)
3666
+ ) {
3667
+ break;
3668
+ }
3669
+ probe = probe.nextSibling;
3670
+ continue;
3671
+ }
3672
+ if (!isSourceOwned(probe)) {
3673
+ probe = probe.nextSibling;
3674
+ continue;
3675
+ }
3676
+ if (
3677
+ probe.nodeType === nextChild.nodeType &&
3678
+ probe.nodeName === nextChild.nodeName
3679
+ ) {
3680
+ reuse = probe;
3681
+ }
3682
+ break;
3683
+ }
3684
+ }
3685
+ if (
3686
+ reuse &&
3687
+ reuse.nodeType === 1 &&
3688
+ scopeDirectiveChanged(reuse as Element, nextChild as Element)
3689
+ ) {
3690
+ // Rebuild just this component rather than the frame: Alpine evaluates
3691
+ // x-data once at init, so patching it in place leaves every binding
3692
+ // underneath reading the previous scope. Alpine's own observer
3693
+ // initialises the replacement.
3694
+ var rebuilt = document.importNode(nextChild as Element, true);
3695
+ // Anchor on `cursor`, not on `reuse`: a keyed candidate can live
3696
+ // anywhere in the document, so when this parent is itself newly
3697
+ // inserted the old node is not its child and insertBefore throws.
3698
+ live.insertBefore(rebuilt, cursor);
3699
+ if (reuse.parentNode) reuse.parentNode.removeChild(reuse);
3700
+ recordSourceSubtree(rebuilt);
3701
+ cursor = rebuilt.nextSibling;
3702
+ nextChild = nextChild.nextSibling;
3703
+ continue;
3704
+ }
3705
+ if (reuse) {
3706
+ if (reuse !== cursor) live.insertBefore(reuse, cursor);
3707
+ if (reuse.nodeType === 1) {
3708
+ morphElement(reuse as Element, nextChild as Element, context);
3709
+ } else if (reuse.nodeValue !== nextChild.nodeValue) {
3710
+ reuse.nodeValue = nextChild.nodeValue;
3711
+ }
3712
+ cursor = reuse.nextSibling;
3713
+ } else if (nextChild.nodeType === 1) {
3714
+ // Shell first, then reconcile: a deep import would clone keyed
3715
+ // descendants that already exist live, and the originals would be
3716
+ // swept as stale right after. Wrapping a component in a new parent
3717
+ // (the Group action) has to move the existing child, not rebuild it.
3718
+ var shell = document.importNode(nextChild as Element, false) as Element;
3719
+ live.insertBefore(shell, cursor);
3720
+ recordSourceOwnership(shell);
3721
+ morphElement(shell, nextChild as Element, context);
3722
+ // That reconcile can pull `cursor` itself into the shell, leaving the
3723
+ // outer walk holding a node this parent no longer owns.
3724
+ cursor = shell.nextSibling;
3725
+ } else {
3726
+ var imported = document.importNode(nextChild, true);
3727
+ live.insertBefore(imported, cursor);
3728
+ recordSourceSubtree(imported);
3729
+ }
3730
+ nextChild = nextChild.nextSibling;
3731
+ }
3732
+ while (cursor) {
3733
+ var stale = cursor;
3734
+ cursor = cursor.nextSibling;
3735
+ // A keyed node can have been moved into a subtree inserted earlier in
3736
+ // this same pass; it is no longer this parent's to remove.
3737
+ if (stale.parentNode !== live) continue;
3738
+ // Only the source's own children are the source's to delete. What is
3739
+ // left is runtime output — an x-for clone, x-text's text node — and the
3740
+ // runtime holds the same parent element, so it never re-renders it.
3741
+ if (!isSourceOwned(stale)) continue;
3742
+ live.removeChild(stale);
3743
+ }
3744
+ }
3745
+
3746
+ function morphElement(
3747
+ live: Element,
3748
+ next: Element,
3749
+ context: MorphContext,
3750
+ ): void {
3751
+ // Before morphAttributes: writing the `value` attribute moves
3752
+ // defaultValue, and the guard inside morphFormState reads defaultValue to
3753
+ // decide whether the SOURCE default changed. Run it after and a dirty
3754
+ // input never sees an explicit source edit.
3755
+ morphFormState(live, next);
3756
+ morphAttributes(live, next);
3757
+ if (declaresRuntimeChildren(next) || declaresRuntimeChildren(live)) return;
3758
+ var liveTemplate = templateContentOf(live);
3759
+ var nextTemplate = templateContentOf(next);
3760
+ if (liveTemplate && nextTemplate) {
3761
+ // Its own key scope: a node id can legitimately appear both inside a
3762
+ // template and in the instantiated body, and the outer map must not
3763
+ // hand the live one over to the template.
3764
+ morphChildren(
3765
+ liveTemplate,
3766
+ nextTemplate,
3767
+ scopedMorphContext(liveTemplate, nextTemplate),
3768
+ );
3769
+ return;
3770
+ }
3771
+ morphChildren(live, next, context);
3772
+ }
3773
+
3774
+ /**
3775
+ * Reconcile the live body against the parsed next document, reusing every
3776
+ * node whose `data-agent-native-node-id` is unchanged.
3777
+ *
3778
+ * Never `body.innerHTML = next`. That rebuilds every node in the screen, so
3779
+ * editing one element restarts Alpine components, CSS transitions and media,
3780
+ * drops focus and inner scroll positions, and re-decodes every image — the
3781
+ * "the frame refreshed" report. This walk touches only what differs.
3782
+ *
3783
+ * Like innerHTML it does not execute an inserted script, so a changed script
3784
+ * is still the caller's cue to rebuild the document — see
3785
+ * `runtimeDocumentNeedsReload` in DesignCanvas.tsx.
3786
+ */
3787
+ function morphRuntimeBody(nextBody: Element): void {
3788
+ var keyed = new Map<string, Element>();
3789
+ document.querySelectorAll("[data-agent-native-node-id]").forEach(function (
3790
+ element: Element,
3791
+ ) {
3792
+ // An x-for clone copies the template's markup, node id and all. Indexing
3793
+ // one would let the morph adopt a runtime clone as the source node and
3794
+ // move it out of the list it belongs to.
3795
+ if (!isSourceOwned(element)) return;
3796
+ var key = element.getAttribute("data-agent-native-node-id");
3797
+ // First occurrence wins: a duplicated id in authored markup must not
3798
+ // let a later node steal an earlier node's identity.
3799
+ if (key && !keyed.has(key)) keyed.set(key, element);
3800
+ });
3801
+ var nextKeys = new Set<string>();
3802
+ nextBody.querySelectorAll("[data-agent-native-node-id]").forEach(function (
3803
+ element: Element,
3804
+ ) {
3805
+ var key = element.getAttribute("data-agent-native-node-id");
3806
+ if (key) nextKeys.add(key);
3807
+ });
3808
+ morphElement(document.body, nextBody, {
3809
+ keyed: keyed,
3810
+ nextKeys: nextKeys,
3811
+ obsolete: new Set<string>(),
3812
+ });
3813
+ }
3814
+
3203
3815
  function replaceRuntimeDocument(
3204
3816
  html: string,
3205
3817
  preferredSelector: string,
@@ -3283,9 +3895,15 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
3283
3895
  var nextHeadHtml = nextDoc.head ? nextDoc.head.innerHTML : "";
3284
3896
  ensureEditorChromeStyle();
3285
3897
  if (lastSourceHeadHtml === null) {
3286
- // First patch after a srcdoc build, so the document already carries this
3287
- // source head. Forced replacements adopt too: treating the live head as
3288
- // the baseline is what wipes the runtime's own nodes.
3898
+ // First patch after a srcdoc build. The document already carries the
3899
+ // head it was built from, so this seeds the baseline — but it cannot
3900
+ // just adopt: when the first patch is itself a head edit (a breakpoint,
3901
+ // motion or token write, none of which reload the frame any more),
3902
+ // adopting means that stylesheet never reaches the live document and
3903
+ // every later diff is measured against a head that was never applied.
3904
+ // Insert only what is genuinely new; replaceSourceHeadNodes skips nodes
3905
+ // already present.
3906
+ replaceSourceHeadNodes(null, nextHeadHtml);
3289
3907
  lastSourceHeadHtml = nextHeadHtml;
3290
3908
  }
3291
3909
  var currentHeadHtml = lastSourceHeadHtml;
@@ -3376,17 +3994,12 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
3376
3994
  ensureEditorChromeStyle();
3377
3995
  lastSourceHeadHtml = nextHeadHtml;
3378
3996
  }
3379
- Array.prototype.slice.call(document.body.attributes).forEach(function (
3380
- attribute: Attr,
3381
- ) {
3382
- document.body.removeAttribute(attribute.name);
3383
- });
3384
- Array.prototype.slice.call(nextDoc.body.attributes).forEach(function (
3385
- attribute: Attr,
3386
- ) {
3387
- document.body.setAttribute(attribute.name, attribute.value);
3997
+ // Detached first so the keyed walk below never sees editor chrome as a
3998
+ // stale child of the source document and removes it.
3999
+ persistentNodes.forEach(function (node) {
4000
+ if (node.parentNode) node.parentNode.removeChild(node);
3388
4001
  });
3389
- document.body.innerHTML = nextDoc.body.innerHTML;
4002
+ morphRuntimeBody(nextDoc.body);
3390
4003
  persistentNodes.forEach(function (node) {
3391
4004
  document.body.appendChild(node);
3392
4005
  });
@@ -4551,6 +5164,19 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
4551
5164
  overlay.style.cssText =
4552
5165
  "position:fixed;pointer-events:none;z-index:99996;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;";
4553
5166
  appendPassiveSelectionHandles(overlay);
5167
+ overlay.addEventListener(
5168
+ "mousedown",
5169
+ function (e) {
5170
+ if (readOnly) return;
5171
+ var corner =
5172
+ e.target &&
5173
+ (e.target as Element).getAttribute &&
5174
+ (e.target as Element).getAttribute("data-corner");
5175
+ if (!corner) return;
5176
+ startGroupResize(corner, e);
5177
+ },
5178
+ true,
5179
+ );
4554
5180
  document.body.appendChild(overlay);
4555
5181
  multiSelectionBoundsOverlay = overlay;
4556
5182
  return overlay;
@@ -4564,6 +5190,11 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
4564
5190
  passiveSelectionEls.forEach(function (el) {
4565
5191
  if (el && document.documentElement.contains(el)) members.push(el);
4566
5192
  });
5193
+ // One set of handles per multi-selection: the primary member's own sit
5194
+ // over the group's at a shared corner and win the hit test.
5195
+ setSelectionOverlayResizeChromeVisible(
5196
+ !readOnly && !activeTextEditEl && members.length < 2,
5197
+ );
4567
5198
  if (members.length < 2 || selectionChromeHidden) {
4568
5199
  if (multiSelectionBoundsOverlay) {
4569
5200
  multiSelectionBoundsOverlay.style.display = "none";
@@ -4634,6 +5265,7 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
4634
5265
  // falls back to its own read in that case.
4635
5266
  updateComponentTag(el, rect);
4636
5267
  updateParentAutoLayoutOverlay(el);
5268
+ showSizeBadge(el);
4637
5269
  } else {
4638
5270
  applyElementOverlayChrome(overlay, el);
4639
5271
  }
@@ -4674,6 +5306,9 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
4674
5306
  var overlay = passiveSelectionOverlays[index];
4675
5307
  if (overlay) positionOverlay(overlay, el);
4676
5308
  });
5309
+ // A multi-selection has no single size to report; positionOverlay owns
5310
+ // the badge for the single-selection case.
5311
+ if (passiveSelectionEls.length > 0) hideSizeBadge();
4677
5312
  positionMultiSelectionBounds();
4678
5313
  positionGradientOverlay();
4679
5314
  syncOverlayObservers();
@@ -7260,17 +7895,10 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
7260
7895
  return true;
7261
7896
  }
7262
7897
 
7263
- // Multi-select group move: when the user drags an element that is a member
7264
- // of the current multi-selection (primary selectedEl + the passive
7265
- // shift-click/marquee set), the whole group moves together — Figma
7266
- // behavior. Returns the full member list in DOCUMENT ORDER (so a group
7267
- // flow-insert lands the members consecutively in their existing visual
7268
- // order) when gestureEl belongs to a 2+ selection, or just [gestureEl]
7269
- // otherwise. Members nested inside another member are dropped: moving the
7270
- // ancestor already moves them, and double-applying the delta would fling
7271
- // them.
7272
- function collectMoveGroupMembers(gestureEl: Element): Element[] {
7273
- if (!gestureEl) return [];
7898
+ /** The current multi-selection's own elements: the primary plus the passive
7899
+ * shift-click/marquee set, minus blocked layers and anything contained by
7900
+ * another member. */
7901
+ function collectSelectionMembers(): Element[] {
7274
7902
  var raw: Element[] = [];
7275
7903
  if (selectedEl) raw.push(selectedEl);
7276
7904
  for (var i = 0; i < passiveSelectionEls.length; i += 1) {
@@ -7291,12 +7919,25 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
7291
7919
  }
7292
7920
  members.push(candidate);
7293
7921
  }
7294
- // Drop members contained by another member.
7295
- members = members.filter(function (member) {
7922
+ return members.filter(function (member) {
7296
7923
  return !members.some(function (other) {
7297
7924
  return other !== member && other.contains(member);
7298
7925
  });
7299
7926
  });
7927
+ }
7928
+
7929
+ // Multi-select group move: when the user drags an element that is a member
7930
+ // of the current multi-selection (primary selectedEl + the passive
7931
+ // shift-click/marquee set), the whole group moves together — Figma
7932
+ // behavior. Returns the full member list in DOCUMENT ORDER (so a group
7933
+ // flow-insert lands the members consecutively in their existing visual
7934
+ // order) when gestureEl belongs to a 2+ selection, or just [gestureEl]
7935
+ // otherwise. Members nested inside another member are dropped: moving the
7936
+ // ancestor already moves them, and double-applying the delta would fling
7937
+ // them.
7938
+ function collectMoveGroupMembers(gestureEl: Element): Element[] {
7939
+ if (!gestureEl) return [];
7940
+ var members = collectSelectionMembers();
7300
7941
  var gestureMember: Element | null = null;
7301
7942
  for (var k = 0; k < members.length; k += 1) {
7302
7943
  if (
@@ -9039,110 +9680,896 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
9039
9680
  return rects;
9040
9681
  }
9041
9682
 
9042
- // Mirrors getAxisSnapCandidates/getBestCandidate/getVerticalGuide/
9043
- // getHorizontalGuide from shared/canvas-math.ts: for each axis, compare the
9044
- // moving rect's left/center/right (or top/center/bottom) against every
9045
- // candidate's same three values and keep the closest match within
9046
- // threshold. Exported off the IIFE closure via the return-value shape below
9047
- // so tests can exercise it directly (see the "extractable pure logic"
9048
- // convention used by motion-preview.bridge.ts).
9049
- function computeMoveSnapOffset(movingRect, candidates, threshold) {
9050
- var moving = rectBounds(movingRect);
9051
- var bestX = null;
9052
- var bestY = null;
9683
+ // Hand-port of shared/canvas-math.ts computeDragSnap and its helpers:
9684
+ // alignment offset per axis, then every guide line the snapped position
9685
+ // actually sits on, then a spacing snap on whichever axes alignment left
9686
+ // free. Keep the two in sync — canvas-math is the reference implementation
9687
+ // and editor-chrome-bridge.snap.test.ts proves this copy against it.
9688
+ var SNAP_ALIGN_EPSILON = 1e-6;
9689
+ // Guides are drawn at this tight tolerance, not the snap pull: on an axis
9690
+ // locked by an alignment guide the element never moved, so a near-equal
9691
+ // pair would otherwise get labelled as an equal gap it does not have.
9692
+ var SPACING_MATCH_EPSILON = 0.5;
9693
+ // Screen px: beyond this a neighbour is not what the user is positioning
9694
+ // against, and its measurement is a line stretched over empty space.
9695
+ var PROXIMITY_RANGE_PX = 160;
9696
+
9697
+ function axisSnapValues(bounds, axis) {
9698
+ return axis === "x"
9699
+ ? [bounds.left, bounds.centerX, bounds.right]
9700
+ : [bounds.top, bounds.centerY, bounds.bottom];
9701
+ }
9702
+
9703
+ function axisStart(bounds, axis) {
9704
+ return axis === "x" ? bounds.left : bounds.top;
9705
+ }
9706
+
9707
+ function axisEnd(bounds, axis) {
9708
+ return axis === "x" ? bounds.right : bounds.bottom;
9709
+ }
9710
+
9711
+ function crossStart(bounds, axis) {
9712
+ return axis === "x" ? bounds.top : bounds.left;
9713
+ }
9714
+
9715
+ function crossEnd(bounds, axis) {
9716
+ return axis === "x" ? bounds.bottom : bounds.right;
9717
+ }
9718
+
9719
+ function crossAxisOverlaps(axis, a, b) {
9720
+ return (
9721
+ crossStart(a, axis) < crossEnd(b, axis) &&
9722
+ crossEnd(a, axis) > crossStart(b, axis)
9723
+ );
9724
+ }
9725
+
9726
+ function translateRectBounds(bounds, dx, dy) {
9727
+ return {
9728
+ left: bounds.left + dx,
9729
+ right: bounds.right + dx,
9730
+ centerX: bounds.centerX + dx,
9731
+ top: bounds.top + dy,
9732
+ bottom: bounds.bottom + dy,
9733
+ centerY: bounds.centerY + dy,
9734
+ };
9735
+ }
9736
+
9737
+ function findAxisSnapOffset(axis, moving, candidates, threshold) {
9738
+ var offset = null;
9739
+ var bestDistance = Infinity;
9740
+ var movingValues = axisSnapValues(moving, axis);
9053
9741
  for (var i = 0; i < candidates.length; i += 1) {
9054
- var candidate = candidates[i];
9055
- var xValues = [moving.left, moving.centerX, moving.right];
9056
- var xTargets = [candidate.left, candidate.centerX, candidate.right];
9057
- for (var xi = 0; xi < xValues.length; xi += 1) {
9058
- for (var xj = 0; xj < xTargets.length; xj += 1) {
9059
- var offsetX = xTargets[xj] - xValues[xi];
9060
- var distanceX = Math.abs(offsetX);
9061
- if (distanceX > threshold) continue;
9062
- if (!bestX || distanceX < bestX.distance) {
9063
- bestX = {
9064
- distance: distanceX,
9065
- offset: offsetX,
9066
- guide: {
9067
- position: xTargets[xj],
9068
- start: Math.min(moving.top, candidate.top),
9069
- end: Math.max(moving.bottom, candidate.bottom),
9070
- },
9071
- };
9072
- }
9742
+ var targets = axisSnapValues(candidates[i], axis);
9743
+ for (var mi = 0; mi < movingValues.length; mi += 1) {
9744
+ for (var ti = 0; ti < targets.length; ti += 1) {
9745
+ var candidate = targets[ti] - movingValues[mi];
9746
+ var distance = Math.abs(candidate);
9747
+ if (distance > threshold || distance >= bestDistance) continue;
9748
+ bestDistance = distance;
9749
+ offset = candidate;
9073
9750
  }
9074
9751
  }
9075
- var yValues = [moving.top, moving.centerY, moving.bottom];
9076
- var yTargets = [candidate.top, candidate.centerY, candidate.bottom];
9077
- for (var yi = 0; yi < yValues.length; yi += 1) {
9078
- for (var yj = 0; yj < yTargets.length; yj += 1) {
9079
- var offsetY = yTargets[yj] - yValues[yi];
9080
- var distanceY = Math.abs(offsetY);
9081
- if (distanceY > threshold) continue;
9082
- if (!bestY || distanceY < bestY.distance) {
9083
- bestY = {
9084
- distance: distanceY,
9085
- offset: offsetY,
9086
- guide: {
9087
- position: yTargets[yj],
9088
- start: Math.min(moving.left, candidate.left),
9089
- end: Math.max(moving.right, candidate.right),
9090
- },
9091
- };
9752
+ }
9753
+ return offset;
9754
+ }
9755
+
9756
+ function buildAxisGuides(axis, moving, candidates) {
9757
+ var positions: number[] = [];
9758
+ var spans: { start: number; end: number }[] = [];
9759
+ var movingValues = axisSnapValues(moving, axis);
9760
+ for (var i = 0; i < candidates.length; i += 1) {
9761
+ var candidate = candidates[i];
9762
+ var targets = axisSnapValues(candidate, axis);
9763
+ for (var mi = 0; mi < movingValues.length; mi += 1) {
9764
+ for (var ti = 0; ti < targets.length; ti += 1) {
9765
+ if (Math.abs(targets[ti] - movingValues[mi]) > SNAP_ALIGN_EPSILON) {
9766
+ continue;
9767
+ }
9768
+ var slot = positions.indexOf(targets[ti]);
9769
+ if (slot === -1) {
9770
+ positions.push(targets[ti]);
9771
+ spans.push({
9772
+ start: Math.min(
9773
+ crossStart(moving, axis),
9774
+ crossStart(candidate, axis),
9775
+ ),
9776
+ end: Math.max(crossEnd(moving, axis), crossEnd(candidate, axis)),
9777
+ });
9778
+ } else {
9779
+ spans[slot].start = Math.min(
9780
+ spans[slot].start,
9781
+ crossStart(candidate, axis),
9782
+ );
9783
+ spans[slot].end = Math.max(
9784
+ spans[slot].end,
9785
+ crossEnd(candidate, axis),
9786
+ );
9092
9787
  }
9093
9788
  }
9094
9789
  }
9095
9790
  }
9096
- return {
9097
- dx: bestX ? bestX.offset : 0,
9098
- dy: bestY ? bestY.offset : 0,
9099
- guideV: bestX ? bestX.guide : null,
9100
- guideH: bestY ? bestY.guide : null,
9101
- };
9791
+ var guides: {
9792
+ orientation: string;
9793
+ position: number;
9794
+ start: number;
9795
+ end: number;
9796
+ }[] = [];
9797
+ for (var g = 0; g < positions.length; g += 1) {
9798
+ guides.push({
9799
+ orientation: axis === "x" ? "vertical" : "horizontal",
9800
+ position: positions[g],
9801
+ start: spans[g].start,
9802
+ end: spans[g].end,
9803
+ });
9804
+ }
9805
+ return guides;
9102
9806
  }
9103
9807
 
9104
- function showSnapGuides(guideV, guideH) {
9105
- // Constant-screen-size chrome: guide THICKNESS compensates for the
9106
- // host's iframe scale (chromeLineScale) so the line stays a crisp 1px
9107
- // on screen at any zoom; the guide's span/position stays in content
9108
- // coordinates.
9109
- var line = 1 * chromeLineScale();
9110
- if (guideV) {
9111
- snapGuideV.style.display = "block";
9112
- snapGuideV.style.width = line + "px";
9113
- snapGuideV.style.left = Math.round(guideV.position) + "px";
9114
- snapGuideV.style.top = Math.round(guideV.start) + "px";
9115
- snapGuideV.style.height = Math.max(1, guideV.end - guideV.start) + "px";
9116
- } else {
9117
- snapGuideV.style.display = "none";
9118
- }
9119
- if (guideH) {
9120
- snapGuideH.style.display = "block";
9121
- snapGuideH.style.height = line + "px";
9122
- snapGuideH.style.top = Math.round(guideH.position) + "px";
9123
- snapGuideH.style.left = Math.round(guideH.start) + "px";
9124
- snapGuideH.style.width = Math.max(1, guideH.end - guideH.start) + "px";
9125
- } else {
9126
- snapGuideH.style.display = "none";
9808
+ function collectAxisGapCandidates(axis, moving, candidates) {
9809
+ var gaps: {
9810
+ side: string;
9811
+ gap: number;
9812
+ gapStart: number;
9813
+ gapEnd: number;
9814
+ crossStart: number;
9815
+ crossEnd: number;
9816
+ }[] = [];
9817
+ for (var i = 0; i < candidates.length; i += 1) {
9818
+ var bounds = candidates[i];
9819
+ if (!crossAxisOverlaps(axis, bounds, moving)) continue;
9820
+ var band = {
9821
+ crossStart: Math.max(
9822
+ crossStart(bounds, axis),
9823
+ crossStart(moving, axis),
9824
+ ),
9825
+ crossEnd: Math.min(crossEnd(bounds, axis), crossEnd(moving, axis)),
9826
+ };
9827
+ if (axisEnd(bounds, axis) <= axisStart(moving, axis)) {
9828
+ gaps.push({
9829
+ side: "before",
9830
+ gap: axisStart(moving, axis) - axisEnd(bounds, axis),
9831
+ gapStart: axisEnd(bounds, axis),
9832
+ gapEnd: axisStart(moving, axis),
9833
+ crossStart: band.crossStart,
9834
+ crossEnd: band.crossEnd,
9835
+ });
9836
+ } else if (axisStart(bounds, axis) >= axisEnd(moving, axis)) {
9837
+ gaps.push({
9838
+ side: "after",
9839
+ gap: axisStart(bounds, axis) - axisEnd(moving, axis),
9840
+ gapStart: axisEnd(moving, axis),
9841
+ gapEnd: axisStart(bounds, axis),
9842
+ crossStart: band.crossStart,
9843
+ crossEnd: band.crossEnd,
9844
+ });
9845
+ }
9127
9846
  }
9847
+ return gaps;
9128
9848
  }
9129
9849
 
9130
- function hideSnapGuides() {
9131
- snapGuideV.style.display = "none";
9132
- snapGuideH.style.display = "none";
9850
+ function closestGapCandidate(gaps, side) {
9851
+ var best = null;
9852
+ for (var i = 0; i < gaps.length; i += 1) {
9853
+ if (gaps[i].side !== side) continue;
9854
+ if (!best || gaps[i].gap < best.gap) best = gaps[i];
9855
+ }
9856
+ return best;
9133
9857
  }
9134
9858
 
9135
- // `gestureElParam` (optional): the specific multi-selection member the
9136
- // pointer went down on when this drag preserves a 2+ selection instead of
9137
- // collapsing to one element (see beginPotentialShieldDrag's group branch).
9138
- // Defaults to selectedEl — the selection-overlay drag path.
9139
- function startMove(
9140
- e,
9141
- gestureElParam?: Element,
9142
- pointerStartParam?: { clientX: number; clientY: number },
9143
- ) {
9144
- if (readOnly) return;
9145
- var gestureEl = gestureElParam || selectedEl;
9859
+ function collectRhythmGaps(axis, moving, candidates) {
9860
+ var row: any[] = [];
9861
+ for (var i = 0; i < candidates.length; i += 1) {
9862
+ var entry = candidates[i];
9863
+ if (!crossAxisOverlaps(axis, entry, moving)) continue;
9864
+ // A candidate that spans the whole moving element — the parent content
9865
+ // box — is a wrapper, not a neighbour in the rhythm, and its span would
9866
+ // swallow every real gap in the row.
9867
+ if (
9868
+ axisStart(entry, axis) <= axisStart(moving, axis) &&
9869
+ axisEnd(entry, axis) >= axisEnd(moving, axis)
9870
+ ) {
9871
+ continue;
9872
+ }
9873
+ row.push(entry);
9874
+ }
9875
+ row.sort(function (a, b) {
9876
+ return axisStart(a, axis) - axisStart(b, axis);
9877
+ });
9878
+ var gaps: { gap: number; band: any }[] = [];
9879
+ var previous: any = null;
9880
+ for (var r = 0; r < row.length; r += 1) {
9881
+ var bounds = row[r];
9882
+ var gap = previous
9883
+ ? axisStart(bounds, axis) - axisEnd(previous, axis)
9884
+ : 0;
9885
+ if (previous && gap > 0 && crossAxisOverlaps(axis, previous, bounds)) {
9886
+ gaps.push({
9887
+ gap: gap,
9888
+ band: {
9889
+ gapStart: axisEnd(previous, axis),
9890
+ gapEnd: axisStart(bounds, axis),
9891
+ crossStart: Math.max(
9892
+ crossStart(previous, axis),
9893
+ crossStart(bounds, axis),
9894
+ ),
9895
+ crossEnd: Math.min(
9896
+ crossEnd(previous, axis),
9897
+ crossEnd(bounds, axis),
9898
+ ),
9899
+ },
9900
+ });
9901
+ }
9902
+ if (!previous || axisEnd(bounds, axis) > axisEnd(previous, axis)) {
9903
+ previous = bounds;
9904
+ }
9905
+ }
9906
+ return gaps;
9907
+ }
9908
+
9909
+ // Returns the matched side with the offset: picking a side independently
9910
+ // builds chrome for a gap the snap never used.
9911
+ function findSpacingSnapOffset(axis, moving, candidates, tolerance) {
9912
+ var gaps = collectAxisGapCandidates(axis, moving, candidates);
9913
+ var before = closestGapCandidate(gaps, "before");
9914
+ var after = closestGapCandidate(gaps, "after");
9915
+ if (!before && !after) return { offset: 0, side: null };
9916
+
9917
+ var offset = 0;
9918
+ var side = null;
9919
+ var bestDistance = Infinity;
9920
+ function consider(value, matched) {
9921
+ var distance = Math.abs(value);
9922
+ if (distance > tolerance || distance >= bestDistance) return;
9923
+ bestDistance = distance;
9924
+ offset = value;
9925
+ side = matched;
9926
+ }
9927
+ if (before && after) consider((after.gap - before.gap) / 2, "both");
9928
+ var rhythms = collectRhythmGaps(axis, moving, candidates);
9929
+ for (var i = 0; i < rhythms.length; i += 1) {
9930
+ if (before) consider(rhythms[i].gap - before.gap, "before");
9931
+ if (after) consider(after.gap - rhythms[i].gap, "after");
9932
+ }
9933
+ return { offset: offset, side: side };
9934
+ }
9935
+
9936
+ function gapCandidateBand(candidate) {
9937
+ return {
9938
+ gapStart: candidate.gapStart,
9939
+ gapEnd: candidate.gapEnd,
9940
+ crossStart: candidate.crossStart,
9941
+ crossEnd: candidate.crossEnd,
9942
+ };
9943
+ }
9944
+
9945
+ // Every gap already in the row/column that matches: Figma and tldraw both
9946
+ // light the whole run, not only the pair the snap landed on.
9947
+ function matchingRhythmBands(rhythms, gap, tolerance) {
9948
+ var bands: any[] = [];
9949
+ for (var i = 0; i < rhythms.length; i += 1) {
9950
+ if (Math.abs(rhythms[i].gap - gap) <= tolerance)
9951
+ bands.push(rhythms[i].band);
9952
+ }
9953
+ return bands;
9954
+ }
9955
+
9956
+ function buildSpacingGuides(
9957
+ axis,
9958
+ moving,
9959
+ candidates,
9960
+ tolerance,
9961
+ gaps,
9962
+ matchedSide,
9963
+ ) {
9964
+ gaps = gaps || collectAxisGapCandidates(axis, moving, candidates);
9965
+ var before = closestGapCandidate(gaps, "before");
9966
+ var after = closestGapCandidate(gaps, "after");
9967
+ var orientation = axis === "x" ? "vertical" : "horizontal";
9968
+ var rhythms = collectRhythmGaps(axis, moving, candidates);
9969
+ if (before && after && Math.abs(before.gap - after.gap) <= tolerance) {
9970
+ var pairGap = (before.gap + after.gap) / 2;
9971
+ return [
9972
+ {
9973
+ orientation: orientation,
9974
+ gap: pairGap,
9975
+ bands: [gapCandidateBand(before), gapCandidateBand(after)].concat(
9976
+ matchingRhythmBands(rhythms, pairGap, tolerance),
9977
+ ),
9978
+ },
9979
+ ];
9980
+ }
9981
+ var neighbor =
9982
+ matchedSide === "after"
9983
+ ? after
9984
+ : matchedSide === "before"
9985
+ ? before
9986
+ : before || after;
9987
+ if (!neighbor) return [];
9988
+ var matched = matchingRhythmBands(rhythms, neighbor.gap, tolerance);
9989
+ if (!matched.length) return [];
9990
+ return [
9991
+ {
9992
+ orientation: orientation,
9993
+ gap: neighbor.gap,
9994
+ bands: [gapCandidateBand(neighbor)].concat(matched),
9995
+ },
9996
+ ];
9997
+ }
9998
+
9999
+ // Nearest neighbour per axis, within range. Stock Figma only prints a gap
10000
+ // that matches an existing one; showing the nearest unconditionally is a
10001
+ // deliberate divergence (see canvas-math computeProximityMeasurements).
10002
+ function computeProximityMeasurements(moving, candidates, range, gapsByAxis) {
10003
+ var measurements: any[] = [];
10004
+ var axes = ["x", "y"];
10005
+ for (var a = 0; a < axes.length; a += 1) {
10006
+ var axis = axes[a];
10007
+ var gaps =
10008
+ (gapsByAxis && gapsByAxis[axis]) ||
10009
+ collectAxisGapCandidates(axis, moving, candidates);
10010
+ var nearest = null;
10011
+ for (var i = 0; i < gaps.length; i += 1) {
10012
+ if (!nearest || gaps[i].gap < nearest.gap) nearest = gaps[i];
10013
+ }
10014
+ if (!nearest || nearest.gap > range) continue;
10015
+ measurements.push({
10016
+ orientation: axis === "x" ? "vertical" : "horizontal",
10017
+ gap: nearest.gap,
10018
+ band: gapCandidateBand(nearest),
10019
+ });
10020
+ }
10021
+ return measurements;
10022
+ }
10023
+
10024
+ function computeMoveSnapOffset(
10025
+ movingRect,
10026
+ candidates,
10027
+ threshold,
10028
+ isGroup,
10029
+ locked,
10030
+ ) {
10031
+ var moving = rectBounds(movingRect);
10032
+ locked = locked || {};
10033
+ var dx = locked.x
10034
+ ? null
10035
+ : findAxisSnapOffset("x", moving, candidates, threshold);
10036
+ var dy = locked.y
10037
+ ? null
10038
+ : findAxisSnapOffset("y", moving, candidates, threshold);
10039
+ var snapped = translateRectBounds(moving, dx || 0, dy || 0);
10040
+ var guides = (
10041
+ locked.x ? [] : buildAxisGuides("x", snapped, candidates)
10042
+ ).concat(locked.y ? [] : buildAxisGuides("y", snapped, candidates));
10043
+
10044
+ // Figma drops spacing guides for a multi-select drag, and so does the
10045
+ // overview canvas; a grouped iframe drag must not diverge from either.
10046
+ if (isGroup) {
10047
+ return {
10048
+ dx: dx || 0,
10049
+ dy: dy || 0,
10050
+ guides: guides,
10051
+ spacingGuides: [],
10052
+ measurements: [],
10053
+ };
10054
+ }
10055
+
10056
+ // Spacing only gets the axes alignment left free — moving a claimed axis
10057
+ // would pull the element off the guide line the user can already see.
10058
+ var idle = { offset: 0, side: null };
10059
+ var spacingXResult = guides.some(function (guide) {
10060
+ return guide.orientation === "vertical";
10061
+ })
10062
+ ? idle
10063
+ : locked.x
10064
+ ? idle
10065
+ : findSpacingSnapOffset("x", snapped, candidates, threshold);
10066
+ var spacingYResult = guides.some(function (guide) {
10067
+ return guide.orientation === "horizontal";
10068
+ })
10069
+ ? idle
10070
+ : locked.y
10071
+ ? idle
10072
+ : findSpacingSnapOffset("y", snapped, candidates, threshold);
10073
+ var spacingX = spacingXResult.offset;
10074
+ var spacingY = spacingYResult.offset;
10075
+ var spaced = translateRectBounds(snapped, spacingX, spacingY);
10076
+ // Spacing and proximity ask the same question of the same bounds; one
10077
+ // scan per axis feeds both instead of four scans per drag tick.
10078
+ var settledGaps = {
10079
+ x: collectAxisGapCandidates("x", spaced, candidates),
10080
+ y: collectAxisGapCandidates("y", spaced, candidates),
10081
+ };
10082
+ var spacingGuides = buildSpacingGuides(
10083
+ "x",
10084
+ spaced,
10085
+ candidates,
10086
+ SPACING_MATCH_EPSILON,
10087
+ settledGaps.x,
10088
+ spacingXResult.side,
10089
+ ).concat(
10090
+ buildSpacingGuides(
10091
+ "y",
10092
+ spaced,
10093
+ candidates,
10094
+ SPACING_MATCH_EPSILON,
10095
+ settledGaps.y,
10096
+ spacingYResult.side,
10097
+ ),
10098
+ );
10099
+
10100
+ var measurements = computeProximityMeasurements(
10101
+ spaced,
10102
+ candidates,
10103
+ (PROXIMITY_RANGE_PX * threshold) / SNAP_THRESHOLD_PX,
10104
+ settledGaps,
10105
+ ).filter(function (measurement) {
10106
+ return !spacingGuides.some(function (guide) {
10107
+ return guide.orientation === measurement.orientation;
10108
+ });
10109
+ });
10110
+
10111
+ return {
10112
+ dx: (dx || 0) + spacingX,
10113
+ dy: (dy || 0) + spacingY,
10114
+ guides: guides,
10115
+ spacingGuides: spacingGuides,
10116
+ measurements: measurements,
10117
+ };
10118
+ }
10119
+
10120
+ // Pooled, not rebuilt: this runs on every rAF of a drag, and tearing the
10121
+ // layer down with innerHTML each frame costs a full style recalc for nodes
10122
+ // that are almost always identical in count from one frame to the next.
10123
+ var snapGuideNodeCount = 0;
10124
+
10125
+ function beginSnapGuideNodes() {
10126
+ snapGuideNodeCount = 0;
10127
+ }
10128
+
10129
+ function appendSnapGuideNode(cssText) {
10130
+ var node = snapGuideLayer.children[snapGuideNodeCount] as
10131
+ | HTMLElement
10132
+ | undefined;
10133
+ if (!node) {
10134
+ node = document.createElement("div");
10135
+ snapGuideLayer.appendChild(node);
10136
+ }
10137
+ node.style.cssText = "position:fixed;" + cssText;
10138
+ node.textContent = "";
10139
+ snapGuideNodeCount += 1;
10140
+ return node;
10141
+ }
10142
+
10143
+ function endSnapGuideNodes() {
10144
+ while (snapGuideLayer.children.length > snapGuideNodeCount) {
10145
+ snapGuideLayer.removeChild(snapGuideLayer.lastChild!);
10146
+ }
10147
+ }
10148
+
10149
+ // Constant-screen-size chrome: guide THICKNESS and the spacing serifs
10150
+ // compensate for the host's iframe scale (chromeLineScale) so they stay
10151
+ // crisp at any zoom; positions and spans stay in content coordinates.
10152
+ function showSnapGuides(guides, spacingGuides, measurements) {
10153
+ measurements = measurements || [];
10154
+ if (!guides.length && !spacingGuides.length && !measurements.length) {
10155
+ hideSnapGuides();
10156
+ return;
10157
+ }
10158
+ beginSnapGuideNodes();
10159
+ if (snapGuideLayer.style.display !== "block") {
10160
+ snapGuideLayer.style.display = "block";
10161
+ }
10162
+ var scale = chromeLineScale();
10163
+ var line = 1 * scale;
10164
+ // Must be a forwarded var (see EDITOR_BRIDGE_VAR_NAMES in DesignCanvas):
10165
+ // an unknown custom property paints transparent, not red, and a guide
10166
+ // with correct geometry and no colour looks exactly like no guide.
10167
+ var fill = "background:var(--design-editor-measure-color);";
10168
+
10169
+ for (var i = 0; i < guides.length; i += 1) {
10170
+ var guide = guides[i];
10171
+ var span = Math.max(1, guide.end - guide.start);
10172
+ appendSnapGuideNode(
10173
+ guide.orientation === "vertical"
10174
+ ? "left:" +
10175
+ guide.position +
10176
+ "px;top:" +
10177
+ guide.start +
10178
+ "px;width:" +
10179
+ line +
10180
+ "px;height:" +
10181
+ span +
10182
+ "px;" +
10183
+ fill
10184
+ : "top:" +
10185
+ guide.position +
10186
+ "px;left:" +
10187
+ guide.start +
10188
+ "px;height:" +
10189
+ line +
10190
+ "px;width:" +
10191
+ span +
10192
+ "px;" +
10193
+ fill,
10194
+ );
10195
+ }
10196
+
10197
+ for (var s = 0; s < spacingGuides.length; s += 1) {
10198
+ var spacing = spacingGuides[s];
10199
+ for (var b = 0; b < spacing.bands.length; b += 1) {
10200
+ appendSnapGuideNode(
10201
+ spacingBandCss(spacing.orientation, spacing.bands[b], line, fill),
10202
+ );
10203
+ appendSnapGuideNode(
10204
+ spacingSerifCss(
10205
+ spacing.orientation,
10206
+ spacing.bands[b],
10207
+ line,
10208
+ 5 * scale,
10209
+ fill,
10210
+ true,
10211
+ ),
10212
+ );
10213
+ appendSnapGuideNode(
10214
+ spacingSerifCss(
10215
+ spacing.orientation,
10216
+ spacing.bands[b],
10217
+ line,
10218
+ 5 * scale,
10219
+ fill,
10220
+ false,
10221
+ ),
10222
+ );
10223
+ }
10224
+ var label = appendSnapGuideNode(
10225
+ spacingLabelCss(spacing.orientation, spacing.bands[0], scale),
10226
+ );
10227
+ label.textContent = String(Math.round(spacing.gap));
10228
+ }
10229
+
10230
+ for (var m = 0; m < measurements.length; m += 1) {
10231
+ var measurement = measurements[m];
10232
+ appendSnapGuideNode(
10233
+ spacingBandCss(measurement.orientation, measurement.band, line, fill),
10234
+ );
10235
+ appendSnapGuideNode(
10236
+ spacingSerifCss(
10237
+ measurement.orientation,
10238
+ measurement.band,
10239
+ line,
10240
+ 5 * scale,
10241
+ fill,
10242
+ true,
10243
+ ),
10244
+ );
10245
+ appendSnapGuideNode(
10246
+ spacingSerifCss(
10247
+ measurement.orientation,
10248
+ measurement.band,
10249
+ line,
10250
+ 5 * scale,
10251
+ fill,
10252
+ false,
10253
+ ),
10254
+ );
10255
+ var measureLabel = appendSnapGuideNode(
10256
+ spacingLabelCss(measurement.orientation, measurement.band, scale),
10257
+ );
10258
+ measureLabel.textContent = String(Math.round(measurement.gap));
10259
+ }
10260
+ endSnapGuideNodes();
10261
+ }
10262
+
10263
+ function spacingBandCss(orientation, band, line, fill) {
10264
+ var crossMid = (band.crossStart + band.crossEnd) / 2;
10265
+ var length = Math.max(0, band.gapEnd - band.gapStart);
10266
+ return orientation === "vertical"
10267
+ ? "left:" +
10268
+ band.gapStart +
10269
+ "px;top:" +
10270
+ (crossMid - line / 2) +
10271
+ "px;width:" +
10272
+ length +
10273
+ "px;height:" +
10274
+ line +
10275
+ "px;" +
10276
+ fill
10277
+ : "top:" +
10278
+ band.gapStart +
10279
+ "px;left:" +
10280
+ (crossMid - line / 2) +
10281
+ "px;height:" +
10282
+ length +
10283
+ "px;width:" +
10284
+ line +
10285
+ "px;" +
10286
+ fill;
10287
+ }
10288
+
10289
+ function spacingSerifCss(orientation, band, line, serif, fill, atStart) {
10290
+ var crossMid = (band.crossStart + band.crossEnd) / 2;
10291
+ var along = atStart ? band.gapStart : band.gapEnd;
10292
+ return orientation === "vertical"
10293
+ ? "left:" +
10294
+ (along - line / 2) +
10295
+ "px;top:" +
10296
+ (crossMid - serif) +
10297
+ "px;width:" +
10298
+ line +
10299
+ "px;height:" +
10300
+ serif * 2 +
10301
+ "px;" +
10302
+ fill
10303
+ : "top:" +
10304
+ (along - line / 2) +
10305
+ "px;left:" +
10306
+ (crossMid - serif) +
10307
+ "px;height:" +
10308
+ line +
10309
+ "px;width:" +
10310
+ serif * 2 +
10311
+ "px;" +
10312
+ fill;
10313
+ }
10314
+
10315
+ function spacingLabelCss(orientation, band, scale) {
10316
+ var crossMid = (band.crossStart + band.crossEnd) / 2;
10317
+ var alongMid = (band.gapStart + band.gapEnd) / 2;
10318
+ return (
10319
+ (orientation === "vertical"
10320
+ ? "left:" + alongMid + "px;top:" + crossMid + "px;"
10321
+ : "left:" + crossMid + "px;top:" + alongMid + "px;") +
10322
+ "transform:translate(-50%,-50%);border-radius:" +
10323
+ 3 * scale +
10324
+ "px;padding:" +
10325
+ 1 * scale +
10326
+ "px " +
10327
+ 4 * scale +
10328
+ "px;font:" +
10329
+ 10 * scale +
10330
+ "px/1 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;" +
10331
+ "background:var(--design-editor-measure-color);color:white;"
10332
+ );
10333
+ }
10334
+
10335
+ // Hand-port of deriveConstraintsValue in edit-panel/position-layout-
10336
+ // properties.tsx — the bridge cannot import. Reads AUTHORED inline offsets
10337
+ // only: an absolutely positioned left-only element still has a computed
10338
+ // `right`, and treating that as a pin would draw a line that lies.
10339
+ function authoredOffset(value) {
10340
+ if (!value || value === "auto") return "";
10341
+ return value;
10342
+ }
10343
+
10344
+ function elementConstraints(el) {
10345
+ var style = (el as HTMLElement).style;
10346
+ var left = authoredOffset(style.left);
10347
+ var right = authoredOffset(style.right);
10348
+ var top = authoredOffset(style.top);
10349
+ var bottom = authoredOffset(style.bottom);
10350
+ var transform = style.transform || "";
10351
+ return {
10352
+ horizontal:
10353
+ style.width === "100%"
10354
+ ? "scale"
10355
+ : left && right
10356
+ ? "left-right"
10357
+ : right && !left
10358
+ ? "right"
10359
+ : transform.indexOf("translateX(-50%)") !== -1
10360
+ ? "center"
10361
+ : "left",
10362
+ vertical:
10363
+ style.height === "100%"
10364
+ ? "scale"
10365
+ : top && bottom
10366
+ ? "top-bottom"
10367
+ : bottom && !top
10368
+ ? "bottom"
10369
+ : transform.indexOf("translateY(-50%)") !== -1
10370
+ ? "center"
10371
+ : "top",
10372
+ };
10373
+ }
10374
+
10375
+ // Pooled for the same reason as the snap guides: this runs every rAF of a
10376
+ // drag, and rebuilding the layer each frame costs a needless style recalc.
10377
+ var constraintNodeCount = 0;
10378
+
10379
+ function appendConstraintLine(
10380
+ from,
10381
+ to,
10382
+ crossPosition,
10383
+ horizontal,
10384
+ thickness,
10385
+ ) {
10386
+ var start = Math.min(from, to);
10387
+ var length = Math.max(1, Math.abs(to - from));
10388
+ var node = constraintGuideLayer.children[constraintNodeCount] as
10389
+ | HTMLElement
10390
+ | undefined;
10391
+ if (!node) {
10392
+ node = document.createElement("div");
10393
+ constraintGuideLayer.appendChild(node);
10394
+ }
10395
+ constraintNodeCount += 1;
10396
+ node.style.cssText =
10397
+ "position:fixed;" +
10398
+ (horizontal
10399
+ ? "left:" +
10400
+ start +
10401
+ "px;top:" +
10402
+ (crossPosition - thickness / 2) +
10403
+ "px;width:" +
10404
+ length +
10405
+ "px;height:0;border-top:"
10406
+ : "top:" +
10407
+ start +
10408
+ "px;left:" +
10409
+ (crossPosition - thickness / 2) +
10410
+ "px;height:" +
10411
+ length +
10412
+ "px;width:0;border-left:") +
10413
+ thickness +
10414
+ "px dashed var(--design-editor-accent-color);";
10415
+ }
10416
+
10417
+ function showConstraintGuides(el) {
10418
+ if (!el || dragChromeSuppressed) return hideConstraintGuides();
10419
+ // body counts here: it is the screen root, which is exactly the frame an
10420
+ // element's offsets are resolved against. (Auto-layout nesting excludes
10421
+ // body for the opposite reason — it is not a drop target.)
10422
+ var parent =
10423
+ (el as HTMLElement).offsetParent || (el as HTMLElement).parentElement;
10424
+ if (!parent) return hideConstraintGuides();
10425
+ var rect = el.getBoundingClientRect();
10426
+ var frame = parent.getBoundingClientRect();
10427
+ if (rect.width <= 0 || rect.height <= 0) return hideConstraintGuides();
10428
+ if (!constraintGuideLayer.isConnected) {
10429
+ document.body.appendChild(constraintGuideLayer);
10430
+ }
10431
+ constraintNodeCount = 0;
10432
+ if (constraintGuideLayer.style.display !== "block") {
10433
+ constraintGuideLayer.style.display = "block";
10434
+ }
10435
+
10436
+ var thickness = chromeLineScale();
10437
+ var midY = rect.top + rect.height / 2;
10438
+ var midX = rect.left + rect.width / 2;
10439
+ var constraints = elementConstraints(el);
10440
+
10441
+ if (constraints.horizontal === "scale") {
10442
+ appendConstraintLine(frame.left, frame.right, midY, true, thickness);
10443
+ } else {
10444
+ if (
10445
+ constraints.horizontal !== "right" &&
10446
+ constraints.horizontal !== "center"
10447
+ ) {
10448
+ appendConstraintLine(frame.left, rect.left, midY, true, thickness);
10449
+ }
10450
+ if (
10451
+ constraints.horizontal === "right" ||
10452
+ constraints.horizontal === "left-right"
10453
+ ) {
10454
+ appendConstraintLine(rect.right, frame.right, midY, true, thickness);
10455
+ }
10456
+ if (constraints.horizontal === "center") {
10457
+ appendConstraintLine(
10458
+ frame.left + frame.width / 2,
10459
+ midX,
10460
+ midY,
10461
+ true,
10462
+ thickness,
10463
+ );
10464
+ }
10465
+ }
10466
+
10467
+ if (constraints.vertical === "scale") {
10468
+ appendConstraintLine(frame.top, frame.bottom, midX, false, thickness);
10469
+ } else {
10470
+ if (
10471
+ constraints.vertical !== "bottom" &&
10472
+ constraints.vertical !== "center"
10473
+ ) {
10474
+ appendConstraintLine(frame.top, rect.top, midX, false, thickness);
10475
+ }
10476
+ if (
10477
+ constraints.vertical === "bottom" ||
10478
+ constraints.vertical === "top-bottom"
10479
+ ) {
10480
+ appendConstraintLine(rect.bottom, frame.bottom, midX, false, thickness);
10481
+ }
10482
+ if (constraints.vertical === "center") {
10483
+ appendConstraintLine(
10484
+ frame.top + frame.height / 2,
10485
+ midY,
10486
+ midX,
10487
+ false,
10488
+ thickness,
10489
+ );
10490
+ }
10491
+ }
10492
+ trimConstraintNodes();
10493
+ }
10494
+
10495
+ function trimConstraintNodes() {
10496
+ while (constraintGuideLayer.children.length > constraintNodeCount) {
10497
+ constraintGuideLayer.removeChild(constraintGuideLayer.lastChild!);
10498
+ }
10499
+ }
10500
+
10501
+ function hideConstraintGuides() {
10502
+ if (constraintGuideLayer.style.display === "none") return;
10503
+ constraintGuideLayer.style.display = "none";
10504
+ constraintGuideLayer.innerHTML = "";
10505
+ constraintNodeCount = 0;
10506
+ }
10507
+
10508
+ // Figma pins the dragged object's dimensions just under it, in canvas
10509
+ // space — unlike the transform badge, which tracks the cursor.
10510
+ var sizeBadgeKey = "";
10511
+ // Set while a drag is in a state that suppresses free-placement chrome (an
10512
+ // auto-layout insert, or the pointer outside the iframe). refreshOverlays
10513
+ // runs on the same pointer event and would otherwise re-show the badge and
10514
+ // constraint lines the drag just hid.
10515
+ var dragChromeSuppressed = false;
10516
+
10517
+ function showSizeBadge(el) {
10518
+ if (!el || dragChromeSuppressed) return hideSizeBadge();
10519
+ var rect = el.getBoundingClientRect();
10520
+ if (rect.width <= 0 || rect.height <= 0) return hideSizeBadge();
10521
+ var scale = chromeLineScale();
10522
+ // refreshOverlays fires on every ResizeObserver/MutationObserver tick, so
10523
+ // re-writing identical styles here would invalidate layout for nothing.
10524
+ var key =
10525
+ rect.left +
10526
+ "|" +
10527
+ rect.bottom +
10528
+ "|" +
10529
+ rect.width +
10530
+ "|" +
10531
+ rect.height +
10532
+ "|" +
10533
+ scale;
10534
+ if (key === sizeBadgeKey && sizeBadge.style.display === "block") return;
10535
+ sizeBadgeKey = key;
10536
+ if (!sizeBadge.isConnected) document.body.appendChild(sizeBadge);
10537
+ sizeBadge.textContent =
10538
+ Math.round(rect.width) + " × " + Math.round(rect.height);
10539
+ sizeBadge.style.display = "block";
10540
+ sizeBadge.style.borderRadius = 3 * scale + "px";
10541
+ sizeBadge.style.padding = 2 * scale + "px " + 4 * scale + "px";
10542
+ sizeBadge.style.fontSize = 10 * scale + "px";
10543
+ sizeBadge.style.left = rect.left + rect.width / 2 + "px";
10544
+ sizeBadge.style.top = rect.bottom + 6 * scale + "px";
10545
+ sizeBadge.style.transform = "translateX(-50%)";
10546
+ }
10547
+
10548
+ function hideSizeBadge() {
10549
+ if (sizeBadge.style.display === "none") return;
10550
+ sizeBadge.style.display = "none";
10551
+ sizeBadgeKey = "";
10552
+ }
10553
+
10554
+ function hideSnapGuides() {
10555
+ dragChromeSuppressed = false;
10556
+ if (snapGuideLayer.style.display === "none") return;
10557
+ snapGuideLayer.style.display = "none";
10558
+ snapGuideLayer.innerHTML = "";
10559
+ snapGuideNodeCount = 0;
10560
+ }
10561
+
10562
+ // `gestureElParam` (optional): the specific multi-selection member the
10563
+ // pointer went down on when this drag preserves a 2+ selection instead of
10564
+ // collapsing to one element (see beginPotentialShieldDrag's group branch).
10565
+ // Defaults to selectedEl — the selection-overlay drag path.
10566
+ function startMove(
10567
+ e,
10568
+ gestureElParam?: Element,
10569
+ pointerStartParam?: { clientX: number; clientY: number },
10570
+ ) {
10571
+ if (readOnly) return;
10572
+ var gestureEl = gestureElParam || selectedEl;
9146
10573
  if (!gestureEl) return;
9147
10574
  if (isLayerInteractionBlocked(gestureEl)) return;
9148
10575
  e.preventDefault();
@@ -10026,6 +11453,28 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10026
11453
  var dragElStartRect = (dragEl as HTMLElement).getBoundingClientRect();
10027
11454
  var dragElStartWidth = dragElStartRect.width;
10028
11455
  var dragElStartHeight = dragElStartRect.height;
11456
+ // Client px per CSS px for this element. 1 unless an ancestor between it
11457
+ // and the viewport is CSS-scaled; offsetWidth is the untransformed box.
11458
+ // Client px per CSS px contributed by ANCESTORS. Measured on the offset
11459
+ // parent, never on dragEl: its own rect already carries its own
11460
+ // transform, so a rotated or scaled layer would report its local
11461
+ // transform as if the parent were scaled. 1 means "no mapping known",
11462
+ // which is the identity, not a measurement.
11463
+ function ancestorScale(el, axis) {
11464
+ var host = el && (el as HTMLElement).offsetParent;
11465
+ if (!host) return 1;
11466
+ var rect = (host as HTMLElement).getBoundingClientRect();
11467
+ var layout =
11468
+ axis === "x"
11469
+ ? (host as HTMLElement).offsetWidth
11470
+ : (host as HTMLElement).offsetHeight;
11471
+ var client = axis === "x" ? rect.width : rect.height;
11472
+ if (!(layout > 0)) return 1;
11473
+ var scale = client / layout;
11474
+ return scale > 0 && Number.isFinite(scale) ? scale : 1;
11475
+ }
11476
+ var dragElOffsetScaleX = ancestorScale(dragEl, "x");
11477
+ var dragElOffsetScaleY = ancestorScale(dragEl, "y");
10029
11478
  if (!duplicatedForDrag && !isGroupDrag) {
10030
11479
  postCrossScreenDrag("start", dragEl, e);
10031
11480
  }
@@ -10084,18 +11533,45 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10084
11533
  !snapBypass && !duplicatedForDrag
10085
11534
  ? computeMoveSnapOffset(
10086
11535
  {
10087
- left: nextLeft,
10088
- top: nextTop,
11536
+ // snapCandidateRects are client space; nextLeft/nextTop are
11537
+ // offset-parent CSS space. Convert, or nothing ever matches.
11538
+ left:
11539
+ dragElStartRect.left +
11540
+ (nextLeft - originLeft) * dragElOffsetScaleX,
11541
+ top:
11542
+ dragElStartRect.top +
11543
+ (nextTop - originTop) * dragElOffsetScaleY,
10089
11544
  width: dragElStartWidth,
10090
11545
  height: dragElStartHeight,
10091
11546
  },
10092
11547
  snapCandidateRects,
10093
11548
  // Convert the screen-space base to content px (1/zoom).
10094
11549
  SNAP_THRESHOLD_PX * chromeLineScale(),
11550
+ isGroupDrag,
11551
+ ev.shiftKey ? { x: rawDx === 0, y: rawDy === 0 } : null,
10095
11552
  )
10096
- : { dx: 0, dy: 0, guideV: null, guideH: null };
10097
- nextLeft += snapResult.dx;
10098
- nextTop += snapResult.dy;
11553
+ : { dx: 0, dy: 0, guides: [], spacingGuides: [], measurements: [] };
11554
+ if ((window as any).__DND_DEBUG)
11555
+ dndLog("snap:tick", {
11556
+ // Ordered so the fields that decide whether snapping ran at all come
11557
+ // first: the console collapses long objects behind an ellipsis.
11558
+ bypass: snapBypass,
11559
+ duplicated: duplicatedForDrag,
11560
+ mods:
11561
+ (ev.metaKey ? "M" : "") +
11562
+ (ev.ctrlKey ? "C" : "") +
11563
+ (ev.altKey ? "A" : "") +
11564
+ (ev.shiftKey ? "S" : "") || "none",
11565
+ guides: snapResult.guides.length,
11566
+ measurements: snapResult.measurements.length,
11567
+ candidates: snapCandidateRects.length,
11568
+ dropMode: currentAutoLayoutTarget
11569
+ ? currentAutoLayoutTarget.dropMode || "(none)"
11570
+ : "no-target",
11571
+ });
11572
+ // Back to CSS space before it is written to style.left/top.
11573
+ nextLeft += snapResult.dx / dragElOffsetScaleX;
11574
+ nextTop += snapResult.dy / dragElOffsetScaleY;
10099
11575
  // Apply the SAME delta to every member (one entry for single drags)
10100
11576
  // so relative offsets within a multi-selection are preserved. For the
10101
11577
  // gesture member this reduces exactly to the previous
@@ -10144,13 +11620,31 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10144
11620
  // about to be reflowed into a flex/grid slot, not placed at an x/y
10145
11621
  // coordinate), and never while the pointer has left the iframe (the
10146
11622
  // host owns a cross-screen drop at that point).
11623
+ //
11624
+ // An "absolute-container" target is a free placement: the element keeps
11625
+ // its x/y inside the frame it lands in, so it is precisely the case
11626
+ // guides are for. Hiding them there left every board drag — where the
11627
+ // primitives live inside an absolutely positioned frame — with no
11628
+ // guides at all.
11629
+ var flowInsertPending =
11630
+ !!currentAutoLayoutTarget &&
11631
+ currentAutoLayoutTarget.dropMode !== "absolute-container";
10147
11632
  if (
10148
- currentAutoLayoutTarget ||
11633
+ flowInsertPending ||
10149
11634
  (!duplicatedForDrag && isOutsideIframeViewport(ev.clientX, ev.clientY))
10150
11635
  ) {
10151
11636
  hideSnapGuides();
11637
+ dragChromeSuppressed = true;
11638
+ hideSizeBadge();
11639
+ hideConstraintGuides();
10152
11640
  } else {
10153
- showSnapGuides(snapResult.guideV, snapResult.guideH);
11641
+ dragChromeSuppressed = false;
11642
+ showSnapGuides(
11643
+ snapResult.guides,
11644
+ snapResult.spacingGuides,
11645
+ snapResult.measurements,
11646
+ );
11647
+ showConstraintGuides(dragEl);
10154
11648
  }
10155
11649
  showTransformBadge(
10156
11650
  Math.round(nextLeft) + ", " + Math.round(nextTop),
@@ -10185,6 +11679,8 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10185
11679
  hideTransformBadge();
10186
11680
  hideInsertionGuide();
10187
11681
  hideSnapGuides();
11682
+ hideSizeBadge();
11683
+ hideConstraintGuides();
10188
11684
  currentAutoLayoutTarget = null;
10189
11685
  if (duplicatedForDrag) {
10190
11686
  if (dragEl && dragEl.parentElement) {
@@ -10215,12 +11711,16 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10215
11711
  hideTransformBadge();
10216
11712
  hideInsertionGuide();
10217
11713
  hideSnapGuides();
11714
+ hideSizeBadge();
11715
+ hideConstraintGuides();
10218
11716
  return;
10219
11717
  }
10220
11718
  cleanupMoveDrag();
10221
11719
  hideTransformBadge();
10222
11720
  hideInsertionGuide();
10223
11721
  hideSnapGuides();
11722
+ hideSizeBadge();
11723
+ hideConstraintGuides();
10224
11724
  if (!dragEl) return;
10225
11725
  var outsideOnDrop = ev
10226
11726
  ? isOutsideIframeViewport(ev.clientX, ev.clientY)
@@ -10356,6 +11856,47 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10356
11856
  setActiveDragCancel(cancelMoveDrag);
10357
11857
  }
10358
11858
 
11859
+ /**
11860
+ * K-scale descendant text (Figma scales the type inside a frame too). Only
11861
+ * elements carrying their OWN size are listed: an inherited size is already
11862
+ * covered by the root's font-size write and a relative inline unit tracks
11863
+ * its parent, so writing either here would scale it twice.
11864
+ */
11865
+ function collectScaleFontTargets(root: Element) {
11866
+ var targets: Array<{
11867
+ el: HTMLElement;
11868
+ originFontSize: number;
11869
+ originalInlineFontSize: string;
11870
+ }> = [];
11871
+ var nodes = root.querySelectorAll("*");
11872
+ for (var i = 0; i < nodes.length; i += 1) {
11873
+ var el = nodes[i] as HTMLElement;
11874
+ if (isOverlayElement(el)) continue;
11875
+ var inlineFontSize = el.style.fontSize || "";
11876
+ if (inlineFontSize && !/px\s*$/i.test(inlineFontSize)) continue;
11877
+ var parent = el.parentElement;
11878
+ var cs = window.getComputedStyle(el);
11879
+ if (
11880
+ !inlineFontSize &&
11881
+ parent &&
11882
+ window.getComputedStyle(parent).fontSize === cs.fontSize
11883
+ ) {
11884
+ continue;
11885
+ }
11886
+ var originFontSize = readPx(inlineFontSize || cs.fontSize);
11887
+ if (!(originFontSize > 0)) continue;
11888
+ // The revert baseline is recorded on first sight, and the commit is too
11889
+ // late: by then the preview has already written the scaled size.
11890
+ rememberLiveVisualEditOriginalStyles(el);
11891
+ targets.push({
11892
+ el: el,
11893
+ originFontSize: originFontSize,
11894
+ originalInlineFontSize: el.style.fontSize,
11895
+ });
11896
+ }
11897
+ return targets;
11898
+ }
11899
+
10359
11900
  function startResize(handle, e) {
10360
11901
  if (readOnly) return;
10361
11902
  if (!selectedEl) return;
@@ -10473,6 +12014,17 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10473
12014
  // in onUp.
10474
12015
  var widthTouched = false;
10475
12016
  var heightTouched = false;
12017
+ // Captured on the first K-scale tick, not at drag start: the host can arm
12018
+ // scale-tool-mode mid-gesture.
12019
+ var scaledTextTargetsCache: ReturnType<
12020
+ typeof collectScaleFontTargets
12021
+ > | null = null;
12022
+ function scaledTextTargets() {
12023
+ if (!scaledTextTargetsCache) {
12024
+ scaledTextTargetsCache = collectScaleFontTargets(resizeEl);
12025
+ }
12026
+ return scaledTextTargetsCache;
12027
+ }
10476
12028
  function nextRect(ev) {
10477
12029
  var screenDx = ev.clientX - startX;
10478
12030
  var screenDy = ev.clientY - startY;
@@ -10608,6 +12160,13 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10608
12160
  Math.max(1, Math.round(originFontSize * kScaleFactor * 100) / 100) +
10609
12161
  "px";
10610
12162
  }
12163
+ scaledTextTargets().forEach(function (target) {
12164
+ target.el.style.fontSize =
12165
+ Math.max(
12166
+ 1,
12167
+ Math.round(target.originFontSize * kScaleFactor * 100) / 100,
12168
+ ) + "px";
12169
+ });
10611
12170
  }
10612
12171
  showTransformBadge(
10613
12172
  Math.round(rect.width) + " x " + Math.round(rect.height),
@@ -10634,6 +12193,9 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10634
12193
  resizeEl.style.height = originalInlineHeight;
10635
12194
  resizeEl.style.borderWidth = originalInlineBorderWidth;
10636
12195
  resizeEl.style.fontSize = originalInlineFontSize;
12196
+ (scaledTextTargetsCache || []).forEach(function (target) {
12197
+ target.el.style.fontSize = target.originalInlineFontSize;
12198
+ });
10637
12199
  selectedEl = resizeEl;
10638
12200
  positionOverlay(selectionOverlay, selectedEl);
10639
12201
  }
@@ -10690,6 +12252,27 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10690
12252
  },
10691
12253
  "*",
10692
12254
  );
12255
+ if (scaleToolEnabled) {
12256
+ (scaledTextTargetsCache || []).forEach(function (target) {
12257
+ var textStyles: Record<string, string> = {
12258
+ fontSize: target.el.style.fontSize,
12259
+ };
12260
+ (window.parent as Window).postMessage(
12261
+ {
12262
+ type: "visual-style-change",
12263
+ selector: getSelector(target.el),
12264
+ styles: textStyles,
12265
+ originalStyles: originalInlineStylesForPatch(
12266
+ target.el,
12267
+ textStyles,
12268
+ ),
12269
+ payload: getElementInfo(target.el),
12270
+ preserveSelection: true,
12271
+ },
12272
+ "*",
12273
+ );
12274
+ });
12275
+ }
10693
12276
  }
10694
12277
  document.addEventListener(events.move, onMove, true);
10695
12278
  document.addEventListener(events.up, onUp, true);
@@ -10697,6 +12280,324 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10697
12280
  setActiveDragCancel(cancelResizeDrag);
10698
12281
  }
10699
12282
 
12283
+ /**
12284
+ * Scales a multi-selection as one box: the drag resizes the group's bounds
12285
+ * and every member keeps its position and size relative to them. Separate
12286
+ * from startResize, whose single-element invariants (Alt-from-center,
12287
+ * per-axis touch tracking) have no group analogue.
12288
+ */
12289
+ function startGroupResize(handle, e) {
12290
+ if (readOnly) return;
12291
+ var members = collectSelectionMembers();
12292
+ if (members.length < 2) return;
12293
+ e.preventDefault();
12294
+ e.stopPropagation();
12295
+ var events = dragEventNames(e);
12296
+ var groupLeft = Infinity;
12297
+ var groupTop = Infinity;
12298
+ var groupRight = -Infinity;
12299
+ var groupBottom = -Infinity;
12300
+ var memberStates = members.map(function (member) {
12301
+ var el = member as HTMLElement;
12302
+ // Snapshot before ensurePositionable, or Escape restores the position
12303
+ // it just wrote onto a static member instead of the authored one.
12304
+ var snapshot = {
12305
+ el: el,
12306
+ originalInlinePosition: el.style.position,
12307
+ originalInlineLeft: el.style.left,
12308
+ originalInlineTop: el.style.top,
12309
+ originalInlineWidth: el.style.width,
12310
+ originalInlineHeight: el.style.height,
12311
+ originalInlineBorderWidth: el.style.borderWidth,
12312
+ originalInlineFontSize: el.style.fontSize,
12313
+ originLeft: 0,
12314
+ originTop: 0,
12315
+ originWidth: 0,
12316
+ originHeight: 0,
12317
+ // The center is the one point rotation leaves alone: a rotated
12318
+ // member's client rect is its inflated axis-aligned box, so corners
12319
+ // would scale it to the wrong place.
12320
+ originCenterX: 0,
12321
+ originCenterY: 0,
12322
+ originBorderWidth: 0,
12323
+ originFontSize: 0,
12324
+ textTargets: null as ReturnType<typeof collectScaleFontTargets> | null,
12325
+ };
12326
+ rememberLiveVisualEditOriginalStyles(el);
12327
+ ensurePositionable(el);
12328
+ var cs = window.getComputedStyle(el);
12329
+ var rect = el.getBoundingClientRect();
12330
+ groupLeft = Math.min(groupLeft, rect.left);
12331
+ groupTop = Math.min(groupTop, rect.top);
12332
+ groupRight = Math.max(groupRight, rect.right);
12333
+ groupBottom = Math.max(groupBottom, rect.bottom);
12334
+ snapshot.originLeft = readPx(el.style.left || cs.left);
12335
+ snapshot.originTop = readPx(el.style.top || cs.top);
12336
+ snapshot.originWidth = readPx(cs.width);
12337
+ snapshot.originHeight = readPx(cs.height);
12338
+ snapshot.originCenterX = rect.left + rect.width / 2;
12339
+ snapshot.originCenterY = rect.top + rect.height / 2;
12340
+ snapshot.originBorderWidth = readPx(
12341
+ el.style.borderWidth || cs.borderWidth,
12342
+ );
12343
+ snapshot.originFontSize = readPx(el.style.fontSize || cs.fontSize);
12344
+ return snapshot;
12345
+ });
12346
+ var groupWidth = Math.max(1, groupRight - groupLeft);
12347
+ var groupHeight = Math.max(1, groupBottom - groupTop);
12348
+ // The group's fixed corner for this handle — every member scales away
12349
+ // from it, so the opposite side of the selection stays put.
12350
+ var anchorX = handle.indexOf("w") !== -1 ? groupRight : groupLeft;
12351
+ var anchorY = handle.indexOf("n") !== -1 ? groupBottom : groupTop;
12352
+ // Clamp by the SMALLEST member, not by the group box: a factor that keeps
12353
+ // the group above 8px can still collapse (or mirror) a small member.
12354
+ var minMemberWidth = Math.max(
12355
+ 1,
12356
+ Math.min.apply(
12357
+ null,
12358
+ memberStates.map(function (state) {
12359
+ return Math.max(1, state.originWidth);
12360
+ }),
12361
+ ),
12362
+ );
12363
+ var minMemberHeight = Math.max(
12364
+ 1,
12365
+ Math.min.apply(
12366
+ null,
12367
+ memberStates.map(function (state) {
12368
+ return Math.max(1, state.originHeight);
12369
+ }),
12370
+ ),
12371
+ );
12372
+ var minFactorX = 8 / minMemberWidth;
12373
+ var minFactorY = 8 / minMemberHeight;
12374
+ var startX = e.clientX;
12375
+ var startY = e.clientY;
12376
+ var groupGestureViewport = bridgeGestureViewport();
12377
+ var bridgeGroupResizeController = createCanvasGestureController({
12378
+ capabilities: { move: true, resize: true },
12379
+ drag: { threshold: 3, duplicateModifier: "alt" },
12380
+ minSize: 8,
12381
+ adapter: {
12382
+ preview: function () {
12383
+ return { handled: true };
12384
+ },
12385
+ commit: function () {
12386
+ return { handled: true };
12387
+ },
12388
+ cancel: function () {
12389
+ return { handled: true };
12390
+ },
12391
+ },
12392
+ });
12393
+ bridgeGroupResizeController.pointerDown({
12394
+ kind: "resize",
12395
+ objectIds: memberStates.map(function (state) {
12396
+ return getSelector(state.el);
12397
+ }),
12398
+ pointer: bridgeGesturePointer(e),
12399
+ viewport: groupGestureViewport,
12400
+ canvas: {
12401
+ width: groupGestureViewport.width,
12402
+ height: groupGestureViewport.height,
12403
+ },
12404
+ handle: handle,
12405
+ rect: {
12406
+ x: groupLeft,
12407
+ y: groupTop,
12408
+ width: groupWidth,
12409
+ height: groupHeight,
12410
+ },
12411
+ });
12412
+
12413
+ function groupFactors(ev) {
12414
+ var dx = ev.clientX - startX;
12415
+ var dy = ev.clientY - startY;
12416
+ var nextWidth =
12417
+ handle.indexOf("e") !== -1 ? groupWidth + dx : groupWidth - dx;
12418
+ var nextHeight =
12419
+ handle.indexOf("s") !== -1 ? groupHeight + dy : groupHeight - dy;
12420
+ var factorX = nextWidth / groupWidth;
12421
+ var factorY = nextHeight / groupHeight;
12422
+ if (ev.shiftKey || scaleToolEnabled) {
12423
+ // Clamping the axes separately against their own minimums would
12424
+ // break the very lock this branch applies.
12425
+ var uniform = Math.max(
12426
+ Math.max(minFactorX, minFactorY),
12427
+ Math.abs(dx) > Math.abs(dy) ? factorX : factorY,
12428
+ );
12429
+ return { x: uniform, y: uniform };
12430
+ }
12431
+ return {
12432
+ x: Math.max(minFactorX, factorX),
12433
+ y: Math.max(minFactorY, factorY),
12434
+ };
12435
+ }
12436
+
12437
+ function memberTextTargets(state) {
12438
+ if (!state.textTargets) {
12439
+ state.textTargets = collectScaleFontTargets(state.el);
12440
+ }
12441
+ return state.textTargets;
12442
+ }
12443
+
12444
+ function onMove(ev) {
12445
+ var controllerMove = bridgeGroupResizeController.pointerMove(
12446
+ bridgeGesturePointer(ev),
12447
+ );
12448
+ if (controllerMove.phase !== "active") return;
12449
+ var factor = groupFactors(ev);
12450
+ memberStates.forEach(function (state) {
12451
+ var nextCenterX = anchorX + (state.originCenterX - anchorX) * factor.x;
12452
+ var nextCenterY = anchorY + (state.originCenterY - anchorY) * factor.y;
12453
+ var nextWidth = state.originWidth * factor.x;
12454
+ var nextHeight = state.originHeight * factor.y;
12455
+ state.el.style.left =
12456
+ Math.round(
12457
+ state.originLeft +
12458
+ (nextCenterX - state.originCenterX) -
12459
+ (nextWidth - state.originWidth) / 2,
12460
+ ) + "px";
12461
+ state.el.style.top =
12462
+ Math.round(
12463
+ state.originTop +
12464
+ (nextCenterY - state.originCenterY) -
12465
+ (nextHeight - state.originHeight) / 2,
12466
+ ) + "px";
12467
+ state.el.style.width = Math.round(nextWidth) + "px";
12468
+ state.el.style.height = Math.round(nextHeight) + "px";
12469
+ if (!scaleToolEnabled) return;
12470
+ if (state.originBorderWidth > 0) {
12471
+ state.el.style.borderWidth =
12472
+ Math.max(
12473
+ 0,
12474
+ Math.round(state.originBorderWidth * factor.x * 100) / 100,
12475
+ ) + "px";
12476
+ }
12477
+ if (state.originFontSize > 0) {
12478
+ state.el.style.fontSize =
12479
+ Math.max(
12480
+ 1,
12481
+ Math.round(state.originFontSize * factor.x * 100) / 100,
12482
+ ) + "px";
12483
+ }
12484
+ memberTextTargets(state).forEach(function (target) {
12485
+ target.el.style.fontSize =
12486
+ Math.max(
12487
+ 1,
12488
+ Math.round(target.originFontSize * factor.x * 100) / 100,
12489
+ ) + "px";
12490
+ });
12491
+ });
12492
+ showTransformBadge(
12493
+ Math.round(groupWidth * factor.x) +
12494
+ " x " +
12495
+ Math.round(groupHeight * factor.y),
12496
+ ev.clientX,
12497
+ ev.clientY,
12498
+ );
12499
+ refreshOverlays();
12500
+ }
12501
+
12502
+ function cleanupGroupResizeDrag() {
12503
+ document.removeEventListener(events.move, onMove, true);
12504
+ document.removeEventListener(events.up, onUp, true);
12505
+ document.removeEventListener("keydown", onGroupResizeKeyDown, true);
12506
+ clearActiveDragCancel(cancelGroupResizeDrag);
12507
+ }
12508
+
12509
+ function cancelGroupResizeDrag() {
12510
+ bridgeGroupResizeController.cancel();
12511
+ cleanupGroupResizeDrag();
12512
+ hideTransformBadge();
12513
+ memberStates.forEach(function (state) {
12514
+ if (!document.documentElement.contains(state.el)) return;
12515
+ state.el.style.position = state.originalInlinePosition;
12516
+ state.el.style.left = state.originalInlineLeft;
12517
+ state.el.style.top = state.originalInlineTop;
12518
+ state.el.style.width = state.originalInlineWidth;
12519
+ state.el.style.height = state.originalInlineHeight;
12520
+ state.el.style.borderWidth = state.originalInlineBorderWidth;
12521
+ state.el.style.fontSize = state.originalInlineFontSize;
12522
+ (state.textTargets || []).forEach(function (target) {
12523
+ target.el.style.fontSize = target.originalInlineFontSize;
12524
+ });
12525
+ });
12526
+ suppressNextShieldClickBriefly();
12527
+ refreshOverlays();
12528
+ return true;
12529
+ }
12530
+
12531
+ function onGroupResizeKeyDown(ev) {
12532
+ if (ev.key !== "Escape") return;
12533
+ stopNativeInteraction(ev);
12534
+ cancelGroupResizeDrag();
12535
+ }
12536
+
12537
+ function onUp(ev) {
12538
+ var controllerEnd = bridgeGroupResizeController.pointerUp(
12539
+ bridgeGesturePointer(ev),
12540
+ );
12541
+ cleanupGroupResizeDrag();
12542
+ hideTransformBadge();
12543
+ if (!controllerEnd.committed) return;
12544
+ // One style-change message per member, in order — the host composes
12545
+ // them against its same-tick content refs exactly like a group move.
12546
+ memberStates.forEach(function (state) {
12547
+ var styles: Record<string, string> = {
12548
+ position: state.el.style.position,
12549
+ left: state.el.style.left,
12550
+ top: state.el.style.top,
12551
+ width: state.el.style.width,
12552
+ height: state.el.style.height,
12553
+ };
12554
+ if (scaleToolEnabled && state.originBorderWidth > 0) {
12555
+ styles.borderWidth = state.el.style.borderWidth;
12556
+ }
12557
+ if (scaleToolEnabled && state.originFontSize > 0) {
12558
+ styles.fontSize = state.el.style.fontSize;
12559
+ }
12560
+ (window.parent as Window).postMessage(
12561
+ {
12562
+ type: "visual-style-change",
12563
+ selector: getSelector(state.el),
12564
+ styles: styles,
12565
+ originalStyles: originalInlineStylesForPatch(state.el, styles),
12566
+ payload: getElementInfo(state.el),
12567
+ },
12568
+ "*",
12569
+ );
12570
+ if (!scaleToolEnabled) return;
12571
+ (state.textTargets || []).forEach(function (target) {
12572
+ var textStyles: Record<string, string> = {
12573
+ fontSize: target.el.style.fontSize,
12574
+ };
12575
+ (window.parent as Window).postMessage(
12576
+ {
12577
+ type: "visual-style-change",
12578
+ selector: getSelector(target.el),
12579
+ styles: textStyles,
12580
+ originalStyles: originalInlineStylesForPatch(
12581
+ target.el,
12582
+ textStyles,
12583
+ ),
12584
+ payload: getElementInfo(target.el),
12585
+ // A scaled descendant is a side effect of the gesture, not the
12586
+ // object the user is holding.
12587
+ preserveSelection: true,
12588
+ },
12589
+ "*",
12590
+ );
12591
+ });
12592
+ });
12593
+ }
12594
+
12595
+ document.addEventListener(events.move, onMove, true);
12596
+ document.addEventListener(events.up, onUp, true);
12597
+ document.addEventListener("keydown", onGroupResizeKeyDown, true);
12598
+ setActiveDragCancel(cancelGroupResizeDrag);
12599
+ }
12600
+
10700
12601
  function startRotate(e) {
10701
12602
  if (readOnly) return;
10702
12603
  if (!selectedEl) return;
@@ -10884,12 +12785,24 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10884
12785
  preferSelected: selectedLayerDragPriorityEnabled,
10885
12786
  });
10886
12787
  var clickTarget = hitTarget;
12788
+ if ((window as any).__DND_DEBUG)
12789
+ dndLog("shield:down", {
12790
+ hit: getSelector(hit),
12791
+ dragTarget: getSelector(dragTarget),
12792
+ board: designCanvasBoardSurface,
12793
+ readOnly: readOnly,
12794
+ position: dragTarget
12795
+ ? window.getComputedStyle(dragTarget as Element).position
12796
+ : null,
12797
+ flowCandidate: dragTarget ? isFlowReorderCandidate(dragTarget) : null,
12798
+ });
10887
12799
  if (
10888
12800
  !dragTarget ||
10889
12801
  dragTarget === document.body ||
10890
12802
  dragTarget === document.documentElement ||
10891
12803
  isLayerInteractionBlocked(dragTarget)
10892
12804
  ) {
12805
+ dndLog("shield:reject", { reason: "no-drag-target" });
10893
12806
  return;
10894
12807
  }
10895
12808
  if (
@@ -10930,6 +12843,8 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
10930
12843
  if (readOnly) return;
10931
12844
  if (Math.hypot(ev.clientX - startX, ev.clientY - startY) <= 3) return;
10932
12845
  clearPendingShieldDrag();
12846
+ if (!didStartDrag)
12847
+ dndLog("shield:drag-start", { board: designCanvasBoardSurface });
10933
12848
  didStartDrag = true;
10934
12849
  // Multi-select group move: when the drag starts on a member of the
10935
12850
  // current 2+ selection, PRESERVE the whole selection (no
@@ -13300,6 +15215,7 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
13300
15215
  ],
13301
15216
  });
13302
15217
  }
15218
+ captureInitialSourceOwnership();
13303
15219
  if (runtimeLayerSnapshotEnabled) scheduleRuntimeLayerSnapshot();
13304
15220
 
13305
15221
  // One-time ready signal: tells the host that every message listener above is