@agent-native/core 0.92.7 → 0.92.8

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 (289) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/a2a-protocol.mdx +12 -0
  4. package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
  5. package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
  6. package/corpus/core/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
  7. package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
  8. package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
  9. package/corpus/core/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
  10. package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
  11. package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
  12. package/corpus/core/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
  13. package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
  14. package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
  15. package/corpus/core/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
  16. package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
  17. package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
  18. package/corpus/core/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
  19. package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
  20. package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
  21. package/corpus/core/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
  22. package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
  23. package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
  24. package/corpus/core/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
  25. package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
  26. package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
  27. package/corpus/core/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
  28. package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
  29. package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
  30. package/corpus/core/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
  31. package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
  32. package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
  33. package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
  34. package/corpus/core/docs/content/pr-visual-recap.mdx +3 -3
  35. package/corpus/core/docs/content/pure-agent-apps.mdx +22 -0
  36. package/corpus/core/package.json +1 -1
  37. package/corpus/core/src/agent/engine/builtin.ts +1 -1
  38. package/corpus/core/src/agent/model-config.ts +30 -30
  39. package/corpus/core/src/agent/production-agent.ts +129 -7
  40. package/corpus/core/src/cli/design-connect.ts +438 -83
  41. package/corpus/core/src/cli/recap.ts +2 -2
  42. package/corpus/core/src/client/AgentPanel.tsx +8 -1
  43. package/corpus/core/src/client/agent-chat-adapter.ts +12 -7
  44. package/corpus/core/src/client/composer/TiptapComposer.tsx +1 -1
  45. package/corpus/core/src/client/index.ts +4 -0
  46. package/corpus/core/src/client/org/TeamPage.tsx +41 -26
  47. package/corpus/core/src/client/use-action.ts +113 -1
  48. package/corpus/core/src/db/client.ts +12 -2
  49. package/corpus/core/src/db/create-get-db.ts +4 -1
  50. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +1 -1
  51. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +1 -1
  52. package/corpus/core/src/server/transcribe-voice.ts +1 -1
  53. package/corpus/core/src/usage/store.ts +13 -4
  54. package/corpus/core/src/vite/client.ts +38 -1
  55. package/corpus/templates/analytics/AGENTS.md +3 -0
  56. package/corpus/templates/analytics/actions/get-data-program.ts +78 -0
  57. package/corpus/templates/analytics/actions/list-data-programs.ts +49 -0
  58. package/corpus/templates/analytics/app/global.css +11 -0
  59. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +163 -17
  60. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  61. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  62. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +96 -3
  63. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -1
  64. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +61 -4
  65. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +40 -2
  66. package/corpus/templates/analytics/changelog/2026-07-09-analytics-no-longer-forgets-successful-data-queries-when-a-l.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-keep-consistent-spacing-when-they-stack-ver.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-tables-and-extensions-can-be-selected-and-d.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-07-09-long-running-analytics-requests-now-recover-automatically-wh.md +6 -0
  70. package/corpus/templates/analytics/server/db/index.ts +6 -0
  71. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +1 -7
  72. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +102 -15
  73. package/corpus/templates/content/app/hooks/use-content-database.ts +52 -0
  74. package/corpus/templates/content/app/hooks/use-document-properties.ts +50 -6
  75. package/corpus/templates/content/app/i18n/zh-TW.ts +2 -0
  76. package/corpus/templates/content/app/i18n-data.ts +28 -0
  77. package/corpus/templates/content/changelog/2026-07-09-notion-conflict-warnings-no-longer-flash-during-normal-synce.md +6 -0
  78. package/corpus/templates/content/changelog/2026-07-09-property-menus-and-delete-confirmations-now-open-visibly-and.md +6 -0
  79. package/corpus/templates/content/server/lib/notion-sync.ts +130 -28
  80. package/corpus/templates/design/actions/add-breakpoint.ts +48 -68
  81. package/corpus/templates/design/actions/add-localhost-screens.ts +496 -61
  82. package/corpus/templates/design/actions/apply-component-prop-edit.ts +2 -18
  83. package/corpus/templates/design/actions/apply-design-token-edit.ts +51 -82
  84. package/corpus/templates/design/actions/apply-tweaks.ts +63 -41
  85. package/corpus/templates/design/actions/capture-design-state.ts +2 -2
  86. package/corpus/templates/design/actions/connect-localhost.ts +58 -12
  87. package/corpus/templates/design/actions/create-component.ts +2 -18
  88. package/corpus/templates/design/actions/create-design-branch.ts +38 -18
  89. package/corpus/templates/design/actions/create-fusion-app.ts +15 -11
  90. package/corpus/templates/design/actions/delete-file.ts +19 -34
  91. package/corpus/templates/design/actions/deploy-design-preview.ts +59 -27
  92. package/corpus/templates/design/actions/deploy-fusion-app.ts +22 -15
  93. package/corpus/templates/design/actions/export-pdf.ts +2 -1
  94. package/corpus/templates/design/actions/export-zip.ts +9 -3
  95. package/corpus/templates/design/actions/generate-design.ts +107 -70
  96. package/corpus/templates/design/actions/get-component-details.ts +2 -18
  97. package/corpus/templates/design/actions/get-design-branch-diff.ts +2 -3
  98. package/corpus/templates/design/actions/get-design-surface-index.ts +2 -18
  99. package/corpus/templates/design/actions/get-design.ts +2 -1
  100. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +7 -1
  101. package/corpus/templates/design/actions/import-design-tokens.ts +114 -76
  102. package/corpus/templates/design/actions/index-components.ts +2 -18
  103. package/corpus/templates/design/actions/list-design-source-capabilities.ts +23 -28
  104. package/corpus/templates/design/actions/list-local-files.ts +9 -2
  105. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +82 -98
  106. package/corpus/templates/design/actions/open-component-source.ts +2 -18
  107. package/corpus/templates/design/actions/open-visual-edit.ts +19 -1
  108. package/corpus/templates/design/actions/present-design-variants.ts +70 -70
  109. package/corpus/templates/design/actions/preview-component-prop-edit.ts +4 -14
  110. package/corpus/templates/design/actions/read-local-file.ts +9 -2
  111. package/corpus/templates/design/actions/remove-breakpoint.ts +40 -61
  112. package/corpus/templates/design/actions/rename-screen.ts +425 -0
  113. package/corpus/templates/design/actions/request-localhost-write-consent.ts +12 -2
  114. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +9 -2
  115. package/corpus/templates/design/actions/sync-fusion-app.ts +56 -39
  116. package/corpus/templates/design/actions/update-design.ts +376 -61
  117. package/corpus/templates/design/actions/update-file.ts +455 -188
  118. package/corpus/templates/design/actions/write-local-file.ts +10 -2
  119. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +685 -608
  120. package/corpus/templates/design/app/components/design/LayersPanel.tsx +43 -15
  121. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +576 -2015
  122. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +62 -14
  123. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +263 -25
  124. package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +34 -0
  125. package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +40 -0
  126. package/corpus/templates/design/app/components/design/design-canvas/creation.ts +47 -0
  127. package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +27 -0
  128. package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +62 -0
  129. package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +137 -0
  130. package/corpus/templates/design/app/components/design/design-canvas/file-drop.ts +10 -0
  131. package/corpus/templates/design/app/components/design/design-canvas/hit-test.ts +23 -0
  132. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +23 -0
  133. package/corpus/templates/design/app/components/design/design-canvas/iframe-pan.ts +177 -0
  134. package/corpus/templates/design/app/components/design/design-canvas/motion-types.ts +7 -0
  135. package/corpus/templates/design/app/components/design/design-canvas/pending-text-edit.ts +62 -0
  136. package/corpus/templates/design/app/components/design/index.ts +2 -2
  137. package/corpus/templates/design/app/components/design/multi-screen/canvas-tools.ts +100 -0
  138. package/corpus/templates/design/app/components/design/multi-screen/coordinate-transforms.ts +81 -0
  139. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +23 -13
  140. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +238 -19
  141. package/corpus/templates/design/app/components/design/multi-screen/draft-primitives.ts +424 -0
  142. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +341 -0
  143. package/corpus/templates/design/app/components/design/multi-screen/iframe-targeting.ts +52 -0
  144. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +62 -0
  145. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +283 -0
  146. package/corpus/templates/design/app/components/design/multi-screen/screen-content-cache.ts +272 -0
  147. package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -1
  148. package/corpus/templates/design/app/components/design/multi-screen/wheel-gesture-state.ts +12 -0
  149. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +325 -84
  150. package/corpus/templates/design/app/hooks/use-navigation-state.ts +15 -1
  151. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +119 -47
  152. package/corpus/templates/design/app/i18n/zh-TW.ts +9 -0
  153. package/corpus/templates/design/app/i18n-data.ts +223 -0
  154. package/corpus/templates/design/app/lib/design-save-outbox.ts +379 -0
  155. package/corpus/templates/design/app/lib/png-clipboard.ts +113 -0
  156. package/corpus/templates/design/app/pages/design-editor/canvas-primitives.ts +229 -0
  157. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +918 -0
  158. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +43 -0
  159. package/corpus/templates/design/app/pages/design-editor/cross-screen-text-color.ts +336 -0
  160. package/corpus/templates/design/app/pages/design-editor/data-operations.ts +270 -0
  161. package/corpus/templates/design/app/pages/design-editor/dom-guards.ts +24 -0
  162. package/corpus/templates/design/app/pages/design-editor/dom-utils.ts +11 -0
  163. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +20 -0
  164. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +408 -0
  165. package/corpus/templates/design/app/pages/design-editor/export-capture.ts +115 -0
  166. package/corpus/templates/design/app/pages/design-editor/geometry-persistence.ts +93 -0
  167. package/corpus/templates/design/app/pages/design-editor/history.ts +169 -0
  168. package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +185 -0
  169. package/corpus/templates/design/app/pages/design-editor/motion-state.ts +309 -0
  170. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +438 -0
  171. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +510 -0
  172. package/corpus/templates/design/app/pages/design-editor/portable-style.ts +115 -0
  173. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +337 -0
  174. package/corpus/templates/design/app/pages/design-editor/style-utils.ts +5 -0
  175. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +92 -0
  176. package/corpus/templates/design/app/pages/design-editor/tweak-save.ts +87 -0
  177. package/corpus/templates/design/app/pages/design-editor/types.ts +36 -0
  178. package/corpus/templates/design/app/routes/design.$id.tsx +11 -1
  179. package/corpus/templates/design/changelog/2026-07-09-design-edits-canvas-positions-tools-and-tab-context-now-pers.md +6 -0
  180. package/corpus/templates/design/changelog/2026-07-09-figma-style-shortcuts-drawing-annotations-and-cross-screen-d.md +6 -0
  181. package/corpus/templates/design/changelog/2026-07-09-hand-and-scale-tools-now-keep-their-active-toolbar-identity-.md +6 -0
  182. package/corpus/templates/design/changelog/2026-07-09-large-multi-screen-designs-now-stay-responsive-during-long-c.md +6 -0
  183. package/corpus/templates/design/changelog/2026-07-09-layer-drags-now-land-exactly-where-the-insertion-indicator-s.md +6 -0
  184. package/corpus/templates/design/server/db/index.ts +2 -52
  185. package/corpus/templates/design/server/db/schema.ts +16 -0
  186. package/corpus/templates/design/server/lib/design-data-access.ts +70 -0
  187. package/corpus/templates/design/server/lib/design-data-mutation.ts +249 -0
  188. package/corpus/templates/design/server/lib/fusion-screens.ts +89 -76
  189. package/corpus/templates/design/server/lib/import-design-files.ts +76 -35
  190. package/corpus/templates/design/server/lib/verify-write-grant.ts +7 -2
  191. package/corpus/templates/design/server/plugins/db.ts +19 -0
  192. package/corpus/templates/design/server/source-workspace.ts +12 -13
  193. package/corpus/templates/design/shared/resolve-tweaks.ts +15 -0
  194. package/corpus/templates/design/shared/screen-rename.ts +42 -0
  195. package/corpus/templates/design/shared/source-mode.ts +84 -4
  196. package/dist/agent/engine/builder-engine.d.ts +1 -1
  197. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  198. package/dist/agent/engine/builtin.js +1 -1
  199. package/dist/agent/engine/builtin.js.map +1 -1
  200. package/dist/agent/model-config.d.ts +9 -9
  201. package/dist/agent/model-config.d.ts.map +1 -1
  202. package/dist/agent/model-config.js +30 -30
  203. package/dist/agent/model-config.js.map +1 -1
  204. package/dist/agent/production-agent.d.ts +60 -1
  205. package/dist/agent/production-agent.d.ts.map +1 -1
  206. package/dist/agent/production-agent.js +106 -7
  207. package/dist/agent/production-agent.js.map +1 -1
  208. package/dist/cli/design-connect.d.ts +30 -1
  209. package/dist/cli/design-connect.d.ts.map +1 -1
  210. package/dist/cli/design-connect.js +359 -75
  211. package/dist/cli/design-connect.js.map +1 -1
  212. package/dist/cli/recap.js +2 -2
  213. package/dist/cli/recap.js.map +1 -1
  214. package/dist/client/AgentPanel.d.ts.map +1 -1
  215. package/dist/client/AgentPanel.js +6 -1
  216. package/dist/client/AgentPanel.js.map +1 -1
  217. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  218. package/dist/client/agent-chat-adapter.js +10 -5
  219. package/dist/client/agent-chat-adapter.js.map +1 -1
  220. package/dist/client/composer/TiptapComposer.js +1 -1
  221. package/dist/client/composer/TiptapComposer.js.map +1 -1
  222. package/dist/client/index.d.ts +1 -1
  223. package/dist/client/index.d.ts.map +1 -1
  224. package/dist/client/index.js +1 -1
  225. package/dist/client/index.js.map +1 -1
  226. package/dist/client/org/TeamPage.d.ts.map +1 -1
  227. package/dist/client/org/TeamPage.js +6 -5
  228. package/dist/client/org/TeamPage.js.map +1 -1
  229. package/dist/client/use-action.d.ts +32 -0
  230. package/dist/client/use-action.d.ts.map +1 -1
  231. package/dist/client/use-action.js +68 -1
  232. package/dist/client/use-action.js.map +1 -1
  233. package/dist/collab/struct-routes.d.ts +1 -1
  234. package/dist/db/client.d.ts +3 -0
  235. package/dist/db/client.d.ts.map +1 -1
  236. package/dist/db/client.js +7 -2
  237. package/dist/db/client.js.map +1 -1
  238. package/dist/db/create-get-db.d.ts.map +1 -1
  239. package/dist/db/create-get-db.js +2 -2
  240. package/dist/db/create-get-db.js.map +1 -1
  241. package/dist/notifications/routes.d.ts +3 -3
  242. package/dist/resources/handlers.d.ts +2 -2
  243. package/dist/scripts/agent-engines/manage-agent-engine.js +1 -1
  244. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  245. package/dist/scripts/agent-engines/set-agent-engine.js +1 -1
  246. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  247. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  248. package/dist/server/transcribe-voice.js +1 -1
  249. package/dist/server/transcribe-voice.js.map +1 -1
  250. package/dist/usage/store.d.ts.map +1 -1
  251. package/dist/usage/store.js +13 -4
  252. package/dist/usage/store.js.map +1 -1
  253. package/dist/vite/client.d.ts.map +1 -1
  254. package/dist/vite/client.js +36 -1
  255. package/dist/vite/client.js.map +1 -1
  256. package/docs/content/a2a-protocol.mdx +12 -0
  257. package/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
  258. package/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
  259. package/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
  260. package/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
  261. package/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
  262. package/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
  263. package/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
  264. package/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
  265. package/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
  266. package/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
  267. package/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
  268. package/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
  269. package/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
  270. package/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
  271. package/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
  272. package/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
  273. package/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
  274. package/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
  275. package/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
  276. package/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
  277. package/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
  278. package/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
  279. package/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
  280. package/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
  281. package/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
  282. package/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
  283. package/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
  284. package/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
  285. package/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
  286. package/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
  287. package/docs/content/pr-visual-recap.mdx +3 -3
  288. package/docs/content/pure-agent-apps.mdx +22 -0
  289. package/package.json +2 -2
