@agent-native/core 0.85.6 → 0.86.0

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 (1069) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +130 -1
  3. package/corpus/core/README.md +0 -11
  4. package/corpus/core/docs/content/cloneable-saas.mdx +0 -1
  5. package/corpus/core/docs/content/locales/ar-SA/cloneable-saas.mdx +0 -1
  6. package/corpus/core/docs/content/locales/ar-SA/plan-plugin.mdx +10 -9
  7. package/corpus/core/docs/content/locales/ar-SA/sharing.mdx +1 -2
  8. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +2 -3
  9. package/corpus/core/docs/content/locales/ar-SA/template-plan.mdx +23 -7
  10. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +1 -1
  11. package/corpus/core/docs/content/locales/ar-SA/workspace-connections.mdx +11 -11
  12. package/corpus/core/docs/content/locales/de-DE/cloneable-saas.mdx +0 -1
  13. package/corpus/core/docs/content/locales/de-DE/plan-plugin.mdx +10 -9
  14. package/corpus/core/docs/content/locales/de-DE/sharing.mdx +1 -2
  15. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +2 -3
  16. package/corpus/core/docs/content/locales/de-DE/template-plan.mdx +23 -7
  17. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +1 -1
  18. package/corpus/core/docs/content/locales/de-DE/workspace-connections.mdx +1 -1
  19. package/corpus/core/docs/content/locales/es-ES/cloneable-saas.mdx +0 -1
  20. package/corpus/core/docs/content/locales/es-ES/plan-plugin.mdx +10 -9
  21. package/corpus/core/docs/content/locales/es-ES/sharing.mdx +1 -2
  22. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +2 -3
  23. package/corpus/core/docs/content/locales/es-ES/template-plan.mdx +23 -7
  24. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +1 -1
  25. package/corpus/core/docs/content/locales/es-ES/workspace-connections.mdx +1 -1
  26. package/corpus/core/docs/content/locales/fr-FR/cloneable-saas.mdx +0 -1
  27. package/corpus/core/docs/content/locales/fr-FR/plan-plugin.mdx +10 -9
  28. package/corpus/core/docs/content/locales/fr-FR/sharing.mdx +1 -2
  29. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +2 -3
  30. package/corpus/core/docs/content/locales/fr-FR/template-plan.mdx +23 -7
  31. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +1 -1
  32. package/corpus/core/docs/content/locales/fr-FR/workspace-connections.mdx +1 -1
  33. package/corpus/core/docs/content/locales/hi-IN/cloneable-saas.mdx +0 -1
  34. package/corpus/core/docs/content/locales/hi-IN/plan-plugin.mdx +10 -9
  35. package/corpus/core/docs/content/locales/hi-IN/sharing.mdx +1 -2
  36. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +2 -3
  37. package/corpus/core/docs/content/locales/hi-IN/template-plan.mdx +23 -7
  38. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +1 -1
  39. package/corpus/core/docs/content/locales/hi-IN/workspace-connections.mdx +1 -1
  40. package/corpus/core/docs/content/locales/ja-JP/cloneable-saas.mdx +0 -1
  41. package/corpus/core/docs/content/locales/ja-JP/plan-plugin.mdx +10 -9
  42. package/corpus/core/docs/content/locales/ja-JP/sharing.mdx +1 -2
  43. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +2 -3
  44. package/corpus/core/docs/content/locales/ja-JP/template-plan.mdx +23 -7
  45. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +1 -1
  46. package/corpus/core/docs/content/locales/ja-JP/workspace-connections.mdx +1 -1
  47. package/corpus/core/docs/content/locales/ko-KR/cloneable-saas.mdx +0 -1
  48. package/corpus/core/docs/content/locales/ko-KR/plan-plugin.mdx +10 -9
  49. package/corpus/core/docs/content/locales/ko-KR/sharing.mdx +1 -2
  50. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +2 -3
  51. package/corpus/core/docs/content/locales/ko-KR/template-plan.mdx +23 -7
  52. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +1 -1
  53. package/corpus/core/docs/content/locales/ko-KR/workspace-connections.mdx +1 -1
  54. package/corpus/core/docs/content/locales/pt-BR/cloneable-saas.mdx +0 -1
  55. package/corpus/core/docs/content/locales/pt-BR/plan-plugin.mdx +10 -9
  56. package/corpus/core/docs/content/locales/pt-BR/sharing.mdx +1 -2
  57. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +2 -3
  58. package/corpus/core/docs/content/locales/pt-BR/template-plan.mdx +23 -7
  59. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +1 -1
  60. package/corpus/core/docs/content/locales/pt-BR/workspace-connections.mdx +1 -1
  61. package/corpus/core/docs/content/locales/zh-CN/cloneable-saas.mdx +0 -1
  62. package/corpus/core/docs/content/locales/zh-CN/plan-plugin.mdx +10 -9
  63. package/corpus/core/docs/content/locales/zh-CN/sharing.mdx +1 -2
  64. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +2 -3
  65. package/corpus/core/docs/content/locales/zh-CN/template-plan.mdx +23 -7
  66. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +1 -1
  67. package/corpus/core/docs/content/locales/zh-CN/workspace-connections.mdx +1 -1
  68. package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.mdx +0 -1
  69. package/corpus/core/docs/content/locales/zh-TW/plan-plugin.mdx +10 -9
  70. package/corpus/core/docs/content/locales/zh-TW/sharing.mdx +1 -2
  71. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +2 -3
  72. package/corpus/core/docs/content/locales/zh-TW/template-plan.mdx +23 -7
  73. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +1 -1
  74. package/corpus/core/docs/content/locales/zh-TW/workspace-connections.mdx +1 -1
  75. package/corpus/core/docs/content/plan-plugin.mdx +12 -11
  76. package/corpus/core/docs/content/sharing.mdx +1 -2
  77. package/corpus/core/docs/content/skills-guide.mdx +4 -3
  78. package/corpus/core/docs/content/template-plan.mdx +37 -6
  79. package/corpus/core/docs/content/tracking.mdx +1 -1
  80. package/corpus/core/docs/content/workspace-connections.mdx +1 -1
  81. package/corpus/core/package.json +6 -6
  82. package/corpus/core/src/agent/run-store.ts +1 -1
  83. package/corpus/core/src/chat-threads/store.ts +73 -11
  84. package/corpus/core/src/checkpoints/store.ts +13 -2
  85. package/corpus/core/src/cli/create.ts +3 -4
  86. package/corpus/core/src/cli/design-connect.ts +469 -9
  87. package/corpus/core/src/cli/index.ts +13 -0
  88. package/corpus/core/src/cli/plan-local.ts +281 -24
  89. package/corpus/core/src/cli/skills.ts +177 -21
  90. package/corpus/core/src/cli/templates-meta.ts +0 -13
  91. package/corpus/core/src/cli/visualize-repo.ts +715 -0
  92. package/corpus/core/src/client/AssistantChat.tsx +126 -17
  93. package/corpus/core/src/client/agent-chat-adapter.ts +6 -5
  94. package/corpus/core/src/client/app-providers.tsx +2 -2
  95. package/corpus/core/src/client/collab/index.ts +31 -0
  96. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +16 -1
  97. package/corpus/core/src/client/components/PresenceBar.tsx +14 -1
  98. package/corpus/core/src/client/components/RecentEditHighlights.tsx +219 -0
  99. package/corpus/core/src/client/components/RemoteSelectionRings.tsx +40 -6
  100. package/corpus/core/src/client/index.ts +48 -0
  101. package/corpus/core/src/client/org/OrgSwitcher.tsx +0 -4
  102. package/corpus/core/src/client/rich-markdown-editor/SharedRichEditor.tsx +49 -32
  103. package/corpus/core/src/client/rich-markdown-editor/index.ts +6 -0
  104. package/corpus/core/src/client/rich-markdown-editor/surgical-apply.ts +192 -0
  105. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +59 -8
  106. package/corpus/core/src/client/session-replay.ts +181 -15
  107. package/corpus/core/src/client/sse-event-processor.ts +90 -1
  108. package/corpus/core/src/client/use-action.ts +123 -25
  109. package/corpus/core/src/client/use-agent-engine-configured.ts +7 -1
  110. package/corpus/core/src/client/use-chat-threads.ts +18 -7
  111. package/corpus/core/src/client/use-db-sync.ts +104 -19
  112. package/corpus/core/src/client/use-pinch-zoom.ts +76 -11
  113. package/corpus/core/src/client/visual-style-controls.tsx +475 -0
  114. package/corpus/core/src/collab/agent-presence.ts +191 -62
  115. package/corpus/core/src/collab/awareness.ts +83 -6
  116. package/corpus/core/src/collab/client.ts +89 -146
  117. package/corpus/core/src/collab/index.ts +34 -0
  118. package/corpus/core/src/collab/presence.ts +63 -3
  119. package/corpus/core/src/collab/recent-edits.ts +171 -0
  120. package/corpus/core/src/collab/undo.ts +432 -0
  121. package/corpus/core/src/collab/ydoc-manager.ts +68 -0
  122. package/corpus/core/src/connections/catalog.ts +2 -3
  123. package/corpus/core/src/db/create-get-db.ts +86 -1
  124. package/corpus/core/src/db/ensure-additive-columns.ts +430 -0
  125. package/corpus/core/src/db/index.ts +6 -0
  126. package/corpus/core/src/db/migrations.ts +236 -35
  127. package/corpus/core/src/jobs/scheduler.ts +17 -14
  128. package/corpus/core/src/observability/traces.ts +182 -1
  129. package/corpus/core/src/org/context.ts +79 -2
  130. package/corpus/core/src/server/agent-chat-plugin.ts +10 -6
  131. package/corpus/core/src/server/auth-marketing.ts +0 -11
  132. package/corpus/core/src/server/auth.ts +11 -4
  133. package/corpus/core/src/server/collab-plugin.ts +56 -13
  134. package/corpus/core/src/server/design-token-utils.ts +1 -1
  135. package/corpus/core/src/server/fusion-app.ts +447 -0
  136. package/corpus/core/src/server/index.ts +13 -0
  137. package/corpus/core/src/server/onboarding-html.ts +3 -4
  138. package/corpus/core/src/server/poll-events.ts +14 -2
  139. package/corpus/core/src/server/poll.ts +219 -12
  140. package/corpus/core/src/server/schema-prompt.ts +28 -9
  141. package/corpus/core/src/settings/store.ts +33 -2
  142. package/corpus/core/src/sharing/actions/set-resource-visibility.ts +2 -0
  143. package/corpus/core/src/sharing/actions/share-resource.ts +3 -0
  144. package/corpus/core/src/sharing/actions/unshare-resource.ts +2 -0
  145. package/corpus/core/src/styles/agent-native.css +0 -5
  146. package/corpus/core/src/templates/default/.agents/skills/real-time-collab/SKILL.md +82 -19
  147. package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +15 -0
  148. package/corpus/core/src/templates/default/.agents/skills/storing-data/SKILL.md +8 -0
  149. package/corpus/core/src/templates/default/package.json +3 -3
  150. package/corpus/core/src/templates/workspace-core/.agents/skills/client-methods/SKILL.md +8 -0
  151. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +22 -0
  152. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +82 -19
  153. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +15 -0
  154. package/corpus/core/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +8 -0
  155. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -0
  156. package/corpus/core/src/tracking/providers.ts +22 -1
  157. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +15 -0
  158. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +20 -5
  159. package/corpus/templates/analytics/.agents/skills/storing-data/SKILL.md +8 -0
  160. package/corpus/templates/analytics/AGENTS.md +33 -6
  161. package/corpus/templates/analytics/actions/navigate.ts +14 -4
  162. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  163. package/corpus/templates/analytics/actions/view-screen.ts +57 -0
  164. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +23 -1
  165. package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -0
  166. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -0
  167. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -0
  168. package/corpus/templates/analytics/app/i18n/zh-TW.ts +15 -1
  169. package/corpus/templates/analytics/app/i18n-data.ts +151 -10
  170. package/corpus/templates/analytics/app/pages/Agents.tsx +118 -0
  171. package/corpus/templates/analytics/app/pages/DataSources.tsx +11 -1
  172. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +76 -40
  173. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +5 -0
  174. package/corpus/templates/analytics/app/routes/agents.tsx +10 -0
  175. package/corpus/templates/analytics/changelog/2026-07-03-agent-admin-tools-page.md +6 -0
  176. package/corpus/templates/analytics/changelog/2026-07-03-agent-llm-observability-dashboard.md +6 -0
  177. package/corpus/templates/analytics/changelog/2026-07-03-session-recordings-now-include-response-bodies-for-5xx-error.md +6 -0
  178. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +34 -0
  179. package/corpus/templates/analytics/docs/schemas/tracked-events.md +6 -0
  180. package/corpus/templates/analytics/package.json +3 -3
  181. package/corpus/templates/analytics/seeds/dashboards/agent-observability-llm.json +173 -0
  182. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +23 -5
  183. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +58 -13
  184. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +23 -0
  185. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +5 -0
  186. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +120 -8
  187. package/corpus/templates/analytics/server/lib/session-replay.ts +3 -2
  188. package/corpus/templates/analytics/server/plugins/db.ts +93 -2
  189. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +45 -1
  190. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +10 -0
  191. package/corpus/templates/assets/.agents/skills/real-time-collab/SKILL.md +82 -19
  192. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +15 -0
  193. package/corpus/templates/assets/.agents/skills/storing-data/SKILL.md +8 -0
  194. package/corpus/templates/assets/package.json +3 -3
  195. package/corpus/templates/assets/server/plugins/db.ts +63 -2
  196. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +15 -0
  197. package/corpus/templates/brain/package.json +3 -3
  198. package/corpus/templates/brain/server/plugins/db.ts +75 -2
  199. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +15 -0
  200. package/corpus/templates/calendar/.agents/skills/storing-data/SKILL.md +8 -0
  201. package/corpus/templates/calendar/package.json +3 -3
  202. package/corpus/templates/calendar/server/plugins/db.ts +64 -2
  203. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +15 -0
  204. package/corpus/templates/chat/package.json +3 -3
  205. package/corpus/templates/clips/actions/finalize-recording.ts +50 -40
  206. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +8 -6
  207. package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -3
  208. package/corpus/templates/clips/changelog/2026-07-03-mobile-recording-pages-keep-videos-in-a-16-9-player-with-act.md +6 -0
  209. package/corpus/templates/clips/desktop/src/lib/recorder.ts +245 -77
  210. package/corpus/templates/clips/package.json +3 -3
  211. package/corpus/templates/clips/server/lib/recording-upload-state.ts +56 -0
  212. package/corpus/templates/clips/server/plugins/db.ts +68 -1
  213. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +27 -3
  214. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +15 -0
  215. package/corpus/templates/content/.agents/skills/storing-data/SKILL.md +8 -0
  216. package/corpus/templates/content/actions/edit-document.ts +48 -1
  217. package/corpus/templates/content/actions/list-documents.ts +101 -94
  218. package/corpus/templates/content/actions/search-documents.ts +19 -3
  219. package/corpus/templates/content/actions/update-document.ts +54 -1
  220. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +127 -7
  221. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +97 -2
  222. package/corpus/templates/content/changelog/2026-07-03-agent-document-edits-now-appear-as-a-live-collaborator-prese.md +6 -0
  223. package/corpus/templates/content/changelog/2026-07-03-document-search-and-list-views-load-faster-on-large-workspac.md +6 -0
  224. package/corpus/templates/content/package.json +3 -3
  225. package/corpus/templates/content/server/plugins/db.ts +57 -1
  226. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +190 -13
  227. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +16 -4
  228. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +46 -3
  229. package/corpus/templates/design/.agents/skills/full-app-build/SKILL.md +140 -0
  230. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +20 -0
  231. package/corpus/templates/design/AGENTS.md +83 -13
  232. package/corpus/templates/design/actions/add-fusion-screens.ts +91 -0
  233. package/corpus/templates/design/actions/apply-a11y-fix.ts +5 -1
  234. package/corpus/templates/design/actions/apply-component-prop-edit.ts +24 -69
  235. package/corpus/templates/design/actions/apply-fusion-edits.ts +161 -0
  236. package/corpus/templates/design/actions/apply-motion-edit.ts +29 -2
  237. package/corpus/templates/design/actions/apply-visual-edit.ts +38 -3
  238. package/corpus/templates/design/actions/connect-localhost.ts +25 -18
  239. package/corpus/templates/design/actions/create-component.ts +5 -1
  240. package/corpus/templates/design/actions/create-fusion-app.ts +202 -0
  241. package/corpus/templates/design/actions/deploy-fusion-app.ts +123 -0
  242. package/corpus/templates/design/actions/edit-design.ts +13 -16
  243. package/corpus/templates/design/actions/export-pdf.ts +8 -1
  244. package/corpus/templates/design/actions/export-zip.ts +10 -1
  245. package/corpus/templates/design/actions/get-fusion-deploy-status.ts +86 -0
  246. package/corpus/templates/design/actions/get-motion-timeline.ts +26 -20
  247. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +10 -0
  248. package/corpus/templates/design/actions/insert-asset.ts +190 -7
  249. package/corpus/templates/design/actions/list-fusion-edits.ts +96 -0
  250. package/corpus/templates/design/actions/list-local-files.ts +133 -0
  251. package/corpus/templates/design/actions/open-visual-edit.ts +27 -5
  252. package/corpus/templates/design/actions/present-design-variants.ts +2 -2
  253. package/corpus/templates/design/actions/push-fusion-app.ts +62 -0
  254. package/corpus/templates/design/actions/queue-fusion-edit.ts +112 -0
  255. package/corpus/templates/design/actions/read-local-file.ts +139 -0
  256. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +18 -14
  257. package/corpus/templates/design/actions/run-design-audit.ts +7 -4
  258. package/corpus/templates/design/actions/send-fusion-message.ts +69 -0
  259. package/corpus/templates/design/actions/sync-fusion-app.ts +196 -0
  260. package/corpus/templates/design/actions/write-local-file.ts +162 -36
  261. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +12 -0
  262. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +600 -66
  263. package/corpus/templates/design/app/components/design/EditPanel.tsx +793 -304
  264. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +328 -0
  265. package/corpus/templates/design/app/components/design/LayersPanel.tsx +671 -154
  266. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +1 -1
  267. package/corpus/templates/design/app/components/design/MotionDock.tsx +234 -46
  268. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2070 -420
  269. package/corpus/templates/design/app/components/design/StatesPanel.tsx +25 -2
  270. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -98
  271. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +640 -72
  272. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +605 -0
  273. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +50 -26
  274. package/corpus/templates/design/app/components/design/code-workbench/ActivityBar.tsx +85 -0
  275. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +466 -0
  276. package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +53 -0
  277. package/corpus/templates/design/app/components/design/code-workbench/commands.ts +339 -0
  278. package/corpus/templates/design/app/components/design/code-workbench/editor/Breadcrumbs.tsx +42 -0
  279. package/corpus/templates/design/app/components/design/code-workbench/editor/BufferSync.tsx +128 -0
  280. package/corpus/templates/design/app/components/design/code-workbench/editor/EditorTabs.tsx +363 -0
  281. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +268 -0
  282. package/corpus/templates/design/app/components/design/code-workbench/editor/StatusBar.tsx +208 -0
  283. package/corpus/templates/design/app/components/design/code-workbench/editor/monaco-setup.ts +61 -0
  284. package/corpus/templates/design/app/components/design/code-workbench/editor/monaco-theme-palette.ts +191 -0
  285. package/corpus/templates/design/app/components/design/code-workbench/editor/monaco-theme.ts +29 -0
  286. package/corpus/templates/design/app/components/design/code-workbench/editor/status-bar-lang.ts +21 -0
  287. package/corpus/templates/design/app/components/design/code-workbench/editor/tab-drag.ts +27 -0
  288. package/corpus/templates/design/app/components/design/code-workbench/explorer/ExplorerView.tsx +155 -0
  289. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +551 -0
  290. package/corpus/templates/design/app/components/design/code-workbench/explorer/file-icons.tsx +115 -0
  291. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +65 -0
  292. package/corpus/templates/design/app/components/design/code-workbench/format/prettier-format.ts +182 -0
  293. package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +133 -0
  294. package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +563 -0
  295. package/corpus/templates/design/app/components/design/code-workbench/quickinput/fuzzy.ts +170 -0
  296. package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +508 -0
  297. package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +280 -0
  298. package/corpus/templates/design/app/components/design/code-workbench/store.tsx +734 -0
  299. package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +39 -0
  300. package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +196 -0
  301. package/corpus/templates/design/app/components/design/code-workbench/workspace/localhost-provider.ts +177 -0
  302. package/corpus/templates/design/app/components/design/code-workbench/workspace/tree.ts +110 -0
  303. package/corpus/templates/design/app/components/design/code-workbench/workspace/types.ts +103 -0
  304. package/corpus/templates/design/app/components/design/code-workbench-theme.ts +68 -0
  305. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +16 -4
  306. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +217 -113
  307. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +111 -5
  308. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -2
  309. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +79 -16
  310. package/corpus/templates/design/app/components/design/inspector/index.ts +9 -0
  311. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +67 -2
  312. package/corpus/templates/design/app/components/design/types.ts +16 -0
  313. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +80 -1
  314. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +24 -6
  315. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +15 -1
  316. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +6 -16
  317. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +77 -0
  318. package/corpus/templates/design/app/i18n/zh-TW.ts +2 -0
  319. package/corpus/templates/design/app/i18n-data.ts +31 -0
  320. package/corpus/templates/design/app/lib/design-import.ts +95 -0
  321. package/corpus/templates/design/app/pages/DesignEditor.tsx +5586 -1112
  322. package/corpus/templates/design/app/pages/Index.tsx +90 -17
  323. package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +6 -0
  324. package/corpus/templates/design/changelog/2026-07-03-inspector-fixes-mixed-selections-show-mixed-instead-of-wrong.md +6 -0
  325. package/corpus/templates/design/changelog/2026-07-03-keyframe-timeline-works-reliably-drag-keyframes-smoothly-pic.md +6 -0
  326. package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +6 -0
  327. package/corpus/templates/design/changelog/2026-07-03-much-faster-editor-smooth-dragging-zooming-and-panel-updates.md +6 -0
  328. package/corpus/templates/design/changelog/2026-07-03-pen-tool-refinements-click-the-first-point-to-close-with-a-d.md +6 -0
  329. package/corpus/templates/design/changelog/2026-07-03-text-editing-enter-adds-a-line-break-international-ime-typin.md +6 -0
  330. package/corpus/templates/design/changelog/2026-07-03-the-ai-now-edits-like-a-collaborator-on-canvas-its-cursor-gl.md +6 -0
  331. package/corpus/templates/design/changelog/2026-07-03-the-code-panel-is-now-a-full-vs-code-style-editor-with-a-fil.md +6 -0
  332. package/corpus/templates/design/changelog/2026-07-03-undo-and-redo-are-dependable-across-agent-edits-screen-switc.md +6 -0
  333. package/corpus/templates/design/package.json +4 -3
  334. package/corpus/templates/design/server/db/schema.ts +31 -0
  335. package/corpus/templates/design/server/lib/design-export.ts +43 -1
  336. package/corpus/templates/design/server/lib/fusion-screens.ts +246 -0
  337. package/corpus/templates/design/server/plugins/db.ts +89 -2
  338. package/corpus/templates/design/shared/board-file.ts +25 -5
  339. package/corpus/templates/design/shared/canvas-math.ts +754 -9
  340. package/corpus/templates/design/shared/code-layer.ts +304 -26
  341. package/corpus/templates/design/shared/collab-selection.ts +52 -0
  342. package/corpus/templates/design/shared/color-utils.ts +84 -0
  343. package/corpus/templates/design/shared/design-source-capabilities.ts +16 -7
  344. package/corpus/templates/design/shared/full-app.ts +126 -0
  345. package/corpus/templates/design/shared/motion-compiler.ts +75 -31
  346. package/corpus/templates/design/shared/motion-timeline.ts +335 -0
  347. package/corpus/templates/design/shared/pen-path.ts +496 -23
  348. package/corpus/templates/dispatch/package.json +3 -3
  349. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +15 -0
  350. package/corpus/templates/forms/.agents/skills/storing-data/SKILL.md +8 -0
  351. package/corpus/templates/forms/package.json +3 -3
  352. package/corpus/templates/forms/server/plugins/db.ts +62 -2
  353. package/corpus/templates/macros/package.json +3 -3
  354. package/corpus/templates/macros/server/plugins/db.ts +63 -2
  355. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +15 -0
  356. package/corpus/templates/mail/.agents/skills/storing-data/SKILL.md +8 -0
  357. package/corpus/templates/mail/package.json +3 -3
  358. package/corpus/templates/mail/server/lib/contact-frequency.ts +42 -15
  359. package/corpus/templates/mail/server/plugins/db.ts +63 -2
  360. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +15 -0
  361. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +9 -3
  362. package/corpus/templates/plan/.agents/skills/visualize-repo/SKILL.md +92 -0
  363. package/corpus/templates/plan/actions/export-visual-plan.ts +5 -0
  364. package/corpus/templates/plan/actions/get-local-plan-folder.ts +4 -22
  365. package/corpus/templates/plan/actions/get-plan-version.ts +6 -16
  366. package/corpus/templates/plan/actions/list-plan-versions.ts +48 -20
  367. package/corpus/templates/plan/actions/patch-visual-plan-source.ts +41 -1
  368. package/corpus/templates/plan/actions/promote-local-plan-folder.ts +4 -22
  369. package/corpus/templates/plan/actions/update-local-plan-comments.ts +4 -2
  370. package/corpus/templates/plan/actions/update-local-plan-folder.ts +4 -22
  371. package/corpus/templates/plan/actions/update-visual-plan.ts +60 -1
  372. package/corpus/templates/plan/agent-native.app-skill.json +10 -0
  373. package/corpus/templates/plan/app/components/editor/PlanDocumentEditor.tsx +138 -39
  374. package/corpus/templates/plan/app/components/editor/usePlanUndoStack.ts +165 -23
  375. package/corpus/templates/plan/app/components/plan/PlanContentRenderer.tsx +80 -4
  376. package/corpus/templates/plan/app/global.css +11 -9
  377. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +10 -1
  378. package/corpus/templates/plan/app/hooks/use-plan-presence.ts +66 -0
  379. package/corpus/templates/plan/app/pages/PlansPage.tsx +215 -63
  380. package/corpus/templates/plan/changelog/2026-07-03-local-visual-plans-and-recaps-can-save-comments-without-sign.md +6 -0
  381. package/corpus/templates/plan/changelog/2026-07-03-plan-tabs-render-nested-text-instead-of-raw-json.md +6 -0
  382. package/corpus/templates/plan/changelog/2026-07-03-plans-are-now-fully-real-time-collaborative-multiple-people-.md +6 -0
  383. package/corpus/templates/plan/changelog/2026-07-03-plans-open-and-refresh-faster-especially-long-lived-plans-wi.md +6 -0
  384. package/corpus/templates/plan/changelog/2026-07-03-undo-now-survives-agent-edits-cmd-z-keeps-working-on-your-ow.md +6 -0
  385. package/corpus/templates/plan/package.json +5 -3
  386. package/corpus/templates/plan/server/db/schema.ts +12 -0
  387. package/corpus/templates/plan/server/lib/local-plan-kind.ts +25 -0
  388. package/corpus/templates/plan/server/lib/plan-versions.ts +83 -1
  389. package/corpus/templates/plan/server/lib/public-action-paths.ts +0 -2
  390. package/corpus/templates/plan/server/plans.ts +26 -3
  391. package/corpus/templates/plan/server/plugins/db.ts +105 -2
  392. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +8 -4
  393. package/corpus/templates/slides/AGENTS.md +4 -0
  394. package/corpus/templates/slides/actions/_agent-presence.ts +48 -0
  395. package/corpus/templates/slides/actions/add-slide.ts +16 -18
  396. package/corpus/templates/slides/actions/patch-deck.ts +4 -2
  397. package/corpus/templates/slides/actions/update-slide.ts +114 -77
  398. package/corpus/templates/slides/actions/view-screen.ts +39 -0
  399. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +47 -3
  400. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +32 -35
  401. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +625 -199
  402. package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +342 -0
  403. package/corpus/templates/slides/app/components/editor/TweaksPanel.tsx +46 -96
  404. package/corpus/templates/slides/app/components/visual-editor/SaveStatusIndicator.tsx +8 -1
  405. package/corpus/templates/slides/app/context/DeckContext.tsx +589 -176
  406. package/corpus/templates/slides/app/hooks/use-deck-presence.ts +84 -42
  407. package/corpus/templates/slides/app/i18n/ar-SA.ts +46 -0
  408. package/corpus/templates/slides/app/i18n/de-DE.ts +46 -0
  409. package/corpus/templates/slides/app/i18n/en-US.ts +46 -0
  410. package/corpus/templates/slides/app/i18n/es-ES.ts +46 -0
  411. package/corpus/templates/slides/app/i18n/fr-FR.ts +46 -0
  412. package/corpus/templates/slides/app/i18n/hi-IN.ts +46 -0
  413. package/corpus/templates/slides/app/i18n/ja-JP.ts +46 -0
  414. package/corpus/templates/slides/app/i18n/ko-KR.ts +46 -0
  415. package/corpus/templates/slides/app/i18n/pt-BR.ts +46 -0
  416. package/corpus/templates/slides/app/i18n/zh-CN.ts +46 -0
  417. package/corpus/templates/slides/app/i18n/zh-TW.ts +46 -0
  418. package/corpus/templates/slides/app/pages/DeckEditor.tsx +25 -13
  419. package/corpus/templates/slides/changelog/2026-07-03-figma-style-slide-editing.md +6 -0
  420. package/corpus/templates/slides/changelog/2026-07-03-see-the-ai-working-with-you-when-the-agent-edits-a-slide-it-.md +6 -0
  421. package/corpus/templates/slides/changelog/2026-07-03-undo-redo-is-now-precise-and-safe-with-collaborators-it-only.md +6 -0
  422. package/corpus/templates/slides/package.json +3 -3
  423. package/corpus/templates/slides/server/handlers/decks.ts +31 -3
  424. package/corpus/templates/slides/server/plugins/db.ts +65 -2
  425. package/corpus/templates/videos/netlify.toml +1 -12
  426. package/dist/agent/run-store.js +1 -1
  427. package/dist/agent/run-store.js.map +1 -1
  428. package/dist/chat-threads/store.d.ts +9 -1
  429. package/dist/chat-threads/store.d.ts.map +1 -1
  430. package/dist/chat-threads/store.js +63 -11
  431. package/dist/chat-threads/store.js.map +1 -1
  432. package/dist/checkpoints/store.d.ts.map +1 -1
  433. package/dist/checkpoints/store.js +10 -2
  434. package/dist/checkpoints/store.js.map +1 -1
  435. package/dist/cli/create.d.ts.map +1 -1
  436. package/dist/cli/create.js +3 -5
  437. package/dist/cli/create.js.map +1 -1
  438. package/dist/cli/design-connect.d.ts +49 -1
  439. package/dist/cli/design-connect.d.ts.map +1 -1
  440. package/dist/cli/design-connect.js +414 -9
  441. package/dist/cli/design-connect.js.map +1 -1
  442. package/dist/cli/index.js +12 -0
  443. package/dist/cli/index.js.map +1 -1
  444. package/dist/cli/plan-local.d.ts +24 -0
  445. package/dist/cli/plan-local.d.ts.map +1 -1
  446. package/dist/cli/plan-local.js +188 -20
  447. package/dist/cli/plan-local.js.map +1 -1
  448. package/dist/cli/skills.d.ts +3 -1
  449. package/dist/cli/skills.d.ts.map +1 -1
  450. package/dist/cli/skills.js +173 -21
  451. package/dist/cli/skills.js.map +1 -1
  452. package/dist/cli/templates-meta.d.ts.map +1 -1
  453. package/dist/cli/templates-meta.js +0 -14
  454. package/dist/cli/templates-meta.js.map +1 -1
  455. package/dist/cli/visualize-repo.d.ts +49 -0
  456. package/dist/cli/visualize-repo.d.ts.map +1 -0
  457. package/dist/cli/visualize-repo.js +561 -0
  458. package/dist/cli/visualize-repo.js.map +1 -0
  459. package/dist/client/AssistantChat.d.ts.map +1 -1
  460. package/dist/client/AssistantChat.js +120 -15
  461. package/dist/client/AssistantChat.js.map +1 -1
  462. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  463. package/dist/client/agent-chat-adapter.js +6 -5
  464. package/dist/client/agent-chat-adapter.js.map +1 -1
  465. package/dist/client/app-providers.d.ts +2 -2
  466. package/dist/client/app-providers.js.map +1 -1
  467. package/dist/client/collab/index.d.ts +4 -1
  468. package/dist/client/collab/index.d.ts.map +1 -1
  469. package/dist/client/collab/index.js +3 -0
  470. package/dist/client/collab/index.js.map +1 -1
  471. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  472. package/dist/client/components/LiveCursorOverlay.js +11 -3
  473. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  474. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  475. package/dist/client/components/PresenceBar.js +6 -1
  476. package/dist/client/components/PresenceBar.js.map +1 -1
  477. package/dist/client/components/RecentEditHighlights.d.ts +43 -0
  478. package/dist/client/components/RecentEditHighlights.d.ts.map +1 -0
  479. package/dist/client/components/RecentEditHighlights.js +146 -0
  480. package/dist/client/components/RecentEditHighlights.js.map +1 -0
  481. package/dist/client/components/RemoteSelectionRings.d.ts +9 -0
  482. package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -1
  483. package/dist/client/components/RemoteSelectionRings.js +22 -8
  484. package/dist/client/components/RemoteSelectionRings.js.map +1 -1
  485. package/dist/client/index.d.ts +6 -1
  486. package/dist/client/index.d.ts.map +1 -1
  487. package/dist/client/index.js +7 -0
  488. package/dist/client/index.js.map +1 -1
  489. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  490. package/dist/client/org/OrgSwitcher.js +1 -3
  491. package/dist/client/org/OrgSwitcher.js.map +1 -1
  492. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +8 -1
  493. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
  494. package/dist/client/rich-markdown-editor/SharedRichEditor.js +8 -2
  495. package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
  496. package/dist/client/rich-markdown-editor/index.d.ts +1 -0
  497. package/dist/client/rich-markdown-editor/index.d.ts.map +1 -1
  498. package/dist/client/rich-markdown-editor/index.js +1 -0
  499. package/dist/client/rich-markdown-editor/index.js.map +1 -1
  500. package/dist/client/rich-markdown-editor/surgical-apply.d.ts +66 -0
  501. package/dist/client/rich-markdown-editor/surgical-apply.d.ts.map +1 -0
  502. package/dist/client/rich-markdown-editor/surgical-apply.js +147 -0
  503. package/dist/client/rich-markdown-editor/surgical-apply.js.map +1 -0
  504. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts +18 -2
  505. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  506. package/dist/client/rich-markdown-editor/useCollabReconcile.js +39 -8
  507. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  508. package/dist/client/session-replay.d.ts +10 -1
  509. package/dist/client/session-replay.d.ts.map +1 -1
  510. package/dist/client/session-replay.js +134 -7
  511. package/dist/client/session-replay.js.map +1 -1
  512. package/dist/client/sse-event-processor.d.ts.map +1 -1
  513. package/dist/client/sse-event-processor.js +81 -1
  514. package/dist/client/sse-event-processor.js.map +1 -1
  515. package/dist/client/use-action.d.ts +24 -0
  516. package/dist/client/use-action.d.ts.map +1 -1
  517. package/dist/client/use-action.js +109 -27
  518. package/dist/client/use-action.js.map +1 -1
  519. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  520. package/dist/client/use-agent-engine-configured.js +7 -1
  521. package/dist/client/use-agent-engine-configured.js.map +1 -1
  522. package/dist/client/use-chat-threads.d.ts.map +1 -1
  523. package/dist/client/use-chat-threads.js +11 -3
  524. package/dist/client/use-chat-threads.js.map +1 -1
  525. package/dist/client/use-db-sync.d.ts +36 -0
  526. package/dist/client/use-db-sync.d.ts.map +1 -1
  527. package/dist/client/use-db-sync.js +68 -12
  528. package/dist/client/use-db-sync.js.map +1 -1
  529. package/dist/client/use-pinch-zoom.d.ts.map +1 -1
  530. package/dist/client/use-pinch-zoom.js +76 -11
  531. package/dist/client/use-pinch-zoom.js.map +1 -1
  532. package/dist/client/visual-style-controls.d.ts +79 -0
  533. package/dist/client/visual-style-controls.d.ts.map +1 -0
  534. package/dist/client/visual-style-controls.js +150 -0
  535. package/dist/client/visual-style-controls.js.map +1 -0
  536. package/dist/collab/agent-presence.d.ts +51 -5
  537. package/dist/collab/agent-presence.d.ts.map +1 -1
  538. package/dist/collab/agent-presence.js +144 -58
  539. package/dist/collab/agent-presence.js.map +1 -1
  540. package/dist/collab/awareness-store.d.ts +32 -0
  541. package/dist/collab/awareness-store.d.ts.map +1 -0
  542. package/dist/collab/awareness-store.js +0 -0
  543. package/dist/collab/awareness-store.js.map +1 -0
  544. package/dist/collab/awareness.d.ts +14 -3
  545. package/dist/collab/awareness.d.ts.map +1 -1
  546. package/dist/collab/awareness.js +53 -5
  547. package/dist/collab/awareness.js.map +1 -1
  548. package/dist/collab/client.d.ts +2 -0
  549. package/dist/collab/client.d.ts.map +1 -1
  550. package/dist/collab/client.js +79 -119
  551. package/dist/collab/client.js.map +1 -1
  552. package/dist/collab/index.d.ts +3 -1
  553. package/dist/collab/index.d.ts.map +1 -1
  554. package/dist/collab/index.js +5 -1
  555. package/dist/collab/index.js.map +1 -1
  556. package/dist/collab/presence.d.ts.map +1 -1
  557. package/dist/collab/presence.js +54 -3
  558. package/dist/collab/presence.js.map +1 -1
  559. package/dist/collab/recent-edits.d.ts +87 -0
  560. package/dist/collab/recent-edits.d.ts.map +1 -0
  561. package/dist/collab/recent-edits.js +116 -0
  562. package/dist/collab/recent-edits.js.map +1 -0
  563. package/dist/collab/undo.d.ts +121 -0
  564. package/dist/collab/undo.d.ts.map +1 -0
  565. package/dist/collab/undo.js +244 -0
  566. package/dist/collab/undo.js.map +1 -0
  567. package/dist/collab/ydoc-manager.d.ts +10 -0
  568. package/dist/collab/ydoc-manager.d.ts.map +1 -1
  569. package/dist/collab/ydoc-manager.js +57 -0
  570. package/dist/collab/ydoc-manager.js.map +1 -1
  571. package/dist/connections/catalog.d.ts +2 -2
  572. package/dist/connections/catalog.d.ts.map +1 -1
  573. package/dist/connections/catalog.js +1 -1
  574. package/dist/connections/catalog.js.map +1 -1
  575. package/dist/db/create-get-db.d.ts +22 -0
  576. package/dist/db/create-get-db.d.ts.map +1 -1
  577. package/dist/db/create-get-db.js +73 -1
  578. package/dist/db/create-get-db.js.map +1 -1
  579. package/dist/db/ensure-additive-columns.d.ts +93 -0
  580. package/dist/db/ensure-additive-columns.d.ts.map +1 -0
  581. package/dist/db/ensure-additive-columns.js +355 -0
  582. package/dist/db/ensure-additive-columns.js.map +1 -0
  583. package/dist/db/index.d.ts +1 -0
  584. package/dist/db/index.d.ts.map +1 -1
  585. package/dist/db/index.js +1 -0
  586. package/dist/db/index.js.map +1 -1
  587. package/dist/db/migrations.d.ts +70 -0
  588. package/dist/db/migrations.d.ts.map +1 -1
  589. package/dist/db/migrations.js +196 -33
  590. package/dist/db/migrations.js.map +1 -1
  591. package/dist/jobs/scheduler.js +17 -10
  592. package/dist/jobs/scheduler.js.map +1 -1
  593. package/dist/notifications/routes.d.ts +3 -3
  594. package/dist/observability/routes.d.ts +5 -5
  595. package/dist/observability/traces.d.ts.map +1 -1
  596. package/dist/observability/traces.js +155 -1
  597. package/dist/observability/traces.js.map +1 -1
  598. package/dist/org/context.d.ts +7 -0
  599. package/dist/org/context.d.ts.map +1 -1
  600. package/dist/org/context.js +54 -2
  601. package/dist/org/context.js.map +1 -1
  602. package/dist/progress/routes.d.ts +1 -1
  603. package/dist/resources/handlers.d.ts +3 -3
  604. package/dist/secrets/routes.d.ts +6 -6
  605. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  606. package/dist/server/agent-chat-plugin.js +10 -6
  607. package/dist/server/agent-chat-plugin.js.map +1 -1
  608. package/dist/server/auth-marketing.d.ts.map +1 -1
  609. package/dist/server/auth-marketing.js +0 -10
  610. package/dist/server/auth-marketing.js.map +1 -1
  611. package/dist/server/auth.d.ts.map +1 -1
  612. package/dist/server/auth.js +6 -4
  613. package/dist/server/auth.js.map +1 -1
  614. package/dist/server/collab-plugin.d.ts.map +1 -1
  615. package/dist/server/collab-plugin.js +45 -13
  616. package/dist/server/collab-plugin.js.map +1 -1
  617. package/dist/server/design-token-utils.d.ts +1 -1
  618. package/dist/server/design-token-utils.js +1 -1
  619. package/dist/server/design-token-utils.js.map +1 -1
  620. package/dist/server/fusion-app.d.ts +107 -0
  621. package/dist/server/fusion-app.d.ts.map +1 -0
  622. package/dist/server/fusion-app.js +371 -0
  623. package/dist/server/fusion-app.js.map +1 -0
  624. package/dist/server/index.d.ts +1 -0
  625. package/dist/server/index.d.ts.map +1 -1
  626. package/dist/server/index.js +1 -0
  627. package/dist/server/index.js.map +1 -1
  628. package/dist/server/onboarding-html.d.ts.map +1 -1
  629. package/dist/server/onboarding-html.js +3 -4
  630. package/dist/server/onboarding-html.js.map +1 -1
  631. package/dist/server/poll-events.d.ts +2 -0
  632. package/dist/server/poll-events.d.ts.map +1 -1
  633. package/dist/server/poll-events.js +7 -2
  634. package/dist/server/poll-events.js.map +1 -1
  635. package/dist/server/poll.d.ts +47 -3
  636. package/dist/server/poll.d.ts.map +1 -1
  637. package/dist/server/poll.js +176 -10
  638. package/dist/server/poll.js.map +1 -1
  639. package/dist/server/schema-prompt.d.ts.map +1 -1
  640. package/dist/server/schema-prompt.js +24 -7
  641. package/dist/server/schema-prompt.js.map +1 -1
  642. package/dist/settings/store.d.ts.map +1 -1
  643. package/dist/settings/store.js +32 -3
  644. package/dist/settings/store.js.map +1 -1
  645. package/dist/sharing/actions/set-resource-visibility.js +2 -0
  646. package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
  647. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  648. package/dist/sharing/actions/share-resource.js +3 -0
  649. package/dist/sharing/actions/share-resource.js.map +1 -1
  650. package/dist/sharing/actions/unshare-resource.js +2 -0
  651. package/dist/sharing/actions/unshare-resource.js.map +1 -1
  652. package/dist/styles/agent-native.css +0 -5
  653. package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +82 -19
  654. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +15 -0
  655. package/dist/templates/default/.agents/skills/storing-data/SKILL.md +8 -0
  656. package/dist/templates/default/package.json +3 -3
  657. package/dist/templates/workspace-core/.agents/skills/client-methods/SKILL.md +8 -0
  658. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +22 -0
  659. package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +82 -19
  660. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +15 -0
  661. package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +8 -0
  662. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -0
  663. package/dist/tracking/providers.d.ts.map +1 -1
  664. package/dist/tracking/providers.js +17 -1
  665. package/dist/tracking/providers.js.map +1 -1
  666. package/docs/content/cloneable-saas.mdx +0 -1
  667. package/docs/content/locales/ar-SA/cloneable-saas.mdx +0 -1
  668. package/docs/content/locales/ar-SA/plan-plugin.mdx +10 -9
  669. package/docs/content/locales/ar-SA/sharing.mdx +1 -2
  670. package/docs/content/locales/ar-SA/skills-guide.mdx +2 -3
  671. package/docs/content/locales/ar-SA/template-plan.mdx +23 -7
  672. package/docs/content/locales/ar-SA/tracking.mdx +1 -1
  673. package/docs/content/locales/ar-SA/workspace-connections.mdx +11 -11
  674. package/docs/content/locales/de-DE/cloneable-saas.mdx +0 -1
  675. package/docs/content/locales/de-DE/plan-plugin.mdx +10 -9
  676. package/docs/content/locales/de-DE/sharing.mdx +1 -2
  677. package/docs/content/locales/de-DE/skills-guide.mdx +2 -3
  678. package/docs/content/locales/de-DE/template-plan.mdx +23 -7
  679. package/docs/content/locales/de-DE/tracking.mdx +1 -1
  680. package/docs/content/locales/de-DE/workspace-connections.mdx +1 -1
  681. package/docs/content/locales/es-ES/cloneable-saas.mdx +0 -1
  682. package/docs/content/locales/es-ES/plan-plugin.mdx +10 -9
  683. package/docs/content/locales/es-ES/sharing.mdx +1 -2
  684. package/docs/content/locales/es-ES/skills-guide.mdx +2 -3
  685. package/docs/content/locales/es-ES/template-plan.mdx +23 -7
  686. package/docs/content/locales/es-ES/tracking.mdx +1 -1
  687. package/docs/content/locales/es-ES/workspace-connections.mdx +1 -1
  688. package/docs/content/locales/fr-FR/cloneable-saas.mdx +0 -1
  689. package/docs/content/locales/fr-FR/plan-plugin.mdx +10 -9
  690. package/docs/content/locales/fr-FR/sharing.mdx +1 -2
  691. package/docs/content/locales/fr-FR/skills-guide.mdx +2 -3
  692. package/docs/content/locales/fr-FR/template-plan.mdx +23 -7
  693. package/docs/content/locales/fr-FR/tracking.mdx +1 -1
  694. package/docs/content/locales/fr-FR/workspace-connections.mdx +1 -1
  695. package/docs/content/locales/hi-IN/cloneable-saas.mdx +0 -1
  696. package/docs/content/locales/hi-IN/plan-plugin.mdx +10 -9
  697. package/docs/content/locales/hi-IN/sharing.mdx +1 -2
  698. package/docs/content/locales/hi-IN/skills-guide.mdx +2 -3
  699. package/docs/content/locales/hi-IN/template-plan.mdx +23 -7
  700. package/docs/content/locales/hi-IN/tracking.mdx +1 -1
  701. package/docs/content/locales/hi-IN/workspace-connections.mdx +1 -1
  702. package/docs/content/locales/ja-JP/cloneable-saas.mdx +0 -1
  703. package/docs/content/locales/ja-JP/plan-plugin.mdx +10 -9
  704. package/docs/content/locales/ja-JP/sharing.mdx +1 -2
  705. package/docs/content/locales/ja-JP/skills-guide.mdx +2 -3
  706. package/docs/content/locales/ja-JP/template-plan.mdx +23 -7
  707. package/docs/content/locales/ja-JP/tracking.mdx +1 -1
  708. package/docs/content/locales/ja-JP/workspace-connections.mdx +1 -1
  709. package/docs/content/locales/ko-KR/cloneable-saas.mdx +0 -1
  710. package/docs/content/locales/ko-KR/plan-plugin.mdx +10 -9
  711. package/docs/content/locales/ko-KR/sharing.mdx +1 -2
  712. package/docs/content/locales/ko-KR/skills-guide.mdx +2 -3
  713. package/docs/content/locales/ko-KR/template-plan.mdx +23 -7
  714. package/docs/content/locales/ko-KR/tracking.mdx +1 -1
  715. package/docs/content/locales/ko-KR/workspace-connections.mdx +1 -1
  716. package/docs/content/locales/pt-BR/cloneable-saas.mdx +0 -1
  717. package/docs/content/locales/pt-BR/plan-plugin.mdx +10 -9
  718. package/docs/content/locales/pt-BR/sharing.mdx +1 -2
  719. package/docs/content/locales/pt-BR/skills-guide.mdx +2 -3
  720. package/docs/content/locales/pt-BR/template-plan.mdx +23 -7
  721. package/docs/content/locales/pt-BR/tracking.mdx +1 -1
  722. package/docs/content/locales/pt-BR/workspace-connections.mdx +1 -1
  723. package/docs/content/locales/zh-CN/cloneable-saas.mdx +0 -1
  724. package/docs/content/locales/zh-CN/plan-plugin.mdx +10 -9
  725. package/docs/content/locales/zh-CN/sharing.mdx +1 -2
  726. package/docs/content/locales/zh-CN/skills-guide.mdx +2 -3
  727. package/docs/content/locales/zh-CN/template-plan.mdx +23 -7
  728. package/docs/content/locales/zh-CN/tracking.mdx +1 -1
  729. package/docs/content/locales/zh-CN/workspace-connections.mdx +1 -1
  730. package/docs/content/locales/zh-TW/cloneable-saas.mdx +0 -1
  731. package/docs/content/locales/zh-TW/plan-plugin.mdx +10 -9
  732. package/docs/content/locales/zh-TW/sharing.mdx +1 -2
  733. package/docs/content/locales/zh-TW/skills-guide.mdx +2 -3
  734. package/docs/content/locales/zh-TW/template-plan.mdx +23 -7
  735. package/docs/content/locales/zh-TW/tracking.mdx +1 -1
  736. package/docs/content/locales/zh-TW/workspace-connections.mdx +1 -1
  737. package/docs/content/plan-plugin.mdx +12 -11
  738. package/docs/content/sharing.mdx +1 -2
  739. package/docs/content/skills-guide.mdx +4 -3
  740. package/docs/content/template-plan.mdx +37 -6
  741. package/docs/content/tracking.mdx +1 -1
  742. package/docs/content/workspace-connections.mdx +1 -1
  743. package/package.json +6 -6
  744. package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +82 -19
  745. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +15 -0
  746. package/src/templates/default/.agents/skills/storing-data/SKILL.md +8 -0
  747. package/src/templates/default/package.json +3 -3
  748. package/src/templates/workspace-core/.agents/skills/client-methods/SKILL.md +8 -0
  749. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +22 -0
  750. package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +82 -19
  751. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +15 -0
  752. package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +8 -0
  753. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -0
  754. package/corpus/core/docs/content/locales/ar-SA/template-videos.mdx +0 -336
  755. package/corpus/core/docs/content/locales/de-DE/template-videos.mdx +0 -336
  756. package/corpus/core/docs/content/locales/es-ES/template-videos.mdx +0 -336
  757. package/corpus/core/docs/content/locales/fr-FR/template-videos.mdx +0 -336
  758. package/corpus/core/docs/content/locales/hi-IN/template-videos.mdx +0 -336
  759. package/corpus/core/docs/content/locales/ja-JP/template-videos.mdx +0 -336
  760. package/corpus/core/docs/content/locales/ko-KR/template-videos.mdx +0 -336
  761. package/corpus/core/docs/content/locales/pt-BR/template-videos.mdx +0 -336
  762. package/corpus/core/docs/content/locales/zh-CN/template-videos.mdx +0 -334
  763. package/corpus/core/docs/content/locales/zh-TW/template-videos.mdx +0 -334
  764. package/corpus/core/docs/content/template-videos.mdx +0 -332
  765. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +0 -697
  766. package/corpus/templates/slides/app/components/editor/CodeEditor.tsx +0 -49
  767. package/corpus/templates/videos/.agents/skills/actions/SKILL.md +0 -432
  768. package/corpus/templates/videos/.agents/skills/animation-tracks/SKILL.md +0 -99
  769. package/corpus/templates/videos/.agents/skills/capture-learnings/SKILL.md +0 -76
  770. package/corpus/templates/videos/.agents/skills/composition-management/SKILL.md +0 -106
  771. package/corpus/templates/videos/.agents/skills/create-skill/SKILL.md +0 -221
  772. package/corpus/templates/videos/.agents/skills/delegate-to-agent/SKILL.md +0 -263
  773. package/corpus/templates/videos/.agents/skills/frontend-design/SKILL.md +0 -112
  774. package/corpus/templates/videos/.agents/skills/security/SKILL.md +0 -263
  775. package/corpus/templates/videos/.agents/skills/self-modifying-code/SKILL.md +0 -88
  776. package/corpus/templates/videos/.agents/skills/shadcn-ui/SKILL.md +0 -95
  777. package/corpus/templates/videos/.builder/ANIMATED_PROPERTIES.md +0 -335
  778. package/corpus/templates/videos/.builder/KEYFRAME_SYNC.md +0 -166
  779. package/corpus/templates/videos/.builder/SEAMLESS_SYNC.md +0 -211
  780. package/corpus/templates/videos/.builder/rules/deploy-app.mdc +0 -22
  781. package/corpus/templates/videos/.builder/rules/organize-ui.mdc +0 -9
  782. package/corpus/templates/videos/.builder/skills/remotion/SKILL.md +0 -29
  783. package/corpus/templates/videos/.builder/skills/remotion/rules/animations.md +0 -27
  784. package/corpus/templates/videos/.builder/skills/remotion/rules/assets.md +0 -43
  785. package/corpus/templates/videos/.builder/skills/remotion/rules/audio.md +0 -58
  786. package/corpus/templates/videos/.builder/skills/remotion/rules/compositions.md +0 -129
  787. package/corpus/templates/videos/.builder/skills/remotion/rules/fonts.md +0 -47
  788. package/corpus/templates/videos/.builder/skills/remotion/rules/images.md +0 -41
  789. package/corpus/templates/videos/.builder/skills/remotion/rules/parameters.md +0 -59
  790. package/corpus/templates/videos/.builder/skills/remotion/rules/sequencing.md +0 -54
  791. package/corpus/templates/videos/.builder/skills/remotion/rules/tailwind.md +0 -11
  792. package/corpus/templates/videos/.builder/skills/remotion/rules/text-animations.md +0 -18
  793. package/corpus/templates/videos/.builder/skills/remotion/rules/timing.md +0 -103
  794. package/corpus/templates/videos/.builder/skills/remotion/rules/transparent-videos.md +0 -35
  795. package/corpus/templates/videos/.builder/skills/remotion/rules/trimming.md +0 -38
  796. package/corpus/templates/videos/.builder/skills/remotion/rules/videos.md +0 -54
  797. package/corpus/templates/videos/.dockerignore +0 -62
  798. package/corpus/templates/videos/.env.example +0 -4
  799. package/corpus/templates/videos/.oxfmtrc.json +0 -8
  800. package/corpus/templates/videos/AGENTS.md +0 -46
  801. package/corpus/templates/videos/CHANGELOG.md +0 -10
  802. package/corpus/templates/videos/DEVELOPING.md +0 -143
  803. package/corpus/templates/videos/README.md +0 -33
  804. package/corpus/templates/videos/actions/analyze-brand-assets.ts +0 -151
  805. package/corpus/templates/videos/actions/apply-design-system.ts +0 -35
  806. package/corpus/templates/videos/actions/create-design-system.ts +0 -84
  807. package/corpus/templates/videos/actions/create-folder.ts +0 -48
  808. package/corpus/templates/videos/actions/db-connect.ts +0 -45
  809. package/corpus/templates/videos/actions/db-health.ts +0 -24
  810. package/corpus/templates/videos/actions/db-status.ts +0 -28
  811. package/corpus/templates/videos/actions/delete-composition.ts +0 -272
  812. package/corpus/templates/videos/actions/delete-folder.ts +0 -27
  813. package/corpus/templates/videos/actions/generate-animated-component.ts +0 -470
  814. package/corpus/templates/videos/actions/get-composition.ts +0 -36
  815. package/corpus/templates/videos/actions/get-design-system.ts +0 -35
  816. package/corpus/templates/videos/actions/import-code.ts +0 -67
  817. package/corpus/templates/videos/actions/import-design-project.ts +0 -42
  818. package/corpus/templates/videos/actions/import-document.ts +0 -125
  819. package/corpus/templates/videos/actions/import-from-url.ts +0 -18
  820. package/corpus/templates/videos/actions/import-github.ts +0 -182
  821. package/corpus/templates/videos/actions/list-compositions.ts +0 -32
  822. package/corpus/templates/videos/actions/list-design-systems.ts +0 -66
  823. package/corpus/templates/videos/actions/list-folders.ts +0 -61
  824. package/corpus/templates/videos/actions/move-composition-to-folder.ts +0 -59
  825. package/corpus/templates/videos/actions/navigate.ts +0 -40
  826. package/corpus/templates/videos/actions/rename-folder.ts +0 -41
  827. package/corpus/templates/videos/actions/run.ts +0 -2
  828. package/corpus/templates/videos/actions/save-composition.ts +0 -114
  829. package/corpus/templates/videos/actions/set-default-design-system.ts +0 -40
  830. package/corpus/templates/videos/actions/update-composition.ts +0 -81
  831. package/corpus/templates/videos/actions/update-design-system.ts +0 -68
  832. package/corpus/templates/videos/actions/validate-compositions.ts +0 -502
  833. package/corpus/templates/videos/actions/view-screen.ts +0 -48
  834. package/corpus/templates/videos/app/components/CameraControls.tsx +0 -442
  835. package/corpus/templates/videos/app/components/CameraToolbar.tsx +0 -437
  836. package/corpus/templates/videos/app/components/CloudUpgrade.tsx +0 -287
  837. package/corpus/templates/videos/app/components/CollabPresenceBar.tsx +0 -81
  838. package/corpus/templates/videos/app/components/CompSettingsEditor.tsx +0 -107
  839. package/corpus/templates/videos/app/components/ComponentLibrarySidebar.tsx +0 -457
  840. package/corpus/templates/videos/app/components/ComponentPropsPanel.tsx +0 -227
  841. package/corpus/templates/videos/app/components/ComponentsSidebar.tsx +0 -70
  842. package/corpus/templates/videos/app/components/CompositionCard.tsx +0 -147
  843. package/corpus/templates/videos/app/components/CurrentElementPanel.tsx +0 -835
  844. package/corpus/templates/videos/app/components/CursorControls.tsx +0 -664
  845. package/corpus/templates/videos/app/components/CursorPositioningOverlay.tsx +0 -238
  846. package/corpus/templates/videos/app/components/LibraryFolderRow.tsx +0 -235
  847. package/corpus/templates/videos/app/components/MotionCurveSelect.tsx +0 -61
  848. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +0 -192
  849. package/corpus/templates/videos/app/components/PropsEditor.tsx +0 -210
  850. package/corpus/templates/videos/app/components/QuestionFlow.tsx +0 -40
  851. package/corpus/templates/videos/app/components/Sidebar.tsx +0 -609
  852. package/corpus/templates/videos/app/components/StudioHeader.tsx +0 -109
  853. package/corpus/templates/videos/app/components/Timeline.tsx +0 -2067
  854. package/corpus/templates/videos/app/components/TrackPropertiesPanel.tsx +0 -922
  855. package/corpus/templates/videos/app/components/TweaksPanel.tsx +0 -277
  856. package/corpus/templates/videos/app/components/VideoPlayer.tsx +0 -487
  857. package/corpus/templates/videos/app/components/design-system/DesignSystemCard.tsx +0 -134
  858. package/corpus/templates/videos/app/components/design-system/DesignSystemPreview.tsx +0 -176
  859. package/corpus/templates/videos/app/components/design-system/DesignSystemSetup.tsx +0 -707
  860. package/corpus/templates/videos/app/components/keyframes/KeyframeActionButtons.tsx +0 -78
  861. package/corpus/templates/videos/app/components/keyframes/KeyframeNavigation.tsx +0 -59
  862. package/corpus/templates/videos/app/components/keyframes/PropertyNumberInput.tsx +0 -56
  863. package/corpus/templates/videos/app/components/layout/Header.tsx +0 -64
  864. package/corpus/templates/videos/app/components/layout/HeaderActions.tsx +0 -84
  865. package/corpus/templates/videos/app/components/layout/Layout.tsx +0 -132
  866. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +0 -180
  867. package/corpus/templates/videos/app/components/ui/accordion.tsx +0 -56
  868. package/corpus/templates/videos/app/components/ui/alert-dialog.tsx +0 -139
  869. package/corpus/templates/videos/app/components/ui/alert.tsx +0 -59
  870. package/corpus/templates/videos/app/components/ui/aspect-ratio.tsx +0 -5
  871. package/corpus/templates/videos/app/components/ui/avatar.tsx +0 -48
  872. package/corpus/templates/videos/app/components/ui/badge.tsx +0 -37
  873. package/corpus/templates/videos/app/components/ui/breadcrumb.tsx +0 -115
  874. package/corpus/templates/videos/app/components/ui/button.tsx +0 -57
  875. package/corpus/templates/videos/app/components/ui/calendar.tsx +0 -68
  876. package/corpus/templates/videos/app/components/ui/card.tsx +0 -86
  877. package/corpus/templates/videos/app/components/ui/carousel.tsx +0 -260
  878. package/corpus/templates/videos/app/components/ui/chart.tsx +0 -375
  879. package/corpus/templates/videos/app/components/ui/checkbox.tsx +0 -28
  880. package/corpus/templates/videos/app/components/ui/collapsible.tsx +0 -9
  881. package/corpus/templates/videos/app/components/ui/command.tsx +0 -154
  882. package/corpus/templates/videos/app/components/ui/context-menu.tsx +0 -202
  883. package/corpus/templates/videos/app/components/ui/dialog.tsx +0 -130
  884. package/corpus/templates/videos/app/components/ui/drawer.tsx +0 -116
  885. package/corpus/templates/videos/app/components/ui/dropdown-menu.tsx +0 -212
  886. package/corpus/templates/videos/app/components/ui/form.tsx +0 -177
  887. package/corpus/templates/videos/app/components/ui/hover-card.tsx +0 -27
  888. package/corpus/templates/videos/app/components/ui/input-otp.tsx +0 -69
  889. package/corpus/templates/videos/app/components/ui/input.tsx +0 -22
  890. package/corpus/templates/videos/app/components/ui/label.tsx +0 -24
  891. package/corpus/templates/videos/app/components/ui/menubar.tsx +0 -234
  892. package/corpus/templates/videos/app/components/ui/navigation-menu.tsx +0 -128
  893. package/corpus/templates/videos/app/components/ui/pagination.tsx +0 -121
  894. package/corpus/templates/videos/app/components/ui/popover.tsx +0 -44
  895. package/corpus/templates/videos/app/components/ui/progress.tsx +0 -26
  896. package/corpus/templates/videos/app/components/ui/radio-group.tsx +0 -42
  897. package/corpus/templates/videos/app/components/ui/resizable.tsx +0 -43
  898. package/corpus/templates/videos/app/components/ui/scroll-area.tsx +0 -46
  899. package/corpus/templates/videos/app/components/ui/select.tsx +0 -158
  900. package/corpus/templates/videos/app/components/ui/separator.tsx +0 -29
  901. package/corpus/templates/videos/app/components/ui/sheet.tsx +0 -160
  902. package/corpus/templates/videos/app/components/ui/sidebar.tsx +0 -782
  903. package/corpus/templates/videos/app/components/ui/skeleton.tsx +0 -15
  904. package/corpus/templates/videos/app/components/ui/slider.tsx +0 -26
  905. package/corpus/templates/videos/app/components/ui/sonner.tsx +0 -58
  906. package/corpus/templates/videos/app/components/ui/spinner.tsx +0 -17
  907. package/corpus/templates/videos/app/components/ui/switch.tsx +0 -27
  908. package/corpus/templates/videos/app/components/ui/table.tsx +0 -117
  909. package/corpus/templates/videos/app/components/ui/tabs.tsx +0 -53
  910. package/corpus/templates/videos/app/components/ui/textarea.tsx +0 -23
  911. package/corpus/templates/videos/app/components/ui/toast.tsx +0 -127
  912. package/corpus/templates/videos/app/components/ui/toaster.tsx +0 -33
  913. package/corpus/templates/videos/app/components/ui/toggle-group.tsx +0 -59
  914. package/corpus/templates/videos/app/components/ui/toggle.tsx +0 -43
  915. package/corpus/templates/videos/app/components/ui/tooltip.tsx +0 -30
  916. package/corpus/templates/videos/app/components/ui/use-toast.ts +0 -3
  917. package/corpus/templates/videos/app/config/constants.ts +0 -204
  918. package/corpus/templates/videos/app/contexts/CompositionContext.tsx +0 -437
  919. package/corpus/templates/videos/app/contexts/CurrentElementContext.tsx +0 -247
  920. package/corpus/templates/videos/app/contexts/PlaybackContext.tsx +0 -65
  921. package/corpus/templates/videos/app/contexts/TimelineContext.tsx +0 -433
  922. package/corpus/templates/videos/app/entry.client.tsx +0 -19
  923. package/corpus/templates/videos/app/entry.server.tsx +0 -10
  924. package/corpus/templates/videos/app/global.css +0 -171
  925. package/corpus/templates/videos/app/hooks/use-composition-collab.ts +0 -269
  926. package/corpus/templates/videos/app/hooks/use-composition-design-system.ts +0 -47
  927. package/corpus/templates/videos/app/hooks/use-database-compositions.ts +0 -144
  928. package/corpus/templates/videos/app/hooks/use-db-status.ts +0 -30
  929. package/corpus/templates/videos/app/hooks/use-design-systems.ts +0 -21
  930. package/corpus/templates/videos/app/hooks/use-folders.ts +0 -199
  931. package/corpus/templates/videos/app/hooks/use-mobile.tsx +0 -21
  932. package/corpus/templates/videos/app/hooks/use-navigation-state.ts +0 -52
  933. package/corpus/templates/videos/app/hooks/use-question-flow.ts +0 -14
  934. package/corpus/templates/videos/app/hooks/use-toast.ts +0 -188
  935. package/corpus/templates/videos/app/hooks/useUnsavedChanges.ts +0 -48
  936. package/corpus/templates/videos/app/i18n/ar-SA.ts +0 -446
  937. package/corpus/templates/videos/app/i18n/de-DE.ts +0 -470
  938. package/corpus/templates/videos/app/i18n/en-US.ts +0 -452
  939. package/corpus/templates/videos/app/i18n/es-ES.ts +0 -468
  940. package/corpus/templates/videos/app/i18n/fr-FR.ts +0 -469
  941. package/corpus/templates/videos/app/i18n/hi-IN.ts +0 -446
  942. package/corpus/templates/videos/app/i18n/index.ts +0 -34
  943. package/corpus/templates/videos/app/i18n/ja-JP.ts +0 -460
  944. package/corpus/templates/videos/app/i18n/ko-KR.ts +0 -453
  945. package/corpus/templates/videos/app/i18n/pt-BR.ts +0 -461
  946. package/corpus/templates/videos/app/i18n/zh-CN.ts +0 -437
  947. package/corpus/templates/videos/app/i18n/zh-TW.ts +0 -437
  948. package/corpus/templates/videos/app/lib/database-compositions.ts +0 -67
  949. package/corpus/templates/videos/app/lib/design-systems.ts +0 -234
  950. package/corpus/templates/videos/app/lib/tab-id.ts +0 -1
  951. package/corpus/templates/videos/app/lib/utils.ts +0 -1
  952. package/corpus/templates/videos/app/pages/ComponentLibrary.tsx +0 -134
  953. package/corpus/templates/videos/app/pages/ComponentLibraryView.tsx +0 -362
  954. package/corpus/templates/videos/app/pages/CompositionView.tsx +0 -715
  955. package/corpus/templates/videos/app/pages/DesignSystems.tsx +0 -180
  956. package/corpus/templates/videos/app/pages/Index.tsx +0 -258
  957. package/corpus/templates/videos/app/pages/NewComposition.tsx +0 -95
  958. package/corpus/templates/videos/app/pages/NotFound.tsx +0 -36
  959. package/corpus/templates/videos/app/remotion/CameraHost.tsx +0 -223
  960. package/corpus/templates/videos/app/remotion/animations/useElementAnimations.ts +0 -231
  961. package/corpus/templates/videos/app/remotion/componentRegistry.ts +0 -473
  962. package/corpus/templates/videos/app/remotion/components/AnimatedElement.tsx +0 -436
  963. package/corpus/templates/videos/app/remotion/components/InteractiveElement.tsx +0 -159
  964. package/corpus/templates/videos/app/remotion/components/InteractiveZoneEditor.tsx +0 -273
  965. package/corpus/templates/videos/app/remotion/compositions/BlankComposition.tsx +0 -204
  966. package/corpus/templates/videos/app/remotion/compositions/index.ts +0 -2
  967. package/corpus/templates/videos/app/remotion/createComponentCursorTrack.ts +0 -132
  968. package/corpus/templates/videos/app/remotion/docs/CURSOR_TRACKS.md +0 -285
  969. package/corpus/templates/videos/app/remotion/easingFunctions.ts +0 -255
  970. package/corpus/templates/videos/app/remotion/hooks/createInteractiveComposition.tsx +0 -283
  971. package/corpus/templates/videos/app/remotion/hooks/useCursorHistory.ts +0 -72
  972. package/corpus/templates/videos/app/remotion/hooks/useCursorTypeFromHover.ts +0 -35
  973. package/corpus/templates/videos/app/remotion/hooks/useEditableZones.tsx +0 -267
  974. package/corpus/templates/videos/app/remotion/hooks/useHoverAnimation.ts +0 -90
  975. package/corpus/templates/videos/app/remotion/hooks/useHoverAnimationSmooth.ts +0 -146
  976. package/corpus/templates/videos/app/remotion/hooks/useInteractiveComponent.ts +0 -711
  977. package/corpus/templates/videos/app/remotion/hooks/useRegisterInteractiveElement.ts +0 -56
  978. package/corpus/templates/videos/app/remotion/library-components/Button.tsx +0 -252
  979. package/corpus/templates/videos/app/remotion/library-components/Card.tsx +0 -241
  980. package/corpus/templates/videos/app/remotion/library-components/CodePanel.tsx +0 -1005
  981. package/corpus/templates/videos/app/remotion/library-components/FileItem.tsx +0 -196
  982. package/corpus/templates/videos/app/remotion/library-components/FolderItem.tsx +0 -199
  983. package/corpus/templates/videos/app/remotion/library-components/PrimaryButton.tsx +0 -177
  984. package/corpus/templates/videos/app/remotion/library-components/SecondaryButton.tsx +0 -167
  985. package/corpus/templates/videos/app/remotion/library-components/SectionHeader.tsx +0 -108
  986. package/corpus/templates/videos/app/remotion/library-components/TEMPLATE.tsx +0 -211
  987. package/corpus/templates/videos/app/remotion/library-components/index.ts +0 -10
  988. package/corpus/templates/videos/app/remotion/registry.ts +0 -91
  989. package/corpus/templates/videos/app/remotion/testing/animationTestUtils.ts +0 -366
  990. package/corpus/templates/videos/app/remotion/trackAnimation.ts +0 -179
  991. package/corpus/templates/videos/app/remotion/trackHelpers.ts +0 -110
  992. package/corpus/templates/videos/app/remotion/ui-components/Cursor.tsx +0 -182
  993. package/corpus/templates/videos/app/remotion/ui-components/InteractiveButton.tsx +0 -116
  994. package/corpus/templates/videos/app/remotion/ui-components/InteractiveCard.tsx +0 -126
  995. package/corpus/templates/videos/app/remotion/ui-components/index.ts +0 -16
  996. package/corpus/templates/videos/app/remotion/utils/animationHelpers.ts +0 -444
  997. package/corpus/templates/videos/app/remotion/utils/interactiveElements.ts +0 -235
  998. package/corpus/templates/videos/app/remotion/utils/springAnimation.ts +0 -76
  999. package/corpus/templates/videos/app/root.tsx +0 -165
  1000. package/corpus/templates/videos/app/routes/$.tsx +0 -10
  1001. package/corpus/templates/videos/app/routes/_index.tsx +0 -25
  1002. package/corpus/templates/videos/app/routes/c.$compositionId.tsx +0 -10
  1003. package/corpus/templates/videos/app/routes/components.tsx +0 -10
  1004. package/corpus/templates/videos/app/routes/design-systems.tsx +0 -10
  1005. package/corpus/templates/videos/app/routes/extensions.$id.$slug.tsx +0 -2
  1006. package/corpus/templates/videos/app/routes/extensions.$id.tsx +0 -5
  1007. package/corpus/templates/videos/app/routes/extensions._index.tsx +0 -5
  1008. package/corpus/templates/videos/app/routes/extensions.tsx +0 -5
  1009. package/corpus/templates/videos/app/routes/settings.tsx +0 -88
  1010. package/corpus/templates/videos/app/routes/team.tsx +0 -11
  1011. package/corpus/templates/videos/app/routes.ts +0 -4
  1012. package/corpus/templates/videos/app/state.ts +0 -77
  1013. package/corpus/templates/videos/app/types/elementAnimations.ts +0 -121
  1014. package/corpus/templates/videos/app/types.ts +0 -200
  1015. package/corpus/templates/videos/app/utils/compositionHelpers.ts +0 -445
  1016. package/corpus/templates/videos/app/utils/debug.ts +0 -63
  1017. package/corpus/templates/videos/app/utils/keyframeUtils.ts +0 -331
  1018. package/corpus/templates/videos/app/utils/timelineCoordinates.ts +0 -79
  1019. package/corpus/templates/videos/app/vite-env.d.ts +0 -6
  1020. package/corpus/templates/videos/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +0 -6
  1021. package/corpus/templates/videos/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +0 -6
  1022. package/corpus/templates/videos/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +0 -6
  1023. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +0 -6
  1024. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +0 -6
  1025. package/corpus/templates/videos/components.json +0 -20
  1026. package/corpus/templates/videos/data/sync-config.json +0 -3
  1027. package/corpus/templates/videos/docs/examples/create-composition.example.ts +0 -128
  1028. package/corpus/templates/videos/drizzle.config.ts +0 -3
  1029. package/corpus/templates/videos/learnings.defaults.md +0 -5
  1030. package/corpus/templates/videos/package.json +0 -114
  1031. package/corpus/templates/videos/public/agent-native-icon-dark.svg +0 -10
  1032. package/corpus/templates/videos/public/agent-native-icon-light.svg +0 -10
  1033. package/corpus/templates/videos/public/agent-native-logo-dark.svg +0 -21
  1034. package/corpus/templates/videos/public/agent-native-logo-light.svg +0 -21
  1035. package/corpus/templates/videos/public/favicon.svg +0 -1
  1036. package/corpus/templates/videos/public/icon-180.svg +0 -1
  1037. package/corpus/templates/videos/public/icon-192.svg +0 -1
  1038. package/corpus/templates/videos/public/icon-512.svg +0 -1
  1039. package/corpus/templates/videos/public/manifest.json +0 -21
  1040. package/corpus/templates/videos/public/placeholder.svg +0 -1
  1041. package/corpus/templates/videos/public/robots.txt +0 -14
  1042. package/corpus/templates/videos/react-router.config.ts +0 -7
  1043. package/corpus/templates/videos/server/db/index.ts +0 -37
  1044. package/corpus/templates/videos/server/db/schema.ts +0 -55
  1045. package/corpus/templates/videos/server/middleware/auth.ts +0 -15
  1046. package/corpus/templates/videos/server/plugins/agent-chat.ts +0 -29
  1047. package/corpus/templates/videos/server/plugins/auth.ts +0 -14
  1048. package/corpus/templates/videos/server/plugins/collab.ts +0 -11
  1049. package/corpus/templates/videos/server/plugins/core-routes.ts +0 -22
  1050. package/corpus/templates/videos/server/plugins/db.ts +0 -160
  1051. package/corpus/templates/videos/server/routes/[...page].get.ts +0 -5
  1052. package/corpus/templates/videos/server/routes/[...page].head.ts +0 -1
  1053. package/corpus/templates/videos/server/routes/api/save-composition-defaults.post.ts +0 -274
  1054. package/corpus/templates/videos/shared/api.ts +0 -75
  1055. package/corpus/templates/videos/ssr-entry.ts +0 -15
  1056. package/corpus/templates/videos/tsconfig.json +0 -21
  1057. package/corpus/templates/videos/vite.config.ts +0 -19
  1058. package/corpus/templates/videos/vitest.config.ts +0 -16
  1059. package/docs/content/locales/ar-SA/template-videos.mdx +0 -336
  1060. package/docs/content/locales/de-DE/template-videos.mdx +0 -336
  1061. package/docs/content/locales/es-ES/template-videos.mdx +0 -336
  1062. package/docs/content/locales/fr-FR/template-videos.mdx +0 -336
  1063. package/docs/content/locales/hi-IN/template-videos.mdx +0 -336
  1064. package/docs/content/locales/ja-JP/template-videos.mdx +0 -336
  1065. package/docs/content/locales/ko-KR/template-videos.mdx +0 -336
  1066. package/docs/content/locales/pt-BR/template-videos.mdx +0 -336
  1067. package/docs/content/locales/zh-CN/template-videos.mdx +0 -334
  1068. package/docs/content/locales/zh-TW/template-videos.mdx +0 -334
  1069. package/docs/content/template-videos.mdx +0 -332
