@agent-native/core 0.92.10 → 0.92.11

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 (334) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +23 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +181 -15
  5. package/corpus/core/src/agent/run-manager.ts +31 -4
  6. package/corpus/core/src/agent/run-store.ts +80 -0
  7. package/corpus/core/src/cli/design-connect.ts +433 -151
  8. package/corpus/core/src/cli/skills.ts +14 -0
  9. package/corpus/core/src/client/AssistantChat.tsx +25 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +40 -6
  11. package/corpus/core/src/client/agent-chat.ts +165 -1
  12. package/corpus/core/src/client/chat/index.ts +9 -1
  13. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +38 -2
  14. package/corpus/core/src/client/index.ts +5 -0
  15. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +18 -7
  16. package/corpus/core/src/collab/awareness-store.ts +23 -12
  17. package/corpus/core/src/collab/client.ts +16 -5
  18. package/corpus/core/src/collab/index.ts +4 -0
  19. package/corpus/core/src/collab/recent-edits.ts +65 -2
  20. package/corpus/core/src/index.browser.ts +1 -0
  21. package/corpus/core/src/server/agent-chat-plugin.ts +98 -11
  22. package/corpus/core/src/server/builder-design-systems.ts +30 -4
  23. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +1 -1
  24. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +12 -6
  25. package/corpus/templates/clips/AGENTS.md +4 -0
  26. package/corpus/templates/clips/actions/create-recording.ts +2 -0
  27. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +5 -3
  28. package/corpus/templates/clips/actions/request-transcript.ts +102 -33
  29. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -0
  30. package/corpus/templates/clips/app/components/player/share-dialog.tsx +59 -19
  31. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +48 -4
  32. package/corpus/templates/clips/app/components/player/video-player.tsx +17 -12
  33. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
  34. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
  35. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  36. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
  37. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
  38. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
  39. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
  40. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
  41. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
  42. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
  43. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
  44. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -37
  45. package/corpus/templates/clips/app/routes/record.tsx +17 -4
  46. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  47. package/corpus/templates/clips/changelog/2026-07-09-desktop-popover-sits-closer-to-the-menu-bar.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-09-desktop-recording-optimization-progress-stays-visible-until-the-clip-opens.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-09-fixed-large-video-file-uploads-failing-before-their-resumabl.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-09-fixed-new-video-uploads-defaulting-to-private.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-09-long-recordings-now-get-enough-time-to-download-their-media-.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-09-meeting-notes-no-longer-lowers-your-microphone-volume-for-ot.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-09-on-mobile-tapping-a-playing-clip-reveals-playback-controls-w.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-09-regenerate-any-clip-transcript-from-its-saved-recording-or-r.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-07-09-slack-previews-now-show-the-clip-length-alongside-its-descri.md +6 -0
  56. package/corpus/templates/clips/desktop/src/app.tsx +12 -2
  57. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +8 -1
  58. package/corpus/templates/clips/desktop/src/lib/recorder.ts +8 -5
  59. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +5 -5
  60. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +3 -0
  61. package/corpus/templates/clips/desktop/src/styles.css +6 -3
  62. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +29 -11
  63. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  64. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +103 -21
  65. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +117 -39
  66. package/corpus/templates/clips/server/lib/slack-unfurls.ts +20 -1
  67. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +10 -4
  68. package/corpus/templates/clips/shared/recording-core.ts +12 -0
  69. package/corpus/templates/content/AGENTS.md +7 -6
  70. package/corpus/templates/content/actions/_document-flush.ts +132 -17
  71. package/corpus/templates/content/actions/_notion-action-utils.ts +15 -0
  72. package/corpus/templates/content/actions/create-and-link-notion-page.ts +2 -0
  73. package/corpus/templates/content/actions/link-notion-page.ts +2 -0
  74. package/corpus/templates/content/actions/pull-document.ts +5 -7
  75. package/corpus/templates/content/actions/pull-notion-page.ts +2 -0
  76. package/corpus/templates/content/actions/push-notion-page.ts +10 -0
  77. package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +2 -0
  78. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +88 -23
  79. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +32 -2
  80. package/corpus/templates/content/app/hooks/use-notion.ts +6 -6
  81. package/corpus/templates/content/app/i18n/zh-TW.ts +1 -0
  82. package/corpus/templates/content/app/i18n-data.ts +18 -0
  83. package/corpus/templates/content/changelog/2026-07-09-notion-conflict-choices-and-version-restores-preserve-docume.md +6 -0
  84. package/corpus/templates/content/server/lib/notion-sync.ts +80 -64
  85. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +29 -2
  86. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +86 -0
  87. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +57 -0
  88. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +11 -0
  89. package/corpus/templates/design/AGENTS.md +45 -10
  90. package/corpus/templates/design/actions/add-localhost-screens.ts +66 -13
  91. package/corpus/templates/design/actions/apply-component-prop-edit.ts +45 -36
  92. package/corpus/templates/design/actions/apply-motion-edit.ts +25 -31
  93. package/corpus/templates/design/actions/apply-shader-fill.ts +29 -42
  94. package/corpus/templates/design/actions/detach-component-instance.ts +337 -0
  95. package/corpus/templates/design/actions/export-design-as-figma-svg.ts +219 -0
  96. package/corpus/templates/design/actions/generate-design.ts +79 -12
  97. package/corpus/templates/design/actions/generate-screens.ts +57 -12
  98. package/corpus/templates/design/actions/get-figma-styles.ts +115 -0
  99. package/corpus/templates/design/actions/go-to-main-component.ts +254 -0
  100. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +4 -3
  101. package/corpus/templates/design/actions/import-design-source.ts +4 -31
  102. package/corpus/templates/design/actions/import-figma-clipboard.ts +134 -0
  103. package/corpus/templates/design/actions/import-figma-frame.ts +95 -0
  104. package/corpus/templates/design/actions/index-design-system-with-builder.ts +16 -1
  105. package/corpus/templates/design/actions/list-design-components.ts +104 -0
  106. package/corpus/templates/design/actions/list-figma-library-assets.ts +1 -24
  107. package/corpus/templates/design/actions/open-component-source.ts +2 -4
  108. package/corpus/templates/design/actions/present-design-variants.ts +412 -16
  109. package/corpus/templates/design/actions/preview-source-edit.ts +14 -1
  110. package/corpus/templates/design/actions/run-design-audit.ts +25 -3
  111. package/corpus/templates/design/actions/show-design-questions.ts +10 -0
  112. package/corpus/templates/design/actions/swap-component-instance.ts +544 -0
  113. package/corpus/templates/design/actions/take-design-screenshot.ts +45 -60
  114. package/corpus/templates/design/actions/write-local-file.ts +25 -1
  115. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +152 -0
  116. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1077 -58
  117. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +5 -5
  118. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +73 -21
  119. package/corpus/templates/design/app/components/design/EditPanel.tsx +16 -11
  120. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +364 -0
  121. package/corpus/templates/design/app/components/design/LayersPanel.tsx +180 -53
  122. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +0 -2
  123. package/corpus/templates/design/app/components/design/MotionDock.tsx +144 -21
  124. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1152 -167
  125. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +27 -5
  126. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +23 -2
  127. package/corpus/templates/design/app/components/design/TokensPanel.tsx +63 -49
  128. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1293 -54
  129. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +7 -0
  130. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +6 -1
  131. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +23 -6
  132. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +7 -0
  133. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -0
  134. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +14 -2
  135. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +37 -2
  136. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open-guard.ts +25 -0
  137. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +19 -1
  138. package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +43 -5
  139. package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +27 -2
  140. package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +10 -1
  141. package/corpus/templates/design/app/components/design/code-workbench/store.tsx +19 -2
  142. package/corpus/templates/design/app/components/design/design-canvas/annotation-snapshot.ts +265 -0
  143. package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +16 -5
  144. package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +16 -2
  145. package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +7 -2
  146. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +3 -1
  147. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +78 -8
  148. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +451 -73
  149. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +12 -4
  150. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +398 -272
  151. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +41 -7
  152. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +16 -3
  153. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +24 -4
  154. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +222 -4
  155. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +171 -78
  156. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +45 -5
  157. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +299 -28
  158. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +120 -150
  159. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +53 -1
  160. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +284 -117
  161. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +81 -0
  162. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +19 -14
  163. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +7 -0
  164. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +155 -0
  165. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +268 -46
  166. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +350 -31
  167. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +13 -4
  168. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +233 -23
  169. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +72 -2
  170. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +51 -11
  171. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +84 -11
  172. package/corpus/templates/design/app/components/design/inspector/index.ts +3 -0
  173. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +552 -0
  174. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +157 -0
  175. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +53 -0
  176. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +184 -0
  177. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +403 -26
  178. package/corpus/templates/design/app/components/design/multi-screen/types.ts +20 -1
  179. package/corpus/templates/design/app/components/design/types.ts +33 -0
  180. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +59 -7
  181. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +119 -17
  182. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +275 -63
  183. package/corpus/templates/design/app/hooks/use-question-flow.ts +14 -3
  184. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +67 -10
  185. package/corpus/templates/design/app/i18n/zh-TW.ts +91 -1
  186. package/corpus/templates/design/app/i18n-data.ts +1217 -10
  187. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +577 -0
  188. package/corpus/templates/design/app/lib/agent-chat.ts +23 -0
  189. package/corpus/templates/design/app/lib/design-import.ts +24 -0
  190. package/corpus/templates/design/app/lib/figma-clipboard.ts +157 -0
  191. package/corpus/templates/design/app/lib/figma-svg-copy.ts +221 -0
  192. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +143 -30
  193. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +15 -0
  194. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +33 -0
  195. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +38 -0
  196. package/corpus/templates/design/app/pages/design-editor/overview-annotation-context.ts +157 -0
  197. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +62 -6
  198. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +245 -0
  199. package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +586 -0
  200. package/corpus/templates/design/app/pages/design-editor/screen-auto-layout.ts +169 -0
  201. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +201 -1
  202. package/corpus/templates/design/changelog/2026-07-09-added-underline-strikethrough-and-text-case-uppercase-lowerc.md +6 -0
  203. package/corpus/templates/design/changelog/2026-07-09-annotate-now-draws-across-the-all-screens-canvas-without-swi.md +6 -0
  204. package/corpus/templates/design/changelog/2026-07-09-auto-layout-children-can-now-be-dragged-into-freeform-screen.md +6 -0
  205. package/corpus/templates/design/changelog/2026-07-09-board-shapes-now-stay-visible-and-nest-correctly-across-the-.md +6 -0
  206. package/corpus/templates/design/changelog/2026-07-09-component-source-navigation-now-opens-reliably-from-the-insp.md +6 -0
  207. package/corpus/templates/design/changelog/2026-07-09-cross-screen-local-react-layer-moves-now-hand-off-exact-sour.md +6 -0
  208. package/corpus/templates/design/changelog/2026-07-09-cross-screen-moves-undo-and-redo-now-keep-both-screens-stabl.md +6 -0
  209. package/corpus/templates/design/changelog/2026-07-09-detach-a-component-instance-into-plain-editable-eleme.md +6 -0
  210. package/corpus/templates/design/changelog/2026-07-09-dragging-layers-into-auto-layout-now-respects-ignore-auto-la.md +6 -0
  211. package/corpus/templates/design/changelog/2026-07-09-drawing-annotations-sent-to-the-agent-now-include-a-rendered.md +6 -0
  212. package/corpus/templates/design/changelog/2026-07-09-editor-sidebars-now-use-figma-matched-default-widths-for-a-r.md +6 -0
  213. package/corpus/templates/design/changelog/2026-07-09-empty-frames-and-rectangles-now-expose-full-auto-layout-cont.md +6 -0
  214. package/corpus/templates/design/changelog/2026-07-09-export-any-design-screen-as-a-real-vector-svg-for-figma-with.md +6 -0
  215. package/corpus/templates/design/changelog/2026-07-09-grid-auto-layout-now-uses-true-responsive-rows-and-columns-w.md +6 -0
  216. package/corpus/templates/design/changelog/2026-07-09-hidden-and-locked-screens-now-behave-consistently-between-la.md +6 -0
  217. package/corpus/templates/design/changelog/2026-07-09-image-fills-now-preserve-valid-urls-and-layered-fit-settings.md +6 -0
  218. package/corpus/templates/design/changelog/2026-07-09-import-figma-frames-as-pixel-accurate-design-screens-via-a-s.md +6 -0
  219. package/corpus/templates/design/changelog/2026-07-09-inspect-code-now-hides-internal-styling-metadata-and-wraps-l.md +6 -0
  220. package/corpus/templates/design/changelog/2026-07-09-inspector-blur-and-shadow-effects-now-preserve-other-effects.md +6 -0
  221. package/corpus/templates/design/changelog/2026-07-09-jump-to-a-components-other-instances-from-the-inspecto.md +6 -0
  222. package/corpus/templates/design/changelog/2026-07-09-keyboard-shortcuts-are-now-discoverable-in-a-figma-style-bot.md +6 -0
  223. package/corpus/templates/design/changelog/2026-07-09-layer-selection-now-keeps-cmd-or-ctrl-toggles-and-shift-rang.md +6 -0
  224. package/corpus/templates/design/changelog/2026-07-09-layers-dropped-into-auto-layout-now-participate-correctly-in.md +6 -0
  225. package/corpus/templates/design/changelog/2026-07-09-local-live-edit-screens-now-enter-and-leave-full-view-withou.md +6 -0
  226. package/corpus/templates/design/changelog/2026-07-09-local-preview-urls-now-live-in-the-inspector-instead-of-taki.md +6 -0
  227. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-can-now-be-hidden-and-locked-from-layers-.md +6 -0
  228. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-now-keep-exact-source-provenance-and-supp.md +6 -0
  229. package/corpus/templates/design/changelog/2026-07-09-local-react-visual-editing-now-loads-reliably-and-shows-live.md +6 -0
  230. package/corpus/templates/design/changelog/2026-07-09-paste-from-figma-cmd-v-now-imports-exact-editable-nodes-when.md +6 -0
  231. package/corpus/templates/design/changelog/2026-07-09-pressing-escape-after-selecting-a-screen-now-reveals-page-pr.md +6 -0
  232. package/corpus/templates/design/changelog/2026-07-09-right-click-overlapping-or-nested-objects-to-choose-the-exac.md +6 -0
  233. package/corpus/templates/design/changelog/2026-07-09-scale-edge-stretch-and-center-constraints-now-preserve-layer.md +6 -0
  234. package/corpus/templates/design/changelog/2026-07-09-screen-rows-can-now-be-reordered-in-layers-with-matching-can.md +6 -0
  235. package/corpus/templates/design/changelog/2026-07-09-screen-selection-stays-responsive-in-designs-with-hundreds-o.md +6 -0
  236. package/corpus/templates/design/changelog/2026-07-09-shift-a-now-enables-auto-layout-on-a-selected-screen-with-fl.md +6 -0
  237. package/corpus/templates/design/changelog/2026-07-09-swap-a-component-instance-for-another-from-the-libra.md +6 -0
  238. package/corpus/templates/design/changelog/2026-07-10-drawing-annotations-now-stay-available-to-retry-when-the.md +6 -0
  239. package/corpus/templates/design/e2e/global-setup.ts +3 -6
  240. package/corpus/templates/design/e2e/helpers.ts +27 -6
  241. package/corpus/templates/design/server/lib/design-to-figma-svg.ts +1412 -0
  242. package/corpus/templates/design/server/lib/figma-clipboard-match.ts +196 -0
  243. package/corpus/templates/design/server/lib/figma-node-import.ts +311 -0
  244. package/corpus/templates/design/server/lib/figma-node-to-html.ts +1334 -0
  245. package/corpus/templates/design/server/lib/figma-paste-fallback.ts +64 -0
  246. package/corpus/templates/design/server/lib/playwright-runtime.ts +78 -0
  247. package/corpus/templates/design/server/plugins/db.ts +41 -0
  248. package/corpus/templates/design/server/source-workspace.ts +64 -0
  249. package/corpus/templates/design/shared/board-file.ts +13 -2
  250. package/corpus/templates/design/shared/canvas-math.ts +231 -33
  251. package/corpus/templates/design/shared/code-layer.ts +122 -8
  252. package/corpus/templates/design/shared/component-library.ts +161 -0
  253. package/corpus/templates/design/shared/db-conflict.ts +27 -0
  254. package/corpus/templates/design/shared/design-source-capabilities.ts +2 -1
  255. package/corpus/templates/design/shared/figma-url.ts +149 -0
  256. package/corpus/templates/design/shared/pen-path.ts +16 -7
  257. package/corpus/templates/design/shared/source-mode.ts +1 -0
  258. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +7 -19
  259. package/corpus/templates/plan/changelog/2026-07-09-canvas-panning-now-keeps-zoom-fixed-even-at-the-edges.md +6 -0
  260. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +16 -1
  261. package/corpus/templates/slides/app/context/DeckContext.tsx +311 -70
  262. package/corpus/templates/slides/changelog/2026-07-09-fixed-the-slide-rail-going-permanently-stale-during-a-long-a.md +6 -0
  263. package/dist/agent/production-agent.d.ts +79 -6
  264. package/dist/agent/production-agent.d.ts.map +1 -1
  265. package/dist/agent/production-agent.js +161 -14
  266. package/dist/agent/production-agent.js.map +1 -1
  267. package/dist/agent/run-manager.d.ts.map +1 -1
  268. package/dist/agent/run-manager.js +29 -5
  269. package/dist/agent/run-manager.js.map +1 -1
  270. package/dist/agent/run-store.d.ts +46 -0
  271. package/dist/agent/run-store.d.ts.map +1 -1
  272. package/dist/agent/run-store.js +61 -0
  273. package/dist/agent/run-store.js.map +1 -1
  274. package/dist/cli/design-connect.d.ts +6 -0
  275. package/dist/cli/design-connect.d.ts.map +1 -1
  276. package/dist/cli/design-connect.js +336 -139
  277. package/dist/cli/design-connect.js.map +1 -1
  278. package/dist/cli/skills.d.ts.map +1 -1
  279. package/dist/cli/skills.js +14 -0
  280. package/dist/cli/skills.js.map +1 -1
  281. package/dist/client/AssistantChat.d.ts +2 -0
  282. package/dist/client/AssistantChat.d.ts.map +1 -1
  283. package/dist/client/AssistantChat.js +19 -3
  284. package/dist/client/AssistantChat.js.map +1 -1
  285. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  286. package/dist/client/MultiTabAssistantChat.js +38 -18
  287. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  288. package/dist/client/agent-chat.d.ts +60 -0
  289. package/dist/client/agent-chat.d.ts.map +1 -1
  290. package/dist/client/agent-chat.js +122 -1
  291. package/dist/client/agent-chat.js.map +1 -1
  292. package/dist/client/chat/index.d.ts +1 -1
  293. package/dist/client/chat/index.d.ts.map +1 -1
  294. package/dist/client/chat/index.js +1 -1
  295. package/dist/client/chat/index.js.map +1 -1
  296. package/dist/client/extensions/EmbeddedExtension.d.ts +12 -0
  297. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  298. package/dist/client/extensions/EmbeddedExtension.js +36 -2
  299. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  300. package/dist/client/index.d.ts +1 -1
  301. package/dist/client/index.d.ts.map +1 -1
  302. package/dist/client/index.js +1 -1
  303. package/dist/client/index.js.map +1 -1
  304. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  305. package/dist/client/rich-markdown-editor/useCollabReconcile.js +18 -6
  306. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  307. package/dist/collab/awareness-store.d.ts +5 -0
  308. package/dist/collab/awareness-store.d.ts.map +1 -1
  309. package/dist/collab/awareness-store.js +19 -12
  310. package/dist/collab/awareness-store.js.map +1 -1
  311. package/dist/collab/client.d.ts.map +1 -1
  312. package/dist/collab/client.js +13 -5
  313. package/dist/collab/client.js.map +1 -1
  314. package/dist/collab/index.d.ts +1 -0
  315. package/dist/collab/index.d.ts.map +1 -1
  316. package/dist/collab/index.js +1 -0
  317. package/dist/collab/index.js.map +1 -1
  318. package/dist/collab/recent-edits.d.ts +3 -1
  319. package/dist/collab/recent-edits.d.ts.map +1 -1
  320. package/dist/collab/recent-edits.js +57 -2
  321. package/dist/collab/recent-edits.js.map +1 -1
  322. package/dist/index.browser.d.ts +1 -1
  323. package/dist/index.browser.d.ts.map +1 -1
  324. package/dist/index.browser.js +1 -1
  325. package/dist/index.browser.js.map +1 -1
  326. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  327. package/dist/server/agent-chat-plugin.js +87 -10
  328. package/dist/server/agent-chat-plugin.js.map +1 -1
  329. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  330. package/dist/server/builder-design-systems.d.ts +15 -0
  331. package/dist/server/builder-design-systems.d.ts.map +1 -1
  332. package/dist/server/builder-design-systems.js +9 -4
  333. package/dist/server/builder-design-systems.js.map +1 -1
  334. package/package.json +1 -1
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared "legacy" Figma paste fallback: saves the visible clipboard HTML
3
+ * (markers stripped by `parseVisibleClipboardHtml`) as a new Design screen.
4
+ *
5
+ * Extracted from `import-design-source.ts`'s `figma-paste-html` branch so the
6
+ * REST-first clipboard import path (`import-figma-clipboard.ts`) can fall back
7
+ * to the exact same behavior when node resolution can't confidently match the
8
+ * pasted content to specific Figma nodes, instead of re-implementing it.
9
+ */
10
+
11
+ import {
12
+ normalizeImportedHtmlDocument,
13
+ resolveImportDesignId,
14
+ saveImportedDesignFiles,
15
+ } from "./import-design-files.js";
16
+ import { parseVisibleClipboardHtml } from "./visible-clipboard-html.js";
17
+
18
+ export interface SaveFigmaPasteHtmlFallbackInput {
19
+ designId?: string;
20
+ clipboardHtml: string;
21
+ originalName?: string;
22
+ }
23
+
24
+ function baseFilename(originalName: string | undefined, fallback: string) {
25
+ return (originalName?.trim() || fallback).replace(/\.[^.]+$/, "") + ".html";
26
+ }
27
+
28
+ export async function saveFigmaPasteHtmlFallback(
29
+ input: SaveFigmaPasteHtmlFallbackInput,
30
+ ) {
31
+ const parsed = parseVisibleClipboardHtml(input.clipboardHtml);
32
+ if (!parsed.fallbackHtml) {
33
+ throw new Error(
34
+ "No visible HTML was found in the clipboard. Copy a frame, then paste into the canvas.",
35
+ );
36
+ }
37
+ const designId = await resolveImportDesignId(input.designId);
38
+ const saved = await saveImportedDesignFiles({
39
+ designId,
40
+ sourceType: "figma-paste-html",
41
+ files: [
42
+ {
43
+ filename: baseFilename(input.originalName, "figma-paste"),
44
+ fileType: "html",
45
+ content: normalizeImportedHtmlDocument(
46
+ parsed.fallbackHtml,
47
+ "visible clipboard HTML",
48
+ ),
49
+ source: {
50
+ sourceType: "figma-paste-html",
51
+ },
52
+ },
53
+ ],
54
+ });
55
+ return {
56
+ ...saved,
57
+ stats: {
58
+ sourceKind: "figma-paste",
59
+ format: "html",
60
+ frameCount: saved.files.length,
61
+ imageCount: 0,
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * playwright-runtime.ts — shared headless-Chromium bootstrap used by every
3
+ * server-side action that needs a real rendered DOM (as opposed to static
4
+ * HTML/CSS analysis): `take-design-screenshot.ts`'s visual diagnostics pass
5
+ * and `design-to-figma-svg.ts`'s scene extractor for the Figma SVG export.
6
+ *
7
+ * Extracted out of `take-design-screenshot.ts` (which originally owned this
8
+ * logic) so `server/lib/*` modules can share it without an inverted
9
+ * lib -> action dependency. `take-design-screenshot.ts` re-exports these same
10
+ * names for backward compatibility with its existing spec/imports.
11
+ */
12
+
13
+ export type PlaywrightModule = {
14
+ chromium: import("@playwright/test").BrowserType;
15
+ };
16
+
17
+ /**
18
+ * Dynamic import of a real Chromium-capable Playwright package. Tries the
19
+ * bare `"playwright"` package first (present when `@agent-native/core`'s
20
+ * optional dependency resolved), then falls back to `@playwright/test` (a
21
+ * direct devDependency of this template, used by its own e2e suite, which
22
+ * re-exports the same chromium/Browser API). Loaded via a non-literal
23
+ * specifier so bundlers don't try to statically resolve/include it — it's
24
+ * optional and can be entirely absent (e.g. in a hosted deploy).
25
+ */
26
+ export async function importPlaywright(): Promise<PlaywrightModule> {
27
+ try {
28
+ const specifier = "playwright";
29
+ return (await import(
30
+ /* @vite-ignore */ specifier
31
+ )) as unknown as PlaywrightModule;
32
+ } catch {
33
+ return (await import("@playwright/test")) as unknown as PlaywrightModule;
34
+ }
35
+ }
36
+
37
+ const SYSTEM_CHROME_EXECUTABLES = [
38
+ "/usr/bin/google-chrome-stable",
39
+ "/usr/bin/google-chrome",
40
+ "/usr/bin/chromium-browser",
41
+ "/usr/bin/chromium",
42
+ ];
43
+
44
+ /** Pure classifier for "no Chromium binary available" errors. */
45
+ export function isMissingBrowserError(err: unknown): boolean {
46
+ const message = err instanceof Error ? err.message : String(err);
47
+ return /Executable doesn't exist|playwright install|browser.*not found|chromium.*not found/i.test(
48
+ message,
49
+ );
50
+ }
51
+
52
+ /** Launches Chromium, falling back to a system Chrome/Chromium binary when
53
+ * Playwright's bundled browser isn't installed (hosted/serverless deploys). */
54
+ export async function launchChromium(
55
+ chromium: import("@playwright/test").BrowserType,
56
+ ): Promise<import("@playwright/test").Browser> {
57
+ const launchOptions = { args: ["--no-sandbox"] };
58
+ try {
59
+ return await chromium.launch(launchOptions);
60
+ } catch (err) {
61
+ if (!isMissingBrowserError(err)) throw err;
62
+ const { existsSync } = await import("node:fs");
63
+ for (const executablePath of SYSTEM_CHROME_EXECUTABLES) {
64
+ if (!existsSync(executablePath)) continue;
65
+ try {
66
+ return await chromium.launch({ ...launchOptions, executablePath });
67
+ } catch {
68
+ // Try the next candidate; the original error is rethrown below.
69
+ }
70
+ }
71
+ throw err;
72
+ }
73
+ }
74
+
75
+ // NOTE: no shared `chromiumUnavailableReason` here on purpose — each caller's
76
+ // message should name ITS OWN fallback (e.g. `take-design-screenshot.ts`
77
+ // points at `run-design-audit`; the Figma SVG export points at `export-svg`),
78
+ // so that stays a small, action-local export next to each call site.
@@ -356,8 +356,49 @@ ALTER TABLE design_files ADD COLUMN IF NOT EXISTS content_operation_result_hash
356
356
  * drops, renames, or retypes anything — and any failure here is logged and
357
357
  * swallowed so it can never fail boot.
358
358
  */
359
+ /**
360
+ * Best-effort unique index guarding against the add-localhost-screens
361
+ * cross-request race: two concurrent calls placing the same localhost route
362
+ * each see "no existing design_files row" from their own snapshot and both
363
+ * insert a fresh row using the same deterministic filename for that route
364
+ * (see actions/add-localhost-screens.ts). A unique index on
365
+ * (design_id, filename) makes the losing insert fail instead of silently
366
+ * creating an overlapping duplicate screen.
367
+ *
368
+ * This intentionally does NOT live in `runDesignMigrations` above:
369
+ * `CREATE UNIQUE INDEX` fails outright against any database that already
370
+ * contains a duplicate (design_id, filename) pair — e.g. from this exact race
371
+ * having already fired before this fix shipped — and `runMigrations` has no
372
+ * way to tolerate that failure class (only lock-timeout / duplicate-column /
373
+ * permission errors are swallowed there). A failed migration entry blocks
374
+ * every later entry in the list forever: it crashes the process outright in
375
+ * local dev, and on serverless it silently stops applying anything past it on
376
+ * every cold start. So this runs as its own best-effort step instead: created
377
+ * when possible, safely skipped (with a warning) when legacy duplicates block
378
+ * it. `add-localhost-screens.ts`'s insert path tolerates either outcome — it
379
+ * catches a real unique-violation from this index and falls back to updating
380
+ * the row that won the race, and is a no-op/no-crash path when the index
381
+ * isn't present yet.
382
+ */
383
+ async function ensureDesignFilesUniqueIndex(): Promise<void> {
384
+ try {
385
+ await getDbExec().execute(
386
+ `CREATE UNIQUE INDEX IF NOT EXISTS design_files_design_filename_unique_idx ON design_files (design_id, filename)`,
387
+ );
388
+ } catch (err) {
389
+ console.warn(
390
+ "[db] design_files_design_filename_unique_idx not created — likely " +
391
+ "pre-existing duplicate (design_id, filename) rows from the " +
392
+ "add-localhost-screens race predating this fix. New concurrent " +
393
+ "inserts remain best-effort until the duplicates are cleaned up:",
394
+ err instanceof Error ? err.message : err,
395
+ );
396
+ }
397
+ }
398
+
359
399
  export default async (nitroApp: any): Promise<void> => {
360
400
  await runDesignMigrations(nitroApp);
401
+ await ensureDesignFilesUniqueIndex();
361
402
  try {
362
403
  const summary = await ensureAdditiveColumns({
363
404
  db: getDbExec(),
@@ -211,6 +211,70 @@ export async function readLiveSourceFile(file: SourceWorkspaceFile): Promise<{
211
211
  };
212
212
  }
213
213
 
214
+ /**
215
+ * A caller-supplied editor snapshot has two distinct identities:
216
+ *
217
+ * - `content` is the working copy the mutation must transform (it may contain
218
+ * unsaved local edits), while
219
+ * - `expectedVersionHash` is the live source version that working copy is
220
+ * allowed to replace.
221
+ *
222
+ * Hashing `currentContent` for both roles creates a false conflict whenever a
223
+ * local working copy is legitimately ahead of the persisted/live base. This
224
+ * helper accepts that working copy only when its SQL revision still matches
225
+ * and the live document is either the persisted base it was derived from or
226
+ * the working copy itself (for callers that already published it to Yjs).
227
+ * Any third live value is a genuine concurrent edit and fails closed. The
228
+ * returned live hash must be passed to `writeInlineSourceFile`, whose
229
+ * read-check-write lock closes the race after this preparation step.
230
+ */
231
+ export class SourceWorkspaceEditConflictError extends Error {
232
+ constructor(
233
+ message = "Source file changed since the editor snapshot was prepared.",
234
+ ) {
235
+ super(message);
236
+ this.name = "SourceWorkspaceEditConflictError";
237
+ }
238
+ }
239
+
240
+ export async function prepareInlineSourceEdit(args: {
241
+ file: SourceWorkspaceFile;
242
+ currentContent?: string;
243
+ revision?: string;
244
+ }): Promise<{ content: string; expectedVersionHash: string }> {
245
+ const live = await readLiveSourceFile(args.file);
246
+
247
+ if (args.currentContent === undefined) {
248
+ return {
249
+ content: live.content,
250
+ expectedVersionHash: live.versionHash,
251
+ };
252
+ }
253
+
254
+ if (!args.revision) {
255
+ throw new SourceWorkspaceEditConflictError(
256
+ "A source revision is required with current editor content.",
257
+ );
258
+ }
259
+ if (!args.file.updatedAt || args.revision !== args.file.updatedAt) {
260
+ throw new SourceWorkspaceEditConflictError();
261
+ }
262
+
263
+ const persistedVersionHash = sourceContentHash(args.file.content ?? "");
264
+ const workingVersionHash = sourceContentHash(args.currentContent);
265
+ if (
266
+ live.versionHash !== persistedVersionHash &&
267
+ live.versionHash !== workingVersionHash
268
+ ) {
269
+ throw new SourceWorkspaceEditConflictError();
270
+ }
271
+
272
+ return {
273
+ content: args.currentContent,
274
+ expectedVersionHash: live.versionHash,
275
+ };
276
+ }
277
+
214
278
  export async function writeInlineSourceFile(args: {
215
279
  designId: string;
216
280
  file: SourceWorkspaceFile;
@@ -680,8 +680,19 @@ export function stripBoardSurfaceOffsetFromCoord(value: number): number {
680
680
  * coordinate space. This is the pure seam used by
681
681
  * `handleOverviewPrimitiveReparent` (DesignEditor.tsx).
682
682
  *
683
- * Both inputs are normalized with `stripBoardSurfaceOffsetFromCoord` first:
684
- * a source that was persisted in board-iframe viewport coordinates
683
+ * The flat subtraction below is only correct when both `source` and `target`
684
+ * are already expressed in the SAME coordinate space — historically that
685
+ * meant "both are direct children of the screen root", because callers used
686
+ * to read a node's own inline left/top verbatim (parent-relative, not
687
+ * root-relative). DesignEditor.tsx's `getAbsolutePositioningForNodeInHtml`
688
+ * now resolves both inputs with an ancestor walk
689
+ * (`authoredElementPosition`) up to the screen root before calling this, so
690
+ * the subtraction is valid regardless of how deeply either node is nested —
691
+ * dropping a node into a container that's itself nested inside another frame
692
+ * no longer produces a garbage delta.
693
+ *
694
+ * Both inputs are also normalized with `stripBoardSurfaceOffsetFromCoord`
695
+ * first: a source that was persisted in board-iframe viewport coordinates
685
696
  * (boardCoord + 65536 — see BOARD_SURFACE_CONTENT_OFFSET_PX) is rebased to
686
697
  * board space before subtracting the target's origin, so the child's new
687
698
  * left/top always comes out parent-relative regardless of which upstream
@@ -1228,51 +1228,34 @@ function projectionsOverlap(
1228
1228
  * For `origin.rotation` falsy this is identical to calling
1229
1229
  * `resizeFrameFromDelta` directly.
1230
1230
  */
1231
- export function resizeRotatedFrameFromDelta(
1231
+ /**
1232
+ * Shared re-anchor step used by both resizeRotatedFrameFromDelta and its
1233
+ * snap-aware counterpart below: given the frame's ORIGIN geometry (still
1234
+ * carrying its rotation) and a candidate RESIZED-LOCAL geometry (unrotated,
1235
+ * already grown/shrunk along the frame's own local axes — optionally
1236
+ * adjusted by a snap pass in between), translates the resized-local geometry
1237
+ * so the same handle-opposite anchor point (or the frame's own center, for
1238
+ * an alt/option "resize from center" drag) stays fixed in WORLD space once
1239
+ * rotation is re-applied. See resizeRotatedFrameFromDelta's original inline
1240
+ * comment (kept there) for why alt-resize anchors on the center instead of
1241
+ * the opposite corner/edge.
1242
+ */
1243
+ function reanchorRotatedResizeToWorld(
1232
1244
  origin: FrameGeometry,
1233
1245
  handle: ResizeHandle,
1234
- worldDx: number,
1235
- worldDy: number,
1236
- options: ResizeFrameOptions = {},
1246
+ degrees: number,
1247
+ resizedLocal: FrameGeometry,
1248
+ options: Pick<ResizeFrameOptions, "resizeFromCenter">,
1237
1249
  ): FrameGeometry {
1238
- const degrees = origin.rotation ?? 0;
1239
- if (!degrees) {
1240
- return resizeFrameFromDelta(origin, handle, worldDx, worldDy, options);
1241
- }
1242
-
1243
1250
  const originCenter = {
1244
1251
  x: origin.x + origin.width / 2,
1245
1252
  y: origin.y + origin.height / 2,
1246
1253
  };
1247
- // Alt/option resize (`resizeFromCenter`) already grows the frame
1248
- // symmetrically about its own center in LOCAL space (resizeFrameFromDelta
1249
- // below keeps the center fixed on both axes for that mode). To keep that
1250
- // center fixed in WORLD space too — matching Figma, where alt-resizing a
1251
- // rotated frame grows around its visual center rather than pivoting off an
1252
- // opposite corner — the re-anchor step below must use the center as its
1253
- // fixed point instead of the handle's opposite corner/edge. Using the
1254
- // center as the anchor is a no-op for the rotation (a point rotates around
1255
- // itself unchanged), so it stays put in world space by construction.
1256
- // Non-center (default) resizes keep anchoring on the opposite corner/edge
1257
- // so that world-fixed-anchor behavior is unchanged.
1258
1254
  const anchorLocalBefore = options.resizeFromCenter
1259
1255
  ? originCenter
1260
1256
  : getResizeAnchorPoint(origin, handle);
1261
1257
  const anchorWorld = rotatePoint(anchorLocalBefore, originCenter, degrees);
1262
1258
 
1263
- // Map the world-space pointer delta into the frame's own unrotated axes so
1264
- // dragging "outward along the handle" behaves the same regardless of how
1265
- // the frame is rotated.
1266
- const localDelta = rotateVector({ x: worldDx, y: worldDy }, -degrees);
1267
-
1268
- const resizedLocal = resizeFrameFromDelta(
1269
- { ...origin, rotation: undefined },
1270
- handle,
1271
- localDelta.x,
1272
- localDelta.y,
1273
- options,
1274
- );
1275
-
1276
1259
  const centerAfterLocal = {
1277
1260
  x: resizedLocal.x + resizedLocal.width / 2,
1278
1261
  y: resizedLocal.y + resizedLocal.height / 2,
@@ -1298,6 +1281,221 @@ export function resizeRotatedFrameFromDelta(
1298
1281
  };
1299
1282
  }
1300
1283
 
1284
+ export function resizeRotatedFrameFromDelta(
1285
+ origin: FrameGeometry,
1286
+ handle: ResizeHandle,
1287
+ worldDx: number,
1288
+ worldDy: number,
1289
+ options: ResizeFrameOptions = {},
1290
+ ): FrameGeometry {
1291
+ const degrees = origin.rotation ?? 0;
1292
+ if (!degrees) {
1293
+ return resizeFrameFromDelta(origin, handle, worldDx, worldDy, options);
1294
+ }
1295
+
1296
+ // Map the world-space pointer delta into the frame's own unrotated axes so
1297
+ // dragging "outward along the handle" behaves the same regardless of how
1298
+ // the frame is rotated.
1299
+ const localDelta = rotateVector({ x: worldDx, y: worldDy }, -degrees);
1300
+
1301
+ const resizedLocal = resizeFrameFromDelta(
1302
+ { ...origin, rotation: undefined },
1303
+ handle,
1304
+ localDelta.x,
1305
+ localDelta.y,
1306
+ options,
1307
+ );
1308
+
1309
+ return reanchorRotatedResizeToWorld(
1310
+ origin,
1311
+ handle,
1312
+ degrees,
1313
+ resizedLocal,
1314
+ options,
1315
+ );
1316
+ }
1317
+
1318
+ /** Resize handles in clockwise screen order (y grows downward), so rotating a
1319
+ * handle by one 90° quadrant clockwise is a +2 step through this ring. */
1320
+ const RESIZE_HANDLES_CLOCKWISE: readonly ResizeHandle[] = [
1321
+ "n",
1322
+ "ne",
1323
+ "e",
1324
+ "se",
1325
+ "s",
1326
+ "sw",
1327
+ "w",
1328
+ "nw",
1329
+ ];
1330
+
1331
+ /** Maps a local resize handle to the world-space direction it points after
1332
+ * rotating the frame clockwise by `quadrants` 90° steps (CSS-positive
1333
+ * rotation is clockwise on screen): at 90°, the local east handle points
1334
+ * due south in world space, so "e" → "s". */
1335
+ function rotateHandleByQuadrants(
1336
+ handle: ResizeHandle,
1337
+ quadrants: number,
1338
+ ): ResizeHandle {
1339
+ const index = RESIZE_HANDLES_CLOCKWISE.indexOf(handle);
1340
+ return RESIZE_HANDLES_CLOCKWISE[(index + 2 * ((quadrants % 4) + 4)) % 8]!;
1341
+ }
1342
+
1343
+ /**
1344
+ * Beyond this many degrees away from the nearest axis-aligned orientation
1345
+ * (0/90/180/270), resize snapping is skipped for rotated frames: the frame's
1346
+ * true edges diverge too far from any axis-aligned box for edge/center
1347
+ * alignment against siblings to mean anything, and a wrong-axis guide is
1348
+ * worse than none.
1349
+ */
1350
+ const ROTATED_RESIZE_SNAP_MAX_OFF_AXIS_DEGREES = 30;
1351
+
1352
+ /**
1353
+ * Rotation-aware counterpart to resizeRotatedFrameFromDelta that also snaps
1354
+ * to nearby stationary siblings, matching computeResizeSnap's contract for
1355
+ * the unrotated group-resize path (CV-style: same threshold/guide shape).
1356
+ *
1357
+ * How rotated snapping works (see WORK ITEM 2 in the canvas bug pass this was
1358
+ * added for, reworked in the pre-ship review): the unsnapped rotated resize
1359
+ * is computed first, then its actual world-space AABB is snapped against
1360
+ * stationary siblings' world bounds with the exact same computeResizeSnap
1361
+ * machinery a non-rotated resize uses — with the resize handle mapped into
1362
+ * its world orientation by the frame's nearest 90° quadrant (at ~90° the
1363
+ * local east edge faces due south in world space, so "e" snaps the AABB's
1364
+ * bottom edge; at ~180° it faces west; etc.). Any snap offset is then fed
1365
+ * back into the rotated resize as extra world pointer travel, so anchoring
1366
+ * stays exact. This is edge-exact at 0/90/180/270 (the AABB edges ARE the
1367
+ * frame's edges there) and approximate in between (pointer travel maps to
1368
+ * AABB-edge movement with a cos² factor); once the rotation is more than
1369
+ * ROTATED_RESIZE_SNAP_MAX_OFF_AXIS_DEGREES from every axis-aligned
1370
+ * orientation, snapping is skipped entirely (no guides) rather than snapping
1371
+ * an axis that no longer matches what the user sees.
1372
+ */
1373
+ export function resizeRotatedFrameFromDeltaWithSnap(
1374
+ origin: FrameGeometry,
1375
+ handle: ResizeHandle,
1376
+ worldDx: number,
1377
+ worldDy: number,
1378
+ stationary: FrameEntry[],
1379
+ snapOptions: ResizeSnapOptions,
1380
+ options: ResizeFrameOptions = {},
1381
+ ): { frame: FrameGeometry; guides: AlignmentGuide[] } {
1382
+ const degrees = origin.rotation ?? 0;
1383
+ if (!degrees) {
1384
+ const resizedLocal = resizeFrameFromDelta(
1385
+ origin,
1386
+ handle,
1387
+ worldDx,
1388
+ worldDy,
1389
+ options,
1390
+ );
1391
+ return computeResizeSnap(resizedLocal, stationary, handle, snapOptions);
1392
+ }
1393
+
1394
+ const localDelta = rotateVector({ x: worldDx, y: worldDy }, -degrees);
1395
+
1396
+ const resizedLocal = resizeFrameFromDelta(
1397
+ { ...origin, rotation: undefined },
1398
+ handle,
1399
+ localDelta.x,
1400
+ localDelta.y,
1401
+ options,
1402
+ );
1403
+
1404
+ // Snapping is skipped outright for a rotated frame whenever the caller's
1405
+ // aspect-preserve flag is set (shift-held or the K scale tool):
1406
+ // independently snapping x and y against the local-bounds approximation
1407
+ // above would distort the locked ratio the aspect-preserving resize above
1408
+ // just computed, and computeAspectPreservingResizeSnap's own single-locked-
1409
+ // ratio reasoning only holds against the frame's true (rotated) shape, not
1410
+ // this approximation. No snap beats a wrong one here.
1411
+ if (snapOptions.preserveAspectRatio || snapOptions.bypass) {
1412
+ return {
1413
+ frame: reanchorRotatedResizeToWorld(
1414
+ origin,
1415
+ handle,
1416
+ degrees,
1417
+ resizedLocal,
1418
+ options,
1419
+ ),
1420
+ guides: [],
1421
+ };
1422
+ }
1423
+
1424
+ const unsnapped = reanchorRotatedResizeToWorld(
1425
+ origin,
1426
+ handle,
1427
+ degrees,
1428
+ resizedLocal,
1429
+ options,
1430
+ );
1431
+
1432
+ // Map the resize handle into its world orientation by the nearest 90°
1433
+ // quadrant (see the doc comment above). Far off-axis, skip snapping
1434
+ // outright — a wrong-axis snap/guide is worse than none.
1435
+ const normalizedDegrees = ((degrees % 360) + 360) % 360;
1436
+ const nearestQuadrant = Math.round(normalizedDegrees / 90) % 4;
1437
+ const offAxisDegrees = Math.abs(
1438
+ normalizedDegrees - Math.round(normalizedDegrees / 90) * 90,
1439
+ );
1440
+ if (offAxisDegrees > ROTATED_RESIZE_SNAP_MAX_OFF_AXIS_DEGREES) {
1441
+ return { frame: unsnapped, guides: [] };
1442
+ }
1443
+
1444
+ const worldHandle = rotateHandleByQuadrants(handle, nearestQuadrant);
1445
+ const aabb = getRotatedFrameAABB(unsnapped);
1446
+ const worldBox: FrameGeometry = {
1447
+ x: aabb.left,
1448
+ y: aabb.top,
1449
+ width: aabb.width,
1450
+ height: aabb.height,
1451
+ };
1452
+ // Min sizes are per-axis, so they swap with the axes at 90/270.
1453
+ const worldSnapOptions =
1454
+ nearestQuadrant % 2 === 1
1455
+ ? {
1456
+ ...snapOptions,
1457
+ minWidth: snapOptions.minHeight,
1458
+ minHeight: snapOptions.minWidth,
1459
+ }
1460
+ : snapOptions;
1461
+
1462
+ const snap = computeResizeSnap(
1463
+ worldBox,
1464
+ stationary,
1465
+ worldHandle,
1466
+ worldSnapOptions,
1467
+ );
1468
+ if (snap.guides.length === 0) {
1469
+ return { frame: unsnapped, guides: [] };
1470
+ }
1471
+
1472
+ // The snap moved the dragged world edge(s) of the AABB; express that
1473
+ // movement as extra world pointer travel and recompute the rotated resize,
1474
+ // so the anchor-fixing translation stays exact instead of being applied to
1475
+ // an already-snapped box.
1476
+ const snapDx = handleAffectsEast(worldHandle)
1477
+ ? snap.frame.x + snap.frame.width - (worldBox.x + worldBox.width)
1478
+ : handleAffectsWest(worldHandle)
1479
+ ? snap.frame.x - worldBox.x
1480
+ : 0;
1481
+ const snapDy = handleAffectsSouth(worldHandle)
1482
+ ? snap.frame.y + snap.frame.height - (worldBox.y + worldBox.height)
1483
+ : handleAffectsNorth(worldHandle)
1484
+ ? snap.frame.y - worldBox.y
1485
+ : 0;
1486
+
1487
+ return {
1488
+ frame: resizeRotatedFrameFromDelta(
1489
+ origin,
1490
+ handle,
1491
+ worldDx + snapDx,
1492
+ worldDy + snapDy,
1493
+ options,
1494
+ ),
1495
+ guides: snap.guides,
1496
+ };
1497
+ }
1498
+
1301
1499
  /** The local (unrotated) point that a given resize handle keeps fixed: the
1302
1500
  * edge/corner opposite the handle, or the center of an axis the handle
1303
1501
  * doesn't affect at all (e.g. "n" leaves the horizontal axis untouched). */