@@ -0,0 +1,20 @@
1
+ import { generateTabId } from "@agent-native/core/client";
2
+
3
+ /** Stable for the lifetime of this module, including editor component refreshes. */
4
+ export const TAB_ID = generateTabId();
5
+
6
+ /**
7
+ * Persistence revisions live on a DesignEditor component instance. Give that
8
+ * instance its own operation source too: reusing module-stable TAB_ID after an
9
+ * editor remount would pair reset revision counters with the server's old
10
+ * high-watermark and make fresh saves look stale.
11
+ */
12
+ export function createEditorSaveOperationSource(
13
+ tabId = TAB_ID,
14
+ editorInstanceId = generateTabId(),
15
+ ): string {
16
+ return `${tabId}:save:${editorInstanceId}`;
17
+ }
18
+
19
+ /** Yjs origin tracked by the local undo manager. */
20
+ export const LOCAL_EDIT_ORIGIN = `${TAB_ID}:local`;
@@ -0,0 +1,408 @@
1
+ import {
2
+ type DesignLeftPanel,
3
+ type DesignTool,
4
+ SHOW_DESIGN_CODE_LEFT_PANEL,
5
+ } from "./types";
6
+
7
+ function isStandaloneHttpUrl(value: string): boolean {
8
+ const trimmed = value.trim();
9
+ if (!/^https?:\/\//i.test(trimmed)) return false;
10
+ try {
11
+ const parsed = new URL(trimmed);
12
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Resolve the source payload behind a localhost-backed Design screen.
20
+ *
21
+ * `design_files.content` intentionally stores the route URL, not its HTML.
22
+ * Writing that value to disk would replace an HTML file with the URL string,
23
+ * so HTML write-back is allowed only after the authenticated snapshot has
24
+ * populated the editor's live source model. CSS can use persisted content,
25
+ * but still fails closed if that payload is a URL-backed screen marker.
26
+ */
27
+ export function resolveLocalhostSourceWriteContent(args: {
28
+ extension: string;
29
+ persistedContent: string | null | undefined;
30
+ liveSnapshotHtml: string | null | undefined;
31
+ }): string | null {
32
+ const extension = args.extension.trim().toLowerCase();
33
+ const candidate =
34
+ extension === ".html" || extension === ".htm"
35
+ ? args.liveSnapshotHtml
36
+ : extension === ".css"
37
+ ? args.persistedContent
38
+ : null;
39
+ if (!candidate?.trim() || isStandaloneHttpUrl(candidate)) return null;
40
+ return candidate;
41
+ }
42
+
43
+ /**
44
+ * Flash-prevention routing for adopting an ALREADY-server-persisted content
45
+ * snapshot into the editor — the "onApplied host-sync" family: shader fill
46
+ * applies (`apply-shader-fill` → onShaderFillApplied), GLSL shader
47
+ * apply/remove/knob commits (GlslShaderPanel's read-source-file →
48
+ * apply-source-edit → onApplied), and component prop edits.
49
+ *
50
+ * These handlers previously passed `refreshPreview: true` for the active
51
+ * file, but in applyLocalContentUpdate that flag means "skip the in-place
52
+ * replace and force a full srcdoc rebuild" — a real iframe reload of the
53
+ * active screen (white flash, lost scroll/Alpine state, and an onload refire
54
+ * that re-runs screen measurement — observed in the wild as the shader-apply
55
+ * flash plus a zoom-badge drift in the same gesture). The patched content is
56
+ * a normal document update, exactly what the bridge's live in-place
57
+ * full-document replace exists for (see applyLocalContentUpdate's
58
+ * "Holistic flash pipeline" comment), so route it through
59
+ * `forcePreviewFullDocument` instead; applyLocalContentUpdate already falls
60
+ * back to the srcdoc rebuild on its own when the live bridge isn't
61
+ * registered for the surface yet.
62
+ *
63
+ * `persist: false` because the server already owns this content — the
64
+ * updatedAt stamp (when present) records it as the acked base for the next
65
+ * guarded update-file save rather than re-queueing a redundant save.
66
+ */
67
+ export function getPersistedContentHostSyncOptions(args: {
68
+ fileId: string;
69
+ activeFileId: string | null | undefined;
70
+ updatedAt?: string;
71
+ }): {
72
+ forcePreviewFullDocument: boolean;
73
+ persist: false;
74
+ updatedAt?: string;
75
+ } {
76
+ return {
77
+ forcePreviewFullDocument:
78
+ args.activeFileId !== null &&
79
+ args.activeFileId !== undefined &&
80
+ args.fileId === args.activeFileId,
81
+ persist: false,
82
+ updatedAt: args.updatedAt,
83
+ };
84
+ }
85
+
86
+ export function getDesignEditorShareUrl(
87
+ id: string,
88
+ origin: string,
89
+ basePath = "",
90
+ ) {
91
+ const normalizedBasePath = basePath.replace(/\/+$/, "");
92
+ const pathname = normalizedBasePath
93
+ ? `${normalizedBasePath}/design/${encodeURIComponent(id)}`
94
+ : `/design/${encodeURIComponent(id)}`;
95
+ return new URL(pathname, origin).toString();
96
+ }
97
+
98
+ function formatDesignEditorUrlZoom(zoom: number): string {
99
+ const rounded = Math.round(zoom * 100) / 100;
100
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2);
101
+ }
102
+
103
+ export function getDesignEditorStateUrlSearch(args: {
104
+ currentSearch: string;
105
+ viewMode: "single" | "overview";
106
+ screenId?: string | null;
107
+ codeFileId?: string | null;
108
+ codeFilename?: string | null;
109
+ selectionId?: string | null;
110
+ leftPanel?: DesignLeftPanel | null;
111
+ zoom?: number | null;
112
+ tool?: DesignTool | null;
113
+ }) {
114
+ const params = new URLSearchParams(args.currentSearch);
115
+ const leftPanel =
116
+ args.leftPanel === "code" && !SHOW_DESIGN_CODE_LEFT_PANEL
117
+ ? null
118
+ : args.leftPanel;
119
+ params.set("view", args.viewMode);
120
+ if (leftPanel && leftPanel !== "file") {
121
+ params.set("panel", leftPanel);
122
+ } else {
123
+ params.delete("panel");
124
+ }
125
+ if (args.screenId) {
126
+ params.set("screen", args.screenId);
127
+ } else {
128
+ params.delete("screen");
129
+ }
130
+ if (leftPanel === "code" && args.codeFileId) {
131
+ params.set("fileId", args.codeFileId);
132
+ } else {
133
+ params.delete("fileId");
134
+ }
135
+ if (leftPanel === "code" && !args.codeFileId && args.codeFilename) {
136
+ params.set("filename", args.codeFilename);
137
+ } else {
138
+ params.delete("filename");
139
+ }
140
+ if (args.selectionId) {
141
+ params.set("selection", args.selectionId);
142
+ } else {
143
+ params.delete("selection");
144
+ }
145
+ if (typeof args.zoom === "number" && Number.isFinite(args.zoom)) {
146
+ params.set("zoom", formatDesignEditorUrlZoom(args.zoom));
147
+ } else {
148
+ params.delete("zoom");
149
+ }
150
+ // Keep the shareable navigation mirror aligned with the tool the editor is
151
+ // actually using. `move` is the default and stays implicit so ordinary
152
+ // editor URLs remain compact; every other tool round-trips explicitly.
153
+ if (args.tool && args.tool !== "move") {
154
+ params.set("tool", args.tool);
155
+ } else {
156
+ params.delete("tool");
157
+ }
158
+ const query = params.toString();
159
+ return query ? `?${query}` : "";
160
+ }
161
+
162
+ export function getLocalhostRouteSourceFile(args: {
163
+ sourceFile?: string;
164
+ source?: string;
165
+ }): string | undefined {
166
+ if (args.sourceFile?.trim()) return args.sourceFile;
167
+ const raw = args.source;
168
+ if (!raw) return undefined;
169
+ try {
170
+ const parsed = JSON.parse(raw) as unknown;
171
+ if (
172
+ parsed &&
173
+ typeof parsed === "object" &&
174
+ "file" in parsed &&
175
+ typeof (parsed as Record<string, unknown>).file === "string"
176
+ ) {
177
+ return (parsed as Record<string, string>).file;
178
+ }
179
+ } catch {
180
+ if (raw.length > 0) return raw;
181
+ }
182
+ return undefined;
183
+ }
184
+
185
+ export function getLayerMoveSourceContent(args: {
186
+ sourceFileId: string;
187
+ activeFileId?: string | null;
188
+ activeContent: string;
189
+ sourceFileContent?: string;
190
+ sourceContentMap: ReadonlyMap<string, string>;
191
+ }) {
192
+ return (
193
+ args.sourceContentMap.get(args.sourceFileId) ??
194
+ (args.sourceFileId === args.activeFileId
195
+ ? args.activeContent
196
+ : args.sourceFileContent) ??
197
+ ""
198
+ );
199
+ }
200
+
201
+ export function getFreshActiveFileContent(args: {
202
+ activeContent: string;
203
+ latestContent?: string | null;
204
+ lastLocalContent?: string | null;
205
+ }) {
206
+ return args.latestContent ?? args.lastLocalContent ?? args.activeContent;
207
+ }
208
+
209
+ export function getFreshScreenContent(args: {
210
+ screenId: string;
211
+ activeFileId?: string | null;
212
+ freshActiveContentFileId?: string | null;
213
+ freshActiveContent: string;
214
+ fileContentById: ReadonlyMap<string, string>;
215
+ /**
216
+ * Nest-conversion clobber fix: the freshest same-tick local write for this
217
+ * screen (DesignEditor's `pendingLocalFileContentsRef`), or null. The
218
+ * `fileContentById` map is memoized off React state, so a write made by
219
+ * `applyFileContentUpdate` earlier in the SAME task burst (e.g. the bridge's
220
+ * auto-layout conversion `visual-style-change` that lands right before the
221
+ * nest-drop's `visual-structure-change`) is not visible in it yet — the
222
+ * structure handler would rebase off pre-conversion content and its own
223
+ * write would silently clobber the conversion. Preferring the synchronous
224
+ * pending write mirrors what the active file already gets through
225
+ * `getFreshActiveFileContent`'s latest/lastLocal refs.
226
+ */
227
+ pendingContent?: string | null;
228
+ }) {
229
+ const freshActiveContentFileId =
230
+ args.freshActiveContentFileId ?? args.activeFileId;
231
+ if (
232
+ args.screenId === args.activeFileId &&
233
+ args.screenId === freshActiveContentFileId
234
+ ) {
235
+ return args.freshActiveContent;
236
+ }
237
+ return args.pendingContent ?? args.fileContentById.get(args.screenId) ?? "";
238
+ }
239
+
240
+ export function shouldReplacePreviewAfterVisualStyleCommit(args: {
241
+ runtimeApplied?: boolean;
242
+ runtimeStyleApplied: boolean;
243
+ }) {
244
+ return !args.runtimeApplied && !args.runtimeStyleApplied;
245
+ }
246
+
247
+ /**
248
+ * PF12: decide whether a style change from EditPanel (ScrubInput scrub tick /
249
+ * DesignColorPicker drag tick) should skip the expensive source commit
250
+ * (commitVisualStyles / commitStylesToSelectedLayers — projection parse, HTML
251
+ * patch, Yjs write, history entry) and instead only try the cheap live
252
+ * iframe preview.
253
+ *
254
+ * Only a genuine mid-gesture "preview" tick against a single selected element
255
+ * qualifies: `meta` is undefined for every existing call site that never
256
+ * passed gesture metadata (keyboard edits, agent edits, discrete commits),
257
+ * so this returns false for all of them — preserving prior full-commit
258
+ * behavior exactly. Multi-layer-selection edits (`selectedLayerCount > 1`)
259
+ * have no equivalent cheap multi-element preview channel, so those
260
+ * conservatively keep committing on every tick, same as before PF12.
261
+ */
262
+ export function shouldSkipVisualStyleCommitForPreview(args: {
263
+ phase?: "preview" | "commit";
264
+ selectedLayerCount: number;
265
+ }): boolean {
266
+ return args.phase === "preview" && args.selectedLayerCount <= 1;
267
+ }
268
+
269
+ /**
270
+ * Unit-aware relative-delta application for a per-node multi-select style
271
+ * commit (see commitRelativeStyleDeltaToSelectedLayers). ScrubInput's arrow-
272
+ * key step and the future `relativeDelta` meta both operate on a plain
273
+ * unitless number in the *same numeric domain* the field displays (px count,
274
+ * degree count, raw opacity/line-height number) — the CSS unit suffix is
275
+ * purely a serialization detail added back on here, matching how
276
+ * formatScrubValue/EditPanel already build the CSS string for a single-target
277
+ * commit.
278
+ *
279
+ * Parses the leading numeric portion of `currentValue` (e.g. "12px" -> 12,
280
+ * "45deg" -> 45, "0.5" -> 0.5), adds `delta`, and re-serializes with
281
+ * whatever unit suffix (if any) the ORIGINAL value used — so a per-node
282
+ * relative nudge preserves that node's own unit instead of assuming every
283
+ * selected node shares the same one. Returns `null` when `currentValue`
284
+ * doesn't parse as a leading number (e.g. a keyword like "auto" or "none"),
285
+ * so the caller can skip that node rather than writing garbage.
286
+ */
287
+ export function applyRelativeDeltaToStyleValue(
288
+ currentValue: string | undefined,
289
+ delta: number,
290
+ ): string | null {
291
+ if (typeof currentValue !== "string") return null;
292
+ const match = currentValue.trim().match(/^(-?\d*\.?\d+)(.*)$/);
293
+ if (!match) return null;
294
+ const [, numeric, unit] = match;
295
+ const base = Number(numeric);
296
+ if (!Number.isFinite(base)) return null;
297
+ const next = base + delta;
298
+ // Match formatScrubValue's collapse of float noise (e.g. avoid
299
+ // "12.000000000000002px" from repeated float addition) without imposing a
300
+ // fixed precision the property doesn't use.
301
+ const rounded = Math.round(next * 1e6) / 1e6;
302
+ return `${Object.is(rounded, -0) ? 0 : rounded}${unit ?? ""}`;
303
+ }
304
+
305
+ export function getLayerMoveIterationOrder<T>(
306
+ orderedIds: readonly T[],
307
+ placement: "before" | "after" | "inside",
308
+ ): T[] {
309
+ return placement === "after" ? [...orderedIds].reverse() : [...orderedIds];
310
+ }
311
+
312
+ export function removeUndoRedoOrderKind<T extends string>(
313
+ order: readonly T[],
314
+ kind: T,
315
+ ): T[] {
316
+ return order.filter((entry) => entry !== kind);
317
+ }
318
+
319
+ export type UndoRedoOrderKind =
320
+ | "content"
321
+ | "file-content"
322
+ | "geometry"
323
+ | "file-created";
324
+
325
+ export function getUndoRedoPriorityOrder(
326
+ preferred: UndoRedoOrderKind | undefined,
327
+ ): UndoRedoOrderKind[] {
328
+ if (preferred === "geometry") return ["geometry", "content", "file-content"];
329
+ if (preferred === "file-content")
330
+ return ["file-content", "content", "geometry"];
331
+ return ["content", "file-content", "geometry"];
332
+ }
333
+
334
+ export interface FileContentSaveRequest {
335
+ id: string;
336
+ content: string;
337
+ syncCollab: boolean;
338
+ /** Stable browser-tab identity used to order normal and keepalive saves. */
339
+ operationSource: string;
340
+ /** Monotonic per-file sequence allocated when the edit enters the queue. */
341
+ operationRevision: number;
342
+ /**
343
+ * Optimistic-concurrency guard forwarded to update-file's
344
+ * `expectedVersionHash` param — the last content hash this client knows
345
+ * the server/collab doc holds for this file. Populated by callers when
346
+ * known (lastAckedFileContentHashRef); omitted when unknown so update-file
347
+ * keeps its legacy unguarded-write behavior.
348
+ */
349
+ expectedVersionHash?: string;
350
+ }
351
+
352
+ /**
353
+ * A completed save may retire the unload retry only when it still represents
354
+ * the latest content queued for that file. A newer edit can enter the debounce
355
+ * slot while an older request is in flight and must remain eligible for the
356
+ * pagehide keepalive.
357
+ */
358
+ export function shouldClearLatestUnloadSave(
359
+ latest: FileContentSaveRequest | undefined,
360
+ completed: FileContentSaveRequest,
361
+ skippedStaleMirror = false,
362
+ ): boolean {
363
+ return Boolean(
364
+ !skippedStaleMirror &&
365
+ latest &&
366
+ latest.id === completed.id &&
367
+ latest.content === completed.content &&
368
+ latest.syncCollab === completed.syncCollab &&
369
+ latest.operationSource === completed.operationSource &&
370
+ latest.operationRevision === completed.operationRevision,
371
+ );
372
+ }
373
+
374
+ /**
375
+ * Flushes debounce slots through the ordinary serialized mutation path during
376
+ * React cleanup (route/design changes). Real document unload is handled by the
377
+ * pagehide keepalive; normal navigation must not fire-and-forget or discard
378
+ * the final edit.
379
+ */
380
+ export function flushPendingFileContentSavesOnCleanup(
381
+ pendingByFileId: Readonly<Record<string, FileContentSaveRequest>>,
382
+ timerIds: readonly number[],
383
+ save: (pending: FileContentSaveRequest) => void,
384
+ clearTimer: (timerId: number) => void,
385
+ ): void {
386
+ for (const pending of Object.values(pendingByFileId)) save(pending);
387
+ for (const timerId of timerIds) clearTimer(timerId);
388
+ }
389
+
390
+ /**
391
+ * Pure decision helper (exported for unit testing) for the pagehide/unload
392
+ * keepalive: should it be sent at all?
393
+ *
394
+ * The keepalive posts a full-document `content` write with `keepalive: true`.
395
+ * Its operation source/revision prevents older same-tab requests from
396
+ * overwriting it, but there is no round trip to recover from a cross-writer
397
+ * hash rejection. When collab is live (this pending save's `syncCollab` is
398
+ * false) and we don't have a known acked hash, skip it: the Yjs document
399
+ * already holds the current content, while an unguarded full replacement
400
+ * could still conflict with a different writer. Every other combination
401
+ * (collab not live, or a hash IS known) sends the versioned keepalive.
402
+ */
403
+ export function shouldSendKeepalive(
404
+ hashKnown: boolean,
405
+ collabLive: boolean,
406
+ ): boolean {
407
+ return hashKnown || !collabLive;
408
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Editor-chrome overlays that editor-chrome.bridge.ts appends inside the preview
3
+ * iframe (the selection outline + resize handles, hover highlight, marquee,
4
+ * spacing/measurement guides, and badges). They live in the iframe DOM, so image
5
+ * exports must strip them from the clone — otherwise a download captures the
6
+ * editor's selection outline instead of just the design. Keep this in sync with
7
+ * the data-agent-native-* markers set in editor-chrome.bridge.ts.
8
+ */
9
+ export const EDITOR_CHROME_OVERLAY_SELECTOR = [
10
+ "[data-agent-native-edit-overlay]",
11
+ "[data-agent-native-edit-handle]",
12
+ "[data-agent-native-edge-handle]",
13
+ "[data-agent-native-rotate-handle]",
14
+ "[data-agent-native-transform-badge]",
15
+ "[data-agent-native-spacing-badge]",
16
+ "[data-agent-native-spacing-overlay]",
17
+ "[data-agent-native-spacing-line]",
18
+ "[data-agent-native-spacing-region]",
19
+ "[data-agent-native-insertion-guide]",
20
+ "[data-agent-native-measurement-overlay]",
21
+ ].join(",");
22
+
23
+ export interface ExportCropRect {
24
+ x: number;
25
+ y: number;
26
+ width: number;
27
+ height: number;
28
+ }
29
+
30
+ /** Return the smallest document-space rectangle containing every valid item. */
31
+ export function unionExportCropRects(
32
+ rects: readonly ExportCropRect[],
33
+ ): ExportCropRect | null {
34
+ const valid = rects.filter(
35
+ (rect) =>
36
+ Number.isFinite(rect.x) &&
37
+ Number.isFinite(rect.y) &&
38
+ Number.isFinite(rect.width) &&
39
+ Number.isFinite(rect.height) &&
40
+ rect.width > 0 &&
41
+ rect.height > 0,
42
+ );
43
+ if (valid.length === 0) return null;
44
+ const left = Math.min(...valid.map((rect) => rect.x));
45
+ const top = Math.min(...valid.map((rect) => rect.y));
46
+ const right = Math.max(...valid.map((rect) => rect.x + rect.width));
47
+ const bottom = Math.max(...valid.map((rect) => rect.y + rect.height));
48
+ return { x: left, y: top, width: right - left, height: bottom - top };
49
+ }
50
+
51
+ export interface ExportCompositeFrame extends ExportCropRect {
52
+ rotation?: number;
53
+ }
54
+
55
+ /**
56
+ * Resolve the world-space bounds of selected screen frames, including rotation,
57
+ * so a multi-frame clipboard image preserves the same spacing as the canvas.
58
+ */
59
+ export function getExportCompositeBounds(
60
+ frames: readonly ExportCompositeFrame[],
61
+ ): ExportCropRect | null {
62
+ return unionExportCropRects(
63
+ frames.flatMap((frame) => {
64
+ if (frame.width <= 0 || frame.height <= 0) return [];
65
+ const radians = ((frame.rotation ?? 0) * Math.PI) / 180;
66
+ if (radians === 0) return [frame];
67
+ const centerX = frame.x + frame.width / 2;
68
+ const centerY = frame.y + frame.height / 2;
69
+ const cosine = Math.cos(radians);
70
+ const sine = Math.sin(radians);
71
+ const corners = [
72
+ [-frame.width / 2, -frame.height / 2],
73
+ [frame.width / 2, -frame.height / 2],
74
+ [frame.width / 2, frame.height / 2],
75
+ [-frame.width / 2, frame.height / 2],
76
+ ].map(([x, y]) => ({
77
+ x: centerX + x * cosine - y * sine,
78
+ y: centerY + x * sine + y * cosine,
79
+ }));
80
+ const left = Math.min(...corners.map((corner) => corner.x));
81
+ const top = Math.min(...corners.map((corner) => corner.y));
82
+ const right = Math.max(...corners.map((corner) => corner.x));
83
+ const bottom = Math.max(...corners.map((corner) => corner.y));
84
+ return [{ x: left, y: top, width: right - left, height: bottom - top }];
85
+ }),
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Map a document-space rect onto pixel coordinates within a rendered canvas of
91
+ * the given size, clamped to stay inside the canvas. `scale` must match the
92
+ * scale passed to html2canvas. Returns null when the crop would be empty or
93
+ * lands fully outside the canvas, so callers can fall back to the full render.
94
+ */
95
+ export function computeExportCropBox(
96
+ sourceWidth: number,
97
+ sourceHeight: number,
98
+ rect: { x: number; y: number; width: number; height: number },
99
+ scale: number,
100
+ ): { sx: number; sy: number; sw: number; sh: number } | null {
101
+ const sx = Math.max(0, Math.round(rect.x * scale));
102
+ const sy = Math.max(0, Math.round(rect.y * scale));
103
+ const right = Math.min(
104
+ sourceWidth,
105
+ Math.round((rect.x + rect.width) * scale),
106
+ );
107
+ const bottom = Math.min(
108
+ sourceHeight,
109
+ Math.round((rect.y + rect.height) * scale),
110
+ );
111
+ const sw = right - sx;
112
+ const sh = bottom - sy;
113
+ if (sw <= 0 || sh <= 0) return null;
114
+ return { sx, sy, sw, sh };
115
+ }
@@ -0,0 +1,93 @@
1
+ import type {
2
+ CanvasFrameGeometry,
3
+ CanvasFrameGeometryById,
4
+ } from "@shared/canvas-frames";
5
+
6
+ export function frameGeometryEquals(
7
+ a: CanvasFrameGeometry | undefined,
8
+ b: CanvasFrameGeometry | undefined,
9
+ ): boolean {
10
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
11
+ }
12
+
13
+ export function geometrySnapshotsEqual(
14
+ a: CanvasFrameGeometryById,
15
+ b: CanvasFrameGeometryById,
16
+ ): boolean {
17
+ if (a === b) return true;
18
+ const aKeys = Object.keys(a);
19
+ const bKeys = Object.keys(b);
20
+ if (aKeys.length !== bKeys.length) return false;
21
+ return aKeys.every((key) => key in b && frameGeometryEquals(a[key], b[key]));
22
+ }
23
+
24
+ export const MAX_SANE_FRAME_DIMENSION_PX = 100000;
25
+ export const MAX_SANE_FRAME_ASPECT_RATIO = 50;
26
+
27
+ export function isSaneCanvasFrameGeometryForPersist(
28
+ geometry: CanvasFrameGeometry,
29
+ ): boolean {
30
+ const numericFields = [
31
+ geometry.x,
32
+ geometry.y,
33
+ geometry.width,
34
+ geometry.height,
35
+ geometry.rotation,
36
+ geometry.z,
37
+ ];
38
+ if (
39
+ numericFields.some(
40
+ (value) => value !== undefined && !Number.isFinite(value),
41
+ )
42
+ ) {
43
+ return false;
44
+ }
45
+ const { width, height } = geometry;
46
+ if (
47
+ width !== undefined &&
48
+ (width <= 0 || width > MAX_SANE_FRAME_DIMENSION_PX)
49
+ ) {
50
+ return false;
51
+ }
52
+ if (
53
+ height !== undefined &&
54
+ (height <= 0 || height > MAX_SANE_FRAME_DIMENSION_PX)
55
+ ) {
56
+ return false;
57
+ }
58
+ if (width !== undefined && height !== undefined) {
59
+ const aspect = Math.max(width / height, height / width);
60
+ if (aspect > MAX_SANE_FRAME_ASPECT_RATIO) return false;
61
+ }
62
+ return true;
63
+ }
64
+
65
+ export function sanitizeCanvasFrameGeometryForPersist(
66
+ nextById: CanvasFrameGeometryById,
67
+ previousById: CanvasFrameGeometryById,
68
+ exemptFrameIds: readonly string[] = [],
69
+ ): { geometryById: CanvasFrameGeometryById; rejectedFrameIds: string[] } {
70
+ const rejectedFrameIds: string[] = [];
71
+ let sanitized: CanvasFrameGeometryById | null = null;
72
+ for (const [frameId, geometry] of Object.entries(nextById)) {
73
+ if (exemptFrameIds.includes(frameId)) continue;
74
+ if (isSaneCanvasFrameGeometryForPersist(geometry)) continue;
75
+ rejectedFrameIds.push(frameId);
76
+ if (sanitized === null) sanitized = { ...nextById };
77
+ const previous = previousById[frameId];
78
+ console.warn(
79
+ "[design] rejected insane canvas frame geometry on persist — reverted to previous geometry",
80
+ {
81
+ frameId,
82
+ rejected: geometry,
83
+ revertedTo: previous ?? null,
84
+ },
85
+ );
86
+ if (previous && isSaneCanvasFrameGeometryForPersist(previous)) {
87
+ sanitized[frameId] = { ...previous };
88
+ } else {
89
+ delete sanitized[frameId];
90
+ }
91
+ }
92
+ return { geometryById: sanitized ?? nextById, rejectedFrameIds };
93
+ }