@agent-native/core 0.79.27 → 0.80.1

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 (497) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +34 -0
  3. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +55 -1
  5. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +66 -1
  7. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +65 -1
  9. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +64 -1
  11. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +59 -1
  13. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +62 -1
  15. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +58 -1
  17. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +62 -1
  19. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +50 -1
  21. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +52 -1
  23. package/corpus/core/docs/content/skills-guide.mdx +5 -0
  24. package/corpus/core/docs/content/template-design.mdx +59 -1
  25. package/corpus/core/package.json +2 -1
  26. package/corpus/core/src/agent/engine/index.ts +1 -0
  27. package/corpus/core/src/agent/engine/registry.ts +40 -0
  28. package/corpus/core/src/agent/production-agent.ts +6 -2
  29. package/corpus/core/src/application-state/handlers.ts +1 -1
  30. package/corpus/core/src/brand-kit/index.ts +1 -1
  31. package/corpus/core/src/cli/design-connect.ts +490 -0
  32. package/corpus/core/src/cli/index.ts +16 -0
  33. package/corpus/core/src/cli/skills.ts +144 -3
  34. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  35. package/corpus/core/src/client/AssistantChat.tsx +43 -7
  36. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  37. package/corpus/core/src/client/agent-chat.ts +27 -3
  38. package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +1 -1
  39. package/corpus/core/src/client/blocks/library/diagram.tsx +2 -2
  40. package/corpus/core/src/client/blocks/library/wireframe.tsx +1 -1
  41. package/corpus/core/src/client/chat/message-components.tsx +1 -1
  42. package/corpus/core/src/client/chat-model-groups.ts +1 -2
  43. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +33 -66
  44. package/corpus/core/src/client/composer/TiptapComposer.tsx +99 -24
  45. package/corpus/core/src/client/composer/useVoiceDictation.ts +429 -260
  46. package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
  47. package/corpus/core/src/client/frame-protocol.ts +17 -2
  48. package/corpus/core/src/client/rich-markdown-editor/RegistryBlockNode.tsx +2 -2
  49. package/corpus/core/src/client/sharing/ShareButton.tsx +20 -15
  50. package/corpus/core/src/client/use-agent-engine-configured.ts +80 -28
  51. package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
  52. package/corpus/core/src/extensions/path.ts +1 -4
  53. package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
  54. package/corpus/core/src/mcp/build-server.ts +1 -1
  55. package/corpus/core/src/org/handlers.ts +5 -5
  56. package/corpus/core/src/org/plugin.ts +2 -2
  57. package/corpus/core/src/scripts/call-agent.ts +1 -1
  58. package/corpus/core/src/scripts/db/safety.ts +1 -1
  59. package/corpus/core/src/server/auth.ts +3 -0
  60. package/corpus/core/src/server/builder-design-systems.ts +308 -0
  61. package/corpus/core/src/server/core-routes-plugin.ts +29 -3
  62. package/corpus/core/src/server/index.ts +8 -0
  63. package/corpus/core/src/server/transcribe-voice.ts +57 -22
  64. package/corpus/core/src/shared/mcp-embed-headers.ts +23 -1
  65. package/corpus/core/src/styles/agent-native.css +60 -4
  66. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  67. package/corpus/core/src/usage/store.ts +2 -2
  68. package/corpus/core/src/vite/client.ts +2 -0
  69. package/corpus/core/src/voice/index.ts +18 -0
  70. package/corpus/core/src/voice/voice-cleanup-prompt.ts +38 -0
  71. package/corpus/core/src/voice/voice-context.ts +217 -0
  72. package/corpus/core/src/voice/voice-replacements.ts +75 -0
  73. package/corpus/templates/analytics/AGENTS.md +4 -3
  74. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
  75. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
  76. package/corpus/templates/analytics/actions/list-session-recordings.ts +6 -1
  77. package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
  78. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
  79. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +557 -400
  80. package/corpus/templates/analytics/app/global.css +0 -4
  81. package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
  82. package/corpus/templates/analytics/app/i18n/zh-TW.ts +39 -0
  83. package/corpus/templates/analytics/app/i18n-data.ts +424 -0
  84. package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
  85. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
  86. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
  87. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +82 -34
  88. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +7 -0
  89. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +144 -36
  90. package/corpus/templates/analytics/changelog/2026-06-28-ask-tab-now-matches-the-darker-app-background.md +6 -0
  91. package/corpus/templates/analytics/changelog/2026-06-28-dashboard-email-reports-now-use-cleaner-light-mode-snapshots.md +6 -0
  92. package/corpus/templates/analytics/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  93. package/corpus/templates/analytics/changelog/2026-06-28-sessions-now-show-only-signed-in-recordings-with-playable-re.md +6 -0
  94. package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
  95. package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
  96. package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
  97. package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
  98. package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
  99. package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
  100. package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
  101. package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
  102. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  103. package/corpus/templates/analytics/server/lib/dashboard-report.ts +75 -30
  104. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +5 -2
  105. package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
  106. package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
  107. package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
  108. package/corpus/templates/analytics/server/lib/session-replay.ts +53 -12
  109. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
  110. package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
  111. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -4
  112. package/corpus/templates/assets/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  113. package/corpus/templates/brain/app/components/layout/Layout.tsx +29 -2
  114. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +131 -42
  115. package/corpus/templates/brain/app/i18n/zh-TW.ts +2 -0
  116. package/corpus/templates/brain/app/i18n-data.ts +20 -0
  117. package/corpus/templates/brain/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  118. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +29 -1
  119. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +417 -299
  120. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  121. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  122. package/corpus/templates/calendar/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  123. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +6 -19
  124. package/corpus/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  125. package/corpus/templates/clips/AGENTS.md +4 -0
  126. package/corpus/templates/clips/actions/cleanup-transcript.ts +39 -9
  127. package/corpus/templates/clips/actions/finalize-meeting.ts +81 -12
  128. package/corpus/templates/clips/actions/lib/agents-md-context.ts +4 -1
  129. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -3
  130. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +1 -1
  131. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  132. package/corpus/templates/clips/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  133. package/corpus/templates/clips/desktop/src/lib/personal-vocabulary.ts +13 -8
  134. package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
  135. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
  136. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +60 -1
  137. package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
  138. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1 -1
  139. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -1
  140. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -5
  141. package/corpus/templates/content/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  142. package/corpus/templates/content/shared/document-export.ts +1 -1
  143. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
  144. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +154 -0
  145. package/corpus/templates/design/AGENTS.md +95 -5
  146. package/corpus/templates/design/DESIGN.md +82 -0
  147. package/corpus/templates/design/DEVELOPING.md +43 -0
  148. package/corpus/templates/design/actions/add-localhost-screens.ts +453 -0
  149. package/corpus/templates/design/actions/apply-shader.ts +277 -0
  150. package/corpus/templates/design/actions/apply-visual-edit.ts +303 -0
  151. package/corpus/templates/design/actions/connect-localhost.ts +183 -0
  152. package/corpus/templates/design/actions/edit-design.ts +195 -42
  153. package/corpus/templates/design/actions/generate-design.ts +71 -1
  154. package/corpus/templates/design/actions/generate-screens.ts +193 -0
  155. package/corpus/templates/design/actions/get-code-layer-projection.ts +191 -0
  156. package/corpus/templates/design/actions/get-shader.ts +119 -0
  157. package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
  158. package/corpus/templates/design/actions/list-localhost-connections.ts +95 -0
  159. package/corpus/templates/design/actions/navigate.ts +55 -2
  160. package/corpus/templates/design/actions/update-file.ts +9 -2
  161. package/corpus/templates/design/actions/view-screen.ts +68 -1
  162. package/corpus/templates/design/agent-native.app-skill.json +10 -0
  163. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +719 -0
  164. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +2120 -103
  165. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +74 -38
  166. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +401 -0
  167. package/corpus/templates/design/app/components/design/EditPanel.tsx +3966 -440
  168. package/corpus/templates/design/app/components/design/LayersPanel.tsx +1473 -0
  169. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +3940 -147
  170. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +700 -13
  171. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +105 -63
  172. package/corpus/templates/design/app/components/design/index.ts +0 -5
  173. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +353 -0
  174. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1589 -0
  175. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +511 -0
  176. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +395 -0
  177. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +2010 -0
  178. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
  179. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
  180. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +420 -0
  181. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +238 -0
  182. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +71 -0
  183. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +575 -0
  184. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +106 -0
  185. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
  186. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +741 -0
  187. package/corpus/templates/design/app/components/design/inspector/index.ts +87 -0
  188. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +201 -0
  189. package/corpus/templates/design/app/components/design/types.ts +23 -0
  190. package/corpus/templates/design/app/components/layout/Layout.tsx +24 -13
  191. package/corpus/templates/design/app/components/layout/Sidebar.tsx +4 -4
  192. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +474 -36
  193. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +22 -5
  194. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +24 -11
  195. package/corpus/templates/design/app/global.css +149 -5
  196. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -0
  197. package/corpus/templates/design/app/hooks/use-navigation-state.ts +130 -8
  198. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +329 -0
  199. package/corpus/templates/design/app/i18n/zh-TW.ts +277 -0
  200. package/corpus/templates/design/app/i18n-data.ts +3028 -1
  201. package/corpus/templates/design/app/pages/DesignEditor.tsx +5831 -904
  202. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
  203. package/corpus/templates/design/changelog/2026-06-28-annotate-mode-now-combines-drawing-and-comment-pins-in-one-c.md +6 -0
  204. package/corpus/templates/design/changelog/2026-06-28-design-editor-panel-borders-are-cleaner-so-the-canvas-edge-s.md +6 -0
  205. package/corpus/templates/design/changelog/2026-06-28-drawing-prompts-now-reliably-open-and-submit-to-the-design-a.md +6 -0
  206. package/corpus/templates/design/changelog/2026-06-28-editor-app-menus-now-use-an-outline-agent-native-mark-with-b.md +6 -0
  207. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +6 -0
  208. package/corpus/templates/design/changelog/2026-06-28-layer-selection-now-updates-chat-context-without-opening-the.md +6 -0
  209. package/corpus/templates/design/changelog/2026-06-28-layer-selections-now-preserve-hidden-and-locked-canvas-state.md +6 -0
  210. package/corpus/templates/design/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  211. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +6 -0
  212. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +6 -0
  213. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-handles-zooming-drawing-opening-and-dele.md +6 -0
  214. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-selects-screens-on-click-and-opens-them-.md +6 -0
  215. package/corpus/templates/design/changelog/2026-06-28-screen-overview-zooms-into-the-active-screen-with-smoother-e.md +6 -0
  216. package/corpus/templates/design/changelog/2026-06-28-selected-element-chips-in-chat-are-shorter-and-easier-to-sca.md +6 -0
  217. package/corpus/templates/design/changelog/2026-06-28-the-design-editor-toolbar-is-more-compact-and-the-app-menu-m.md +6 -0
  218. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-at-the-top-of-the-design-sidebar-instead-of-.md +6 -0
  219. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-in-the-right-inspector-beside-the-design-tab.md +6 -0
  220. package/corpus/templates/design/changelog/2026-06-29-active-sidebar-rows-use-soft-highlights.md +6 -0
  221. package/corpus/templates/design/changelog/2026-06-29-agents-can-now-open-screen-overview-focus-screens-and-add.md +6 -0
  222. package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
  223. package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
  224. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-edit-broad-copy-changes-more-reliably.md +6 -0
  225. package/corpus/templates/design/changelog/2026-06-29-design-editor-extensions-now-run-inline-beside-the-artboard.md +6 -0
  226. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
  227. package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
  228. package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
  229. package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
  230. package/corpus/templates/design/changelog/2026-06-29-sidebars-use-darker-chrome-and-chat-warns-immediately.md +6 -0
  231. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
  232. package/corpus/templates/design/changelog/2026-06-29-visual-edit-can-open-localhost-routes-as-url-backed-screens.md +6 -0
  233. package/corpus/templates/design/package.json +1 -0
  234. package/corpus/templates/design/server/db/schema.ts +26 -0
  235. package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
  236. package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
  237. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -0
  238. package/corpus/templates/design/shared/api.ts +27 -0
  239. package/corpus/templates/design/shared/canvas-frames.ts +103 -0
  240. package/corpus/templates/design/shared/canvas-math.ts +1058 -0
  241. package/corpus/templates/design/shared/code-layer.ts +2412 -0
  242. package/corpus/templates/design/shared/color-utils.ts +180 -0
  243. package/corpus/templates/design/shared/generation-session.ts +61 -0
  244. package/corpus/templates/design/shared/html-content.ts +21 -0
  245. package/corpus/templates/design/shared/pen-path.ts +207 -0
  246. package/corpus/templates/design/shared/shader-presets.ts +782 -0
  247. package/corpus/templates/design/shared/shader-safety.ts +137 -0
  248. package/corpus/templates/design/shared/source-mode.ts +237 -0
  249. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +149 -6
  250. package/corpus/templates/forms/app/i18n/ar-SA.ts +2 -0
  251. package/corpus/templates/forms/app/i18n/de-DE.ts +2 -0
  252. package/corpus/templates/forms/app/i18n/en-US.ts +2 -0
  253. package/corpus/templates/forms/app/i18n/es-ES.ts +2 -0
  254. package/corpus/templates/forms/app/i18n/fr-FR.ts +2 -0
  255. package/corpus/templates/forms/app/i18n/hi-IN.ts +2 -0
  256. package/corpus/templates/forms/app/i18n/ja-JP.ts +2 -0
  257. package/corpus/templates/forms/app/i18n/ko-KR.ts +2 -0
  258. package/corpus/templates/forms/app/i18n/pt-BR.ts +2 -0
  259. package/corpus/templates/forms/app/i18n/zh-CN.ts +2 -0
  260. package/corpus/templates/forms/app/i18n/zh-TW.ts +2 -0
  261. package/corpus/templates/forms/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  262. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +2 -2
  263. package/corpus/templates/macros/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  264. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +408 -247
  265. package/corpus/templates/mail/app/i18n/ar-SA.ts +4 -0
  266. package/corpus/templates/mail/app/i18n/de-DE.ts +4 -0
  267. package/corpus/templates/mail/app/i18n/en-US.ts +4 -0
  268. package/corpus/templates/mail/app/i18n/es-ES.ts +4 -0
  269. package/corpus/templates/mail/app/i18n/fr-FR.ts +4 -0
  270. package/corpus/templates/mail/app/i18n/hi-IN.ts +4 -0
  271. package/corpus/templates/mail/app/i18n/ja-JP.ts +4 -0
  272. package/corpus/templates/mail/app/i18n/ko-KR.ts +4 -0
  273. package/corpus/templates/mail/app/i18n/pt-BR.ts +4 -0
  274. package/corpus/templates/mail/app/i18n/zh-CN.ts +4 -0
  275. package/corpus/templates/mail/app/i18n/zh-TW.ts +4 -0
  276. package/corpus/templates/mail/app/lib/utils.ts +2 -2
  277. package/corpus/templates/mail/changelog/2026-06-28-pinned-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  278. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +6 -8
  279. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +1 -1
  280. package/corpus/templates/plan/app/global.css +61 -78
  281. package/corpus/templates/plan/app/pages/PlansPage.tsx +8 -0
  282. package/corpus/templates/plan/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  283. package/corpus/templates/plan/changelog/2026-06-28-plans-use-the-shared-neutral-light-and-dark-theme.md +6 -0
  284. package/corpus/templates/plan/changelog/2026-06-28-sending-open-feedback-from-a-plan-now-opens-the-inline-plan-.md +6 -0
  285. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
  286. package/corpus/templates/slides/AGENTS.md +3 -2
  287. package/corpus/templates/slides/actions/import-file.ts +26 -17
  288. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
  289. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
  290. package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
  291. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
  292. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
  293. package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
  294. package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
  295. package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
  296. package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
  297. package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
  298. package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
  299. package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
  300. package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
  301. package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
  302. package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
  303. package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
  304. package/corpus/templates/slides/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  305. package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
  306. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  307. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
  308. package/corpus/templates/videos/app/components/Sidebar.tsx +2 -2
  309. package/corpus/templates/videos/app/components/layout/Layout.tsx +29 -2
  310. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +113 -30
  311. package/corpus/templates/videos/app/i18n/ar-SA.ts +2 -0
  312. package/corpus/templates/videos/app/i18n/de-DE.ts +2 -0
  313. package/corpus/templates/videos/app/i18n/en-US.ts +2 -0
  314. package/corpus/templates/videos/app/i18n/es-ES.ts +2 -0
  315. package/corpus/templates/videos/app/i18n/fr-FR.ts +2 -0
  316. package/corpus/templates/videos/app/i18n/hi-IN.ts +2 -0
  317. package/corpus/templates/videos/app/i18n/ja-JP.ts +2 -0
  318. package/corpus/templates/videos/app/i18n/ko-KR.ts +2 -0
  319. package/corpus/templates/videos/app/i18n/pt-BR.ts +2 -0
  320. package/corpus/templates/videos/app/i18n/zh-CN.ts +2 -0
  321. package/corpus/templates/videos/app/i18n/zh-TW.ts +2 -0
  322. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  323. package/dist/agent/engine/index.d.ts +1 -1
  324. package/dist/agent/engine/index.d.ts.map +1 -1
  325. package/dist/agent/engine/index.js +1 -1
  326. package/dist/agent/engine/index.js.map +1 -1
  327. package/dist/agent/engine/registry.d.ts +9 -0
  328. package/dist/agent/engine/registry.d.ts.map +1 -1
  329. package/dist/agent/engine/registry.js +37 -0
  330. package/dist/agent/engine/registry.js.map +1 -1
  331. package/dist/agent/production-agent.d.ts.map +1 -1
  332. package/dist/agent/production-agent.js +4 -3
  333. package/dist/agent/production-agent.js.map +1 -1
  334. package/dist/application-state/handlers.js +1 -1
  335. package/dist/application-state/handlers.js.map +1 -1
  336. package/dist/brand-kit/index.d.ts +1 -1
  337. package/dist/brand-kit/index.js +1 -1
  338. package/dist/brand-kit/index.js.map +1 -1
  339. package/dist/cli/design-connect.d.ts +52 -0
  340. package/dist/cli/design-connect.d.ts.map +1 -0
  341. package/dist/cli/design-connect.js +409 -0
  342. package/dist/cli/design-connect.js.map +1 -0
  343. package/dist/cli/index.js +15 -0
  344. package/dist/cli/index.js.map +1 -1
  345. package/dist/cli/skills.d.ts +3 -0
  346. package/dist/cli/skills.d.ts.map +1 -1
  347. package/dist/cli/skills.js +139 -3
  348. package/dist/cli/skills.js.map +1 -1
  349. package/dist/client/AgentPanel.js +1 -1
  350. package/dist/client/AgentPanel.js.map +1 -1
  351. package/dist/client/AssistantChat.d.ts.map +1 -1
  352. package/dist/client/AssistantChat.js +38 -8
  353. package/dist/client/AssistantChat.js.map +1 -1
  354. package/dist/client/MultiTabAssistantChat.js +1 -1
  355. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  356. package/dist/client/agent-chat.d.ts +6 -0
  357. package/dist/client/agent-chat.d.ts.map +1 -1
  358. package/dist/client/agent-chat.js +17 -3
  359. package/dist/client/agent-chat.js.map +1 -1
  360. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  361. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  362. package/dist/client/blocks/library/diagram.js +1 -1
  363. package/dist/client/blocks/library/diagram.js.map +1 -1
  364. package/dist/client/blocks/library/wireframe.js +1 -1
  365. package/dist/client/blocks/library/wireframe.js.map +1 -1
  366. package/dist/client/chat/message-components.js +1 -1
  367. package/dist/client/chat/message-components.js.map +1 -1
  368. package/dist/client/chat-model-groups.d.ts.map +1 -1
  369. package/dist/client/chat-model-groups.js +1 -2
  370. package/dist/client/chat-model-groups.js.map +1 -1
  371. package/dist/client/components/LiveCursorOverlay.d.ts +1 -2
  372. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  373. package/dist/client/components/LiveCursorOverlay.js +19 -40
  374. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  375. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  376. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  377. package/dist/client/composer/TiptapComposer.js +86 -24
  378. package/dist/client/composer/TiptapComposer.js.map +1 -1
  379. package/dist/client/composer/useVoiceDictation.d.ts +9 -0
  380. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  381. package/dist/client/composer/useVoiceDictation.js +154 -32
  382. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  383. package/dist/client/extensions/portable-extension.d.ts +1 -1
  384. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  385. package/dist/client/extensions/portable-extension.js.map +1 -1
  386. package/dist/client/frame-protocol.d.ts +17 -2
  387. package/dist/client/frame-protocol.d.ts.map +1 -1
  388. package/dist/client/frame-protocol.js.map +1 -1
  389. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +2 -2
  390. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  391. package/dist/client/sharing/ShareButton.d.ts +2 -0
  392. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  393. package/dist/client/sharing/ShareButton.js +7 -6
  394. package/dist/client/sharing/ShareButton.js.map +1 -1
  395. package/dist/client/use-agent-engine-configured.d.ts +5 -0
  396. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  397. package/dist/client/use-agent-engine-configured.js +51 -25
  398. package/dist/client/use-agent-engine-configured.js.map +1 -1
  399. package/dist/code-agents/transcript-normalizer.js +1 -1
  400. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  401. package/dist/collab/routes.d.ts +1 -1
  402. package/dist/extensions/path.d.ts +1 -1
  403. package/dist/extensions/path.d.ts.map +1 -1
  404. package/dist/extensions/path.js.map +1 -1
  405. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  406. package/dist/integrations/a2a-continuation-processor.js +1 -1
  407. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  408. package/dist/mcp/build-server.d.ts +1 -1
  409. package/dist/mcp/build-server.d.ts.map +1 -1
  410. package/dist/mcp/build-server.js.map +1 -1
  411. package/dist/notifications/routes.d.ts +3 -3
  412. package/dist/observability/routes.d.ts +5 -5
  413. package/dist/org/handlers.js +5 -5
  414. package/dist/org/handlers.js.map +1 -1
  415. package/dist/org/plugin.js +2 -2
  416. package/dist/org/plugin.js.map +1 -1
  417. package/dist/scripts/call-agent.js +1 -1
  418. package/dist/scripts/call-agent.js.map +1 -1
  419. package/dist/scripts/db/safety.js +1 -1
  420. package/dist/scripts/db/safety.js.map +1 -1
  421. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  422. package/dist/server/auth.d.ts.map +1 -1
  423. package/dist/server/auth.js +3 -0
  424. package/dist/server/auth.js.map +1 -1
  425. package/dist/server/builder-design-systems.d.ts +28 -0
  426. package/dist/server/builder-design-systems.d.ts.map +1 -0
  427. package/dist/server/builder-design-systems.js +217 -0
  428. package/dist/server/builder-design-systems.js.map +1 -0
  429. package/dist/server/core-routes-plugin.d.ts +1 -0
  430. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  431. package/dist/server/core-routes-plugin.js +20 -3
  432. package/dist/server/core-routes-plugin.js.map +1 -1
  433. package/dist/server/index.d.ts +1 -0
  434. package/dist/server/index.d.ts.map +1 -1
  435. package/dist/server/index.js +1 -0
  436. package/dist/server/index.js.map +1 -1
  437. package/dist/server/transcribe-voice.d.ts +1 -1
  438. package/dist/server/transcribe-voice.d.ts.map +1 -1
  439. package/dist/server/transcribe-voice.js +40 -24
  440. package/dist/server/transcribe-voice.js.map +1 -1
  441. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  442. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  443. package/dist/shared/mcp-embed-headers.js +20 -1
  444. package/dist/shared/mcp-embed-headers.js.map +1 -1
  445. package/dist/styles/agent-native.css +60 -4
  446. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  447. package/dist/usage/store.js +2 -2
  448. package/dist/usage/store.js.map +1 -1
  449. package/dist/vite/client.d.ts.map +1 -1
  450. package/dist/vite/client.js +2 -0
  451. package/dist/vite/client.js.map +1 -1
  452. package/dist/voice/index.d.ts +5 -0
  453. package/dist/voice/index.d.ts.map +1 -0
  454. package/dist/voice/index.js +4 -0
  455. package/dist/voice/index.js.map +1 -0
  456. package/dist/voice/voice-cleanup-prompt.d.ts +6 -0
  457. package/dist/voice/voice-cleanup-prompt.d.ts.map +1 -0
  458. package/dist/voice/voice-cleanup-prompt.js +26 -0
  459. package/dist/voice/voice-cleanup-prompt.js.map +1 -0
  460. package/dist/voice/voice-context.d.ts +25 -0
  461. package/dist/voice/voice-context.d.ts.map +1 -0
  462. package/dist/voice/voice-context.js +156 -0
  463. package/dist/voice/voice-context.js.map +1 -0
  464. package/dist/voice/voice-replacements.d.ts +4 -0
  465. package/dist/voice/voice-replacements.d.ts.map +1 -0
  466. package/dist/voice/voice-replacements.js +52 -0
  467. package/dist/voice/voice-replacements.js.map +1 -0
  468. package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  469. package/docs/content/locales/ar-SA/template-design.mdx +55 -1
  470. package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  471. package/docs/content/locales/de-DE/template-design.mdx +66 -1
  472. package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  473. package/docs/content/locales/es-ES/template-design.mdx +65 -1
  474. package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  475. package/docs/content/locales/fr-FR/template-design.mdx +64 -1
  476. package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  477. package/docs/content/locales/hi-IN/template-design.mdx +59 -1
  478. package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  479. package/docs/content/locales/ja-JP/template-design.mdx +62 -1
  480. package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  481. package/docs/content/locales/ko-KR/template-design.mdx +58 -1
  482. package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  483. package/docs/content/locales/pt-BR/template-design.mdx +62 -1
  484. package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  485. package/docs/content/locales/zh-CN/template-design.mdx +50 -1
  486. package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  487. package/docs/content/locales/zh-TW/template-design.mdx +52 -1
  488. package/docs/content/skills-guide.mdx +5 -0
  489. package/docs/content/template-design.mdx +59 -1
  490. package/package.json +2 -1
  491. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  492. package/corpus/templates/design/actions/import-figma-file.ts +0 -166
  493. package/corpus/templates/design/app/components/design/DesignToolbar.tsx +0 -339
  494. package/corpus/templates/design/app/components/design/PresentMode.tsx +0 -68
  495. package/corpus/templates/design/app/components/design/ViewportTabs.tsx +0 -149
  496. package/corpus/templates/design/app/components/design/ZoomControls.tsx +0 -157
  497. package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