@@ -67,6 +67,7 @@ import {
67
67
  } from "@tabler/icons-react";
68
68
  import { useQueryClient } from "@tanstack/react-query";
69
69
  import {
70
+ memo,
70
71
  useCallback,
71
72
  useEffect,
72
73
  useMemo,
@@ -82,6 +83,7 @@ import {
82
83
  DropdownMenu,
83
84
  DropdownMenuContent,
84
85
  DropdownMenuItem,
86
+ DropdownMenuSeparator,
85
87
  DropdownMenuTrigger,
86
88
  } from "@/components/ui/dropdown-menu";
87
89
  import { Input } from "@/components/ui/input";
@@ -129,6 +131,7 @@ import {
129
131
  type DesignGradientStopPatch,
130
132
  type DesignGradientType,
131
133
  type ImageFillValue,
134
+ type ScrubInputChangeMeta,
132
135
  } from "./inspector";
133
136
  import { IconLayoutSettings } from "./inspector/design-icons";
134
137
  import type { DesignPaintType } from "./inspector/DesignColorPicker";
@@ -140,6 +143,33 @@ import type { ElementInfo } from "./types";
140
143
 
141
144
  export type InspectorTab = "design" | "tweaks";
142
145
 
146
+ /**
147
+ * PF12: gesture-lifecycle metadata threaded alongside a style commit.
148
+ *
149
+ * - "preview": a live, in-progress tick (a ScrubInput scrub sample or a
150
+ * DesignColorPicker drag tick) — cheap to show in the live iframe preview,
151
+ * but must NOT trigger the expensive source commit (projection parse + HTML
152
+ * patch + history entry) on every tick.
153
+ * - "commit" (or omitted, for callers that don't pass meta at all): the
154
+ * gesture's authoritative final value — exactly one per gesture — which
155
+ * DOES trigger the full source commit. Omitting meta entirely preserves
156
+ * prior behavior (treated as "commit") for every non-scrub/color call site.
157
+ */
158
+ export interface StyleChangeMeta {
159
+ phase?: "preview" | "commit";
160
+ }
161
+
162
+ export type StyleChangeHandler = (
163
+ property: string,
164
+ value: string,
165
+ meta?: StyleChangeMeta,
166
+ ) => void;
167
+
168
+ export type StylesChangeHandler = (
169
+ styles: Record<string, string>,
170
+ meta?: StyleChangeMeta,
171
+ ) => void;
172
+
143
173
  const MIXED_VALUE = "Mixed";
144
174
 
145
175
  function isMixedValue(value: string | undefined): boolean {
@@ -152,7 +182,7 @@ function sameOrMixed(values: string[]): string {
152
182
  return values.every((value) => value === first) ? first : MIXED_VALUE;
153
183
  }
154
184
 
155
- function mixedElementFromSelection(
185
+ export function mixedElementFromSelection(
156
186
  elements: ElementInfo[],
157
187
  ): ElementInfo | null {
158
188
  const base = elements[elements.length - 1];
@@ -167,6 +197,27 @@ function mixedElementFromSelection(
167
197
  sameOrMixed(elements.map((element) => element.computedStyles[key] ?? "")),
168
198
  ]),
169
199
  );
200
+ // Mix inlineStyles the same way as computedStyles so authoredStyleValue()
201
+ // sees a proper Mixed sentinel across a multi-selection instead of
202
+ // silently inheriting the last-selected element's raw inline value
203
+ // (spreading ...base alone would leak that stale single-element value).
204
+ const inlineStyleKeys = new Set<string>();
205
+ elements.forEach((element) => {
206
+ Object.keys(element.inlineStyles ?? {}).forEach((key) =>
207
+ inlineStyleKeys.add(key),
208
+ );
209
+ });
210
+ const inlineStyles =
211
+ inlineStyleKeys.size > 0
212
+ ? Object.fromEntries(
213
+ Array.from(inlineStyleKeys).map((key) => [
214
+ key,
215
+ sameOrMixed(
216
+ elements.map((element) => element.inlineStyles?.[key] ?? ""),
217
+ ),
218
+ ]),
219
+ )
220
+ : undefined;
170
221
  const minX = Math.min(...elements.map((element) => element.boundingRect.x));
171
222
  const minY = Math.min(...elements.map((element) => element.boundingRect.y));
172
223
  const maxX = Math.max(
@@ -187,6 +238,12 @@ function mixedElementFromSelection(
187
238
  selector: base.selector,
188
239
  classes: [],
189
240
  computedStyles,
241
+ inlineStyles,
242
+ // Mix like tagName above — otherwise isTextElement() would trust
243
+ // base.primitiveKind alone and misclassify a mixed text+shape selection.
244
+ primitiveKind: sameOrMixed(
245
+ elements.map((element) => element.primitiveKind ?? ""),
246
+ ),
190
247
  boundingRect: {
191
248
  x: minX,
192
249
  y: minY,
@@ -217,8 +274,8 @@ interface EditPanelProps {
217
274
  tweakValues?: Record<string, string | number | boolean>;
218
275
  onTweakChange?: (id: string, value: string | number | boolean) => void;
219
276
  onRequestTweaks?: (anchor: HTMLElement) => void;
220
- onStyleChange: (property: string, value: string) => void;
221
- onStylesChange?: (styles: Record<string, string>) => void;
277
+ onStyleChange: StyleChangeHandler;
278
+ onStylesChange?: StylesChangeHandler;
222
279
  onExport?: (settings: ExportSettingsValue[]) => void;
223
280
  exporting?: boolean;
224
281
  /** Active file id — used for component prop editing context. */
@@ -393,6 +450,9 @@ function PropInput({
393
450
  }) {
394
451
  const [draft, setDraft] = useState(value);
395
452
  const mixed = isMixedValue(value);
453
+ // Escape reverts and blurs; the blur handler must then skip its commit or it
454
+ // would re-commit the stale draft closure (mirrors ScrubInput's Escape path).
455
+ const skipNextBlurCommitRef = useRef(false);
396
456
 
397
457
  useEffect(() => {
398
458
  setDraft(value);
@@ -431,12 +491,27 @@ function PropInput({
431
491
  // defaultUnit) keep the responsive live-update behavior.
432
492
  if (defaultUnit === undefined) onChange(e.target.value);
433
493
  }}
434
- onBlur={commit}
494
+ onBlur={() => {
495
+ if (skipNextBlurCommitRef.current) {
496
+ skipNextBlurCommitRef.current = false;
497
+ return;
498
+ }
499
+ commit();
500
+ }}
435
501
  onKeyDown={(e) => {
436
502
  if (e.key === "Enter") {
437
503
  e.preventDefault();
438
504
  commit();
439
505
  (e.currentTarget as HTMLInputElement).blur();
506
+ return;
507
+ }
508
+ if (e.key === "Escape") {
509
+ e.preventDefault();
510
+ // Revert the draft to the last committed value and blur, matching
511
+ // ScrubInput's Escape behavior.
512
+ setDraft(value);
513
+ skipNextBlurCommitRef.current = true;
514
+ (e.currentTarget as HTMLInputElement).blur();
440
515
  }
441
516
  }}
442
517
  placeholder={placeholder}
@@ -465,7 +540,7 @@ function ColorInput({
465
540
  }: {
466
541
  label: string;
467
542
  value: string;
468
- onChange: (value: string) => void;
543
+ onChange: (value: string, meta?: StyleChangeMeta) => void;
469
544
  backgroundImage?: string;
470
545
  backgroundSize?: string;
471
546
  backgroundRepeat?: string;
@@ -533,9 +608,28 @@ function ColorInput({
533
608
  }
534
609
  }, [activeStopIds, selectedStopId]);
535
610
 
536
- const setNext = (next: string) => {
611
+ // PF12: `phase` defaults to "commit" so every discrete/one-shot caller
612
+ // (swatch clicks, paint-type switches, hex commit, fill-row edits) keeps
613
+ // committing immediately as before. Only the raw per-tick `onChange` wired
614
+ // to DesignColorPicker below passes "preview" explicitly — the picker's own
615
+ // `onChangeComplete` re-invokes setNext with the same final value tagged
616
+ // "commit" once the gesture ends (see the DesignColorPicker render below).
617
+ const setNext = (next: string, phase: "preview" | "commit" = "commit") => {
618
+ // Guard rail for callers that don't wire onBackgroundImageChange (i.e.
619
+ // supportsLayeredFills is false, e.g. the text-fill "color" row): the
620
+ // picker manages gradient/image paint-type selection as *local* UI state
621
+ // independent of props (see DesignColorPicker's localPaintType), so a
622
+ // user can still open the Gradient/Image tab there even when this
623
+ // ColorInput never offered layered fills. When that happens,
624
+ // emitPaintValue falls back to this onChange with a full gradient/url()
625
+ // CSS string, which is invalid for a plain color property (color /
626
+ // backgroundColor) and gets silently dropped by the browser — but not
627
+ // before clobbering the last-known-good value in this component's own
628
+ // state. Reject anything that doesn't parse as a plain solid color in
629
+ // that case instead of forwarding it.
630
+ if (!supportsLayeredFills && !parseCssColor(next)) return;
537
631
  setDraft(next);
538
- onChange(next);
632
+ onChange(next, { phase });
539
633
  };
540
634
 
541
635
  const replaceBackgroundLayer = (index: number, nextLayer: string) => {
@@ -794,7 +888,14 @@ function ColorInput({
794
888
  key={pickerKey}
795
889
  label={label}
796
890
  value={pickerValue}
797
- onChange={setNext}
891
+ // PF12: `onChange` fires on every SV/hue/alpha drag tick — tag those as
892
+ // "preview" so the caller can skip the expensive source commit and only
893
+ // update the live iframe preview. `onChangeComplete` fires exactly once
894
+ // per gesture (drag-end, hex commit, keyboard nudge, swatch click,
895
+ // paint-type switch) with the same final value, tagged "commit" so the
896
+ // authoritative source write always happens exactly once.
897
+ onChange={(v) => setNext(v, "preview")}
898
+ onChangeComplete={(v) => setNext(v, "commit")}
798
899
  onPaintValueChange={
799
900
  supportsLayeredFills ? handlePaintValueChange : undefined
800
901
  }
@@ -911,6 +1012,46 @@ function averageGradientOpacity(stops: DesignGradientStop[]): number {
911
1012
  return Math.round(total / stops.length);
912
1013
  }
913
1014
 
1015
+ /**
1016
+ * Marker used to non-destructively hide a single backgroundImage layer
1017
+ * (gradient or image). CSS strips comments from computed style values — a
1018
+ * trailing comment appended to a backgroundImage layer does not survive
1019
+ * getComputedStyle (verified: browsers normalize/serialize computed values
1020
+ * without their source comments) — so we can't tag the layer text itself.
1021
+ * Instead we pair the untouched original layer with a zero-size
1022
+ * background-size entry at the same index: `background-size: 0px 0px` makes
1023
+ * that layer render nothing while backgroundImage keeps the exact original
1024
+ * CSS text. Both backgroundImage and backgroundSize are real, valid,
1025
+ * positionally-paired CSS lists that DO round-trip through computed style,
1026
+ * so hiding survives reselect/reload with no React state stash required.
1027
+ */
1028
+ const HIDDEN_LAYER_SIZE_MARKER = "0px 0px";
1029
+
1030
+ export function isLayerHiddenBySize(sizeEntry: string | undefined): boolean {
1031
+ return (
1032
+ (sizeEntry ?? "").trim().replace(/\s+/g, " ") === HIDDEN_LAYER_SIZE_MARKER
1033
+ );
1034
+ }
1035
+
1036
+ /**
1037
+ * Rewrites the background-size list so `index` is hidden/shown via the
1038
+ * zero-size marker, padding shorter lists with "auto" (the CSS default) so
1039
+ * every other layer keeps rendering at its current/default size.
1040
+ */
1041
+ export function withLayerSizeMarker(
1042
+ sizeLayers: string[],
1043
+ layerCount: number,
1044
+ index: number,
1045
+ hidden: boolean,
1046
+ ): string {
1047
+ const next = Array.from(
1048
+ { length: layerCount },
1049
+ (_, i) => sizeLayers[i] || "auto",
1050
+ );
1051
+ next[index] = hidden ? HIDDEN_LAYER_SIZE_MARKER : "auto";
1052
+ return joinCssLayers(next);
1053
+ }
1054
+
914
1055
  function splitCssLayers(value: string): string[] {
915
1056
  const trimmed = value.trim();
916
1057
  if (!trimmed || trimmed === "none") return [];
@@ -1578,8 +1719,30 @@ function parseScaleValue(value: string | undefined): [number, number] {
1578
1719
  return [Number.isFinite(x) ? x : 1, Number.isFinite(y) ? y : 1];
1579
1720
  }
1580
1721
 
1581
- function mergeRotationValue(transform: string | undefined, degrees: number) {
1582
- const nextRotate = `rotate(${Math.round(degrees * 10) / 10}deg)`;
1722
+ /**
1723
+ * Normalize an angle in degrees into the (-180, 180] range the inspector
1724
+ * displays and commits (design-tool convention: 270° reads as -90°, a full
1725
+ * 360° turn reads as 0°). Exported for tests.
1726
+ */
1727
+ export function normalizeRotationDegrees(degrees: number): number {
1728
+ if (!Number.isFinite(degrees)) return 0;
1729
+ let normalized = degrees % 360;
1730
+ if (normalized > 180) normalized -= 360;
1731
+ else if (normalized <= -180) normalized += 360;
1732
+ return Object.is(normalized, -0) ? 0 : normalized;
1733
+ }
1734
+
1735
+ /** Exported for tests. */
1736
+ export function mergeRotationValue(
1737
+ transform: string | undefined,
1738
+ degrees: number,
1739
+ ) {
1740
+ // Round first, then normalize, so a rounded -179.96 → -180 still lands
1741
+ // inside (-180, 180] as +180.
1742
+ const normalizedDegrees = normalizeRotationDegrees(
1743
+ Math.round(degrees * 10) / 10,
1744
+ );
1745
+ const nextRotate = `rotate(${normalizedDegrees}deg)`;
1583
1746
  if (!transform || transform === "none") return nextRotate;
1584
1747
  // Replace an existing rotate()/rotateZ() in ANY unit so we don't append a
1585
1748
  // second rotate() (which would compound, e.g. "rotate(0.5turn) rotate(30deg)").
@@ -1628,7 +1791,7 @@ function ScrubStyleInput({
1628
1791
  label: string;
1629
1792
  value: string;
1630
1793
  placeholder?: number;
1631
- onChange: (value: number) => void;
1794
+ onChange: (value: number, meta?: ScrubInputChangeMeta) => void;
1632
1795
  unit?: string;
1633
1796
  min?: number;
1634
1797
  max?: number;
@@ -1670,8 +1833,8 @@ function ScrubStyleInput({
1670
1833
 
1671
1834
  function commitStylePatch(
1672
1835
  styles: Record<string, string>,
1673
- onStyleChange: (property: string, value: string) => void,
1674
- onStylesChange?: (styles: Record<string, string>) => void,
1836
+ onStyleChange: StyleChangeHandler,
1837
+ onStylesChange?: StylesChangeHandler,
1675
1838
  ) {
1676
1839
  if (onStylesChange) {
1677
1840
  onStylesChange(styles);
@@ -1695,6 +1858,11 @@ function cssLengthNumber(value: string | undefined, fallback = 0): number {
1695
1858
  return Number.isFinite(parsed) ? parsed : fallback;
1696
1859
  }
1697
1860
 
1861
+ /** Round to one decimal place — matches the `precision={1}` ScrubInput controls advertise (e.g. stroke weight, font size) so 0.5-unit values aren't silently floored to whole numbers. */
1862
+ export function roundToOneDecimal(value: number): number {
1863
+ return Math.round(value * 10) / 10;
1864
+ }
1865
+
1698
1866
  function cssColorOrFallback(value: string | undefined, fallback: string) {
1699
1867
  const normalized = value?.trim();
1700
1868
  if (
@@ -1711,6 +1879,17 @@ function strokeIsVisible(width: string | undefined, style: string | undefined) {
1711
1879
  return cssLengthNumber(width) > 0 && style !== "none";
1712
1880
  }
1713
1881
 
1882
+ /**
1883
+ * True when a stroke's own color is the reason it's invisible (alpha
1884
+ * zeroed) rather than its width/style. Used so the eye toggle can hide a
1885
+ * stroke by zeroing color alpha — preserving the original border-style
1886
+ * (solid/dashed/dotted/etc, which has no "unset" round-trip once written as
1887
+ * "none") — instead of forcing borderStyle back to "solid" on every show.
1888
+ */
1889
+ export function strokeHiddenByColor(color: string | undefined): boolean {
1890
+ return Boolean(color) && !colorHasVisibleAlpha(color);
1891
+ }
1892
+
1714
1893
  function swatchStyle(value: string | undefined) {
1715
1894
  return {
1716
1895
  background:
@@ -1894,8 +2073,29 @@ function parentFlexDirection(element: ElementInfo): AutoLayoutSizingAxis {
1894
2073
  : "horizontal";
1895
2074
  }
1896
2075
 
1897
- function isTextElement(element: ElementInfo): boolean {
1898
- return TEXT_TAGS.has((element.tagName || "").toLowerCase());
2076
+ export function isTextElement(element: ElementInfo): boolean {
2077
+ const tag = (element.tagName || "").toLowerCase();
2078
+ if (TEXT_TAGS.has(tag)) return true;
2079
+ // T-tool text primitives are plain `div`s stamped with
2080
+ // data-an-primitive="text" (see DesignEditor primitive creation). The
2081
+ // bridge now forwards that marker as ElementInfo.primitiveKind — prefer it
2082
+ // when present since it's exact.
2083
+ if (element.primitiveKind) return element.primitiveKind === "text";
2084
+ // Fallback for older payloads that predate primitiveKind: approximate a
2085
+ // T-tool text div with a content heuristic — a childless div that has its
2086
+ // own text content and isn't already flagged as a layout container. This
2087
+ // intentionally excludes empty frames/shapes (no text) and containers with
2088
+ // element children.
2089
+ if (
2090
+ tag === "div" &&
2091
+ !element.isFlexContainer &&
2092
+ !element.isGridContainer &&
2093
+ (element.childElementCount ?? 0) === 0 &&
2094
+ Boolean(element.textContent?.trim())
2095
+ ) {
2096
+ return true;
2097
+ }
2098
+ return false;
1899
2099
  }
1900
2100
 
1901
2101
  /**
@@ -1972,7 +2172,7 @@ function commitElementMinMax(
1972
2172
  axis: AutoLayoutSizingAxis,
1973
2173
  kind: "min" | "max",
1974
2174
  value: number | null,
1975
- onStyleChange: (property: string, value: string) => void,
2175
+ onStyleChange: StyleChangeHandler,
1976
2176
  ) {
1977
2177
  const isHorizontal = axis === "horizontal";
1978
2178
  const property =
@@ -2047,8 +2247,8 @@ function commitElementSizing(
2047
2247
  element: ElementInfo,
2048
2248
  axis: AutoLayoutSizingAxis,
2049
2249
  sizing: AutoLayoutSizing,
2050
- onStyleChange: (property: string, value: string) => void,
2051
- onStylesChange?: (styles: Record<string, string>) => void,
2250
+ onStyleChange: StyleChangeHandler,
2251
+ onStylesChange?: StylesChangeHandler,
2052
2252
  ) {
2053
2253
  const isHorizontal = axis === "horizontal";
2054
2254
  const sizeProperty = isHorizontal ? "width" : "height";
@@ -3327,39 +3527,83 @@ function CornerRadiusControl({
3327
3527
  onStyleChange,
3328
3528
  }: {
3329
3529
  styles: Record<string, string>;
3330
- onStyleChange: (property: string, value: string) => void;
3530
+ onStyleChange: StyleChangeHandler;
3331
3531
  }) {
3332
3532
  const t = useT();
3333
3533
  const independentCornersLabel = t("editPanel.labels.independentCorners");
3534
+ const cornerSources = {
3535
+ topLeft: styles.borderTopLeftRadius || styles.borderRadius,
3536
+ topRight: styles.borderTopRightRadius || styles.borderRadius,
3537
+ bottomRight: styles.borderBottomRightRadius || styles.borderRadius,
3538
+ bottomLeft: styles.borderBottomLeftRadius || styles.borderRadius,
3539
+ };
3540
+ const cornerMixed = {
3541
+ topLeft: isMixedValue(cornerSources.topLeft),
3542
+ topRight: isMixedValue(cornerSources.topRight),
3543
+ bottomRight: isMixedValue(cornerSources.bottomRight),
3544
+ bottomLeft: isMixedValue(cornerSources.bottomLeft),
3545
+ };
3546
+ // Guard cssLengthNumber against the Mixed sentinel — parseFloat("Mixed")
3547
+ // would silently coerce it to 0 and render a concrete value.
3334
3548
  const corners = {
3335
- topLeft: cssLengthNumber(styles.borderTopLeftRadius || styles.borderRadius),
3336
- topRight: cssLengthNumber(
3337
- styles.borderTopRightRadius || styles.borderRadius,
3338
- ),
3339
- bottomRight: cssLengthNumber(
3340
- styles.borderBottomRightRadius || styles.borderRadius,
3341
- ),
3342
- bottomLeft: cssLengthNumber(
3343
- styles.borderBottomLeftRadius || styles.borderRadius,
3344
- ),
3549
+ topLeft: cornerMixed.topLeft ? 0 : cssLengthNumber(cornerSources.topLeft),
3550
+ topRight: cornerMixed.topRight
3551
+ ? 0
3552
+ : cssLengthNumber(cornerSources.topRight),
3553
+ bottomRight: cornerMixed.bottomRight
3554
+ ? 0
3555
+ : cssLengthNumber(cornerSources.bottomRight),
3556
+ bottomLeft: cornerMixed.bottomLeft
3557
+ ? 0
3558
+ : cssLengthNumber(cornerSources.bottomLeft),
3345
3559
  };
3346
- const cornersDiffer =
3347
- corners.topLeft !== corners.topRight ||
3348
- corners.topLeft !== corners.bottomRight ||
3349
- corners.topLeft !== corners.bottomLeft;
3560
+ const anyCornerMixed =
3561
+ cornerMixed.topLeft ||
3562
+ cornerMixed.topRight ||
3563
+ cornerMixed.bottomRight ||
3564
+ cornerMixed.bottomLeft;
3565
+ const allCornersMixed =
3566
+ cornerMixed.topLeft &&
3567
+ cornerMixed.topRight &&
3568
+ cornerMixed.bottomRight &&
3569
+ cornerMixed.bottomLeft;
3570
+ // With mixed sentinels the parsed numbers are placeholders, so compare
3571
+ // mixed-ness instead: all-mixed reads as uniform (each element may still be
3572
+ // uniform), partially-mixed means at least one element has differing corners.
3573
+ const cornersDiffer = anyCornerMixed
3574
+ ? !allCornersMixed
3575
+ : corners.topLeft !== corners.topRight ||
3576
+ corners.topLeft !== corners.bottomRight ||
3577
+ corners.topLeft !== corners.bottomLeft;
3350
3578
  const [showIndependentCorners, setShowIndependentCorners] =
3351
3579
  useState(cornersDiffer);
3352
- const radius = cornersDiffer
3353
- ? corners.topLeft
3354
- : cssLengthNumber(styles.borderRadius || String(corners.topLeft));
3355
- const commitRadius = (value: number) => {
3580
+ const radiusMixed =
3581
+ anyCornerMixed || (!cornersDiffer && isMixedValue(styles.borderRadius));
3582
+ const radius = radiusMixed
3583
+ ? 0
3584
+ : cornersDiffer
3585
+ ? corners.topLeft
3586
+ : cssLengthNumber(styles.borderRadius || String(corners.topLeft));
3587
+ const commitRadius = (value: number, meta?: ScrubInputChangeMeta) => {
3356
3588
  const next = `${Math.max(0, Math.round(value))}px`;
3357
- onStyleChange("borderRadius", next);
3358
- if (!showIndependentCorners) return;
3359
- onStyleChange("borderTopLeftRadius", next);
3360
- onStyleChange("borderTopRightRadius", next);
3361
- onStyleChange("borderBottomRightRadius", next);
3362
- onStyleChange("borderBottomLeftRadius", next);
3589
+ // Always write the longhands along with the shorthand: stale inline
3590
+ // longhand declarations serialize after the shorthand and would override
3591
+ // it, turning uniform-radius commits into silent no-ops.
3592
+ onStyleChange("borderRadius", next, meta);
3593
+ onStyleChange("borderTopLeftRadius", next, meta);
3594
+ onStyleChange("borderTopRightRadius", next, meta);
3595
+ onStyleChange("borderBottomRightRadius", next, meta);
3596
+ onStyleChange("borderBottomLeftRadius", next, meta);
3597
+ };
3598
+ const toggleIndependentCorners = () => {
3599
+ // Collapsing while corners differ flattens them to the displayed uniform
3600
+ // value; otherwise the stale longhands would keep overriding the shorthand
3601
+ // and the single field would silently no-op. Mixed selections collapse the
3602
+ // UI only — committing would stamp the placeholder 0 onto every object.
3603
+ if (showIndependentCorners && cornersDiffer && !radiusMixed) {
3604
+ commitRadius(radius);
3605
+ }
3606
+ setShowIndependentCorners(!showIndependentCorners);
3363
3607
  };
3364
3608
 
3365
3609
  useEffect(() => {
@@ -3373,6 +3617,7 @@ function CornerRadiusControl({
3373
3617
  icon={IconBorderRadius}
3374
3618
  value={radius}
3375
3619
  onChange={commitRadius}
3620
+ mixed={radiusMixed}
3376
3621
  min={0}
3377
3622
  precision={0}
3378
3623
  />
@@ -3389,7 +3634,7 @@ function CornerRadiusControl({
3389
3634
  )}
3390
3635
  aria-label={independentCornersLabel}
3391
3636
  aria-pressed={showIndependentCorners}
3392
- onClick={() => setShowIndependentCorners((value) => !value)}
3637
+ onClick={toggleIndependentCorners}
3393
3638
  >
3394
3639
  <IconBorderCorners className="size-3.5" />
3395
3640
  </Button>
@@ -3403,12 +3648,14 @@ function CornerRadiusControl({
3403
3648
  ariaLabel="Top left"
3404
3649
  icon={IconRadiusTopLeft}
3405
3650
  value={corners.topLeft}
3406
- onChange={(value) =>
3651
+ onChange={(value, meta) =>
3407
3652
  onStyleChange(
3408
3653
  "borderTopLeftRadius",
3409
3654
  `${Math.max(0, Math.round(value))}px`,
3655
+ meta,
3410
3656
  )
3411
3657
  }
3658
+ mixed={cornerMixed.topLeft}
3412
3659
  min={0}
3413
3660
  precision={1}
3414
3661
  />
@@ -3417,12 +3664,14 @@ function CornerRadiusControl({
3417
3664
  ariaLabel="Top right"
3418
3665
  icon={IconRadiusTopRight}
3419
3666
  value={corners.topRight}
3420
- onChange={(value) =>
3667
+ onChange={(value, meta) =>
3421
3668
  onStyleChange(
3422
3669
  "borderTopRightRadius",
3423
3670
  `${Math.max(0, Math.round(value))}px`,
3671
+ meta,
3424
3672
  )
3425
3673
  }
3674
+ mixed={cornerMixed.topRight}
3426
3675
  min={0}
3427
3676
  precision={1}
3428
3677
  />
@@ -3432,12 +3681,14 @@ function CornerRadiusControl({
3432
3681
  ariaLabel="Bottom left"
3433
3682
  icon={IconRadiusBottomLeft}
3434
3683
  value={corners.bottomLeft}
3435
- onChange={(value) =>
3684
+ onChange={(value, meta) =>
3436
3685
  onStyleChange(
3437
3686
  "borderBottomLeftRadius",
3438
3687
  `${Math.max(0, Math.round(value))}px`,
3688
+ meta,
3439
3689
  )
3440
3690
  }
3691
+ mixed={cornerMixed.bottomLeft}
3441
3692
  min={0}
3442
3693
  precision={1}
3443
3694
  />
@@ -3446,12 +3697,14 @@ function CornerRadiusControl({
3446
3697
  ariaLabel="Bottom right"
3447
3698
  icon={IconRadiusBottomRight}
3448
3699
  value={corners.bottomRight}
3449
- onChange={(value) =>
3700
+ onChange={(value, meta) =>
3450
3701
  onStyleChange(
3451
3702
  "borderBottomRightRadius",
3452
3703
  `${Math.max(0, Math.round(value))}px`,
3704
+ meta,
3453
3705
  )
3454
3706
  }
3707
+ mixed={cornerMixed.bottomRight}
3455
3708
  min={0}
3456
3709
  precision={1}
3457
3710
  />
@@ -3479,7 +3732,7 @@ function AppearanceScrubField({
3479
3732
  ariaLabel?: string;
3480
3733
  icon: (props: { className?: string }) => ReactNode;
3481
3734
  value: number;
3482
- onChange: (value: number) => void;
3735
+ onChange: (value: number, meta?: ScrubInputChangeMeta) => void;
3483
3736
  mixed?: boolean;
3484
3737
  min?: number;
3485
3738
  max?: number;
@@ -3512,7 +3765,7 @@ function BlendModeMenu({
3512
3765
  onStyleChange,
3513
3766
  }: {
3514
3767
  styles: Record<string, string>;
3515
- onStyleChange: (property: string, value: string) => void;
3768
+ onStyleChange: StyleChangeHandler;
3516
3769
  }) {
3517
3770
  const [open, setOpen] = useState(false);
3518
3771
  const blendMode = optionValue(
@@ -3520,8 +3773,16 @@ function BlendModeMenu({
3520
3773
  styles.mixBlendMode || "normal",
3521
3774
  "normal",
3522
3775
  );
3523
- const selectedBlendMode =
3524
- blendMode === "normal" && styles.isolation !== "isolate"
3776
+ // Recognize the Mixed sentinel BEFORE optionValue's fallback maps it to
3777
+ // "normal" a mixed selection must not check a wrong concrete mode.
3778
+ // Isolation only disambiguates pass-through vs normal, so it only makes the
3779
+ // state mixed when the blend mode itself resolves to normal.
3780
+ const blendModeMixed =
3781
+ isMixedValue(styles.mixBlendMode) ||
3782
+ (blendMode === "normal" && isMixedValue(styles.isolation));
3783
+ const selectedBlendMode = blendModeMixed
3784
+ ? MIXED_VALUE
3785
+ : blendMode === "normal" && styles.isolation !== "isolate"
3525
3786
  ? "pass-through"
3526
3787
  : blendMode;
3527
3788
  const options = [
@@ -3565,6 +3826,20 @@ function BlendModeMenu({
3565
3826
  sideOffset={8}
3566
3827
  className="z-[100010] w-48 rounded-xl border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)] p-1 text-[13px] text-foreground shadow-2xl"
3567
3828
  >
3829
+ {blendModeMixed ? (
3830
+ <>
3831
+ {/* Placeholder state for a mixed selection: the check sits next to
3832
+ "Mixed" instead of a wrong concrete mode. Picking any option
3833
+ below applies it to every selected object. */}
3834
+ <div className="flex h-9 items-center gap-3 rounded-md px-3 text-[13px] text-muted-foreground">
3835
+ <span className="flex size-4 shrink-0 items-center justify-center">
3836
+ <IconCheck className="size-4" />
3837
+ </span>
3838
+ <span>{MIXED_VALUE}</span>
3839
+ </div>
3840
+ <DropdownMenuSeparator />
3841
+ </>
3842
+ ) : null}
3568
3843
  {options.map((option) => (
3569
3844
  <DropdownMenuItem
3570
3845
  key={option.value}
@@ -3600,7 +3875,7 @@ function StrokeLayerControl({
3600
3875
  color: string;
3601
3876
  width: string;
3602
3877
  styleValue: string;
3603
- onStyleChange: (property: string, value: string) => void;
3878
+ onStyleChange: StyleChangeHandler;
3604
3879
  onRemove: () => void;
3605
3880
  }) {
3606
3881
  const t = useT();
@@ -3631,7 +3906,9 @@ function StrokeLayerControl({
3631
3906
  <ColorInput
3632
3907
  label=""
3633
3908
  value={cssColorOrFallback(color, "#000000")}
3634
- onChange={(value) => onStyleChange(`${prefix}Color`, value)}
3909
+ onChange={(value, meta) =>
3910
+ onStyleChange(`${prefix}Color`, value, meta)
3911
+ }
3635
3912
  />
3636
3913
  </div>
3637
3914
  <SectionIconButton
@@ -3641,11 +3918,25 @@ function StrokeLayerControl({
3641
3918
  : t("editPanel.labels.showLayer")
3642
3919
  }
3643
3920
  onClick={() => {
3921
+ // Hide/show by zeroing the stroke color's alpha (preserving its
3922
+ // RGB channels — same durable, comment-free technique as the
3923
+ // fill visibility toggle) instead of forcing borderStyle to
3924
+ // "none"/"solid". Writing "none" would lose a dashed/dotted
3925
+ // style permanently, since there is no round-trippable "unset"
3926
+ // for that keyword once it's overwritten.
3927
+ const parsed = parseCssColor(color);
3644
3928
  if (visible) {
3645
- onStyleChange(`${prefix}Style`, "none");
3929
+ onStyleChange(
3930
+ `${prefix}Color`,
3931
+ parsed ? rgbaToCss(withColorOpacity(parsed, 0)) : "transparent",
3932
+ );
3646
3933
  return;
3647
3934
  }
3648
- onStyleChange(`${prefix}Style`, "solid");
3935
+ const restoredColor = parsed
3936
+ ? rgbaToCss(withColorOpacity(parsed, 100))
3937
+ : "#000000";
3938
+ onStyleChange(`${prefix}Color`, restoredColor);
3939
+ if (styleValue === "none") onStyleChange(`${prefix}Style`, "solid");
3649
3940
  onStyleChange(
3650
3941
  `${prefix}Width`,
3651
3942
  width === "0px" ? "1px" : width || "1px",
@@ -3688,10 +3979,11 @@ function StrokeLayerControl({
3688
3979
  ariaLabel={t("editPanel.labels.weight")}
3689
3980
  icon={IconBorderStyle}
3690
3981
  value={cssLengthNumber(width)}
3691
- onChange={(value) =>
3982
+ onChange={(value, meta) =>
3692
3983
  onStyleChange(
3693
3984
  `${prefix}Width`,
3694
- `${Math.max(0, Math.round(value))}px`,
3985
+ `${Math.max(0, roundToOneDecimal(value))}px`,
3986
+ meta,
3695
3987
  )
3696
3988
  }
3697
3989
  unit="px"
@@ -3789,7 +4081,9 @@ function serializeShadowLayers(layers: ShadowLayer[]) {
3789
4081
  `${Math.round(layer.x)}px`,
3790
4082
  `${Math.round(layer.y)}px`,
3791
4083
  `${Math.max(0, Math.round(layer.blur))}px`,
3792
- `${layer.inset ? Math.round(layer.spread) : Math.max(0, Math.round(layer.spread))}px`,
4084
+ // Spread radius may legitimately be negative for either inset or
4085
+ // drop shadows — only blur-radius is clamped to >= 0 in CSS.
4086
+ `${Math.round(layer.spread)}px`,
3793
4087
  layer.color,
3794
4088
  ]
3795
4089
  .filter(Boolean)
@@ -3833,7 +4127,7 @@ function ShadowEffectRow({
3833
4127
  }: {
3834
4128
  layer: ShadowLayer;
3835
4129
  index: number;
3836
- onChange: (patch: Partial<ShadowLayer>) => void;
4130
+ onChange: (patch: Partial<ShadowLayer>, meta?: StyleChangeMeta) => void;
3837
4131
  onRemove: () => void;
3838
4132
  onToggleVisibility: () => void;
3839
4133
  }) {
@@ -3912,7 +4206,9 @@ function ShadowEffectRow({
3912
4206
  <ScrubInput
3913
4207
  label="X"
3914
4208
  value={layer.x}
3915
- onChange={(value) => onChange({ x: value })}
4209
+ onChange={(value, meta) =>
4210
+ onChange({ x: value }, { phase: meta?.phase })
4211
+ }
3916
4212
  unit="px"
3917
4213
  precision={1}
3918
4214
  inputClassName="h-6"
@@ -3920,7 +4216,9 @@ function ShadowEffectRow({
3920
4216
  <ScrubInput
3921
4217
  label="Y"
3922
4218
  value={layer.y}
3923
- onChange={(value) => onChange({ y: value })}
4219
+ onChange={(value, meta) =>
4220
+ onChange({ y: value }, { phase: meta?.phase })
4221
+ }
3924
4222
  unit="px"
3925
4223
  precision={1}
3926
4224
  inputClassName="h-6"
@@ -3928,7 +4226,9 @@ function ShadowEffectRow({
3928
4226
  <ScrubInput
3929
4227
  label={t("editPanel.labels.blur")}
3930
4228
  value={layer.blur}
3931
- onChange={(value) => onChange({ blur: Math.max(0, value) })}
4229
+ onChange={(value, meta) =>
4230
+ onChange({ blur: Math.max(0, value) }, { phase: meta?.phase })
4231
+ }
3932
4232
  unit="px"
3933
4233
  min={0}
3934
4234
  precision={1}
@@ -3937,11 +4237,14 @@ function ShadowEffectRow({
3937
4237
  <ScrubInput
3938
4238
  label={t("editPanel.labels.spread")}
3939
4239
  value={layer.spread}
3940
- onChange={(value) =>
3941
- onChange({ spread: layer.inset ? value : Math.max(0, value) })
4240
+ // Spread radius is valid negative for both inset AND drop
4241
+ // (non-inset) shadows in real CSS negative spread shrinks
4242
+ // the shadow smaller than the box before blurring, a common
4243
+ // technique. Only blur-radius must stay >= 0.
4244
+ onChange={(value, meta) =>
4245
+ onChange({ spread: value }, { phase: meta?.phase })
3942
4246
  }
3943
4247
  unit="px"
3944
- min={layer.inset ? undefined : 0}
3945
4248
  precision={1}
3946
4249
  inputClassName="h-6"
3947
4250
  />
@@ -3949,7 +4252,7 @@ function ShadowEffectRow({
3949
4252
  <ColorInput
3950
4253
  label={t("editPanel.labels.color")}
3951
4254
  value={cssColorOrFallback(layer.color, "rgba(0, 0, 0, 0.25)")}
3952
- onChange={(value) => onChange({ color: value })}
4255
+ onChange={(value, meta) => onChange({ color: value }, meta)}
3953
4256
  />
3954
4257
  </div>
3955
4258
  </PopoverContent>
@@ -3964,8 +4267,8 @@ function PageProperties({
3964
4267
  onStylesChange,
3965
4268
  }: {
3966
4269
  styles: Record<string, string>;
3967
- onStyleChange: (property: string, value: string) => void;
3968
- onStylesChange?: (styles: Record<string, string>) => void;
4270
+ onStyleChange: StyleChangeHandler;
4271
+ onStylesChange?: StylesChangeHandler;
3969
4272
  }) {
3970
4273
  const t = useT();
3971
4274
  const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
@@ -3991,7 +4294,7 @@ function PageProperties({
3991
4294
  <ColorInput
3992
4295
  label={t("editPanel.labels.background")}
3993
4296
  value={styles.backgroundColor || ""}
3994
- onChange={(v) => onStyleChange("backgroundColor", v)}
4297
+ onChange={(v, meta) => onStyleChange("backgroundColor", v, meta)}
3995
4298
  backgroundImage={styles.backgroundImage}
3996
4299
  backgroundSize={styles.backgroundSize}
3997
4300
  backgroundRepeat={styles.backgroundRepeat}
@@ -4032,7 +4335,7 @@ function TypographyProperties({
4032
4335
  onStyleChange,
4033
4336
  }: {
4034
4337
  element: ElementInfo;
4035
- onStyleChange: (property: string, value: string) => void;
4338
+ onStyleChange: StyleChangeHandler;
4036
4339
  }) {
4037
4340
  const t = useT();
4038
4341
  const styles = element.computedStyles;
@@ -4058,16 +4361,48 @@ function TypographyProperties({
4058
4361
  }));
4059
4362
  const textAlign = styles.textAlign || "left";
4060
4363
 
4061
- // M1 · Text resizing mode (auto-width / auto-height / fixed). the design editor's text
4062
- // nodes always expose this segment. Infer the current mode from the live CSS:
4063
- // auto-width hugs both axes (width:auto + no wrapping), auto-height hugs the
4064
- // height only (fixed width, content wraps), fixed pins both width and height.
4065
- const widthIsAuto =
4066
- !styles.width || styles.width === "auto" || styles.width === "max-content";
4067
- const heightIsAuto = !styles.height || styles.height === "auto";
4068
- const noWrap = styles.whiteSpace === "nowrap";
4364
+ // Mixed-selection guards: a multi-selection with differing values injects
4365
+ // the MIXED_VALUE sentinel string into these computedStyles fields (see
4366
+ // mixedElementFromSelection/sameOrMixed). Parsing that sentinel with
4367
+ // parseNumericValue/Number() silently yields 0/NaN-fallback instead of
4368
+ // reflecting "differs across selection", which previously showed a
4369
+ // fabricated 0 (size), 1.2 (line-height), or blank (tracking) rather than
4370
+ // the Mixed state ScrubInput already knows how to render — same pattern as
4371
+ // the rotation field above.
4372
+ const fontWeightIsMixed = isMixedValue(styles.fontWeight);
4373
+ const fontSizeIsMixed = isMixedValue(styles.fontSize);
4374
+ const lineHeightIsMixed = isMixedValue(styles.lineHeight);
4375
+ const letterSpacingIsMixed = isMixedValue(styles.letterSpacing);
4376
+
4377
+ // M1 · Text resizing mode (auto-width / auto-height / fixed). the design
4378
+ // editor's text nodes always expose this segment. Read authored
4379
+ // (inlineStyles) values, not computed ones: an absolutely-positioned
4380
+ // element's computed width/height always resolve to a real px value even
4381
+ // when the author never set them, so "auto" and "a specific 200px" were
4382
+ // indistinguishable before — every text node misread as "fixed". Falls
4383
+ // back to the computed-style heuristic for older payloads that predate
4384
+ // inlineStyles. Convention (matches DesignEditor primitive creation and
4385
+ // setResizeMode below): auto-width = width unset/max-content + pre-wrap;
4386
+ // auto-height = fixed width + height unset/auto; fixed = both fixed. A
4387
+ // drag-created box (display:flex, explicit width+height, whiteSpace
4388
+ // unset→normal) correctly falls through to "fixed".
4389
+ const authoredResizeWidth = authoredStyleValue(element, "width");
4390
+ const authoredResizeHeight = authoredStyleValue(element, "height");
4391
+ const authoredWhiteSpace = authoredStyleValue(element, "whiteSpace");
4392
+ const hasInlineStyleInfo = Boolean(element.inlineStyles);
4393
+ const widthIsAuto = hasInlineStyleInfo
4394
+ ? !authoredResizeWidth || authoredResizeWidth === "max-content"
4395
+ : !styles.width ||
4396
+ styles.width === "auto" ||
4397
+ styles.width === "max-content";
4398
+ const heightIsAuto = hasInlineStyleInfo
4399
+ ? !authoredResizeHeight || authoredResizeHeight === "auto"
4400
+ : !styles.height || styles.height === "auto";
4401
+ const isPreWrapOrNoWrap = hasInlineStyleInfo
4402
+ ? authoredWhiteSpace === "pre-wrap" || authoredWhiteSpace === "nowrap"
4403
+ : styles.whiteSpace === "nowrap";
4069
4404
  const resizeMode: TextResizeMode =
4070
- widthIsAuto && noWrap
4405
+ widthIsAuto && isPreWrapOrNoWrap
4071
4406
  ? "auto-width"
4072
4407
  : !heightIsAuto && !widthIsAuto
4073
4408
  ? "fixed"
@@ -4076,9 +4411,9 @@ function TypographyProperties({
4076
4411
  const currentHeight = styles.height && !heightIsAuto ? styles.height : "48px";
4077
4412
  const setResizeMode = (mode: TextResizeMode) => {
4078
4413
  if (mode === "auto-width") {
4079
- onStyleChange("width", "auto");
4414
+ onStyleChange("width", "max-content");
4080
4415
  onStyleChange("height", "auto");
4081
- onStyleChange("whiteSpace", "nowrap");
4416
+ onStyleChange("whiteSpace", "pre-wrap");
4082
4417
  } else if (mode === "auto-height") {
4083
4418
  onStyleChange("width", currentWidth);
4084
4419
  onStyleChange("height", "auto");
@@ -4090,36 +4425,44 @@ function TypographyProperties({
4090
4425
  }
4091
4426
  };
4092
4427
 
4093
- // M2 · Vertical text alignment (top / middle / bottom). For auto-layout text
4094
- // containers (display:flex) the design editor maps this to `justifyContent`; for normal
4095
- // text we fall back to `verticalAlign`, which is what an inline/grid text box
4096
- // honors. Read whichever the element currently expresses.
4428
+ // M2 · Vertical text alignment (top / middle / bottom). For an auto-layout
4429
+ // text container (display:flex) this maps to whichever flex property
4430
+ // controls the vertical/cross axis justifyContent when flex-direction is
4431
+ // column, alignItems when row (the DesignEditor drag-created default; see
4432
+ // primitive creation, which sets display:flex + alignItems:center with no
4433
+ // explicit flex-direction, i.e. row). For any non-flex display,
4434
+ // `verticalAlign` is a no-op: it only affects how an inline/inline-block/
4435
+ // table-cell box sits relative to *sibling* line-box content, not how its
4436
+ // own content sits within its own box — exactly the case for point text
4437
+ // (inline-block). So instead of ever writing verticalAlign, convert the
4438
+ // element to flex the same way a drag-created box is authored, then read/
4439
+ // write through the row-axis property (alignItems) like that default.
4097
4440
  const display = (styles.display || "").toLowerCase();
4098
4441
  const isFlexText = display.includes("flex");
4099
- const verticalAlign = isFlexText
4100
- ? styles.justifyContent === "center"
4101
- ? "middle"
4102
- : styles.justifyContent === "flex-end"
4103
- ? "bottom"
4104
- : "top"
4105
- : styles.verticalAlign === "middle"
4442
+ const isColumnFlexText =
4443
+ isFlexText && styles.flexDirection?.includes("column");
4444
+ const verticalAlignSourceProp = isColumnFlexText
4445
+ ? styles.justifyContent
4446
+ : styles.alignItems;
4447
+ const verticalAlign = !isFlexText
4448
+ ? "top"
4449
+ : verticalAlignSourceProp === "center"
4106
4450
  ? "middle"
4107
- : styles.verticalAlign === "bottom"
4451
+ : verticalAlignSourceProp === "flex-end"
4108
4452
  ? "bottom"
4109
4453
  : "top";
4110
4454
  const setVerticalAlign = (mode: "top" | "middle" | "bottom") => {
4111
- if (isFlexText) {
4112
- onStyleChange(
4113
- "justifyContent",
4114
- mode === "middle"
4115
- ? "center"
4116
- : mode === "bottom"
4117
- ? "flex-end"
4118
- : "flex-start",
4119
- );
4120
- } else {
4121
- onStyleChange("verticalAlign", mode);
4122
- }
4455
+ // Converting a non-flex element matches the drag-created fixed-size text
4456
+ // box exactly: display:flex, default (row) flex-direction — so the
4457
+ // vertical axis is alignItems, same as the pre-existing row case below.
4458
+ if (!isFlexText) onStyleChange("display", "flex");
4459
+ const cssValue =
4460
+ mode === "middle"
4461
+ ? "center"
4462
+ : mode === "bottom"
4463
+ ? "flex-end"
4464
+ : "flex-start";
4465
+ onStyleChange(isColumnFlexText ? "justifyContent" : "alignItems", cssValue);
4123
4466
  };
4124
4467
 
4125
4468
  return (
@@ -4158,13 +4501,22 @@ function TypographyProperties({
4158
4501
  {/* Row 2: weight + size side by side */}
4159
4502
  <div className="grid grid-cols-2 gap-1.5">
4160
4503
  <Select
4161
- value={styles.fontWeight || "400"}
4504
+ value={fontWeightIsMixed ? MIXED_VALUE : styles.fontWeight || "400"}
4162
4505
  onValueChange={(v) => onStyleChange("fontWeight", v)}
4163
4506
  >
4164
4507
  <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)]">
4165
4508
  <SelectValue />
4166
4509
  </SelectTrigger>
4167
4510
  <SelectContent>
4511
+ {fontWeightIsMixed ? (
4512
+ <SelectItem
4513
+ value={MIXED_VALUE}
4514
+ disabled
4515
+ className="!text-[11px] text-muted-foreground"
4516
+ >
4517
+ {MIXED_VALUE}
4518
+ </SelectItem>
4519
+ ) : null}
4168
4520
  {fontWeightOptions.map((opt) => (
4169
4521
  <SelectItem
4170
4522
  key={opt.value}
@@ -4180,9 +4532,20 @@ function TypographyProperties({
4180
4532
  label={t("editPanel.labels.size")}
4181
4533
  ariaLabel={t("editPanel.labels.size")}
4182
4534
  icon={IconLetterCase}
4183
- value={styles.fontSize ? parseNumericValue(styles.fontSize) : 16}
4184
- onChange={(value) =>
4185
- onStyleChange("fontSize", `${Math.max(1, Math.round(value))}px`)
4535
+ value={
4536
+ fontSizeIsMixed
4537
+ ? 0
4538
+ : styles.fontSize
4539
+ ? parseNumericValue(styles.fontSize)
4540
+ : 16
4541
+ }
4542
+ mixed={fontSizeIsMixed}
4543
+ onChange={(value, meta) =>
4544
+ onStyleChange(
4545
+ "fontSize",
4546
+ `${Math.max(1, roundToOneDecimal(value))}px`,
4547
+ meta,
4548
+ )
4186
4549
  }
4187
4550
  unit="px"
4188
4551
  min={1}
@@ -4199,9 +4562,14 @@ function TypographyProperties({
4199
4562
  label={t("editPanel.labels.lineHeight")}
4200
4563
  ariaLabel={t("editPanel.labels.lineHeight")}
4201
4564
  icon={IconLineHeight}
4202
- value={resolveLineHeight(styles.lineHeight, styles.fontSize)}
4203
- onChange={(value) =>
4204
- onStyleChange("lineHeight", String(Math.max(0.1, value)))
4565
+ value={
4566
+ lineHeightIsMixed
4567
+ ? 0
4568
+ : resolveLineHeight(styles.lineHeight, styles.fontSize)
4569
+ }
4570
+ mixed={lineHeightIsMixed}
4571
+ onChange={(value, meta) =>
4572
+ onStyleChange("lineHeight", String(Math.max(0.1, value)), meta)
4205
4573
  }
4206
4574
  min={0.1}
4207
4575
  step={0.1}
@@ -4215,9 +4583,16 @@ function TypographyProperties({
4215
4583
  ariaLabel={t("editPanel.labels.tracking")}
4216
4584
  icon={IconLetterSpacing}
4217
4585
  value={
4218
- styles.letterSpacing ? parseNumericValue(styles.letterSpacing) : 0
4586
+ letterSpacingIsMixed
4587
+ ? 0
4588
+ : styles.letterSpacing
4589
+ ? parseNumericValue(styles.letterSpacing)
4590
+ : 0
4591
+ }
4592
+ mixed={letterSpacingIsMixed}
4593
+ onChange={(value, meta) =>
4594
+ onStyleChange("letterSpacing", `${value}px`, meta)
4219
4595
  }
4220
- onChange={(value) => onStyleChange("letterSpacing", `${value}px`)}
4221
4596
  unit="px"
4222
4597
  precision={1}
4223
4598
  className="gap-0"
@@ -4299,8 +4674,8 @@ function FlexContainerControls({
4299
4674
  onStylesChange,
4300
4675
  }: {
4301
4676
  element: ElementInfo;
4302
- onStyleChange: (property: string, value: string) => void;
4303
- onStylesChange?: (styles: Record<string, string>) => void;
4677
+ onStyleChange: StyleChangeHandler;
4678
+ onStylesChange?: StylesChangeHandler;
4304
4679
  }) {
4305
4680
  const t = useT();
4306
4681
  const styles = element.computedStyles;
@@ -4484,7 +4859,7 @@ function FlexChildControls({
4484
4859
  onStyleChange,
4485
4860
  }: {
4486
4861
  element: ElementInfo;
4487
- onStyleChange: (property: string, value: string) => void;
4862
+ onStyleChange: StyleChangeHandler;
4488
4863
  }) {
4489
4864
  const t = useT();
4490
4865
  const styles = element.computedStyles;
@@ -4536,7 +4911,7 @@ function GridChildControls({
4536
4911
  onStyleChange,
4537
4912
  }: {
4538
4913
  element: ElementInfo;
4539
- onStyleChange: (property: string, value: string) => void;
4914
+ onStyleChange: StyleChangeHandler;
4540
4915
  }) {
4541
4916
  const t = useT();
4542
4917
  const styles = element.computedStyles;
@@ -4578,8 +4953,8 @@ function LayoutContextProperties({
4578
4953
  onStylesChange,
4579
4954
  }: {
4580
4955
  element: ElementInfo;
4581
- onStyleChange: (property: string, value: string) => void;
4582
- onStylesChange?: (styles: Record<string, string>) => void;
4956
+ onStyleChange: StyleChangeHandler;
4957
+ onStylesChange?: StylesChangeHandler;
4583
4958
  }) {
4584
4959
  const t = useT();
4585
4960
  const flexChild = isParentFlex(element);
@@ -4697,7 +5072,7 @@ function LayoutGuideProperties({
4697
5072
  onStyleChange,
4698
5073
  }: {
4699
5074
  element: ElementInfo;
4700
- onStyleChange: (property: string, value: string) => void;
5075
+ onStyleChange: StyleChangeHandler;
4701
5076
  }) {
4702
5077
  const styles = element.computedStyles;
4703
5078
  const active = hasLayoutGuide(styles);
@@ -4801,43 +5176,96 @@ function ExportPreview({ element }: { element: ElementInfo | null }) {
4801
5176
  );
4802
5177
  }
4803
5178
 
5179
+ /**
5180
+ * Reads an authored (as-written) offset/size style, preferring the bridge's
5181
+ * `inlineStyles` capture (raw `el.style.<prop>` — "auto"/"" reliably means
5182
+ * unset) over `computedStyles` (which always resolves to a px value for
5183
+ * absolutely-positioned elements even when the author never set the
5184
+ * property, making "unset" indistinguishable from "0px"). Falls back to the
5185
+ * computed value for older payloads that predate `inlineStyles`.
5186
+ */
5187
+ export function authoredStyleValue(
5188
+ element: ElementInfo,
5189
+ property: string,
5190
+ ): string | undefined {
5191
+ const inline = element.inlineStyles?.[property];
5192
+ if (inline !== undefined) return inline === "auto" ? "" : inline;
5193
+ return element.computedStyles[property];
5194
+ }
5195
+
4804
5196
  /** Position, size, and spacing properties */
4805
5197
  function PositionLayoutProperties({
4806
5198
  element,
4807
5199
  onStyleChange,
4808
5200
  }: {
4809
5201
  element: ElementInfo;
4810
- onStyleChange: (property: string, value: string) => void;
5202
+ onStyleChange: StyleChangeHandler;
4811
5203
  }) {
4812
5204
  const t = useT();
4813
5205
  const styles = element.computedStyles;
4814
5206
  const constrainedPosition =
4815
5207
  styles.position === "absolute" || styles.position === "fixed";
4816
- // Reflect the active packing in the alignment segments (the design editor highlights the
4817
- // current alignment). For a flex container the main axis is justifyContent.
4818
- const alignH = justifyToHorizontal(styles.justifyContent);
4819
- const alignV = alignToVertical(styles.alignItems);
5208
+ // Reflect the active packing in the alignment segments (the design editor
5209
+ // highlights the current alignment) these buttons align the selected
5210
+ // element's own children, the same operation FlexContainerControls' full
5211
+ // AutoLayoutMatrix alignment grid performs. A column-direction flex
5212
+ // container swaps which CSS axis is "horizontal"/"vertical" from the
5213
+ // user's point of view: justifyContent packs the main (here: vertical)
5214
+ // axis and alignItems packs the cross (horizontal) axis, so reading/
5215
+ // writing must go through autoLayoutAlignmentFromStyles /
5216
+ // horizontalToJustify / verticalToAlign like the matrix control does,
5217
+ // instead of assuming justifyContent is always horizontal.
5218
+ const positionFlexDirection: AutoLayoutMatrixValue["direction"] =
5219
+ styles.flexDirection?.includes("column") ? "vertical" : "horizontal";
5220
+ const positionAlignment = autoLayoutAlignmentFromStyles(
5221
+ styles,
5222
+ positionFlexDirection,
5223
+ );
5224
+ const alignH = positionAlignment.horizontal;
5225
+ const alignV = positionAlignment.vertical;
5226
+ const handlePositionAlignH = (value: AlignmentMatrixValue["horizontal"]) => {
5227
+ onStyleChange(
5228
+ positionFlexDirection === "vertical" ? "alignItems" : "justifyContent",
5229
+ horizontalToJustify(value),
5230
+ );
5231
+ };
5232
+ const handlePositionAlignV = (value: AlignmentMatrixValue["vertical"]) => {
5233
+ onStyleChange(
5234
+ positionFlexDirection === "vertical" ? "justifyContent" : "alignItems",
5235
+ verticalToAlign(value),
5236
+ );
5237
+ };
5238
+ // Authored (not computed) offsets: when inlineStyles is present, "auto"/absent
5239
+ // is treated as truly unset instead of the computed px fallback, so a plain
5240
+ // top-left-anchored element reads as "left" rather than always "left-right".
5241
+ const authoredLeft = authoredStyleValue(element, "left");
5242
+ const authoredRight = authoredStyleValue(element, "right");
5243
+ const authoredTop = authoredStyleValue(element, "top");
5244
+ const authoredBottom = authoredStyleValue(element, "bottom");
5245
+ const authoredWidth = authoredStyleValue(element, "width");
5246
+ const authoredHeight = authoredStyleValue(element, "height");
5247
+ const authoredTransform = authoredStyleValue(element, "transform");
4820
5248
  const constraintsValue: ConstraintsValue = {
4821
5249
  horizontal:
4822
5250
  // Check scale before left+right: "scale" writes width:100% and clears
4823
5251
  // left/right to auto, but legacy data may have 0px values that are truthy.
4824
- styles.width === "100%"
5252
+ authoredWidth === "100%"
4825
5253
  ? "scale"
4826
- : styles.left && styles.right
5254
+ : authoredLeft && authoredRight
4827
5255
  ? "left-right"
4828
- : styles.right
5256
+ : authoredRight
4829
5257
  ? "right"
4830
- : styles.transform?.includes("translateX(-50%)")
5258
+ : authoredTransform?.includes("translateX(-50%)")
4831
5259
  ? "center"
4832
5260
  : "left",
4833
5261
  vertical:
4834
- styles.height === "100%"
5262
+ authoredHeight === "100%"
4835
5263
  ? "scale"
4836
- : styles.top && styles.bottom
5264
+ : authoredTop && authoredBottom
4837
5265
  ? "top-bottom"
4838
- : styles.bottom
5266
+ : authoredBottom
4839
5267
  ? "bottom"
4840
- : styles.transform?.includes("translateY(-50%)")
5268
+ : authoredTransform?.includes("translateY(-50%)")
4841
5269
  ? "center"
4842
5270
  : "top",
4843
5271
  };
@@ -4854,7 +5282,7 @@ function PositionLayoutProperties({
4854
5282
  const tyValue = value.vertical === "center" ? "-50%" : null;
4855
5283
  // Start from the current transform, apply X, then apply Y on top.
4856
5284
  const transformAfterX = mergeTranslateFunction(
4857
- styles.transform,
5285
+ authoredTransform,
4858
5286
  "X",
4859
5287
  txValue,
4860
5288
  );
@@ -4867,7 +5295,7 @@ function PositionLayoutProperties({
4867
5295
  if (value.horizontal === "left") {
4868
5296
  onStyleChange(
4869
5297
  "left",
4870
- styles.left || `${Math.round(element.boundingRect.x)}px`,
5298
+ authoredLeft || `${Math.round(element.boundingRect.x)}px`,
4871
5299
  );
4872
5300
  onStyleChange("right", "auto");
4873
5301
  } else if (value.horizontal === "right") {
@@ -4876,7 +5304,7 @@ function PositionLayoutProperties({
4876
5304
  } else if (value.horizontal === "left-right") {
4877
5305
  onStyleChange(
4878
5306
  "left",
4879
- styles.left || `${Math.round(element.boundingRect.x)}px`,
5307
+ authoredLeft || `${Math.round(element.boundingRect.x)}px`,
4880
5308
  );
4881
5309
  onStyleChange("right", "0px");
4882
5310
  } else if (value.horizontal === "center") {
@@ -4893,7 +5321,7 @@ function PositionLayoutProperties({
4893
5321
  if (value.vertical === "top") {
4894
5322
  onStyleChange(
4895
5323
  "top",
4896
- styles.top || `${Math.round(element.boundingRect.y)}px`,
5324
+ authoredTop || `${Math.round(element.boundingRect.y)}px`,
4897
5325
  );
4898
5326
  onStyleChange("bottom", "auto");
4899
5327
  } else if (value.vertical === "bottom") {
@@ -4902,7 +5330,7 @@ function PositionLayoutProperties({
4902
5330
  } else if (value.vertical === "top-bottom") {
4903
5331
  onStyleChange(
4904
5332
  "top",
4905
- styles.top || `${Math.round(element.boundingRect.y)}px`,
5333
+ authoredTop || `${Math.round(element.boundingRect.y)}px`,
4906
5334
  );
4907
5335
  onStyleChange("bottom", "0px");
4908
5336
  } else if (value.vertical === "center") {
@@ -4922,9 +5350,9 @@ function PositionLayoutProperties({
4922
5350
  element.boundingRect.x,
4923
5351
  element.boundingRect.y,
4924
5352
  onStyleChange,
4925
- styles.left,
4926
- styles.top,
4927
- styles.transform,
5353
+ authoredLeft,
5354
+ authoredTop,
5355
+ authoredTransform,
4928
5356
  ],
4929
5357
  );
4930
5358
 
@@ -4955,21 +5383,21 @@ function PositionLayoutProperties({
4955
5383
  <InspectorIconButton
4956
5384
  label={t("editPanel.textAligns.left")}
4957
5385
  active={alignH === "left"}
4958
- onClick={() => onStyleChange("justifyContent", "flex-start")}
5386
+ onClick={() => handlePositionAlignH("left")}
4959
5387
  >
4960
5388
  <IconLayoutAlignLeft className="size-3.5" />
4961
5389
  </InspectorIconButton>
4962
5390
  <InspectorIconButton
4963
5391
  label={t("editPanel.textAligns.center")}
4964
5392
  active={alignH === "center"}
4965
- onClick={() => onStyleChange("justifyContent", "center")}
5393
+ onClick={() => handlePositionAlignH("center")}
4966
5394
  >
4967
5395
  <IconLayoutAlignCenter className="size-3.5" />
4968
5396
  </InspectorIconButton>
4969
5397
  <InspectorIconButton
4970
5398
  label={t("editPanel.textAligns.right")}
4971
5399
  active={alignH === "right"}
4972
- onClick={() => onStyleChange("justifyContent", "flex-end")}
5400
+ onClick={() => handlePositionAlignH("right")}
4973
5401
  >
4974
5402
  <IconLayoutAlignRight className="size-3.5" />
4975
5403
  </InspectorIconButton>
@@ -4978,21 +5406,21 @@ function PositionLayoutProperties({
4978
5406
  <InspectorIconButton
4979
5407
  label={t("editPanel.alignSelfOptions.start")}
4980
5408
  active={alignV === "top"}
4981
- onClick={() => onStyleChange("alignItems", "flex-start")}
5409
+ onClick={() => handlePositionAlignV("top")}
4982
5410
  >
4983
5411
  <IconLayoutAlignTop className="size-3.5" />
4984
5412
  </InspectorIconButton>
4985
5413
  <InspectorIconButton
4986
5414
  label={t("editPanel.alignSelfOptions.center")}
4987
5415
  active={alignV === "middle"}
4988
- onClick={() => onStyleChange("alignItems", "center")}
5416
+ onClick={() => handlePositionAlignV("middle")}
4989
5417
  >
4990
5418
  <IconLayoutAlignMiddle className="size-3.5" />
4991
5419
  </InspectorIconButton>
4992
5420
  <InspectorIconButton
4993
5421
  label={t("editPanel.alignSelfOptions.end")}
4994
5422
  active={alignV === "bottom"}
4995
- onClick={() => onStyleChange("alignItems", "flex-end")}
5423
+ onClick={() => handlePositionAlignV("bottom")}
4996
5424
  >
4997
5425
  <IconLayoutAlignBottom className="size-3.5" />
4998
5426
  </InspectorIconButton>
@@ -5007,19 +5435,32 @@ function PositionLayoutProperties({
5007
5435
  label="X"
5008
5436
  ariaLabel="X-position"
5009
5437
  tooltipLabel="X-position"
5010
- value={styles.left || ""}
5438
+ value={
5439
+ isMixedValue(authoredLeft) ? MIXED_VALUE : authoredLeft || ""
5440
+ }
5011
5441
  placeholder={element.boundingRect.x}
5012
5442
  inputClassName="h-6"
5013
- onChange={(v) => onStyleChange("left", `${Math.round(v)}px`)}
5443
+ onChange={(v, meta) => {
5444
+ // Typing X/Y on a static (non-positioned) element is a no-op on
5445
+ // canvas unless we first give it a position to offset from —
5446
+ // mirror handleConstraintsChange, which always sets
5447
+ // position:absolute (the convention canvas drag/resize and
5448
+ // primitive creation both use) before writing left/top.
5449
+ if (!constrainedPosition) onStyleChange("position", "absolute");
5450
+ onStyleChange("left", `${Math.round(v)}px`, meta);
5451
+ }}
5014
5452
  />
5015
5453
  <ScrubStyleInput
5016
5454
  label="Y"
5017
5455
  ariaLabel="Y-position"
5018
5456
  tooltipLabel="Y-position"
5019
- value={styles.top || ""}
5457
+ value={isMixedValue(authoredTop) ? MIXED_VALUE : authoredTop || ""}
5020
5458
  placeholder={element.boundingRect.y}
5021
5459
  inputClassName="h-6"
5022
- onChange={(v) => onStyleChange("top", `${Math.round(v)}px`)}
5460
+ onChange={(v, meta) => {
5461
+ if (!constrainedPosition) onStyleChange("position", "absolute");
5462
+ onStyleChange("top", `${Math.round(v)}px`, meta);
5463
+ }}
5023
5464
  />
5024
5465
  <Tooltip>
5025
5466
  <TooltipTrigger asChild>
@@ -5067,13 +5508,30 @@ function PositionLayoutProperties({
5067
5508
  hideIcon={false}
5068
5509
  icon={IconAngle}
5069
5510
  labelClassName="[&>span]:sr-only"
5070
- value={`${parseRotationValue(styles.transform)}deg`}
5511
+ // Detect the Mixed sentinel BEFORE parsing: parseRotationValue
5512
+ // would silently turn "Mixed" into 0 and render "0deg" instead
5513
+ // of the mixed state (mirrors the opacity field's guard).
5514
+ value={
5515
+ isMixedValue(styles.transform)
5516
+ ? MIXED_VALUE
5517
+ : `${parseRotationValue(styles.transform)}deg`
5518
+ }
5071
5519
  unit="deg"
5072
5520
  inputClassName="h-6"
5073
- onChange={(v) =>
5521
+ onChange={(v, meta) =>
5074
5522
  onStyleChange(
5075
5523
  "transform",
5076
- mergeRotationValue(styles.transform, v),
5524
+ // From a mixed selection the sentinel is not a transform
5525
+ // treat it as absent so the typed value applies cleanly to
5526
+ // every selected object instead of producing
5527
+ // "Mixed rotate(…)".
5528
+ mergeRotationValue(
5529
+ isMixedValue(styles.transform)
5530
+ ? undefined
5531
+ : styles.transform,
5532
+ v,
5533
+ ),
5534
+ meta,
5077
5535
  )
5078
5536
  }
5079
5537
  />
@@ -5110,26 +5568,26 @@ function FillProperties({
5110
5568
  onStylesChange,
5111
5569
  }: {
5112
5570
  element: ElementInfo;
5113
- onStyleChange: (property: string, value: string) => void;
5114
- onStylesChange?: (styles: Record<string, string>) => void;
5571
+ onStyleChange: StyleChangeHandler;
5572
+ onStylesChange?: StylesChangeHandler;
5115
5573
  }) {
5116
5574
  const t = useT();
5117
5575
  const styles = element.computedStyles;
5118
- const isTextElement = TEXT_TAGS.has(element.tagName);
5119
- const fillProperty = isTextElement ? "color" : "backgroundColor";
5120
- const fillValue = isTextElement
5576
+ const isTextFillElement = isTextElement(element);
5577
+ const fillProperty = isTextFillElement ? "color" : "backgroundColor";
5578
+ const fillValue = isTextFillElement
5121
5579
  ? styles.color || ""
5122
5580
  : styles.backgroundColor || "";
5123
- const backgroundLayers = isTextElement
5581
+ const backgroundLayers = isTextFillElement
5124
5582
  ? []
5125
5583
  : splitCssLayers(styles.backgroundImage || "");
5126
- const backgroundSizeLayers = isTextElement
5584
+ const backgroundSizeLayers = isTextFillElement
5127
5585
  ? []
5128
5586
  : splitCssLayers(styles.backgroundSize || "");
5129
- const backgroundRepeatLayers = isTextElement
5587
+ const backgroundRepeatLayers = isTextFillElement
5130
5588
  ? []
5131
5589
  : splitCssLayers(styles.backgroundRepeat || "");
5132
- const backgroundPositionLayers = isTextElement
5590
+ const backgroundPositionLayers = isTextFillElement
5133
5591
  ? []
5134
5592
  : splitCssLayers(styles.backgroundPosition || "");
5135
5593
  const fillIsMixed =
@@ -5138,39 +5596,36 @@ function FillProperties({
5138
5596
  isMixedValue(styles.backgroundSize) ||
5139
5597
  isMixedValue(styles.backgroundRepeat) ||
5140
5598
  isMixedValue(styles.backgroundPosition);
5141
- const hasBackgroundLayer = !isTextElement && backgroundLayers.length > 0;
5599
+ const hasBackgroundLayer = !isTextFillElement && backgroundLayers.length > 0;
5142
5600
  const hasVisibleFill =
5143
- isTextElement || colorHasVisibleAlpha(fillValue) || hasBackgroundLayer;
5144
-
5145
- // Non-destructive fill hide: stash the color before hiding so toggling
5146
- // visible again restores the exact original value (the design editor never loses color).
5147
- // Keyed by a stable selected-element identity so anonymous same-tag elements
5148
- // don't share stash slots.
5149
- const [hiddenFillStash, setHiddenFillStash] = useState<
5150
- Record<string, string>
5151
- >({});
5152
- // Same non-destructive idea for background gradient/image layers: stash the
5153
- // exact layer string on hide so per-stop opacity survives a hide→show toggle
5154
- // instead of being flattened to all-0 then all-100.
5155
- const [hiddenLayerStash, setHiddenLayerStash] = useState<
5156
- Record<string, string>
5157
- >({});
5158
- const stashKey = `${elementIdentityKey(element)}:${fillProperty}`;
5601
+ isTextFillElement || colorHasVisibleAlpha(fillValue) || hasBackgroundLayer;
5602
+
5603
+ // Non-destructive fill hide: instead of stashing the pre-hide color in
5604
+ // React state (lost on unmount e.g. deselect then reselect the same
5605
+ // element, since FillProperties only mounts while something is selected),
5606
+ // zero the alpha channel while preserving the RGB channels in the
5607
+ // persisted CSS itself: rgba(r,g,b,0) renders identically to fully
5608
+ // transparent, but getComputedStyle round-trips the r/g/b losslessly (CSS
5609
+ // color-list channels are real data, unlike comments — verified
5610
+ // separately: computed style strips comments but keeps rgba() channels).
5611
+ // Showing again just restores alpha to 1 using those same channels, so no
5612
+ // stash is required and the hide survives reselect/reload.
5159
5613
  const isHidden = !colorHasVisibleAlpha(fillValue);
5160
5614
  const handleFillVisibilityToggle = () => {
5615
+ const parsed = parseCssColor(fillValue);
5161
5616
  if (isHidden) {
5162
- // Restore the stashed color, or fall back to a sensible default.
5163
- const restored =
5164
- hiddenFillStash[stashKey] ?? (isTextElement ? "#000000" : "#ffffff");
5617
+ const restored = parsed
5618
+ ? rgbaToCss(withColorOpacity(parsed, 100))
5619
+ : isTextFillElement
5620
+ ? "#000000"
5621
+ : "#ffffff";
5165
5622
  onStyleChange(fillProperty, restored);
5166
- setHiddenFillStash((prev) => {
5167
- const next = { ...prev };
5168
- delete next[stashKey];
5169
- return next;
5170
- });
5623
+ } else if (parsed) {
5624
+ onStyleChange(fillProperty, rgbaToCss(withColorOpacity(parsed, 0)));
5171
5625
  } else {
5172
- // Stash the current color before going transparent.
5173
- setHiddenFillStash((prev) => ({ ...prev, [stashKey]: fillValue }));
5626
+ // Value wasn't a parseable color (e.g. already the literal
5627
+ // "transparent") nothing to preserve, transparent is the best we
5628
+ // can do.
5174
5629
  onStyleChange(fillProperty, "transparent");
5175
5630
  }
5176
5631
  };
@@ -5219,7 +5674,7 @@ function FillProperties({
5219
5674
  );
5220
5675
  return;
5221
5676
  }
5222
- if (isTextElement) {
5677
+ if (isTextFillElement) {
5223
5678
  onStyleChange(
5224
5679
  "color",
5225
5680
  cssColorOrFallback(styles.color, "#000000"),
@@ -5257,25 +5712,59 @@ function FillProperties({
5257
5712
  </p>
5258
5713
  ) : hasVisibleFill ? (
5259
5714
  <div className="space-y-1.5">
5260
- {isTextElement || colorHasVisibleAlpha(fillValue) ? (
5715
+ {isTextFillElement || colorHasVisibleAlpha(fillValue) ? (
5261
5716
  /* design row: [swatch+hex trigger (flex-1)] [eye] [remove] */
5262
5717
  <div className="group flex items-center gap-1.5">
5263
5718
  <div className="min-w-0 flex-1">
5264
5719
  <ColorInput
5265
5720
  label=""
5266
5721
  value={fillValue}
5267
- onChange={(v) => onStyleChange(fillProperty, v)}
5268
- backgroundImage=""
5722
+ onChange={(v, meta) => onStyleChange(fillProperty, v, meta)}
5723
+ // Pass the real layer stack (not "") so that switching this
5724
+ // swatch's paint type to gradient/image composes a new
5725
+ // layer on top of any existing backgroundImage layers
5726
+ // (rendered as their own rows below) instead of clobbering
5727
+ // them — ColorInput derives its add/replace-layer logic
5728
+ // from this prop.
5729
+ backgroundImage={
5730
+ isTextFillElement ? "" : styles.backgroundImage
5731
+ }
5269
5732
  blendMode={
5270
- isTextElement
5733
+ isTextFillElement
5271
5734
  ? undefined
5272
5735
  : styles.backgroundBlendMode || "normal"
5273
5736
  }
5274
5737
  onBlendModeChange={
5275
- isTextElement
5738
+ isTextFillElement
5276
5739
  ? undefined
5277
5740
  : (v) => onStyleChange("backgroundBlendMode", v)
5278
5741
  }
5742
+ // Text fill ("color") can't hold a gradient/image paint —
5743
+ // there is no background-clip:text support here — so never
5744
+ // offer layered fills for it; the picker's Gradient/Image
5745
+ // tabs remain reachable as raw UI (it manages that tab
5746
+ // selection as local state independent of these props) but
5747
+ // ColorInput's setNext guard rejects non-color writes back
5748
+ // into `color` when supportsLayeredFills is false. For any
5749
+ // other element the base fill is a real backgroundImage
5750
+ // layer stack, so wire the same layered-fill handlers the
5751
+ // page background row uses (see PageProperties above).
5752
+ supportsLayeredFills={!isTextFillElement}
5753
+ onBackgroundImageChange={
5754
+ isTextFillElement
5755
+ ? undefined
5756
+ : (v) => onStyleChange("backgroundImage", v)
5757
+ }
5758
+ onImageFillChange={
5759
+ isTextFillElement
5760
+ ? undefined
5761
+ : (value) =>
5762
+ commitStylePatch(
5763
+ imageFillToBackgroundStyles(value),
5764
+ onStyleChange,
5765
+ onStylesChange,
5766
+ )
5767
+ }
5279
5768
  documentColors={documentColors}
5280
5769
  pickerKey={[
5281
5770
  element.sourceId ??
@@ -5304,7 +5793,7 @@ function FillProperties({
5304
5793
  <SectionIconButton
5305
5794
  label={t("editPanel.labels.removeLayer")}
5306
5795
  onClick={() => {
5307
- if (isTextElement) {
5796
+ if (isTextFillElement) {
5308
5797
  onStyleChange(fillProperty, "transparent");
5309
5798
  return;
5310
5799
  }
@@ -5322,23 +5811,21 @@ function FillProperties({
5322
5811
  </SectionIconButton>
5323
5812
  </div>
5324
5813
  ) : null}
5325
- {!isTextElement
5814
+ {!isTextFillElement
5326
5815
  ? backgroundLayers.map((layer, index) => {
5327
5816
  const gradient = parseGradientLayer(layer);
5817
+ // Hidden state lives in the real, persisted backgroundSize
5818
+ // marker (see withLayerSizeMarker) rather than React state,
5819
+ // so it survives deselect/reselect. Opacity still reflects
5820
+ // the gradient's own stop opacities for display, but no
5821
+ // longer drives hide/show — a layer can be a fully-opaque
5822
+ // gradient and still be hidden via zero-size.
5823
+ const hidden = isLayerHiddenBySize(backgroundSizeLayers[index]);
5328
5824
  const opacity = gradient
5329
5825
  ? averageGradientOpacity(gradient.stops)
5330
5826
  : 100;
5331
- const layerStashKey = `${elementIdentityKey(element)}:layer:${index}`;
5332
- const stashedLayer = hiddenLayerStash[layerStashKey];
5333
- const hiddenImagePlaceholder = Boolean(
5334
- stashedLayer && gradient && opacity <= 0,
5335
- );
5336
5827
  const label = gradient
5337
- ? hiddenImagePlaceholder
5338
- ? `${"Image" /* i18n-ignore design inspector paint row */} ${
5339
- index + 1
5340
- }`
5341
- : `${gradientLabel(gradient.type)} ${index + 1}`
5828
+ ? `${gradientLabel(gradient.type)} ${index + 1}`
5342
5829
  : `${"Image" /* i18n-ignore design inspector paint row */} ${
5343
5830
  index + 1
5344
5831
  }`;
@@ -5356,6 +5843,25 @@ function FillProperties({
5356
5843
  ),
5357
5844
  ),
5358
5845
  );
5846
+ onStyleChange(
5847
+ "backgroundSize",
5848
+ joinCssLayers(
5849
+ backgroundSizeLayers.filter(
5850
+ (_, layerIndex) => layerIndex !== index,
5851
+ ),
5852
+ ),
5853
+ );
5854
+ };
5855
+ const setLayerHidden = (nextHidden: boolean) => {
5856
+ onStyleChange(
5857
+ "backgroundSize",
5858
+ withLayerSizeMarker(
5859
+ backgroundSizeLayers,
5860
+ backgroundLayers.length,
5861
+ index,
5862
+ nextHidden,
5863
+ ),
5864
+ );
5359
5865
  };
5360
5866
 
5361
5867
  return (
@@ -5378,7 +5884,7 @@ function FillProperties({
5378
5884
  {label}
5379
5885
  </span>
5380
5886
  <span className="shrink-0 tabular-nums text-muted-foreground">
5381
- {opacity}%
5887
+ {hidden ? 0 : opacity}%
5382
5888
  </span>
5383
5889
  </button>
5384
5890
  </PopoverTrigger>
@@ -5389,7 +5895,7 @@ function FillProperties({
5389
5895
  className="w-80 p-0"
5390
5896
  >
5391
5897
  <DesignColorPicker
5392
- value={gradient?.stops[0]?.color ?? layer}
5898
+ value={layer}
5393
5899
  onPaintValueChange={replaceLayer}
5394
5900
  onChange={(nextColor) => {
5395
5901
  if (!gradient) return;
@@ -5434,78 +5940,14 @@ function FillProperties({
5434
5940
  </Popover>
5435
5941
  <SectionIconButton
5436
5942
  label={
5437
- opacity <= 0
5943
+ hidden
5438
5944
  ? t("editPanel.labels.showLayer")
5439
5945
  : t("editPanel.labels.hideLayer")
5440
5946
  }
5441
- onClick={() => {
5442
- if (opacity <= 0) {
5443
- // Show: restore the exact pre-hide layer if stashed,
5444
- // otherwise fall back to forcing every stop opaque.
5445
- const stashed = hiddenLayerStash[layerStashKey];
5446
- if (stashed !== undefined) {
5447
- replaceLayer(stashed);
5448
- setHiddenLayerStash((prev) => {
5449
- const next = { ...prev };
5450
- delete next[layerStashKey];
5451
- return next;
5452
- });
5453
- } else if (gradient) {
5454
- replaceLayer(
5455
- buildGradientLayer(
5456
- gradient.type,
5457
- gradient.stops.map((stop) => ({
5458
- ...stop,
5459
- opacity: 100,
5460
- })),
5461
- gradient.prefix,
5462
- ),
5463
- );
5464
- }
5465
- return;
5466
- }
5467
- if (!gradient) {
5468
- setHiddenLayerStash((prev) => ({
5469
- ...prev,
5470
- [layerStashKey]: layer,
5471
- }));
5472
- replaceLayer(
5473
- buildGradientLayer("linear", [
5474
- {
5475
- id: "stop-0",
5476
- color: "rgba(0, 0, 0, 0)",
5477
- position: 0,
5478
- opacity: 0,
5479
- },
5480
- {
5481
- id: "stop-1",
5482
- color: "rgba(0, 0, 0, 0)",
5483
- position: 100,
5484
- opacity: 0,
5485
- },
5486
- ]),
5487
- );
5488
- } else {
5489
- // Hide: stash the current layer (with its real per-stop
5490
- // opacities) before zeroing every stop's alpha.
5491
- setHiddenLayerStash((prev) => ({
5492
- ...prev,
5493
- [layerStashKey]: layer,
5494
- }));
5495
- replaceLayer(
5496
- buildGradientLayer(
5497
- gradient.type,
5498
- gradient.stops.map((stop) => ({
5499
- ...stop,
5500
- opacity: 0,
5501
- })),
5502
- gradient.prefix,
5503
- ),
5504
- );
5505
- }
5506
- }}
5947
+ onClick={() => setLayerHidden(!hidden)}
5948
+ activateOnPointerDown
5507
5949
  >
5508
- {opacity <= 0 ? (
5950
+ {hidden ? (
5509
5951
  <IconEyeOff className="size-3.5" />
5510
5952
  ) : (
5511
5953
  <IconEye className="size-3.5" />
@@ -5533,16 +5975,19 @@ function StrokeProperties({
5533
5975
  onStylesChange,
5534
5976
  }: {
5535
5977
  element: ElementInfo;
5536
- onStyleChange: (property: string, value: string) => void;
5537
- onStylesChange?: (styles: Record<string, string>) => void;
5978
+ onStyleChange: StyleChangeHandler;
5979
+ onStylesChange?: StylesChangeHandler;
5538
5980
  }) {
5539
5981
  const t = useT();
5540
5982
  const styles = element.computedStyles;
5541
- const borderVisible = strokeIsVisible(styles.borderWidth, styles.borderStyle);
5542
- const outlineVisible = strokeIsVisible(
5543
- styles.outlineWidth,
5544
- styles.outlineStyle,
5545
- );
5983
+ // Visible requires: real width, style not "none" (legacy hide path), and
5984
+ // color not zero-alpha (current hide path — see strokeHiddenByColor).
5985
+ const borderVisible =
5986
+ strokeIsVisible(styles.borderWidth, styles.borderStyle) &&
5987
+ !strokeHiddenByColor(styles.borderColor);
5988
+ const outlineVisible =
5989
+ strokeIsVisible(styles.outlineWidth, styles.outlineStyle) &&
5990
+ !strokeHiddenByColor(styles.outlineColor);
5546
5991
  const strokeIsMixed = [
5547
5992
  styles.borderWidth,
5548
5993
  styles.borderStyle,
@@ -5580,8 +6025,17 @@ function StrokeProperties({
5580
6025
  return;
5581
6026
  }
5582
6027
  if (!borderVisible) {
6028
+ // Restore full alpha before falling back to cssColorOrFallback
6029
+ // — a border previously hidden via the eye toggle (zero-alpha,
6030
+ // real RGB preserved) is not "transparent" by that helper's
6031
+ // narrow literal check, so without this an "Add" click here
6032
+ // could silently re-add an invisible border.
6033
+ const existingBorderColor = styles.borderColor || styles.color;
6034
+ const existingParsed = parseCssColor(existingBorderColor || "");
5583
6035
  const borderColor = cssColorOrFallback(
5584
- styles.borderColor || styles.color,
6036
+ existingParsed
6037
+ ? rgbaToCss(withColorOpacity(existingParsed, 100))
6038
+ : existingBorderColor,
5585
6039
  "#000000",
5586
6040
  );
5587
6041
  commitStylePatch(
@@ -5684,7 +6138,7 @@ function AppearanceProperties({
5684
6138
  onStyleChange,
5685
6139
  }: {
5686
6140
  element: ElementInfo;
5687
- onStyleChange: (property: string, value: string) => void;
6141
+ onStyleChange: StyleChangeHandler;
5688
6142
  }) {
5689
6143
  const t = useT();
5690
6144
  const styles = element.computedStyles;
@@ -5734,7 +6188,9 @@ function AppearanceProperties({
5734
6188
  ? 0
5735
6189
  : parseNumericValue(styles.opacity || "1") * 100
5736
6190
  }
5737
- onChange={(v) => onStyleChange("opacity", String(v / 100))}
6191
+ onChange={(v, meta) =>
6192
+ onStyleChange("opacity", String(v / 100), meta)
6193
+ }
5738
6194
  mixed={isMixedValue(styles.opacity)}
5739
6195
  min={0}
5740
6196
  max={100}
@@ -5742,7 +6198,15 @@ function AppearanceProperties({
5742
6198
  unit="%"
5743
6199
  precision={1}
5744
6200
  />
5745
- <CornerRadiusControl styles={styles} onStyleChange={onStyleChange} />
6201
+ {/* Selection-stable key so per-selection UI state (the independent-
6202
+ corners toggle, which ratchets open while corners differ) resets on
6203
+ selection change instead of leaking to the next element — same
6204
+ pattern as ExportSettingsPanel. */}
6205
+ <CornerRadiusControl
6206
+ key={elementIdentityKey(element)}
6207
+ styles={styles}
6208
+ onStyleChange={onStyleChange}
6209
+ />
5746
6210
  </div>
5747
6211
  </PanelSection>
5748
6212
  );
@@ -5754,8 +6218,8 @@ function EffectsProperties({
5754
6218
  onStylesChange,
5755
6219
  }: {
5756
6220
  element: ElementInfo;
5757
- onStyleChange: (property: string, value: string) => void;
5758
- onStylesChange?: (styles: Record<string, string>) => void;
6221
+ onStyleChange: StyleChangeHandler;
6222
+ onStylesChange?: StylesChangeHandler;
5759
6223
  }) {
5760
6224
  const t = useT();
5761
6225
  const styles = element.computedStyles;
@@ -5774,10 +6238,10 @@ function EffectsProperties({
5774
6238
  const layerBlurStashKey = `${effectStashKey}:filter:blur`;
5775
6239
  const backdropBlurStashKey = `${effectStashKey}:backdrop-filter:blur`;
5776
6240
  const shadowLayers = parseShadowLayers(styles.boxShadow);
5777
- const setShadowLayers = (layers: ShadowLayer[]) => {
6241
+ const setShadowLayers = (layers: ShadowLayer[], meta?: StyleChangeMeta) => {
5778
6242
  const boxShadow = serializeShadowLayers(layers);
5779
- if (onStylesChange) onStylesChange({ boxShadow });
5780
- else onStyleChange("boxShadow", boxShadow);
6243
+ if (onStylesChange) onStylesChange({ boxShadow }, meta);
6244
+ else onStyleChange("boxShadow", boxShadow, meta);
5781
6245
  };
5782
6246
  const addDropShadow = () =>
5783
6247
  setShadowLayers([
@@ -5836,13 +6300,13 @@ function EffectsProperties({
5836
6300
  key={layer.id}
5837
6301
  layer={layer}
5838
6302
  index={index}
5839
- onChange={(patch) => {
6303
+ onChange={(patch, meta) => {
5840
6304
  const next = shadowLayers.map((candidate) =>
5841
6305
  candidate.id === layer.id
5842
6306
  ? { ...candidate, ...patch }
5843
6307
  : candidate,
5844
6308
  );
5845
- setShadowLayers(next);
6309
+ setShadowLayers(next, meta);
5846
6310
  }}
5847
6311
  onToggleVisibility={() => {
5848
6312
  const visible = colorHasVisibleAlpha(layer.color);
@@ -5958,10 +6422,11 @@ function EffectsProperties({
5958
6422
  <ScrubInput
5959
6423
  label={t("editPanel.labels.blur")}
5960
6424
  value={blurValue}
5961
- onChange={(value) =>
6425
+ onChange={(value, meta) =>
5962
6426
  onStyleChange(
5963
6427
  "filter",
5964
6428
  setBlurFilterValue(styles.filter, value),
6429
+ meta,
5965
6430
  )
5966
6431
  }
5967
6432
  unit="px"
@@ -6048,10 +6513,11 @@ function EffectsProperties({
6048
6513
  <ScrubInput
6049
6514
  label={t("editPanel.labels.blur")}
6050
6515
  value={backdropBlurValue}
6051
- onChange={(value) =>
6516
+ onChange={(value, meta) =>
6052
6517
  onStyleChange(
6053
6518
  "backdropFilter",
6054
6519
  setBlurFilterValue(backdropFilterValue, value),
6520
+ meta,
6055
6521
  )
6056
6522
  }
6057
6523
  unit="px"
@@ -6108,7 +6574,7 @@ function SelectionColorsProperties({
6108
6574
  onStyleChange,
6109
6575
  }: {
6110
6576
  element: ElementInfo;
6111
- onStyleChange: (property: string, value: string) => void;
6577
+ onStyleChange: StyleChangeHandler;
6112
6578
  }) {
6113
6579
  // M6 · the design editor's Selection colors collapses to a single "Show selection colors"
6114
6580
  // affordance, expanding to one editable [swatch · hex · opacity] row per
@@ -6154,7 +6620,19 @@ function SelectionColorsProperties({
6154
6620
  >
6155
6621
  <DesignColorPicker
6156
6622
  value={cssColorOrFallback(color.value, "#000000")}
6157
- onChange={(value) => onStyleChange(color.property, value)}
6623
+ // PF12: per-tick drag preview vs. one authoritative
6624
+ // commit on gesture-end — same split as ColorInput's
6625
+ // setNext (see its PF12 comment above).
6626
+ onChange={(value) =>
6627
+ onStyleChange(color.property, value, {
6628
+ phase: "preview",
6629
+ })
6630
+ }
6631
+ onChangeComplete={(value) =>
6632
+ onStyleChange(color.property, value, {
6633
+ phase: "commit",
6634
+ })
6635
+ }
6158
6636
  />
6159
6637
  </PopoverContent>
6160
6638
  </Popover>
@@ -6928,7 +7406,18 @@ export function ComponentSection({
6928
7406
  );
6929
7407
  }
6930
7408
 
6931
- export function EditPanel({
7409
+ // PF8: EditPanel re-renders on every DesignEditor state change (drag,
7410
+ // hover, zoom) unless memoized. Nearly all props are already stabilized at
7411
+ // the call site (useMemo/useCallback — see DesignEditor.tsx's
7412
+ // selectedInspectorElements/selectedScreenGeometry/pageStyles/tweaks/
7413
+ // sourceCapabilities/statesPanelProps/reviewPanelProps and the onXxx
7414
+ // handlers passed to <EditPanel>). `headerTrailing` and `aiActions` are
7415
+ // legitimately-dynamic ReactNode slots (the zoom control repaints its own
7416
+ // live percentage every zoom tick; aiActions depends on the live
7417
+ // selection) — their identity is expected to change often and a custom
7418
+ // comparator that ignored them would hide real content changes, so this
7419
+ // uses the default shallow comparison rather than special-casing them.
7420
+ export const EditPanel = memo(function EditPanel({
6932
7421
  selectedElement,
6933
7422
  selectedElements,
6934
7423
  selectedScreenGeometry,
@@ -7344,4 +7833,4 @@ export function EditPanel({
7344
7833
  ) : null}
7345
7834
  </div>
7346
7835
  );
7347
- }
7836
+ });