@agent-native/core 0.83.0 → 0.84.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 (424) hide show
  1. package/README.md +2 -2
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +28 -0
  4. package/corpus/core/README.md +2 -2
  5. package/corpus/core/docs/content/cloneable-saas.mdx +2 -2
  6. package/corpus/core/docs/content/faq.mdx +1 -1
  7. package/corpus/core/docs/content/locales/ar-SA/cloneable-saas.mdx +2 -2
  8. package/corpus/core/docs/content/locales/ar-SA/faq.mdx +1 -1
  9. package/corpus/core/docs/content/locales/ar-SA/template-chat.mdx +1 -1
  10. package/corpus/core/docs/content/locales/de-DE/cloneable-saas.mdx +2 -2
  11. package/corpus/core/docs/content/locales/de-DE/faq.mdx +1 -1
  12. package/corpus/core/docs/content/locales/de-DE/template-chat.mdx +1 -1
  13. package/corpus/core/docs/content/locales/es-ES/cloneable-saas.mdx +2 -2
  14. package/corpus/core/docs/content/locales/es-ES/faq.mdx +1 -1
  15. package/corpus/core/docs/content/locales/es-ES/template-chat.mdx +1 -1
  16. package/corpus/core/docs/content/locales/fr-FR/cloneable-saas.mdx +2 -2
  17. package/corpus/core/docs/content/locales/fr-FR/faq.mdx +1 -1
  18. package/corpus/core/docs/content/locales/fr-FR/template-chat.mdx +1 -1
  19. package/corpus/core/docs/content/locales/hi-IN/cloneable-saas.mdx +2 -2
  20. package/corpus/core/docs/content/locales/hi-IN/faq.mdx +1 -1
  21. package/corpus/core/docs/content/locales/hi-IN/template-chat.mdx +1 -1
  22. package/corpus/core/docs/content/locales/ja-JP/cloneable-saas.mdx +2 -2
  23. package/corpus/core/docs/content/locales/ja-JP/faq.mdx +1 -1
  24. package/corpus/core/docs/content/locales/ja-JP/template-chat.mdx +1 -1
  25. package/corpus/core/docs/content/locales/ko-KR/cloneable-saas.mdx +2 -2
  26. package/corpus/core/docs/content/locales/ko-KR/faq.mdx +1 -1
  27. package/corpus/core/docs/content/locales/ko-KR/template-chat.mdx +1 -1
  28. package/corpus/core/docs/content/locales/pt-BR/cloneable-saas.mdx +2 -2
  29. package/corpus/core/docs/content/locales/pt-BR/faq.mdx +1 -1
  30. package/corpus/core/docs/content/locales/pt-BR/template-chat.mdx +1 -1
  31. package/corpus/core/docs/content/locales/zh-CN/cloneable-saas.mdx +2 -2
  32. package/corpus/core/docs/content/locales/zh-CN/faq.mdx +1 -1
  33. package/corpus/core/docs/content/locales/zh-CN/template-chat.mdx +1 -1
  34. package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.mdx +2 -2
  35. package/corpus/core/docs/content/locales/zh-TW/faq.mdx +1 -1
  36. package/corpus/core/docs/content/locales/zh-TW/template-chat.mdx +1 -1
  37. package/corpus/core/docs/content/template-chat.mdx +1 -1
  38. package/corpus/core/package.json +1 -1
  39. package/corpus/core/src/agent/engine/builtin.ts +1 -1
  40. package/corpus/core/src/agent/engine/output-tokens.ts +1 -1
  41. package/corpus/core/src/agent/harness/acp-adapter.ts +28 -4
  42. package/corpus/core/src/agent/harness/ai-sdk-adapter.ts +3 -0
  43. package/corpus/core/src/agent/harness/translate.ts +3 -0
  44. package/corpus/core/src/agent/harness/types.ts +1 -0
  45. package/corpus/core/src/agent/model-config.ts +34 -13
  46. package/corpus/core/src/agent/production-agent.ts +87 -10
  47. package/corpus/core/src/agent/thread-data-builder.ts +29 -4
  48. package/corpus/core/src/agent/tool-call-journal.ts +109 -48
  49. package/corpus/core/src/agent/types.ts +6 -1
  50. package/corpus/core/src/cli/design-connect.ts +106 -0
  51. package/corpus/core/src/cli/gateway-helpers.ts +17 -0
  52. package/corpus/core/src/cli/recap.ts +0 -2
  53. package/corpus/core/src/cli/skills.ts +54 -16
  54. package/corpus/core/src/cli/workspace-dev.ts +29 -5
  55. package/corpus/core/src/client/AssistantChat.tsx +89 -42
  56. package/corpus/core/src/client/DefaultSpinner.tsx +8 -1
  57. package/corpus/core/src/client/chat/message-components.tsx +49 -1
  58. package/corpus/core/src/client/chat/repo-helpers.ts +71 -0
  59. package/corpus/core/src/client/chat/run-recovery.tsx +15 -5
  60. package/corpus/core/src/client/components/ApiKeySettings.tsx +8 -8
  61. package/corpus/core/src/client/components/CodeRequiredDialog.tsx +26 -15
  62. package/corpus/core/src/client/components/MissingKeyCard.tsx +13 -6
  63. package/corpus/core/src/client/composer/TiptapComposer.tsx +4 -3
  64. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +0 -1
  65. package/corpus/core/src/client/guided-questions.tsx +21 -27
  66. package/corpus/core/src/client/integrations/IntegrationCard.tsx +1 -1
  67. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +1 -1
  68. package/corpus/core/src/client/onboarding/OnboardingBanner.tsx +2 -2
  69. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +23 -19
  70. package/corpus/core/src/client/resources/ResourceEditor.tsx +18 -10
  71. package/corpus/core/src/client/resources/ResourcesPanel.tsx +5 -1
  72. package/corpus/core/src/client/settings/SettingsPanel.tsx +8 -3
  73. package/corpus/core/src/client/settings/useBuilderStatus.ts +8 -41
  74. package/corpus/core/src/client/useProductionAgent.ts +6 -2
  75. package/corpus/core/src/deploy/build.ts +1 -1
  76. package/corpus/core/src/observability/traces.ts +3 -3
  77. package/corpus/core/src/onboarding/default-steps.ts +1 -1
  78. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +1 -1
  79. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +1 -1
  80. package/corpus/core/src/server/agent-chat-plugin.ts +177 -81
  81. package/corpus/core/src/server/core-routes-plugin.ts +7 -1
  82. package/corpus/core/src/server/open-route.ts +59 -40
  83. package/corpus/core/src/shared/reasoning-effort.ts +2 -0
  84. package/corpus/core/src/sharing/access.ts +22 -2
  85. package/corpus/core/src/sharing/registry.ts +18 -0
  86. package/corpus/core/src/styles/agent-native.css +104 -20
  87. package/corpus/core/src/styles/rich-markdown-editor.css +11 -10
  88. package/corpus/templates/analytics/app/components/dashboard/CumulativeNetChart.tsx +10 -10
  89. package/corpus/templates/analytics/app/components/dashboard/RevenueChart.tsx +10 -10
  90. package/corpus/templates/analytics/app/components/dashboard/RevenueComparisonChart.tsx +6 -5
  91. package/corpus/templates/analytics/app/components/dashboard/TimeSeriesChart.tsx +9 -9
  92. package/corpus/templates/analytics/app/global.css +4 -0
  93. package/corpus/templates/analytics/app/lib/chart-theme.ts +13 -0
  94. package/corpus/templates/analytics/app/pages/adhoc/_shared/KpiChart.tsx +22 -24
  95. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-charts-now-use-theme-aware-tooltip-and-grid-colors.md +6 -0
  96. package/corpus/templates/analytics/changelog/2026-06-30-fixed-ask-composer-spacing-so-the-chat-field-has-a-comfortab.md +6 -0
  97. package/corpus/templates/calendar/AGENTS.md +4 -0
  98. package/corpus/templates/calendar/actions/connect-google-calendar.ts +80 -0
  99. package/corpus/templates/calendar/changelog/2026-06-30-calendar-now-gives-the-agent-a-safe-google-connection-link-i.md +6 -0
  100. package/corpus/templates/calendar/server/plugins/agent-chat.ts +4 -0
  101. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +5 -5
  102. package/corpus/templates/content/app/global.css +6 -5
  103. package/corpus/templates/content/changelog/2026-06-30-editor-floating-toolbars-now-use-theme-aware-colors-in-light.md +6 -0
  104. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +6 -1
  105. package/corpus/templates/design/actions/add-localhost-screens.ts +63 -8
  106. package/corpus/templates/design/actions/apply-motion-edit.ts +12 -46
  107. package/corpus/templates/design/actions/list-design-native-assets.ts +2 -1
  108. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +49 -1
  109. package/corpus/templates/design/actions/navigate.ts +3 -12
  110. package/corpus/templates/design/actions/open-visual-edit.ts +341 -0
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +1 -1
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +345 -14
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +71 -91
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +25 -27
  116. package/corpus/templates/design/app/components/design/DeviceFrame.tsx +1 -1
  117. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +2 -2
  118. package/corpus/templates/design/app/components/design/EditPanel.tsx +1111 -570
  119. package/corpus/templates/design/app/components/design/LayersPanel.tsx +162 -32
  120. package/corpus/templates/design/app/components/design/MotionDock.tsx +268 -240
  121. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1038 -150
  122. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +1 -1
  123. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +81 -102
  124. package/corpus/templates/design/app/components/design/StatesPanel.tsx +6 -6
  125. package/corpus/templates/design/app/components/design/TokensPanel.tsx +11 -11
  126. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +9 -7
  127. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1129 -155
  128. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +130 -37
  129. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +24 -11
  130. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +184 -162
  131. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +137 -79
  132. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +13 -13
  133. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +8 -8
  134. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +2 -2
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +4 -4
  136. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +3 -3
  137. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +13 -7
  138. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +14 -11
  139. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +4 -4
  140. package/corpus/templates/design/app/components/design/inspector/index.ts +2 -0
  141. package/corpus/templates/design/app/components/design/types.ts +19 -0
  142. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +61 -78
  143. package/corpus/templates/design/app/components/layout/Header.tsx +0 -1
  144. package/corpus/templates/design/app/components/layout/Layout.tsx +3 -2
  145. package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
  146. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +5 -4
  147. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  148. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +2 -2
  149. package/corpus/templates/design/app/global.css +56 -0
  150. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -5
  151. package/corpus/templates/design/app/hooks/use-navigation-state.ts +6 -7
  152. package/corpus/templates/design/app/hooks/use-question-flow.ts +3 -2
  153. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +6 -4
  154. package/corpus/templates/design/app/i18n/zh-TW.ts +5 -14
  155. package/corpus/templates/design/app/i18n-data.ts +63 -188
  156. package/corpus/templates/design/app/lib/agent-chat.ts +13 -0
  157. package/corpus/templates/design/app/pages/DesignEditor.tsx +1428 -384
  158. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -5
  159. package/corpus/templates/design/app/pages/DesignSystems.tsx +2 -2
  160. package/corpus/templates/design/app/pages/Index.tsx +49 -26
  161. package/corpus/templates/design/app/pages/NotFound.tsx +25 -8
  162. package/corpus/templates/design/app/pages/VisualEdit.tsx +17 -5
  163. package/corpus/templates/design/app/routes/examples.tsx +1 -7
  164. package/corpus/templates/design/app/routes/templates.tsx +7 -4
  165. package/corpus/templates/design/changelog/2026-06-30-board-shapes-and-auto-layout-controls-are-clearer.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-component-selections-now-use-purple-component-chrome-and-sho.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-constraints-now-stay-tucked-into-a-compact-inspector-preview.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-creating-a-component-now-opens-in-a-compact-inspector-popove.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-design-cards-no-longer-show-legacy-project-type-badges.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-design-chat-now-keeps-new-turns-in-the-visible-design-agent-.md +6 -0
  171. package/corpus/templates/design/changelog/2026-06-30-design-editor-panels-now-keep-their-compact-text-sizing-cons.md +6 -0
  172. package/corpus/templates/design/changelog/2026-06-30-design-no-longer-shows-the-connected-local-code-warning-bann.md +6 -0
  173. package/corpus/templates/design/changelog/2026-06-30-design-now-opens-to-the-files-panel-unless-a-generation-is-a.md +6 -0
  174. package/corpus/templates/design/changelog/2026-06-30-design-opens-with-a-cleaner-empty-canvas-and-loading-preview.md +6 -0
  175. package/corpus/templates/design/changelog/2026-06-30-design-tools-use-simpler-less-decorative-icons.md +6 -0
  176. package/corpus/templates/design/changelog/2026-06-30-dragging-absolute-elements-no-longer-shows-coordinate-labels.md +6 -0
  177. package/corpus/templates/design/changelog/2026-06-30-fixed-all-screens-opening-too-zoomed-in-by-default.md +6 -0
  178. package/corpus/templates/design/changelog/2026-06-30-fixed-the-public-design-assets-sidebar-so-native-components-.md +6 -0
  179. package/corpus/templates/design/changelog/2026-06-30-fixed-the-screen-overview-board-so-empty-board-space-no-long.md +6 -0
  180. package/corpus/templates/design/changelog/2026-06-30-full-view-screens-can-now-scroll-while-remaining-editable.md +6 -0
  181. package/corpus/templates/design/changelog/2026-06-30-inspector-element-headers-show-the-selected-html-tag-and-the.md +6 -0
  182. package/corpus/templates/design/changelog/2026-06-30-inspector-inputs-stay-compact-across-desktop-breakpoints.md +6 -0
  183. package/corpus/templates/design/changelog/2026-06-30-layers-can-be-multi-selected-from-the-canvas-and-edited-toge.md +6 -0
  184. package/corpus/templates/design/changelog/2026-06-30-local-visual-edit-links-can-edit-localhost-frames-without-si.md +6 -0
  185. package/corpus/templates/design/changelog/2026-06-30-localhost-visual-edit-frames-can-now-be-selected-and-edited.md +6 -0
  186. package/corpus/templates/design/changelog/2026-06-30-marquee-selection-now-works-inside-screens-multi-selected-la.md +6 -0
  187. package/corpus/templates/design/changelog/2026-06-30-motion-is-now-available-from-the-persistent-left-rail-and-th.md +6 -0
  188. package/corpus/templates/design/changelog/2026-06-30-motion-timeline-collapse-and-reopen-animations-feel-smoother.md +6 -0
  189. package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +6 -0
  190. package/corpus/templates/design/changelog/2026-06-30-new-board-shapes-now-start-with-neutral-gray-styling.md +6 -0
  191. package/corpus/templates/design/changelog/2026-06-30-new-designs-open-into-a-steadier-loading-state-before-the-ed.md +6 -0
  192. package/corpus/templates/design/changelog/2026-06-30-new-designs-show-a-loading-state-during-handoff-and-keep-edi.md +6 -0
  193. package/corpus/templates/design/changelog/2026-06-30-public-visual-edit-design-links-now-open-directly-instead-of.md +6 -0
  194. package/corpus/templates/design/changelog/2026-06-30-removed-the-confusing-states-section-from-the-design-editor-.md +6 -0
  195. package/corpus/templates/design/changelog/2026-06-30-removed-the-templates-gallery-from-design-navigation.md +6 -0
  196. package/corpus/templates/design/changelog/2026-06-30-share-stays-visible-as-soon-as-editable-designs-load.md +6 -0
  197. package/corpus/templates/design/changelog/2026-06-30-signed-out-save-and-share-controls-use-simpler-labels-and-op.md +6 -0
  198. package/corpus/templates/design/changelog/2026-06-30-text-added-on-the-canvas-now-focuses-cleanly-with-the-right-.md +6 -0
  199. package/corpus/templates/design/changelog/2026-06-30-the-design-editor-loading-skeleton-is-simpler-and-its-bottom.md +6 -0
  200. package/corpus/templates/design/changelog/2026-06-30-the-design-editor-loading-skeleton-now-follows-the-current-t.md +6 -0
  201. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-no-longer-shows-child-layout-controls-o.md +6 -0
  202. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-no-longer-shows-the-ask-ai-section-abov.md +6 -0
  203. package/corpus/templates/design/changelog/2026-06-30-the-design-review-panel-no-longer-shows-a-separate-inline-au.md +6 -0
  204. package/corpus/templates/design/changelog/2026-06-30-the-empty-designs-page-now-focuses-on-a-single-primary-new-d.md +6 -0
  205. package/corpus/templates/design/changelog/2026-06-30-the-layers-panel-header-stays-as-layers-while-multiple-items.md +6 -0
  206. package/corpus/templates/design/changelog/2026-06-30-the-missing-design-screen-now-uses-a-neutral-back-button.md +6 -0
  207. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-now-closes-without-squeezing-the-canvas-.md +6 -0
  208. package/corpus/templates/design/changelog/2026-06-30-the-new-design-connect-ai-setup-now-lines-up-with-the-compos.md +6 -0
  209. package/corpus/templates/design/changelog/2026-06-30-the-screen-overview-board-now-blends-into-the-canvas-and-new.md +6 -0
  210. package/corpus/templates/design/changelog/2026-06-30-top-level-canvas-drops-now-show-screen-guides.md +6 -0
  211. package/corpus/templates/design/changelog/2026-06-30-top-level-canvas-layers-now-keep-a-transparent-backdrop-and-.md +6 -0
  212. package/corpus/templates/design/changelog/2026-06-30-top-level-canvas-layers-now-select-reliably-from-all-screens.md +6 -0
  213. package/corpus/templates/design/changelog/2026-06-30-visual-edit-can-open-and-refresh-localhost-screens-in-one-au.md +6 -0
  214. package/corpus/templates/design/e2e/helpers.ts +15 -4
  215. package/corpus/templates/design/server/db/index.ts +52 -0
  216. package/corpus/templates/design/server/plugins/agent-chat.ts +5 -0
  217. package/corpus/templates/design/server/plugins/auth.ts +1 -0
  218. package/corpus/templates/design/server/plugins/core-routes.ts +4 -0
  219. package/corpus/templates/design/shared/board-file.ts +380 -5
  220. package/corpus/templates/design/shared/board-objects.ts +2 -2
  221. package/corpus/templates/design/shared/code-layer.ts +50 -6
  222. package/corpus/templates/design/shared/motion-compiler.ts +27 -0
  223. package/corpus/templates/macros/app/components/DailyProgress.tsx +41 -37
  224. package/corpus/templates/macros/app/components/ExerciseCard.tsx +2 -2
  225. package/corpus/templates/macros/app/components/MealCard.tsx +2 -2
  226. package/corpus/templates/macros/app/components/VoiceDictation.tsx +1 -1
  227. package/corpus/templates/macros/app/components/WeeklyCaloriesChart.tsx +1 -1
  228. package/corpus/templates/macros/app/components/WeightCard.tsx +2 -2
  229. package/corpus/templates/macros/app/components/WeightTracker.tsx +1 -1
  230. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -1
  231. package/corpus/templates/macros/app/routes/_index.tsx +5 -5
  232. package/corpus/templates/macros/app/routes/analytics.tsx +1 -1
  233. package/corpus/templates/macros/changelog/2026-06-30-dashboard-loading-and-chart-surfaces-now-use-theme-aware-col.md +6 -0
  234. package/corpus/templates/mail/app/components/email/ComposeBubbleToolbar.tsx +14 -11
  235. package/corpus/templates/mail/app/global.css +3 -2
  236. package/corpus/templates/mail/changelog/2026-06-30-compose-floating-toolbars-now-use-theme-aware-colors-in-ligh.md +6 -0
  237. package/corpus/templates/plan/app/pages/PlansPage.tsx +2 -2
  238. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +3 -3
  239. package/corpus/templates/slides/app/global.css +10 -0
  240. package/corpus/templates/slides/app/pages/Index.tsx +55 -51
  241. package/corpus/templates/slides/changelog/2026-06-30-deck-thumbnails-collapse-cleanly-to-one-column-when-the-chat.md +6 -0
  242. package/dist/agent/engine/anthropic-engine.d.ts +2 -2
  243. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  244. package/dist/agent/engine/builder-engine.d.ts +2 -2
  245. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  246. package/dist/agent/engine/builtin.js +1 -1
  247. package/dist/agent/engine/builtin.js.map +1 -1
  248. package/dist/agent/engine/output-tokens.js +1 -1
  249. package/dist/agent/engine/output-tokens.js.map +1 -1
  250. package/dist/agent/harness/acp-adapter.d.ts +4 -1
  251. package/dist/agent/harness/acp-adapter.d.ts.map +1 -1
  252. package/dist/agent/harness/acp-adapter.js +19 -2
  253. package/dist/agent/harness/acp-adapter.js.map +1 -1
  254. package/dist/agent/harness/ai-sdk-adapter.d.ts.map +1 -1
  255. package/dist/agent/harness/ai-sdk-adapter.js +3 -0
  256. package/dist/agent/harness/ai-sdk-adapter.js.map +1 -1
  257. package/dist/agent/harness/translate.d.ts.map +1 -1
  258. package/dist/agent/harness/translate.js +3 -0
  259. package/dist/agent/harness/translate.js.map +1 -1
  260. package/dist/agent/harness/types.d.ts +1 -0
  261. package/dist/agent/harness/types.d.ts.map +1 -1
  262. package/dist/agent/harness/types.js.map +1 -1
  263. package/dist/agent/model-config.d.ts +21 -20
  264. package/dist/agent/model-config.d.ts.map +1 -1
  265. package/dist/agent/model-config.js +31 -13
  266. package/dist/agent/model-config.js.map +1 -1
  267. package/dist/agent/production-agent.d.ts.map +1 -1
  268. package/dist/agent/production-agent.js +85 -10
  269. package/dist/agent/production-agent.js.map +1 -1
  270. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  271. package/dist/agent/thread-data-builder.js +27 -6
  272. package/dist/agent/thread-data-builder.js.map +1 -1
  273. package/dist/agent/tool-call-journal.d.ts +11 -9
  274. package/dist/agent/tool-call-journal.d.ts.map +1 -1
  275. package/dist/agent/tool-call-journal.js +92 -40
  276. package/dist/agent/tool-call-journal.js.map +1 -1
  277. package/dist/agent/types.d.ts +5 -1
  278. package/dist/agent/types.d.ts.map +1 -1
  279. package/dist/agent/types.js.map +1 -1
  280. package/dist/cli/design-connect.d.ts.map +1 -1
  281. package/dist/cli/design-connect.js +84 -0
  282. package/dist/cli/design-connect.js.map +1 -1
  283. package/dist/cli/gateway-helpers.d.ts +4 -0
  284. package/dist/cli/gateway-helpers.d.ts.map +1 -1
  285. package/dist/cli/gateway-helpers.js +9 -0
  286. package/dist/cli/gateway-helpers.js.map +1 -1
  287. package/dist/cli/recap.d.ts.map +1 -1
  288. package/dist/cli/recap.js +0 -2
  289. package/dist/cli/recap.js.map +1 -1
  290. package/dist/cli/skills.d.ts.map +1 -1
  291. package/dist/cli/skills.js +54 -16
  292. package/dist/cli/skills.js.map +1 -1
  293. package/dist/cli/workspace-dev.d.ts.map +1 -1
  294. package/dist/cli/workspace-dev.js +33 -6
  295. package/dist/cli/workspace-dev.js.map +1 -1
  296. package/dist/client/AssistantChat.d.ts.map +1 -1
  297. package/dist/client/AssistantChat.js +72 -26
  298. package/dist/client/AssistantChat.js.map +1 -1
  299. package/dist/client/DefaultSpinner.d.ts.map +1 -1
  300. package/dist/client/DefaultSpinner.js +8 -1
  301. package/dist/client/DefaultSpinner.js.map +1 -1
  302. package/dist/client/chat/message-components.d.ts +7 -1
  303. package/dist/client/chat/message-components.d.ts.map +1 -1
  304. package/dist/client/chat/message-components.js +37 -1
  305. package/dist/client/chat/message-components.js.map +1 -1
  306. package/dist/client/chat/repo-helpers.d.ts +1 -0
  307. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  308. package/dist/client/chat/repo-helpers.js +59 -0
  309. package/dist/client/chat/repo-helpers.js.map +1 -1
  310. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  311. package/dist/client/chat/run-recovery.js +4 -4
  312. package/dist/client/chat/run-recovery.js.map +1 -1
  313. package/dist/client/components/ApiKeySettings.js +8 -8
  314. package/dist/client/components/ApiKeySettings.js.map +1 -1
  315. package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
  316. package/dist/client/components/CodeRequiredDialog.js +24 -15
  317. package/dist/client/components/CodeRequiredDialog.js.map +1 -1
  318. package/dist/client/components/MissingKeyCard.d.ts.map +1 -1
  319. package/dist/client/components/MissingKeyCard.js +11 -6
  320. package/dist/client/components/MissingKeyCard.js.map +1 -1
  321. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  322. package/dist/client/composer/TiptapComposer.js +4 -3
  323. package/dist/client/composer/TiptapComposer.js.map +1 -1
  324. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  325. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  326. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  327. package/dist/client/guided-questions.d.ts.map +1 -1
  328. package/dist/client/guided-questions.js +6 -6
  329. package/dist/client/guided-questions.js.map +1 -1
  330. package/dist/client/integrations/IntegrationCard.js +1 -1
  331. package/dist/client/integrations/IntegrationCard.js.map +1 -1
  332. package/dist/client/integrations/IntegrationsPanel.js +1 -1
  333. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  334. package/dist/client/onboarding/OnboardingBanner.js +2 -2
  335. package/dist/client/onboarding/OnboardingBanner.js.map +1 -1
  336. package/dist/client/onboarding/OnboardingPanel.js +23 -19
  337. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  338. package/dist/client/resources/ResourceEditor.d.ts.map +1 -1
  339. package/dist/client/resources/ResourceEditor.js +13 -10
  340. package/dist/client/resources/ResourceEditor.js.map +1 -1
  341. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  342. package/dist/client/resources/ResourcesPanel.js +2 -1
  343. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  344. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  345. package/dist/client/settings/SettingsPanel.js +8 -3
  346. package/dist/client/settings/SettingsPanel.js.map +1 -1
  347. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  348. package/dist/client/settings/useBuilderStatus.js +6 -38
  349. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  350. package/dist/client/useProductionAgent.d.ts +2 -1
  351. package/dist/client/useProductionAgent.d.ts.map +1 -1
  352. package/dist/client/useProductionAgent.js.map +1 -1
  353. package/dist/deploy/build.js +1 -1
  354. package/dist/deploy/build.js.map +1 -1
  355. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  356. package/dist/notifications/routes.d.ts +1 -1
  357. package/dist/observability/routes.d.ts +5 -5
  358. package/dist/observability/traces.d.ts.map +1 -1
  359. package/dist/observability/traces.js.map +1 -1
  360. package/dist/onboarding/default-steps.js +1 -1
  361. package/dist/onboarding/default-steps.js.map +1 -1
  362. package/dist/scripts/agent-engines/manage-agent-engine.js +1 -1
  363. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  364. package/dist/scripts/agent-engines/set-agent-engine.js +1 -1
  365. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  366. package/dist/server/agent-chat-plugin.d.ts +3 -0
  367. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  368. package/dist/server/agent-chat-plugin.js +134 -72
  369. package/dist/server/agent-chat-plugin.js.map +1 -1
  370. package/dist/server/core-routes-plugin.d.ts +3 -0
  371. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  372. package/dist/server/core-routes-plugin.js +4 -1
  373. package/dist/server/core-routes-plugin.js.map +1 -1
  374. package/dist/server/open-route.d.ts +9 -0
  375. package/dist/server/open-route.d.ts.map +1 -1
  376. package/dist/server/open-route.js +48 -38
  377. package/dist/server/open-route.js.map +1 -1
  378. package/dist/shared/reasoning-effort.js +3 -0
  379. package/dist/shared/reasoning-effort.js.map +1 -1
  380. package/dist/sharing/access.d.ts.map +1 -1
  381. package/dist/sharing/access.js +18 -2
  382. package/dist/sharing/access.js.map +1 -1
  383. package/dist/sharing/registry.d.ts +10 -0
  384. package/dist/sharing/registry.d.ts.map +1 -1
  385. package/dist/sharing/registry.js.map +1 -1
  386. package/dist/styles/agent-native.css +104 -20
  387. package/dist/styles/rich-markdown-editor.css +11 -10
  388. package/docs/content/cloneable-saas.mdx +2 -2
  389. package/docs/content/faq.mdx +1 -1
  390. package/docs/content/locales/ar-SA/cloneable-saas.mdx +2 -2
  391. package/docs/content/locales/ar-SA/faq.mdx +1 -1
  392. package/docs/content/locales/ar-SA/template-chat.mdx +1 -1
  393. package/docs/content/locales/de-DE/cloneable-saas.mdx +2 -2
  394. package/docs/content/locales/de-DE/faq.mdx +1 -1
  395. package/docs/content/locales/de-DE/template-chat.mdx +1 -1
  396. package/docs/content/locales/es-ES/cloneable-saas.mdx +2 -2
  397. package/docs/content/locales/es-ES/faq.mdx +1 -1
  398. package/docs/content/locales/es-ES/template-chat.mdx +1 -1
  399. package/docs/content/locales/fr-FR/cloneable-saas.mdx +2 -2
  400. package/docs/content/locales/fr-FR/faq.mdx +1 -1
  401. package/docs/content/locales/fr-FR/template-chat.mdx +1 -1
  402. package/docs/content/locales/hi-IN/cloneable-saas.mdx +2 -2
  403. package/docs/content/locales/hi-IN/faq.mdx +1 -1
  404. package/docs/content/locales/hi-IN/template-chat.mdx +1 -1
  405. package/docs/content/locales/ja-JP/cloneable-saas.mdx +2 -2
  406. package/docs/content/locales/ja-JP/faq.mdx +1 -1
  407. package/docs/content/locales/ja-JP/template-chat.mdx +1 -1
  408. package/docs/content/locales/ko-KR/cloneable-saas.mdx +2 -2
  409. package/docs/content/locales/ko-KR/faq.mdx +1 -1
  410. package/docs/content/locales/ko-KR/template-chat.mdx +1 -1
  411. package/docs/content/locales/pt-BR/cloneable-saas.mdx +2 -2
  412. package/docs/content/locales/pt-BR/faq.mdx +1 -1
  413. package/docs/content/locales/pt-BR/template-chat.mdx +1 -1
  414. package/docs/content/locales/zh-CN/cloneable-saas.mdx +2 -2
  415. package/docs/content/locales/zh-CN/faq.mdx +1 -1
  416. package/docs/content/locales/zh-CN/template-chat.mdx +1 -1
  417. package/docs/content/locales/zh-TW/cloneable-saas.mdx +2 -2
  418. package/docs/content/locales/zh-TW/faq.mdx +1 -1
  419. package/docs/content/locales/zh-TW/template-chat.mdx +1 -1
  420. package/docs/content/template-chat.mdx +1 -1
  421. package/package.json +1 -1
  422. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +0 -157
  423. package/corpus/templates/design/app/lib/design-templates.ts +0 -1305
  424. package/corpus/templates/design/app/pages/Templates.tsx +0 -279
