@agent-native/core 0.80.4 → 0.80.5

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 (292) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -2
  3. package/corpus/core/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +1 -1
  5. package/corpus/core/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +1 -1
  7. package/corpus/core/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +1 -1
  9. package/corpus/core/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +1 -1
  11. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +1 -1
  12. package/corpus/core/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
  13. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +1 -1
  14. package/corpus/core/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
  15. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +1 -1
  16. package/corpus/core/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
  17. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +1 -1
  18. package/corpus/core/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
  19. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +1 -1
  20. package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
  21. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +1 -1
  22. package/corpus/core/docs/content/real-time-collaboration.mdx +2 -2
  23. package/corpus/core/docs/content/template-design.mdx +1 -1
  24. package/corpus/core/package.json +3 -7
  25. package/corpus/core/src/action.ts +21 -0
  26. package/corpus/core/src/agent/durable-background.ts +32 -11
  27. package/corpus/core/src/agent/production-agent.ts +11 -1
  28. package/corpus/core/src/agent/run-loop-with-resume.ts +3 -1
  29. package/corpus/core/src/brand-kit/fig/index.ts +17 -37
  30. package/corpus/core/src/cli/index.ts +1 -1
  31. package/corpus/core/src/cli/mcp-config-writers.ts +187 -51
  32. package/corpus/core/src/cli/skills.ts +18 -3
  33. package/corpus/core/src/client/AssistantChat.tsx +11 -1
  34. package/corpus/core/src/client/agent-chat.ts +12 -1
  35. package/corpus/core/src/client/composer/PromptComposer.tsx +100 -41
  36. package/corpus/core/src/client/org/OrgSwitcher.tsx +1 -1
  37. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -27
  38. package/corpus/core/src/collab/presence.ts +1 -1
  39. package/corpus/core/src/mcp/connect-route.ts +34 -5
  40. package/corpus/core/src/server/action-discovery.ts +4 -0
  41. package/corpus/core/src/server/agent-chat-plugin.ts +20 -0
  42. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -0
  43. package/corpus/templates/analytics/actions/update-dashboard.ts +8 -0
  44. package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
  45. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +117 -33
  46. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
  47. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -0
  48. package/corpus/templates/analytics/changelog/2026-06-27-dashboards-can-now-nest-under-a-parent-in-the-sidebar-via-a-.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-06-29-analytics-pages-now-sit-closer-under-the-header-with-tighter.md +6 -0
  50. package/corpus/templates/assets/actions/generate-image-batch.ts +3 -0
  51. package/corpus/templates/assets/actions/generate-image.ts +3 -0
  52. package/corpus/templates/assets/changelog/2026-06-29-longer-agent-generation-window.md +6 -0
  53. package/corpus/templates/assets/netlify.toml +3 -3
  54. package/corpus/templates/assets/server/plugins/agent-chat.ts +4 -1
  55. package/corpus/templates/clips/app/components/capture-install-options.tsx +7 -8
  56. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +1 -1
  57. package/corpus/templates/content/AGENTS.md +12 -0
  58. package/corpus/templates/content/app/blocks/BuilderDocsBlocks.tsx +10 -0
  59. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -0
  60. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +185 -0
  61. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -0
  62. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +19 -0
  63. package/corpus/templates/content/app/components/editor/extensions/ContentReferenceNode.tsx +82 -0
  64. package/corpus/templates/content/app/global.css +9 -0
  65. package/corpus/templates/content/app/i18n/zh-TW.ts +12 -0
  66. package/corpus/templates/content/app/i18n-data.ts +140 -0
  67. package/corpus/templates/content/changelog/2026-06-29-reusable-mdx-references-now-preview-linked-local-documents-i.md +6 -0
  68. package/corpus/templates/content/shared/builder-docs-blocks.ts +8 -0
  69. package/corpus/templates/content/shared/builder-mdx.ts +141 -4
  70. package/corpus/templates/content/shared/nfm.ts +62 -0
  71. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +10 -11
  72. package/corpus/templates/design/AGENTS.md +5 -0
  73. package/corpus/templates/design/_gitignore +7 -0
  74. package/corpus/templates/design/actions/apply-visual-edit.ts +22 -12
  75. package/corpus/templates/design/actions/create-design-system.ts +15 -4
  76. package/corpus/templates/design/actions/create-file.ts +20 -1
  77. package/corpus/templates/design/actions/delete-design-system.ts +2 -2
  78. package/corpus/templates/design/actions/duplicate-design.ts +35 -9
  79. package/corpus/templates/design/actions/generate-design.ts +146 -69
  80. package/corpus/templates/design/actions/generate-screens.ts +51 -19
  81. package/corpus/templates/design/actions/get-design-snapshot.ts +5 -1
  82. package/corpus/templates/design/actions/get-design.ts +2 -0
  83. package/corpus/templates/design/actions/import-design-project.ts +28 -9
  84. package/corpus/templates/design/actions/import-document.ts +22 -8
  85. package/corpus/templates/design/actions/index-design-system-with-builder.ts +6 -153
  86. package/corpus/templates/design/actions/navigate.ts +105 -54
  87. package/corpus/templates/design/actions/set-default-design-system.ts +17 -3
  88. package/corpus/templates/design/actions/show-design-questions.ts +2 -0
  89. package/corpus/templates/design/actions/update-design.ts +2 -1
  90. package/corpus/templates/design/actions/view-screen.ts +12 -6
  91. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +4 -4
  92. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +159 -41
  93. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +8 -4
  94. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +37 -42
  95. package/corpus/templates/design/app/components/design/EditPanel.tsx +336 -183
  96. package/corpus/templates/design/app/components/design/LayersPanel.tsx +153 -34
  97. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +135 -33
  98. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +6 -0
  99. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
  100. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +4 -10
  101. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +84 -43
  102. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +4 -4
  103. package/corpus/templates/design/app/components/design/inspector/{FigmaColorPicker.tsx → DesignColorPicker.tsx} +89 -62
  104. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +14 -26
  105. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +40 -12
  106. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -4
  107. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +13 -13
  108. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +13 -9
  109. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +13 -4
  110. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +6 -6
  111. package/corpus/templates/design/app/components/design/inspector/{figma-icons.tsx → design-icons.tsx} +2 -2
  112. package/corpus/templates/design/app/components/design/inspector/index.ts +12 -12
  113. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +5 -2
  114. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +3 -3
  115. package/corpus/templates/design/app/components/layout/Layout.tsx +5 -2
  116. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  117. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +167 -91
  118. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +16 -3
  119. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +15 -0
  120. package/corpus/templates/design/app/global.css +4 -0
  121. package/corpus/templates/design/app/hooks/use-design-system.ts +4 -1
  122. package/corpus/templates/design/app/hooks/use-navigation-state.ts +38 -3
  123. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +9 -2
  124. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  125. package/corpus/templates/design/app/i18n-data.ts +527 -0
  126. package/corpus/templates/design/app/lib/design-systems.ts +6 -4
  127. package/corpus/templates/design/app/lib/design-templates.ts +15 -3
  128. package/corpus/templates/design/app/pages/DesignEditor.tsx +641 -158
  129. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +42 -34
  130. package/corpus/templates/design/app/pages/VisualEdit.tsx +100 -0
  131. package/corpus/templates/design/app/root.tsx +42 -9
  132. package/corpus/templates/design/app/routes/visual-edit.tsx +7 -0
  133. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +1 -1
  134. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +1 -1
  135. package/corpus/templates/design/changelog/2026-06-28-visual-code-design-mode.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-29-design-generation-prompts-now-show-connect-ai-before-sending.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +1 -1
  138. package/corpus/templates/design/changelog/2026-06-29-fixed-design-editor-exports-and-gradient-edits-so-multi-scre.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-29-png-export-handles-modern-css-color-functions-more-reliably.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-29-png-export-now-ignores-duplicate-clicks-while-a-download-is-.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-29-public-visual-edit-and-shared-design-links-now-invite-visito.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-29-visual-edit-actions-now-reject-missing-design-targets-before.md +6 -0
  144. package/corpus/templates/design/e2e/README.md +71 -0
  145. package/corpus/templates/design/e2e/global-setup.ts +157 -0
  146. package/corpus/templates/design/e2e/helpers.ts +148 -0
  147. package/corpus/templates/design/netlify.toml +3 -4
  148. package/corpus/templates/design/package.json +5 -1
  149. package/corpus/templates/design/playwright.config.ts +51 -0
  150. package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +121 -0
  151. package/corpus/templates/design/server/handlers/uploads.ts +89 -38
  152. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +181 -0
  153. package/corpus/templates/design/server/lib/coding-handoff.ts +33 -4
  154. package/corpus/templates/design/server/lib/design-export.ts +35 -8
  155. package/corpus/templates/design/server/plugins/agent-chat.ts +4 -0
  156. package/corpus/templates/design/server/plugins/auth.ts +10 -1
  157. package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +1 -0
  158. package/corpus/templates/design/shared/api.ts +2 -0
  159. package/corpus/templates/design/shared/apply-edits.ts +12 -8
  160. package/corpus/templates/design/shared/canvas-math.ts +19 -1
  161. package/corpus/templates/design/shared/code-layer.ts +87 -28
  162. package/corpus/templates/design/shared/generation-session.ts +1 -0
  163. package/corpus/templates/design/shared/resolve-tweaks.ts +4 -3
  164. package/corpus/templates/design/shared/shader-presets.ts +10 -0
  165. package/corpus/templates/design/shared/source-mode.ts +9 -0
  166. package/corpus/templates/design/vitest.config.ts +2 -1
  167. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -1
  168. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +1 -1
  169. package/corpus/templates/plan/AGENTS.md +1 -1
  170. package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
  171. package/corpus/templates/plan/actions/validate-local-plan-source.ts +2 -10
  172. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -0
  173. package/corpus/templates/plan/docs/commenting-ux-plan.md +2 -2
  174. package/corpus/templates/plan/server/plan-content.ts +1 -1
  175. package/corpus/templates/plan/server/ui-plan-html.ts +2 -2
  176. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +6 -153
  177. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +53 -38
  178. package/corpus/templates/slides/app/components/design-system/{fig-import-response.ts → builder-index-response.ts} +9 -6
  179. package/corpus/templates/slides/app/components/visual-editor/MultiSelectChip.tsx +1 -1
  180. package/corpus/templates/slides/changelog/2026-06-29-design-system-setup-now-indexes-uploaded-design-files-throug.md +6 -0
  181. package/corpus/templates/slides/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
  182. package/corpus/templates/slides/netlify.toml +3 -6
  183. package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +116 -0
  184. package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +179 -0
  185. package/corpus/templates/slides/server/plugins/agent-chat.ts +4 -1
  186. package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +1 -0
  187. package/dist/action.d.ts +15 -0
  188. package/dist/action.d.ts.map +1 -1
  189. package/dist/action.js +6 -0
  190. package/dist/action.js.map +1 -1
  191. package/dist/agent/durable-background.d.ts +9 -7
  192. package/dist/agent/durable-background.d.ts.map +1 -1
  193. package/dist/agent/durable-background.js +27 -11
  194. package/dist/agent/durable-background.js.map +1 -1
  195. package/dist/agent/production-agent.d.ts +7 -0
  196. package/dist/agent/production-agent.d.ts.map +1 -1
  197. package/dist/agent/production-agent.js +4 -1
  198. package/dist/agent/production-agent.js.map +1 -1
  199. package/dist/agent/run-loop-with-resume.d.ts +2 -1
  200. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  201. package/dist/agent/run-loop-with-resume.js +2 -2
  202. package/dist/agent/run-loop-with-resume.js.map +1 -1
  203. package/dist/brand-kit/fig/index.d.ts +4 -4
  204. package/dist/brand-kit/fig/index.d.ts.map +1 -1
  205. package/dist/brand-kit/fig/index.js +14 -27
  206. package/dist/brand-kit/fig/index.js.map +1 -1
  207. package/dist/cli/index.js +1 -1
  208. package/dist/cli/index.js.map +1 -1
  209. package/dist/cli/mcp-config-writers.d.ts +15 -7
  210. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  211. package/dist/cli/mcp-config-writers.js +198 -45
  212. package/dist/cli/mcp-config-writers.js.map +1 -1
  213. package/dist/cli/skills.d.ts +1 -1
  214. package/dist/cli/skills.d.ts.map +1 -1
  215. package/dist/cli/skills.js +18 -3
  216. package/dist/cli/skills.js.map +1 -1
  217. package/dist/client/AssistantChat.d.ts.map +1 -1
  218. package/dist/client/AssistantChat.js +11 -1
  219. package/dist/client/AssistantChat.js.map +1 -1
  220. package/dist/client/agent-chat.d.ts.map +1 -1
  221. package/dist/client/agent-chat.js +15 -2
  222. package/dist/client/agent-chat.js.map +1 -1
  223. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  224. package/dist/client/composer/PromptComposer.js +35 -1
  225. package/dist/client/composer/PromptComposer.js.map +1 -1
  226. package/dist/client/org/OrgSwitcher.js +1 -1
  227. package/dist/client/org/OrgSwitcher.js.map +1 -1
  228. package/dist/client/sharing/ShareButton.d.ts +1 -1
  229. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  230. package/dist/client/sharing/ShareButton.js +2 -15
  231. package/dist/client/sharing/ShareButton.js.map +1 -1
  232. package/dist/collab/presence.d.ts +1 -1
  233. package/dist/collab/presence.js +1 -1
  234. package/dist/collab/presence.js.map +1 -1
  235. package/dist/collab/routes.d.ts +1 -1
  236. package/dist/mcp/connect-route.d.ts.map +1 -1
  237. package/dist/mcp/connect-route.js +34 -5
  238. package/dist/mcp/connect-route.js.map +1 -1
  239. package/dist/notifications/routes.d.ts +3 -3
  240. package/dist/observability/routes.d.ts +3 -3
  241. package/dist/resources/handlers.d.ts +1 -1
  242. package/dist/server/action-discovery.d.ts.map +1 -1
  243. package/dist/server/action-discovery.js +5 -0
  244. package/dist/server/action-discovery.js.map +1 -1
  245. package/dist/server/agent-chat-plugin.d.ts +6 -0
  246. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  247. package/dist/server/agent-chat-plugin.js +12 -3
  248. package/dist/server/agent-chat-plugin.js.map +1 -1
  249. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  250. package/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
  251. package/docs/content/locales/ar-SA/template-design.mdx +1 -1
  252. package/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
  253. package/docs/content/locales/de-DE/template-design.mdx +1 -1
  254. package/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
  255. package/docs/content/locales/es-ES/template-design.mdx +1 -1
  256. package/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
  257. package/docs/content/locales/fr-FR/template-design.mdx +1 -1
  258. package/docs/content/locales/hi-IN/template-design.mdx +1 -1
  259. package/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
  260. package/docs/content/locales/ja-JP/template-design.mdx +1 -1
  261. package/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
  262. package/docs/content/locales/ko-KR/template-design.mdx +1 -1
  263. package/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
  264. package/docs/content/locales/pt-BR/template-design.mdx +1 -1
  265. package/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
  266. package/docs/content/locales/zh-CN/template-design.mdx +1 -1
  267. package/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
  268. package/docs/content/locales/zh-TW/template-design.mdx +1 -1
  269. package/docs/content/real-time-collaboration.mdx +2 -2
  270. package/docs/content/template-design.mdx +1 -1
  271. package/package.json +3 -7
  272. package/corpus/core/src/brand-kit/fig/decode.ts +0 -410
  273. package/corpus/core/src/brand-kit/fig/extract-design-system.ts +0 -947
  274. package/corpus/core/src/brand-kit/fig/fig-to-html.ts +0 -1906
  275. package/corpus/templates/design/actions/import-figma.ts +0 -55
  276. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +0 -6
  277. package/corpus/templates/design/server/handlers/import-figma-system.ts +0 -86
  278. package/corpus/templates/design/server/routes/api/import-figma-system.post.ts +0 -1
  279. package/corpus/templates/slides/server/handlers/import-figma-system.ts +0 -101
  280. package/corpus/templates/slides/server/routes/api/import-figma-system.post.ts +0 -1
  281. package/dist/brand-kit/fig/decode.d.ts +0 -33
  282. package/dist/brand-kit/fig/decode.d.ts.map +0 -1
  283. package/dist/brand-kit/fig/decode.js +0 -358
  284. package/dist/brand-kit/fig/decode.js.map +0 -1
  285. package/dist/brand-kit/fig/extract-design-system.d.ts +0 -44
  286. package/dist/brand-kit/fig/extract-design-system.d.ts.map +0 -1
  287. package/dist/brand-kit/fig/extract-design-system.js +0 -752
  288. package/dist/brand-kit/fig/extract-design-system.js.map +0 -1
  289. package/dist/brand-kit/fig/fig-to-html.d.ts +0 -246
  290. package/dist/brand-kit/fig/fig-to-html.d.ts.map +0 -1
  291. package/dist/brand-kit/fig/fig-to-html.js +0 -1506
  292. package/dist/brand-kit/fig/fig-to-html.js.map +0 -1
