@agent-native/core 0.80.4 → 0.80.6

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 (341) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +28 -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/create.ts +1 -0
  31. package/corpus/core/src/cli/index.ts +1 -1
  32. package/corpus/core/src/cli/mcp-config-writers.ts +187 -51
  33. package/corpus/core/src/cli/skills.ts +18 -3
  34. package/corpus/core/src/client/AssistantChat.tsx +11 -1
  35. package/corpus/core/src/client/agent-chat.ts +12 -1
  36. package/corpus/core/src/client/composer/PromptComposer.tsx +100 -41
  37. package/corpus/core/src/client/org/OrgSwitcher.tsx +1 -1
  38. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -27
  39. package/corpus/core/src/collab/presence.ts +1 -1
  40. package/corpus/core/src/mcp/connect-route.ts +34 -5
  41. package/corpus/core/src/server/action-discovery.ts +4 -0
  42. package/corpus/core/src/server/agent-chat-plugin.ts +20 -0
  43. package/corpus/core/src/sharing/registry.ts +17 -0
  44. package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
  45. package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
  46. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -0
  47. package/corpus/templates/analytics/actions/update-dashboard.ts +8 -0
  48. package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
  49. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +117 -33
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
  51. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -0
  52. package/corpus/templates/analytics/changelog/2026-06-27-dashboards-can-now-nest-under-a-parent-in-the-sidebar-via-a-.md +6 -0
  53. package/corpus/templates/analytics/changelog/2026-06-29-analytics-pages-now-sit-closer-under-the-header-with-tighter.md +6 -0
  54. package/corpus/templates/assets/actions/generate-image-batch.ts +3 -0
  55. package/corpus/templates/assets/actions/generate-image.ts +3 -0
  56. package/corpus/templates/assets/changelog/2026-06-29-longer-agent-generation-window.md +6 -0
  57. package/corpus/templates/assets/netlify.toml +3 -3
  58. package/corpus/templates/assets/server/plugins/agent-chat.ts +4 -1
  59. package/corpus/templates/clips/app/components/capture-install-options.tsx +7 -8
  60. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +1 -1
  61. package/corpus/templates/content/AGENTS.md +57 -44
  62. package/corpus/templates/content/README.md +16 -0
  63. package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
  64. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
  65. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
  66. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
  67. package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
  68. package/corpus/templates/content/actions/_database-utils.ts +17 -14
  69. package/corpus/templates/content/actions/_federation-join.ts +14 -2
  70. package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
  71. package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
  72. package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
  73. package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
  74. package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
  75. package/corpus/templates/content/actions/create-document.ts +8 -0
  76. package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
  77. package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
  78. package/corpus/templates/content/actions/get-content-database.ts +10 -2
  79. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
  80. package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
  81. package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
  82. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
  83. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
  84. package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
  85. package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
  86. package/corpus/templates/content/app/blocks/BuilderDocsBlocks.tsx +10 -0
  87. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -0
  88. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +185 -0
  89. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
  90. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -0
  91. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
  92. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +19 -0
  93. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
  94. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
  95. package/corpus/templates/content/app/components/editor/extensions/ContentReferenceNode.tsx +82 -0
  96. package/corpus/templates/content/app/global.css +9 -0
  97. package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
  98. package/corpus/templates/content/app/i18n/zh-TW.ts +76 -0
  99. package/corpus/templates/content/app/i18n-data.ts +924 -0
  100. package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
  101. package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
  102. package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
  103. package/corpus/templates/content/changelog/2026-06-29-reusable-mdx-references-now-preview-linked-local-documents-i.md +6 -0
  104. package/corpus/templates/content/shared/api.ts +91 -1
  105. package/corpus/templates/content/shared/builder-docs-blocks.ts +8 -0
  106. package/corpus/templates/content/shared/builder-mdx.ts +141 -4
  107. package/corpus/templates/content/shared/nfm.ts +62 -0
  108. package/corpus/templates/content/vite.config.ts +14 -7
  109. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +10 -11
  110. package/corpus/templates/design/AGENTS.md +5 -0
  111. package/corpus/templates/design/_gitignore +7 -0
  112. package/corpus/templates/design/actions/apply-visual-edit.ts +22 -12
  113. package/corpus/templates/design/actions/create-design-system.ts +15 -4
  114. package/corpus/templates/design/actions/create-file.ts +20 -1
  115. package/corpus/templates/design/actions/delete-design-system.ts +2 -2
  116. package/corpus/templates/design/actions/duplicate-design.ts +35 -9
  117. package/corpus/templates/design/actions/generate-design.ts +146 -69
  118. package/corpus/templates/design/actions/generate-screens.ts +51 -19
  119. package/corpus/templates/design/actions/get-design-snapshot.ts +5 -1
  120. package/corpus/templates/design/actions/get-design.ts +2 -0
  121. package/corpus/templates/design/actions/import-design-project.ts +28 -9
  122. package/corpus/templates/design/actions/import-document.ts +22 -8
  123. package/corpus/templates/design/actions/index-design-system-with-builder.ts +6 -153
  124. package/corpus/templates/design/actions/navigate.ts +105 -54
  125. package/corpus/templates/design/actions/set-default-design-system.ts +17 -3
  126. package/corpus/templates/design/actions/show-design-questions.ts +2 -0
  127. package/corpus/templates/design/actions/update-design.ts +2 -1
  128. package/corpus/templates/design/actions/view-screen.ts +12 -6
  129. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +4 -4
  130. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +159 -41
  131. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +8 -4
  132. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +37 -42
  133. package/corpus/templates/design/app/components/design/EditPanel.tsx +336 -183
  134. package/corpus/templates/design/app/components/design/LayersPanel.tsx +153 -34
  135. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +135 -33
  136. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +6 -0
  137. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
  138. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +4 -10
  139. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +84 -43
  140. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +4 -4
  141. package/corpus/templates/design/app/components/design/inspector/{FigmaColorPicker.tsx → DesignColorPicker.tsx} +89 -62
  142. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +14 -26
  143. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +40 -12
  144. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -4
  145. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +13 -13
  146. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +13 -9
  147. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +13 -4
  148. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +6 -6
  149. package/corpus/templates/design/app/components/design/inspector/{figma-icons.tsx → design-icons.tsx} +2 -2
  150. package/corpus/templates/design/app/components/design/inspector/index.ts +12 -12
  151. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +5 -2
  152. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +3 -3
  153. package/corpus/templates/design/app/components/layout/Layout.tsx +5 -2
  154. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  155. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +167 -91
  156. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +16 -3
  157. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +15 -0
  158. package/corpus/templates/design/app/global.css +4 -0
  159. package/corpus/templates/design/app/hooks/use-design-system.ts +4 -1
  160. package/corpus/templates/design/app/hooks/use-navigation-state.ts +38 -3
  161. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +9 -2
  162. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  163. package/corpus/templates/design/app/i18n-data.ts +527 -0
  164. package/corpus/templates/design/app/lib/design-systems.ts +6 -4
  165. package/corpus/templates/design/app/lib/design-templates.ts +15 -3
  166. package/corpus/templates/design/app/pages/DesignEditor.tsx +641 -158
  167. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +42 -34
  168. package/corpus/templates/design/app/pages/VisualEdit.tsx +100 -0
  169. package/corpus/templates/design/app/root.tsx +42 -9
  170. package/corpus/templates/design/app/routes/visual-edit.tsx +7 -0
  171. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +1 -1
  172. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +1 -1
  173. package/corpus/templates/design/changelog/2026-06-28-visual-code-design-mode.md +6 -0
  174. package/corpus/templates/design/changelog/2026-06-29-design-generation-prompts-now-show-connect-ai-before-sending.md +6 -0
  175. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +1 -1
  176. package/corpus/templates/design/changelog/2026-06-29-fixed-design-editor-exports-and-gradient-edits-so-multi-scre.md +6 -0
  177. package/corpus/templates/design/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
  178. package/corpus/templates/design/changelog/2026-06-29-png-export-handles-modern-css-color-functions-more-reliably.md +6 -0
  179. package/corpus/templates/design/changelog/2026-06-29-png-export-now-ignores-duplicate-clicks-while-a-download-is-.md +6 -0
  180. package/corpus/templates/design/changelog/2026-06-29-public-visual-edit-and-shared-design-links-now-invite-visito.md +6 -0
  181. package/corpus/templates/design/changelog/2026-06-29-visual-edit-actions-now-reject-missing-design-targets-before.md +6 -0
  182. package/corpus/templates/design/e2e/README.md +71 -0
  183. package/corpus/templates/design/e2e/global-setup.ts +157 -0
  184. package/corpus/templates/design/e2e/helpers.ts +148 -0
  185. package/corpus/templates/design/netlify.toml +3 -4
  186. package/corpus/templates/design/package.json +5 -1
  187. package/corpus/templates/design/playwright.config.ts +51 -0
  188. package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +121 -0
  189. package/corpus/templates/design/server/handlers/uploads.ts +89 -38
  190. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +181 -0
  191. package/corpus/templates/design/server/lib/coding-handoff.ts +33 -4
  192. package/corpus/templates/design/server/lib/design-export.ts +35 -8
  193. package/corpus/templates/design/server/plugins/agent-chat.ts +4 -0
  194. package/corpus/templates/design/server/plugins/auth.ts +10 -1
  195. package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +1 -0
  196. package/corpus/templates/design/shared/api.ts +2 -0
  197. package/corpus/templates/design/shared/apply-edits.ts +12 -8
  198. package/corpus/templates/design/shared/canvas-math.ts +19 -1
  199. package/corpus/templates/design/shared/code-layer.ts +87 -28
  200. package/corpus/templates/design/shared/generation-session.ts +1 -0
  201. package/corpus/templates/design/shared/resolve-tweaks.ts +4 -3
  202. package/corpus/templates/design/shared/shader-presets.ts +10 -0
  203. package/corpus/templates/design/shared/source-mode.ts +9 -0
  204. package/corpus/templates/design/vitest.config.ts +2 -1
  205. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -1
  206. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +1 -1
  207. package/corpus/templates/plan/AGENTS.md +1 -1
  208. package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
  209. package/corpus/templates/plan/actions/validate-local-plan-source.ts +2 -10
  210. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -0
  211. package/corpus/templates/plan/docs/commenting-ux-plan.md +2 -2
  212. package/corpus/templates/plan/server/plan-content.ts +1 -1
  213. package/corpus/templates/plan/server/ui-plan-html.ts +2 -2
  214. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +6 -153
  215. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +53 -38
  216. package/corpus/templates/slides/app/components/design-system/{fig-import-response.ts → builder-index-response.ts} +9 -6
  217. package/corpus/templates/slides/app/components/visual-editor/MultiSelectChip.tsx +1 -1
  218. package/corpus/templates/slides/changelog/2026-06-29-design-system-setup-now-indexes-uploaded-design-files-throug.md +6 -0
  219. package/corpus/templates/slides/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
  220. package/corpus/templates/slides/netlify.toml +3 -6
  221. package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +116 -0
  222. package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +179 -0
  223. package/corpus/templates/slides/server/plugins/agent-chat.ts +4 -1
  224. package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +1 -0
  225. package/dist/action.d.ts +15 -0
  226. package/dist/action.d.ts.map +1 -1
  227. package/dist/action.js +6 -0
  228. package/dist/action.js.map +1 -1
  229. package/dist/agent/durable-background.d.ts +9 -7
  230. package/dist/agent/durable-background.d.ts.map +1 -1
  231. package/dist/agent/durable-background.js +27 -11
  232. package/dist/agent/durable-background.js.map +1 -1
  233. package/dist/agent/production-agent.d.ts +7 -0
  234. package/dist/agent/production-agent.d.ts.map +1 -1
  235. package/dist/agent/production-agent.js +4 -1
  236. package/dist/agent/production-agent.js.map +1 -1
  237. package/dist/agent/run-loop-with-resume.d.ts +2 -1
  238. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  239. package/dist/agent/run-loop-with-resume.js +2 -2
  240. package/dist/agent/run-loop-with-resume.js.map +1 -1
  241. package/dist/brand-kit/fig/index.d.ts +4 -4
  242. package/dist/brand-kit/fig/index.d.ts.map +1 -1
  243. package/dist/brand-kit/fig/index.js +14 -27
  244. package/dist/brand-kit/fig/index.js.map +1 -1
  245. package/dist/cli/create.d.ts.map +1 -1
  246. package/dist/cli/create.js +1 -0
  247. package/dist/cli/create.js.map +1 -1
  248. package/dist/cli/index.js +1 -1
  249. package/dist/cli/index.js.map +1 -1
  250. package/dist/cli/mcp-config-writers.d.ts +15 -7
  251. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  252. package/dist/cli/mcp-config-writers.js +198 -45
  253. package/dist/cli/mcp-config-writers.js.map +1 -1
  254. package/dist/cli/skills.d.ts +1 -1
  255. package/dist/cli/skills.d.ts.map +1 -1
  256. package/dist/cli/skills.js +18 -3
  257. package/dist/cli/skills.js.map +1 -1
  258. package/dist/client/AssistantChat.d.ts.map +1 -1
  259. package/dist/client/AssistantChat.js +11 -1
  260. package/dist/client/AssistantChat.js.map +1 -1
  261. package/dist/client/agent-chat.d.ts.map +1 -1
  262. package/dist/client/agent-chat.js +15 -2
  263. package/dist/client/agent-chat.js.map +1 -1
  264. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  265. package/dist/client/composer/PromptComposer.js +35 -1
  266. package/dist/client/composer/PromptComposer.js.map +1 -1
  267. package/dist/client/org/OrgSwitcher.js +1 -1
  268. package/dist/client/org/OrgSwitcher.js.map +1 -1
  269. package/dist/client/sharing/ShareButton.d.ts +1 -1
  270. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  271. package/dist/client/sharing/ShareButton.js +2 -15
  272. package/dist/client/sharing/ShareButton.js.map +1 -1
  273. package/dist/collab/presence.d.ts +1 -1
  274. package/dist/collab/presence.js +1 -1
  275. package/dist/collab/presence.js.map +1 -1
  276. package/dist/collab/routes.d.ts +1 -1
  277. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  278. package/dist/mcp/connect-route.d.ts.map +1 -1
  279. package/dist/mcp/connect-route.js +34 -5
  280. package/dist/mcp/connect-route.js.map +1 -1
  281. package/dist/notifications/routes.d.ts +2 -2
  282. package/dist/observability/routes.d.ts +8 -8
  283. package/dist/resources/handlers.d.ts +1 -1
  284. package/dist/server/action-discovery.d.ts.map +1 -1
  285. package/dist/server/action-discovery.js +5 -0
  286. package/dist/server/action-discovery.js.map +1 -1
  287. package/dist/server/agent-chat-plugin.d.ts +6 -0
  288. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  289. package/dist/server/agent-chat-plugin.js +12 -3
  290. package/dist/server/agent-chat-plugin.js.map +1 -1
  291. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  292. package/dist/sharing/registry.d.ts.map +1 -1
  293. package/dist/sharing/registry.js +11 -0
  294. package/dist/sharing/registry.js.map +1 -1
  295. package/dist/templates/default/pnpm-workspace.yaml +1 -0
  296. package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
  297. package/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
  298. package/docs/content/locales/ar-SA/template-design.mdx +1 -1
  299. package/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
  300. package/docs/content/locales/de-DE/template-design.mdx +1 -1
  301. package/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
  302. package/docs/content/locales/es-ES/template-design.mdx +1 -1
  303. package/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
  304. package/docs/content/locales/fr-FR/template-design.mdx +1 -1
  305. package/docs/content/locales/hi-IN/template-design.mdx +1 -1
  306. package/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
  307. package/docs/content/locales/ja-JP/template-design.mdx +1 -1
  308. package/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
  309. package/docs/content/locales/ko-KR/template-design.mdx +1 -1
  310. package/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
  311. package/docs/content/locales/pt-BR/template-design.mdx +1 -1
  312. package/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
  313. package/docs/content/locales/zh-CN/template-design.mdx +1 -1
  314. package/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
  315. package/docs/content/locales/zh-TW/template-design.mdx +1 -1
  316. package/docs/content/real-time-collaboration.mdx +2 -2
  317. package/docs/content/template-design.mdx +1 -1
  318. package/package.json +3 -7
  319. package/src/templates/default/pnpm-workspace.yaml +1 -0
  320. package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
  321. package/corpus/core/src/brand-kit/fig/decode.ts +0 -410
  322. package/corpus/core/src/brand-kit/fig/extract-design-system.ts +0 -947
  323. package/corpus/core/src/brand-kit/fig/fig-to-html.ts +0 -1906
  324. package/corpus/templates/design/actions/import-figma.ts +0 -55
  325. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +0 -6
  326. package/corpus/templates/design/server/handlers/import-figma-system.ts +0 -86
  327. package/corpus/templates/design/server/routes/api/import-figma-system.post.ts +0 -1
  328. package/corpus/templates/slides/server/handlers/import-figma-system.ts +0 -101
  329. package/corpus/templates/slides/server/routes/api/import-figma-system.post.ts +0 -1
  330. package/dist/brand-kit/fig/decode.d.ts +0 -33
  331. package/dist/brand-kit/fig/decode.d.ts.map +0 -1
  332. package/dist/brand-kit/fig/decode.js +0 -358
  333. package/dist/brand-kit/fig/decode.js.map +0 -1
  334. package/dist/brand-kit/fig/extract-design-system.d.ts +0 -44
  335. package/dist/brand-kit/fig/extract-design-system.d.ts.map +0 -1
  336. package/dist/brand-kit/fig/extract-design-system.js +0 -752
  337. package/dist/brand-kit/fig/extract-design-system.js.map +0 -1
  338. package/dist/brand-kit/fig/fig-to-html.d.ts +0 -246
  339. package/dist/brand-kit/fig/fig-to-html.d.ts.map +0 -1
  340. package/dist/brand-kit/fig/fig-to-html.js +0 -1506
  341. package/dist/brand-kit/fig/fig-to-html.js.map +0 -1