@@ -17,16 +17,19 @@ import {
17
17
  IconAlignJustified,
18
18
  IconAlignLeft,
19
19
  IconAlignRight,
20
+ IconAngle,
20
21
  IconArrowAutofitHeight,
21
22
  IconArrowAutofitWidth,
22
23
  IconArrowRight,
23
24
  IconBackground,
24
25
  IconBlur,
26
+ IconBorderCorners,
27
+ IconBorderRadius,
25
28
  IconBorderStyle,
26
29
  IconBrush,
30
+ IconCheck,
27
31
  IconCode,
28
32
  IconComponents,
29
- IconDeviceDesktop,
30
33
  IconExternalLink,
31
34
  IconDroplet,
32
35
  IconEye,
@@ -34,11 +37,10 @@ import {
34
37
  IconFlipHorizontal,
35
38
  IconFlipVertical,
36
39
  IconFrame,
40
+ IconGridDots,
37
41
  IconLayoutDistributeHorizontal,
38
42
  IconLayoutGrid,
39
43
  IconLoader2,
40
- IconShieldCheck,
41
- IconSlice,
42
44
  IconLayoutAlignBottom,
43
45
  IconLayoutAlignCenter,
44
46
  IconLayoutAlignLeft,
@@ -49,11 +51,14 @@ import {
49
51
  IconLetterSpacing,
50
52
  IconLineHeight,
51
53
  IconLink,
52
- IconLock,
53
- IconMaximize,
54
54
  IconMinus,
55
55
  IconPhoto,
56
56
  IconPlus,
57
+ IconRadiusBottomLeft,
58
+ IconRadiusBottomRight,
59
+ IconRadiusTopLeft,
60
+ IconRadiusTopRight,
61
+ IconRefresh,
57
62
  IconShadow,
58
63
  IconSquare,
59
64
  IconTypography,
@@ -64,6 +69,7 @@ import { useQueryClient } from "@tanstack/react-query";
64
69
  import {
65
70
  useCallback,
66
71
  useEffect,
72
+ useMemo,
67
73
  useRef,
68
74
  useState,
69
75
  type ReactNode,
@@ -72,14 +78,6 @@ import { useParams } from "react-router";
72
78
  import { toast } from "sonner";
73
79
 
74
80
  import { Button } from "@/components/ui/button";
75
- import {
76
- Dialog,
77
- DialogContent,
78
- DialogDescription,
79
- DialogFooter,
80
- DialogHeader,
81
- DialogTitle,
82
- } from "@/components/ui/dialog";
83
81
  import {
84
82
  DropdownMenu,
85
83
  DropdownMenuContent,
@@ -112,6 +110,7 @@ import { cn } from "@/lib/utils";
112
110
 
113
111
  import {
114
112
  AutoLayoutMatrix,
113
+ ConstraintsPreview,
115
114
  ConstraintsWidget,
116
115
  ExportSettingsPanel,
117
116
  DesignColorPicker,
@@ -133,18 +132,80 @@ import {
133
132
  } from "./inspector";
134
133
  import { IconLayoutSettings } from "./inspector/design-icons";
135
134
  import type { DesignPaintType } from "./inspector/DesignColorPicker";
136
- import { InspectorAiActions } from "./inspector/InspectorAiActions";
137
135
  import { ReviewPanel } from "./ReviewPanel";
138
136
  import type { ReviewPanelProps } from "./ReviewPanel";
139
- import { StatesPanel } from "./StatesPanel";
140
137
  import type { StatesPanelProps } from "./StatesPanel";
141
138
  import { TweaksPanelContent } from "./TweaksPanel";
142
139
  import type { ElementInfo } from "./types";
143
140
 
144
141
  export type InspectorTab = "design" | "tweaks";
145
142
 
143
+ const MIXED_VALUE = "Mixed";
144
+
145
+ function isMixedValue(value: string | undefined): boolean {
146
+ return value === MIXED_VALUE;
147
+ }
148
+
149
+ function sameOrMixed(values: string[]): string {
150
+ if (values.length === 0) return "";
151
+ const first = values[0] ?? "";
152
+ return values.every((value) => value === first) ? first : MIXED_VALUE;
153
+ }
154
+
155
+ function mixedElementFromSelection(
156
+ elements: ElementInfo[],
157
+ ): ElementInfo | null {
158
+ const base = elements[elements.length - 1];
159
+ if (!base) return null;
160
+ const styleKeys = new Set<string>();
161
+ elements.forEach((element) => {
162
+ Object.keys(element.computedStyles).forEach((key) => styleKeys.add(key));
163
+ });
164
+ const computedStyles = Object.fromEntries(
165
+ Array.from(styleKeys).map((key) => [
166
+ key,
167
+ sameOrMixed(elements.map((element) => element.computedStyles[key] ?? "")),
168
+ ]),
169
+ );
170
+ const minX = Math.min(...elements.map((element) => element.boundingRect.x));
171
+ const minY = Math.min(...elements.map((element) => element.boundingRect.y));
172
+ const maxX = Math.max(
173
+ ...elements.map(
174
+ (element) => element.boundingRect.x + element.boundingRect.width,
175
+ ),
176
+ );
177
+ const maxY = Math.max(
178
+ ...elements.map(
179
+ (element) => element.boundingRect.y + element.boundingRect.height,
180
+ ),
181
+ );
182
+ return {
183
+ ...base,
184
+ tagName: sameOrMixed(elements.map((element) => element.tagName)),
185
+ id: undefined,
186
+ sourceId: undefined,
187
+ selector: base.selector,
188
+ classes: [],
189
+ computedStyles,
190
+ boundingRect: {
191
+ x: minX,
192
+ y: minY,
193
+ width: maxX - minX,
194
+ height: maxY - minY,
195
+ },
196
+ textContent: sameOrMixed(
197
+ elements.map((element) => element.textContent ?? ""),
198
+ ),
199
+ htmlContent: undefined,
200
+ childElementCount: undefined,
201
+ isFlexChild: elements.every((element) => element.isFlexChild),
202
+ isFlexContainer: elements.every((element) => element.isFlexContainer),
203
+ };
204
+ }
205
+
146
206
  interface EditPanelProps {
147
207
  selectedElement: ElementInfo | null;
208
+ selectedElements?: ElementInfo[];
148
209
  pageStyles?: Record<string, string>;
149
210
  zoom?: number;
150
211
  headerTrailing?: ReactNode;
@@ -157,17 +218,10 @@ interface EditPanelProps {
157
218
  onRequestTweaks?: (anchor: HTMLElement) => void;
158
219
  onStyleChange: (property: string, value: string) => void;
159
220
  onStylesChange?: (styles: Record<string, string>) => void;
160
- onAutoLayoutConvert?: (
161
- targetNodeId: string,
162
- opts?: { direction?: "row" | "column"; gap?: string },
163
- ) => void;
164
221
  onExport?: (settings: ExportSettingsValue[]) => void;
165
222
  exporting?: boolean;
166
- readOnly?: boolean;
167
- /** Active file id — forwarded to InspectorAiActions for agent context. */
223
+ /** Active file id — used for component prop editing context. */
168
224
  fileId?: string;
169
- /** Active file name (e.g. "index.html") — forwarded to InspectorAiActions. */
170
- filename?: string;
171
225
  /** Latest active file HTML, used to compose rapid sequential source edits. */
172
226
  activeContent?: string;
173
227
  /** Server revision for activeContent. */
@@ -223,6 +277,8 @@ interface EditPanelProps {
223
277
  * omitted the "Create component" button is disabled.
224
278
  */
225
279
  onCreateComponent?: (name: string) => void;
280
+ /** True when the selected element is already represented as a component. */
281
+ selectedElementAlreadyComponent?: boolean;
226
282
  /** Suggested default name for the create-component dialog. */
227
283
  defaultComponentName?: string;
228
284
  /** Code-inspection data for the "Inspect code" popover. */
@@ -236,6 +292,12 @@ interface EditPanelProps {
236
292
  export interface InspectCodeData {
237
293
  /** Outer HTML of the selected element (inline / Alpine source). */
238
294
  html?: string | null;
295
+ /** Selected element tag, used when only runtime selection metadata exists. */
296
+ tagName?: string | null;
297
+ /** Selected element id, used for the runtime-metadata fallback preview. */
298
+ id?: string | null;
299
+ /** Selected element classes, used for the runtime-metadata fallback preview. */
300
+ classes?: string[];
239
301
  /**
240
302
  * Resolved source file for real-app sources (localhost / fusion), when the
241
303
  * resolveNodeToFile capability is available.
@@ -250,6 +312,23 @@ export interface InspectCodeData {
250
312
  } | null;
251
313
  }
252
314
 
315
+ const VOID_HTML_TAGS = new Set([
316
+ "area",
317
+ "base",
318
+ "br",
319
+ "col",
320
+ "embed",
321
+ "hr",
322
+ "img",
323
+ "input",
324
+ "link",
325
+ "meta",
326
+ "param",
327
+ "source",
328
+ "track",
329
+ "wbr",
330
+ ]);
331
+
253
332
  /**
254
333
  * Normalize a CSS length-ish value typed by the user. If the input is bare
255
334
  * digits (e.g. "32" or "32.5"), append the default unit so it parses as a
@@ -301,12 +380,14 @@ function PropInput({
301
380
  defaultUnit?: string;
302
381
  }) {
303
382
  const [draft, setDraft] = useState(value);
383
+ const mixed = isMixedValue(value);
304
384
 
305
385
  useEffect(() => {
306
386
  setDraft(value);
307
387
  }, [value]);
308
388
 
309
389
  const commit = () => {
390
+ if (isMixedValue(draft)) return;
310
391
  if (defaultUnit === undefined) {
311
392
  if (draft !== value) onChange(draft);
312
393
  return;
@@ -327,6 +408,9 @@ function PropInput({
327
408
  <Input
328
409
  type={type}
329
410
  value={draft}
411
+ onFocus={(e) => {
412
+ if (mixed) e.currentTarget.select();
413
+ }}
330
414
  onChange={(e) => {
331
415
  setDraft(e.target.value);
332
416
  // For length fields, defer the live update until blur/Enter so that
@@ -344,7 +428,7 @@ function PropInput({
344
428
  }
345
429
  }}
346
430
  placeholder={placeholder}
347
- className="h-6 min-w-0 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
431
+ className="h-6 min-w-0 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 !text-[11px] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)] md:!text-[11px]"
348
432
  />
349
433
  </div>
350
434
  );
@@ -681,6 +765,18 @@ function ColorInput({
681
765
  setSelectedStopId("stop-0");
682
766
  };
683
767
 
768
+ if (isMixedValue(value)) {
769
+ return (
770
+ <button
771
+ type="button"
772
+ className="flex h-6 w-full items-center rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left !text-[11px] text-muted-foreground"
773
+ onClick={() => onChange("#000000")}
774
+ >
775
+ {MIXED_VALUE}
776
+ </button>
777
+ );
778
+ }
779
+
684
780
  return (
685
781
  <DesignColorPicker
686
782
  key={pickerKey}
@@ -995,7 +1091,7 @@ function PropSelect({
995
1091
  <div className="flex items-center gap-1.5">
996
1092
  <FieldLabel>{label}</FieldLabel>
997
1093
  <Select value={value} onValueChange={onChange}>
998
- <SelectTrigger className="h-6 min-w-0 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)]">
1094
+ <SelectTrigger className="h-6 min-w-0 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)]">
999
1095
  <SelectValue />
1000
1096
  </SelectTrigger>
1001
1097
  <SelectContent>
@@ -1003,7 +1099,7 @@ function PropSelect({
1003
1099
  <SelectItem
1004
1100
  key={opt.value}
1005
1101
  value={opt.value}
1006
- className="text-[11px]"
1102
+ className="!text-[11px]"
1007
1103
  >
1008
1104
  {opt.label}
1009
1105
  </SelectItem>
@@ -1043,7 +1139,7 @@ function PropSlider({
1043
1139
  step={step}
1044
1140
  className="flex-1"
1045
1141
  />
1046
- <span className="w-12 text-right text-[11px] tabular-nums text-muted-foreground">
1142
+ <span className="w-12 text-right !text-[11px] tabular-nums text-muted-foreground">
1047
1143
  {value}
1048
1144
  {unit}
1049
1145
  </span>
@@ -1083,7 +1179,7 @@ function PanelSection({
1083
1179
  onClick={() => setCollapsed((c) => !c)}
1084
1180
  aria-expanded={!collapsed}
1085
1181
  >
1086
- <h3 className="min-w-0 flex-1 truncate text-[11px] font-semibold text-foreground">
1182
+ <h3 className="min-w-0 flex-1 truncate !text-[11px] font-semibold text-foreground">
1087
1183
  {title}
1088
1184
  </h3>
1089
1185
  </button>
@@ -1092,7 +1188,7 @@ function PanelSection({
1092
1188
  ) : null}
1093
1189
  </div>
1094
1190
  {!collapsed && children ? (
1095
- <div className="space-y-1.5 px-3 pb-3 pt-0.5 text-[11px]">
1191
+ <div className="space-y-1.5 px-3 pb-3 pt-0.5 !text-[11px]">
1096
1192
  {children}
1097
1193
  </div>
1098
1194
  ) : null}
@@ -1102,7 +1198,7 @@ function PanelSection({
1102
1198
 
1103
1199
  function FieldLabel({ children }: { children: ReactNode }) {
1104
1200
  return (
1105
- <Label className="w-[64px] shrink-0 text-[11px] font-medium text-muted-foreground">
1201
+ <Label className="w-[64px] shrink-0 !text-[11px] font-medium text-muted-foreground">
1106
1202
  {children}
1107
1203
  </Label>
1108
1204
  );
@@ -1110,7 +1206,7 @@ function FieldLabel({ children }: { children: ReactNode }) {
1110
1206
 
1111
1207
  function SubsectionLabel({ children }: { children: ReactNode }) {
1112
1208
  return (
1113
- <p className="text-[11px] font-medium text-muted-foreground">{children}</p>
1209
+ <p className="!text-[11px] font-medium text-muted-foreground">{children}</p>
1114
1210
  );
1115
1211
  }
1116
1212
 
@@ -1152,7 +1248,7 @@ function DesignSpacingControl({
1152
1248
  return (
1153
1249
  <div className="space-y-1.5">
1154
1250
  <div className="flex items-center justify-between gap-1.5">
1155
- <Label className="text-[11px] font-medium text-muted-foreground">
1251
+ <Label className="!text-[11px] font-medium text-muted-foreground">
1156
1252
  {label}
1157
1253
  </Label>
1158
1254
  <Tooltip>
@@ -1515,6 +1611,7 @@ function ScrubStyleInput({
1515
1611
  ariaLabel,
1516
1612
  tooltipLabel,
1517
1613
  hideIcon = true,
1614
+ icon,
1518
1615
  }: {
1519
1616
  label: string;
1520
1617
  value: string;
@@ -1529,15 +1626,18 @@ function ScrubStyleInput({
1529
1626
  hideIcon?: boolean;
1530
1627
  ariaLabel?: string;
1531
1628
  tooltipLabel?: string;
1629
+ icon?: (props: { className?: string }) => ReactNode;
1532
1630
  }) {
1631
+ const mixed = isMixedValue(value);
1533
1632
  return (
1534
1633
  <ScrubInput
1535
1634
  label={label}
1536
1635
  ariaLabel={ariaLabel}
1537
1636
  tooltipLabel={tooltipLabel}
1538
- icon={hideIcon ? null : undefined}
1539
- value={value ? parseNumericValue(value) : (placeholder ?? 0)}
1637
+ icon={hideIcon ? null : icon}
1638
+ value={mixed ? 0 : value ? parseNumericValue(value) : (placeholder ?? 0)}
1540
1639
  onChange={onChange}
1640
+ mixed={mixed}
1541
1641
  unit={unit}
1542
1642
  min={min}
1543
1643
  max={max}
@@ -1545,7 +1645,7 @@ function ScrubStyleInput({
1545
1645
  precision={1}
1546
1646
  className="gap-0"
1547
1647
  labelClassName={cn(
1548
- "h-6 w-7 justify-center gap-0 rounded-l-md border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-[11px] tabular-nums",
1648
+ "h-6 w-7 justify-center gap-0 rounded-l-md rounded-r-none border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] !text-[11px] tabular-nums",
1549
1649
  labelClassName,
1550
1650
  )}
1551
1651
  inputClassName={cn(
@@ -1620,17 +1720,28 @@ function colorHasVisibleAlpha(value: string | undefined): boolean {
1620
1720
  return parsed.a > 0;
1621
1721
  }
1622
1722
 
1723
+ function normalizedElementTagName(tagName: string | null | undefined): string {
1724
+ return tagName?.trim().toLowerCase() || "element";
1725
+ }
1726
+
1623
1727
  function inspectorObjectTitle(element: ElementInfo): string {
1624
- const tag = element.tagName || "element";
1728
+ const componentName = componentNameForElementInfo(element);
1729
+ if (componentName) return componentName;
1730
+ const tag = normalizedElementTagName(element.tagName);
1625
1731
  if (TEXT_TAGS.has(tag)) return "Text";
1626
- if (tag === "section") return "Section";
1627
- if (tag === "img" || tag === "video" || tag === "picture") return "Image";
1628
- if (tag === "button" || tag === "a") return "Button";
1629
- if (tag === "svg" || tag === "path") return "Vector";
1630
- if (element.isFlexContainer || tag === "div" || tag === "main") {
1631
- return "Frame";
1632
- }
1633
- return tag.charAt(0).toUpperCase() + tag.slice(1);
1732
+ return tag;
1733
+ }
1734
+
1735
+ function componentNameForElementInfo(
1736
+ element: ElementInfo | null | undefined,
1737
+ ): string {
1738
+ return element?.componentName?.trim() ?? "";
1739
+ }
1740
+
1741
+ function elementIsComponentSelection(
1742
+ element: ElementInfo | null | undefined,
1743
+ ): boolean {
1744
+ return componentNameForElementInfo(element).length > 0;
1634
1745
  }
1635
1746
 
1636
1747
  function displayLabel(value: string | undefined): string {
@@ -1741,7 +1852,7 @@ const LEAF_TAGS = new Set([
1741
1852
  * children show the full Auto layout section the same way does.
1742
1853
  */
1743
1854
  function isContainerElement(element: ElementInfo): boolean {
1744
- if (element.isFlexContainer) return true;
1855
+ if (element.isFlexContainer || element.isGridContainer) return true;
1745
1856
  const tag = (element.tagName || "").toLowerCase();
1746
1857
  if (TEXT_TAGS.has(tag) || LEAF_TAGS.has(tag)) return false;
1747
1858
  return CONTAINER_TAGS.has(tag);
@@ -1758,6 +1869,13 @@ function isParentGrid(element: ElementInfo): boolean {
1758
1869
  return Boolean(element.parentDisplay?.toLowerCase().includes("grid"));
1759
1870
  }
1760
1871
 
1872
+ function elementHasLayoutChildren(element: ElementInfo): boolean {
1873
+ if (typeof element.childElementCount === "number") {
1874
+ return element.childElementCount > 0;
1875
+ }
1876
+ return Boolean(element.htmlContent?.match(/<\s*[a-zA-Z][^>]*>/));
1877
+ }
1878
+
1761
1879
  function parentFlexDirection(element: ElementInfo): AutoLayoutSizingAxis {
1762
1880
  return element.parentLayout?.flexDirection?.includes("column")
1763
1881
  ? "vertical"
@@ -1977,10 +2095,10 @@ function commitElementSizing(
1977
2095
  }
1978
2096
 
1979
2097
  /**
1980
- * Small modal that prompts for a component name, then promotes the current
1981
- * selection into a reusable component via `onSubmit`.
2098
+ * Header-anchored popover that prompts for a component name, then promotes the
2099
+ * current selection into a reusable component via `onSubmit`.
1982
2100
  */
1983
- function CreateComponentDialog({
2101
+ function CreateComponentPopover({
1984
2102
  open,
1985
2103
  onOpenChange,
1986
2104
  defaultName,
@@ -1992,8 +2110,9 @@ function CreateComponentDialog({
1992
2110
  onSubmit: (name: string) => void;
1993
2111
  }) {
1994
2112
  const [name, setName] = useState(defaultName);
2113
+ const inputRef = useRef<HTMLInputElement>(null);
1995
2114
 
1996
- // Reset the field to the freshest default each time the dialog opens.
2115
+ // Reset the field to the freshest default each time the popover opens.
1997
2116
  useEffect(() => {
1998
2117
  if (open) setName(defaultName);
1999
2118
  }, [open, defaultName]);
@@ -2006,55 +2125,86 @@ function CreateComponentDialog({
2006
2125
  };
2007
2126
 
2008
2127
  return (
2009
- <Dialog open={open} onOpenChange={onOpenChange}>
2010
- <DialogContent className="sm:max-w-sm">
2011
- <DialogHeader>
2012
- <DialogTitle>
2013
- {"Create component" /* i18n-ignore design inspector action */}
2014
- </DialogTitle>
2015
- <DialogDescription>
2016
- {
2017
- "Name this element so it becomes a reusable component. The agent can then extract props and replace repeated instances." /* i18n-ignore design inspector copy */
2018
- }
2019
- </DialogDescription>
2020
- </DialogHeader>
2021
- <div className="space-y-1.5">
2022
- <Label
2023
- htmlFor="create-component-name"
2024
- className="text-[11px] font-medium text-muted-foreground"
2025
- >
2026
- {"Component name" /* i18n-ignore design inspector label */}
2027
- </Label>
2028
- <Input
2029
- id="create-component-name"
2030
- autoFocus
2031
- value={name}
2032
- onChange={(e) => setName(e.target.value)}
2033
- onKeyDown={(e) => {
2034
- if (e.key === "Enter") {
2035
- e.preventDefault();
2036
- commit();
2128
+ <Popover open={open} onOpenChange={onOpenChange}>
2129
+ <Tooltip>
2130
+ <TooltipTrigger asChild>
2131
+ <PopoverTrigger asChild>
2132
+ <Button
2133
+ type="button"
2134
+ variant="ghost"
2135
+ size="icon"
2136
+ className="size-6 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:text-foreground"
2137
+ aria-label={
2138
+ "Create component" /* i18n-ignore design inspector action */
2037
2139
  }
2038
- }}
2039
- placeholder={
2040
- "PrimaryButton" /* i18n-ignore design inspector placeholder */
2041
- }
2042
- />
2043
- </div>
2044
- <DialogFooter>
2045
- <Button
2046
- type="button"
2047
- variant="ghost"
2048
- onClick={() => onOpenChange(false)}
2049
- >
2050
- {"Cancel" /* i18n-ignore design inspector action */}
2051
- </Button>
2052
- <Button type="button" onClick={commit} disabled={!name.trim()}>
2053
- {"Create" /* i18n-ignore design inspector action */}
2054
- </Button>
2055
- </DialogFooter>
2056
- </DialogContent>
2057
- </Dialog>
2140
+ >
2141
+ <IconComponents className="size-3.5" />
2142
+ </Button>
2143
+ </PopoverTrigger>
2144
+ </TooltipTrigger>
2145
+ <TooltipContent>
2146
+ {"Create component" /* i18n-ignore design inspector action */}
2147
+ </TooltipContent>
2148
+ </Tooltip>
2149
+ <PopoverContent
2150
+ align="end"
2151
+ sideOffset={8}
2152
+ className="w-80 p-3 text-[12px]"
2153
+ onOpenAutoFocus={(event) => {
2154
+ event.preventDefault();
2155
+ window.requestAnimationFrame(() => inputRef.current?.select());
2156
+ }}
2157
+ >
2158
+ <form
2159
+ className="space-y-3"
2160
+ onSubmit={(event) => {
2161
+ event.preventDefault();
2162
+ commit();
2163
+ }}
2164
+ >
2165
+ <div className="space-y-1">
2166
+ <h3 className="text-[13px] font-semibold text-foreground">
2167
+ {"Create component" /* i18n-ignore design inspector action */}
2168
+ </h3>
2169
+ <p className="!text-[11px] leading-4 text-muted-foreground">
2170
+ {
2171
+ "Name this element so it becomes a reusable component. The agent can then extract props and replace repeated instances." /* i18n-ignore design inspector copy */
2172
+ }
2173
+ </p>
2174
+ </div>
2175
+ <div className="space-y-1.5">
2176
+ <Label
2177
+ htmlFor="create-component-name"
2178
+ className="!text-[11px] font-medium text-muted-foreground"
2179
+ >
2180
+ {"Component name" /* i18n-ignore design inspector label */}
2181
+ </Label>
2182
+ <Input
2183
+ ref={inputRef}
2184
+ id="create-component-name"
2185
+ value={name}
2186
+ onChange={(e) => setName(e.target.value)}
2187
+ placeholder={
2188
+ "PrimaryButton" /* i18n-ignore design inspector placeholder */
2189
+ }
2190
+ />
2191
+ </div>
2192
+ <div className="flex justify-end gap-2">
2193
+ <Button
2194
+ type="button"
2195
+ variant="ghost"
2196
+ size="sm"
2197
+ onClick={() => onOpenChange(false)}
2198
+ >
2199
+ {"Cancel" /* i18n-ignore design inspector action */}
2200
+ </Button>
2201
+ <Button type="submit" size="sm" disabled={!name.trim()}>
2202
+ {"Create" /* i18n-ignore design inspector action */}
2203
+ </Button>
2204
+ </div>
2205
+ </form>
2206
+ </PopoverContent>
2207
+ </Popover>
2058
2208
  );
2059
2209
  }
2060
2210
 
@@ -2105,6 +2255,141 @@ export function truncateOpeningTag(openTag: string, max = 32): string {
2105
2255
  );
2106
2256
  }
2107
2257
 
2258
+ function tagNameFromOpeningTag(openTag: string): string | null {
2259
+ const match = /^<\/?\s*([a-zA-Z][\w:-]*)/.exec(openTag.trim());
2260
+ return match?.[1]?.toLowerCase() ?? null;
2261
+ }
2262
+
2263
+ function isSelfClosingOpeningTag(openTag: string, tagName: string): boolean {
2264
+ return /\/>\s*$/.test(openTag) || VOID_HTML_TAGS.has(tagName);
2265
+ }
2266
+
2267
+ function escapeHtmlAttribute(value: string): string {
2268
+ return value
2269
+ .replace(/&/g, "&amp;")
2270
+ .replace(/"/g, "&quot;")
2271
+ .replace(/</g, "&lt;")
2272
+ .replace(/>/g, "&gt;");
2273
+ }
2274
+
2275
+ function fallbackOpeningTag(
2276
+ data: Pick<InspectCodeData, "tagName" | "id" | "classes">,
2277
+ ) {
2278
+ const tag = normalizedElementTagName(data.tagName);
2279
+ const attrs: string[] = [];
2280
+ const id = data.id?.trim();
2281
+ const classes = data.classes?.map((item) => item.trim()).filter(Boolean);
2282
+ if (id) attrs.push(`id="${escapeHtmlAttribute(id)}"`);
2283
+ if (classes?.length) {
2284
+ attrs.push(`class="${escapeHtmlAttribute(classes.join(" "))}"`);
2285
+ }
2286
+ return `<${tag}${attrs.length ? ` ${attrs.join(" ")}` : ""}>`;
2287
+ }
2288
+
2289
+ export function elementHtmlPreview(
2290
+ data: Pick<InspectCodeData, "html" | "tagName" | "id" | "classes">,
2291
+ ): string | null {
2292
+ const openingTag = openingTagOf(data.html);
2293
+ const hasFallbackMetadata = Boolean(
2294
+ data.tagName?.trim() ||
2295
+ data.id?.trim() ||
2296
+ data.classes?.some((item) => item.trim()),
2297
+ );
2298
+ if (!openingTag && !hasFallbackMetadata) return null;
2299
+ const previewOpeningTag = openingTag ?? fallbackOpeningTag(data);
2300
+ const tagName =
2301
+ tagNameFromOpeningTag(previewOpeningTag) ??
2302
+ normalizedElementTagName(data.tagName);
2303
+ if (isSelfClosingOpeningTag(previewOpeningTag, tagName)) {
2304
+ return previewOpeningTag;
2305
+ }
2306
+ return `${previewOpeningTag}\n ...\n</${tagName}>`;
2307
+ }
2308
+
2309
+ type HtmlTokenKind = "plain" | "punctuation" | "tag" | "attribute" | "value";
2310
+
2311
+ interface HtmlToken {
2312
+ text: string;
2313
+ kind: HtmlTokenKind;
2314
+ }
2315
+
2316
+ function tokenizeHtmlAttributes(source: string): HtmlToken[] {
2317
+ const tokens: HtmlToken[] = [];
2318
+ const attrPattern =
2319
+ /(\s+)([^\s=/>]+)(?:\s*(=)\s*("[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
2320
+ let cursor = 0;
2321
+ for (const match of source.matchAll(attrPattern)) {
2322
+ const index = match.index ?? 0;
2323
+ if (index > cursor) {
2324
+ tokens.push({ text: source.slice(cursor, index), kind: "plain" });
2325
+ }
2326
+ tokens.push({ text: match[1] ?? "", kind: "plain" });
2327
+ tokens.push({ text: match[2] ?? "", kind: "attribute" });
2328
+ if (match[3]) tokens.push({ text: match[3], kind: "punctuation" });
2329
+ if (match[4]) tokens.push({ text: match[4], kind: "value" });
2330
+ cursor = index + match[0].length;
2331
+ }
2332
+ if (cursor < source.length) {
2333
+ tokens.push({ text: source.slice(cursor), kind: "plain" });
2334
+ }
2335
+ return tokens;
2336
+ }
2337
+
2338
+ function tokenizeHtmlTag(source: string): HtmlToken[] {
2339
+ const match = /^(<\/?)([a-zA-Z][\w:-]*)([\s\S]*?)(\/?>)$/.exec(source);
2340
+ if (!match) return [{ text: source, kind: "plain" }];
2341
+ return [
2342
+ { text: match[1] ?? "", kind: "punctuation" },
2343
+ { text: match[2] ?? "", kind: "tag" },
2344
+ ...tokenizeHtmlAttributes(match[3] ?? ""),
2345
+ { text: match[4] ?? "", kind: "punctuation" },
2346
+ ];
2347
+ }
2348
+
2349
+ function tokenizeHtml(source: string): HtmlToken[] {
2350
+ const tokens: HtmlToken[] = [];
2351
+ const tagPattern = /<\/?[a-zA-Z][\w:-]*(?:"[^"]*"|'[^']*'|[^'">])*>/g;
2352
+ let cursor = 0;
2353
+ for (const match of source.matchAll(tagPattern)) {
2354
+ const index = match.index ?? 0;
2355
+ if (index > cursor) {
2356
+ tokens.push({ text: source.slice(cursor, index), kind: "plain" });
2357
+ }
2358
+ tokens.push(...tokenizeHtmlTag(match[0]));
2359
+ cursor = index + match[0].length;
2360
+ }
2361
+ if (cursor < source.length) {
2362
+ tokens.push({ text: source.slice(cursor), kind: "plain" });
2363
+ }
2364
+ return tokens;
2365
+ }
2366
+
2367
+ function htmlTokenClassName(kind: HtmlTokenKind): string {
2368
+ switch (kind) {
2369
+ case "punctuation":
2370
+ return "text-muted-foreground/70";
2371
+ case "tag":
2372
+ return "text-[var(--design-editor-accent-color)]";
2373
+ case "attribute":
2374
+ return "text-foreground/90";
2375
+ case "value":
2376
+ return "text-[var(--design-editor-measure-color)]";
2377
+ default:
2378
+ return "text-muted-foreground";
2379
+ }
2380
+ }
2381
+
2382
+ function highlightedHtml(source: string): ReactNode {
2383
+ return tokenizeHtml(source).map((token, index) => (
2384
+ <span
2385
+ key={`${index}:${token.kind}`}
2386
+ className={htmlTokenClassName(token.kind)}
2387
+ >
2388
+ {token.text}
2389
+ </span>
2390
+ ));
2391
+ }
2392
+
2108
2393
  /**
2109
2394
  * Parse the top-level `key: value` pairs from an Alpine `x-data` object literal
2110
2395
  * (e.g. `{ variant: 'outline', size: 'lg', disabled: false }`).
@@ -2467,22 +2752,12 @@ export function isBooleanPropValue(value: string): boolean {
2467
2752
  * TODO: replace the read-only <pre> with an inline Monaco editor for richer
2468
2753
  * (editable) code inspection once the editor bundle is wired into the inspector.
2469
2754
  */
2470
- function InspectCodePopover({
2471
- trigger,
2472
- data,
2473
- }: {
2474
- trigger: ReactNode;
2475
- data: InspectCodeData;
2476
- }) {
2755
+ function InspectCodePopover({ data }: { data: InspectCodeData }) {
2477
2756
  const [copied, setCopied] = useState(false);
2478
2757
  const html = data.html ?? "";
2479
2758
  const source = data.sourceLocation ?? null;
2480
- const snippet = source?.snippet || html;
2481
- // At-a-glance opening tag (tag name + attributes), truncated for readability.
2482
- const openingTag = (() => {
2483
- const raw = openingTagOf(html);
2484
- return raw ? truncateOpeningTag(raw) : null;
2485
- })();
2759
+ const snippet =
2760
+ elementHtmlPreview(data) ?? source?.snippet ?? (html.trim() || null);
2486
2761
 
2487
2762
  const handleCopy = () => {
2488
2763
  if (!snippet) return;
@@ -2499,8 +2774,27 @@ function InspectCodePopover({
2499
2774
 
2500
2775
  return (
2501
2776
  <Popover>
2502
- <PopoverTrigger asChild>{trigger}</PopoverTrigger>
2503
- <PopoverContent align="end" className="w-80 space-y-2 p-2 text-[11px]">
2777
+ <Tooltip>
2778
+ <TooltipTrigger asChild>
2779
+ <PopoverTrigger asChild>
2780
+ <Button
2781
+ type="button"
2782
+ variant="ghost"
2783
+ size="icon"
2784
+ className="size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground"
2785
+ aria-label={
2786
+ "Inspect code" /* i18n-ignore design inspector action */
2787
+ }
2788
+ >
2789
+ <IconCode className="size-3.5" />
2790
+ </Button>
2791
+ </PopoverTrigger>
2792
+ </TooltipTrigger>
2793
+ <TooltipContent>
2794
+ {"Inspect code" /* i18n-ignore design inspector action */}
2795
+ </TooltipContent>
2796
+ </Tooltip>
2797
+ <PopoverContent align="end" className="w-80 space-y-2 p-2 !text-[11px]">
2504
2798
  <div className="flex items-center justify-between gap-2">
2505
2799
  <span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">
2506
2800
  {"Inspect code" /* i18n-ignore design inspector label */}
@@ -2521,16 +2815,6 @@ function InspectCodePopover({
2521
2815
  </Button>
2522
2816
  </div>
2523
2817
 
2524
- {/* At-a-glance opening tag (tag name + attributes) above the full HTML. */}
2525
- {openingTag && (
2526
- <code
2527
- className="block truncate rounded bg-[var(--design-editor-control-bg)] px-2 py-1 font-mono text-[10px] text-foreground"
2528
- title={openingTag}
2529
- >
2530
- {openingTag}
2531
- </code>
2532
- )}
2533
-
2534
2818
  {source && (
2535
2819
  <div
2536
2820
  className="flex items-center gap-1 rounded bg-[var(--design-editor-control-bg)] px-2 py-1"
@@ -2546,7 +2830,7 @@ function InspectCodePopover({
2546
2830
 
2547
2831
  {snippet ? (
2548
2832
  <pre className="max-h-64 overflow-auto rounded bg-[var(--design-editor-control-bg)] p-2 font-mono text-[10px] leading-relaxed text-foreground">
2549
- <code>{snippet}</code>
2833
+ <code>{highlightedHtml(snippet)}</code>
2550
2834
  </pre>
2551
2835
  ) : (
2552
2836
  <p className="px-1 py-2 text-muted-foreground">
@@ -2569,7 +2853,7 @@ function InspectCodePopover({
2569
2853
  type="button"
2570
2854
  variant="outline"
2571
2855
  size="sm"
2572
- className="h-7 w-full gap-1.5 text-[11px]"
2856
+ className="h-7 w-full gap-1.5 !text-[11px]"
2573
2857
  >
2574
2858
  <IconExternalLink className="size-3.5" />
2575
2859
  {"Open in VS Code" /* i18n-ignore design inspector action */}
@@ -2582,7 +2866,8 @@ function InspectCodePopover({
2582
2866
  }
2583
2867
 
2584
2868
  function elementTypeIcon(element: ElementInfo) {
2585
- const tag = element.tagName || "element";
2869
+ if (elementIsComponentSelection(element)) return IconComponents;
2870
+ const tag = normalizedElementTagName(element.tagName);
2586
2871
  if (TEXT_TAGS.has(tag)) return IconTypography;
2587
2872
  if (tag === "img" || tag === "video" || tag === "picture") return IconPhoto;
2588
2873
  if (tag === "svg" || tag === "path") return IconVector;
@@ -2592,53 +2877,80 @@ function elementTypeIcon(element: ElementInfo) {
2592
2877
 
2593
2878
  function SelectionHeader({
2594
2879
  element,
2880
+ selectedCount = 0,
2595
2881
  onCreateComponent,
2596
- inspectCodePopover,
2882
+ createComponentOpen = false,
2883
+ onCreateComponentOpenChange,
2884
+ showCreateComponentAction = true,
2885
+ defaultComponentName = "Component",
2886
+ inspectCode,
2597
2887
  }: {
2598
2888
  element: ElementInfo | null;
2889
+ selectedCount?: number;
2599
2890
  /** Promote the current selection into a reusable component. Omit/undefined to disable. */
2600
- onCreateComponent?: () => void;
2601
- /**
2602
- * Render prop for the "Inspect code" affordance. Receives the trigger button
2603
- * so the parent can anchor a Popover to it; returns the full popover element.
2604
- * When omitted the button renders disabled.
2605
- */
2606
- inspectCodePopover?: (trigger: ReactNode) => ReactNode;
2891
+ onCreateComponent?: (name: string) => void;
2892
+ createComponentOpen?: boolean;
2893
+ onCreateComponentOpenChange?: (open: boolean) => void;
2894
+ showCreateComponentAction?: boolean;
2895
+ defaultComponentName?: string;
2896
+ /** Data for the "Inspect code" popover. When omitted the button renders disabled. */
2897
+ inspectCode?: InspectCodeData;
2607
2898
  }) {
2608
2899
  if (!element) return null;
2609
2900
 
2610
- const title = inspectorObjectTitle(element);
2901
+ const title =
2902
+ selectedCount > 1
2903
+ ? `${selectedCount} selected`
2904
+ : inspectorObjectTitle(element);
2611
2905
  const TypeIcon = elementTypeIcon(element);
2612
-
2613
- const inspectTrigger = (
2614
- <SectionIconButton
2615
- label={"Inspect code" /* i18n-ignore design inspector action */}
2616
- disabled={!inspectCodePopover}
2617
- >
2618
- <IconCode className="size-3.5" />
2619
- </SectionIconButton>
2620
- );
2906
+ const isComponentSelection = elementIsComponentSelection(element);
2621
2907
 
2622
2908
  return (
2623
2909
  <div className="flex min-h-8 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
2624
2910
  {/* Node-type label. Rename lives in the layers panel and device sizing
2625
2911
  lives elsewhere, so this is a plain non-interactive label. */}
2626
2912
  <div className="flex min-w-0 items-center gap-1.5 text-left text-[13px] font-semibold text-foreground">
2627
- <TypeIcon className="size-3.5 shrink-0 text-muted-foreground" />
2913
+ <TypeIcon
2914
+ className={cn(
2915
+ "size-3.5 shrink-0",
2916
+ isComponentSelection
2917
+ ? "text-[var(--design-editor-component-color)]"
2918
+ : "text-muted-foreground",
2919
+ )}
2920
+ />
2628
2921
  <span className="truncate">{title}</span>
2629
2922
  </div>
2630
2923
  {/* Right-aligned quick actions: create-component + dev inspect (</>) */}
2631
2924
  <div className="flex shrink-0 items-center gap-0.5">
2632
- <SectionIconButton
2633
- label={"Create component" /* i18n-ignore design inspector action */}
2634
- disabled={!onCreateComponent}
2635
- onClick={onCreateComponent}
2636
- >
2637
- <IconComponents className="size-3.5" />
2638
- </SectionIconButton>
2639
- {inspectCodePopover
2640
- ? inspectCodePopover(inspectTrigger)
2641
- : inspectTrigger}
2925
+ {showCreateComponentAction ? (
2926
+ onCreateComponent && onCreateComponentOpenChange ? (
2927
+ <CreateComponentPopover
2928
+ open={createComponentOpen}
2929
+ onOpenChange={onCreateComponentOpenChange}
2930
+ defaultName={defaultComponentName}
2931
+ onSubmit={onCreateComponent}
2932
+ />
2933
+ ) : (
2934
+ <SectionIconButton
2935
+ label={
2936
+ "Create component" /* i18n-ignore design inspector action */
2937
+ }
2938
+ disabled
2939
+ >
2940
+ <IconComponents className="size-3.5" />
2941
+ </SectionIconButton>
2942
+ )
2943
+ ) : null}
2944
+ {inspectCode ? (
2945
+ <InspectCodePopover data={inspectCode} />
2946
+ ) : (
2947
+ <SectionIconButton
2948
+ label={"Inspect code" /* i18n-ignore design inspector action */}
2949
+ disabled
2950
+ >
2951
+ <IconCode className="size-3.5" />
2952
+ </SectionIconButton>
2953
+ )}
2642
2954
  </div>
2643
2955
  </div>
2644
2956
  );
@@ -2664,13 +2976,13 @@ function InspectorTabsHeader({
2664
2976
  <TabsList className="h-7 justify-start gap-0.5 rounded-none bg-transparent p-0">
2665
2977
  <TabsTrigger
2666
2978
  value="design"
2667
- className="h-6 rounded-md px-1.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
2979
+ className="h-6 rounded-md px-1.5 !text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
2668
2980
  >
2669
2981
  {"Design" /* i18n-ignore design inspector tab */}
2670
2982
  </TabsTrigger>
2671
2983
  <TabsTrigger
2672
2984
  value="tweaks"
2673
- className="h-6 rounded-md px-1.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
2985
+ className="h-6 rounded-md px-1.5 !text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
2674
2986
  >
2675
2987
  {t("designEditor.tweaks")}
2676
2988
  </TabsTrigger>
@@ -2706,7 +3018,7 @@ function SectionIconButton({
2706
3018
  variant="ghost"
2707
3019
  size="icon"
2708
3020
  className={cn(
2709
- "size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground disabled:cursor-not-allowed",
3021
+ "size-6 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:text-foreground disabled:cursor-not-allowed",
2710
3022
  className,
2711
3023
  )}
2712
3024
  disabled={disabled}
@@ -2888,23 +3200,23 @@ function TypographyDetailsPopover({
2888
3200
  >
2889
3201
  <div className="flex items-center gap-1 border-b border-[var(--design-editor-control-border)] p-2.5">
2890
3202
  <div className="flex rounded-md bg-[var(--design-editor-control-bg)] p-0.5">
2891
- <span className="rounded bg-[var(--design-editor-panel-raised-bg)] px-2.5 py-1 text-[11px] font-semibold text-foreground">
3203
+ <span className="rounded bg-[var(--design-editor-panel-raised-bg)] px-2.5 py-1 !text-[11px] font-semibold text-foreground">
2892
3204
  {"Basics" /* i18n-ignore design typography details tab */}
2893
3205
  </span>
2894
- <span className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
3206
+ <span className="px-2.5 py-1 !text-[11px] font-medium text-muted-foreground">
2895
3207
  {"Details" /* i18n-ignore design typography details tab */}
2896
3208
  </span>
2897
- <span className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
3209
+ <span className="px-2.5 py-1 !text-[11px] font-medium text-muted-foreground">
2898
3210
  {"Variable" /* i18n-ignore design typography details tab */}
2899
3211
  </span>
2900
3212
  </div>
2901
3213
  </div>
2902
- <div className="space-y-3 p-4 text-[11px]">
3214
+ <div className="space-y-3 p-4 !text-[11px]">
2903
3215
  <div className="flex h-20 items-center justify-center rounded-md bg-[var(--design-editor-control-bg)] text-[18px] text-muted-foreground/80">
2904
3216
  {"Preview" /* i18n-ignore design typography details preview */}
2905
3217
  </div>
2906
3218
  <div className="flex items-center justify-between gap-3">
2907
- <span className="text-[11px] font-medium text-muted-foreground">
3219
+ <span className="!text-[11px] font-medium text-muted-foreground">
2908
3220
  {"Text box" /* i18n-ignore design typography details label */}
2909
3221
  </span>
2910
3222
  <TextResizeControls
@@ -2927,7 +3239,6 @@ function CornerRadiusControl({
2927
3239
  }) {
2928
3240
  const t = useT();
2929
3241
  const independentCornersLabel = t("editPanel.labels.independentCorners");
2930
- const radius = cssLengthNumber(styles.borderRadius || "0");
2931
3242
  const corners = {
2932
3243
  topLeft: cssLengthNumber(styles.borderTopLeftRadius || styles.borderRadius),
2933
3244
  topRight: cssLengthNumber(
@@ -2940,110 +3251,238 @@ function CornerRadiusControl({
2940
3251
  styles.borderBottomLeftRadius || styles.borderRadius,
2941
3252
  ),
2942
3253
  };
3254
+ const cornersDiffer =
3255
+ corners.topLeft !== corners.topRight ||
3256
+ corners.topLeft !== corners.bottomRight ||
3257
+ corners.topLeft !== corners.bottomLeft;
3258
+ const [showIndependentCorners, setShowIndependentCorners] =
3259
+ useState(cornersDiffer);
3260
+ const radius = cornersDiffer
3261
+ ? corners.topLeft
3262
+ : cssLengthNumber(styles.borderRadius || String(corners.topLeft));
3263
+ const commitRadius = (value: number) => {
3264
+ const next = `${Math.max(0, Math.round(value))}px`;
3265
+ onStyleChange("borderRadius", next);
3266
+ if (!showIndependentCorners) return;
3267
+ onStyleChange("borderTopLeftRadius", next);
3268
+ onStyleChange("borderTopRightRadius", next);
3269
+ onStyleChange("borderBottomRightRadius", next);
3270
+ onStyleChange("borderBottomLeftRadius", next);
3271
+ };
3272
+
3273
+ useEffect(() => {
3274
+ if (cornersDiffer) setShowIndependentCorners(true);
3275
+ }, [cornersDiffer]);
2943
3276
 
2944
3277
  return (
2945
- <div className="grid grid-cols-[1fr_auto] items-center gap-1.5">
2946
- <ScrubInput
3278
+ <>
3279
+ <AppearanceScrubField
2947
3280
  label={t("editPanel.labels.cornerRadius")}
3281
+ icon={IconBorderRadius}
2948
3282
  value={radius}
2949
- onChange={(value) =>
2950
- onStyleChange("borderRadius", `${Math.max(0, Math.round(value))}px`)
2951
- }
2952
- unit="px"
3283
+ onChange={commitRadius}
2953
3284
  min={0}
2954
3285
  precision={1}
2955
- labelClassName="w-24"
2956
- inputClassName="h-6"
2957
3286
  />
2958
- <Popover>
2959
- <Tooltip>
2960
- <TooltipTrigger asChild>
2961
- <PopoverTrigger asChild>
2962
- <Button
2963
- type="button"
2964
- variant="outline"
2965
- size="icon"
2966
- className="size-6 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)]"
2967
- aria-label={independentCornersLabel}
2968
- >
2969
- <IconMaximize className="size-3.5" />
2970
- </Button>
2971
- </PopoverTrigger>
2972
- </TooltipTrigger>
2973
- <TooltipContent>{independentCornersLabel}</TooltipContent>
2974
- </Tooltip>
2975
- <PopoverContent
2976
- side="left"
2977
- align="start"
2978
- sideOffset={8}
2979
- className="w-64 p-3"
3287
+ <Tooltip>
3288
+ <TooltipTrigger asChild>
3289
+ <Button
3290
+ type="button"
3291
+ variant="ghost"
3292
+ size="icon"
3293
+ className={cn(
3294
+ "size-6 rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
3295
+ showIndependentCorners &&
3296
+ "bg-[var(--design-editor-accent-color)]/20 text-[var(--design-editor-accent-color)] hover:bg-[var(--design-editor-accent-color)]/20 hover:text-[var(--design-editor-accent-color)]",
3297
+ )}
3298
+ aria-label={independentCornersLabel}
3299
+ aria-pressed={showIndependentCorners}
3300
+ onClick={() => setShowIndependentCorners((value) => !value)}
3301
+ >
3302
+ <IconBorderCorners className="size-3.5" />
3303
+ </Button>
3304
+ </TooltipTrigger>
3305
+ <TooltipContent>{independentCornersLabel}</TooltipContent>
3306
+ </Tooltip>
3307
+ {showIndependentCorners ? (
3308
+ <>
3309
+ <AppearanceScrubField
3310
+ label={t("editPanel.labels.topLeft")}
3311
+ icon={IconRadiusTopLeft}
3312
+ value={corners.topLeft}
3313
+ onChange={(value) =>
3314
+ onStyleChange(
3315
+ "borderTopLeftRadius",
3316
+ `${Math.max(0, Math.round(value))}px`,
3317
+ )
3318
+ }
3319
+ min={0}
3320
+ precision={1}
3321
+ />
3322
+ <AppearanceScrubField
3323
+ label={t("editPanel.labels.topRight")}
3324
+ icon={IconRadiusTopRight}
3325
+ value={corners.topRight}
3326
+ onChange={(value) =>
3327
+ onStyleChange(
3328
+ "borderTopRightRadius",
3329
+ `${Math.max(0, Math.round(value))}px`,
3330
+ )
3331
+ }
3332
+ min={0}
3333
+ precision={1}
3334
+ />
3335
+ <span aria-hidden="true" />
3336
+ <AppearanceScrubField
3337
+ label={t("editPanel.labels.bottomLeft")}
3338
+ icon={IconRadiusBottomLeft}
3339
+ value={corners.bottomLeft}
3340
+ onChange={(value) =>
3341
+ onStyleChange(
3342
+ "borderBottomLeftRadius",
3343
+ `${Math.max(0, Math.round(value))}px`,
3344
+ )
3345
+ }
3346
+ min={0}
3347
+ precision={1}
3348
+ />
3349
+ <AppearanceScrubField
3350
+ label={t("editPanel.labels.bottomRight")}
3351
+ icon={IconRadiusBottomRight}
3352
+ value={corners.bottomRight}
3353
+ onChange={(value) =>
3354
+ onStyleChange(
3355
+ "borderBottomRightRadius",
3356
+ `${Math.max(0, Math.round(value))}px`,
3357
+ )
3358
+ }
3359
+ min={0}
3360
+ precision={1}
3361
+ />
3362
+ <span aria-hidden="true" />
3363
+ </>
3364
+ ) : null}
3365
+ </>
3366
+ );
3367
+ }
3368
+
3369
+ function AppearanceScrubField({
3370
+ label,
3371
+ icon,
3372
+ value,
3373
+ onChange,
3374
+ mixed = false,
3375
+ min,
3376
+ max,
3377
+ step,
3378
+ unit,
3379
+ precision,
3380
+ }: {
3381
+ label: string;
3382
+ icon: (props: { className?: string }) => ReactNode;
3383
+ value: number;
3384
+ onChange: (value: number) => void;
3385
+ mixed?: boolean;
3386
+ min?: number;
3387
+ max?: number;
3388
+ step?: number;
3389
+ unit?: string;
3390
+ precision?: number;
3391
+ }) {
3392
+ return (
3393
+ <ScrubInput
3394
+ label={label}
3395
+ ariaLabel={label}
3396
+ icon={icon}
3397
+ value={value}
3398
+ onChange={onChange}
3399
+ mixed={mixed}
3400
+ min={min}
3401
+ max={max}
3402
+ step={step}
3403
+ unit={unit}
3404
+ precision={precision}
3405
+ className="min-w-0 gap-0"
3406
+ labelClassName="h-6 w-7 justify-center gap-0 rounded-l-md rounded-r-none border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-muted-foreground [&>span]:sr-only"
3407
+ inputClassName="h-6 min-w-0 rounded-l-none rounded-r-md border-[var(--design-editor-control-border)] border-l-0 bg-[var(--design-editor-control-bg)] px-0 text-left shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
3408
+ />
3409
+ );
3410
+ }
3411
+
3412
+ function BlendModeMenu({
3413
+ styles,
3414
+ onStyleChange,
3415
+ }: {
3416
+ styles: Record<string, string>;
3417
+ onStyleChange: (property: string, value: string) => void;
3418
+ }) {
3419
+ const [open, setOpen] = useState(false);
3420
+ const blendMode = optionValue(
3421
+ BLEND_MODE_OPTIONS,
3422
+ styles.mixBlendMode || "normal",
3423
+ "normal",
3424
+ );
3425
+ const selectedBlendMode =
3426
+ blendMode === "normal" && styles.isolation !== "isolate"
3427
+ ? "pass-through"
3428
+ : blendMode;
3429
+ const options = [
3430
+ {
3431
+ value: "pass-through",
3432
+ label: "Pass through", // i18n-ignore design blend mode label
3433
+ },
3434
+ ...BLEND_MODE_OPTIONS,
3435
+ ] as const;
3436
+ const selectBlendMode = (value: (typeof options)[number]["value"]) => {
3437
+ if (value === "pass-through") {
3438
+ onStyleChange("mixBlendMode", "normal");
3439
+ onStyleChange("isolation", "auto");
3440
+ return;
3441
+ }
3442
+ onStyleChange("mixBlendMode", value);
3443
+ if (value === "normal") onStyleChange("isolation", "isolate");
3444
+ };
3445
+
3446
+ return (
3447
+ <DropdownMenu open={open} onOpenChange={setOpen}>
3448
+ <DropdownMenuTrigger asChild>
3449
+ <Button
3450
+ type="button"
3451
+ variant="ghost"
3452
+ size="icon"
3453
+ aria-label={"Blend mode" /* i18n-ignore design inspector action */}
3454
+ aria-pressed={open}
3455
+ className={cn(
3456
+ "size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground",
3457
+ open &&
3458
+ "bg-[var(--design-editor-accent-color)]/20 text-[var(--design-editor-accent-color)] hover:text-[var(--design-editor-accent-color)]",
3459
+ )}
2980
3460
  >
2981
- <div className="space-y-2">
2982
- <p className="text-xs font-semibold text-foreground">
2983
- {independentCornersLabel}
2984
- </p>
2985
- <div className="grid grid-cols-2 gap-2">
2986
- <ScrubInput
2987
- label={t("editPanel.labels.topLeft")}
2988
- value={corners.topLeft}
2989
- onChange={(value) =>
2990
- onStyleChange(
2991
- "borderTopLeftRadius",
2992
- `${Math.max(0, Math.round(value))}px`,
2993
- )
2994
- }
2995
- unit="px"
2996
- min={0}
2997
- precision={1}
2998
- inputClassName="h-6"
2999
- />
3000
- <ScrubInput
3001
- label={t("editPanel.labels.topRight")}
3002
- value={corners.topRight}
3003
- onChange={(value) =>
3004
- onStyleChange(
3005
- "borderTopRightRadius",
3006
- `${Math.max(0, Math.round(value))}px`,
3007
- )
3008
- }
3009
- unit="px"
3010
- min={0}
3011
- precision={1}
3012
- inputClassName="h-6"
3013
- />
3014
- <ScrubInput
3015
- label={t("editPanel.labels.bottomLeft")}
3016
- value={corners.bottomLeft}
3017
- onChange={(value) =>
3018
- onStyleChange(
3019
- "borderBottomLeftRadius",
3020
- `${Math.max(0, Math.round(value))}px`,
3021
- )
3022
- }
3023
- unit="px"
3024
- min={0}
3025
- precision={1}
3026
- inputClassName="h-6"
3027
- />
3028
- <ScrubInput
3029
- label={t("editPanel.labels.bottomRight")}
3030
- value={corners.bottomRight}
3031
- onChange={(value) =>
3032
- onStyleChange(
3033
- "borderBottomRightRadius",
3034
- `${Math.max(0, Math.round(value))}px`,
3035
- )
3036
- }
3037
- unit="px"
3038
- min={0}
3039
- precision={1}
3040
- inputClassName="h-6"
3041
- />
3042
- </div>
3043
- </div>
3044
- </PopoverContent>
3045
- </Popover>
3046
- </div>
3461
+ <IconDroplet className="size-3.5" />
3462
+ </Button>
3463
+ </DropdownMenuTrigger>
3464
+ <DropdownMenuContent
3465
+ side="left"
3466
+ align="start"
3467
+ sideOffset={8}
3468
+ 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"
3469
+ >
3470
+ {options.map((option) => (
3471
+ <DropdownMenuItem
3472
+ key={option.value}
3473
+ className="flex h-9 cursor-pointer items-center gap-3 rounded-md px-3 text-[13px] focus:bg-[var(--design-editor-control-bg)]"
3474
+ onSelect={() => selectBlendMode(option.value)}
3475
+ >
3476
+ <span className="flex size-4 shrink-0 items-center justify-center">
3477
+ {selectedBlendMode === option.value ? (
3478
+ <IconCheck className="size-4" />
3479
+ ) : null}
3480
+ </span>
3481
+ <span>{option.label}</span>
3482
+ </DropdownMenuItem>
3483
+ ))}
3484
+ </DropdownMenuContent>
3485
+ </DropdownMenu>
3047
3486
  );
3048
3487
  }
3049
3488
 
@@ -3098,7 +3537,6 @@ function StrokeLayerControl({
3098
3537
  />
3099
3538
  </div>
3100
3539
  <SectionIconButton
3101
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
3102
3540
  label={
3103
3541
  visible
3104
3542
  ? t("editPanel.labels.hideLayer")
@@ -3123,7 +3561,6 @@ function StrokeLayerControl({
3123
3561
  )}
3124
3562
  </SectionIconButton>
3125
3563
  <SectionIconButton
3126
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
3127
3564
  label={t("editPanel.labels.removeLayer")}
3128
3565
  onClick={onRemove}
3129
3566
  >
@@ -3133,7 +3570,7 @@ function StrokeLayerControl({
3133
3570
  {/* design stroke geometry: position + weight side by side */}
3134
3571
  <div className="grid grid-cols-2 gap-1.5">
3135
3572
  <Select value={position} onValueChange={movePosition}>
3136
- <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)]">
3573
+ <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)]">
3137
3574
  <SelectValue />
3138
3575
  </SelectTrigger>
3139
3576
  <SelectContent>
@@ -3141,7 +3578,7 @@ function StrokeLayerControl({
3141
3578
  <SelectItem
3142
3579
  key={option.value}
3143
3580
  value={option.value}
3144
- className="text-[11px]"
3581
+ className="!text-[11px]"
3145
3582
  >
3146
3583
  {option.label}
3147
3584
  </SelectItem>
@@ -3163,7 +3600,7 @@ function StrokeLayerControl({
3163
3600
  min={0}
3164
3601
  precision={1}
3165
3602
  className="gap-0"
3166
- labelClassName="h-6 w-6 justify-center gap-0 rounded-l-md border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-[11px] [&>span]:hidden"
3603
+ labelClassName="h-6 w-6 justify-center gap-0 rounded-l-md rounded-r-none border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] !text-[11px] [&>span]:hidden"
3167
3604
  inputClassName="h-6 rounded-l-none rounded-r-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
3168
3605
  />
3169
3606
  </div>
@@ -3268,26 +3705,50 @@ function readBlurFilter(value: string | undefined): number {
3268
3705
  return match ? Math.max(0, Number(match[1])) : 0;
3269
3706
  }
3270
3707
 
3708
+ function hasBlurFilter(value: string | undefined): boolean {
3709
+ return /blur\(/.test(value || "");
3710
+ }
3711
+
3712
+ function setBlurFilterValue(value: string | undefined, blur: number): string {
3713
+ const blurFn = `blur(${Math.max(0, Math.round(blur))}px)`;
3714
+ const existing = compactCssValue(value, "");
3715
+ return existing.includes("blur(")
3716
+ ? existing.replace(/blur\([^)]*\)/, blurFn)
3717
+ : blurFn;
3718
+ }
3719
+
3720
+ function shadowColorWithOpacity(color: string, opacity: number): string {
3721
+ const parsed = parseCssColor(color);
3722
+ return parsed
3723
+ ? rgbaToCss(withColorOpacity(parsed, opacity))
3724
+ : opacity <= 0
3725
+ ? "rgba(0, 0, 0, 0)"
3726
+ : color;
3727
+ }
3728
+
3271
3729
  function ShadowEffectRow({
3272
3730
  layer,
3273
3731
  index,
3274
3732
  onChange,
3275
3733
  onRemove,
3734
+ onToggleVisibility,
3276
3735
  }: {
3277
3736
  layer: ShadowLayer;
3278
3737
  index: number;
3279
3738
  onChange: (patch: Partial<ShadowLayer>) => void;
3280
3739
  onRemove: () => void;
3740
+ onToggleVisibility: () => void;
3281
3741
  }) {
3282
3742
  const t = useT();
3743
+ const visible = colorHasVisibleAlpha(layer.color);
3283
3744
  return (
3284
3745
  <Popover>
3285
- {/* design effect row: [swatch+label+x,y,blur trigger (flex-1)] [remove] */}
3746
+ {/* design effect row: [swatch+label+x,y,blur trigger (flex-1)] [eye] [remove] */}
3286
3747
  <div className="group flex items-center gap-1.5">
3287
3748
  <PopoverTrigger asChild>
3288
3749
  <button
3289
3750
  type="button"
3290
- className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
3751
+ className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left !text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
3291
3752
  >
3292
3753
  <span
3293
3754
  className="size-4 shrink-0 rounded-sm border border-[var(--design-editor-control-border)]"
@@ -3305,7 +3766,20 @@ function ShadowEffectRow({
3305
3766
  </button>
3306
3767
  </PopoverTrigger>
3307
3768
  <SectionIconButton
3308
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
3769
+ label={
3770
+ visible
3771
+ ? t("editPanel.labels.hideLayer")
3772
+ : t("editPanel.labels.showLayer")
3773
+ }
3774
+ onClick={onToggleVisibility}
3775
+ >
3776
+ {visible ? (
3777
+ <IconEye className="size-3.5" />
3778
+ ) : (
3779
+ <IconEyeOff className="size-3.5" />
3780
+ )}
3781
+ </SectionIconButton>
3782
+ <SectionIconButton
3309
3783
  label={t("editPanel.labels.removeLayer")}
3310
3784
  onClick={onRemove}
3311
3785
  >
@@ -3326,7 +3800,7 @@ function ShadowEffectRow({
3326
3800
  <button
3327
3801
  type="button"
3328
3802
  className={cn(
3329
- "rounded border px-2 py-1 text-[11px]",
3803
+ "rounded border px-2 py-1 !text-[11px]",
3330
3804
  layer.inset
3331
3805
  ? "border-[var(--design-editor-accent-color)] bg-[var(--design-editor-selection-color)] text-foreground"
3332
3806
  : "border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-muted-foreground",
@@ -3565,7 +4039,7 @@ function TypographyProperties({
3565
4039
  >
3566
4040
  <SelectTrigger
3567
4041
  aria-label={t("editPanel.labels.font")}
3568
- className="h-6 w-full 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)]"
4042
+ className="h-6 w-full 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)]"
3569
4043
  >
3570
4044
  <SelectValue />
3571
4045
  </SelectTrigger>
@@ -3574,7 +4048,7 @@ function TypographyProperties({
3574
4048
  <SelectItem
3575
4049
  key={opt.value}
3576
4050
  value={opt.value}
3577
- className="text-[11px]"
4051
+ className="!text-[11px]"
3578
4052
  >
3579
4053
  {opt.label}
3580
4054
  </SelectItem>
@@ -3589,7 +4063,7 @@ function TypographyProperties({
3589
4063
  value={styles.fontWeight || "400"}
3590
4064
  onValueChange={(v) => onStyleChange("fontWeight", v)}
3591
4065
  >
3592
- <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)]">
4066
+ <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)]">
3593
4067
  <SelectValue />
3594
4068
  </SelectTrigger>
3595
4069
  <SelectContent>
@@ -3597,7 +4071,7 @@ function TypographyProperties({
3597
4071
  <SelectItem
3598
4072
  key={opt.value}
3599
4073
  value={opt.value}
3600
- className="text-[11px]"
4074
+ className="!text-[11px]"
3601
4075
  >
3602
4076
  {opt.label}
3603
4077
  </SelectItem>
@@ -3616,7 +4090,7 @@ function TypographyProperties({
3616
4090
  min={1}
3617
4091
  precision={1}
3618
4092
  className="gap-0"
3619
- labelClassName="h-6 w-6 justify-center gap-0 rounded-l-md border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-[11px] [&>span]:hidden"
4093
+ labelClassName="h-6 w-6 justify-center gap-0 rounded-l-md rounded-r-none border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] !text-[11px] [&>span]:hidden"
3620
4094
  inputClassName="h-6 rounded-l-none rounded-r-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
3621
4095
  />
3622
4096
  </div>
@@ -3635,7 +4109,7 @@ function TypographyProperties({
3635
4109
  step={0.1}
3636
4110
  precision={2}
3637
4111
  className="gap-0"
3638
- labelClassName="h-6 w-6 justify-center gap-0 rounded-l-md border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-[11px] [&>span]:hidden"
4112
+ labelClassName="h-6 w-6 justify-center gap-0 rounded-l-md rounded-r-none border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] !text-[11px] [&>span]:hidden"
3639
4113
  inputClassName="h-6 rounded-l-none rounded-r-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
3640
4114
  />
3641
4115
  <ScrubInput
@@ -3649,7 +4123,7 @@ function TypographyProperties({
3649
4123
  unit="px"
3650
4124
  precision={1}
3651
4125
  className="gap-0"
3652
- labelClassName="h-6 w-6 justify-center gap-0 rounded-l-md border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-[11px] [&>span]:hidden"
4126
+ labelClassName="h-6 w-6 justify-center gap-0 rounded-l-md rounded-r-none border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] !text-[11px] [&>span]:hidden"
3653
4127
  inputClassName="h-6 rounded-l-none rounded-r-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
3654
4128
  />
3655
4129
  </div>
@@ -3725,15 +4199,10 @@ function FlexContainerControls({
3725
4199
  element,
3726
4200
  onStyleChange,
3727
4201
  onStylesChange,
3728
- onAutoLayoutConvert,
3729
4202
  }: {
3730
4203
  element: ElementInfo;
3731
4204
  onStyleChange: (property: string, value: string) => void;
3732
4205
  onStylesChange?: (styles: Record<string, string>) => void;
3733
- onAutoLayoutConvert?: (
3734
- targetNodeId: string,
3735
- opts?: { direction?: "row" | "column"; gap?: string },
3736
- ) => void;
3737
4206
  }) {
3738
4207
  const t = useT();
3739
4208
  const styles = element.computedStyles;
@@ -3743,7 +4212,11 @@ function FlexContainerControls({
3743
4212
  // control can show the right state (normal vs horizontal/vertical/wrap)
3744
4213
  // instead of an empty "add" affordance.
3745
4214
  const display = (styles.display || "").toLowerCase();
3746
- const isFlex = display.includes("flex");
4215
+ const isFlex = element.isFlexContainer || display.includes("flex");
4216
+ const displayMode: AutoLayoutMatrixValue["display"] = isFlex
4217
+ ? "flex"
4218
+ : "block";
4219
+ const hasLayoutChildren = elementHasLayoutChildren(element);
3747
4220
  const flexDirection: AutoLayoutMatrixValue["direction"] =
3748
4221
  styles.flexDirection?.includes("column") ? "vertical" : "horizontal";
3749
4222
  const mainGapAxis =
@@ -3772,29 +4245,6 @@ function FlexContainerControls({
3772
4245
  onStyleChange("display", "block");
3773
4246
  };
3774
4247
 
3775
- /**
3776
- * "Convert to auto layout" — enables flex on a non-flex container and strips
3777
- * position:absolute from direct children (full reflow via the autoLayout
3778
- * substrate intent). Falls back to a style-only patch when no substrate
3779
- * handler is wired (e.g. read-only or non-editable contexts).
3780
- */
3781
- const handleConvertToAutoLayout = () => {
3782
- if (onAutoLayoutConvert && element.sourceId) {
3783
- onAutoLayoutConvert(element.sourceId, { direction: "row", gap: "8px" });
3784
- return;
3785
- }
3786
- // Fallback: set display:flex on the parent only (no child strip).
3787
- commitStylePatch(
3788
- {
3789
- display: "flex",
3790
- flexDirection: "row",
3791
- gap: "8px",
3792
- alignItems: "flex-start",
3793
- },
3794
- onStyleChange,
3795
- onStylesChange,
3796
- );
3797
- };
3798
4248
  const padding = {
3799
4249
  top: parseNumericValue(styles.paddingTop || "0"),
3800
4250
  right: parseNumericValue(styles.paddingRight || "0"),
@@ -3831,29 +4281,16 @@ function FlexContainerControls({
3831
4281
  horizontal: cssElementSize(element, "horizontal"),
3832
4282
  vertical: cssElementSize(element, "vertical"),
3833
4283
  },
3834
- // Forward the raw CSS display so the matrix can render the correct Flow
3835
- // state (normal flow for block/grid, flex for flex/inline-flex). Added as an
3836
- // optional contract field consumed by AutoLayoutMatrix; harmless when the
3837
- // matrix ignores it.
3838
- ...({ display } as Partial<AutoLayoutMatrixValue>),
4284
+ mixedSize: {
4285
+ horizontal: isMixedValue(styles.width),
4286
+ vertical: isMixedValue(styles.height),
4287
+ },
4288
+ display: displayMode,
3839
4289
  spaceBetween: styles.justifyContent === "space-between",
3840
4290
  };
3841
4291
 
3842
4292
  return (
3843
4293
  <div className="space-y-2">
3844
- {/* Convert to auto layout — shown when the container is not yet flex */}
3845
- {!isFlex ? (
3846
- <Button
3847
- type="button"
3848
- variant="outline"
3849
- size="sm"
3850
- className="h-7 w-full gap-1.5 text-[11px]"
3851
- onClick={handleConvertToAutoLayout}
3852
- >
3853
- <IconLayoutSettings className="size-3.5 shrink-0" />
3854
- {"Convert to auto layout" /* i18n-ignore design inspector action */}
3855
- </Button>
3856
- ) : null}
3857
4294
  <AutoLayoutMatrix
3858
4295
  value={autoLayoutValue}
3859
4296
  onDisplayChange={handleDisplayChange}
@@ -3938,6 +4375,7 @@ function FlexContainerControls({
3938
4375
  onChildMinMaxChange={(axis, kind, val) =>
3939
4376
  commitElementMinMax(axis, kind, val, onStyleChange)
3940
4377
  }
4378
+ showChildLayoutControls={hasLayoutChildren}
3941
4379
  />
3942
4380
  </div>
3943
4381
  );
@@ -4040,15 +4478,10 @@ function LayoutContextProperties({
4040
4478
  element,
4041
4479
  onStyleChange,
4042
4480
  onStylesChange,
4043
- onAutoLayoutConvert,
4044
4481
  }: {
4045
4482
  element: ElementInfo;
4046
4483
  onStyleChange: (property: string, value: string) => void;
4047
4484
  onStylesChange?: (styles: Record<string, string>) => void;
4048
- onAutoLayoutConvert?: (
4049
- targetNodeId: string,
4050
- opts?: { direction?: "row" | "column"; gap?: string },
4051
- ) => void;
4052
4485
  }) {
4053
4486
  const t = useT();
4054
4487
  const flexChild = isParentFlex(element);
@@ -4084,6 +4517,7 @@ function LayoutContextProperties({
4084
4517
  sizingAxis="horizontal"
4085
4518
  value={inferElementSizing(element, "horizontal")}
4086
4519
  resolvedSize={cssElementSize(element, "horizontal")}
4520
+ mixed={isMixedValue(element.computedStyles.width)}
4087
4521
  minMax={readElementMinMax(element, "horizontal")}
4088
4522
  options={availableSizing.horizontal ?? ["fixed"]}
4089
4523
  disabled={false}
@@ -4106,6 +4540,7 @@ function LayoutContextProperties({
4106
4540
  sizingAxis="vertical"
4107
4541
  value={inferElementSizing(element, "vertical")}
4108
4542
  resolvedSize={cssElementSize(element, "vertical")}
4543
+ mixed={isMixedValue(element.computedStyles.height)}
4109
4544
  minMax={readElementMinMax(element, "vertical")}
4110
4545
  options={availableSizing.vertical ?? ["fixed"]}
4111
4546
  disabled={false}
@@ -4141,7 +4576,6 @@ function LayoutContextProperties({
4141
4576
  element={element}
4142
4577
  onStyleChange={onStyleChange}
4143
4578
  onStylesChange={onStylesChange}
4144
- onAutoLayoutConvert={onAutoLayoutConvert}
4145
4579
  />
4146
4580
  {childControls}
4147
4581
  </PanelSection>
@@ -4214,7 +4648,7 @@ function LayoutGuideProperties({
4214
4648
  }
4215
4649
  >
4216
4650
  {active ? (
4217
- <div className="flex items-center gap-2 rounded-md bg-[var(--design-editor-control-bg)] px-2 py-1.5 text-[11px] text-muted-foreground">
4651
+ <div className="flex items-center gap-2 rounded-md bg-[var(--design-editor-control-bg)] px-2 py-1.5 !text-[11px] text-muted-foreground">
4218
4652
  <IconLayoutGrid className="size-3.5 shrink-0" />
4219
4653
  <span className="min-w-0 flex-1 truncate text-foreground">
4220
4654
  {"Columns" /* i18n-ignore design inspector label */}
@@ -4222,7 +4656,7 @@ function LayoutGuideProperties({
4222
4656
  <span className="shrink-0 tabular-nums">12</span>
4223
4657
  </div>
4224
4658
  ) : (
4225
- <p className="text-[11px] text-muted-foreground">
4659
+ <p className="!text-[11px] text-muted-foreground">
4226
4660
  {"No layout guides" /* i18n-ignore design inspector empty state */}
4227
4661
  </p>
4228
4662
  )}
@@ -4309,6 +4743,7 @@ function PositionLayoutProperties({
4309
4743
  ? "center"
4310
4744
  : "top",
4311
4745
  };
4746
+ const [constraintsExpanded, setConstraintsExpanded] = useState(false);
4312
4747
 
4313
4748
  const handleConstraintsChange = useCallback(
4314
4749
  (value: ConstraintsValue) => {
@@ -4469,7 +4904,7 @@ function PositionLayoutProperties({
4469
4904
 
4470
4905
  <div className="space-y-1.5">
4471
4906
  <SubsectionLabel>{t("editPanel.labels.position")}</SubsectionLabel>
4472
- <div className="grid grid-cols-2 gap-2">
4907
+ <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_1.75rem] gap-2">
4473
4908
  <ScrubStyleInput
4474
4909
  label="X"
4475
4910
  ariaLabel="X-position"
@@ -4488,7 +4923,39 @@ function PositionLayoutProperties({
4488
4923
  inputClassName="h-6"
4489
4924
  onChange={(v) => onStyleChange("top", `${Math.round(v)}px`)}
4490
4925
  />
4926
+ <Tooltip>
4927
+ <TooltipTrigger asChild>
4928
+ <button
4929
+ type="button"
4930
+ aria-label={
4931
+ "Constraints" /* i18n-ignore design inspector action */
4932
+ }
4933
+ aria-pressed={constraintsExpanded}
4934
+ onClick={() => setConstraintsExpanded((expanded) => !expanded)}
4935
+ className={cn(
4936
+ "flex size-7 items-center justify-center rounded-md transition-colors",
4937
+ "hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
4938
+ "focus:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
4939
+ constraintsExpanded
4940
+ ? "bg-[var(--design-editor-selection-color)] text-[var(--design-editor-accent-color)] hover:text-[var(--design-editor-accent-color)]"
4941
+ : "text-muted-foreground",
4942
+ )}
4943
+ >
4944
+ <ConstraintsPreview value={constraintsValue} />
4945
+ </button>
4946
+ </TooltipTrigger>
4947
+ <TooltipContent>
4948
+ {"Constraints" /* i18n-ignore design inspector tooltip */}
4949
+ </TooltipContent>
4950
+ </Tooltip>
4491
4951
  </div>
4952
+ {constraintsExpanded ? (
4953
+ <ConstraintsWidget
4954
+ value={constraintsValue}
4955
+ onChange={handleConstraintsChange}
4956
+ className="pt-1"
4957
+ />
4958
+ ) : null}
4492
4959
  </div>
4493
4960
 
4494
4961
  <div className="space-y-1.5">
@@ -4496,7 +4963,12 @@ function PositionLayoutProperties({
4496
4963
  <div className="flex items-center gap-2">
4497
4964
  <div className="min-w-0 flex-1">
4498
4965
  <ScrubStyleInput
4499
- label="R"
4966
+ label="Rotation"
4967
+ ariaLabel={t("editPanel.labels.rotation")}
4968
+ tooltipLabel={t("editPanel.labels.rotation")}
4969
+ hideIcon={false}
4970
+ icon={IconAngle}
4971
+ labelClassName="[&>span]:sr-only"
4500
4972
  value={`${parseRotationValue(styles.transform)}deg`}
4501
4973
  unit="deg"
4502
4974
  inputClassName="h-6"
@@ -4530,13 +5002,6 @@ function PositionLayoutProperties({
4530
5002
  </InspectorSegment>
4531
5003
  </div>
4532
5004
  </div>
4533
-
4534
- {constrainedPosition ? (
4535
- <ConstraintsWidget
4536
- value={constraintsValue}
4537
- onChange={handleConstraintsChange}
4538
- />
4539
- ) : null}
4540
5005
  </PanelSection>
4541
5006
  );
4542
5007
  }
@@ -4569,6 +5034,12 @@ function FillProperties({
4569
5034
  const backgroundPositionLayers = isTextElement
4570
5035
  ? []
4571
5036
  : splitCssLayers(styles.backgroundPosition || "");
5037
+ const fillIsMixed =
5038
+ isMixedValue(fillValue) ||
5039
+ isMixedValue(styles.backgroundImage) ||
5040
+ isMixedValue(styles.backgroundSize) ||
5041
+ isMixedValue(styles.backgroundRepeat) ||
5042
+ isMixedValue(styles.backgroundPosition);
4572
5043
  const hasBackgroundLayer = !isTextElement && backgroundLayers.length > 0;
4573
5044
  const hasVisibleFill =
4574
5045
  isTextElement || colorHasVisibleAlpha(fillValue) || hasBackgroundLayer;
@@ -4638,6 +5109,18 @@ function FillProperties({
4638
5109
  <SectionIconButton
4639
5110
  label={t("editPanel.labels.addLayer")}
4640
5111
  onClick={() => {
5112
+ if (fillIsMixed) {
5113
+ commitStylePatch(
5114
+ {
5115
+ color: "#000000",
5116
+ backgroundColor: "#ffffff",
5117
+ backgroundImage: "none",
5118
+ },
5119
+ onStyleChange,
5120
+ onStylesChange,
5121
+ );
5122
+ return;
5123
+ }
4641
5124
  if (isTextElement) {
4642
5125
  onStyleChange(
4643
5126
  "color",
@@ -4668,7 +5151,13 @@ function FillProperties({
4668
5151
  </>
4669
5152
  }
4670
5153
  >
4671
- {hasVisibleFill ? (
5154
+ {fillIsMixed ? (
5155
+ <p className="px-1.5 py-2 !text-[11px] text-muted-foreground">
5156
+ {
5157
+ "Click + to replace mixed content" /* i18n-ignore figma mixed fill hint */
5158
+ }
5159
+ </p>
5160
+ ) : hasVisibleFill ? (
4672
5161
  <div className="space-y-1.5">
4673
5162
  {isTextElement || colorHasVisibleAlpha(fillValue) ? (
4674
5163
  /* design row: [swatch+hex trigger (flex-1)] [eye] [remove] */
@@ -4700,7 +5189,6 @@ function FillProperties({
4700
5189
  />
4701
5190
  </div>
4702
5191
  <SectionIconButton
4703
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
4704
5192
  label={
4705
5193
  isHidden
4706
5194
  ? t("editPanel.labels.showLayer")
@@ -4716,7 +5204,6 @@ function FillProperties({
4716
5204
  )}
4717
5205
  </SectionIconButton>
4718
5206
  <SectionIconButton
4719
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
4720
5207
  label={t("editPanel.labels.removeLayer")}
4721
5208
  onClick={() => {
4722
5209
  if (isTextElement) {
@@ -4783,7 +5270,7 @@ function FillProperties({
4783
5270
  <PopoverTrigger asChild>
4784
5271
  <button
4785
5272
  type="button"
4786
- className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
5273
+ className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left !text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
4787
5274
  >
4788
5275
  <span
4789
5276
  className="size-4 shrink-0 rounded-sm border border-[var(--design-editor-control-border)]"
@@ -4848,7 +5335,6 @@ function FillProperties({
4848
5335
  </PopoverContent>
4849
5336
  </Popover>
4850
5337
  <SectionIconButton
4851
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
4852
5338
  label={
4853
5339
  opacity <= 0
4854
5340
  ? t("editPanel.labels.showLayer")
@@ -4928,7 +5414,6 @@ function FillProperties({
4928
5414
  )}
4929
5415
  </SectionIconButton>
4930
5416
  <SectionIconButton
4931
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
4932
5417
  label={t("editPanel.labels.removeLayer")}
4933
5418
  onClick={removeLayer}
4934
5419
  >
@@ -4960,6 +5445,15 @@ function StrokeProperties({
4960
5445
  styles.outlineWidth,
4961
5446
  styles.outlineStyle,
4962
5447
  );
5448
+ const strokeIsMixed = [
5449
+ styles.borderWidth,
5450
+ styles.borderStyle,
5451
+ styles.borderColor,
5452
+ styles.outlineWidth,
5453
+ styles.outlineStyle,
5454
+ styles.outlineColor,
5455
+ styles.outlineOffset,
5456
+ ].some(isMixedValue);
4963
5457
  // Render the row whenever a stroke has been configured (non-zero width),
4964
5458
  // even when its style is "none" (hidden). This mirrors Figma's behavior where
4965
5459
  // hidden stroke rows remain present so the user can re-show them via the eye icon.
@@ -4973,6 +5467,20 @@ function StrokeProperties({
4973
5467
  <SectionIconButton
4974
5468
  label={t("editPanel.labels.addLayer")}
4975
5469
  onClick={() => {
5470
+ if (strokeIsMixed) {
5471
+ commitStylePatch(
5472
+ {
5473
+ borderWidth: "1px",
5474
+ borderStyle: "solid",
5475
+ borderColor: "#000000",
5476
+ outlineWidth: "0px",
5477
+ outlineStyle: "none",
5478
+ },
5479
+ onStyleChange,
5480
+ onStylesChange,
5481
+ );
5482
+ return;
5483
+ }
4976
5484
  if (!borderVisible) {
4977
5485
  const borderColor = cssColorOrFallback(
4978
5486
  styles.borderColor || styles.color,
@@ -5029,7 +5537,13 @@ function StrokeProperties({
5029
5537
  </SectionIconButton>
5030
5538
  }
5031
5539
  >
5032
- {borderExists ? (
5540
+ {strokeIsMixed ? (
5541
+ <p className="px-1.5 py-2 !text-[11px] text-muted-foreground">
5542
+ {
5543
+ "Click + to replace mixed content" /* i18n-ignore figma mixed stroke hint */
5544
+ }
5545
+ </p>
5546
+ ) : borderExists ? (
5033
5547
  <StrokeLayerControl
5034
5548
  kind="border"
5035
5549
  visible={borderVisible}
@@ -5085,15 +5599,6 @@ function AppearanceProperties({
5085
5599
  title={t("root.commandAppearance")}
5086
5600
  actions={
5087
5601
  <>
5088
- {/* Opacity / blend-mode affordance — matches the design editor's pill icon */}
5089
- <SectionIconButton
5090
- label={
5091
- "Opacity & blend mode" /* i18n-ignore design inspector action */
5092
- }
5093
- >
5094
- <IconSlice className="size-3.5" />
5095
- </SectionIconButton>
5096
- {/* Visibility toggle */}
5097
5602
  <SectionIconToggle
5098
5603
  label={
5099
5604
  hidden
@@ -5111,55 +5616,36 @@ function AppearanceProperties({
5111
5616
  <IconEye className="size-3.5" />
5112
5617
  )}
5113
5618
  </SectionIconToggle>
5114
- {/* Styles / fill library affordance — matches the design editor's droplet icon */}
5115
- <SectionIconButton
5116
- label={"Styles" /* i18n-ignore design inspector action */}
5117
- >
5118
- <IconDroplet className="size-3.5" />
5119
- </SectionIconButton>
5619
+ <BlendModeMenu styles={styles} onStyleChange={onStyleChange} />
5120
5620
  </>
5121
5621
  }
5122
5622
  >
5123
- <div className="grid grid-cols-2 gap-2">
5124
- <ScrubInput
5623
+ <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-center gap-x-2 gap-y-1.5">
5624
+ <p className="min-w-0 truncate !text-[11px] font-medium text-muted-foreground">
5625
+ {t("editPanel.labels.opacity")}
5626
+ </p>
5627
+ <p className="min-w-0 truncate !text-[11px] font-medium text-muted-foreground">
5628
+ {t("editPanel.labels.cornerRadius")}
5629
+ </p>
5630
+ <span aria-hidden="true" />
5631
+ <AppearanceScrubField
5125
5632
  label={t("editPanel.labels.opacity")}
5126
- value={parseNumericValue(styles.opacity || "1") * 100}
5633
+ icon={IconGridDots}
5634
+ value={
5635
+ isMixedValue(styles.opacity)
5636
+ ? 0
5637
+ : parseNumericValue(styles.opacity || "1") * 100
5638
+ }
5127
5639
  onChange={(v) => onStyleChange("opacity", String(v / 100))}
5640
+ mixed={isMixedValue(styles.opacity)}
5128
5641
  min={0}
5129
5642
  max={100}
5130
5643
  step={1}
5131
5644
  unit="%"
5132
5645
  precision={1}
5133
- labelClassName="w-0 overflow-hidden"
5134
- inputClassName="h-6 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] shadow-none"
5135
5646
  />
5136
- {/* M9: blend mode compact — inline select next to opacity, no separate labeled row */}
5137
- <Select
5138
- value={optionValue(
5139
- BLEND_MODE_OPTIONS,
5140
- styles.mixBlendMode || "normal",
5141
- "normal",
5142
- )}
5143
- onValueChange={(value) => onStyleChange("mixBlendMode", value)}
5144
- >
5145
- <SelectTrigger className="h-6 min-w-0 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)]">
5146
- <SelectValue />
5147
- </SelectTrigger>
5148
- <SelectContent>
5149
- {BLEND_MODE_OPTIONS.map((opt) => (
5150
- <SelectItem
5151
- key={opt.value}
5152
- value={opt.value}
5153
- className="text-[11px]"
5154
- >
5155
- {opt.label}
5156
- </SelectItem>
5157
- ))}
5158
- </SelectContent>
5159
- </Select>
5647
+ <CornerRadiusControl styles={styles} onStyleChange={onStyleChange} />
5160
5648
  </div>
5161
- {/* M7: use CornerRadiusControl (with independent-corners toggle) instead of bare ScrubInput */}
5162
- <CornerRadiusControl styles={styles} onStyleChange={onStyleChange} />
5163
5649
  </PanelSection>
5164
5650
  );
5165
5651
  }
@@ -5176,11 +5662,19 @@ function EffectsProperties({
5176
5662
  const t = useT();
5177
5663
  const styles = element.computedStyles;
5178
5664
  const blurValue = readBlurFilter(styles.filter);
5665
+ const filterHasBlur = hasBlurFilter(styles.filter);
5179
5666
  // M5 · Background (backdrop) blur is a distinct design effect type, backed by
5180
5667
  // CSS `backdrop-filter: blur()` (vs layer blur's `filter: blur()`).
5181
- const backdropBlurValue = readBlurFilter(
5182
- styles.backdropFilter || styles.webkitBackdropFilter,
5183
- );
5668
+ const backdropFilterValue =
5669
+ styles.backdropFilter || styles.webkitBackdropFilter;
5670
+ const backdropFilterHasBlur = hasBlurFilter(backdropFilterValue);
5671
+ const backdropBlurValue = readBlurFilter(backdropFilterValue);
5672
+ const [hiddenEffectStash, setHiddenEffectStash] = useState<
5673
+ Record<string, string>
5674
+ >({});
5675
+ const effectStashKey = elementIdentityKey(element);
5676
+ const layerBlurStashKey = `${effectStashKey}:filter:blur`;
5677
+ const backdropBlurStashKey = `${effectStashKey}:backdrop-filter:blur`;
5184
5678
  const shadowLayers = parseShadowLayers(styles.boxShadow);
5185
5679
  const setShadowLayers = (layers: ShadowLayer[]) => {
5186
5680
  const boxShadow = serializeShadowLayers(layers);
@@ -5213,21 +5707,21 @@ function EffectsProperties({
5213
5707
  </DropdownMenuTrigger>
5214
5708
  <DropdownMenuContent align="end" className="min-w-44">
5215
5709
  <DropdownMenuItem
5216
- className="gap-2 text-[11px]"
5710
+ className="gap-2 !text-[11px]"
5217
5711
  onSelect={addDropShadow}
5218
5712
  >
5219
5713
  <IconShadow className="size-3.5" />
5220
5714
  {t("editPanel.labels.dropShadow")}
5221
5715
  </DropdownMenuItem>
5222
5716
  <DropdownMenuItem
5223
- className="gap-2 text-[11px]"
5717
+ className="gap-2 !text-[11px]"
5224
5718
  onSelect={addLayerBlur}
5225
5719
  >
5226
5720
  <IconBlur className="size-3.5" />
5227
5721
  {t("editPanel.labels.layerBlur")}
5228
5722
  </DropdownMenuItem>
5229
5723
  <DropdownMenuItem
5230
- className="gap-2 text-[11px]"
5724
+ className="gap-2 !text-[11px]"
5231
5725
  onSelect={addBackgroundBlur}
5232
5726
  >
5233
5727
  <IconBackground className="size-3.5" />
@@ -5252,6 +5746,41 @@ function EffectsProperties({
5252
5746
  );
5253
5747
  setShadowLayers(next);
5254
5748
  }}
5749
+ onToggleVisibility={() => {
5750
+ const visible = colorHasVisibleAlpha(layer.color);
5751
+ const shadowStashKey = `${effectStashKey}:shadow:${layer.id}`;
5752
+ if (visible) {
5753
+ setHiddenEffectStash((prev) => ({
5754
+ ...prev,
5755
+ [shadowStashKey]: layer.color,
5756
+ }));
5757
+ const next = shadowLayers.map((candidate) =>
5758
+ candidate.id === layer.id
5759
+ ? {
5760
+ ...candidate,
5761
+ color: shadowColorWithOpacity(candidate.color, 0),
5762
+ }
5763
+ : candidate,
5764
+ );
5765
+ setShadowLayers(next);
5766
+ return;
5767
+ }
5768
+
5769
+ const restored =
5770
+ hiddenEffectStash[shadowStashKey] ??
5771
+ shadowColorWithOpacity(layer.color, 25);
5772
+ setHiddenEffectStash((prev) => {
5773
+ const next = { ...prev };
5774
+ delete next[shadowStashKey];
5775
+ return next;
5776
+ });
5777
+ const next = shadowLayers.map((candidate) =>
5778
+ candidate.id === layer.id
5779
+ ? { ...candidate, color: restored }
5780
+ : candidate,
5781
+ );
5782
+ setShadowLayers(next);
5783
+ }}
5255
5784
  onRemove={() =>
5256
5785
  setShadowLayers(
5257
5786
  shadowLayers.filter((candidate) => candidate.id !== layer.id),
@@ -5261,14 +5790,14 @@ function EffectsProperties({
5261
5790
  ))}
5262
5791
  </div>
5263
5792
  ) : null}
5264
- {blurValue > 0 ? (
5793
+ {filterHasBlur ? (
5265
5794
  /* design effect row for layer blur: flat row matching shadow rows */
5266
5795
  <Popover>
5267
5796
  <div className="group flex items-center gap-1.5">
5268
5797
  <PopoverTrigger asChild>
5269
5798
  <button
5270
5799
  type="button"
5271
- className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
5800
+ className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left !text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
5272
5801
  >
5273
5802
  <span className="min-w-0 flex-1 truncate font-medium text-foreground">
5274
5803
  {t("editPanel.labels.layerBlur")}
@@ -5279,10 +5808,45 @@ function EffectsProperties({
5279
5808
  </button>
5280
5809
  </PopoverTrigger>
5281
5810
  <SectionIconButton
5282
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
5811
+ label={
5812
+ blurValue > 0
5813
+ ? t("editPanel.labels.hideLayer")
5814
+ : t("editPanel.labels.showLayer")
5815
+ }
5816
+ onClick={() => {
5817
+ if (blurValue > 0) {
5818
+ setHiddenEffectStash((prev) => ({
5819
+ ...prev,
5820
+ [layerBlurStashKey]: String(blurValue),
5821
+ }));
5822
+ onStyleChange("filter", setBlurFilterValue(styles.filter, 0));
5823
+ return;
5824
+ }
5825
+
5826
+ const restored = Number(hiddenEffectStash[layerBlurStashKey]);
5827
+ const nextBlur =
5828
+ Number.isFinite(restored) && restored > 0 ? restored : 4;
5829
+ setHiddenEffectStash((prev) => {
5830
+ const next = { ...prev };
5831
+ delete next[layerBlurStashKey];
5832
+ return next;
5833
+ });
5834
+ onStyleChange(
5835
+ "filter",
5836
+ setBlurFilterValue(styles.filter, nextBlur),
5837
+ );
5838
+ }}
5839
+ >
5840
+ {blurValue > 0 ? (
5841
+ <IconEye className="size-3.5" />
5842
+ ) : (
5843
+ <IconEyeOff className="size-3.5" />
5844
+ )}
5845
+ </SectionIconButton>
5846
+ <SectionIconButton
5283
5847
  label={t("editPanel.labels.removeLayer")}
5284
5848
  onClick={() => onStyleChange("filter", "none")}
5285
- disabled={!styles.filter || styles.filter === "none"}
5849
+ disabled={!filterHasBlur}
5286
5850
  >
5287
5851
  <IconMinus className="size-3.5" />
5288
5852
  </SectionIconButton>
@@ -5296,14 +5860,12 @@ function EffectsProperties({
5296
5860
  <ScrubInput
5297
5861
  label={t("editPanel.labels.blur")}
5298
5862
  value={blurValue}
5299
- onChange={(value) => {
5300
- const blurFn = `blur(${Math.max(0, Math.round(value))}px)`;
5301
- const existing = styles.filter || "";
5302
- const next = existing.includes("blur(")
5303
- ? existing.replace(/blur\([^)]*\)/, blurFn)
5304
- : blurFn;
5305
- onStyleChange("filter", next);
5306
- }}
5863
+ onChange={(value) =>
5864
+ onStyleChange(
5865
+ "filter",
5866
+ setBlurFilterValue(styles.filter, value),
5867
+ )
5868
+ }
5307
5869
  unit="px"
5308
5870
  min={0}
5309
5871
  precision={1}
@@ -5313,14 +5875,14 @@ function EffectsProperties({
5313
5875
  </PopoverContent>
5314
5876
  </Popover>
5315
5877
  ) : null}
5316
- {backdropBlurValue > 0 ? (
5878
+ {backdropFilterHasBlur ? (
5317
5879
  /* M5 · Background (backdrop) blur effect row — mirrors the layer-blur row */
5318
5880
  <Popover>
5319
5881
  <div className="group flex items-center gap-1.5">
5320
5882
  <PopoverTrigger asChild>
5321
5883
  <button
5322
5884
  type="button"
5323
- className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
5885
+ className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left !text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
5324
5886
  >
5325
5887
  <span className="min-w-0 flex-1 truncate font-medium text-foreground">
5326
5888
  {"Background blur" /* i18n-ignore design effect type */}
@@ -5331,12 +5893,50 @@ function EffectsProperties({
5331
5893
  </button>
5332
5894
  </PopoverTrigger>
5333
5895
  <SectionIconButton
5334
- className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
5896
+ label={
5897
+ backdropBlurValue > 0
5898
+ ? t("editPanel.labels.hideLayer")
5899
+ : t("editPanel.labels.showLayer")
5900
+ }
5901
+ onClick={() => {
5902
+ if (backdropBlurValue > 0) {
5903
+ setHiddenEffectStash((prev) => ({
5904
+ ...prev,
5905
+ [backdropBlurStashKey]: String(backdropBlurValue),
5906
+ }));
5907
+ onStyleChange(
5908
+ "backdropFilter",
5909
+ setBlurFilterValue(backdropFilterValue, 0),
5910
+ );
5911
+ return;
5912
+ }
5913
+
5914
+ const restored = Number(
5915
+ hiddenEffectStash[backdropBlurStashKey],
5916
+ );
5917
+ const nextBlur =
5918
+ Number.isFinite(restored) && restored > 0 ? restored : 8;
5919
+ setHiddenEffectStash((prev) => {
5920
+ const next = { ...prev };
5921
+ delete next[backdropBlurStashKey];
5922
+ return next;
5923
+ });
5924
+ onStyleChange(
5925
+ "backdropFilter",
5926
+ setBlurFilterValue(backdropFilterValue, nextBlur),
5927
+ );
5928
+ }}
5929
+ >
5930
+ {backdropBlurValue > 0 ? (
5931
+ <IconEye className="size-3.5" />
5932
+ ) : (
5933
+ <IconEyeOff className="size-3.5" />
5934
+ )}
5935
+ </SectionIconButton>
5936
+ <SectionIconButton
5335
5937
  label={t("editPanel.labels.removeLayer")}
5336
5938
  onClick={() => onStyleChange("backdropFilter", "none")}
5337
- disabled={
5338
- !styles.backdropFilter || styles.backdropFilter === "none"
5339
- }
5939
+ disabled={!backdropFilterHasBlur}
5340
5940
  >
5341
5941
  <IconMinus className="size-3.5" />
5342
5942
  </SectionIconButton>
@@ -5353,7 +5953,7 @@ function EffectsProperties({
5353
5953
  onChange={(value) =>
5354
5954
  onStyleChange(
5355
5955
  "backdropFilter",
5356
- `blur(${Math.max(0, Math.round(value))}px)`,
5956
+ setBlurFilterValue(backdropFilterValue, value),
5357
5957
  )
5358
5958
  }
5359
5959
  unit="px"
@@ -5433,7 +6033,7 @@ function SelectionColorsProperties({
5433
6033
  <PopoverTrigger asChild>
5434
6034
  <button
5435
6035
  type="button"
5436
- className="flex h-6 w-full items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
6036
+ className="flex h-6 w-full items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 !text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
5437
6037
  aria-label={color.value}
5438
6038
  >
5439
6039
  <span
@@ -5466,7 +6066,7 @@ function SelectionColorsProperties({
5466
6066
  ) : (
5467
6067
  <button
5468
6068
  type="button"
5469
- className="flex h-6 w-full items-center justify-between gap-2 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-left text-[11px] text-muted-foreground hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground"
6069
+ className="flex h-6 w-full items-center justify-between gap-2 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-left !text-[11px] text-muted-foreground hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground"
5470
6070
  onClick={() => setExpanded(true)}
5471
6071
  >
5472
6072
  <span className="truncate">
@@ -5637,11 +6237,11 @@ function MakeItRealCard({
5637
6237
 
5638
6238
  const summary =
5639
6239
  cta.kind === "configure-project"
5640
- ? `Configure Builder to enable ${featureLabel}.`
6240
+ ? `Choose a Builder project to enable ${featureLabel}.`
5641
6241
  : `Connect Builder to enable ${featureLabel}.`;
5642
6242
  const primaryLabel =
5643
6243
  cta.kind === "configure-project"
5644
- ? "Configure" /* i18n-ignore make-it-real card */
6244
+ ? "Choose" /* i18n-ignore make-it-real card */
5645
6245
  : "Connect"; /* i18n-ignore make-it-real card */
5646
6246
 
5647
6247
  return (
@@ -6087,10 +6687,10 @@ export function ComponentSection({
6087
6687
  <div className="flex min-h-9 items-center gap-2 px-3">
6088
6688
  {/* Accent diamond matching the workbench artboard component rows */}
6089
6689
  <span
6090
- className="size-2 shrink-0 rotate-45 rounded-[2px] bg-[var(--design-editor-accent-color)]"
6690
+ className="size-2 shrink-0 rotate-45 rounded-[2px] bg-[var(--design-editor-component-color)]"
6091
6691
  aria-hidden="true"
6092
6692
  />
6093
- <h3 className="min-w-0 flex-1 truncate text-[11px] font-semibold text-foreground">
6693
+ <h3 className="min-w-0 flex-1 truncate !text-[11px] font-semibold text-foreground">
6094
6694
  {name}
6095
6695
  </h3>
6096
6696
  {/* Jump-to-source action */}
@@ -6121,14 +6721,14 @@ export function ComponentSection({
6121
6721
  canJumpToSource
6122
6722
  ? "Edit component source" /* i18n-ignore design inspector action */
6123
6723
  : (capabilities.ctaMessage ??
6124
- "Connect Builder to jump to component source") /* i18n-ignore design inspector tooltip */
6724
+ "Source jump needs a connected app") /* i18n-ignore design inspector tooltip */
6125
6725
  }
6126
6726
  </TooltipContent>
6127
6727
  </Tooltip>
6128
6728
  </div>
6129
6729
 
6130
6730
  {/* ── Body ── */}
6131
- <div className="space-y-1.5 px-3 pb-3 pt-0.5 text-[11px]">
6731
+ <div className="space-y-1.5 px-3 pb-3 pt-0.5 !text-[11px]">
6132
6732
  {/* Source path chip */}
6133
6733
  {sourceChip && (
6134
6734
  <div
@@ -6156,7 +6756,7 @@ export function ComponentSection({
6156
6756
  const disabled = !editingEnabled || applyPropMutation.isPending;
6157
6757
  return (
6158
6758
  <div key={row.name} className="flex items-center gap-1.5">
6159
- <Label className="w-[64px] shrink-0 truncate text-[11px] font-medium capitalize text-muted-foreground">
6759
+ <Label className="w-[64px] shrink-0 truncate !text-[11px] font-medium capitalize text-muted-foreground">
6160
6760
  {row.name}
6161
6761
  </Label>
6162
6762
  {hasOptions ? (
@@ -6166,7 +6766,7 @@ export function ComponentSection({
6166
6766
  onValueChange={(v) => commitProp(row, v)}
6167
6767
  disabled={disabled}
6168
6768
  >
6169
- <SelectTrigger className="h-6 min-w-0 flex-1 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)]">
6769
+ <SelectTrigger className="h-6 min-w-0 flex-1 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)]">
6170
6770
  <SelectValue />
6171
6771
  </SelectTrigger>
6172
6772
  <SelectContent>
@@ -6174,7 +6774,7 @@ export function ComponentSection({
6174
6774
  <SelectItem
6175
6775
  key={opt}
6176
6776
  value={opt}
6177
- className="text-[11px]"
6777
+ className="!text-[11px]"
6178
6778
  >
6179
6779
  {opt}
6180
6780
  </SelectItem>
@@ -6209,7 +6809,7 @@ export function ComponentSection({
6209
6809
  e.currentTarget.blur();
6210
6810
  }
6211
6811
  }}
6212
- className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
6812
+ className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 !text-[11px] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)] md:!text-[11px]"
6213
6813
  />
6214
6814
  )}
6215
6815
  </div>
@@ -6218,11 +6818,11 @@ export function ComponentSection({
6218
6818
  </div>
6219
6819
  )}
6220
6820
 
6221
- {/* Connect-Builder CTA (inline, only when real-app features are gated) */}
6222
- {capabilities.ctaRequired && (
6821
+ {/* Connect-Builder CTA (only when prop editing is actually gated). */}
6822
+ {capabilities.ctaRequired && !editingEnabled && (
6223
6823
  <MakeItRealCard
6224
6824
  designId={designId}
6225
- featureLabel="component source jump and full prop controls"
6825
+ featureLabel="component source jump and typed prop metadata"
6226
6826
  />
6227
6827
  )}
6228
6828
  </div>
@@ -6232,6 +6832,7 @@ export function ComponentSection({
6232
6832
 
6233
6833
  export function EditPanel({
6234
6834
  selectedElement,
6835
+ selectedElements,
6235
6836
  pageStyles = {},
6236
6837
  headerTrailing,
6237
6838
  width = 256,
@@ -6243,21 +6844,18 @@ export function EditPanel({
6243
6844
  onRequestTweaks,
6244
6845
  onStyleChange,
6245
6846
  onStylesChange,
6246
- onAutoLayoutConvert,
6247
6847
  onExport,
6248
6848
  exporting = false,
6249
- readOnly = false,
6250
6849
  fileId,
6251
- filename,
6252
6850
  activeContent,
6253
6851
  activeFileUpdatedAt,
6254
6852
  designId,
6255
6853
  onComponentPropApplied,
6256
- statesPanelProps,
6257
6854
  reviewPanelProps,
6258
6855
  componentNodeId,
6259
6856
  sourceCapabilities = [],
6260
6857
  onCreateComponent,
6858
+ selectedElementAlreadyComponent = false,
6261
6859
  defaultComponentName = "Component",
6262
6860
  inspectCode,
6263
6861
  }: EditPanelProps) {
@@ -6268,12 +6866,42 @@ export function EditPanel({
6268
6866
  );
6269
6867
  const [showExportPreview, setShowExportPreview] = useState(false);
6270
6868
 
6271
- const selectedElementKey = selectedElement
6272
- ? elementIdentityKey(selectedElement)
6869
+ const effectiveSelectedElements = useMemo(
6870
+ () =>
6871
+ selectedElements && selectedElements.length > 0
6872
+ ? selectedElements
6873
+ : selectedElement
6874
+ ? [selectedElement]
6875
+ : [],
6876
+ [selectedElement, selectedElements],
6877
+ );
6878
+ const inspectorElement = useMemo(
6879
+ () =>
6880
+ effectiveSelectedElements.length > 1
6881
+ ? mixedElementFromSelection(effectiveSelectedElements)
6882
+ : (effectiveSelectedElements[0] ?? null),
6883
+ [effectiveSelectedElements],
6884
+ );
6885
+ const selectedCount = effectiveSelectedElements.length;
6886
+ const selectionAlreadyComponent =
6887
+ selectedCount === 1 &&
6888
+ (selectedElementAlreadyComponent ||
6889
+ elementIsComponentSelection(selectedElement));
6890
+ const canCreateComponent = Boolean(
6891
+ onCreateComponent &&
6892
+ selectedElement &&
6893
+ selectedCount <= 1 &&
6894
+ !selectionAlreadyComponent,
6895
+ );
6896
+ const selectedElementKey = inspectorElement
6897
+ ? `${selectedCount}:${elementIdentityKey(inspectorElement)}`
6273
6898
  : "none";
6274
- const isTextElement = selectedElement
6275
- ? TEXT_TAGS.has(selectedElement.tagName)
6276
- : false;
6899
+ const selectionHasTextElement = effectiveSelectedElements.some((element) =>
6900
+ isTextElement(element),
6901
+ );
6902
+ const selectionHasContainerElement = effectiveSelectedElements.some(
6903
+ (element) => isContainerElement(element),
6904
+ );
6277
6905
  const handleActiveTabChange = useCallback(
6278
6906
  (tab: InspectorTab) => onActiveTabChange?.(tab),
6279
6907
  [onActiveTabChange],
@@ -6296,6 +6924,10 @@ export function EditPanel({
6296
6924
  setShowExportPreview(false);
6297
6925
  }, [selectedElementKey]);
6298
6926
 
6927
+ useEffect(() => {
6928
+ if (!canCreateComponent) setCreateComponentOpen(false);
6929
+ }, [canCreateComponent]);
6930
+
6299
6931
  // Scroll guard: suppress the click that fires immediately after a scroll
6300
6932
  // gesture ends (rubber-band or normal scroll). Using onScroll instead of
6301
6933
  // onPointerDown avoids side-effects like Radix DismissableLayer detecting a
@@ -6306,41 +6938,6 @@ export function EditPanel({
6306
6938
  const userScrollIntentRef = useRef(false);
6307
6939
  const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
6308
6940
 
6309
- if (readOnly) {
6310
- return (
6311
- <div
6312
- className={cn(
6313
- "shrink-0 bg-[var(--design-editor-panel-bg)]",
6314
- "flex h-full min-h-0 flex-col overflow-hidden",
6315
- )}
6316
- style={{ width }}
6317
- >
6318
- <div className="flex min-h-8 shrink-0 items-center border-b border-border/90 px-3">
6319
- <h2 className="min-w-0 truncate text-[12px] font-semibold text-foreground">
6320
- {t("editPanel.properties")}
6321
- </h2>
6322
- </div>
6323
- <div className="flex min-h-0 flex-1 items-center justify-center px-5">
6324
- <div
6325
- className="max-w-[188px] text-center"
6326
- aria-live="polite"
6327
- aria-busy="true"
6328
- >
6329
- <div className="mx-auto mb-3 flex size-9 items-center justify-center rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-muted-foreground/60">
6330
- <IconLock className="size-4" aria-hidden="true" />
6331
- </div>
6332
- <h3 className="text-[12px] font-semibold leading-snug text-foreground">
6333
- {t("designEditor.inspectorLockedTitle")}
6334
- </h3>
6335
- <p className="mt-1.5 text-[11px] leading-snug text-muted-foreground/70">
6336
- {t("designEditor.inspectorLockedDescription")}
6337
- </p>
6338
- </div>
6339
- </div>
6340
- </div>
6341
- );
6342
- }
6343
-
6344
6941
  return (
6345
6942
  <div
6346
6943
  className={cn(
@@ -6358,28 +6955,21 @@ export function EditPanel({
6358
6955
  {activeTab === "design" ? (
6359
6956
  <>
6360
6957
  <SelectionHeader
6361
- element={selectedElement}
6958
+ element={inspectorElement}
6959
+ selectedCount={selectedCount}
6362
6960
  onCreateComponent={
6363
- onCreateComponent && selectedElement
6364
- ? () => setCreateComponentOpen(true)
6365
- : undefined
6961
+ canCreateComponent ? onCreateComponent : undefined
6366
6962
  }
6367
- inspectCodePopover={
6368
- inspectCode && selectedElement
6369
- ? (trigger) => (
6370
- <InspectCodePopover trigger={trigger} data={inspectCode} />
6371
- )
6963
+ createComponentOpen={createComponentOpen}
6964
+ onCreateComponentOpenChange={setCreateComponentOpen}
6965
+ showCreateComponentAction={!selectionAlreadyComponent}
6966
+ defaultComponentName={defaultComponentName}
6967
+ inspectCode={
6968
+ inspectCode && selectedElement && selectedCount <= 1
6969
+ ? inspectCode
6372
6970
  : undefined
6373
6971
  }
6374
6972
  />
6375
- {onCreateComponent && (
6376
- <CreateComponentDialog
6377
- open={createComponentOpen}
6378
- onOpenChange={setCreateComponentOpen}
6379
- defaultName={defaultComponentName}
6380
- onSubmit={onCreateComponent}
6381
- />
6382
- )}
6383
6973
 
6384
6974
  <div
6385
6975
  className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain"
@@ -6456,7 +7046,7 @@ export function EditPanel({
6456
7046
  >
6457
7047
  {/* §6.1 Component section — shown at the top when a component
6458
7048
  instance is selected. Requires designId + componentNodeId. */}
6459
- {designId && componentNodeId && (
7049
+ {designId && componentNodeId && selectedCount <= 1 && (
6460
7050
  <ComponentSection
6461
7051
  designId={designId}
6462
7052
  fileId={fileId}
@@ -6468,7 +7058,7 @@ export function EditPanel({
6468
7058
  />
6469
7059
  )}
6470
7060
 
6471
- {!selectedElement && (
7061
+ {!inspectorElement && (
6472
7062
  <PageProperties
6473
7063
  styles={pageStyles}
6474
7064
  onStyleChange={onStyleChange}
@@ -6476,64 +7066,52 @@ export function EditPanel({
6476
7066
  />
6477
7067
  )}
6478
7068
 
6479
- {selectedElement && (
7069
+ {inspectorElement && (
6480
7070
  <>
6481
7071
  <PositionLayoutProperties
6482
- element={selectedElement}
7072
+ element={inspectorElement}
6483
7073
  onStyleChange={onStyleChange}
6484
7074
  />
6485
7075
  <LayoutContextProperties
6486
- element={selectedElement}
7076
+ element={inspectorElement}
6487
7077
  onStyleChange={onStyleChange}
6488
7078
  onStylesChange={onStylesChange}
6489
- onAutoLayoutConvert={onAutoLayoutConvert}
6490
7079
  />
6491
7080
  <AppearanceProperties
6492
- element={selectedElement}
7081
+ element={inspectorElement}
6493
7082
  onStyleChange={onStyleChange}
6494
7083
  />
6495
- {isTextElement ? (
7084
+ {selectionHasTextElement ? (
6496
7085
  <TypographyProperties
6497
- element={selectedElement}
7086
+ element={inspectorElement}
6498
7087
  onStyleChange={onStyleChange}
6499
7088
  />
6500
7089
  ) : null}
6501
7090
  <FillProperties
6502
- element={selectedElement}
7091
+ element={inspectorElement}
6503
7092
  onStyleChange={onStyleChange}
6504
7093
  onStylesChange={onStylesChange}
6505
7094
  />
6506
7095
  <StrokeProperties
6507
- element={selectedElement}
7096
+ element={inspectorElement}
6508
7097
  onStyleChange={onStyleChange}
6509
7098
  onStylesChange={onStylesChange}
6510
7099
  />
6511
7100
  <EffectsProperties
6512
- element={selectedElement}
7101
+ element={inspectorElement}
6513
7102
  onStyleChange={onStyleChange}
6514
7103
  onStylesChange={onStylesChange}
6515
7104
  />
6516
7105
  <SelectionColorsProperties
6517
- element={selectedElement}
7106
+ element={inspectorElement}
6518
7107
  onStyleChange={onStyleChange}
6519
7108
  />
6520
- {isContainerElement(selectedElement) ? (
7109
+ {selectionHasContainerElement ? (
6521
7110
  <LayoutGuideProperties
6522
- element={selectedElement}
7111
+ element={inspectorElement}
6523
7112
  onStyleChange={onStyleChange}
6524
7113
  />
6525
7114
  ) : null}
6526
- {/* AI edit request block — collapsible, collapsed by default */}
6527
- <section className="shrink-0 border-t border-[var(--design-editor-control-border)]">
6528
- <InspectorAiActions
6529
- selector={selectedElement.selector}
6530
- sourceId={selectedElement.sourceId}
6531
- designId={designId}
6532
- fileId={fileId}
6533
- filename={filename}
6534
- canEdit={!readOnly}
6535
- />
6536
- </section>
6537
7115
  </>
6538
7116
  )}
6539
7117
  {onExport ? (
@@ -6564,56 +7142,11 @@ export function EditPanel({
6564
7142
  onExport={onExport}
6565
7143
  />
6566
7144
  {showExportPreview ? (
6567
- <ExportPreview element={selectedElement} />
7145
+ <ExportPreview element={inspectorElement} />
6568
7146
  ) : null}
6569
7147
  </PanelSection>
6570
7148
  ) : null}
6571
7149
 
6572
- {/* §6.4 States & Responsive — contextual section in Design tab.
6573
- Collapsed by default so existing Design content is not buried.
6574
- Only mounts when designId is provided so there is something to
6575
- load; without it the panel would fire an action with no id. */}
6576
- {designId && statesPanelProps ? (
6577
- <PanelSection
6578
- title={"States" /* i18n-ignore design inspector section */}
6579
- defaultCollapsed
6580
- actions={
6581
- <Tooltip>
6582
- <TooltipTrigger asChild>
6583
- <Button
6584
- type="button"
6585
- variant="ghost"
6586
- size="icon"
6587
- className="size-6 rounded-md text-muted-foreground hover:text-foreground"
6588
- aria-label={
6589
- "Breakpoints & states" /* i18n-ignore design inspector action */
6590
- }
6591
- >
6592
- <IconDeviceDesktop className="size-3.5" />
6593
- </Button>
6594
- </TooltipTrigger>
6595
- <TooltipContent>
6596
- {
6597
- "Breakpoints & states" /* i18n-ignore design inspector action */
6598
- }
6599
- </TooltipContent>
6600
- </Tooltip>
6601
- }
6602
- >
6603
- <StatesPanel designId={designId} {...statesPanelProps} />
6604
- {/* §6.4 / §6.6 — live captures (real running-app data, route
6605
- props, API responses) are a real-app capability. When
6606
- canCapture is false (inline source) surface a compact
6607
- "Make it real" CTA so the user knows it's one step away. */}
6608
- {!statesPanelProps.canCapture && (
6609
- <MakeItRealCard
6610
- designId={designId}
6611
- featureLabel="live data captures and real-app states"
6612
- />
6613
- )}
6614
- </PanelSection>
6615
- ) : null}
6616
-
6617
7150
  {/* §6.5 Review — contextual section in Design tab.
6618
7151
  Collapsed by default. Renders when reviewPanelProps is provided,
6619
7152
  no designId check needed since ReviewPanel is statically fed. */}
@@ -6629,17 +7162,25 @@ export function EditPanel({
6629
7162
  variant="ghost"
6630
7163
  size="icon"
6631
7164
  className="size-6 rounded-md text-muted-foreground hover:text-foreground"
7165
+ disabled={reviewPanelProps.auditLoading}
7166
+ onClick={(event) => {
7167
+ event.stopPropagation();
7168
+ reviewPanelProps.onRunAudit?.();
7169
+ }}
6632
7170
  aria-label={
6633
- "Accessibility & visual diff" /* i18n-ignore design inspector action */
7171
+ "Run audit" /* i18n-ignore design inspector action */
6634
7172
  }
6635
7173
  >
6636
- <IconShieldCheck className="size-3.5" />
7174
+ <IconRefresh
7175
+ className={cn(
7176
+ "size-3.5",
7177
+ reviewPanelProps.auditLoading && "animate-spin",
7178
+ )}
7179
+ />
6637
7180
  </Button>
6638
7181
  </TooltipTrigger>
6639
7182
  <TooltipContent>
6640
- {
6641
- "Accessibility & visual diff" /* i18n-ignore design inspector action */
6642
- }
7183
+ {"Run audit" /* i18n-ignore design inspector action */}
6643
7184
  </TooltipContent>
6644
7185
  </Tooltip>
6645
7186
  }