@@ -24,7 +24,8 @@ import {
24
24
  LiveCursorOverlay,
25
25
  useT,
26
26
  useChangeVersion,
27
- useAgentChatContext,
27
+ setAgentChatContextItem,
28
+ removeAgentChatContextItem,
28
29
  useAvatarUrl,
29
30
  type CollabUser,
30
31
  type PromptComposerSubmitOptions,
@@ -88,6 +89,7 @@ import {
88
89
  IconDownload,
89
90
  IconFileExport,
90
91
  IconPlayerPlay,
92
+ IconDeviceFloppy,
91
93
  } from "@tabler/icons-react";
92
94
  import { useQueryClient } from "@tanstack/react-query";
93
95
  import {
@@ -206,6 +208,28 @@ const KEEPALIVE_FILE_SAVE_MAX_BYTES = 60_000;
206
208
  // Higher than the toolbar presets so overview zooming still feels like canvas
207
209
  // work; trackpad/pinch zooming past this commits to editing that screen.
208
210
  const OVERVIEW_EDIT_ZOOM_THRESHOLD = 250;
211
+ const UNSUPPORTED_HTML2CANVAS_COLOR_RE =
212
+ /\b(?:color|color-mix|oklch|oklab|lab|lch)\(/i;
213
+ const HTML2CANVAS_COLOR_PROPERTIES = [
214
+ "color",
215
+ "background-color",
216
+ "border-top-color",
217
+ "border-right-color",
218
+ "border-bottom-color",
219
+ "border-left-color",
220
+ "outline-color",
221
+ "text-decoration-color",
222
+ "fill",
223
+ "stroke",
224
+ ] as const;
225
+ const HTML2CANVAS_SHADOW_PROPERTIES = ["box-shadow", "text-shadow"] as const;
226
+ const HTML2CANVAS_UNSUPPORTED_VALUE_PROPERTIES = [
227
+ "background-image",
228
+ "border-image-source",
229
+ "list-style-image",
230
+ ] as const;
231
+
232
+ let html2CanvasColorContext: CanvasRenderingContext2D | null | undefined;
209
233
 
210
234
  interface FileContentSaveRequest {
211
235
  id: string;
@@ -213,6 +237,115 @@ interface FileContentSaveRequest {
213
237
  syncCollab: boolean;
214
238
  }
215
239
 
240
+ function getHtml2CanvasColorContext(): CanvasRenderingContext2D | null {
241
+ if (html2CanvasColorContext !== undefined) return html2CanvasColorContext;
242
+ if (typeof document === "undefined") {
243
+ html2CanvasColorContext = null;
244
+ return html2CanvasColorContext;
245
+ }
246
+ html2CanvasColorContext = document.createElement("canvas").getContext("2d");
247
+ return html2CanvasColorContext;
248
+ }
249
+
250
+ function parseColorFunctionComponent(component: string): number {
251
+ const trimmed = component.trim();
252
+ if (trimmed.endsWith("%")) {
253
+ return (Number(trimmed.slice(0, -1)) / 100) * 255;
254
+ }
255
+ const value = Number(trimmed);
256
+ if (!Number.isFinite(value)) return 0;
257
+ return Math.abs(value) <= 1 ? value * 255 : value;
258
+ }
259
+
260
+ function parseColorFunctionAlpha(alpha: string | undefined): number {
261
+ if (!alpha) return 1;
262
+ const trimmed = alpha.trim();
263
+ if (trimmed.endsWith("%")) return Number(trimmed.slice(0, -1)) / 100;
264
+ const value = Number(trimmed);
265
+ return Number.isFinite(value) ? value : 1;
266
+ }
267
+
268
+ function parseRgbLikeColorFunction(value: string): string | null {
269
+ const match = value.match(/color\(\s*[\w-]+\s+([^)]+)\)/i);
270
+ if (!match) return null;
271
+ const [componentsPart, alphaPart] = match[1].split("/");
272
+ const channels = componentsPart.trim().split(/\s+/).slice(0, 3);
273
+ if (channels.length < 3) return null;
274
+ const [red, green, blue] = channels
275
+ .map(parseColorFunctionComponent)
276
+ .map((channel) => Math.round(Math.max(0, Math.min(255, channel))));
277
+ const alpha = Math.max(0, Math.min(1, parseColorFunctionAlpha(alphaPart)));
278
+ return alpha < 1
279
+ ? `rgba(${red}, ${green}, ${blue}, ${alpha})`
280
+ : `rgb(${red}, ${green}, ${blue})`;
281
+ }
282
+
283
+ function normalizeHtml2CanvasColor(value: string): string {
284
+ if (!UNSUPPORTED_HTML2CANVAS_COLOR_RE.test(value)) return value;
285
+ const context = getHtml2CanvasColorContext();
286
+ if (context) {
287
+ try {
288
+ context.fillStyle = "#000";
289
+ context.fillStyle = value;
290
+ const normalized = String(context.fillStyle);
291
+ if (normalized && !UNSUPPORTED_HTML2CANVAS_COLOR_RE.test(normalized)) {
292
+ return normalized;
293
+ }
294
+ } catch {
295
+ // Fall back to small parser below.
296
+ }
297
+ }
298
+ return parseRgbLikeColorFunction(value) ?? "rgb(0, 0, 0)";
299
+ }
300
+
301
+ function elementInlineStyle(
302
+ element: Element | undefined,
303
+ ): CSSStyleDeclaration | null {
304
+ if (!element) return null;
305
+ const style = (element as Element & { style?: CSSStyleDeclaration }).style;
306
+ return style && typeof style.setProperty === "function" ? style : null;
307
+ }
308
+
309
+ function sanitizeHtml2CanvasClone(
310
+ sourceDocument: Document,
311
+ clonedDocument: Document,
312
+ ) {
313
+ const sourceView = sourceDocument.defaultView;
314
+ if (!sourceView) return;
315
+ const sourceElements = [
316
+ sourceDocument.documentElement,
317
+ ...Array.from(sourceDocument.documentElement.querySelectorAll("*")),
318
+ ];
319
+ const clonedElements = [
320
+ clonedDocument.documentElement,
321
+ ...Array.from(clonedDocument.documentElement.querySelectorAll("*")),
322
+ ];
323
+ sourceElements.forEach((sourceElement, index) => {
324
+ const clonedStyle = elementInlineStyle(clonedElements[index]);
325
+ if (!clonedStyle) return;
326
+ const computed = sourceView.getComputedStyle(sourceElement);
327
+ for (const property of HTML2CANVAS_COLOR_PROPERTIES) {
328
+ const value = computed.getPropertyValue(property);
329
+ if (!value || !UNSUPPORTED_HTML2CANVAS_COLOR_RE.test(value)) continue;
330
+ clonedStyle.setProperty(
331
+ property,
332
+ normalizeHtml2CanvasColor(value),
333
+ "important",
334
+ );
335
+ }
336
+ for (const property of HTML2CANVAS_SHADOW_PROPERTIES) {
337
+ const value = computed.getPropertyValue(property);
338
+ if (!value || !UNSUPPORTED_HTML2CANVAS_COLOR_RE.test(value)) continue;
339
+ clonedStyle.setProperty(property, "none", "important");
340
+ }
341
+ for (const property of HTML2CANVAS_UNSUPPORTED_VALUE_PROPERTIES) {
342
+ const value = computed.getPropertyValue(property);
343
+ if (!value || !UNSUPPORTED_HTML2CANVAS_COLOR_RE.test(value)) continue;
344
+ clonedStyle.setProperty(property, "none", "important");
345
+ }
346
+ });
347
+ }
348
+
216
349
  function byteLength(value: string): number {
217
350
  if (typeof TextEncoder === "undefined") return value.length;
218
351
  return new TextEncoder().encode(value).length;
@@ -257,6 +390,34 @@ type DesignTool =
257
390
  | "scale";
258
391
  type ShapeTool = "rect" | "line" | "arrow" | "ellipse" | "polygon" | "star";
259
392
 
393
+ const DESIGN_EDITOR_TOOLS = new Set<DesignTool>([
394
+ "move",
395
+ "frame",
396
+ "rect",
397
+ "line",
398
+ "arrow",
399
+ "ellipse",
400
+ "polygon",
401
+ "star",
402
+ "text",
403
+ "pen",
404
+ "hand",
405
+ "comment",
406
+ "draw",
407
+ "scale",
408
+ ]);
409
+
410
+ function normalizeDesignTool(value: unknown): DesignTool | null {
411
+ return typeof value === "string" &&
412
+ DESIGN_EDITOR_TOOLS.has(value as DesignTool)
413
+ ? (value as DesignTool)
414
+ : null;
415
+ }
416
+
417
+ function isSingleScreenAnnotationTool(tool: DesignTool): boolean {
418
+ return tool === "draw" || tool === "comment";
419
+ }
420
+
260
421
  interface DesignFile {
261
422
  id: string;
262
423
  filename: string;
@@ -273,9 +434,23 @@ interface DesignData {
273
434
  projectType: string;
274
435
  designSystemId?: string | null;
275
436
  data?: string | null;
437
+ accessRole?: DesignAccessRole;
276
438
  files: DesignFile[];
277
439
  }
278
440
 
441
+ type DesignAccessRole = "owner" | "admin" | "editor" | "viewer";
442
+ type PostAuthDesignIntent = "save" | "share";
443
+
444
+ function buildSignInHrefForDesignIntent(intent: PostAuthDesignIntent): string {
445
+ const base = agentNativePath("/_agent-native/sign-in");
446
+ if (typeof window === "undefined") return base;
447
+
448
+ const returnUrl = new URL(window.location.href);
449
+ returnUrl.searchParams.set("intent", intent);
450
+ const ret = returnUrl.pathname + returnUrl.search + returnUrl.hash;
451
+ return `${base}?return=${encodeURIComponent(ret)}`;
452
+ }
453
+
279
454
  interface GeometryHistoryEntry {
280
455
  before: CanvasFrameGeometryById;
281
456
  after: CanvasFrameGeometryById;
@@ -424,14 +599,17 @@ function designEditorCommandFromSearchParams(
424
599
  searchParams.get("screen") ??
425
600
  searchParams.get("fileId") ??
426
601
  searchParams.get("filename");
427
- const zoom = Number(searchParams.get("zoom"));
602
+ const rawZoom = searchParams.get("zoom");
603
+ const zoom = rawZoom !== null ? Number(rawZoom) : NaN;
604
+ const tool = normalizeDesignTool(searchParams.get("tool"));
428
605
  if (
429
606
  editorView !== "overview" &&
430
607
  editorView !== "single" &&
431
608
  inspector !== "design" &&
432
609
  inspector !== "tweaks" &&
433
610
  inspector !== "extensions" &&
434
- !screen
611
+ !screen &&
612
+ !tool
435
613
  ) {
436
614
  return null;
437
615
  }
@@ -455,6 +633,7 @@ function designEditorCommandFromSearchParams(
455
633
  } else if (editorView === "single") {
456
634
  command.zoom = FOCUSED_SCREEN_ZOOM;
457
635
  }
636
+ if (tool) command.tool = tool;
458
637
  return command;
459
638
  }
460
639
 
@@ -662,7 +841,7 @@ function appendCanvasPrimitiveToHtml(
662
841
  const top = Math.max(0, Math.round(geometry.y));
663
842
  const width = Math.max(1, Math.round(geometry.width));
664
843
  const height = Math.max(1, Math.round(geometry.height));
665
- const nodeId = uniqueLayerId(primitive.kind);
844
+ const nodeId = primitive.nodeId ?? uniqueLayerId(primitive.kind);
666
845
  const layerName = primitiveLayerName(primitive);
667
846
 
668
847
  if (
@@ -1191,8 +1370,8 @@ function codeLayerTreeToPanelNodes(
1191
1370
  renamable: node.renamable,
1192
1371
  lockable: true,
1193
1372
  hideable: true,
1194
- locked: selfLocked,
1195
- hidden: selfHidden,
1373
+ locked,
1374
+ hidden,
1196
1375
  children: codeLayerTreeToPanelNodes(
1197
1376
  node.children,
1198
1377
  lockedIds,
@@ -1204,6 +1383,36 @@ function codeLayerTreeToPanelNodes(
1204
1383
  });
1205
1384
  }
1206
1385
 
1386
+ interface EffectiveCodeLayerState {
1387
+ lockedIds: Set<string>;
1388
+ hiddenIds: Set<string>;
1389
+ }
1390
+
1391
+ function collectEffectiveCodeLayerState(
1392
+ nodes: CodeLayerTreeNode[],
1393
+ lockedIds: Set<string>,
1394
+ hiddenIds: Set<string>,
1395
+ inheritedLocked: boolean,
1396
+ inheritedHidden: boolean,
1397
+ state: EffectiveCodeLayerState,
1398
+ ): EffectiveCodeLayerState {
1399
+ nodes.forEach((node) => {
1400
+ const locked = inheritedLocked || lockedIds.has(node.id);
1401
+ const hidden = inheritedHidden || hiddenIds.has(node.id);
1402
+ if (locked) state.lockedIds.add(node.id);
1403
+ if (hidden) state.hiddenIds.add(node.id);
1404
+ collectEffectiveCodeLayerState(
1405
+ node.children,
1406
+ lockedIds,
1407
+ hiddenIds,
1408
+ locked,
1409
+ hidden,
1410
+ state,
1411
+ );
1412
+ });
1413
+ return state;
1414
+ }
1415
+
1207
1416
  function bridgeSourceIdForCodeLayerNode(node: CodeLayerNode): string {
1208
1417
  return (
1209
1418
  node.dataAttributes["data-agent-native-node-id"] ??
@@ -1477,7 +1686,7 @@ function fullPreviewHtml(content: string): string {
1477
1686
  return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${content}</body></html>`;
1478
1687
  }
1479
1688
 
1480
- type FigmaToolbarOption = {
1689
+ type DesignToolbarOption = {
1481
1690
  key: string;
1482
1691
  label: string;
1483
1692
  icon: ReactNode;
@@ -1487,7 +1696,7 @@ type FigmaToolbarOption = {
1487
1696
  onSelect: () => void;
1488
1697
  };
1489
1698
 
1490
- function FigmaToolbarTool({
1699
+ function DesignToolbarTool({
1491
1700
  active,
1492
1701
  label,
1493
1702
  icon,
@@ -1497,7 +1706,7 @@ function FigmaToolbarTool({
1497
1706
  active: boolean;
1498
1707
  label: string;
1499
1708
  icon: ReactNode;
1500
- options: FigmaToolbarOption[];
1709
+ options: DesignToolbarOption[];
1501
1710
  onPrimary: () => void;
1502
1711
  }) {
1503
1712
  const hasOptionsMenu = options.length > 1;
@@ -1506,7 +1715,7 @@ function FigmaToolbarTool({
1506
1715
  className={cn(
1507
1716
  "flex h-8 items-center overflow-hidden rounded-md text-neutral-200 transition-colors",
1508
1717
  active
1509
- ? "bg-[#0d99ff] text-white"
1718
+ ? "bg-[var(--design-editor-accent-color)] text-white"
1510
1719
  : "hover:bg-white/10 hover:text-white",
1511
1720
  )}
1512
1721
  >
@@ -1574,7 +1783,7 @@ function FigmaToolbarTool({
1574
1783
  );
1575
1784
  }
1576
1785
 
1577
- function FigmaModeTab({
1786
+ function DesignModeTab({
1578
1787
  active,
1579
1788
  disabled,
1580
1789
  label,
@@ -1610,11 +1819,13 @@ function FigmaModeTab({
1610
1819
  );
1611
1820
  }
1612
1821
 
1613
- function FigmaBottomToolbar({
1822
+ function DesignBottomToolbar({
1614
1823
  mode,
1615
1824
  pinMode,
1616
1825
  drawMode,
1617
1826
  activeTool,
1827
+ isOverview,
1828
+ hasActiveFile,
1618
1829
  onMove,
1619
1830
  onFrame,
1620
1831
  onShape,
@@ -1630,6 +1841,8 @@ function FigmaBottomToolbar({
1630
1841
  pinMode: boolean;
1631
1842
  drawMode: boolean;
1632
1843
  activeTool: DesignTool;
1844
+ isOverview: boolean;
1845
+ hasActiveFile: boolean;
1633
1846
  onMove: () => void;
1634
1847
  onFrame: () => void;
1635
1848
  onShape: (tool: ShapeTool) => void;
@@ -1670,7 +1883,7 @@ function FigmaBottomToolbar({
1670
1883
  return <IconSquare className={className} />;
1671
1884
  }
1672
1885
  };
1673
- const shapeOptions: FigmaToolbarOption[] = [
1886
+ const shapeOptions: DesignToolbarOption[] = [
1674
1887
  {
1675
1888
  key: "rect",
1676
1889
  label: t("designEditor.tools.rect"),
@@ -1735,7 +1948,7 @@ function FigmaBottomToolbar({
1735
1948
  label: string;
1736
1949
  icon: ReactNode;
1737
1950
  onClick: () => void;
1738
- options: FigmaToolbarOption[];
1951
+ options: DesignToolbarOption[];
1739
1952
  }> = [
1740
1953
  {
1741
1954
  key: "move",
@@ -1832,6 +2045,7 @@ function FigmaBottomToolbar({
1832
2045
  label: t("designEditor.modes.draw"),
1833
2046
  icon: <IconBrush className="size-4" />,
1834
2047
  active: activeTool === "draw" && mode === "annotate" && drawMode,
2048
+ disabled: !hasActiveFile || isOverview,
1835
2049
  onSelect: onDraw,
1836
2050
  },
1837
2051
  ],
@@ -1849,6 +2063,7 @@ function FigmaBottomToolbar({
1849
2063
  icon: <IconMessage className="size-4" />,
1850
2064
  shortcut: "C",
1851
2065
  active: activeTool === "comment" && mode === "annotate" && pinMode,
2066
+ disabled: !hasActiveFile || isOverview,
1852
2067
  onSelect: onCommentPin,
1853
2068
  },
1854
2069
  {
@@ -1856,6 +2071,7 @@ function FigmaBottomToolbar({
1856
2071
  label: t("designEditor.modes.draw"),
1857
2072
  icon: <IconBrush className="size-4" />,
1858
2073
  active: activeTool === "draw" && mode === "annotate" && drawMode,
2074
+ disabled: !hasActiveFile || isOverview,
1859
2075
  onSelect: onDraw,
1860
2076
  },
1861
2077
  ],
@@ -1896,7 +2112,7 @@ function FigmaBottomToolbar({
1896
2112
  <div className="absolute bottom-4 left-1/2 z-[70] flex max-w-[calc(100%-2rem)] -translate-x-1/2 items-center gap-1.5 overflow-hidden rounded-xl border border-white/10 bg-[#2c2c2c]/95 p-1.5 text-neutral-100 shadow-[0_22px_55px_-24px_rgba(0,0,0,0.9),0_0_0_1px_rgba(0,0,0,0.25)] backdrop-blur">
1897
2113
  <div className="flex min-w-0 items-center gap-0.5">
1898
2114
  {tools.map((tool) => (
1899
- <FigmaToolbarTool
2115
+ <DesignToolbarTool
1900
2116
  key={tool.key}
1901
2117
  active={tool.active}
1902
2118
  label={tool.label}
@@ -1911,7 +2127,7 @@ function FigmaBottomToolbar({
1911
2127
 
1912
2128
  <div className="flex shrink-0 items-center gap-0.5 rounded-md bg-white/10 p-0.5">
1913
2129
  {modes.map((item) => (
1914
- <FigmaModeTab
2130
+ <DesignModeTab
1915
2131
  key={item.key}
1916
2132
  active={item.active}
1917
2133
  label={item.label}
@@ -2002,6 +2218,10 @@ export default function DesignEditor() {
2002
2218
  () => new URLSearchParams(location.search),
2003
2219
  [location.search],
2004
2220
  );
2221
+ const postAuthIntent = useMemo<PostAuthDesignIntent | null>(() => {
2222
+ const value = searchParams.get("intent");
2223
+ return value === "save" || value === "share" ? value : null;
2224
+ }, [searchParams]);
2005
2225
  const queryClient = useQueryClient();
2006
2226
  const appStateVersion = useChangeVersion("app-state");
2007
2227
  const browserTabId = getBrowserTabId();
@@ -2027,6 +2247,10 @@ export default function DesignEditor() {
2027
2247
  const [deviceFrame, setDeviceFrame] = useState<DeviceFrameType>("none");
2028
2248
  const [viewMode, setViewMode] = useState<"single" | "overview">("overview");
2029
2249
  const viewModeRef = useRef<"single" | "overview">("overview");
2250
+ // Trusted parent origin captured from the first validated inbound message.
2251
+ // Used to restrict outgoing postMessage calls that carry user data so they
2252
+ // are never broadcast to an arbitrary embedding page.
2253
+ const parentOriginRef = useRef<string | null>(null);
2030
2254
  const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(
2031
2255
  null,
2032
2256
  );
@@ -2063,13 +2287,12 @@ export default function DesignEditor() {
2063
2287
  const copiedLayerHtmlRef = useRef<string | null>(null);
2064
2288
  const copiedStylePropsRef = useRef<Record<string, string> | null>(null);
2065
2289
  const spaceHandPreviousToolRef = useRef<DesignTool | null>(null);
2066
- const { set: setAgentChatContextItem, remove: removeAgentChatContextItem } =
2067
- useAgentChatContext();
2068
2290
  const hasSelectedElement = Boolean(selectedElement);
2069
2291
 
2070
2292
  useEffect(() => {
2071
2293
  if (!isBuilderDesignEmbed) return;
2072
- // Announce ready to Builder
2294
+ // Announce ready to Builder. The trusted origin is not yet known at this
2295
+ // point so we use "*" — this message carries no user data.
2073
2296
  window.parent.postMessage({ type: "agentNative.appReady" }, "*");
2074
2297
 
2075
2298
  function handleDesignHostMessage(event: MessageEvent) {
@@ -2093,6 +2316,12 @@ export default function DesignEditor() {
2093
2316
  if (!data || typeof data.type !== "string") return;
2094
2317
 
2095
2318
  if (data.type === "design:init") {
2319
+ // Capture the trusted parent origin on the first validated message so
2320
+ // outgoing postMessage calls that carry user data can restrict the
2321
+ // target instead of broadcasting to "*".
2322
+ if (!parentOriginRef.current) {
2323
+ parentOriginRef.current = origin;
2324
+ }
2096
2325
  const { previewUrl, themeVars } = data.data ?? {};
2097
2326
  // Apply theme vars
2098
2327
  if (themeVars && typeof themeVars === "object") {
@@ -2213,7 +2442,9 @@ export default function DesignEditor() {
2213
2442
  const [pinMode, setPinMode] = useState(false);
2214
2443
  const [showPrompt, setShowPrompt] = useState(false);
2215
2444
  const [showTweakPrompt, setShowTweakPrompt] = useState(false);
2445
+ const [pngExporting, setPngExporting] = useState(false);
2216
2446
  const [svgExporting, setSvgExporting] = useState(false);
2447
+ const pngExportingRef = useRef(false);
2217
2448
  const generateBtnRef = useRef<HTMLButtonElement | null>(null);
2218
2449
  const promptAnchorRef = useRef<HTMLElement | null>(null);
2219
2450
  const tweakPromptAnchorRef = useRef<HTMLElement | null>(null);
@@ -2438,6 +2669,7 @@ export default function DesignEditor() {
2438
2669
  );
2439
2670
 
2440
2671
  const { session } = useSession();
2672
+ const isSignedIn = Boolean(session?.email);
2441
2673
  const pendingVariantKey = useMemo(
2442
2674
  () =>
2443
2675
  pendingVariants
@@ -2492,6 +2724,12 @@ export default function DesignEditor() {
2492
2724
  : undefined,
2493
2725
  [session?.email, session?.name],
2494
2726
  );
2727
+ const handleSignInToSave = useCallback(() => {
2728
+ window.location.href = buildSignInHrefForDesignIntent("save");
2729
+ }, []);
2730
+ const handleSignInToShare = useCallback(() => {
2731
+ window.location.href = buildSignInHrefForDesignIntent("share");
2732
+ }, []);
2495
2733
 
2496
2734
  // Data fetching
2497
2735
  useEffect(() => {
@@ -2565,22 +2803,22 @@ export default function DesignEditor() {
2565
2803
  const deleteFileMutation = useActionMutation("delete-file");
2566
2804
  const updateDesignMutation = useActionMutation("update-design");
2567
2805
  const applyTweaksMutation = useActionMutation("apply-tweaks");
2806
+ const duplicateDesignMutation = useActionMutation("duplicate-design");
2568
2807
  const createCodingHandoffMutation = useActionMutation(
2569
2808
  "export-coding-handoff",
2570
2809
  );
2571
2810
  const exportHtmlMutation = useActionMutation("export-html");
2572
2811
  const exportZipMutation = useActionMutation("export-zip");
2573
2812
  const [, setPatchProof] = useState<PatchProofState | null>(null);
2574
- const pendingFileSaveRef = useRef<{
2575
- id: string;
2576
- content: string;
2577
- syncCollab: boolean;
2578
- } | null>(null);
2813
+ const pendingFileSavesRef = useRef<Record<string, FileContentSaveRequest>>(
2814
+ {},
2815
+ );
2579
2816
  const fileSaveChainsRef = useRef<Record<string, Promise<void>>>({});
2580
2817
  const latestFileSaveForUnloadRef = useRef<
2581
2818
  Record<string, FileContentSaveRequest>
2582
2819
  >({});
2583
- const fileSaveTimerRef = useRef<number | null>(null);
2820
+ const fileSaveTimersRef = useRef<Record<string, number>>({});
2821
+ const postAuthSaveRef = useRef<string | null>(null);
2584
2822
 
2585
2823
  const saveFileContent = useCallback(
2586
2824
  (pending: FileContentSaveRequest) => {
@@ -2639,22 +2877,24 @@ export default function DesignEditor() {
2639
2877
  };
2640
2878
  latestFileSaveForUnloadRef.current[fileId] = pending;
2641
2879
  if (options.immediate) {
2642
- if (fileSaveTimerRef.current) {
2643
- window.clearTimeout(fileSaveTimerRef.current);
2644
- fileSaveTimerRef.current = null;
2880
+ const timer = fileSaveTimersRef.current[fileId];
2881
+ if (timer) {
2882
+ window.clearTimeout(timer);
2883
+ delete fileSaveTimersRef.current[fileId];
2645
2884
  }
2646
- pendingFileSaveRef.current = null;
2885
+ delete pendingFileSavesRef.current[fileId];
2647
2886
  saveFileContent(pending);
2648
2887
  return;
2649
2888
  }
2650
- pendingFileSaveRef.current = pending;
2651
- if (fileSaveTimerRef.current) {
2652
- window.clearTimeout(fileSaveTimerRef.current);
2889
+ pendingFileSavesRef.current[fileId] = pending;
2890
+ const timer = fileSaveTimersRef.current[fileId];
2891
+ if (timer) {
2892
+ window.clearTimeout(timer);
2653
2893
  }
2654
- fileSaveTimerRef.current = window.setTimeout(() => {
2655
- const pending = pendingFileSaveRef.current;
2656
- pendingFileSaveRef.current = null;
2657
- fileSaveTimerRef.current = null;
2894
+ fileSaveTimersRef.current[fileId] = window.setTimeout(() => {
2895
+ const pending = pendingFileSavesRef.current[fileId];
2896
+ delete pendingFileSavesRef.current[fileId];
2897
+ delete fileSaveTimersRef.current[fileId];
2658
2898
  if (!pending) return;
2659
2899
  saveFileContent(pending);
2660
2900
  }, 400);
@@ -2663,21 +2903,26 @@ export default function DesignEditor() {
2663
2903
  );
2664
2904
 
2665
2905
  useEffect(() => {
2666
- const handlePageHide = () => {
2667
- if (pendingFileSaveRef.current) {
2668
- latestFileSaveForUnloadRef.current[pendingFileSaveRef.current.id] =
2669
- pendingFileSaveRef.current;
2906
+ const sendPendingKeepaliveSaves = () => {
2907
+ for (const pending of Object.values(pendingFileSavesRef.current)) {
2908
+ latestFileSaveForUnloadRef.current[pending.id] = pending;
2670
2909
  }
2671
2910
  Object.values(latestFileSaveForUnloadRef.current).forEach(
2672
2911
  sendFileContentSaveKeepalive,
2673
2912
  );
2674
2913
  };
2914
+ const handlePageHide = () => {
2915
+ sendPendingKeepaliveSaves();
2916
+ };
2675
2917
  window.addEventListener("pagehide", handlePageHide);
2676
2918
  return () => {
2677
2919
  window.removeEventListener("pagehide", handlePageHide);
2678
- if (fileSaveTimerRef.current) {
2679
- window.clearTimeout(fileSaveTimerRef.current);
2920
+ sendPendingKeepaliveSaves();
2921
+ for (const timer of Object.values(fileSaveTimersRef.current)) {
2922
+ window.clearTimeout(timer);
2680
2923
  }
2924
+ fileSaveTimersRef.current = {};
2925
+ pendingFileSavesRef.current = {};
2681
2926
  };
2682
2927
  }, []);
2683
2928
 
@@ -2733,12 +2978,55 @@ export default function DesignEditor() {
2733
2978
  }, []);
2734
2979
 
2735
2980
  const design = isDesignData(designResult) ? designResult : null;
2981
+ const designAccessRole = design?.accessRole;
2982
+ const canShareDesign =
2983
+ designAccessRole === "owner" || designAccessRole === "admin";
2984
+ const canEditDesign = canShareDesign || designAccessRole === "editor";
2985
+ const shouldOpenShare = postAuthIntent === "share" && canShareDesign;
2986
+ const editorShareUrl = useMemo(() => {
2987
+ if (!id || typeof window === "undefined") return undefined;
2988
+ return `${window.location.origin}/design/${encodeURIComponent(id)}`;
2989
+ }, [id]);
2736
2990
  const {
2737
2991
  designSystems,
2738
2992
  defaultSystem,
2739
2993
  isLoading: designSystemsLoading,
2740
2994
  } = useDesignSystems();
2741
2995
 
2996
+ useEffect(() => {
2997
+ if (!id || !design || !isSignedIn || !postAuthIntent) return;
2998
+
2999
+ const shouldDuplicate =
3000
+ postAuthIntent === "share" ? !canShareDesign : !canEditDesign;
3001
+ if (!shouldDuplicate) return;
3002
+
3003
+ const key = `${postAuthIntent}:${id}`;
3004
+ if (postAuthSaveRef.current === key) return;
3005
+ postAuthSaveRef.current = key;
3006
+
3007
+ duplicateDesignMutation
3008
+ .mutateAsync({ id, title: design.title } as any)
3009
+ .then((result: any) => {
3010
+ if (!result?.id) throw new Error("Missing copied design id");
3011
+ const nextSearch = postAuthIntent === "share" ? "?intent=share" : "";
3012
+ navigate(`/design/${result.id}${nextSearch}`, { replace: true });
3013
+ })
3014
+ .catch(() => {
3015
+ postAuthSaveRef.current = null;
3016
+ toast.error(t("designEditor.toasts.saveCopyError"));
3017
+ });
3018
+ }, [
3019
+ canEditDesign,
3020
+ canShareDesign,
3021
+ design,
3022
+ duplicateDesignMutation,
3023
+ id,
3024
+ isSignedIn,
3025
+ navigate,
3026
+ postAuthIntent,
3027
+ t,
3028
+ ]);
3029
+
2742
3030
  const resolvePromptDesignSystemId = useCallback(
2743
3031
  () =>
2744
3032
  design?.designSystemId ??
@@ -2841,6 +3129,12 @@ export default function DesignEditor() {
2841
3129
  () => parseDesignDataJson(design?.data),
2842
3130
  [design?.data],
2843
3131
  );
3132
+ // Keep a ref in sync so debounced timer callbacks can read the freshest
3133
+ // designDataJson without closing over a stale snapshot from render time.
3134
+ const designDataJsonRef = useRef(designDataJson);
3135
+ useEffect(() => {
3136
+ designDataJsonRef.current = designDataJson;
3137
+ }, [designDataJson]);
2844
3138
  const canvasFrameGeometryById = useMemo(
2845
3139
  () => getCanvasFrameGeometry(designDataJson),
2846
3140
  [designDataJson],
@@ -2853,8 +3147,11 @@ export default function DesignEditor() {
2853
3147
  }
2854
3148
  frameGeometrySaveTimerRef.current = window.setTimeout(() => {
2855
3149
  frameGeometrySaveTimerRef.current = null;
3150
+ // Read the freshest designDataJson from the ref so any concurrent
3151
+ // server writes (e.g. apply-tweaks) that arrived during the 500 ms
3152
+ // debounce window are not overwritten with stale closure data.
2856
3153
  const nextData = {
2857
- ...designDataJson,
3154
+ ...designDataJsonRef.current,
2858
3155
  canvasFrames: geometryById,
2859
3156
  };
2860
3157
  updateDesignMutation.mutate(
@@ -2872,7 +3169,7 @@ export default function DesignEditor() {
2872
3169
  );
2873
3170
  }, 500);
2874
3171
  },
2875
- [designDataJson, id, queryClient, updateDesignMutation],
3172
+ [id, queryClient, updateDesignMutation],
2876
3173
  );
2877
3174
 
2878
3175
  const writeFrameGeometrySnapshot = useCallback(
@@ -2884,7 +3181,7 @@ export default function DesignEditor() {
2884
3181
  }
2885
3182
  const snapshot = cloneCanvasFrameGeometry(geometryById);
2886
3183
  const nextData = {
2887
- ...designDataJson,
3184
+ ...designDataJsonRef.current,
2888
3185
  canvasFrames: snapshot,
2889
3186
  };
2890
3187
  queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
@@ -2905,7 +3202,7 @@ export default function DesignEditor() {
2905
3202
  },
2906
3203
  );
2907
3204
  },
2908
- [designDataJson, id, queryClient, updateDesignMutation],
3205
+ [id, queryClient, updateDesignMutation],
2909
3206
  );
2910
3207
 
2911
3208
  const handleGeometryCommit = useCallback(
@@ -3026,10 +3323,6 @@ export default function DesignEditor() {
3026
3323
  clearGenerationCompleteTimer,
3027
3324
  ]);
3028
3325
 
3029
- useEffect(() => {
3030
- return () => clearPendingGeneration(id);
3031
- }, [id]);
3032
-
3033
3326
  useEffect(() => {
3034
3327
  return () => {
3035
3328
  if (frameGeometrySaveTimerRef.current !== null) {
@@ -3081,6 +3374,27 @@ export default function DesignEditor() {
3081
3374
  : undefined;
3082
3375
  if (inspectorTab) setActiveInspectorTab(inspectorTab);
3083
3376
 
3377
+ const commandTool = normalizeDesignTool(command.tool);
3378
+ const effectiveCommandTool =
3379
+ editorView === "overview" &&
3380
+ commandTool &&
3381
+ isSingleScreenAnnotationTool(commandTool)
3382
+ ? "move"
3383
+ : commandTool;
3384
+ const applyCommandTool = (fallback: DesignTool) => {
3385
+ const nextTool = effectiveCommandTool ?? fallback;
3386
+ setActiveTool(nextTool);
3387
+ if (isSingleScreenAnnotationTool(nextTool)) {
3388
+ setMode("annotate");
3389
+ setDrawMode(true);
3390
+ setPinMode(nextTool === "comment");
3391
+ return;
3392
+ }
3393
+ setMode("edit");
3394
+ setDrawMode(false);
3395
+ setPinMode(false);
3396
+ };
3397
+
3084
3398
  if (targetFile) {
3085
3399
  setActiveFileId(targetFile.id);
3086
3400
  }
@@ -3091,19 +3405,13 @@ export default function DesignEditor() {
3091
3405
 
3092
3406
  if (editorView === "overview") {
3093
3407
  viewModeRef.current = "overview";
3094
- setDrawMode(false);
3095
- setPinMode(false);
3096
- setMode("edit");
3097
3408
  setSelectedElement(null);
3098
- setActiveTool("move");
3409
+ applyCommandTool("move");
3099
3410
  setViewMode("overview");
3100
3411
  } else if (editorView === "single") {
3101
3412
  viewModeRef.current = "single";
3102
- setDrawMode(false);
3103
- setPinMode(false);
3104
- setMode("edit");
3105
3413
  setSelectedElement(null);
3106
- setActiveTool("move");
3414
+ applyCommandTool("move");
3107
3415
  if (
3108
3416
  typeof command.zoom !== "number" ||
3109
3417
  !Number.isFinite(command.zoom)
@@ -3111,6 +3419,8 @@ export default function DesignEditor() {
3111
3419
  setZoom((currentZoom) => Math.max(currentZoom, FOCUSED_SCREEN_ZOOM));
3112
3420
  }
3113
3421
  setViewMode("single");
3422
+ } else if (effectiveCommandTool) {
3423
+ applyCommandTool("move");
3114
3424
  }
3115
3425
 
3116
3426
  return true;
@@ -3308,7 +3618,7 @@ export default function DesignEditor() {
3308
3618
  // Collaborative editing for the active file
3309
3619
  const { ydoc, awareness, isSynced, activeUsers, agentActive } =
3310
3620
  useCollaborativeDoc({
3311
- docId: activeFileId,
3621
+ docId: isSignedIn ? activeFileId : null,
3312
3622
  requestSource: TAB_ID,
3313
3623
  user: currentUser,
3314
3624
  });
@@ -3973,7 +4283,7 @@ export default function DesignEditor() {
3973
4283
  });
3974
4284
  }
3975
4285
 
3976
- return true;
4286
+ return primitive.nodeId ?? true;
3977
4287
  },
3978
4288
  [
3979
4289
  activeContent,
@@ -4284,15 +4594,7 @@ export default function DesignEditor() {
4284
4594
  context: contextLines.join("\n"),
4285
4595
  openSidebar: false,
4286
4596
  });
4287
- }, [
4288
- activeFile,
4289
- design?.title,
4290
- id,
4291
- removeAgentChatContextItem,
4292
- selectedCodeLayerNode,
4293
- selectedElement,
4294
- setAgentChatContextItem,
4295
- ]);
4597
+ }, [activeFile, design?.title, id, selectedCodeLayerNode, selectedElement]);
4296
4598
 
4297
4599
  const designExtensionContext = useMemo<DesignExtensionSlotContext>(
4298
4600
  () => ({
@@ -5078,6 +5380,15 @@ export default function DesignEditor() {
5078
5380
  if (!replacePreviewContent(next)) {
5079
5381
  setContentRenderRevision((revision) => revision + 1);
5080
5382
  }
5383
+ // Clear stale selection if the undo removed the selected element.
5384
+ setSelectedElement((prev) => {
5385
+ if (!prev) return prev;
5386
+ return elementInfoExistsInContent(next, prev) ? prev : null;
5387
+ });
5388
+ setHoveredElement((prev) => {
5389
+ if (!prev) return prev;
5390
+ return elementInfoExistsInContent(next, prev) ? prev : null;
5391
+ });
5081
5392
  }
5082
5393
  redoOrderRef.current = [
5083
5394
  ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
@@ -5134,6 +5445,15 @@ export default function DesignEditor() {
5134
5445
  if (!replacePreviewContent(next)) {
5135
5446
  setContentRenderRevision((revision) => revision + 1);
5136
5447
  }
5448
+ // Clear stale selection if the redo removed the selected element.
5449
+ setSelectedElement((prev) => {
5450
+ if (!prev) return prev;
5451
+ return elementInfoExistsInContent(next, prev) ? prev : null;
5452
+ });
5453
+ setHoveredElement((prev) => {
5454
+ if (!prev) return prev;
5455
+ return elementInfoExistsInContent(next, prev) ? prev : null;
5456
+ });
5137
5457
  }
5138
5458
  historyOrderRef.current = [
5139
5459
  ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
@@ -5268,6 +5588,7 @@ export default function DesignEditor() {
5268
5588
  }
5269
5589
 
5270
5590
  if (activeFile && viewMode === "overview") {
5591
+ viewModeRef.current = "single";
5271
5592
  setViewMode("single");
5272
5593
  }
5273
5594
  setMode(next);
@@ -5493,6 +5814,20 @@ export default function DesignEditor() {
5493
5814
  ].join("\n");
5494
5815
  clearGenerationCompleteTimer();
5495
5816
  setGenerationIssue(null);
5817
+ const startedAt = Date.now();
5818
+ patchPendingGeneration(id, {
5819
+ prompt: promptState.prompt,
5820
+ files: promptState.files,
5821
+ title: design.title,
5822
+ designSystemId: promptState.designSystemId,
5823
+ model: promptState.model,
5824
+ engine: promptState.engine,
5825
+ effort: promptState.effort,
5826
+ attempt,
5827
+ startedAt,
5828
+ });
5829
+ setHasPendingGeneration(true);
5830
+ setRetryablePrompt(null);
5496
5831
  const runTabId = agentSubmit(
5497
5832
  `Generate design for "${design.title}": ${promptState.prompt}`,
5498
5833
  context,
@@ -5514,10 +5849,8 @@ export default function DesignEditor() {
5514
5849
  effort: promptState.effort,
5515
5850
  attempt,
5516
5851
  runTabId,
5517
- startedAt: Date.now(),
5852
+ startedAt,
5518
5853
  });
5519
- setHasPendingGeneration(true);
5520
- setRetryablePrompt(null);
5521
5854
  },
5522
5855
  [
5523
5856
  id,
@@ -5670,6 +6003,7 @@ export default function DesignEditor() {
5670
6003
 
5671
6004
  const handleDownloadPng = useCallback(
5672
6005
  async (settings?: Partial<ExportSettingsValue>) => {
6006
+ if (pngExportingRef.current) return;
5673
6007
  const iframe = document.querySelector<HTMLIFrameElement>(
5674
6008
  "iframe[data-design-preview-iframe]",
5675
6009
  );
@@ -5678,6 +6012,8 @@ export default function DesignEditor() {
5678
6012
  toast.error(t("designEditor.toasts.openScreenPng"));
5679
6013
  return;
5680
6014
  }
6015
+ pngExportingRef.current = true;
6016
+ setPngExporting(true);
5681
6017
  try {
5682
6018
  const html2canvas = (await import("html2canvas")).default;
5683
6019
  const width = Math.max(
@@ -5703,32 +6039,42 @@ export default function DesignEditor() {
5703
6039
  ),
5704
6040
  ),
5705
6041
  useCORS: true,
6042
+ foreignObjectRendering: true,
5706
6043
  backgroundColor: null,
6044
+ onclone: (clonedDocument) =>
6045
+ sanitizeHtml2CanvasClone(doc, clonedDocument),
5707
6046
  });
5708
- canvas.toBlob((blob) => {
5709
- try {
5710
- if (!blob) {
5711
- toast.error(t("designEditor.toasts.pngCreateError"));
5712
- return;
6047
+ await new Promise<void>((resolve) => {
6048
+ canvas.toBlob((blob) => {
6049
+ try {
6050
+ if (!blob) {
6051
+ toast.error(t("designEditor.toasts.pngCreateError"));
6052
+ return;
6053
+ }
6054
+ triggerBlobDownload(
6055
+ blob,
6056
+ fallbackExportName("png", settings?.suffix),
6057
+ );
6058
+ toast.success(t("designEditor.toasts.pngDownloaded"));
6059
+ } catch (callbackError) {
6060
+ // `triggerBlobDownload` does DOM mutation + `URL.createObjectURL`,
6061
+ // either of which can throw inside this async callback — outside
6062
+ // the outer try/catch. Surface the failure instead of silently
6063
+ // dropping it.
6064
+ console.error(
6065
+ "PNG export failed during download:",
6066
+ callbackError,
6067
+ );
6068
+ toast.error(
6069
+ callbackError instanceof Error
6070
+ ? callbackError.message
6071
+ : t("designEditor.toasts.pngSaveError"),
6072
+ );
6073
+ } finally {
6074
+ resolve();
5713
6075
  }
5714
- triggerBlobDownload(
5715
- blob,
5716
- fallbackExportName("png", settings?.suffix),
5717
- );
5718
- toast.success(t("designEditor.toasts.pngDownloaded"));
5719
- } catch (callbackError) {
5720
- // `triggerBlobDownload` does DOM mutation + `URL.createObjectURL`,
5721
- // either of which can throw inside this async callback — outside
5722
- // the outer try/catch. Surface the failure instead of silently
5723
- // dropping it.
5724
- console.error("PNG export failed during download:", callbackError);
5725
- toast.error(
5726
- callbackError instanceof Error
5727
- ? callbackError.message
5728
- : t("designEditor.toasts.pngSaveError"),
5729
- );
5730
- }
5731
- }, "image/png");
6076
+ }, "image/png");
6077
+ });
5732
6078
  } catch (error) {
5733
6079
  console.error("PNG export failed:", error);
5734
6080
  toast.error(
@@ -5736,6 +6082,9 @@ export default function DesignEditor() {
5736
6082
  ? error.message
5737
6083
  : t("designEditor.toasts.pngExportError"),
5738
6084
  );
6085
+ } finally {
6086
+ pngExportingRef.current = false;
6087
+ setPngExporting(false);
5739
6088
  }
5740
6089
  },
5741
6090
  [fallbackExportName, t, triggerBlobDownload],
@@ -5846,12 +6195,14 @@ ${serializedHtml}
5846
6195
  );
5847
6196
 
5848
6197
  const handleInspectorExport = useCallback(
5849
- (settings: ExportSettingsValue) => {
5850
- if (settings.format === "svg") {
5851
- void handleDownloadSvg(settings);
5852
- return;
6198
+ (settingsList: ExportSettingsValue[]) => {
6199
+ for (const settings of settingsList) {
6200
+ if (settings.format === "svg") {
6201
+ void handleDownloadSvg(settings);
6202
+ } else {
6203
+ void handleDownloadPng(settings);
6204
+ }
5853
6205
  }
5854
- void handleDownloadPng(settings);
5855
6206
  },
5856
6207
  [handleDownloadPng, handleDownloadSvg],
5857
6208
  );
@@ -5926,6 +6277,27 @@ ${serializedHtml}
5926
6277
  });
5927
6278
  return owners;
5928
6279
  }, [codeLayerModelsByFile]);
6280
+ const effectiveCodeLayerState = useMemo(() => {
6281
+ const state: EffectiveCodeLayerState = {
6282
+ lockedIds: new Set(),
6283
+ hiddenIds: new Set(),
6284
+ };
6285
+ codeLayerModelsByFile.forEach((model) => {
6286
+ const fileLocked = lockedLayerIds.has(model.fileId);
6287
+ const fileHidden = hiddenLayerIds.has(model.fileId);
6288
+ if (fileLocked) state.lockedIds.add(model.fileId);
6289
+ if (fileHidden) state.hiddenIds.add(model.fileId);
6290
+ collectEffectiveCodeLayerState(
6291
+ model.tree,
6292
+ lockedLayerIds,
6293
+ hiddenLayerIds,
6294
+ fileLocked,
6295
+ fileHidden,
6296
+ state,
6297
+ );
6298
+ });
6299
+ return state;
6300
+ }, [codeLayerModelsByFile, hiddenLayerIds, lockedLayerIds]);
5929
6301
  useEffect(() => {
5930
6302
  const fileIds = new Set(files.map((file) => file.id));
5931
6303
  const allCodeLayerNodes = codeLayerModelsByFile.flatMap(
@@ -6106,11 +6478,14 @@ ${serializedHtml}
6106
6478
  ];
6107
6479
  const activeFileBlocked =
6108
6480
  activeFile?.id &&
6109
- (lockedLayerIds.has(activeFile.id) || hiddenLayerIds.has(activeFile.id));
6481
+ (effectiveCodeLayerState.lockedIds.has(activeFile.id) ||
6482
+ effectiveCodeLayerState.hiddenIds.has(activeFile.id));
6110
6483
  const selectionBlocked =
6111
6484
  Boolean(activeFileBlocked) ||
6112
6485
  selectedPathIds.some(
6113
- (layerId) => lockedLayerIds.has(layerId) || hiddenLayerIds.has(layerId),
6486
+ (layerId) =>
6487
+ effectiveCodeLayerState.lockedIds.has(layerId) ||
6488
+ effectiveCodeLayerState.hiddenIds.has(layerId),
6114
6489
  );
6115
6490
  if (!selectionBlocked) return;
6116
6491
  setSelectedElement(null);
@@ -6121,8 +6496,7 @@ ${serializedHtml}
6121
6496
  activeCodeLayerTree,
6122
6497
  activeFile?.id,
6123
6498
  codeLayerOwnerByNodeId,
6124
- hiddenLayerIds,
6125
- lockedLayerIds,
6499
+ effectiveCodeLayerState,
6126
6500
  selectedElementLayerId,
6127
6501
  ]);
6128
6502
 
@@ -6191,19 +6565,44 @@ ${serializedHtml}
6191
6565
  activeFile?.id ??
6192
6566
  "";
6193
6567
  const activeLayerLocked = Boolean(
6194
- activeLayerId && lockedLayerIds.has(activeLayerId),
6568
+ activeLayerId && effectiveCodeLayerState.lockedIds.has(activeLayerId),
6195
6569
  );
6196
6570
  const activeLayerHidden = Boolean(
6197
- activeLayerId && hiddenLayerIds.has(activeLayerId),
6571
+ activeLayerId && effectiveCodeLayerState.hiddenIds.has(activeLayerId),
6572
+ );
6573
+
6574
+ const canMoveLayer = useCallback(
6575
+ (intent: LayersPanelMoveIntent) => {
6576
+ const targetOwner = codeLayerOwnerByNodeId.get(intent.targetId);
6577
+ if (
6578
+ !targetOwner ||
6579
+ effectiveCodeLayerState.lockedIds.has(intent.targetId) ||
6580
+ effectiveCodeLayerState.hiddenIds.has(intent.targetId)
6581
+ ) {
6582
+ return false;
6583
+ }
6584
+ return intent.draggedIds.some((draggedId) => {
6585
+ const draggedOwner = codeLayerOwnerByNodeId.get(draggedId);
6586
+ return (
6587
+ draggedId !== intent.targetId &&
6588
+ !!draggedOwner &&
6589
+ draggedOwner.fileId === targetOwner.fileId &&
6590
+ !effectiveCodeLayerState.lockedIds.has(draggedId) &&
6591
+ !effectiveCodeLayerState.hiddenIds.has(draggedId)
6592
+ );
6593
+ });
6594
+ },
6595
+ [codeLayerOwnerByNodeId, effectiveCodeLayerState],
6198
6596
  );
6199
6597
 
6200
6598
  const handleLayerMove = useCallback(
6201
6599
  (intent: LayersPanelMoveIntent) => {
6600
+ if (!canMoveLayer(intent)) return;
6202
6601
  const targetOwner = codeLayerOwnerByNodeId.get(intent.targetId);
6203
6602
  if (!targetOwner) return;
6204
6603
  if (
6205
- lockedLayerIds.has(intent.targetId) ||
6206
- hiddenLayerIds.has(intent.targetId)
6604
+ effectiveCodeLayerState.lockedIds.has(intent.targetId) ||
6605
+ effectiveCodeLayerState.hiddenIds.has(intent.targetId)
6207
6606
  ) {
6208
6607
  return;
6209
6608
  }
@@ -6221,8 +6620,8 @@ ${serializedHtml}
6221
6620
  draggedId === intent.targetId ||
6222
6621
  !draggedOwner ||
6223
6622
  draggedOwner.fileId !== targetOwner.fileId ||
6224
- lockedLayerIds.has(draggedId) ||
6225
- hiddenLayerIds.has(draggedId)
6623
+ effectiveCodeLayerState.lockedIds.has(draggedId) ||
6624
+ effectiveCodeLayerState.hiddenIds.has(draggedId)
6226
6625
  ) {
6227
6626
  continue;
6228
6627
  }
@@ -6250,10 +6649,10 @@ ${serializedHtml}
6250
6649
  activeContent,
6251
6650
  activeFile?.id,
6252
6651
  applyFileContentUpdate,
6652
+ canMoveLayer,
6253
6653
  codeLayerOwnerByNodeId,
6254
6654
  files,
6255
- hiddenLayerIds,
6256
- lockedLayerIds,
6655
+ effectiveCodeLayerState,
6257
6656
  t,
6258
6657
  ],
6259
6658
  );
@@ -6366,12 +6765,18 @@ ${serializedHtml}
6366
6765
 
6367
6766
  const handleToggleLayerLocked = useCallback(
6368
6767
  (layerId: string, locked: boolean) => {
6369
- setLockedLayerIds((current) => {
6370
- const next = new Set(current);
6371
- if (locked) next.add(layerId);
6372
- else next.delete(layerId);
6373
- return next;
6374
- });
6768
+ const applyLockedState = () => {
6769
+ setLockedLayerIds((current) => {
6770
+ const next = new Set(current);
6771
+ if (locked) next.add(layerId);
6772
+ else next.delete(layerId);
6773
+ return next;
6774
+ });
6775
+ };
6776
+ if (files.some((file) => file.id === layerId)) {
6777
+ applyLockedState();
6778
+ return;
6779
+ }
6375
6780
  const owner = codeLayerOwnerByNodeId.get(layerId);
6376
6781
  const node = owner?.node;
6377
6782
  if (!owner || !node) return;
@@ -6387,11 +6792,11 @@ ${serializedHtml}
6387
6792
  "data-agent-native-locked",
6388
6793
  locked ? "true" : null,
6389
6794
  );
6390
- if (nextContent && nextContent !== sourceContent) {
6391
- applyFileContentUpdate(owner.fileId, nextContent, {
6392
- refreshPreview: false,
6393
- });
6394
- }
6795
+ if (!nextContent || nextContent === sourceContent) return;
6796
+ applyFileContentUpdate(owner.fileId, nextContent, {
6797
+ refreshPreview: false,
6798
+ });
6799
+ applyLockedState();
6395
6800
  },
6396
6801
  [
6397
6802
  activeContent,
@@ -6404,12 +6809,18 @@ ${serializedHtml}
6404
6809
 
6405
6810
  const handleToggleLayerHidden = useCallback(
6406
6811
  (layerId: string, hidden: boolean) => {
6407
- setHiddenLayerIds((current) => {
6408
- const next = new Set(current);
6409
- if (hidden) next.add(layerId);
6410
- else next.delete(layerId);
6411
- return next;
6412
- });
6812
+ const applyHiddenState = () => {
6813
+ setHiddenLayerIds((current) => {
6814
+ const next = new Set(current);
6815
+ if (hidden) next.add(layerId);
6816
+ else next.delete(layerId);
6817
+ return next;
6818
+ });
6819
+ };
6820
+ if (files.some((file) => file.id === layerId)) {
6821
+ applyHiddenState();
6822
+ return;
6823
+ }
6413
6824
  const owner = codeLayerOwnerByNodeId.get(layerId);
6414
6825
  const node = owner?.node;
6415
6826
  if (!owner || !node) return;
@@ -6425,11 +6836,11 @@ ${serializedHtml}
6425
6836
  "data-agent-native-hidden",
6426
6837
  hidden ? "true" : null,
6427
6838
  );
6428
- if (nextContent && nextContent !== sourceContent) {
6429
- applyFileContentUpdate(owner.fileId, nextContent, {
6430
- refreshPreview: false,
6431
- });
6432
- }
6839
+ if (!nextContent || nextContent === sourceContent) return;
6840
+ applyFileContentUpdate(owner.fileId, nextContent, {
6841
+ refreshPreview: false,
6842
+ });
6843
+ applyHiddenState();
6433
6844
  },
6434
6845
  [
6435
6846
  activeContent,
@@ -6454,10 +6865,14 @@ ${serializedHtml}
6454
6865
 
6455
6866
  const zoomLabel = `${Math.round(zoom)}%`;
6456
6867
 
6457
- if (!id) {
6458
- navigate("/");
6459
- return null;
6460
- }
6868
+ // Hooks must not be called conditionally; keep navigate as an effect so the
6869
+ // render phase stays pure. This branch is unreachable in practice because the
6870
+ // design.$id.tsx route always supplies an id param.
6871
+ useEffect(() => {
6872
+ if (!id) navigate("/");
6873
+ }, [id, navigate]);
6874
+
6875
+ if (!id) return null;
6461
6876
 
6462
6877
  if (designLoading || (!design && pendingGenerationActive)) {
6463
6878
  return <DesignEditorSkeleton embedded={embedded} />;
@@ -6573,7 +6988,7 @@ ${serializedHtml}
6573
6988
  </DropdownMenuItem>
6574
6989
  <DropdownMenuItem
6575
6990
  onClick={() => void handleDownloadPng()}
6576
- disabled={!activeFile}
6991
+ disabled={!activeFile || pngExporting}
6577
6992
  >
6578
6993
  <IconPhoto className="mr-2 h-4 w-4" />
6579
6994
  {t("designEditor.downloadPng")}
@@ -6744,6 +7159,42 @@ ${serializedHtml}
6744
7159
  </DropdownMenu>
6745
7160
  );
6746
7161
 
7162
+ const signedOutPersistenceActions = (
7163
+ <>
7164
+ <Tooltip>
7165
+ <TooltipTrigger asChild>
7166
+ <Button
7167
+ variant="outline"
7168
+ size="sm"
7169
+ onClick={handleSignInToSave}
7170
+ className="h-8 max-w-[13rem] cursor-pointer gap-1.5 truncate rounded-md bg-[var(--design-editor-panel-raised-bg)] px-2 text-xs shadow-none"
7171
+ aria-label={t("designEditor.signUpToSaveDescription")}
7172
+ >
7173
+ <IconDeviceFloppy className="size-4 shrink-0" />
7174
+ <span className="truncate">{t("designEditor.signUpToSave")}</span>
7175
+ </Button>
7176
+ </TooltipTrigger>
7177
+ <TooltipContent>
7178
+ {t("designEditor.signUpToSaveDescription")}
7179
+ </TooltipContent>
7180
+ </Tooltip>
7181
+ <Tooltip>
7182
+ <TooltipTrigger asChild>
7183
+ <Button
7184
+ variant="default"
7185
+ size="sm"
7186
+ onClick={handleSignInToShare}
7187
+ className="h-8 cursor-pointer gap-1.5 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 text-sm !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)]"
7188
+ >
7189
+ <span>{t("designEditor.share")}</span>
7190
+ <IconArrowUpRight className="size-4 shrink-0" />
7191
+ </Button>
7192
+ </TooltipTrigger>
7193
+ <TooltipContent>{t("designEditor.signUpToShare")}</TooltipContent>
7194
+ </Tooltip>
7195
+ </>
7196
+ );
7197
+
6747
7198
  const rightSidebarActions = (
6748
7199
  <div className="shrink-0 border-b border-border bg-[var(--design-editor-panel-bg)] px-2 py-1.5">
6749
7200
  <div className="flex min-h-8 items-center justify-between gap-2">
@@ -6771,15 +7222,23 @@ ${serializedHtml}
6771
7222
  <TooltipContent>{t("designEditor.designPreview")}</TooltipContent>
6772
7223
  </Tooltip>
6773
7224
 
6774
- <ShareButton
6775
- resourceType="design"
6776
- resourceId={id}
6777
- resourceTitle={design.title}
6778
- hideTriggerIcon
6779
- triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 text-sm !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
6780
- />
7225
+ {isSignedIn ? (
7226
+ <ShareButton
7227
+ resourceType="design"
7228
+ resourceId={id}
7229
+ resourceTitle={design.title}
7230
+ hideTriggerIcon
7231
+ defaultOpen={shouldOpenShare}
7232
+ shareUrl={editorShareUrl}
7233
+ shareUrlLabel={t("designEditor.shareEditorLink")}
7234
+ shareUrlDescription={t("designEditor.shareEditorLinkDescription")}
7235
+ triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 text-sm !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
7236
+ />
7237
+ ) : (
7238
+ signedOutPersistenceActions
7239
+ )}
6781
7240
 
6782
- <AgentToggleButton />
7241
+ {isSignedIn && <AgentToggleButton />}
6783
7242
  </div>
6784
7243
  </div>
6785
7244
  </div>
@@ -6802,7 +7261,10 @@ ${serializedHtml}
6802
7261
  size="icon"
6803
7262
  className="size-8 cursor-pointer"
6804
7263
  onClick={() => {
6805
- window.parent.postMessage({ type: "design:close" }, "*");
7264
+ window.parent.postMessage(
7265
+ { type: "design:close" },
7266
+ parentOriginRef.current ?? window.location.origin,
7267
+ );
6806
7268
  }}
6807
7269
  >
6808
7270
  <IconX className="size-4" />
@@ -6856,7 +7318,7 @@ ${serializedHtml}
6856
7318
  </DropdownMenuItem>
6857
7319
  <DropdownMenuItem
6858
7320
  onClick={() => void handleDownloadPng()}
6859
- disabled={!activeFile}
7321
+ disabled={!activeFile || pngExporting}
6860
7322
  >
6861
7323
  <IconPhoto className="mr-2 h-4 w-4" />
6862
7324
  {t("designEditor.downloadPng")}
@@ -7125,7 +7587,7 @@ ${serializedHtml}
7125
7587
  </>
7126
7588
  )}
7127
7589
 
7128
- {!embedded && (
7590
+ {!embedded && isSignedIn && (
7129
7591
  <PresenceBar
7130
7592
  activeUsers={activeUsers}
7131
7593
  agentActive={agentActive}
@@ -7135,17 +7597,25 @@ ${serializedHtml}
7135
7597
  />
7136
7598
  )}
7137
7599
 
7138
- {!embedded && (
7600
+ {!embedded && isSignedIn ? (
7139
7601
  <ShareButton
7140
7602
  resourceType="design"
7141
7603
  resourceId={id}
7142
7604
  resourceTitle={design.title}
7143
7605
  hideTriggerIcon
7606
+ defaultOpen={shouldOpenShare}
7607
+ shareUrl={editorShareUrl}
7608
+ shareUrlLabel={t("designEditor.shareEditorLink")}
7609
+ shareUrlDescription={t(
7610
+ "designEditor.shareEditorLinkDescription",
7611
+ )}
7144
7612
  triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
7145
7613
  />
7146
- )}
7614
+ ) : !embedded ? (
7615
+ signedOutPersistenceActions
7616
+ ) : null}
7147
7617
 
7148
- {!embedded && <AgentToggleButton />}
7618
+ {!embedded && isSignedIn && <AgentToggleButton />}
7149
7619
  </div>
7150
7620
  </div>
7151
7621
  </header>
@@ -7198,6 +7668,7 @@ ${serializedHtml}
7198
7668
  onHoverLayer={handleLayerHover}
7199
7669
  onLeaveLayer={handleLayerLeave}
7200
7670
  onMoveLayer={handleLayerMove}
7671
+ canMoveLayer={canMoveLayer}
7201
7672
  />
7202
7673
  </div>
7203
7674
  <div
@@ -7282,11 +7753,13 @@ ${serializedHtml}
7282
7753
  )}
7283
7754
 
7284
7755
  {!embedded && activeFile && !pendingVariants && !questionFlowActive && (
7285
- <FigmaBottomToolbar
7756
+ <DesignBottomToolbar
7286
7757
  mode={mode}
7287
7758
  pinMode={pinMode}
7288
7759
  drawMode={drawMode}
7289
7760
  activeTool={activeTool}
7761
+ isOverview={viewMode === "overview"}
7762
+ hasActiveFile={Boolean(activeFile)}
7290
7763
  onMove={handleMoveTool}
7291
7764
  onFrame={handleFrameTool}
7292
7765
  onShape={handleShapeTool}
@@ -7622,7 +8095,7 @@ ${serializedHtml}
7622
8095
  onStyleChange={handleStyleChange}
7623
8096
  onStylesChange={handleStylesChange}
7624
8097
  onExport={handleInspectorExport}
7625
- exporting={svgExporting}
8098
+ exporting={pngExporting || svgExporting}
7626
8099
  />
7627
8100
  </div>
7628
8101
  ) : (
@@ -7648,7 +8121,7 @@ ${serializedHtml}
7648
8121
  type: "agentNative.submitChat",
7649
8122
  data: { message: prompt, submit: true },
7650
8123
  },
7651
- "*",
8124
+ parentOriginRef.current ?? window.location.origin,
7652
8125
  );
7653
8126
  handlePromptOpenChange(false);
7654
8127
  return;
@@ -7667,6 +8140,17 @@ ${serializedHtml}
7667
8140
  ].join("\n");
7668
8141
  clearGenerationCompleteTimer();
7669
8142
  setGenerationIssue(null);
8143
+ const startedAt = Date.now();
8144
+ patchPendingGeneration(id, {
8145
+ prompt,
8146
+ files,
8147
+ title: design.title,
8148
+ designSystemId,
8149
+ ...options,
8150
+ attempt: 1,
8151
+ startedAt,
8152
+ });
8153
+ setHasPendingGeneration(true);
7670
8154
  const runTabId = agentSubmit(
7671
8155
  `Prepare design questions for "${design.title}": ${prompt}`,
7672
8156
  context,
@@ -7681,9 +8165,8 @@ ${serializedHtml}
7681
8165
  ...options,
7682
8166
  runTabId,
7683
8167
  attempt: 1,
7684
- startedAt: Date.now(),
8168
+ startedAt,
7685
8169
  });
7686
- setHasPendingGeneration(true);
7687
8170
  handlePromptOpenChange(false);
7688
8171
  }}
7689
8172
  loading={generating}