@@ -91,7 +91,7 @@ import {
91
91
  AutoLayoutMatrix,
92
92
  ConstraintsWidget,
93
93
  ExportSettingsPanel,
94
- FigmaColorPicker,
94
+ DesignColorPicker,
95
95
  ScrubInput,
96
96
  SizingField,
97
97
  type AlignmentMatrixValue,
@@ -100,13 +100,13 @@ import {
100
100
  type AutoLayoutSizingAxis,
101
101
  type ConstraintsValue,
102
102
  type ExportSettingsValue,
103
- type FigmaFillRow,
104
- type FigmaFillRowPatch,
105
- type FigmaGradientStop,
106
- type FigmaGradientStopPatch,
107
- type FigmaGradientType,
103
+ type DesignFillRow,
104
+ type DesignFillRowPatch,
105
+ type DesignGradientStop,
106
+ type DesignGradientStopPatch,
107
+ type DesignGradientType,
108
108
  } from "./inspector";
109
- import type { FigmaPaintType } from "./inspector/FigmaColorPicker";
109
+ import type { DesignPaintType } from "./inspector/DesignColorPicker";
110
110
  import { TweaksPanelContent } from "./TweaksPanel";
111
111
  import type { ElementInfo } from "./types";
112
112
 
@@ -127,7 +127,7 @@ interface EditPanelProps {
127
127
  onRequestTweaks?: (anchor: HTMLElement) => void;
128
128
  onStyleChange: (property: string, value: string) => void;
129
129
  onStylesChange?: (styles: Record<string, string>) => void;
130
- onExport?: (settings: ExportSettingsValue) => void;
130
+ onExport?: (settings: ExportSettingsValue[]) => void;
131
131
  exporting?: boolean;
132
132
  }
133
133
 
@@ -175,7 +175,10 @@ function PropInput({
175
175
  }, [value]);
176
176
 
177
177
  const commit = () => {
178
- if (defaultUnit === undefined) return;
178
+ if (defaultUnit === undefined) {
179
+ if (draft !== value) onChange(draft);
180
+ return;
181
+ }
179
182
  const next = normalizeLengthValue(draft, defaultUnit);
180
183
  if (next !== draft) setDraft(next);
181
184
  if (next !== value) onChange(next);
@@ -210,7 +213,7 @@ function PropInput({
210
213
  );
211
214
  }
212
215
 
213
- /** Compact color input: label + Figma-style picker popover. */
216
+ /** Compact color input: label + design-editor picker popover. */
214
217
  function ColorInput({
215
218
  label,
216
219
  value,
@@ -221,6 +224,7 @@ function ColorInput({
221
224
  onBlendModeChange,
222
225
  supportsLayeredFills = false,
223
226
  documentColors,
227
+ pickerKey,
224
228
  }: {
225
229
  label: string;
226
230
  value: string;
@@ -232,6 +236,7 @@ function ColorInput({
232
236
  supportsLayeredFills?: boolean;
233
237
  /** Hex strings already in use on the page — forwarded to the color picker swatch grid. */
234
238
  documentColors?: string[];
239
+ pickerKey?: string;
235
240
  }) {
236
241
  const [draft, setDraft] = useState(value);
237
242
  const [selectedFillId, setSelectedFillId] = useState(SOLID_FILL_ID);
@@ -333,7 +338,7 @@ function ColorInput({
333
338
  )
334
339
  : undefined;
335
340
 
336
- const handleFillChange = (id: string, patch: FigmaFillRowPatch) => {
341
+ const handleFillChange = (id: string, patch: DesignFillRowPatch) => {
337
342
  if (id === SOLID_FILL_ID) {
338
343
  if (patch.value !== undefined) setNext(patch.value);
339
344
  if (patch.opacity !== undefined) {
@@ -361,6 +366,7 @@ function ColorInput({
361
366
  ...stop,
362
367
  opacity: patch.opacity,
363
368
  })),
369
+ gradient.prefix,
364
370
  ),
365
371
  );
366
372
  };
@@ -387,7 +393,7 @@ function ColorInput({
387
393
 
388
394
  const handleGradientTypeChange =
389
395
  activeGradient && activeGradientIndex !== null
390
- ? (type: FigmaGradientType) => {
396
+ ? (type: DesignGradientType) => {
391
397
  replaceBackgroundLayer(
392
398
  activeGradientIndex,
393
399
  buildGradientLayer(type, activeGradient.stops),
@@ -397,13 +403,17 @@ function ColorInput({
397
403
 
398
404
  const handleGradientStopChange =
399
405
  activeGradient && activeGradientIndex !== null
400
- ? (id: string, patch: FigmaGradientStopPatch) => {
406
+ ? (id: string, patch: DesignGradientStopPatch) => {
401
407
  const nextStops = activeGradient.stops.map((stop) =>
402
408
  stop.id === id ? { ...stop, ...patch } : stop,
403
409
  );
404
410
  replaceBackgroundLayer(
405
411
  activeGradientIndex,
406
- buildGradientLayer(activeGradient.type, nextStops),
412
+ buildGradientLayer(
413
+ activeGradient.type,
414
+ nextStops,
415
+ activeGradient.prefix,
416
+ ),
407
417
  );
408
418
  }
409
419
  : undefined;
@@ -411,7 +421,7 @@ function ColorInput({
411
421
  const handleAddGradientStop = onBackgroundImageChange
412
422
  ? () => {
413
423
  if (activeGradient && activeGradientIndex !== null) {
414
- const nextStop: FigmaGradientStop = {
424
+ const nextStop: DesignGradientStop = {
415
425
  id: `stop-${activeGradient.stops.length}`,
416
426
  color: draft || "#000000",
417
427
  position: 50,
@@ -419,10 +429,11 @@ function ColorInput({
419
429
  };
420
430
  replaceBackgroundLayer(
421
431
  activeGradientIndex,
422
- buildGradientLayer(activeGradient.type, [
423
- ...activeGradient.stops,
424
- nextStop,
425
- ]),
432
+ buildGradientLayer(
433
+ activeGradient.type,
434
+ [...activeGradient.stops, nextStop],
435
+ activeGradient.prefix,
436
+ ),
426
437
  );
427
438
  setSelectedStopId(nextStop.id);
428
439
  return;
@@ -448,13 +459,17 @@ function ColorInput({
448
459
  );
449
460
  replaceBackgroundLayer(
450
461
  activeGradientIndex,
451
- buildGradientLayer(activeGradient.type, nextStops),
462
+ buildGradientLayer(
463
+ activeGradient.type,
464
+ nextStops,
465
+ activeGradient.prefix,
466
+ ),
452
467
  );
453
468
  setSelectedStopId(nextStops[0]?.id);
454
469
  }
455
470
  : undefined;
456
471
 
457
- const selectedPaintType: FigmaPaintType =
472
+ const selectedPaintType: DesignPaintType =
458
473
  selectedFillId !== SOLID_FILL_ID
459
474
  ? selectedGradient
460
475
  ? selectedGradient.type
@@ -466,8 +481,7 @@ function ColorInput({
466
481
  selectedLayerIndex !== null
467
482
  ? (backgroundLayers[selectedLayerIndex] ?? draft ?? value ?? "#000000")
468
483
  : draft || "#000000";
469
-
470
- const handlePaintTypeChange = (type: FigmaPaintType) => {
484
+ const handlePaintTypeChange = (type: DesignPaintType) => {
471
485
  const selectedLayer = fillLayerIndex(selectedFillId);
472
486
  if (type === "solid") {
473
487
  if (selectedLayer !== null) removeBackgroundLayer(selectedLayer);
@@ -493,7 +507,7 @@ function ColorInput({
493
507
  ) {
494
508
  return;
495
509
  }
496
- const nextType: FigmaGradientType = type;
510
+ const nextType: DesignGradientType = type;
497
511
  const layerIndex = selectedLayer ?? activeGradientIndex;
498
512
  if (layerIndex !== null) {
499
513
  const currentGradient = parseGradientLayer(
@@ -518,7 +532,8 @@ function ColorInput({
518
532
  };
519
533
 
520
534
  return (
521
- <FigmaColorPicker
535
+ <DesignColorPicker
536
+ key={pickerKey}
522
537
  label={label}
523
538
  value={pickerValue}
524
539
  onChange={setNext}
@@ -555,8 +570,25 @@ const SOLID_FILL_ID = "solid";
555
570
  const FILL_LAYER_PREFIX = "layer:";
556
571
 
557
572
  interface ParsedGradientLayer {
558
- type: FigmaGradientType;
559
- stops: FigmaGradientStop[];
573
+ type: DesignGradientType;
574
+ prefix?: string;
575
+ stops: DesignGradientStop[];
576
+ }
577
+
578
+ const DEFAULT_EXPORT_SETTINGS: ExportSettingsValue = {
579
+ scale: 1,
580
+ format: "png",
581
+ suffix: "",
582
+ };
583
+
584
+ function elementIdentityKey(element: ElementInfo): string {
585
+ return [
586
+ element.sourceId ?? element.id ?? element.selector ?? element.tagName,
587
+ Math.round(element.boundingRect.x),
588
+ Math.round(element.boundingRect.y),
589
+ Math.round(element.boundingRect.width),
590
+ Math.round(element.boundingRect.height),
591
+ ].join(":");
560
592
  }
561
593
 
562
594
  function fillLayerId(index: number): string {
@@ -573,9 +605,9 @@ function buildFillRows(
573
605
  colorValue: string,
574
606
  backgroundLayers: string[],
575
607
  selectedFillId: string,
576
- ): FigmaFillRow[] {
608
+ ): DesignFillRow[] {
577
609
  const solid = parseCssColor(colorValue);
578
- const rows: FigmaFillRow[] = [
610
+ const rows: DesignFillRow[] = [
579
611
  {
580
612
  id: SOLID_FILL_ID,
581
613
  label: "Solid", // i18n-ignore inspector fallback label
@@ -605,7 +637,7 @@ function buildFillRows(
605
637
  return rows;
606
638
  }
607
639
 
608
- function averageGradientOpacity(stops: FigmaGradientStop[]): number {
640
+ function averageGradientOpacity(stops: DesignGradientStop[]): number {
609
641
  if (!stops.length) return 100;
610
642
  const total = stops.reduce((sum, stop) => {
611
643
  const parsed = parseCssColor(stop.color);
@@ -642,27 +674,28 @@ function joinCssLayers(layers: string[]): string {
642
674
  return cleaned.length ? cleaned.join(", ") : "none";
643
675
  }
644
676
 
645
- function parseGradientLayer(layer: string): ParsedGradientLayer | null {
677
+ export function parseGradientLayer(layer: string): ParsedGradientLayer | null {
646
678
  const match = layer.trim().match(/^(linear|radial|conic)-gradient\((.*)\)$/i);
647
679
  if (!match) return null;
648
680
 
649
681
  const parts = splitCssLayers(match[2] || "");
650
682
  const type = gradientTypeFromCss(match[1] || "", layer);
651
683
  const firstStop = parseGradientStop(parts[0] || "", 0, parts.length);
684
+ const prefix = firstStop ? undefined : parts[0]?.trim();
652
685
  const stopParts = firstStop ? parts : parts.slice(1);
653
686
  const stops = stopParts
654
687
  .map((part, index) => parseGradientStop(part, index, stopParts.length))
655
- .filter((stop): stop is FigmaGradientStop => Boolean(stop));
688
+ .filter((stop): stop is DesignGradientStop => Boolean(stop));
656
689
 
657
690
  if (!stops.length) return null;
658
- return { type, stops };
691
+ return { type, prefix, stops };
659
692
  }
660
693
 
661
694
  function parseGradientStop(
662
695
  part: string,
663
696
  index: number,
664
697
  total: number,
665
- ): FigmaGradientStop | null {
698
+ ): DesignGradientStop | null {
666
699
  const color = readLeadingColor(part);
667
700
  if (!color) return null;
668
701
  const parsed = parseCssColor(color.value);
@@ -690,7 +723,7 @@ function readLeadingColor(part: string): { raw: string; value: string } | null {
690
723
  if (transparent) {
691
724
  return { raw: transparent[0], value: "rgba(0, 0, 0, 0)" };
692
725
  }
693
- const functionName = trimmed.match(/^(rgb|rgba|hsl|hsla)\(/i);
726
+ const functionName = trimmed.match(/^[a-z][a-z0-9-]*\(/i);
694
727
  if (!functionName) return null;
695
728
  let depth = 0;
696
729
  for (let index = 0; index < trimmed.length; index += 1) {
@@ -710,29 +743,37 @@ function readLeadingColor(part: string): { raw: string; value: string } | null {
710
743
  function gradientTypeFromCss(
711
744
  functionName: string,
712
745
  layer: string,
713
- ): FigmaGradientType {
746
+ ): DesignGradientType {
714
747
  if (functionName.toLowerCase() === "conic") return "angular";
715
748
  if (/closest-corner/i.test(layer)) return "diamond";
716
749
  if (functionName.toLowerCase() === "radial") return "radial";
717
750
  return "linear";
718
751
  }
719
752
 
720
- function gradientLabel(type: FigmaGradientType): string {
753
+ function gradientLabel(type: DesignGradientType): string {
721
754
  if (type === "radial") {
722
- return "Radial gradient"; // i18n-ignore Figma inspector paint row
755
+ return "Radial gradient"; // i18n-ignore design inspector paint row
723
756
  }
724
757
  if (type === "angular") {
725
- return "Angular gradient"; // i18n-ignore Figma inspector paint row
758
+ return "Angular gradient"; // i18n-ignore design inspector paint row
726
759
  }
727
760
  if (type === "diamond") {
728
- return "Diamond gradient"; // i18n-ignore Figma inspector paint row
761
+ return "Diamond gradient"; // i18n-ignore design inspector paint row
729
762
  }
730
- return "Linear gradient"; // i18n-ignore Figma inspector paint row
763
+ return "Linear gradient"; // i18n-ignore design inspector paint row
731
764
  }
732
765
 
733
- function buildGradientLayer(
734
- type: FigmaGradientType,
735
- stops: FigmaGradientStop[],
766
+ function defaultGradientPrefix(type: DesignGradientType): string {
767
+ if (type === "radial") return "circle at 50% 50%";
768
+ if (type === "angular") return "from 0deg at 50% 50%";
769
+ if (type === "diamond") return "closest-corner at 50% 50%";
770
+ return "90deg";
771
+ }
772
+
773
+ export function buildGradientLayer(
774
+ type: DesignGradientType,
775
+ stops: DesignGradientStop[],
776
+ prefix = defaultGradientPrefix(type),
736
777
  ): string {
737
778
  const stopList = [...stops]
738
779
  .sort((a, b) => a.position - b.position)
@@ -746,19 +787,14 @@ function buildGradientLayer(
746
787
  })
747
788
  .join(", ");
748
789
 
749
- if (type === "radial") {
750
- return `radial-gradient(circle at 50% 50%, ${stopList})`;
751
- }
752
- if (type === "angular") {
753
- return `conic-gradient(from 0deg at 50% 50%, ${stopList})`;
790
+ if (type === "radial" || type === "diamond") {
791
+ return `radial-gradient(${prefix}, ${stopList})`;
754
792
  }
755
- if (type === "diamond") {
756
- return `radial-gradient(closest-corner at 50% 50%, ${stopList})`;
757
- }
758
- return `linear-gradient(90deg, ${stopList})`;
793
+ if (type === "angular") return `conic-gradient(${prefix}, ${stopList})`;
794
+ return `linear-gradient(${prefix}, ${stopList})`;
759
795
  }
760
796
 
761
- function defaultGradientStops(colorValue: string): FigmaGradientStop[] {
797
+ function defaultGradientStops(colorValue: string): DesignGradientStop[] {
762
798
  const parsed =
763
799
  parseCssColor(cssColorOrFallback(colorValue, "#000000")) ??
764
800
  parseCssColor("#000000");
@@ -773,7 +809,7 @@ function defaultGradientStops(colorValue: string): FigmaGradientStop[] {
773
809
  ];
774
810
  }
775
811
 
776
- function defaultGradientLayer(type: FigmaGradientType, colorValue: string) {
812
+ function defaultGradientLayer(type: DesignGradientType, colorValue: string) {
777
813
  return buildGradientLayer(type, defaultGradientStops(colorValue));
778
814
  }
779
815
 
@@ -857,14 +893,14 @@ function PropSlider({
857
893
  const EmptyScrubIcon = () => null;
858
894
 
859
895
  /**
860
- * Figma-style inspector section. Matches Figma's real "Design" panel chrome:
861
- * - NO left collapse chevron (Figma has none).
896
+ * design-editor inspector section. Matches the design editor "Design" panel chrome:
897
+ * - NO left collapse chevron (the design editor uses none).
862
898
  * - A thin divider line above each section.
863
899
  * - A bold left-aligned title.
864
900
  * - Right-aligned action icons (add layer, toggles, styles, etc.).
865
901
  *
866
- * The title is still clickable to collapse the body (Figma collapses sections
867
- * on title click) but renders no chevron glyph, just like Figma.
902
+ * The title is still clickable to collapse the body (design sections collapse
903
+ * on title click) but renders no chevron glyph, just the same way.
868
904
  */
869
905
  function PanelSection({
870
906
  title,
@@ -919,7 +955,7 @@ function SubsectionLabel({ children }: { children: ReactNode }) {
919
955
  );
920
956
  }
921
957
 
922
- function FigmaSpacingControl({
958
+ function DesignSpacingControl({
923
959
  label,
924
960
  values,
925
961
  onChange,
@@ -987,7 +1023,6 @@ function FigmaSpacingControl({
987
1023
  onChange={setAll}
988
1024
  unit="px"
989
1025
  min={0}
990
- precision={1}
991
1026
  labelClassName="w-16"
992
1027
  inputClassName="h-6"
993
1028
  />
@@ -1082,9 +1117,11 @@ const ALIGN_SELF_OPTIONS = [
1082
1117
  { value: "stretch", key: "stretch" },
1083
1118
  { value: "baseline", key: "baseline" },
1084
1119
  ] as const;
1120
+ // "center" stroke position is omitted: CSS has no native single-property
1121
+ // centered stroke; choosing it in the UI caused a confusing revert to "inside"
1122
+ // on next render. Inside (border) and outside (outline) are fully supported.
1085
1123
  const STROKE_POSITION_OPTIONS = [
1086
1124
  { value: "inside", key: "inside" },
1087
- { value: "center", key: "center" },
1088
1125
  { value: "outside", key: "outside" },
1089
1126
  ] as const;
1090
1127
  const BLEND_MODE_OPTIONS = [
@@ -1094,10 +1131,10 @@ const BLEND_MODE_OPTIONS = [
1094
1131
  { value: "overlay", label: "Overlay" },
1095
1132
  { value: "darken", label: "Darken" },
1096
1133
  { value: "lighten", label: "Lighten" },
1097
- { value: "color-dodge", label: "Color dodge" }, // i18n-ignore Figma blend mode label
1098
- { value: "color-burn", label: "Color burn" }, // i18n-ignore Figma blend mode label
1099
- { value: "hard-light", label: "Hard light" }, // i18n-ignore Figma blend mode label
1100
- { value: "soft-light", label: "Soft light" }, // i18n-ignore Figma blend mode label
1134
+ { value: "color-dodge", label: "Color dodge" }, // i18n-ignore design blend mode label
1135
+ { value: "color-burn", label: "Color burn" }, // i18n-ignore design blend mode label
1136
+ { value: "hard-light", label: "Hard light" }, // i18n-ignore design blend mode label
1137
+ { value: "soft-light", label: "Soft light" }, // i18n-ignore design blend mode label
1101
1138
  { value: "difference", label: "Difference" },
1102
1139
  { value: "exclusion", label: "Exclusion" },
1103
1140
  { value: "hue", label: "Hue" },
@@ -1110,11 +1147,47 @@ function parseNumericValue(value: string): number {
1110
1147
  return parseFloat(value) || 0;
1111
1148
  }
1112
1149
 
1150
+ /**
1151
+ * Resolve a CSS line-height value to a unitless ratio for display/editing.
1152
+ * When the browser returns a px-computed value (e.g. "19.2px" for line-height
1153
+ * 1.2 on a 16px font), divide by the font-size to recover the unitless ratio.
1154
+ * Falls back to 1.2 when the value cannot be parsed.
1155
+ */
1156
+ function resolveLineHeight(
1157
+ lineHeight: string | undefined,
1158
+ fontSize: string | undefined,
1159
+ ): number {
1160
+ const lh = lineHeight?.trim() || "";
1161
+ if (!lh || lh === "normal") return 1.2;
1162
+ if (lh.endsWith("px")) {
1163
+ const lhPx = parseFloat(lh);
1164
+ const fsPx = parseFloat(fontSize || "");
1165
+ if (Number.isFinite(lhPx) && Number.isFinite(fsPx) && fsPx > 0) {
1166
+ return Math.round((lhPx / fsPx) * 100) / 100;
1167
+ }
1168
+ }
1169
+ const numeric = parseFloat(lh);
1170
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : 1.2;
1171
+ }
1172
+
1113
1173
  function parseRotationValue(transform: string | undefined): number {
1114
1174
  const match = transform?.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
1115
1175
  return match ? Number(match[1]) : 0;
1116
1176
  }
1117
1177
 
1178
+ /**
1179
+ * Parse a CSS `scale` property value (e.g. "-1 1", "1", "none") into two
1180
+ * numeric components [scaleX, scaleY]. Defaults both axes to 1 when absent
1181
+ * or unparseable, matching the CSS initial value.
1182
+ */
1183
+ function parseScaleValue(value: string | undefined): [number, number] {
1184
+ if (!value || value === "none") return [1, 1];
1185
+ const parts = value.trim().split(/\s+/);
1186
+ const x = Number(parts[0]);
1187
+ const y = parts.length > 1 ? Number(parts[1]) : x;
1188
+ return [Number.isFinite(x) ? x : 1, Number.isFinite(y) ? y : 1];
1189
+ }
1190
+
1118
1191
  function mergeRotationValue(transform: string | undefined, degrees: number) {
1119
1192
  const nextRotate = `rotate(${Math.round(degrees * 10) / 10}deg)`;
1120
1193
  if (!transform || transform === "none") return nextRotate;
@@ -1124,6 +1197,26 @@ function mergeRotationValue(transform: string | undefined, degrees: number) {
1124
1197
  return `${transform} ${nextRotate}`;
1125
1198
  }
1126
1199
 
1200
+ /**
1201
+ * Replace or remove a translateX/translateY function within an existing
1202
+ * transform string while preserving all other transform functions (rotate,
1203
+ * scale, skew, etc.). Pass `null` as `value` to strip the function.
1204
+ */
1205
+ function mergeTranslateFunction(
1206
+ transform: string | undefined,
1207
+ axis: "X" | "Y",
1208
+ value: string | null,
1209
+ ): string {
1210
+ const pattern =
1211
+ axis === "X" ? /translateX\([^)]*\)/g : /translateY\([^)]*\)/g;
1212
+ const base = (!transform || transform === "none" ? "" : transform)
1213
+ .replace(pattern, "")
1214
+ .trim();
1215
+ if (value === null) return base || "none";
1216
+ const fn = `translate${axis}(${value})`;
1217
+ return base ? `${fn} ${base}` : fn;
1218
+ }
1219
+
1127
1220
  function ScrubStyleInput({
1128
1221
  label,
1129
1222
  value,
@@ -1307,9 +1400,9 @@ function autoLayoutAlignmentFromStyles(
1307
1400
  }
1308
1401
 
1309
1402
  /**
1310
- * Block-level container tags that act like Figma frames. Selecting any of
1403
+ * Block-level container tags that act the same way frames. Selecting any of
1311
1404
  * these shows the Auto layout section (in an "add" state when not yet flex),
1312
- * mirroring Figma where any frame/container exposes auto-layout controls.
1405
+ * mirroring the editor pattern where any frame/container exposes auto-layout controls.
1313
1406
  */
1314
1407
  const CONTAINER_TAGS = new Set([
1315
1408
  "div",
@@ -1355,7 +1448,7 @@ const LEAF_TAGS = new Set([
1355
1448
  * Whether the element should expose the Auto layout section. True for anything
1356
1449
  * already laid out with flexbox, or any block-level container tag that isn't a
1357
1450
  * known leaf/text element. This is what makes a plain frame/container with
1358
- * children show the full Auto layout section like Figma does.
1451
+ * children show the full Auto layout section the same way does.
1359
1452
  */
1360
1453
  function isContainerElement(element: ElementInfo): boolean {
1361
1454
  if (element.isFlexContainer) return true;
@@ -1386,7 +1479,7 @@ function isTextElement(element: ElementInfo): boolean {
1386
1479
  }
1387
1480
 
1388
1481
  /**
1389
- * Per-axis sizing availability following Figma's contextual rules:
1482
+ * Per-axis sizing availability following the design editor's contextual rules:
1390
1483
  * - Fixed: always.
1391
1484
  * - Hug contents: only CONTAINERS (flex/container frames) and TEXT can hug
1392
1485
  * their content. Leaves like img/svg/input cannot.
@@ -1597,12 +1690,12 @@ function SelectionHeader({ element }: { element: ElementInfo | null }) {
1597
1690
  {/* M3 · Right-aligned quick actions: create-component + dev inspect (</>) */}
1598
1691
  <div className="flex shrink-0 items-center gap-0.5">
1599
1692
  <SectionIconButton
1600
- label={"Create component" /* i18n-ignore Figma inspector action */}
1693
+ label={"Create component" /* i18n-ignore design inspector action */}
1601
1694
  >
1602
1695
  <IconComponents className="size-3.5" />
1603
1696
  </SectionIconButton>
1604
1697
  <SectionIconButton
1605
- label={"Inspect code" /* i18n-ignore Figma inspector action */}
1698
+ label={"Inspect code" /* i18n-ignore design inspector action */}
1606
1699
  >
1607
1700
  <IconCode className="size-3.5" />
1608
1701
  </SectionIconButton>
@@ -1633,7 +1726,7 @@ function InspectorTabsHeader({
1633
1726
  value="design"
1634
1727
  className="h-6 rounded-md px-1.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
1635
1728
  >
1636
- {"Design" /* i18n-ignore Figma inspector tab */}
1729
+ {"Design" /* i18n-ignore design inspector tab */}
1637
1730
  </TabsTrigger>
1638
1731
  <TabsTrigger
1639
1732
  value="tweaks"
@@ -1691,7 +1784,7 @@ function SectionIconButton({
1691
1784
  }
1692
1785
 
1693
1786
  /**
1694
- * Section-header toggle icon (Figma's right-aligned section actions, e.g. the
1787
+ * Section-header toggle icon (the design editor's right-aligned section actions, e.g. the
1695
1788
  * auto-layout ⊞ toggle). Highlights with the accent color when active.
1696
1789
  */
1697
1790
  function SectionIconToggle({
@@ -1922,10 +2015,7 @@ function StrokeLayerControl({
1922
2015
  const t = useT();
1923
2016
  const strokePositionOptions = STROKE_POSITION_OPTIONS.map((option) => ({
1924
2017
  value: option.value,
1925
- label:
1926
- option.key === "center"
1927
- ? t("editPanel.textAligns.center")
1928
- : t(`editPanel.labels.${option.key}`),
2018
+ label: t(`editPanel.labels.${option.key}`),
1929
2019
  }));
1930
2020
  const prefix = kind === "border" ? "border" : "outline";
1931
2021
  const position = kind === "border" ? "inside" : "outside";
@@ -1944,7 +2034,7 @@ function StrokeLayerControl({
1944
2034
 
1945
2035
  return (
1946
2036
  <div className="space-y-1.5">
1947
- {/* Figma stroke row: [swatch+hex trigger (flex-1)] [eye] [remove] */}
2037
+ {/* design stroke row: [swatch+hex trigger (flex-1)] [eye] [remove] */}
1948
2038
  <div className="group flex items-center gap-1.5">
1949
2039
  <div className="min-w-0 flex-1">
1950
2040
  <ColorInput
@@ -1986,7 +2076,7 @@ function StrokeLayerControl({
1986
2076
  <IconMinus className="size-3.5" />
1987
2077
  </SectionIconButton>
1988
2078
  </div>
1989
- {/* Figma stroke geometry: position + weight side by side */}
2079
+ {/* design stroke geometry: position + weight side by side */}
1990
2080
  <div className="grid grid-cols-2 gap-1.5">
1991
2081
  <Select value={position} onValueChange={movePosition}>
1992
2082
  <SelectTrigger className="h-6 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)]">
@@ -2105,7 +2195,7 @@ function serializeShadowLayers(layers: ShadowLayer[]) {
2105
2195
  `${Math.round(layer.x)}px`,
2106
2196
  `${Math.round(layer.y)}px`,
2107
2197
  `${Math.max(0, Math.round(layer.blur))}px`,
2108
- `${Math.max(0, Math.round(layer.spread))}px`,
2198
+ `${layer.inset ? Math.round(layer.spread) : Math.max(0, Math.round(layer.spread))}px`,
2109
2199
  layer.color,
2110
2200
  ]
2111
2201
  .filter(Boolean)
@@ -2133,7 +2223,7 @@ function ShadowEffectRow({
2133
2223
  const t = useT();
2134
2224
  return (
2135
2225
  <Popover>
2136
- {/* Figma effect row: [swatch+label+x,y,blur trigger (flex-1)] [remove] */}
2226
+ {/* design effect row: [swatch+label+x,y,blur trigger (flex-1)] [remove] */}
2137
2227
  <div className="group flex items-center gap-1.5">
2138
2228
  <PopoverTrigger asChild>
2139
2229
  <button
@@ -2216,9 +2306,11 @@ function ShadowEffectRow({
2216
2306
  <ScrubInput
2217
2307
  label={t("editPanel.labels.spread")}
2218
2308
  value={layer.spread}
2219
- onChange={(value) => onChange({ spread: Math.max(0, value) })}
2309
+ onChange={(value) =>
2310
+ onChange({ spread: layer.inset ? value : Math.max(0, value) })
2311
+ }
2220
2312
  unit="px"
2221
- min={0}
2313
+ min={layer.inset ? undefined : 0}
2222
2314
  precision={1}
2223
2315
  inputClassName="h-6"
2224
2316
  />
@@ -2304,7 +2396,7 @@ function TypographyProperties({
2304
2396
  }));
2305
2397
  const textAlign = styles.textAlign || "left";
2306
2398
 
2307
- // M1 · Text resizing mode (auto-width / auto-height / fixed). Figma's text
2399
+ // M1 · Text resizing mode (auto-width / auto-height / fixed). the design editor's text
2308
2400
  // nodes always expose this segment. Infer the current mode from the live CSS:
2309
2401
  // auto-width hugs both axes (width:auto + no wrapping), auto-height hugs the
2310
2402
  // height only (fixed width, content wraps), fixed pins both width and height.
@@ -2337,7 +2429,7 @@ function TypographyProperties({
2337
2429
  };
2338
2430
 
2339
2431
  // M2 · Vertical text alignment (top / middle / bottom). For auto-layout text
2340
- // containers (display:flex) Figma maps this to `justifyContent`; for normal
2432
+ // containers (display:flex) the design editor maps this to `justifyContent`; for normal
2341
2433
  // text we fall back to `verticalAlign`, which is what an inline/grid text box
2342
2434
  // honors. Read whichever the element currently expresses.
2343
2435
  const display = (styles.display || "").toLowerCase();
@@ -2373,21 +2465,21 @@ function TypographyProperties({
2373
2465
  {/* Row 0: text resizing (auto-width / auto-height / fixed) */}
2374
2466
  <InspectorSegment>
2375
2467
  <InspectorIconButton
2376
- label={"Auto width" /* i18n-ignore Figma text-resize mode */}
2468
+ label={"Auto width" /* i18n-ignore design text-resize mode */}
2377
2469
  active={resizeMode === "auto-width"}
2378
2470
  onClick={() => setResizeMode("auto-width")}
2379
2471
  >
2380
2472
  <IconArrowAutofitWidth className="size-3.5" />
2381
2473
  </InspectorIconButton>
2382
2474
  <InspectorIconButton
2383
- label={"Auto height" /* i18n-ignore Figma text-resize mode */}
2475
+ label={"Auto height" /* i18n-ignore design text-resize mode */}
2384
2476
  active={resizeMode === "auto-height"}
2385
2477
  onClick={() => setResizeMode("auto-height")}
2386
2478
  >
2387
2479
  <IconArrowAutofitHeight className="size-3.5" />
2388
2480
  </InspectorIconButton>
2389
2481
  <InspectorIconButton
2390
- label={"Fixed size" /* i18n-ignore Figma text-resize mode */}
2482
+ label={"Fixed size" /* i18n-ignore design text-resize mode */}
2391
2483
  active={resizeMode === "fixed"}
2392
2484
  onClick={() => setResizeMode("fixed")}
2393
2485
  >
@@ -2454,13 +2546,13 @@ function TypographyProperties({
2454
2546
  />
2455
2547
  </div>
2456
2548
 
2457
- {/* Row 3: line-height + letter-spacing with Figma-style leading icons */}
2549
+ {/* Row 3: line-height + letter-spacing with design-editor leading icons */}
2458
2550
  <div className="grid grid-cols-2 gap-1.5">
2459
2551
  <ScrubInput
2460
2552
  label={t("editPanel.labels.lineHeight")}
2461
2553
  ariaLabel={t("editPanel.labels.lineHeight")}
2462
2554
  icon={IconLineHeight}
2463
- value={parseNumericValue(styles.lineHeight || "1.2")}
2555
+ value={resolveLineHeight(styles.lineHeight, styles.fontSize)}
2464
2556
  onChange={(value) =>
2465
2557
  onStyleChange("lineHeight", String(Math.max(0.1, value)))
2466
2558
  }
@@ -2521,21 +2613,21 @@ function TypographyProperties({
2521
2613
  </InspectorSegment>
2522
2614
  <InspectorSegment>
2523
2615
  <InspectorIconButton
2524
- label={"Align top" /* i18n-ignore Figma vertical text align */}
2616
+ label={"Align top" /* i18n-ignore design vertical text align */}
2525
2617
  active={verticalAlign === "top"}
2526
2618
  onClick={() => setVerticalAlign("top")}
2527
2619
  >
2528
2620
  <IconLayoutAlignTop className="size-3.5" />
2529
2621
  </InspectorIconButton>
2530
2622
  <InspectorIconButton
2531
- label={"Align middle" /* i18n-ignore Figma vertical text align */}
2623
+ label={"Align middle" /* i18n-ignore design vertical text align */}
2532
2624
  active={verticalAlign === "middle"}
2533
2625
  onClick={() => setVerticalAlign("middle")}
2534
2626
  >
2535
2627
  <IconLayoutAlignMiddle className="size-3.5" />
2536
2628
  </InspectorIconButton>
2537
2629
  <InspectorIconButton
2538
- label={"Align bottom" /* i18n-ignore Figma vertical text align */}
2630
+ label={"Align bottom" /* i18n-ignore design vertical text align */}
2539
2631
  active={verticalAlign === "bottom"}
2540
2632
  onClick={() => setVerticalAlign("bottom")}
2541
2633
  >
@@ -2660,10 +2752,13 @@ function FlexContainerControls({
2660
2752
  onPaddingLinkedChange={(linked) => {
2661
2753
  setPaddingLinked(linked);
2662
2754
  if (!linked) return;
2663
- onStyleChange("paddingTop", `${padding.top}px`);
2664
- onStyleChange("paddingRight", `${padding.top}px`);
2665
- onStyleChange("paddingBottom", `${padding.top}px`);
2666
- onStyleChange("paddingLeft", `${padding.top}px`);
2755
+ const avg = Math.round(
2756
+ (padding.top + padding.right + padding.bottom + padding.left) / 4,
2757
+ );
2758
+ onStyleChange("paddingTop", `${avg}px`);
2759
+ onStyleChange("paddingRight", `${avg}px`);
2760
+ onStyleChange("paddingBottom", `${avg}px`);
2761
+ onStyleChange("paddingLeft", `${avg}px`);
2667
2762
  }}
2668
2763
  onClipContentChange={(clipContent) =>
2669
2764
  onStyleChange("overflow", clipContent ? "hidden" : "visible")
@@ -2821,11 +2916,11 @@ function LayoutContextProperties({
2821
2916
  );
2822
2917
 
2823
2918
  // Leaf elements (text, img, svg, etc.) never get auto layout — show the plain
2824
- // Figma W/H sizing block instead.
2919
+ // design W/H sizing block instead.
2825
2920
  if (!isContainer) {
2826
2921
  return (
2827
2922
  <PanelSection title={t("editPanel.sections.layout")}>
2828
- {/* Figma-style single-row-per-axis: [W | value | Fixed/Hug/Fill ▾] with
2923
+ {/* design-editor single-row-per-axis: [W | value | Fixed/Hug/Fill ▾] with
2829
2924
  the full sizing menu (modes + min/max + variable) per axis. */}
2830
2925
  <div className="grid grid-cols-2 items-start gap-1.5">
2831
2926
  <SizingField
@@ -2877,7 +2972,7 @@ function LayoutContextProperties({
2877
2972
  }
2878
2973
 
2879
2974
  // Any container element ALREADY has a layout in code — normal flow (block) by
2880
- // default, or flex when it uses flexbox. Figma never makes you "add" auto
2975
+ // default, or flex when it uses flexbox. the design editor never makes you "add" auto
2881
2976
  // layout for a frame, so we always render the full layout controls and let
2882
2977
  // the Flow control reflect/switch the element's current `display`. Choosing a
2883
2978
  // horizontal/vertical/wrap/grid flow applies `display:flex`; choosing the
@@ -2895,7 +2990,7 @@ function LayoutContextProperties({
2895
2990
  }
2896
2991
 
2897
2992
  /**
2898
- * Figma "Layout grid" / layout-guide section. Shown for frame/container
2993
+ * design layout-guide section. Shown for frame/container
2899
2994
  * elements. Renders an overlay column/row guide by applying a non-destructive
2900
2995
  * `backgroundImage` repeating gradient layer tagged so it can be toggled off
2901
2996
  * without disturbing real fills.
@@ -2903,7 +2998,7 @@ function LayoutContextProperties({
2903
2998
  const LAYOUT_GUIDE_MARKER = "/* an-layout-guide */";
2904
2999
 
2905
3000
  function hasLayoutGuide(styles: Record<string, string>): boolean {
2906
- return Boolean(styles.backgroundImage?.includes("repeating-linear-gradient"));
3001
+ return Boolean(styles.backgroundImage?.includes(LAYOUT_GUIDE_MARKER));
2907
3002
  }
2908
3003
 
2909
3004
  function LayoutGuideProperties({
@@ -2917,9 +3012,10 @@ function LayoutGuideProperties({
2917
3012
  const active = hasLayoutGuide(styles);
2918
3013
 
2919
3014
  const addGuide = () => {
2920
- // 12-column overlay guide — Figma's default columns layout grid.
2921
- const guide =
2922
- "repeating-linear-gradient(to right, color-mix(in srgb, var(--design-editor-accent-color) 22%, transparent) 0 1px, transparent 1px calc(100% / 12))";
3015
+ // 12-column overlay guide — the design editor's default columns layout grid.
3016
+ // The LAYOUT_GUIDE_MARKER comment is embedded so hasLayoutGuide and removeGuide
3017
+ // can detect/remove it without touching unrelated repeating-linear-gradient fills.
3018
+ const guide = `repeating-linear-gradient(to right, color-mix(in srgb, var(--design-editor-accent-color) 22%, transparent) 0 1px, transparent 1px calc(100% / 12)) ${LAYOUT_GUIDE_MARKER}`;
2923
3019
  const existing = compactCssValue(styles.backgroundImage, "");
2924
3020
  onStyleChange(
2925
3021
  "backgroundImage",
@@ -2929,7 +3025,7 @@ function LayoutGuideProperties({
2929
3025
 
2930
3026
  const removeGuide = () => {
2931
3027
  const layers = splitCssLayers(styles.backgroundImage || "").filter(
2932
- (layer) => !layer.includes("repeating-linear-gradient"),
3028
+ (layer) => !layer.includes(LAYOUT_GUIDE_MARKER),
2933
3029
  );
2934
3030
  onStyleChange(
2935
3031
  "backgroundImage",
@@ -2939,14 +3035,14 @@ function LayoutGuideProperties({
2939
3035
 
2940
3036
  return (
2941
3037
  <PanelSection
2942
- title={"Layout guide" /* i18n-ignore Figma inspector label */}
3038
+ title={"Layout guide" /* i18n-ignore design inspector label */}
2943
3039
  defaultCollapsed
2944
3040
  actions={
2945
3041
  <SectionIconButton
2946
3042
  label={
2947
3043
  active
2948
- ? "Remove layout guide" /* i18n-ignore Figma inspector action */
2949
- : "Add layout guide" /* i18n-ignore Figma inspector action */
3044
+ ? "Remove layout guide" /* i18n-ignore design inspector action */
3045
+ : "Add layout guide" /* i18n-ignore design inspector action */
2950
3046
  }
2951
3047
  onClick={active ? removeGuide : addGuide}
2952
3048
  >
@@ -2962,13 +3058,13 @@ function LayoutGuideProperties({
2962
3058
  <div className="flex items-center gap-2 rounded-md bg-[var(--design-editor-control-bg)] px-2 py-1.5 text-[11px] text-muted-foreground">
2963
3059
  <IconLayoutGrid className="size-3.5 shrink-0" />
2964
3060
  <span className="min-w-0 flex-1 truncate text-foreground">
2965
- {"Columns" /* i18n-ignore Figma inspector label */}
3061
+ {"Columns" /* i18n-ignore design inspector label */}
2966
3062
  </span>
2967
3063
  <span className="shrink-0 tabular-nums">12</span>
2968
3064
  </div>
2969
3065
  ) : (
2970
3066
  <p className="text-[11px] text-muted-foreground">
2971
- {"No layout guides" /* i18n-ignore Figma inspector empty state */}
3067
+ {"No layout guides" /* i18n-ignore design inspector empty state */}
2972
3068
  </p>
2973
3069
  )}
2974
3070
  </PanelSection>
@@ -2976,7 +3072,7 @@ function LayoutGuideProperties({
2976
3072
  }
2977
3073
 
2978
3074
  /**
2979
- * Togglable export preview thumbnail (Figma shows a small preview of the export
3075
+ * Togglable export preview thumbnail (the design editor shows a small preview of the export
2980
3076
  * frame above the export rows). Renders a proportional placeholder reflecting
2981
3077
  * the selected element's aspect ratio, fill, radius and dimensions.
2982
3078
  */
@@ -3026,36 +3122,56 @@ function PositionLayoutProperties({
3026
3122
  const styles = element.computedStyles;
3027
3123
  const constrainedPosition =
3028
3124
  styles.position === "absolute" || styles.position === "fixed";
3029
- // Reflect the active packing in the alignment segments (Figma highlights the
3125
+ // Reflect the active packing in the alignment segments (the design editor highlights the
3030
3126
  // current alignment). For a flex container the main axis is justifyContent.
3031
3127
  const alignH = justifyToHorizontal(styles.justifyContent);
3032
3128
  const alignV = alignToVertical(styles.alignItems);
3033
3129
  const constraintsValue: ConstraintsValue = {
3034
3130
  horizontal:
3035
- styles.left && styles.right
3036
- ? "left-right"
3037
- : styles.right
3038
- ? "right"
3039
- : styles.transform?.includes("translateX(-50%)")
3040
- ? "center"
3041
- : styles.width === "100%"
3042
- ? "scale"
3131
+ // Check scale before left+right: "scale" writes width:100% and clears
3132
+ // left/right to auto, but legacy data may have 0px values that are truthy.
3133
+ styles.width === "100%"
3134
+ ? "scale"
3135
+ : styles.left && styles.right
3136
+ ? "left-right"
3137
+ : styles.right
3138
+ ? "right"
3139
+ : styles.transform?.includes("translateX(-50%)")
3140
+ ? "center"
3043
3141
  : "left",
3044
3142
  vertical:
3045
- styles.top && styles.bottom
3046
- ? "top-bottom"
3047
- : styles.bottom
3048
- ? "bottom"
3049
- : styles.transform?.includes("translateY(-50%)")
3050
- ? "center"
3051
- : styles.height === "100%"
3052
- ? "scale"
3143
+ styles.height === "100%"
3144
+ ? "scale"
3145
+ : styles.top && styles.bottom
3146
+ ? "top-bottom"
3147
+ : styles.bottom
3148
+ ? "bottom"
3149
+ : styles.transform?.includes("translateY(-50%)")
3150
+ ? "center"
3053
3151
  : "top",
3054
3152
  };
3055
3153
 
3056
3154
  const handleConstraintsChange = useCallback(
3057
3155
  (value: ConstraintsValue) => {
3058
3156
  onStyleChange("position", "absolute");
3157
+
3158
+ // Compute the desired translateX/Y for each axis independently, then
3159
+ // compose both into a single transform write so the two axes don't
3160
+ // overwrite each other when both change simultaneously.
3161
+ const txValue = value.horizontal === "center" ? "-50%" : null;
3162
+ const tyValue = value.vertical === "center" ? "-50%" : null;
3163
+ // Start from the current transform, apply X, then apply Y on top.
3164
+ const transformAfterX = mergeTranslateFunction(
3165
+ styles.transform,
3166
+ "X",
3167
+ txValue,
3168
+ );
3169
+ const transformAfterXY = mergeTranslateFunction(
3170
+ transformAfterX,
3171
+ "Y",
3172
+ tyValue,
3173
+ );
3174
+
3059
3175
  if (value.horizontal === "left") {
3060
3176
  onStyleChange(
3061
3177
  "left",
@@ -3074,10 +3190,11 @@ function PositionLayoutProperties({
3074
3190
  } else if (value.horizontal === "center") {
3075
3191
  onStyleChange("left", "50%");
3076
3192
  onStyleChange("right", "auto");
3077
- onStyleChange("transform", "translateX(-50%)");
3078
3193
  } else {
3079
- onStyleChange("left", "0px");
3080
- onStyleChange("right", "0px");
3194
+ // scale: use auto (not 0px) so the left && right truthiness check
3195
+ // in the reader does not misidentify this as "left-right".
3196
+ onStyleChange("left", "auto");
3197
+ onStyleChange("right", "auto");
3081
3198
  onStyleChange("width", "100%");
3082
3199
  }
3083
3200
 
@@ -3099,12 +3216,15 @@ function PositionLayoutProperties({
3099
3216
  } else if (value.vertical === "center") {
3100
3217
  onStyleChange("top", "50%");
3101
3218
  onStyleChange("bottom", "auto");
3102
- onStyleChange("transform", "translateY(-50%)");
3103
3219
  } else {
3104
- onStyleChange("top", "0px");
3105
- onStyleChange("bottom", "0px");
3220
+ // scale
3221
+ onStyleChange("top", "auto");
3222
+ onStyleChange("bottom", "auto");
3106
3223
  onStyleChange("height", "100%");
3107
3224
  }
3225
+
3226
+ // Write the composed transform once, after both axes are resolved.
3227
+ onStyleChange("transform", transformAfterXY);
3108
3228
  },
3109
3229
  [
3110
3230
  element.boundingRect.x,
@@ -3112,6 +3232,7 @@ function PositionLayoutProperties({
3112
3232
  onStyleChange,
3113
3233
  styles.left,
3114
3234
  styles.top,
3235
+ styles.transform,
3115
3236
  ],
3116
3237
  );
3117
3238
 
@@ -3120,7 +3241,7 @@ function PositionLayoutProperties({
3120
3241
  title={t("editPanel.sections.positionLayout")}
3121
3242
  actions={
3122
3243
  <SectionIconToggle
3123
- label={"Absolute position" /* i18n-ignore Figma inspector action */}
3244
+ label={"Absolute position" /* i18n-ignore design inspector action */}
3124
3245
  active={constrainedPosition}
3125
3246
  onClick={() =>
3126
3247
  onStyleChange(
@@ -3135,7 +3256,7 @@ function PositionLayoutProperties({
3135
3256
  >
3136
3257
  <div className="space-y-1.5">
3137
3258
  <SubsectionLabel>
3138
- {"Alignment" /* i18n-ignore Figma inspector label */}
3259
+ {"Alignment" /* i18n-ignore design inspector label */}
3139
3260
  </SubsectionLabel>
3140
3261
  <div className="flex items-center gap-3">
3141
3262
  <InspectorSegment>
@@ -3227,13 +3348,19 @@ function PositionLayoutProperties({
3227
3348
  <InspectorSegment>
3228
3349
  <InspectorIconButton
3229
3350
  label={t("editPanel.labels.flipHorizontal")}
3230
- onClick={() => onStyleChange("scale", "-1 1")}
3351
+ onClick={() => {
3352
+ const [sx, sy] = parseScaleValue(styles.scale);
3353
+ onStyleChange("scale", `${sx === -1 ? 1 : -1} ${sy}`);
3354
+ }}
3231
3355
  >
3232
3356
  <IconFlipHorizontal className="size-4" />
3233
3357
  </InspectorIconButton>
3234
3358
  <InspectorIconButton
3235
3359
  label={t("editPanel.labels.flipVertical")}
3236
- onClick={() => onStyleChange("scale", "1 -1")}
3360
+ onClick={() => {
3361
+ const [sx, sy] = parseScaleValue(styles.scale);
3362
+ onStyleChange("scale", `${sx} ${sy === -1 ? 1 : -1}`);
3363
+ }}
3237
3364
  >
3238
3365
  <IconFlipVertical className="size-4" />
3239
3366
  </InspectorIconButton>
@@ -3275,13 +3402,13 @@ function FillProperties({
3275
3402
  isTextElement || colorHasVisibleAlpha(fillValue) || hasBackgroundLayer;
3276
3403
 
3277
3404
  // Non-destructive fill hide: stash the color before hiding so toggling
3278
- // visible again restores the exact original value (Figma never loses color).
3279
- // Keyed by `${element.id ?? tagName}:${fillProperty}` so separate elements
3405
+ // visible again restores the exact original value (the design editor never loses color).
3406
+ // Keyed by a stable selected-element identity so anonymous same-tag elements
3280
3407
  // don't share stash slots.
3281
3408
  const [hiddenFillStash, setHiddenFillStash] = useState<
3282
3409
  Record<string, string>
3283
3410
  >({});
3284
- const stashKey = `${element.id ?? element.tagName}:${fillProperty}`;
3411
+ const stashKey = `${elementIdentityKey(element)}:${fillProperty}`;
3285
3412
  const isHidden = fillValue === "transparent" || fillValue === "";
3286
3413
  const handleFillVisibilityToggle = () => {
3287
3414
  if (isHidden) {
@@ -3324,9 +3451,9 @@ function FillProperties({
3324
3451
  title={t("editPanel.sections.fill")}
3325
3452
  actions={
3326
3453
  <>
3327
- {/* Figma color-styles affordance (grid icon) to the left of "+". */}
3454
+ {/* design color-styles affordance (grid icon) to the left of "+". */}
3328
3455
  <SectionIconButton
3329
- label={"Styles" /* i18n-ignore Figma inspector action */}
3456
+ label={"Styles" /* i18n-ignore design inspector action */}
3330
3457
  >
3331
3458
  <IconLayoutGrid className="size-3.5" />
3332
3459
  </SectionIconButton>
@@ -3366,7 +3493,7 @@ function FillProperties({
3366
3493
  {hasVisibleFill ? (
3367
3494
  <div className="space-y-1.5">
3368
3495
  {isTextElement || colorHasVisibleAlpha(fillValue) ? (
3369
- /* Figma row: [swatch+hex trigger (flex-1)] [eye] [remove] */
3496
+ /* design row: [swatch+hex trigger (flex-1)] [eye] [remove] */
3370
3497
  <div className="group flex items-center gap-1.5">
3371
3498
  <div className="min-w-0 flex-1">
3372
3499
  <ColorInput
@@ -3385,6 +3512,13 @@ function FillProperties({
3385
3512
  : (v) => onStyleChange("backgroundBlendMode", v)
3386
3513
  }
3387
3514
  documentColors={documentColors}
3515
+ pickerKey={[
3516
+ element.sourceId ??
3517
+ element.id ??
3518
+ element.selector ??
3519
+ element.tagName,
3520
+ fillProperty,
3521
+ ].join(":")}
3388
3522
  />
3389
3523
  </div>
3390
3524
  <SectionIconButton
@@ -3432,7 +3566,7 @@ function FillProperties({
3432
3566
  : 100;
3433
3567
  const label = gradient
3434
3568
  ? `${gradientLabel(gradient.type)} ${index + 1}`
3435
- : `${"Image" /* i18n-ignore Figma inspector paint row */} ${
3569
+ : `${"Image" /* i18n-ignore design inspector paint row */} ${
3436
3570
  index + 1
3437
3571
  }`;
3438
3572
  const replaceLayer = (nextLayer: string) => {
@@ -3452,7 +3586,7 @@ function FillProperties({
3452
3586
  };
3453
3587
 
3454
3588
  return (
3455
- /* Figma row: [swatch+label+opacity% trigger (flex-1)] [eye] [remove] */
3589
+ /* design row: [swatch+label+opacity% trigger (flex-1)] [eye] [remove] */
3456
3590
  <div
3457
3591
  key={`${layer}-${index}`}
3458
3592
  className="group flex items-center gap-1.5"
@@ -3481,7 +3615,7 @@ function FillProperties({
3481
3615
  sideOffset={8}
3482
3616
  className="w-80 p-0"
3483
3617
  >
3484
- <FigmaColorPicker
3618
+ <DesignColorPicker
3485
3619
  value={gradient?.stops[0]?.color ?? layer}
3486
3620
  onPaintValueChange={replaceLayer}
3487
3621
  onChange={(nextColor) => {
@@ -3489,10 +3623,14 @@ function FillProperties({
3489
3623
  const firstStop = gradient.stops[0];
3490
3624
  if (!firstStop) return;
3491
3625
  replaceLayer(
3492
- buildGradientLayer(gradient.type, [
3493
- { ...firstStop, color: nextColor },
3494
- ...gradient.stops.slice(1),
3495
- ]),
3626
+ buildGradientLayer(
3627
+ gradient.type,
3628
+ [
3629
+ { ...firstStop, color: nextColor },
3630
+ ...gradient.stops.slice(1),
3631
+ ],
3632
+ gradient.prefix,
3633
+ ),
3496
3634
  );
3497
3635
  }}
3498
3636
  paintType={gradient?.type ?? "image"}
@@ -3533,6 +3671,7 @@ function FillProperties({
3533
3671
  ...stop,
3534
3672
  opacity: opacity <= 0 ? 100 : 0,
3535
3673
  })),
3674
+ gradient.prefix,
3536
3675
  ),
3537
3676
  );
3538
3677
  }}
@@ -3576,6 +3715,11 @@ function StrokeProperties({
3576
3715
  styles.outlineWidth,
3577
3716
  styles.outlineStyle,
3578
3717
  );
3718
+ // Render the row whenever a stroke has been configured (non-zero width),
3719
+ // even when its style is "none" (hidden). This mirrors Figma's behavior where
3720
+ // hidden stroke rows remain present so the user can re-show them via the eye icon.
3721
+ const borderExists = cssLengthNumber(styles.borderWidth) > 0;
3722
+ const outlineExists = cssLengthNumber(styles.outlineWidth) > 0;
3579
3723
 
3580
3724
  return (
3581
3725
  <PanelSection
@@ -3640,7 +3784,7 @@ function StrokeProperties({
3640
3784
  </SectionIconButton>
3641
3785
  }
3642
3786
  >
3643
- {borderVisible ? (
3787
+ {borderExists ? (
3644
3788
  <StrokeLayerControl
3645
3789
  kind="border"
3646
3790
  visible={borderVisible}
@@ -3657,7 +3801,7 @@ function StrokeProperties({
3657
3801
  }}
3658
3802
  />
3659
3803
  ) : null}
3660
- {outlineVisible ? (
3804
+ {outlineExists ? (
3661
3805
  <StrokeLayerControl
3662
3806
  kind="outline"
3663
3807
  visible={outlineVisible}
@@ -3696,10 +3840,10 @@ function AppearanceProperties({
3696
3840
  title={t("root.commandAppearance")}
3697
3841
  actions={
3698
3842
  <>
3699
- {/* Opacity / blend-mode affordance — matches Figma's pill icon */}
3843
+ {/* Opacity / blend-mode affordance — matches the design editor's pill icon */}
3700
3844
  <SectionIconButton
3701
3845
  label={
3702
- "Opacity & blend mode" /* i18n-ignore Figma inspector action */
3846
+ "Opacity & blend mode" /* i18n-ignore design inspector action */
3703
3847
  }
3704
3848
  >
3705
3849
  <IconSlice className="size-3.5" />
@@ -3708,8 +3852,8 @@ function AppearanceProperties({
3708
3852
  <SectionIconToggle
3709
3853
  label={
3710
3854
  hidden
3711
- ? "Show" /* i18n-ignore Figma inspector action */
3712
- : "Hide" /* i18n-ignore Figma inspector action */
3855
+ ? "Show" /* i18n-ignore design inspector action */
3856
+ : "Hide" /* i18n-ignore design inspector action */
3713
3857
  }
3714
3858
  active={hidden}
3715
3859
  onClick={() =>
@@ -3722,9 +3866,9 @@ function AppearanceProperties({
3722
3866
  <IconEye className="size-3.5" />
3723
3867
  )}
3724
3868
  </SectionIconToggle>
3725
- {/* Styles / fill library affordance — matches Figma's droplet icon */}
3869
+ {/* Styles / fill library affordance — matches the design editor's droplet icon */}
3726
3870
  <SectionIconButton
3727
- label={"Styles" /* i18n-ignore Figma inspector action */}
3871
+ label={"Styles" /* i18n-ignore design inspector action */}
3728
3872
  >
3729
3873
  <IconDroplet className="size-3.5" />
3730
3874
  </SectionIconButton>
@@ -3787,7 +3931,7 @@ function EffectsProperties({
3787
3931
  const t = useT();
3788
3932
  const styles = element.computedStyles;
3789
3933
  const blurValue = readBlurFilter(styles.filter);
3790
- // M5 · Background (backdrop) blur is a distinct Figma effect type, backed by
3934
+ // M5 · Background (backdrop) blur is a distinct design effect type, backed by
3791
3935
  // CSS `backdrop-filter: blur()` (vs layer blur's `filter: blur()`).
3792
3936
  const backdropBlurValue = readBlurFilter(
3793
3937
  styles.backdropFilter || styles.webkitBackdropFilter,
@@ -3842,7 +3986,7 @@ function EffectsProperties({
3842
3986
  onSelect={addBackgroundBlur}
3843
3987
  >
3844
3988
  <IconBackground className="size-3.5" />
3845
- {"Background blur" /* i18n-ignore Figma effect type */}
3989
+ {"Background blur" /* i18n-ignore design effect type */}
3846
3990
  </DropdownMenuItem>
3847
3991
  </DropdownMenuContent>
3848
3992
  </DropdownMenu>
@@ -3873,7 +4017,7 @@ function EffectsProperties({
3873
4017
  </div>
3874
4018
  ) : null}
3875
4019
  {blurValue > 0 ? (
3876
- /* Figma effect row for layer blur: flat row matching shadow rows */
4020
+ /* design effect row for layer blur: flat row matching shadow rows */
3877
4021
  <Popover>
3878
4022
  <div className="group flex items-center gap-1.5">
3879
4023
  <PopoverTrigger asChild>
@@ -3907,12 +4051,14 @@ function EffectsProperties({
3907
4051
  <ScrubInput
3908
4052
  label={t("editPanel.labels.blur")}
3909
4053
  value={blurValue}
3910
- onChange={(value) =>
3911
- onStyleChange(
3912
- "filter",
3913
- `blur(${Math.max(0, Math.round(value))}px)`,
3914
- )
3915
- }
4054
+ onChange={(value) => {
4055
+ const blurFn = `blur(${Math.max(0, Math.round(value))}px)`;
4056
+ const existing = styles.filter || "";
4057
+ const next = existing.includes("blur(")
4058
+ ? existing.replace(/blur\([^)]*\)/, blurFn)
4059
+ : blurFn;
4060
+ onStyleChange("filter", next);
4061
+ }}
3916
4062
  unit="px"
3917
4063
  min={0}
3918
4064
  precision={1}
@@ -3932,7 +4078,7 @@ function EffectsProperties({
3932
4078
  className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
3933
4079
  >
3934
4080
  <span className="min-w-0 flex-1 truncate font-medium text-foreground">
3935
- {"Background blur" /* i18n-ignore Figma effect type */}
4081
+ {"Background blur" /* i18n-ignore design effect type */}
3936
4082
  </span>
3937
4083
  <span className="shrink-0 tabular-nums text-muted-foreground">
3938
4084
  {Math.round(backdropBlurValue)}px
@@ -4007,7 +4153,7 @@ function selectionColorValues(element: ElementInfo): SelectionColorValue[] {
4007
4153
  });
4008
4154
  }
4009
4155
 
4010
- /** Uppercase 6-char hex (no #) for a CSS color, matching Figma's row readout. */
4156
+ /** Uppercase 6-char hex (no #) for a CSS color, matching the design editor's row readout. */
4011
4157
  function selectionDisplayHex(value: string): string {
4012
4158
  const parsed = parseCssColor(value);
4013
4159
  if (!parsed) return value.replace(/^#/, "").toUpperCase();
@@ -4021,7 +4167,7 @@ function SelectionColorsProperties({
4021
4167
  element: ElementInfo;
4022
4168
  onStyleChange: (property: string, value: string) => void;
4023
4169
  }) {
4024
- // M6 · Figma's Selection colors collapses to a single "Show selection colors"
4170
+ // M6 · the design editor's Selection colors collapses to a single "Show selection colors"
4025
4171
  // affordance, expanding to one editable [swatch · hex · opacity] row per
4026
4172
  // unique color — matching the Fill row grammar instead of a swatch strip.
4027
4173
  const [expanded, setExpanded] = useState(false);
@@ -4030,7 +4176,7 @@ function SelectionColorsProperties({
4030
4176
 
4031
4177
  return (
4032
4178
  <PanelSection
4033
- title={"Selection colors" /* i18n-ignore Figma inspector label */}
4179
+ title={"Selection colors" /* i18n-ignore design inspector label */}
4034
4180
  >
4035
4181
  {expanded ? (
4036
4182
  <div className="space-y-1.5">
@@ -4063,7 +4209,7 @@ function SelectionColorsProperties({
4063
4209
  sideOffset={8}
4064
4210
  className="w-80 p-0"
4065
4211
  >
4066
- <FigmaColorPicker
4212
+ <DesignColorPicker
4067
4213
  value={cssColorOrFallback(color.value, "#000000")}
4068
4214
  onChange={(value) => onStyleChange(color.property, value)}
4069
4215
  />
@@ -4079,7 +4225,7 @@ function SelectionColorsProperties({
4079
4225
  onClick={() => setExpanded(true)}
4080
4226
  >
4081
4227
  <span className="truncate">
4082
- {"Show selection colors" /* i18n-ignore Figma inspector label */}
4228
+ {"Show selection colors" /* i18n-ignore design inspector label */}
4083
4229
  </span>
4084
4230
  <div className="flex shrink-0 items-center -space-x-1">
4085
4231
  {colors.slice(0, 3).map((color, index) => (
@@ -4130,12 +4276,13 @@ export function EditPanel({
4130
4276
  exporting = false,
4131
4277
  }: EditPanelProps) {
4132
4278
  const t = useT();
4133
- const [exportSettings, setExportSettings] = useState<ExportSettingsValue>({
4134
- scale: 1,
4135
- format: "png",
4136
- suffix: "",
4137
- });
4279
+ const [exportSettings, setExportSettings] = useState<ExportSettingsValue>(
4280
+ DEFAULT_EXPORT_SETTINGS,
4281
+ );
4138
4282
  const [showExportPreview, setShowExportPreview] = useState(false);
4283
+ const selectedElementKey = selectedElement
4284
+ ? elementIdentityKey(selectedElement)
4285
+ : "none";
4139
4286
  const isTextElement = selectedElement
4140
4287
  ? TEXT_TAGS.has(selectedElement.tagName)
4141
4288
  : false;
@@ -4150,6 +4297,11 @@ export function EditPanel({
4150
4297
  [onTweakChange],
4151
4298
  );
4152
4299
 
4300
+ useEffect(() => {
4301
+ setExportSettings(DEFAULT_EXPORT_SETTINGS);
4302
+ setShowExportPreview(false);
4303
+ }, [selectedElementKey]);
4304
+
4153
4305
  return (
4154
4306
  <div
4155
4307
  className={cn(
@@ -4231,8 +4383,8 @@ export function EditPanel({
4231
4383
  <SectionIconToggle
4232
4384
  label={
4233
4385
  showExportPreview
4234
- ? "Hide preview" /* i18n-ignore Figma inspector action */
4235
- : "Show preview" /* i18n-ignore Figma inspector action */
4386
+ ? "Hide preview" /* i18n-ignore design inspector action */
4387
+ : "Show preview" /* i18n-ignore design inspector action */
4236
4388
  }
4237
4389
  active={showExportPreview}
4238
4390
  onClick={() => setShowExportPreview((shown) => !shown)}
@@ -4242,6 +4394,7 @@ export function EditPanel({
4242
4394
  }
4243
4395
  >
4244
4396
  <ExportSettingsPanel
4397
+ key={selectedElementKey}
4245
4398
  value={exportSettings}
4246
4399
  formats={["png", "svg"]}
4247
4400
  exporting={exporting}