@@ -0,0 +1,1058 @@
1
+ export interface CanvasPoint {
2
+ x: number;
3
+ y: number;
4
+ }
5
+
6
+ export interface CanvasCamera {
7
+ x: number;
8
+ y: number;
9
+ zoom: number;
10
+ }
11
+
12
+ export interface CanvasSize {
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ export interface FrameGeometry {
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ }
23
+
24
+ export interface FrameEntry {
25
+ id: string;
26
+ geometry: FrameGeometry;
27
+ }
28
+
29
+ export type ResizeHandle = "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w";
30
+
31
+ export type FrameBoundsInput = FrameEntry | FrameGeometry;
32
+
33
+ export interface FrameBounds {
34
+ left: number;
35
+ top: number;
36
+ right: number;
37
+ bottom: number;
38
+ width: number;
39
+ height: number;
40
+ centerX: number;
41
+ centerY: number;
42
+ }
43
+
44
+ export interface AssignedCanvasRegion extends FrameGeometry {
45
+ index: number;
46
+ row: number;
47
+ column: number;
48
+ }
49
+
50
+ export interface AssignRegionsOptions {
51
+ origin?: CanvasPoint;
52
+ regionSize?: CanvasSize;
53
+ gap?: number;
54
+ columns?: number;
55
+ maxColumns?: number;
56
+ }
57
+
58
+ export interface CanvasSnapOptions {
59
+ thresholdScreenPx?: number;
60
+ zoom: number;
61
+ bypass?: boolean;
62
+ }
63
+
64
+ export interface ResizeFrameOptions {
65
+ preserveAspectRatio?: boolean;
66
+ resizeFromCenter?: boolean;
67
+ minWidth?: number;
68
+ minHeight?: number;
69
+ }
70
+
71
+ export interface ResizeGroupResult {
72
+ bounds: FrameGeometry;
73
+ frames: FrameEntry[];
74
+ }
75
+
76
+ export interface DraftGeometryOptions {
77
+ minWidth?: number;
78
+ minHeight?: number;
79
+ defaultWidth?: number;
80
+ defaultHeight?: number;
81
+ }
82
+
83
+ export interface AlignmentGuide {
84
+ orientation: "vertical" | "horizontal";
85
+ position: number;
86
+ start: number;
87
+ end: number;
88
+ }
89
+
90
+ export interface RotateFrameMetadata {
91
+ id: string;
92
+ geometry: FrameGeometry;
93
+ center: CanvasPoint;
94
+ startAngle: number;
95
+ initialRotation: number;
96
+ }
97
+
98
+ export interface RotateFrameResult {
99
+ id: string;
100
+ angle: number;
101
+ rawAngle: number;
102
+ delta: number;
103
+ snapped: boolean;
104
+ }
105
+
106
+ export interface RotationSnapOptions {
107
+ shiftKey?: boolean;
108
+ incrementDegrees?: number;
109
+ }
110
+
111
+ export interface FitViewportOptions {
112
+ paddingScreenPx?: number;
113
+ canvasPadding?: number;
114
+ minZoom?: number;
115
+ maxZoom?: number;
116
+ fallbackZoom?: number;
117
+ }
118
+
119
+ export interface RulerTick {
120
+ value: number;
121
+ position: number;
122
+ label: string;
123
+ }
124
+
125
+ export interface RulerTicks {
126
+ x: RulerTick[];
127
+ y: RulerTick[];
128
+ }
129
+
130
+ export interface RulerTickOptions {
131
+ minTickSpacingPx?: number;
132
+ canvasPadding?: number;
133
+ maxTicks?: number;
134
+ }
135
+
136
+ export type ArrowNudgeKey =
137
+ | "ArrowUp"
138
+ | "ArrowRight"
139
+ | "ArrowDown"
140
+ | "ArrowLeft";
141
+
142
+ export interface NudgeModifiers {
143
+ shiftKey?: boolean;
144
+ altKey?: boolean;
145
+ metaKey?: boolean;
146
+ ctrlKey?: boolean;
147
+ }
148
+
149
+ export interface NudgeOptions {
150
+ baseStep?: number;
151
+ shiftMultiplier?: number;
152
+ }
153
+
154
+ export interface NudgeDelta {
155
+ dx: number;
156
+ dy: number;
157
+ step: number;
158
+ snap: {
159
+ bypass: boolean;
160
+ reason: "modifier" | null;
161
+ };
162
+ }
163
+
164
+ interface SnapCandidate {
165
+ distance: number;
166
+ offset: number;
167
+ guide: AlignmentGuide;
168
+ }
169
+
170
+ export const DEFAULT_SNAP_THRESHOLD_SCREEN_PX = 6;
171
+ export const DEFAULT_ROTATION_SNAP_DEGREES = 15;
172
+ export const DEFAULT_PIXEL_GRID_MIN_ZOOM = 800;
173
+ export const MIN_CANVAS_FRAME_WIDTH = 120;
174
+ export const MIN_CANVAS_FRAME_HEIGHT = 120;
175
+ export const DEFAULT_ASSIGNED_REGION_WIDTH = 1440;
176
+ export const DEFAULT_ASSIGNED_REGION_HEIGHT = 1024;
177
+ export const DEFAULT_ASSIGNED_REGION_GAP = 320;
178
+ export const DEFAULT_ASSIGNED_REGION_MAX_COLUMNS = 3;
179
+
180
+ export function screenToCanvasPoint(
181
+ point: CanvasPoint,
182
+ camera: CanvasCamera,
183
+ surfaceOrigin: CanvasPoint = { x: 0, y: 0 },
184
+ padding = 0,
185
+ round = false,
186
+ ): CanvasPoint {
187
+ const scale = camera.zoom / 100;
188
+ if (scale === 0) return { x: 0, y: 0 };
189
+ const next = {
190
+ x: (point.x - surfaceOrigin.x - camera.x) / scale - padding,
191
+ y: (point.y - surfaceOrigin.y - camera.y) / scale - padding,
192
+ };
193
+ return round ? { x: Math.round(next.x), y: Math.round(next.y) } : next;
194
+ }
195
+
196
+ export function canvasToScreenPoint(
197
+ point: CanvasPoint,
198
+ camera: CanvasCamera,
199
+ surfaceOrigin: CanvasPoint = { x: 0, y: 0 },
200
+ padding = 0,
201
+ ): CanvasPoint {
202
+ const scale = camera.zoom / 100;
203
+ return {
204
+ x: surfaceOrigin.x + camera.x + (point.x + padding) * scale,
205
+ y: surfaceOrigin.y + camera.y + (point.y + padding) * scale,
206
+ };
207
+ }
208
+
209
+ export function getPanForZoomToCursor({
210
+ pan,
211
+ cursor,
212
+ oldZoom,
213
+ nextZoom,
214
+ }: {
215
+ pan: CanvasPoint;
216
+ cursor: CanvasPoint;
217
+ oldZoom: number;
218
+ nextZoom: number;
219
+ }): CanvasPoint {
220
+ const ratio = nextZoom / oldZoom;
221
+ return {
222
+ x: cursor.x - (cursor.x - pan.x) * ratio,
223
+ y: cursor.y - (cursor.y - pan.y) * ratio,
224
+ };
225
+ }
226
+
227
+ export function getAngleFromCenter(
228
+ center: CanvasPoint,
229
+ point: CanvasPoint,
230
+ ): number {
231
+ return radiansToDegrees(Math.atan2(point.y - center.y, point.x - center.x));
232
+ }
233
+
234
+ export function getAngleDeltaDegrees(from: number, to: number): number {
235
+ return ((((to - from) % 360) + 540) % 360) - 180;
236
+ }
237
+
238
+ export function snapAngleToIncrement(
239
+ angle: number,
240
+ {
241
+ shiftKey = false,
242
+ incrementDegrees = DEFAULT_ROTATION_SNAP_DEGREES,
243
+ }: RotationSnapOptions = {},
244
+ ): number {
245
+ if (!shiftKey || incrementDegrees <= 0) return angle;
246
+ return Math.round(angle / incrementDegrees) * incrementDegrees;
247
+ }
248
+
249
+ export function getRotateFrameMetadata(
250
+ entry: FrameEntry,
251
+ pointer: CanvasPoint,
252
+ {
253
+ center,
254
+ initialRotation = 0,
255
+ }: { center?: CanvasPoint; initialRotation?: number } = {},
256
+ ): RotateFrameMetadata {
257
+ const bounds = getFrameBounds(entry.geometry);
258
+ const rotationCenter = center ?? { x: bounds.centerX, y: bounds.centerY };
259
+ return {
260
+ id: entry.id,
261
+ geometry: entry.geometry,
262
+ center: rotationCenter,
263
+ startAngle: getAngleFromCenter(rotationCenter, pointer),
264
+ initialRotation,
265
+ };
266
+ }
267
+
268
+ export function getRotatedFrameAngle(
269
+ metadata: RotateFrameMetadata,
270
+ pointer: CanvasPoint,
271
+ options: RotationSnapOptions = {},
272
+ ): RotateFrameResult {
273
+ const currentAngle = getAngleFromCenter(metadata.center, pointer);
274
+ const delta = getAngleDeltaDegrees(metadata.startAngle, currentAngle);
275
+ const rawAngle = metadata.initialRotation + delta;
276
+ const angle = snapAngleToIncrement(rawAngle, options);
277
+ const incrementDegrees =
278
+ options.incrementDegrees ?? DEFAULT_ROTATION_SNAP_DEGREES;
279
+ return {
280
+ id: metadata.id,
281
+ angle,
282
+ rawAngle,
283
+ delta,
284
+ snapped: !!options.shiftKey && incrementDegrees > 0,
285
+ };
286
+ }
287
+
288
+ export function getFrameBounds(geometry: FrameGeometry): FrameBounds {
289
+ const width = geometry.width;
290
+ const height = geometry.height;
291
+ return {
292
+ left: geometry.x,
293
+ top: geometry.y,
294
+ right: geometry.x + width,
295
+ bottom: geometry.y + height,
296
+ width,
297
+ height,
298
+ centerX: geometry.x + width / 2,
299
+ centerY: geometry.y + height / 2,
300
+ };
301
+ }
302
+
303
+ export function getFrameGroupBounds(
304
+ frames: readonly FrameBoundsInput[],
305
+ ): FrameBounds | null {
306
+ if (frames.length === 0) return null;
307
+
308
+ const bounds = frames.map((frame) => getFrameBounds(getFrameGeometry(frame)));
309
+ const left = Math.min(...bounds.map((bound) => bound.left));
310
+ const top = Math.min(...bounds.map((bound) => bound.top));
311
+ const right = Math.max(...bounds.map((bound) => bound.right));
312
+ const bottom = Math.max(...bounds.map((bound) => bound.bottom));
313
+ return getFrameBounds({
314
+ x: left,
315
+ y: top,
316
+ width: right - left,
317
+ height: bottom - top,
318
+ });
319
+ }
320
+
321
+ export function assignRegions(
322
+ count: number,
323
+ options: AssignRegionsOptions = {},
324
+ ): AssignedCanvasRegion[] {
325
+ if (!Number.isFinite(count) || count <= 0) return [];
326
+
327
+ const total = Math.floor(count);
328
+ const origin = options.origin ?? { x: 0, y: 0 };
329
+ const width = getPositiveFiniteNumber(
330
+ options.regionSize?.width,
331
+ DEFAULT_ASSIGNED_REGION_WIDTH,
332
+ );
333
+ const height = getPositiveFiniteNumber(
334
+ options.regionSize?.height,
335
+ DEFAULT_ASSIGNED_REGION_HEIGHT,
336
+ );
337
+ const gap = Math.max(
338
+ 0,
339
+ getFiniteNumber(options.gap, DEFAULT_ASSIGNED_REGION_GAP),
340
+ );
341
+ const maxColumns = getWholeNumberAtLeast(
342
+ options.maxColumns,
343
+ DEFAULT_ASSIGNED_REGION_MAX_COLUMNS,
344
+ 1,
345
+ );
346
+ const requestedColumns =
347
+ options.columns == null
348
+ ? maxColumns
349
+ : getWholeNumberAtLeast(options.columns, maxColumns, 1);
350
+ const columns = Math.min(total, requestedColumns);
351
+
352
+ return Array.from({ length: total }, (_, index) => {
353
+ const row = Math.floor(index / columns);
354
+ const column = index % columns;
355
+ return {
356
+ index,
357
+ row,
358
+ column,
359
+ x: origin.x + column * (width + gap),
360
+ y: origin.y + row * (height + gap),
361
+ width,
362
+ height,
363
+ };
364
+ });
365
+ }
366
+
367
+ export function getCameraForBounds(
368
+ bounds: FrameBounds | FrameGeometry | null,
369
+ viewport: CanvasSize,
370
+ {
371
+ paddingScreenPx = 48,
372
+ canvasPadding = 0,
373
+ minZoom = 10,
374
+ maxZoom = 400,
375
+ fallbackZoom = 100,
376
+ }: FitViewportOptions = {},
377
+ ): CanvasCamera {
378
+ if (!bounds || viewport.width <= 0 || viewport.height <= 0) {
379
+ return { x: 0, y: 0, zoom: fallbackZoom };
380
+ }
381
+
382
+ const geometry = getBoundsGeometry(bounds);
383
+ const availableWidth = Math.max(1, viewport.width - paddingScreenPx * 2);
384
+ const availableHeight = Math.max(1, viewport.height - paddingScreenPx * 2);
385
+ const scale = Math.min(
386
+ availableWidth / Math.max(1, geometry.width),
387
+ availableHeight / Math.max(1, geometry.height),
388
+ );
389
+ const zoom = clamp(scale * 100, minZoom, maxZoom);
390
+ const nextScale = zoom / 100;
391
+
392
+ return {
393
+ x:
394
+ (viewport.width - geometry.width * nextScale) / 2 -
395
+ (geometry.x + canvasPadding) * nextScale,
396
+ y:
397
+ (viewport.height - geometry.height * nextScale) / 2 -
398
+ (geometry.y + canvasPadding) * nextScale,
399
+ zoom,
400
+ };
401
+ }
402
+
403
+ export function getRulerTicks(
404
+ camera: CanvasCamera,
405
+ viewport: CanvasSize,
406
+ options: RulerTickOptions = {},
407
+ ): RulerTicks {
408
+ return {
409
+ x: getAxisRulerTicks("x", camera, viewport.width, options),
410
+ y: getAxisRulerTicks("y", camera, viewport.height, options),
411
+ };
412
+ }
413
+
414
+ export function shouldShowPixelGrid(
415
+ zoom: number,
416
+ minZoom = DEFAULT_PIXEL_GRID_MIN_ZOOM,
417
+ ): boolean {
418
+ return zoom >= minZoom;
419
+ }
420
+
421
+ export function getNudgeDelta(
422
+ key: ArrowNudgeKey,
423
+ modifiers: NudgeModifiers = {},
424
+ { baseStep = 1, shiftMultiplier = 10 }: NudgeOptions = {},
425
+ ): NudgeDelta {
426
+ const step = baseStep * (modifiers.shiftKey ? shiftMultiplier : 1);
427
+ const vector = getNudgeVector(key);
428
+ const bypass = !!(modifiers.altKey || modifiers.metaKey || modifiers.ctrlKey);
429
+
430
+ return {
431
+ dx: vector.x * step,
432
+ dy: vector.y * step,
433
+ step,
434
+ snap: {
435
+ bypass,
436
+ reason: bypass ? "modifier" : null,
437
+ },
438
+ };
439
+ }
440
+
441
+ export function getDraftGeometryFromPoints(
442
+ start: CanvasPoint,
443
+ end: CanvasPoint,
444
+ {
445
+ minWidth = 1,
446
+ minHeight = 1,
447
+ defaultWidth,
448
+ defaultHeight,
449
+ }: DraftGeometryOptions = {},
450
+ ): FrameGeometry {
451
+ const rawWidth = Math.abs(end.x - start.x);
452
+ const rawHeight = Math.abs(end.y - start.y);
453
+ const width = Math.max(rawWidth || defaultWidth || 0, minWidth);
454
+ const height = Math.max(rawHeight || defaultHeight || 0, minHeight);
455
+ const drawingLeft = end.x < start.x;
456
+ const drawingUp = end.y < start.y;
457
+
458
+ return {
459
+ x: drawingLeft ? start.x - width : start.x,
460
+ y: drawingUp ? start.y - height : start.y,
461
+ width,
462
+ height,
463
+ };
464
+ }
465
+
466
+ export function appendPolylinePoint(
467
+ points: readonly CanvasPoint[],
468
+ nextPoint: CanvasPoint,
469
+ minDistance = 4,
470
+ ): CanvasPoint[] {
471
+ const previous = points[points.length - 1];
472
+ if (!previous) return [nextPoint];
473
+ if (
474
+ Math.hypot(nextPoint.x - previous.x, nextPoint.y - previous.y) < minDistance
475
+ ) {
476
+ return [...points];
477
+ }
478
+ return [...points, nextPoint];
479
+ }
480
+
481
+ export function computeMoveSnap(
482
+ moving: FrameEntry[],
483
+ stationary: FrameEntry[],
484
+ options: CanvasSnapOptions,
485
+ ) {
486
+ if (options.bypass) {
487
+ return { dx: 0, dy: 0, guides: [] as AlignmentGuide[] };
488
+ }
489
+
490
+ let bestX: SnapCandidate | null = null;
491
+ let bestY: SnapCandidate | null = null;
492
+ const threshold = getCanvasSnapThreshold(options);
493
+ const stationaryBounds = stationary.map((entry) => ({
494
+ ...entry,
495
+ bounds: getFrameBounds(entry.geometry),
496
+ }));
497
+
498
+ for (const entry of moving) {
499
+ const movingBounds = getFrameBounds(entry.geometry);
500
+ for (const stationaryEntry of stationaryBounds) {
501
+ bestX = getBestCandidate(
502
+ bestX,
503
+ getAxisSnapCandidates(
504
+ "x",
505
+ movingBounds,
506
+ stationaryEntry.bounds,
507
+ threshold,
508
+ ),
509
+ );
510
+ bestY = getBestCandidate(
511
+ bestY,
512
+ getAxisSnapCandidates(
513
+ "y",
514
+ movingBounds,
515
+ stationaryEntry.bounds,
516
+ threshold,
517
+ ),
518
+ );
519
+ }
520
+ }
521
+
522
+ return {
523
+ dx: bestX?.offset ?? 0,
524
+ dy: bestY?.offset ?? 0,
525
+ guides: [bestX?.guide, bestY?.guide].filter(Boolean) as AlignmentGuide[],
526
+ };
527
+ }
528
+
529
+ export function resizeFrameFromDelta(
530
+ origin: FrameGeometry,
531
+ handle: ResizeHandle,
532
+ dx: number,
533
+ dy: number,
534
+ options: ResizeFrameOptions = {},
535
+ ) {
536
+ const ratio = origin.width / Math.max(1, origin.height);
537
+ const affectsHorizontal =
538
+ handleAffectsWest(handle) || handleAffectsEast(handle);
539
+ const affectsVertical =
540
+ handleAffectsNorth(handle) || handleAffectsSouth(handle);
541
+ const horizontalDelta = handleAffectsWest(handle) ? -dx : dx;
542
+ const verticalDelta = handleAffectsNorth(handle) ? -dy : dy;
543
+ let width = affectsHorizontal
544
+ ? origin.width + horizontalDelta * (options.resizeFromCenter ? 2 : 1)
545
+ : origin.width;
546
+ let height = affectsVertical
547
+ ? origin.height + verticalDelta * (options.resizeFromCenter ? 2 : 1)
548
+ : origin.height;
549
+
550
+ if (options.preserveAspectRatio) {
551
+ if (affectsHorizontal && affectsVertical) {
552
+ const widthChange = Math.abs(width - origin.width);
553
+ const heightChange = Math.abs(height - origin.height);
554
+ if (widthChange >= heightChange) {
555
+ height = width / ratio;
556
+ } else {
557
+ width = height * ratio;
558
+ }
559
+ } else if (affectsHorizontal) {
560
+ height = width / ratio;
561
+ } else if (affectsVertical) {
562
+ width = height * ratio;
563
+ }
564
+ }
565
+
566
+ width = Math.max(options.minWidth ?? MIN_CANVAS_FRAME_WIDTH, width);
567
+ height = Math.max(options.minHeight ?? MIN_CANVAS_FRAME_HEIGHT, height);
568
+
569
+ return {
570
+ ...origin,
571
+ x: getResizedAxisStart(
572
+ origin.x,
573
+ origin.width,
574
+ width,
575
+ handleAffectsWest(handle),
576
+ handleAffectsEast(handle),
577
+ options.resizeFromCenter ||
578
+ (!affectsHorizontal && width !== origin.width),
579
+ ),
580
+ y: getResizedAxisStart(
581
+ origin.y,
582
+ origin.height,
583
+ height,
584
+ handleAffectsNorth(handle),
585
+ handleAffectsSouth(handle),
586
+ options.resizeFromCenter ||
587
+ (!affectsVertical && height !== origin.height),
588
+ ),
589
+ width,
590
+ height,
591
+ };
592
+ }
593
+
594
+ export function resizeFrameGroupFromDelta(
595
+ frames: FrameEntry[],
596
+ originBounds: FrameBounds | FrameGeometry,
597
+ handle: ResizeHandle,
598
+ dx: number,
599
+ dy: number,
600
+ options: ResizeFrameOptions = {},
601
+ ): ResizeGroupResult {
602
+ const originGeometry = getBoundsGeometry(originBounds);
603
+ const minimums = getGroupMinimumBounds(frames, originGeometry, options);
604
+ const bounds = resizeFrameFromDelta(originGeometry, handle, dx, dy, {
605
+ ...options,
606
+ minWidth: minimums.width,
607
+ minHeight: minimums.height,
608
+ });
609
+
610
+ return {
611
+ bounds,
612
+ frames: resizeFrameGroupToBounds(frames, originGeometry, bounds),
613
+ };
614
+ }
615
+
616
+ export function resizeFrameGroupToBounds(
617
+ frames: FrameEntry[],
618
+ originBounds: FrameBounds | FrameGeometry,
619
+ nextBounds: FrameBounds | FrameGeometry,
620
+ ): FrameEntry[] {
621
+ const originGeometry = getBoundsGeometry(originBounds);
622
+ const nextGeometry = getBoundsGeometry(nextBounds);
623
+ const scaleX = nextGeometry.width / Math.max(1, originGeometry.width);
624
+ const scaleY = nextGeometry.height / Math.max(1, originGeometry.height);
625
+
626
+ return frames.map((frame) => ({
627
+ id: frame.id,
628
+ geometry: {
629
+ x: nextGeometry.x + (frame.geometry.x - originGeometry.x) * scaleX,
630
+ y: nextGeometry.y + (frame.geometry.y - originGeometry.y) * scaleY,
631
+ width: frame.geometry.width * scaleX,
632
+ height: frame.geometry.height * scaleY,
633
+ },
634
+ }));
635
+ }
636
+
637
+ export function computeResizeSnap(
638
+ frame: FrameGeometry,
639
+ stationary: FrameEntry[],
640
+ handle: ResizeHandle,
641
+ options: CanvasSnapOptions,
642
+ ) {
643
+ if (options.bypass) {
644
+ return { frame, guides: [] as AlignmentGuide[] };
645
+ }
646
+
647
+ let nextFrame = frame;
648
+ const guides: AlignmentGuide[] = [];
649
+ const threshold = getCanvasSnapThreshold(options);
650
+
651
+ if (handleAffectsWest(handle) || handleAffectsEast(handle)) {
652
+ const candidate = getResizeSnapCandidate(
653
+ "x",
654
+ nextFrame,
655
+ stationary,
656
+ handle,
657
+ threshold,
658
+ );
659
+ if (candidate) {
660
+ nextFrame = applyResizeSnapOffset(
661
+ nextFrame,
662
+ handle,
663
+ "x",
664
+ candidate.offset,
665
+ );
666
+ guides.push(candidate.guide);
667
+ }
668
+ }
669
+
670
+ if (handleAffectsNorth(handle) || handleAffectsSouth(handle)) {
671
+ const candidate = getResizeSnapCandidate(
672
+ "y",
673
+ nextFrame,
674
+ stationary,
675
+ handle,
676
+ threshold,
677
+ );
678
+ if (candidate) {
679
+ nextFrame = applyResizeSnapOffset(
680
+ nextFrame,
681
+ handle,
682
+ "y",
683
+ candidate.offset,
684
+ );
685
+ guides.push(candidate.guide);
686
+ }
687
+ }
688
+
689
+ return { frame: nextFrame, guides };
690
+ }
691
+
692
+ function getResizedAxisStart(
693
+ originStart: number,
694
+ originSize: number,
695
+ nextSize: number,
696
+ affectsStart: boolean,
697
+ affectsEnd: boolean,
698
+ fromCenter: boolean,
699
+ ) {
700
+ if (fromCenter && (affectsStart || affectsEnd)) {
701
+ return originStart - (nextSize - originSize) / 2;
702
+ }
703
+ if (fromCenter) return originStart + (originSize - nextSize) / 2;
704
+ if (affectsStart) return originStart + originSize - nextSize;
705
+ return originStart;
706
+ }
707
+
708
+ function getGroupMinimumBounds(
709
+ frames: FrameEntry[],
710
+ originBounds: FrameGeometry,
711
+ options: ResizeFrameOptions,
712
+ ): CanvasSize {
713
+ const minimumFrameWidth = options.minWidth ?? MIN_CANVAS_FRAME_WIDTH;
714
+ const minimumFrameHeight = options.minHeight ?? MIN_CANVAS_FRAME_HEIGHT;
715
+ const minimumWidth = frames.reduce(
716
+ (best, frame) =>
717
+ Math.max(
718
+ best,
719
+ originBounds.width *
720
+ (minimumFrameWidth / Math.max(1, frame.geometry.width)),
721
+ ),
722
+ minimumFrameWidth,
723
+ );
724
+ const minimumHeight = frames.reduce(
725
+ (best, frame) =>
726
+ Math.max(
727
+ best,
728
+ originBounds.height *
729
+ (minimumFrameHeight / Math.max(1, frame.geometry.height)),
730
+ ),
731
+ minimumFrameHeight,
732
+ );
733
+ return { width: minimumWidth, height: minimumHeight };
734
+ }
735
+
736
+ function getCanvasSnapThreshold({
737
+ thresholdScreenPx = DEFAULT_SNAP_THRESHOLD_SCREEN_PX,
738
+ zoom,
739
+ }: {
740
+ thresholdScreenPx?: number;
741
+ zoom: number;
742
+ }) {
743
+ const scale = getCameraScale(zoom);
744
+ return thresholdScreenPx / scale;
745
+ }
746
+
747
+ function getAxisSnapCandidates(
748
+ axis: "x" | "y",
749
+ movingBounds: FrameBounds,
750
+ stationaryBounds: FrameBounds,
751
+ threshold: number,
752
+ ): SnapCandidate[] {
753
+ const movingValues =
754
+ axis === "x"
755
+ ? [movingBounds.left, movingBounds.centerX, movingBounds.right]
756
+ : [movingBounds.top, movingBounds.centerY, movingBounds.bottom];
757
+ const stationaryValues =
758
+ axis === "x"
759
+ ? [
760
+ stationaryBounds.left,
761
+ stationaryBounds.centerX,
762
+ stationaryBounds.right,
763
+ ]
764
+ : [
765
+ stationaryBounds.top,
766
+ stationaryBounds.centerY,
767
+ stationaryBounds.bottom,
768
+ ];
769
+
770
+ return movingValues.flatMap((movingValue) =>
771
+ stationaryValues
772
+ .map((stationaryValue) => {
773
+ const offset = stationaryValue - movingValue;
774
+ const distance = Math.abs(offset);
775
+ if (distance > threshold) return null;
776
+ return {
777
+ distance,
778
+ offset,
779
+ guide:
780
+ axis === "x"
781
+ ? getVerticalGuide(
782
+ stationaryValue,
783
+ movingBounds,
784
+ stationaryBounds,
785
+ )
786
+ : getHorizontalGuide(
787
+ stationaryValue,
788
+ movingBounds,
789
+ stationaryBounds,
790
+ ),
791
+ };
792
+ })
793
+ .filter(Boolean),
794
+ ) as SnapCandidate[];
795
+ }
796
+
797
+ function getBestCandidate(
798
+ current: SnapCandidate | null,
799
+ candidates: SnapCandidate[],
800
+ ) {
801
+ return candidates.reduce<SnapCandidate | null>(
802
+ (best, candidate) =>
803
+ !best || candidate.distance < best.distance ? candidate : best,
804
+ current,
805
+ );
806
+ }
807
+
808
+ function getResizeSnapCandidate(
809
+ axis: "x" | "y",
810
+ frame: FrameGeometry,
811
+ stationary: FrameEntry[],
812
+ handle: ResizeHandle,
813
+ threshold: number,
814
+ ) {
815
+ const frameBounds = getFrameBounds(frame);
816
+ const sourceValue =
817
+ axis === "x"
818
+ ? handleAffectsWest(handle)
819
+ ? frameBounds.left
820
+ : frameBounds.right
821
+ : handleAffectsNorth(handle)
822
+ ? frameBounds.top
823
+ : frameBounds.bottom;
824
+
825
+ return stationary.reduce<SnapCandidate | null>((best, entry) => {
826
+ const stationaryBounds = getFrameBounds(entry.geometry);
827
+ const targetValues =
828
+ axis === "x"
829
+ ? [
830
+ stationaryBounds.left,
831
+ stationaryBounds.centerX,
832
+ stationaryBounds.right,
833
+ ]
834
+ : [
835
+ stationaryBounds.top,
836
+ stationaryBounds.centerY,
837
+ stationaryBounds.bottom,
838
+ ];
839
+
840
+ const candidates = targetValues
841
+ .map((targetValue) => {
842
+ const offset = targetValue - sourceValue;
843
+ const distance = Math.abs(offset);
844
+ if (distance > threshold) return null;
845
+ return {
846
+ distance,
847
+ offset,
848
+ guide:
849
+ axis === "x"
850
+ ? getVerticalGuide(targetValue, frameBounds, stationaryBounds)
851
+ : getHorizontalGuide(targetValue, frameBounds, stationaryBounds),
852
+ };
853
+ })
854
+ .filter(Boolean) as SnapCandidate[];
855
+
856
+ return getBestCandidate(best, candidates);
857
+ }, null);
858
+ }
859
+
860
+ function applyResizeSnapOffset(
861
+ frame: FrameGeometry,
862
+ handle: ResizeHandle,
863
+ axis: "x" | "y",
864
+ offset: number,
865
+ ) {
866
+ if (axis === "x") {
867
+ return clampFrameSize(
868
+ handleAffectsWest(handle)
869
+ ? { ...frame, x: frame.x + offset, width: frame.width - offset }
870
+ : { ...frame, width: frame.width + offset },
871
+ handle,
872
+ );
873
+ }
874
+
875
+ return clampFrameSize(
876
+ handleAffectsNorth(handle)
877
+ ? { ...frame, y: frame.y + offset, height: frame.height - offset }
878
+ : { ...frame, height: frame.height + offset },
879
+ handle,
880
+ );
881
+ }
882
+
883
+ function clampFrameSize(frame: FrameGeometry, handle: ResizeHandle) {
884
+ let next = { ...frame };
885
+ if (next.width < MIN_CANVAS_FRAME_WIDTH) {
886
+ if (handleAffectsWest(handle)) {
887
+ next.x = next.x + next.width - MIN_CANVAS_FRAME_WIDTH;
888
+ }
889
+ next.width = MIN_CANVAS_FRAME_WIDTH;
890
+ }
891
+ if (next.height < MIN_CANVAS_FRAME_HEIGHT) {
892
+ if (handleAffectsNorth(handle)) {
893
+ next.y = next.y + next.height - MIN_CANVAS_FRAME_HEIGHT;
894
+ }
895
+ next.height = MIN_CANVAS_FRAME_HEIGHT;
896
+ }
897
+ return next;
898
+ }
899
+
900
+ function getVerticalGuide(
901
+ position: number,
902
+ movingBounds: FrameBounds,
903
+ stationaryBounds: FrameBounds,
904
+ ): AlignmentGuide {
905
+ return {
906
+ orientation: "vertical",
907
+ position,
908
+ start: Math.min(movingBounds.top, stationaryBounds.top),
909
+ end: Math.max(movingBounds.bottom, stationaryBounds.bottom),
910
+ };
911
+ }
912
+
913
+ function getHorizontalGuide(
914
+ position: number,
915
+ movingBounds: FrameBounds,
916
+ stationaryBounds: FrameBounds,
917
+ ): AlignmentGuide {
918
+ return {
919
+ orientation: "horizontal",
920
+ position,
921
+ start: Math.min(movingBounds.left, stationaryBounds.left),
922
+ end: Math.max(movingBounds.right, stationaryBounds.right),
923
+ };
924
+ }
925
+
926
+ function handleAffectsWest(handle: ResizeHandle) {
927
+ return handle.includes("w");
928
+ }
929
+
930
+ function handleAffectsEast(handle: ResizeHandle) {
931
+ return handle.includes("e");
932
+ }
933
+
934
+ function handleAffectsNorth(handle: ResizeHandle) {
935
+ return handle.includes("n");
936
+ }
937
+
938
+ function handleAffectsSouth(handle: ResizeHandle) {
939
+ return handle.includes("s");
940
+ }
941
+
942
+ function getFrameGeometry(frame: FrameBoundsInput): FrameGeometry {
943
+ return "geometry" in frame ? frame.geometry : frame;
944
+ }
945
+
946
+ function getBoundsGeometry(bounds: FrameBounds | FrameGeometry): FrameGeometry {
947
+ if ("left" in bounds) {
948
+ return {
949
+ x: bounds.left,
950
+ y: bounds.top,
951
+ width: bounds.width,
952
+ height: bounds.height,
953
+ };
954
+ }
955
+ return bounds;
956
+ }
957
+
958
+ function getAxisRulerTicks(
959
+ axis: "x" | "y",
960
+ camera: CanvasCamera,
961
+ viewportLength: number,
962
+ {
963
+ minTickSpacingPx = 64,
964
+ canvasPadding = 0,
965
+ maxTicks = 200,
966
+ }: RulerTickOptions,
967
+ ): RulerTick[] {
968
+ if (viewportLength <= 0) return [];
969
+
970
+ const scale = getCameraScale(camera.zoom);
971
+ const pan = axis === "x" ? camera.x : camera.y;
972
+ const minCanvasStep = minTickSpacingPx / scale;
973
+ const step = getNiceCanvasStep(minCanvasStep);
974
+ const start = -pan / scale - canvasPadding;
975
+ const end = (viewportLength - pan) / scale - canvasPadding;
976
+ const first = Math.ceil(start / step) * step;
977
+ const ticks: RulerTick[] = [];
978
+
979
+ for (
980
+ let value = first;
981
+ value <= end + 1e-9 && ticks.length < maxTicks;
982
+ value += step
983
+ ) {
984
+ ticks.push({
985
+ value: normalizeTickValue(value),
986
+ position: pan + (value + canvasPadding) * scale,
987
+ label: formatTickLabel(value, step),
988
+ });
989
+ }
990
+
991
+ return ticks;
992
+ }
993
+
994
+ function getNiceCanvasStep(minStep: number): number {
995
+ if (!Number.isFinite(minStep) || minStep <= 0) return 1;
996
+
997
+ const magnitude = Math.pow(10, Math.floor(Math.log10(minStep)));
998
+ for (const multiplier of [1, 2, 5, 10]) {
999
+ const step = multiplier * magnitude;
1000
+ if (step >= minStep) return step;
1001
+ }
1002
+ return 10 * magnitude;
1003
+ }
1004
+
1005
+ function formatTickLabel(value: number, step: number): string {
1006
+ const decimals = step >= 1 ? 0 : Math.ceil(Math.abs(Math.log10(step)));
1007
+ if (decimals === 0) return String(Math.round(normalizeTickValue(value)));
1008
+ const label = normalizeTickValue(value)
1009
+ .toFixed(decimals)
1010
+ .replace(/\.?0+$/, "");
1011
+ return label === "" ? "0" : label;
1012
+ }
1013
+
1014
+ function normalizeTickValue(value: number): number {
1015
+ return Object.is(value, -0) || Math.abs(value) < 1e-9 ? 0 : value;
1016
+ }
1017
+
1018
+ function getNudgeVector(key: ArrowNudgeKey): CanvasPoint {
1019
+ if (key === "ArrowUp") return { x: 0, y: -1 };
1020
+ if (key === "ArrowRight") return { x: 1, y: 0 };
1021
+ if (key === "ArrowDown") return { x: 0, y: 1 };
1022
+ return { x: -1, y: 0 };
1023
+ }
1024
+
1025
+ function getCameraScale(zoom: number): number {
1026
+ return Math.max(0.01, zoom / 100);
1027
+ }
1028
+
1029
+ function getFiniteNumber(value: number | undefined, fallback: number): number {
1030
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1031
+ }
1032
+
1033
+ function getPositiveFiniteNumber(
1034
+ value: number | undefined,
1035
+ fallback: number,
1036
+ ): number {
1037
+ const next = getFiniteNumber(value, fallback);
1038
+ return next > 0 ? next : fallback;
1039
+ }
1040
+
1041
+ function getWholeNumberAtLeast(
1042
+ value: number | undefined,
1043
+ fallback: number,
1044
+ minimum: number,
1045
+ ): number {
1046
+ return Math.max(
1047
+ minimum,
1048
+ Math.floor(getPositiveFiniteNumber(value, fallback)),
1049
+ );
1050
+ }
1051
+
1052
+ function radiansToDegrees(radians: number): number {
1053
+ return (radians * 180) / Math.PI;
1054
+ }
1055
+
1056
+ function clamp(value: number, min: number, max: number): number {
1057
+ return Math.min(max, Math.max(min, value));
1058
+ }