@agent-native/core 0.80.8 → 0.80.10

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 (359) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +6 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +8 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +10 -2
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +10 -2
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +7 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +7 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +7 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +9 -2
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +6 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +6 -0
  13. package/corpus/core/docs/content/template-design.mdx +9 -2
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/cli/design-connect.ts +28 -2
  16. package/corpus/core/src/cli/mcp.ts +10 -0
  17. package/corpus/core/src/cli/skills.ts +103 -36
  18. package/corpus/core/src/client/error-format.ts +25 -0
  19. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
  20. package/corpus/core/src/client/guided-questions.tsx +32 -4
  21. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
  22. package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
  23. package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
  24. package/corpus/core/src/client/session-replay.ts +34 -0
  25. package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
  26. package/corpus/core/src/coding-tools/index.ts +1 -1
  27. package/corpus/core/src/mcp/index.ts +2 -0
  28. package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
  29. package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
  30. package/corpus/core/src/mcp-client/index.ts +9 -0
  31. package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
  32. package/corpus/core/src/onboarding/types.ts +7 -0
  33. package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
  34. package/corpus/core/src/server/agents-bundle.ts +35 -7
  35. package/corpus/core/src/styles/agent-native.css +5 -0
  36. package/corpus/core/src/templates/default/app/global.css +35 -35
  37. package/corpus/templates/analytics/AGENTS.md +6 -0
  38. package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
  39. package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
  40. package/corpus/templates/analytics/app/global.css +144 -40
  41. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
  45. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
  46. package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  48. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
  49. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
  50. package/corpus/templates/assets/app/global.css +99 -35
  51. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  52. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
  53. package/corpus/templates/assets/app/routes/library.tsx +4 -4
  54. package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
  55. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  56. package/corpus/templates/brain/app/global.css +63 -35
  57. package/corpus/templates/brain/app/routes/search.tsx +1 -1
  58. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  59. package/corpus/templates/brain/app/routes/sources.tsx +2 -2
  60. package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
  61. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
  62. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
  63. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
  64. package/corpus/templates/calendar/app/global.css +69 -35
  65. package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  66. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  67. package/corpus/templates/chat/app/global.css +35 -35
  68. package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  69. package/corpus/templates/clips/AGENTS.md +13 -0
  70. package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
  71. package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
  72. package/corpus/templates/clips/actions/view-screen.ts +2 -2
  73. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  74. package/corpus/templates/clips/app/global.css +47 -35
  75. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
  76. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
  77. package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
  79. package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
  80. package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
  81. package/corpus/templates/clips/desktop/src/app.tsx +290 -1
  82. package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
  83. package/corpus/templates/clips/desktop/src/styles.css +67 -52
  84. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
  87. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
  88. package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
  89. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
  90. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  91. package/corpus/templates/content/app/global.css +29 -15
  92. package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  93. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
  94. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  95. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
  96. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
  97. package/corpus/templates/design/AGENTS.md +20 -12
  98. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
  99. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  100. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  101. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  102. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  103. package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
  104. package/corpus/templates/design/actions/apply-design-state.ts +200 -0
  105. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  106. package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
  107. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  108. package/corpus/templates/design/actions/apply-visual-edit.ts +214 -8
  109. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  110. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  111. package/corpus/templates/design/actions/create-component.ts +447 -0
  112. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  113. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  114. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  115. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  116. package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
  117. package/corpus/templates/design/actions/generate-design.ts +6 -21
  118. package/corpus/templates/design/actions/generate-screens.ts +43 -2
  119. package/corpus/templates/design/actions/get-component-details.ts +242 -0
  120. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  121. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  122. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  123. package/corpus/templates/design/actions/get-design.ts +1 -0
  124. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  125. package/corpus/templates/design/actions/index-components.ts +258 -0
  126. package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
  127. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  128. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  129. package/corpus/templates/design/actions/list-design-states.ts +72 -0
  130. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  131. package/corpus/templates/design/actions/open-component-source.ts +237 -0
  132. package/corpus/templates/design/actions/present-design-variants.ts +355 -30
  133. package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
  134. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  135. package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
  136. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  137. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  138. package/corpus/templates/design/actions/run-design-audit.ts +421 -0
  139. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  140. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  141. package/corpus/templates/design/actions/view-screen.ts +75 -31
  142. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1738 -135
  143. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
  144. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
  145. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  146. package/corpus/templates/design/app/components/design/EditPanel.tsx +1873 -132
  147. package/corpus/templates/design/app/components/design/LayersPanel.tsx +384 -219
  148. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  149. package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
  150. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2103 -295
  151. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
  152. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
  153. package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
  154. package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
  155. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
  156. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  157. package/corpus/templates/design/app/components/design/index.ts +16 -1
  158. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +238 -137
  159. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +188 -14
  160. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +24 -5
  161. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +57 -1
  162. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  163. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  164. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
  165. package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
  166. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +12 -5
  167. package/corpus/templates/design/app/components/design/types.ts +9 -0
  168. package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
  169. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  170. package/corpus/templates/design/app/global.css +77 -49
  171. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  172. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  173. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -2
  174. package/corpus/templates/design/app/i18n/zh-TW.ts +34 -0
  175. package/corpus/templates/design/app/i18n-data.ts +314 -3
  176. package/corpus/templates/design/app/pages/DesignEditor.tsx +4929 -892
  177. package/corpus/templates/design/app/root.tsx +1 -1
  178. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  179. package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
  180. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  181. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
  182. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
  183. package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
  184. package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
  185. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  186. package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
  187. package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
  188. package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
  189. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  190. package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
  191. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  192. package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
  193. package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
  194. package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
  195. package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
  196. package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
  197. package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
  198. package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
  199. package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
  200. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
  201. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
  202. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
  203. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
  204. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
  205. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  206. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  207. package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
  208. package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
  209. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  210. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
  211. package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
  212. package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
  213. package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
  214. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
  215. package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
  216. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  217. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  218. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  219. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  220. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  221. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  222. package/corpus/templates/design/e2e/README.md +3 -3
  223. package/corpus/templates/design/e2e/global-setup.ts +11 -22
  224. package/corpus/templates/design/e2e/helpers.ts +128 -11
  225. package/corpus/templates/design/playwright.config.ts +9 -3
  226. package/corpus/templates/design/server/db/schema.ts +123 -0
  227. package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
  228. package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
  229. package/corpus/templates/design/server/plugins/db.ts +90 -0
  230. package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
  231. package/corpus/templates/design/shared/builder-app.ts +297 -0
  232. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  233. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  234. package/corpus/templates/design/shared/code-layer.ts +1141 -93
  235. package/corpus/templates/design/shared/component-model.ts +239 -0
  236. package/corpus/templates/design/shared/design-review.ts +275 -0
  237. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  238. package/corpus/templates/design/shared/design-state.ts +112 -0
  239. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  240. package/corpus/templates/design/shared/generation-session.ts +41 -0
  241. package/corpus/templates/design/shared/motion-compiler.ts +278 -0
  242. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  243. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  244. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  245. package/corpus/templates/design/shared/source-mode.ts +245 -0
  246. package/corpus/templates/dispatch/app/global.css +79 -35
  247. package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
  248. package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  249. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
  250. package/corpus/templates/forms/app/global.css +64 -35
  251. package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
  252. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
  253. package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
  254. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
  255. package/corpus/templates/macros/app/global.css +25 -9
  256. package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  257. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
  258. package/corpus/templates/mail/app/global.css +42 -36
  259. package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
  260. package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
  261. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
  262. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  263. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
  264. package/corpus/templates/plan/app/global.css +74 -42
  265. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  266. package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
  267. package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
  268. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
  269. package/corpus/templates/slides/app/global.css +49 -33
  270. package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  271. package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
  272. package/corpus/templates/videos/app/global.css +35 -35
  273. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  274. package/dist/cli/design-connect.d.ts.map +1 -1
  275. package/dist/cli/design-connect.js +28 -2
  276. package/dist/cli/design-connect.js.map +1 -1
  277. package/dist/cli/mcp.d.ts.map +1 -1
  278. package/dist/cli/mcp.js +10 -0
  279. package/dist/cli/mcp.js.map +1 -1
  280. package/dist/cli/skills.d.ts.map +1 -1
  281. package/dist/cli/skills.js +95 -33
  282. package/dist/cli/skills.js.map +1 -1
  283. package/dist/client/error-format.d.ts.map +1 -1
  284. package/dist/client/error-format.js +17 -0
  285. package/dist/client/error-format.js.map +1 -1
  286. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  287. package/dist/client/extensions/ExtensionViewer.js +3 -4
  288. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  289. package/dist/client/guided-questions.d.ts +4 -0
  290. package/dist/client/guided-questions.d.ts.map +1 -1
  291. package/dist/client/guided-questions.js +16 -7
  292. package/dist/client/guided-questions.js.map +1 -1
  293. package/dist/client/onboarding/OnboardingPanel.js +26 -1
  294. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  295. package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
  296. package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
  297. package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
  298. package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
  299. package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
  300. package/dist/client/resources/use-builtin-capabilities.js +2 -1
  301. package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
  302. package/dist/client/session-replay.d.ts.map +1 -1
  303. package/dist/client/session-replay.js +31 -0
  304. package/dist/client/session-replay.js.map +1 -1
  305. package/dist/client/sharing/ShareButton.d.ts +14 -0
  306. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  307. package/dist/client/sharing/ShareButton.js +36 -7
  308. package/dist/client/sharing/ShareButton.js.map +1 -1
  309. package/dist/coding-tools/index.js +1 -1
  310. package/dist/coding-tools/index.js.map +1 -1
  311. package/dist/mcp/index.d.ts +2 -0
  312. package/dist/mcp/index.d.ts.map +1 -1
  313. package/dist/mcp/index.js +1 -0
  314. package/dist/mcp/index.js.map +1 -1
  315. package/dist/mcp/screen-memory-stdio.d.ts +6 -0
  316. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
  317. package/dist/mcp/screen-memory-stdio.js +213 -0
  318. package/dist/mcp/screen-memory-stdio.js.map +1 -0
  319. package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
  320. package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
  321. package/dist/mcp-client/builtin-capabilities.js +10 -0
  322. package/dist/mcp-client/builtin-capabilities.js.map +1 -1
  323. package/dist/mcp-client/index.d.ts +1 -0
  324. package/dist/mcp-client/index.d.ts.map +1 -1
  325. package/dist/mcp-client/index.js +1 -0
  326. package/dist/mcp-client/index.js.map +1 -1
  327. package/dist/mcp-client/screen-memory-local.d.ts +61 -0
  328. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
  329. package/dist/mcp-client/screen-memory-local.js +336 -0
  330. package/dist/mcp-client/screen-memory-local.js.map +1 -0
  331. package/dist/observability/routes.d.ts +3 -3
  332. package/dist/onboarding/types.d.ts +7 -0
  333. package/dist/onboarding/types.d.ts.map +1 -1
  334. package/dist/onboarding/types.js.map +1 -1
  335. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  336. package/dist/server/agent-chat-plugin.js +26 -0
  337. package/dist/server/agent-chat-plugin.js.map +1 -1
  338. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  339. package/dist/server/agents-bundle.d.ts.map +1 -1
  340. package/dist/server/agents-bundle.js +22 -7
  341. package/dist/server/agents-bundle.js.map +1 -1
  342. package/dist/styles/agent-native.css +5 -0
  343. package/dist/templates/default/app/global.css +35 -35
  344. package/docs/content/locales/ar-SA/template-design.mdx +6 -0
  345. package/docs/content/locales/de-DE/template-design.mdx +8 -0
  346. package/docs/content/locales/es-ES/template-design.mdx +10 -2
  347. package/docs/content/locales/fr-FR/template-design.mdx +10 -2
  348. package/docs/content/locales/hi-IN/template-design.mdx +7 -0
  349. package/docs/content/locales/ja-JP/template-design.mdx +7 -0
  350. package/docs/content/locales/ko-KR/template-design.mdx +7 -0
  351. package/docs/content/locales/pt-BR/template-design.mdx +9 -2
  352. package/docs/content/locales/zh-CN/template-design.mdx +6 -0
  353. package/docs/content/locales/zh-TW/template-design.mdx +6 -0
  354. package/docs/content/template-design.mdx +9 -2
  355. package/package.json +1 -1
  356. package/src/templates/default/app/global.css +35 -35
  357. package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
  358. package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
  359. package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -342
@@ -1,4 +1,8 @@
1
- import { useT } from "@agent-native/core/client";
1
+ import {
2
+ useActionMutation,
3
+ useActionQuery,
4
+ useT,
5
+ } from "@agent-native/core/client";
2
6
  import type { TweakDefinition } from "@shared/api";
3
7
  import {
4
8
  alphaToOpacity,
@@ -14,13 +18,15 @@ import {
14
18
  IconAlignRight,
15
19
  IconArrowAutofitHeight,
16
20
  IconArrowAutofitWidth,
21
+ IconArrowRight,
17
22
  IconBackground,
18
23
  IconBlur,
19
24
  IconBorderStyle,
20
25
  IconBrush,
21
- IconChevronDown,
22
26
  IconCode,
23
27
  IconComponents,
28
+ IconDeviceDesktop,
29
+ IconExternalLink,
24
30
  IconDroplet,
25
31
  IconEye,
26
32
  IconEyeOff,
@@ -29,6 +35,8 @@ import {
29
35
  IconFrame,
30
36
  IconLayoutDistributeHorizontal,
31
37
  IconLayoutGrid,
38
+ IconLoader2,
39
+ IconShieldCheck,
32
40
  IconSlice,
33
41
  IconLayoutAlignBottom,
34
42
  IconLayoutAlignCenter,
@@ -40,6 +48,7 @@ import {
40
48
  IconLetterSpacing,
41
49
  IconLineHeight,
42
50
  IconLink,
51
+ IconLock,
43
52
  IconMaximize,
44
53
  IconMinus,
45
54
  IconPalette,
@@ -51,9 +60,25 @@ import {
51
60
  IconUnlink,
52
61
  IconVector,
53
62
  } from "@tabler/icons-react";
54
- import { useCallback, useEffect, useState, type ReactNode } from "react";
63
+ import { useQueryClient } from "@tanstack/react-query";
64
+ import {
65
+ useCallback,
66
+ useEffect,
67
+ useRef,
68
+ useState,
69
+ type ReactNode,
70
+ } from "react";
71
+ import { useParams } from "react-router";
55
72
 
56
73
  import { Button } from "@/components/ui/button";
74
+ import {
75
+ Dialog,
76
+ DialogContent,
77
+ DialogDescription,
78
+ DialogFooter,
79
+ DialogHeader,
80
+ DialogTitle,
81
+ } from "@/components/ui/dialog";
57
82
  import {
58
83
  DropdownMenu,
59
84
  DropdownMenuContent,
@@ -75,6 +100,7 @@ import {
75
100
  SelectValue,
76
101
  } from "@/components/ui/select";
77
102
  import { Slider } from "@/components/ui/slider";
103
+ import { Switch } from "@/components/ui/switch";
78
104
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
79
105
  import {
80
106
  Tooltip,
@@ -100,18 +126,34 @@ import {
100
126
  type AutoLayoutSizingAxis,
101
127
  type ConstraintsValue,
102
128
  type ExportSettingsValue,
129
+ imageFillToBackgroundStyles,
103
130
  type DesignFillRow,
104
131
  type DesignFillRowPatch,
105
132
  type DesignGradientStop,
106
133
  type DesignGradientStopPatch,
107
134
  type DesignGradientType,
135
+ type ImageFillValue,
108
136
  } from "./inspector";
137
+ import { IconLayoutSettings } from "./inspector/design-icons";
109
138
  import type { DesignPaintType } from "./inspector/DesignColorPicker";
139
+ import { InspectorAiActions } from "./inspector/InspectorAiActions";
140
+ import { ReviewPanel } from "./ReviewPanel";
141
+ import type { ReviewPanelProps } from "./ReviewPanel";
142
+ import { StatesPanel } from "./StatesPanel";
143
+ import type { StatesPanelProps } from "./StatesPanel";
144
+ import { TokensPanel } from "./TokensPanel";
110
145
  import { TweaksPanelContent } from "./TweaksPanel";
111
146
  import type { ElementInfo } from "./types";
112
147
 
113
148
  export type InspectorTab = "design" | "tweaks" | "extensions";
114
149
 
150
+ /**
151
+ * Sub-tab within the Tweaks area: "tokens" = TokensPanel face (CSS-var
152
+ * editing through the Tweaks loop); "tweaks" = raw TweakDefinition controls.
153
+ * Defaults to "tokens" when a designId is provided, otherwise "tweaks".
154
+ */
155
+ type TweaksSubTab = "tokens" | "tweaks";
156
+
115
157
  interface EditPanelProps {
116
158
  selectedElement: ElementInfo | null;
117
159
  pageStyles?: Record<string, string>;
@@ -127,8 +169,84 @@ interface EditPanelProps {
127
169
  onRequestTweaks?: (anchor: HTMLElement) => void;
128
170
  onStyleChange: (property: string, value: string) => void;
129
171
  onStylesChange?: (styles: Record<string, string>) => void;
172
+ onAutoLayoutConvert?: (
173
+ targetNodeId: string,
174
+ opts?: { direction?: "row" | "column"; gap?: string },
175
+ ) => void;
130
176
  onExport?: (settings: ExportSettingsValue[]) => void;
131
177
  exporting?: boolean;
178
+ readOnly?: boolean;
179
+ /** Active file id — forwarded to InspectorAiActions for agent context. */
180
+ fileId?: string;
181
+ /** Active file name (e.g. "index.html") — forwarded to InspectorAiActions. */
182
+ filename?: string;
183
+ // -------------------------------------------------------------------------
184
+ // Design Studio panels (§6.2, §6.4, §6.5)
185
+ // Pass `designId` to unlock Tokens, States, and Review sections.
186
+ // -------------------------------------------------------------------------
187
+ /** The active design's id — required to mount Tokens / States / Review. */
188
+ designId?: string;
189
+ /**
190
+ * Called after a token edit is applied so the parent can push the resolved
191
+ * CSS-var map into the iframe via the tweak-values postMessage.
192
+ */
193
+ onTokensApplied?: (resolvedCssVars: Record<string, string>) => void;
194
+ /** Props forwarded to the StatesPanel (§6.4). Requires `designId`. */
195
+ statesPanelProps?: Omit<StatesPanelProps, "designId">;
196
+ /** Props forwarded to the ReviewPanel (§6.5). */
197
+ reviewPanelProps?: Omit<ReviewPanelProps, "className">;
198
+ // -------------------------------------------------------------------------
199
+ // Component section (§6.1)
200
+ // When a component instance is selected, pass its node id here to unlock
201
+ // the contextual Component section at the top of the Design tab.
202
+ // -------------------------------------------------------------------------
203
+ /**
204
+ * The `data-agent-native-node-id` of the currently-selected component root
205
+ * element. When provided (along with `designId`), a Component section is
206
+ * shown at the top of the Design tab with name, source path, prop controls,
207
+ * and an Edit component action.
208
+ */
209
+ componentNodeId?: string;
210
+ /**
211
+ * Source capabilities for the current design. Used to gate the Edit
212
+ * component / jump-to-source affordances. When absent all writes default
213
+ * to disabled (inline / Alpine tier behaviour).
214
+ */
215
+ sourceCapabilities?: string[];
216
+ // -------------------------------------------------------------------------
217
+ // Selection header quick actions ("Create component" + "Inspect code")
218
+ // -------------------------------------------------------------------------
219
+ /**
220
+ * Promote the current selection into a reusable component. Receives the
221
+ * (already-normalized-by-the-action) component name the user typed. When
222
+ * omitted the "Create component" button is disabled.
223
+ */
224
+ onCreateComponent?: (name: string) => void;
225
+ /** Suggested default name for the create-component dialog. */
226
+ defaultComponentName?: string;
227
+ /** Code-inspection data for the "Inspect code" popover. */
228
+ inspectCode?: InspectCodeData;
229
+ }
230
+
231
+ /**
232
+ * Data backing the "Inspect code" popover. The parent resolves the selected
233
+ * node's HTML and (for real-app sources) its source file location.
234
+ */
235
+ export interface InspectCodeData {
236
+ /** Outer HTML of the selected element (inline / Alpine source). */
237
+ html?: string | null;
238
+ /**
239
+ * Resolved source file for real-app sources (localhost / fusion), when the
240
+ * resolveNodeToFile capability is available.
241
+ */
242
+ sourceLocation?: {
243
+ /** Absolute path on disk — used to build the vscode:// deep link. */
244
+ absolutePath: string;
245
+ line?: number;
246
+ column?: number;
247
+ /** Optional snippet to show above the Open-in-VS-Code button. */
248
+ snippet?: string;
249
+ } | null;
132
250
  }
133
251
 
134
252
  /**
@@ -238,6 +356,7 @@ function ColorInput({
238
356
  onChange,
239
357
  backgroundImage,
240
358
  onBackgroundImageChange,
359
+ onImageFillChange,
241
360
  blendMode,
242
361
  onBlendModeChange,
243
362
  supportsLayeredFills = false,
@@ -249,6 +368,7 @@ function ColorInput({
249
368
  onChange: (value: string) => void;
250
369
  backgroundImage?: string;
251
370
  onBackgroundImageChange?: (value: string) => void;
371
+ onImageFillChange?: (value: ImageFillValue) => void;
252
372
  blendMode?: string;
253
373
  onBlendModeChange?: (value: string) => void;
254
374
  supportsLayeredFills?: boolean;
@@ -558,9 +678,10 @@ function ColorInput({
558
678
  onPaintValueChange={
559
679
  supportsLayeredFills ? handlePaintValueChange : undefined
560
680
  }
681
+ onImageFillChange={onImageFillChange}
561
682
  blendMode={blendMode}
562
683
  onBlendModeChange={onBlendModeChange}
563
- showBlendMode={supportsLayeredFills}
684
+ showBlendMode={Boolean(onBlendModeChange)}
564
685
  fillRows={fillRows}
565
686
  selectedFillId={selectedFillId}
566
687
  onFillSelect={supportsLayeredFills ? setSelectedFillId : undefined}
@@ -912,8 +1033,6 @@ function PropSlider({
912
1033
  );
913
1034
  }
914
1035
 
915
- const EmptyScrubIcon = () => null;
916
-
917
1036
  /**
918
1037
  * design-editor inspector section. Matches the design editor "Design" panel chrome:
919
1038
  * - NO left collapse chevron (the design editor uses none).
@@ -1131,6 +1250,99 @@ const FONT_WEIGHT_OPTIONS = [
1131
1250
  { value: "900", key: "black" },
1132
1251
  ] as const;
1133
1252
 
1253
+ type TextResizeMode = "auto-width" | "auto-height" | "fixed";
1254
+
1255
+ function cleanFontFamilyName(value: string): string {
1256
+ const trimmed = value.trim();
1257
+ if (
1258
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
1259
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
1260
+ ) {
1261
+ return trimmed.slice(1, -1).trim();
1262
+ }
1263
+ return trimmed;
1264
+ }
1265
+
1266
+ function splitFontFamilyList(value: string | undefined): string[] {
1267
+ const raw = value?.trim();
1268
+ if (!raw) return [];
1269
+
1270
+ const families: string[] = [];
1271
+ let token = "";
1272
+ let quote: '"' | "'" | null = null;
1273
+
1274
+ for (let i = 0; i < raw.length; i += 1) {
1275
+ const char = raw[i];
1276
+ if ((char === '"' || char === "'") && raw[i - 1] !== "\\") {
1277
+ if (quote === char) quote = null;
1278
+ else if (!quote) quote = char;
1279
+ token += char;
1280
+ continue;
1281
+ }
1282
+ if (char === "," && !quote) {
1283
+ const cleaned = cleanFontFamilyName(token);
1284
+ if (cleaned) families.push(cleaned);
1285
+ token = "";
1286
+ continue;
1287
+ }
1288
+ token += char;
1289
+ }
1290
+
1291
+ const cleaned = cleanFontFamilyName(token);
1292
+ if (cleaned) families.push(cleaned);
1293
+ return families;
1294
+ }
1295
+
1296
+ function normalizeFontFamilyName(value: string): string {
1297
+ return cleanFontFamilyName(value).replace(/\s+/g, " ").toLowerCase();
1298
+ }
1299
+
1300
+ function normalizeFontFamilyStack(value: string): string {
1301
+ return splitFontFamilyList(value).map(normalizeFontFamilyName).join(",");
1302
+ }
1303
+
1304
+ function displayFontFamilyName(value: string | undefined): string {
1305
+ const first = splitFontFamilyList(value)[0];
1306
+ if (!first) return "Sans Serif"; // i18n-ignore design generic font label
1307
+
1308
+ const normalized = normalizeFontFamilyName(first);
1309
+ if (normalized === "sans-serif") {
1310
+ return "Sans Serif"; // i18n-ignore design generic font label
1311
+ }
1312
+ if (normalized === "serif") return "Serif"; // i18n-ignore design generic font label
1313
+ if (normalized === "monospace") {
1314
+ return "Monospace"; // i18n-ignore design generic font label
1315
+ }
1316
+ if (normalized === "system-ui" || normalized === "-apple-system") {
1317
+ return "System UI"; // i18n-ignore design generic font label
1318
+ }
1319
+ if (normalized === "blinkmacsystemfont") {
1320
+ return "Apple System"; // i18n-ignore design generic font label
1321
+ }
1322
+ return first;
1323
+ }
1324
+
1325
+ function resolveFontFamilySelectValue(value: string | undefined): string {
1326
+ const raw = value?.trim();
1327
+ if (!raw) return "sans-serif";
1328
+
1329
+ const normalizedStack = normalizeFontFamilyStack(raw);
1330
+ const exactOption = FONT_FAMILY_OPTIONS.find(
1331
+ (option) => normalizeFontFamilyStack(option.value) === normalizedStack,
1332
+ );
1333
+ if (exactOption) return exactOption.value;
1334
+
1335
+ const firstFamily = normalizeFontFamilyName(
1336
+ splitFontFamilyList(raw)[0] ?? "",
1337
+ );
1338
+ const firstFamilyOption = FONT_FAMILY_OPTIONS.find(
1339
+ (option) =>
1340
+ normalizeFontFamilyName(splitFontFamilyList(option.value)[0] ?? "") ===
1341
+ firstFamily,
1342
+ );
1343
+ return firstFamilyOption?.value ?? raw;
1344
+ }
1345
+
1134
1346
  const ALIGN_SELF_OPTIONS = [
1135
1347
  { value: "auto", key: "auto" },
1136
1348
  { value: "flex-start", key: "start" },
@@ -1282,6 +1494,8 @@ function ScrubStyleInput({
1282
1494
  step = 1,
1283
1495
  labelClassName,
1284
1496
  inputClassName,
1497
+ ariaLabel,
1498
+ tooltipLabel,
1285
1499
  hideIcon = true,
1286
1500
  }: {
1287
1501
  label: string;
@@ -1295,11 +1509,15 @@ function ScrubStyleInput({
1295
1509
  labelClassName?: string;
1296
1510
  inputClassName?: string;
1297
1511
  hideIcon?: boolean;
1512
+ ariaLabel?: string;
1513
+ tooltipLabel?: string;
1298
1514
  }) {
1299
1515
  return (
1300
1516
  <ScrubInput
1301
1517
  label={label}
1302
- icon={hideIcon ? EmptyScrubIcon : undefined}
1518
+ ariaLabel={ariaLabel}
1519
+ tooltipLabel={tooltipLabel}
1520
+ icon={hideIcon ? null : undefined}
1303
1521
  value={value ? parseNumericValue(value) : (placeholder ?? 0)}
1304
1522
  onChange={onChange}
1305
1523
  unit={unit}
@@ -1650,6 +1868,33 @@ function inferElementSizing(
1650
1868
  return "fixed";
1651
1869
  }
1652
1870
 
1871
+ /**
1872
+ * Return the element's geometric dimension on the given axis in CSS pixels.
1873
+ *
1874
+ * `getComputedStyle().width/height` always resolves to a computed px value
1875
+ * (even for `width: auto` the browser returns e.g. "200px"). For rotated
1876
+ * elements this is the pre-rotation CSS box size — what Figma shows in the
1877
+ * inspector — while `getBoundingClientRect().width/height` would be the
1878
+ * axis-aligned bounding box which is inflated by the rotation.
1879
+ *
1880
+ * Falls back to the bounding-rect dimension only when the computed style is
1881
+ * missing or unparseable (e.g. the bridge hasn't populated it yet).
1882
+ */
1883
+ function cssElementSize(
1884
+ element: ElementInfo,
1885
+ axis: AutoLayoutSizingAxis,
1886
+ ): number {
1887
+ const isHorizontal = axis === "horizontal";
1888
+ const cssValue = isHorizontal
1889
+ ? element.computedStyles.width
1890
+ : element.computedStyles.height;
1891
+ const parsed = parseFloat(cssValue || "");
1892
+ const fallback = isHorizontal
1893
+ ? element.boundingRect.width
1894
+ : element.boundingRect.height;
1895
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1896
+ }
1897
+
1653
1898
  function commitElementSizing(
1654
1899
  element: ElementInfo,
1655
1900
  axis: AutoLayoutSizingAxis,
@@ -1659,12 +1904,10 @@ function commitElementSizing(
1659
1904
  ) {
1660
1905
  const isHorizontal = axis === "horizontal";
1661
1906
  const sizeProperty = isHorizontal ? "width" : "height";
1662
- const resolvedSize = Math.max(
1663
- 1,
1664
- Math.round(
1665
- isHorizontal ? element.boundingRect.width : element.boundingRect.height,
1666
- ),
1667
- );
1907
+ // Use CSS computed dimension (pre-rotation box size) as the seed for "fixed"
1908
+ // sizing so a rotated element is locked to its actual CSS width/height rather
1909
+ // than the inflated axis-aligned bounding rect.
1910
+ const resolvedSize = Math.max(1, Math.round(cssElementSize(element, axis)));
1668
1911
  const parentDirection = parentFlexDirection(element);
1669
1912
  const isFlex = isParentFlex(element);
1670
1913
  const isGrid = isParentGrid(element);
@@ -1715,6 +1958,325 @@ function commitElementSizing(
1715
1958
  commitStylePatch(patch, onStyleChange, onStylesChange);
1716
1959
  }
1717
1960
 
1961
+ /**
1962
+ * Small modal that prompts for a component name, then promotes the current
1963
+ * selection into a reusable component via `onSubmit`.
1964
+ */
1965
+ function CreateComponentDialog({
1966
+ open,
1967
+ onOpenChange,
1968
+ defaultName,
1969
+ onSubmit,
1970
+ }: {
1971
+ open: boolean;
1972
+ onOpenChange: (open: boolean) => void;
1973
+ defaultName: string;
1974
+ onSubmit: (name: string) => void;
1975
+ }) {
1976
+ const [name, setName] = useState(defaultName);
1977
+
1978
+ // Reset the field to the freshest default each time the dialog opens.
1979
+ useEffect(() => {
1980
+ if (open) setName(defaultName);
1981
+ }, [open, defaultName]);
1982
+
1983
+ const commit = () => {
1984
+ const trimmed = name.trim();
1985
+ if (!trimmed) return;
1986
+ onSubmit(trimmed);
1987
+ onOpenChange(false);
1988
+ };
1989
+
1990
+ return (
1991
+ <Dialog open={open} onOpenChange={onOpenChange}>
1992
+ <DialogContent className="sm:max-w-sm">
1993
+ <DialogHeader>
1994
+ <DialogTitle>
1995
+ {"Create component" /* i18n-ignore design inspector action */}
1996
+ </DialogTitle>
1997
+ <DialogDescription>
1998
+ {
1999
+ "Name this element so it becomes a reusable component. The agent can then extract props and replace repeated instances." /* i18n-ignore design inspector copy */
2000
+ }
2001
+ </DialogDescription>
2002
+ </DialogHeader>
2003
+ <div className="space-y-1.5">
2004
+ <Label
2005
+ htmlFor="create-component-name"
2006
+ className="text-[11px] font-medium text-muted-foreground"
2007
+ >
2008
+ {"Component name" /* i18n-ignore design inspector label */}
2009
+ </Label>
2010
+ <Input
2011
+ id="create-component-name"
2012
+ autoFocus
2013
+ value={name}
2014
+ onChange={(e) => setName(e.target.value)}
2015
+ onKeyDown={(e) => {
2016
+ if (e.key === "Enter") {
2017
+ e.preventDefault();
2018
+ commit();
2019
+ }
2020
+ }}
2021
+ placeholder={
2022
+ "PrimaryButton" /* i18n-ignore design inspector placeholder */
2023
+ }
2024
+ />
2025
+ </div>
2026
+ <DialogFooter>
2027
+ <Button
2028
+ type="button"
2029
+ variant="ghost"
2030
+ onClick={() => onOpenChange(false)}
2031
+ >
2032
+ {"Cancel" /* i18n-ignore design inspector action */}
2033
+ </Button>
2034
+ <Button type="button" onClick={commit} disabled={!name.trim()}>
2035
+ {"Create" /* i18n-ignore design inspector action */}
2036
+ </Button>
2037
+ </DialogFooter>
2038
+ </DialogContent>
2039
+ </Dialog>
2040
+ );
2041
+ }
2042
+
2043
+ /** Build a `vscode://file/...` deep link for an absolute path + position. */
2044
+ function vscodeDeepLink(
2045
+ absolutePath: string,
2046
+ line?: number,
2047
+ column?: number,
2048
+ ): string {
2049
+ const base = `vscode://file/${absolutePath}`;
2050
+ if (line == null) return base;
2051
+ return column == null ? `${base}:${line}` : `${base}:${line}:${column}`;
2052
+ }
2053
+
2054
+ /**
2055
+ * Extract the *opening tag* from an element's outer HTML for an at-a-glance
2056
+ * summary (e.g. `<main class="hero" data-x="y">`). Self-closing tags keep
2057
+ * their `/>`. Returns `null` when no tag can be parsed.
2058
+ *
2059
+ * Pure — exported for tests.
2060
+ */
2061
+ export function openingTagOf(html: string | null | undefined): string | null {
2062
+ if (!html) return null;
2063
+ const trimmed = html.trimStart();
2064
+ // Match the first `<tag ...>` (greedy up to the first unquoted `>`), allowing
2065
+ // quoted attribute values to contain `>`.
2066
+ const match = /^<([a-zA-Z][\w-]*)((?:"[^"]*"|'[^']*'|[^>])*?)\/?>/.exec(
2067
+ trimmed,
2068
+ );
2069
+ if (!match) return null;
2070
+ return match[0];
2071
+ }
2072
+
2073
+ /**
2074
+ * Collapse long attribute values in an opening tag so the at-a-glance summary
2075
+ * stays readable. Values longer than `max` chars are truncated with an
2076
+ * ellipsis (the surrounding quotes are preserved).
2077
+ *
2078
+ * Pure — exported for tests.
2079
+ */
2080
+ export function truncateOpeningTag(openTag: string, max = 32): string {
2081
+ return openTag.replace(
2082
+ /("|')((?:\\.|(?!\1)[^\\])*)\1/g,
2083
+ (full, quote, value) => {
2084
+ if (typeof value !== "string" || value.length <= max) return full;
2085
+ return `${quote}${value.slice(0, max - 1)}…${quote}`;
2086
+ },
2087
+ );
2088
+ }
2089
+
2090
+ /**
2091
+ * Parse the top-level `key: value` pairs from an Alpine `x-data` object literal
2092
+ * (e.g. `{ variant: 'outline', size: 'lg', disabled: false }`).
2093
+ *
2094
+ * Best-effort: only handles a flat object of simple string / boolean / number
2095
+ * literals — exactly the shape used for component variant + state props. Nested
2096
+ * objects, methods, and computed expressions are ignored. Returns `null` when
2097
+ * the value is not a recognizable flat object literal.
2098
+ *
2099
+ * Pure — exported for tests.
2100
+ */
2101
+ export function parseAlpineDataObject(
2102
+ xData: string | null | undefined,
2103
+ ): Record<string, string> | null {
2104
+ if (!xData) return null;
2105
+ const trimmed = xData.trim();
2106
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
2107
+ const inner = trimmed.slice(1, -1).trim();
2108
+ if (!inner) return {};
2109
+
2110
+ const out: Record<string, string> = {};
2111
+ // Split on top-level commas only (no nesting / quotes inside values here).
2112
+ const pairRe =
2113
+ /(?:^|,)\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_$][\w$]*))\s*:\s*('[^']*'|"[^"]*"|true|false|-?\d+(?:\.\d+)?)/g;
2114
+ let m: RegExpExecArray | null;
2115
+ let matched = false;
2116
+ while ((m = pairRe.exec(inner)) !== null) {
2117
+ matched = true;
2118
+ const key = m[1] ?? m[2] ?? m[3];
2119
+ let raw = m[4]!;
2120
+ // Unwrap quotes for string literals; keep booleans / numbers verbatim.
2121
+ if (
2122
+ (raw.startsWith("'") && raw.endsWith("'")) ||
2123
+ (raw.startsWith('"') && raw.endsWith('"'))
2124
+ ) {
2125
+ raw = raw.slice(1, -1);
2126
+ }
2127
+ if (key) out[key] = raw;
2128
+ }
2129
+ // If there was content but nothing parsed, the shape is too complex to edit
2130
+ // safely — bail so the caller falls back to attribute-based prop edits.
2131
+ if (!matched) return null;
2132
+ return out;
2133
+ }
2134
+
2135
+ /**
2136
+ * Re-serialize a flat Alpine data object back into an `x-data` literal,
2137
+ * preserving boolean / number literals unquoted and single-quoting strings.
2138
+ *
2139
+ * Pure — exported for tests.
2140
+ */
2141
+ export function serializeAlpineDataObject(obj: Record<string, string>): string {
2142
+ const parts = Object.entries(obj).map(([key, value]) => {
2143
+ const isBoolean = value === "true" || value === "false";
2144
+ const isNumber = /^-?\d+(\.\d+)?$/.test(value);
2145
+ const literal =
2146
+ isBoolean || isNumber ? value : `'${value.replace(/'/g, "\\'")}'`;
2147
+ return `${key}: ${literal}`;
2148
+ });
2149
+ return parts.length ? `{ ${parts.join(", ")} }` : "{}";
2150
+ }
2151
+
2152
+ /** A boolean-ish prop value (`"true"` / `"false"`), case-insensitive. */
2153
+ export function isBooleanPropValue(value: string): boolean {
2154
+ const v = value.trim().toLowerCase();
2155
+ return v === "true" || v === "false";
2156
+ }
2157
+
2158
+ /**
2159
+ * Popover anchored to the "Inspect code" button showing the selected node's
2160
+ * code. Inline/Alpine sources show the element's outer HTML with a Copy
2161
+ * button; real-app sources additionally render an "Open in VS Code" button.
2162
+ *
2163
+ * TODO: replace the read-only <pre> with an inline Monaco editor for richer
2164
+ * (editable) code inspection once the editor bundle is wired into the inspector.
2165
+ */
2166
+ function InspectCodePopover({
2167
+ trigger,
2168
+ data,
2169
+ }: {
2170
+ trigger: ReactNode;
2171
+ data: InspectCodeData;
2172
+ }) {
2173
+ const [copied, setCopied] = useState(false);
2174
+ const html = data.html ?? "";
2175
+ const source = data.sourceLocation ?? null;
2176
+ const snippet = source?.snippet || html;
2177
+ // At-a-glance opening tag (tag name + attributes), truncated for readability.
2178
+ const openingTag = (() => {
2179
+ const raw = openingTagOf(html);
2180
+ return raw ? truncateOpeningTag(raw) : null;
2181
+ })();
2182
+
2183
+ const handleCopy = () => {
2184
+ if (!snippet) return;
2185
+ void navigator.clipboard
2186
+ ?.writeText(snippet)
2187
+ .then(() => {
2188
+ setCopied(true);
2189
+ window.setTimeout(() => setCopied(false), 1200);
2190
+ })
2191
+ .catch(() => {
2192
+ /* clipboard may be unavailable; ignore */
2193
+ });
2194
+ };
2195
+
2196
+ return (
2197
+ <Popover>
2198
+ <PopoverTrigger asChild>{trigger}</PopoverTrigger>
2199
+ <PopoverContent align="end" className="w-80 space-y-2 p-2 text-[11px]">
2200
+ <div className="flex items-center justify-between gap-2">
2201
+ <span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">
2202
+ {"Inspect code" /* i18n-ignore design inspector label */}
2203
+ </span>
2204
+ <Button
2205
+ type="button"
2206
+ variant="ghost"
2207
+ size="sm"
2208
+ className="h-6 px-2 text-[10px]"
2209
+ onClick={handleCopy}
2210
+ disabled={!snippet}
2211
+ >
2212
+ {
2213
+ copied
2214
+ ? "Copied" /* i18n-ignore design inspector action */
2215
+ : "Copy" /* i18n-ignore design inspector action */
2216
+ }
2217
+ </Button>
2218
+ </div>
2219
+
2220
+ {/* At-a-glance opening tag (tag name + attributes) above the full HTML. */}
2221
+ {openingTag && (
2222
+ <code
2223
+ className="block truncate rounded bg-[var(--design-editor-control-bg)] px-2 py-1 font-mono text-[10px] text-foreground"
2224
+ title={openingTag}
2225
+ >
2226
+ {openingTag}
2227
+ </code>
2228
+ )}
2229
+
2230
+ {source && (
2231
+ <div
2232
+ className="flex items-center gap-1 rounded bg-[var(--design-editor-control-bg)] px-2 py-1"
2233
+ title={source.absolutePath}
2234
+ >
2235
+ <IconCode className="size-3 shrink-0 text-muted-foreground/60" />
2236
+ <span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
2237
+ {source.absolutePath}
2238
+ {source.line != null ? `:${source.line}` : ""}
2239
+ </span>
2240
+ </div>
2241
+ )}
2242
+
2243
+ {snippet ? (
2244
+ <pre className="max-h-64 overflow-auto rounded bg-[var(--design-editor-control-bg)] p-2 font-mono text-[10px] leading-relaxed text-foreground">
2245
+ <code>{snippet}</code>
2246
+ </pre>
2247
+ ) : (
2248
+ <p className="px-1 py-2 text-muted-foreground">
2249
+ {
2250
+ "No source available for this element." /* i18n-ignore design inspector empty */
2251
+ }
2252
+ </p>
2253
+ )}
2254
+
2255
+ {source && (
2256
+ <a
2257
+ href={vscodeDeepLink(
2258
+ source.absolutePath,
2259
+ source.line,
2260
+ source.column,
2261
+ )}
2262
+ className="block"
2263
+ >
2264
+ <Button
2265
+ type="button"
2266
+ variant="outline"
2267
+ size="sm"
2268
+ className="h-7 w-full gap-1.5 text-[11px]"
2269
+ >
2270
+ <IconExternalLink className="size-3.5" />
2271
+ {"Open in VS Code" /* i18n-ignore design inspector action */}
2272
+ </Button>
2273
+ </a>
2274
+ )}
2275
+ </PopoverContent>
2276
+ </Popover>
2277
+ );
2278
+ }
2279
+
1718
2280
  function elementTypeIcon(element: ElementInfo) {
1719
2281
  const tag = element.tagName || "element";
1720
2282
  if (TEXT_TAGS.has(tag)) return IconTypography;
@@ -1724,35 +2286,55 @@ function elementTypeIcon(element: ElementInfo) {
1724
2286
  return IconFrame;
1725
2287
  }
1726
2288
 
1727
- function SelectionHeader({ element }: { element: ElementInfo | null }) {
2289
+ function SelectionHeader({
2290
+ element,
2291
+ onCreateComponent,
2292
+ inspectCodePopover,
2293
+ }: {
2294
+ element: ElementInfo | null;
2295
+ /** Promote the current selection into a reusable component. Omit/undefined to disable. */
2296
+ onCreateComponent?: () => void;
2297
+ /**
2298
+ * Render prop for the "Inspect code" affordance. Receives the trigger button
2299
+ * so the parent can anchor a Popover to it; returns the full popover element.
2300
+ * When omitted the button renders disabled.
2301
+ */
2302
+ inspectCodePopover?: (trigger: ReactNode) => ReactNode;
2303
+ }) {
1728
2304
  if (!element) return null;
1729
2305
 
1730
2306
  const title = inspectorObjectTitle(element);
1731
2307
  const TypeIcon = elementTypeIcon(element);
1732
2308
 
2309
+ const inspectTrigger = (
2310
+ <SectionIconButton
2311
+ label={"Inspect code" /* i18n-ignore design inspector action */}
2312
+ disabled={!inspectCodePopover}
2313
+ >
2314
+ <IconCode className="size-3.5" />
2315
+ </SectionIconButton>
2316
+ );
2317
+
1733
2318
  return (
1734
2319
  <div className="flex min-h-8 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
1735
- {/* M3 · Node-type label + rename/type dropdown affordance (▾) */}
1736
- <button
1737
- type="button"
1738
- className="flex min-w-0 items-center gap-1.5 bg-transparent text-left text-[13px] font-semibold text-foreground"
1739
- >
2320
+ {/* Node-type label. Rename lives in the layers panel and device sizing
2321
+ lives elsewhere, so this is a plain non-interactive label. */}
2322
+ <div className="flex min-w-0 items-center gap-1.5 text-left text-[13px] font-semibold text-foreground">
1740
2323
  <TypeIcon className="size-3.5 shrink-0 text-muted-foreground" />
1741
2324
  <span className="truncate">{title}</span>
1742
- <IconChevronDown className="size-2.5 shrink-0 text-muted-foreground" />
1743
- </button>
1744
- {/* M3 · Right-aligned quick actions: create-component + dev inspect (</>) */}
2325
+ </div>
2326
+ {/* Right-aligned quick actions: create-component + dev inspect (</>) */}
1745
2327
  <div className="flex shrink-0 items-center gap-0.5">
1746
2328
  <SectionIconButton
1747
2329
  label={"Create component" /* i18n-ignore design inspector action */}
2330
+ disabled={!onCreateComponent}
2331
+ onClick={onCreateComponent}
1748
2332
  >
1749
2333
  <IconComponents className="size-3.5" />
1750
2334
  </SectionIconButton>
1751
- <SectionIconButton
1752
- label={"Inspect code" /* i18n-ignore design inspector action */}
1753
- >
1754
- <IconCode className="size-3.5" />
1755
- </SectionIconButton>
2335
+ {inspectCodePopover
2336
+ ? inspectCodePopover(inspectTrigger)
2337
+ : inspectTrigger}
1756
2338
  </div>
1757
2339
  </div>
1758
2340
  );
@@ -1770,7 +2352,7 @@ function InspectorTabsHeader({
1770
2352
  const t = useT();
1771
2353
 
1772
2354
  return (
1773
- <div className="flex min-h-8 shrink-0 items-center justify-between gap-1 border-b border-border/90 px-2">
2355
+ <div className="flex min-h-8 shrink-0 items-center justify-between gap-1 border-b border-border/90 px-2 py-1">
1774
2356
  <Tabs
1775
2357
  value={activeTab}
1776
2358
  onValueChange={(value) => onActiveTabChange(value as InspectorTab)}
@@ -1912,12 +2494,113 @@ function InspectorIconButton({
1912
2494
 
1913
2495
  function InspectorSegment({ children }: { children: ReactNode }) {
1914
2496
  return (
1915
- <div className="flex min-w-0 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
2497
+ <div className="flex w-fit max-w-full min-w-0 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
1916
2498
  {children}
1917
2499
  </div>
1918
2500
  );
1919
2501
  }
1920
2502
 
2503
+ function TextResizeControls({
2504
+ resizeMode,
2505
+ onResizeModeChange,
2506
+ }: {
2507
+ resizeMode: TextResizeMode;
2508
+ onResizeModeChange: (mode: TextResizeMode) => void;
2509
+ }) {
2510
+ const t = useT();
2511
+
2512
+ return (
2513
+ <InspectorSegment>
2514
+ <InspectorIconButton
2515
+ label={t("editPanel.textResize.autoWidth")}
2516
+ active={resizeMode === "auto-width"}
2517
+ onClick={() => onResizeModeChange("auto-width")}
2518
+ >
2519
+ <IconArrowAutofitWidth className="size-3.5" />
2520
+ </InspectorIconButton>
2521
+ <InspectorIconButton
2522
+ label={t("editPanel.textResize.autoHeight")}
2523
+ active={resizeMode === "auto-height"}
2524
+ onClick={() => onResizeModeChange("auto-height")}
2525
+ >
2526
+ <IconArrowAutofitHeight className="size-3.5" />
2527
+ </InspectorIconButton>
2528
+ <InspectorIconButton
2529
+ label={t("editPanel.textResize.fixed")}
2530
+ active={resizeMode === "fixed"}
2531
+ onClick={() => onResizeModeChange("fixed")}
2532
+ >
2533
+ <IconSquare className="size-3.5" />
2534
+ </InspectorIconButton>
2535
+ </InspectorSegment>
2536
+ );
2537
+ }
2538
+
2539
+ function TypographyDetailsPopover({
2540
+ resizeMode,
2541
+ onResizeModeChange,
2542
+ }: {
2543
+ resizeMode: TextResizeMode;
2544
+ onResizeModeChange: (mode: TextResizeMode) => void;
2545
+ }) {
2546
+ const [open, setOpen] = useState(false);
2547
+
2548
+ return (
2549
+ <Popover open={open} onOpenChange={setOpen}>
2550
+ <PopoverTrigger asChild>
2551
+ <Button
2552
+ type="button"
2553
+ variant="ghost"
2554
+ size="icon"
2555
+ aria-label={"Typography details" /* i18n-ignore design action */}
2556
+ aria-pressed={open}
2557
+ className={cn(
2558
+ "h-6 min-w-6 cursor-pointer rounded-md text-muted-foreground hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground",
2559
+ open &&
2560
+ "bg-[var(--design-editor-accent-color)]/20 text-[var(--design-editor-accent-color)] hover:text-[var(--design-editor-accent-color)]",
2561
+ )}
2562
+ >
2563
+ <IconLayoutSettings className="size-3.5" />
2564
+ </Button>
2565
+ </PopoverTrigger>
2566
+ <PopoverContent
2567
+ side="left"
2568
+ align="end"
2569
+ sideOffset={8}
2570
+ className="z-[100010] w-[360px] rounded-xl border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)] p-0 text-foreground shadow-2xl"
2571
+ >
2572
+ <div className="flex items-center gap-1 border-b border-[var(--design-editor-control-border)] p-2.5">
2573
+ <div className="flex rounded-md bg-[var(--design-editor-control-bg)] p-0.5">
2574
+ <span className="rounded bg-[var(--design-editor-panel-raised-bg)] px-2.5 py-1 text-[11px] font-semibold text-foreground">
2575
+ {"Basics" /* i18n-ignore design typography details tab */}
2576
+ </span>
2577
+ <span className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
2578
+ {"Details" /* i18n-ignore design typography details tab */}
2579
+ </span>
2580
+ <span className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
2581
+ {"Variable" /* i18n-ignore design typography details tab */}
2582
+ </span>
2583
+ </div>
2584
+ </div>
2585
+ <div className="space-y-3 p-4 text-[11px]">
2586
+ <div className="flex h-20 items-center justify-center rounded-md bg-[var(--design-editor-control-bg)] text-[18px] text-muted-foreground/80">
2587
+ {"Preview" /* i18n-ignore design typography details preview */}
2588
+ </div>
2589
+ <div className="flex items-center justify-between gap-3">
2590
+ <span className="text-[11px] font-medium text-muted-foreground">
2591
+ {"Text box" /* i18n-ignore design typography details label */}
2592
+ </span>
2593
+ <TextResizeControls
2594
+ resizeMode={resizeMode}
2595
+ onResizeModeChange={onResizeModeChange}
2596
+ />
2597
+ </div>
2598
+ </div>
2599
+ </PopoverContent>
2600
+ </Popover>
2601
+ );
2602
+ }
2603
+
1921
2604
  function CornerRadiusControl({
1922
2605
  styles,
1923
2606
  onStyleChange,
@@ -2389,20 +3072,29 @@ function ShadowEffectRow({
2389
3072
  function PageProperties({
2390
3073
  styles,
2391
3074
  onStyleChange,
3075
+ onStylesChange,
2392
3076
  }: {
2393
3077
  styles: Record<string, string>;
2394
3078
  onStyleChange: (property: string, value: string) => void;
3079
+ onStylesChange?: (styles: Record<string, string>) => void;
2395
3080
  }) {
2396
3081
  const t = useT();
2397
- const fontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
3082
+ const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2398
3083
  value: option.value,
2399
3084
  label: t(`editPanel.fontFamilies.${option.key}`),
2400
3085
  }));
2401
- const fontFamily = FONT_FAMILY_OPTIONS.some(
2402
- (option) => option.value === styles.fontFamily,
3086
+ const fontFamily = resolveFontFamilySelectValue(styles.fontFamily);
3087
+ const fontFamilyOptions = FONT_FAMILY_OPTIONS.some(
3088
+ (option) => option.value === fontFamily,
2403
3089
  )
2404
- ? styles.fontFamily
2405
- : "sans-serif";
3090
+ ? baseFontFamilyOptions
3091
+ : [
3092
+ {
3093
+ value: fontFamily,
3094
+ label: displayFontFamilyName(styles.fontFamily || fontFamily),
3095
+ },
3096
+ ...baseFontFamilyOptions,
3097
+ ];
2406
3098
 
2407
3099
  return (
2408
3100
  <div>
@@ -2413,6 +3105,13 @@ function PageProperties({
2413
3105
  onChange={(v) => onStyleChange("backgroundColor", v)}
2414
3106
  backgroundImage={styles.backgroundImage || ""}
2415
3107
  onBackgroundImageChange={(v) => onStyleChange("backgroundImage", v)}
3108
+ onImageFillChange={(value) =>
3109
+ commitStylePatch(
3110
+ imageFillToBackgroundStyles(value),
3111
+ onStyleChange,
3112
+ onStylesChange,
3113
+ )
3114
+ }
2416
3115
  blendMode={styles.backgroundBlendMode || "normal"}
2417
3116
  onBlendModeChange={(v) => onStyleChange("backgroundBlendMode", v)}
2418
3117
  supportsLayeredFills
@@ -2445,10 +3144,22 @@ function TypographyProperties({
2445
3144
  }) {
2446
3145
  const t = useT();
2447
3146
  const styles = element.computedStyles;
2448
- const fontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
3147
+ const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2449
3148
  value: option.value,
2450
3149
  label: t(`editPanel.fontFamilies.${option.key}`),
2451
3150
  }));
3151
+ const fontFamily = resolveFontFamilySelectValue(styles.fontFamily);
3152
+ const fontFamilyOptions = FONT_FAMILY_OPTIONS.some(
3153
+ (option) => option.value === fontFamily,
3154
+ )
3155
+ ? baseFontFamilyOptions
3156
+ : [
3157
+ {
3158
+ value: fontFamily,
3159
+ label: displayFontFamilyName(styles.fontFamily || fontFamily),
3160
+ },
3161
+ ...baseFontFamilyOptions,
3162
+ ];
2452
3163
  const fontWeightOptions = FONT_WEIGHT_OPTIONS.map((option) => ({
2453
3164
  value: option.value,
2454
3165
  label: t(`editPanel.fontWeights.${option.key}`),
@@ -2463,7 +3174,7 @@ function TypographyProperties({
2463
3174
  !styles.width || styles.width === "auto" || styles.width === "max-content";
2464
3175
  const heightIsAuto = !styles.height || styles.height === "auto";
2465
3176
  const noWrap = styles.whiteSpace === "nowrap";
2466
- const resizeMode: "auto-width" | "auto-height" | "fixed" =
3177
+ const resizeMode: TextResizeMode =
2467
3178
  widthIsAuto && noWrap
2468
3179
  ? "auto-width"
2469
3180
  : !heightIsAuto && !widthIsAuto
@@ -2471,7 +3182,7 @@ function TypographyProperties({
2471
3182
  : "auto-height";
2472
3183
  const currentWidth = styles.width && !widthIsAuto ? styles.width : "200px";
2473
3184
  const currentHeight = styles.height && !heightIsAuto ? styles.height : "48px";
2474
- const setResizeMode = (mode: "auto-width" | "auto-height" | "fixed") => {
3185
+ const setResizeMode = (mode: TextResizeMode) => {
2475
3186
  if (mode === "auto-width") {
2476
3187
  onStyleChange("width", "auto");
2477
3188
  onStyleChange("height", "auto");
@@ -2521,51 +3232,36 @@ function TypographyProperties({
2521
3232
 
2522
3233
  return (
2523
3234
  <PanelSection title={t("editPanel.sections.typography")}>
2524
- {/* Row 0: text resizing (auto-width / auto-height / fixed) */}
2525
- <InspectorSegment>
2526
- <InspectorIconButton
2527
- label={"Auto width" /* i18n-ignore design text-resize mode */}
2528
- active={resizeMode === "auto-width"}
2529
- onClick={() => setResizeMode("auto-width")}
2530
- >
2531
- <IconArrowAutofitWidth className="size-3.5" />
2532
- </InspectorIconButton>
2533
- <InspectorIconButton
2534
- label={"Auto height" /* i18n-ignore design text-resize mode */}
2535
- active={resizeMode === "auto-height"}
2536
- onClick={() => setResizeMode("auto-height")}
2537
- >
2538
- <IconArrowAutofitHeight className="size-3.5" />
2539
- </InspectorIconButton>
2540
- <InspectorIconButton
2541
- label={"Fixed size" /* i18n-ignore design text-resize mode */}
2542
- active={resizeMode === "fixed"}
2543
- onClick={() => setResizeMode("fixed")}
3235
+ {/* Row 1: font family full-width.
3236
+ Wrapped in a height-constrained div so the SelectTrigger button's
3237
+ hit-target is exactly h-6 (24 px) and cannot visually or physically
3238
+ overlap the weight/size row below (bug: trigger extended ~12 px into
3239
+ the next row, causing clicks meant for the size input to open this
3240
+ dropdown instead). */}
3241
+ <div className="h-6 overflow-hidden">
3242
+ <Select
3243
+ value={fontFamily}
3244
+ onValueChange={(v) => onStyleChange("fontFamily", v)}
2544
3245
  >
2545
- <IconSquare className="size-3.5" />
2546
- </InspectorIconButton>
2547
- </InspectorSegment>
2548
-
2549
- {/* Row 1: font family full-width */}
2550
- <Select
2551
- value={styles.fontFamily || "sans-serif"}
2552
- onValueChange={(v) => onStyleChange("fontFamily", v)}
2553
- >
2554
- <SelectTrigger 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)]">
2555
- <SelectValue />
2556
- </SelectTrigger>
2557
- <SelectContent>
2558
- {fontFamilyOptions.map((opt) => (
2559
- <SelectItem
2560
- key={opt.value}
2561
- value={opt.value}
2562
- className="text-[11px]"
2563
- >
2564
- {opt.label}
2565
- </SelectItem>
2566
- ))}
2567
- </SelectContent>
2568
- </Select>
3246
+ <SelectTrigger
3247
+ aria-label={t("editPanel.labels.font")}
3248
+ 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)]"
3249
+ >
3250
+ <SelectValue />
3251
+ </SelectTrigger>
3252
+ <SelectContent>
3253
+ {fontFamilyOptions.map((opt) => (
3254
+ <SelectItem
3255
+ key={opt.value}
3256
+ value={opt.value}
3257
+ className="text-[11px]"
3258
+ >
3259
+ {opt.label}
3260
+ </SelectItem>
3261
+ ))}
3262
+ </SelectContent>
3263
+ </Select>
3264
+ </div>
2569
3265
 
2570
3266
  {/* Row 2: weight + size side by side */}
2571
3267
  <div className="grid grid-cols-2 gap-1.5">
@@ -2639,7 +3335,7 @@ function TypographyProperties({
2639
3335
  </div>
2640
3336
 
2641
3337
  {/* Row 4: horizontal + vertical text alignment */}
2642
- <div className="grid grid-cols-2 gap-1.5">
3338
+ <div className="flex items-center gap-1.5">
2643
3339
  <InspectorSegment>
2644
3340
  <InspectorIconButton
2645
3341
  label={t("editPanel.textAligns.left")}
@@ -2693,6 +3389,12 @@ function TypographyProperties({
2693
3389
  <IconLayoutAlignBottom className="size-3.5" />
2694
3390
  </InspectorIconButton>
2695
3391
  </InspectorSegment>
3392
+ <div className="ml-auto shrink-0">
3393
+ <TypographyDetailsPopover
3394
+ resizeMode={resizeMode}
3395
+ onResizeModeChange={setResizeMode}
3396
+ />
3397
+ </div>
2696
3398
  </div>
2697
3399
  </PanelSection>
2698
3400
  );
@@ -2703,10 +3405,15 @@ function FlexContainerControls({
2703
3405
  element,
2704
3406
  onStyleChange,
2705
3407
  onStylesChange,
3408
+ onAutoLayoutConvert,
2706
3409
  }: {
2707
3410
  element: ElementInfo;
2708
3411
  onStyleChange: (property: string, value: string) => void;
2709
3412
  onStylesChange?: (styles: Record<string, string>) => void;
3413
+ onAutoLayoutConvert?: (
3414
+ targetNodeId: string,
3415
+ opts?: { direction?: "row" | "column"; gap?: string },
3416
+ ) => void;
2710
3417
  }) {
2711
3418
  const t = useT();
2712
3419
  const styles = element.computedStyles;
@@ -2719,12 +3426,55 @@ function FlexContainerControls({
2719
3426
  const isFlex = display.includes("flex");
2720
3427
  const flexDirection: AutoLayoutMatrixValue["direction"] =
2721
3428
  styles.flexDirection?.includes("column") ? "vertical" : "horizontal";
3429
+ const mainGapAxis =
3430
+ flexDirection === "horizontal" ? "horizontal" : "vertical";
2722
3431
  // When the element is in normal flow (not flex yet), picking any flow option
2723
3432
  // must first turn it into a flex container; otherwise setting flex-direction
2724
3433
  // alone is a no-op against a block element.
2725
3434
  const ensureFlex = () => {
2726
3435
  if (!isFlex) onStyleChange("display", "flex");
2727
3436
  };
3437
+
3438
+ /**
3439
+ * Handle the Flow control switching between flex and normal-flow (block).
3440
+ *
3441
+ * For 'flex': ensures display:flex is set (ensureFlex path).
3442
+ * For 'block': sets display:block and leaves children unchanged — mirrors
3443
+ * the { kind:"autoLayout", enabled:false } substrate intent exactly.
3444
+ */
3445
+ const handleDisplayChange = (nextDisplay: "flex" | "block") => {
3446
+ if (nextDisplay === "flex") {
3447
+ ensureFlex();
3448
+ return;
3449
+ }
3450
+ // Turn auto-layout off: set display:block, leaving children unchanged.
3451
+ // This is the direct equivalent of the autoLayout substrate with enabled:false.
3452
+ onStyleChange("display", "block");
3453
+ };
3454
+
3455
+ /**
3456
+ * "Convert to auto layout" — enables flex on a non-flex container and strips
3457
+ * position:absolute from direct children (full reflow via the autoLayout
3458
+ * substrate intent). Falls back to a style-only patch when no substrate
3459
+ * handler is wired (e.g. read-only or non-editable contexts).
3460
+ */
3461
+ const handleConvertToAutoLayout = () => {
3462
+ if (onAutoLayoutConvert && element.sourceId) {
3463
+ onAutoLayoutConvert(element.sourceId, { direction: "row", gap: "8px" });
3464
+ return;
3465
+ }
3466
+ // Fallback: set display:flex on the parent only (no child strip).
3467
+ commitStylePatch(
3468
+ {
3469
+ display: "flex",
3470
+ flexDirection: "row",
3471
+ gap: "8px",
3472
+ alignItems: "flex-start",
3473
+ },
3474
+ onStyleChange,
3475
+ onStylesChange,
3476
+ );
3477
+ };
2728
3478
  const padding = {
2729
3479
  top: parseNumericValue(styles.paddingTop || "0"),
2730
3480
  right: parseNumericValue(styles.paddingRight || "0"),
@@ -2758,21 +3508,36 @@ function FlexContainerControls({
2758
3508
  },
2759
3509
  clipContent: styles.overflow === "hidden",
2760
3510
  resolvedSize: {
2761
- horizontal: element.boundingRect.width,
2762
- vertical: element.boundingRect.height,
3511
+ horizontal: cssElementSize(element, "horizontal"),
3512
+ vertical: cssElementSize(element, "vertical"),
2763
3513
  },
2764
3514
  // Forward the raw CSS display so the matrix can render the correct Flow
2765
3515
  // state (normal flow for block/grid, flex for flex/inline-flex). Added as an
2766
3516
  // optional contract field consumed by AutoLayoutMatrix; harmless when the
2767
3517
  // matrix ignores it.
2768
3518
  ...({ display } as Partial<AutoLayoutMatrixValue>),
3519
+ spaceBetween: styles.justifyContent === "space-between",
2769
3520
  };
2770
3521
 
2771
3522
  return (
2772
3523
  <div className="space-y-2">
2773
- <AutoLayoutMatrix
2774
- value={autoLayoutValue}
2775
- onDirectionChange={(direction) => {
3524
+ {/* Convert to auto layout — shown when the container is not yet flex */}
3525
+ {!isFlex ? (
3526
+ <Button
3527
+ type="button"
3528
+ variant="outline"
3529
+ size="sm"
3530
+ className="h-7 w-full gap-1.5 text-[11px]"
3531
+ onClick={handleConvertToAutoLayout}
3532
+ >
3533
+ <IconLayoutSettings className="size-3.5 shrink-0" />
3534
+ {"Convert to auto layout" /* i18n-ignore design inspector action */}
3535
+ </Button>
3536
+ ) : null}
3537
+ <AutoLayoutMatrix
3538
+ value={autoLayoutValue}
3539
+ onDisplayChange={handleDisplayChange}
3540
+ onDirectionChange={(direction) => {
2776
3541
  ensureFlex();
2777
3542
  onStyleChange(
2778
3543
  "flexDirection",
@@ -2823,16 +3588,20 @@ function FlexContainerControls({
2823
3588
  onStyleChange("overflow", clipContent ? "hidden" : "visible")
2824
3589
  }
2825
3590
  onDistribute={(axis) => {
2826
- const mainAxis =
2827
- autoLayoutValue.direction === "horizontal"
2828
- ? "horizontal"
2829
- : "vertical";
2830
- if (axis === mainAxis) {
3591
+ if (axis === mainGapAxis) {
2831
3592
  onStyleChange("justifyContent", "space-between");
2832
3593
  } else if (autoLayoutValue.wrap === "wrap") {
2833
3594
  onStyleChange("alignContent", "space-between");
2834
3595
  }
2835
3596
  }}
3597
+ onGapModeChange={(gapMode, axis) => {
3598
+ if (axis !== mainGapAxis) return;
3599
+ ensureFlex();
3600
+ onStyleChange(
3601
+ "justifyContent",
3602
+ gapMode === "auto" ? "space-between" : "flex-start",
3603
+ );
3604
+ }}
2836
3605
  availableChildSizing={availableSizingForElement(element)}
2837
3606
  onChildSizingChange={(axis, sizing) => {
2838
3607
  commitElementSizing(
@@ -2843,6 +3612,9 @@ function FlexContainerControls({
2843
3612
  onStylesChange,
2844
3613
  );
2845
3614
  }}
3615
+ onChildSizeChange={(axis, px) =>
3616
+ onStyleChange(axis === "horizontal" ? "width" : "height", `${px}px`)
3617
+ }
2846
3618
  onChildMinMaxChange={(axis, kind, val) =>
2847
3619
  commitElementMinMax(axis, kind, val, onStyleChange)
2848
3620
  }
@@ -2948,10 +3720,15 @@ function LayoutContextProperties({
2948
3720
  element,
2949
3721
  onStyleChange,
2950
3722
  onStylesChange,
3723
+ onAutoLayoutConvert,
2951
3724
  }: {
2952
3725
  element: ElementInfo;
2953
3726
  onStyleChange: (property: string, value: string) => void;
2954
3727
  onStylesChange?: (styles: Record<string, string>) => void;
3728
+ onAutoLayoutConvert?: (
3729
+ targetNodeId: string,
3730
+ opts?: { direction?: "row" | "column"; gap?: string },
3731
+ ) => void;
2955
3732
  }) {
2956
3733
  const t = useT();
2957
3734
  const flexChild = isParentFlex(element);
@@ -2986,7 +3763,7 @@ function LayoutContextProperties({
2986
3763
  axis="W"
2987
3764
  sizingAxis="horizontal"
2988
3765
  value={inferElementSizing(element, "horizontal")}
2989
- resolvedSize={element.boundingRect.width}
3766
+ resolvedSize={cssElementSize(element, "horizontal")}
2990
3767
  minMax={readElementMinMax(element, "horizontal")}
2991
3768
  options={availableSizing.horizontal ?? ["fixed"]}
2992
3769
  disabled={false}
@@ -2999,6 +3776,7 @@ function LayoutContextProperties({
2999
3776
  onStylesChange,
3000
3777
  )
3001
3778
  }
3779
+ onSizeChange={(px) => onStyleChange("width", `${px}px`)}
3002
3780
  onMinMaxChange={(axis, kind, val) =>
3003
3781
  commitElementMinMax(axis, kind, val, onStyleChange)
3004
3782
  }
@@ -3007,7 +3785,7 @@ function LayoutContextProperties({
3007
3785
  axis="H"
3008
3786
  sizingAxis="vertical"
3009
3787
  value={inferElementSizing(element, "vertical")}
3010
- resolvedSize={element.boundingRect.height}
3788
+ resolvedSize={cssElementSize(element, "vertical")}
3011
3789
  minMax={readElementMinMax(element, "vertical")}
3012
3790
  options={availableSizing.vertical ?? ["fixed"]}
3013
3791
  disabled={false}
@@ -3020,6 +3798,7 @@ function LayoutContextProperties({
3020
3798
  onStylesChange,
3021
3799
  )
3022
3800
  }
3801
+ onSizeChange={(px) => onStyleChange("height", `${px}px`)}
3023
3802
  onMinMaxChange={(axis, kind, val) =>
3024
3803
  commitElementMinMax(axis, kind, val, onStyleChange)
3025
3804
  }
@@ -3042,6 +3821,7 @@ function LayoutContextProperties({
3042
3821
  element={element}
3043
3822
  onStyleChange={onStyleChange}
3044
3823
  onStylesChange={onStylesChange}
3824
+ onAutoLayoutConvert={onAutoLayoutConvert}
3045
3825
  />
3046
3826
  {childControls}
3047
3827
  </PanelSection>
@@ -3372,6 +4152,8 @@ function PositionLayoutProperties({
3372
4152
  <div className="grid grid-cols-2 gap-2">
3373
4153
  <ScrubStyleInput
3374
4154
  label="X"
4155
+ ariaLabel="X-position"
4156
+ tooltipLabel="X-position"
3375
4157
  value={styles.left || ""}
3376
4158
  placeholder={element.boundingRect.x}
3377
4159
  inputClassName="h-6"
@@ -3379,6 +4161,8 @@ function PositionLayoutProperties({
3379
4161
  />
3380
4162
  <ScrubStyleInput
3381
4163
  label="Y"
4164
+ ariaLabel="Y-position"
4165
+ tooltipLabel="Y-position"
3382
4166
  value={styles.top || ""}
3383
4167
  placeholder={element.boundingRect.y}
3384
4168
  inputClassName="h-6"
@@ -3474,7 +4258,7 @@ function FillProperties({
3474
4258
  Record<string, string>
3475
4259
  >({});
3476
4260
  const stashKey = `${elementIdentityKey(element)}:${fillProperty}`;
3477
- const isHidden = fillValue === "transparent" || fillValue === "";
4261
+ const isHidden = !colorHasVisibleAlpha(fillValue);
3478
4262
  const handleFillVisibilityToggle = () => {
3479
4263
  if (isHidden) {
3480
4264
  // Restore the stashed color, or fall back to a sensible default.
@@ -3629,8 +4413,17 @@ function FillProperties({
3629
4413
  const opacity = gradient
3630
4414
  ? averageGradientOpacity(gradient.stops)
3631
4415
  : 100;
4416
+ const layerStashKey = `${elementIdentityKey(element)}:layer:${index}`;
4417
+ const stashedLayer = hiddenLayerStash[layerStashKey];
4418
+ const hiddenImagePlaceholder = Boolean(
4419
+ stashedLayer && gradient && opacity <= 0,
4420
+ );
3632
4421
  const label = gradient
3633
- ? `${gradientLabel(gradient.type)} ${index + 1}`
4422
+ ? hiddenImagePlaceholder
4423
+ ? `${"Image" /* i18n-ignore design inspector paint row */} ${
4424
+ index + 1
4425
+ }`
4426
+ : `${gradientLabel(gradient.type)} ${index + 1}`
3634
4427
  : `${"Image" /* i18n-ignore design inspector paint row */} ${
3635
4428
  index + 1
3636
4429
  }`;
@@ -3728,10 +4521,6 @@ function FillProperties({
3728
4521
  : t("editPanel.labels.hideLayer")
3729
4522
  }
3730
4523
  onClick={() => {
3731
- if (!gradient) return;
3732
- const layerStashKey = `${elementIdentityKey(
3733
- element,
3734
- )}:layer:${index}`;
3735
4524
  if (opacity <= 0) {
3736
4525
  // Show: restore the exact pre-hide layer if stashed,
3737
4526
  // otherwise fall back to forcing every stop opaque.
@@ -3743,7 +4532,7 @@ function FillProperties({
3743
4532
  delete next[layerStashKey];
3744
4533
  return next;
3745
4534
  });
3746
- } else {
4535
+ } else if (gradient) {
3747
4536
  replaceLayer(
3748
4537
  buildGradientLayer(
3749
4538
  gradient.type,
@@ -3755,6 +4544,29 @@ function FillProperties({
3755
4544
  ),
3756
4545
  );
3757
4546
  }
4547
+ return;
4548
+ }
4549
+ if (!gradient) {
4550
+ setHiddenLayerStash((prev) => ({
4551
+ ...prev,
4552
+ [layerStashKey]: layer,
4553
+ }));
4554
+ replaceLayer(
4555
+ buildGradientLayer("linear", [
4556
+ {
4557
+ id: "stop-0",
4558
+ color: "rgba(0, 0, 0, 0)",
4559
+ position: 0,
4560
+ opacity: 0,
4561
+ },
4562
+ {
4563
+ id: "stop-1",
4564
+ color: "rgba(0, 0, 0, 0)",
4565
+ position: 100,
4566
+ opacity: 0,
4567
+ },
4568
+ ]),
4569
+ );
3758
4570
  } else {
3759
4571
  // Hide: stash the current layer (with its real per-stop
3760
4572
  // opacities) before zeroing every stop's alpha.
@@ -4357,6 +5169,580 @@ const TEXT_TAGS = new Set([
4357
5169
  "li",
4358
5170
  ]);
4359
5171
 
5172
+ // ─── Make it real — inline upgrade card (§3, §6.6) ──────────────────────────
5173
+
5174
+ /**
5175
+ * Payload shape returned by `connect-builder-app`. Only the fields used by
5176
+ * the card UI are typed here; the action may return additional fields.
5177
+ */
5178
+ interface ConnectBuilderAppResult {
5179
+ connected: boolean;
5180
+ builderEnabled: boolean;
5181
+ connectUrl: string;
5182
+ appHost: string;
5183
+ branchProjectId?: string;
5184
+ cta: {
5185
+ kind: "connect-builder" | "configure-project";
5186
+ label: string;
5187
+ description: string;
5188
+ primaryAction: string;
5189
+ connectUrl: string;
5190
+ } | null;
5191
+ message: string;
5192
+ }
5193
+
5194
+ /**
5195
+ * Inline "Make it real" upgrade card.
5196
+ *
5197
+ * Rendered wherever a real-app-only control is reached on an inline design
5198
+ * (Component source jump, token write-back, live captures, etc.). Queries
5199
+ * `connect-builder-app` to determine the current connection state, then
5200
+ * offers the appropriate CTA:
5201
+ *
5202
+ * - Not connected → "Connect Builder.io" button (opens connectUrl)
5203
+ * - Connected, no project → "Open Builder settings" (configure project ID)
5204
+ * - Fully enabled → "Make it real" button (calls migrate-inline-design-to-app)
5205
+ *
5206
+ * The card is progressively disclosed: it only mounts when a gated control is
5207
+ * actually reached, so it never appears for users who are already on a real-app
5208
+ * source (`localhost` / `fusion`) or whose `sourceCapabilities` already include
5209
+ * the needed capability.
5210
+ *
5211
+ * Matches the design-editor panel chrome: dashed-border, accent tint, small
5212
+ * text at 10px — same idiom as the existing `ctaRequired` block in
5213
+ * ComponentSection.
5214
+ */
5215
+ function MakeItRealCard({
5216
+ designId,
5217
+ featureLabel,
5218
+ }: {
5219
+ /** The active design id — required to call connect-builder-app. */
5220
+ designId: string;
5221
+ /**
5222
+ * Short human-readable label for the gated feature (e.g. "token write-back",
5223
+ * "component source jump", "live captures"). Shown in the card body so the
5224
+ * user understands exactly what they're unlocking.
5225
+ */
5226
+ featureLabel: string;
5227
+ }) {
5228
+ const { data, isLoading } = useActionQuery<ConnectBuilderAppResult>(
5229
+ "connect-builder-app",
5230
+ { designId },
5231
+ );
5232
+
5233
+ const migrateMutation = useActionMutation("migrate-inline-design-to-app");
5234
+
5235
+ // While fetching status, show a muted placeholder that matches the card
5236
+ // height so the inspector doesn't jump when the data arrives.
5237
+ if (isLoading || !data) {
5238
+ return (
5239
+ <div className="rounded-md border border-dashed border-[var(--design-editor-accent-color)]/20 bg-[var(--design-editor-accent-color)]/3 px-2.5 py-2">
5240
+ <div className="h-3 w-32 animate-pulse rounded bg-muted/40" />
5241
+ </div>
5242
+ );
5243
+ }
5244
+
5245
+ // Determine which CTA to show.
5246
+ const cta = data.cta;
5247
+
5248
+ // Already fully enabled — no CTA needed (caller should already have gated
5249
+ // this component away, but guard here for safety).
5250
+ if (!cta) return null;
5251
+
5252
+ const isPending = migrateMutation.isPending;
5253
+ const migrateError = migrateMutation.error;
5254
+
5255
+ // "Make it real" primary action: open the connect URL or migrate.
5256
+ const handlePrimary = () => {
5257
+ if (cta.kind === "connect-builder") {
5258
+ // Open the Builder OAuth connect flow in a new tab. The user completes
5259
+ // it there and comes back; the card will re-query on next render.
5260
+ window.open(cta.connectUrl, "_blank", "noopener,noreferrer");
5261
+ return;
5262
+ }
5263
+ if (cta.kind === "configure-project") {
5264
+ window.open(cta.connectUrl, "_blank", "noopener,noreferrer");
5265
+ return;
5266
+ }
5267
+ };
5268
+
5269
+ const handleMigrate = () => {
5270
+ migrateMutation.mutate({ designId });
5271
+ };
5272
+
5273
+ // Migration result — show branch link.
5274
+ const migrateResult = migrateMutation.data as
5275
+ | {
5276
+ status: "processing";
5277
+ branchName?: string;
5278
+ url?: string;
5279
+ message?: string;
5280
+ }
5281
+ | undefined;
5282
+
5283
+ if (migrateResult?.status === "processing" && migrateResult.url) {
5284
+ return (
5285
+ <div className="rounded-md border border-[var(--design-editor-accent-color)]/30 bg-[var(--design-editor-accent-color)]/8 px-2.5 py-2 space-y-1.5">
5286
+ <p className="text-[10px] font-semibold text-[var(--design-editor-accent-color)]">
5287
+ {"Builder is working on it" /* i18n-ignore make-it-real card */}
5288
+ </p>
5289
+ <p className="text-[10px] leading-snug text-muted-foreground">
5290
+ {migrateResult.message ??
5291
+ `Branch "${migrateResult.branchName}" is being generated.`}
5292
+ </p>
5293
+ <a
5294
+ href={migrateResult.url}
5295
+ target="_blank"
5296
+ rel="noopener noreferrer"
5297
+ className="inline-flex items-center gap-1 text-[10px] font-medium text-[var(--design-editor-accent-color)] hover:underline"
5298
+ >
5299
+ {"Open in Builder" /* i18n-ignore make-it-real card */}
5300
+ <IconExternalLink className="size-2.5" />
5301
+ </a>
5302
+ </div>
5303
+ );
5304
+ }
5305
+
5306
+ return (
5307
+ <div className="rounded-md border border-dashed border-[var(--design-editor-accent-color)]/30 bg-[var(--design-editor-accent-color)]/5 px-2.5 py-2 space-y-1.5">
5308
+ {/* Header */}
5309
+ <p className="text-[10px] font-semibold text-foreground">
5310
+ {"Make this a real app" /* i18n-ignore make-it-real card */}
5311
+ </p>
5312
+
5313
+ {/* Body */}
5314
+ <p className="text-[10px] leading-snug text-muted-foreground">
5315
+ {cta.description}
5316
+ {featureLabel ? (
5317
+ <>
5318
+ {" "}
5319
+ <span className="italic">{featureLabel}</span>
5320
+ {" requires a real-app source."}
5321
+ </>
5322
+ ) : null}
5323
+ </p>
5324
+
5325
+ {/* Error */}
5326
+ {migrateError ? (
5327
+ <p className="text-[10px] text-destructive">
5328
+ {migrateError instanceof Error
5329
+ ? migrateError.message
5330
+ : "Migration failed. Please try again."}
5331
+ </p>
5332
+ ) : null}
5333
+
5334
+ {/* Actions */}
5335
+ <div className="flex items-center gap-1.5 flex-wrap">
5336
+ <button
5337
+ type="button"
5338
+ onClick={handlePrimary}
5339
+ className="inline-flex items-center gap-1 rounded px-2 py-1 text-[10px] font-medium bg-[var(--design-editor-accent-color)] text-white hover:opacity-90 transition-opacity"
5340
+ >
5341
+ {cta.primaryAction}
5342
+ <IconArrowRight className="size-2.5" />
5343
+ </button>
5344
+
5345
+ {/* When Builder is fully connected, also offer direct migration */}
5346
+ {data.connected && data.builderEnabled && (
5347
+ <button
5348
+ type="button"
5349
+ onClick={handleMigrate}
5350
+ disabled={isPending}
5351
+ className="inline-flex items-center gap-1 rounded border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 py-1 text-[10px] font-medium text-muted-foreground hover:text-foreground transition-colors disabled:cursor-wait disabled:opacity-60"
5352
+ >
5353
+ {isPending ? (
5354
+ <>
5355
+ <IconLoader2 className="size-2.5 animate-spin" />
5356
+ {"Migrating…" /* i18n-ignore make-it-real card */}
5357
+ </>
5358
+ ) : (
5359
+ <>{"Generate React app" /* i18n-ignore make-it-real card */}</>
5360
+ )}
5361
+ </button>
5362
+ )}
5363
+ </div>
5364
+ </div>
5365
+ );
5366
+ }
5367
+
5368
+ // ─── Component section (§6.1) ─────────────────────────────────────────────────
5369
+
5370
+ /**
5371
+ * Shape returned by `get-component-details`. Only the fields the UI needs are
5372
+ * typed here; the action may return additional fields.
5373
+ */
5374
+ interface ComponentDetailsResult {
5375
+ name: string;
5376
+ sourceType: string;
5377
+ observedProps: Array<{ name: string; value: string }>;
5378
+ persistedVariants: Record<string, string[]>;
5379
+ sourceLocation?: { filePath: string; exportName?: string } | null;
5380
+ /** Component instance shape, including the Alpine `x-data` expression. */
5381
+ instance?: {
5382
+ alpineData?: string | null;
5383
+ nodeId?: string;
5384
+ selector?: string;
5385
+ } | null;
5386
+ capabilities: {
5387
+ canResolveToFile: boolean;
5388
+ hasFullIndex: boolean;
5389
+ canEditProps: boolean;
5390
+ ctaRequired: boolean;
5391
+ ctaMessage?: string;
5392
+ };
5393
+ }
5394
+
5395
+ /**
5396
+ * Contextual COMPONENT section rendered inside the Design tab when the
5397
+ * selected element is a component instance (carries
5398
+ * `data-agent-native-component`).
5399
+ *
5400
+ * Shows: component name, source path (when capability available), observed
5401
+ * prop values, variant/size/state controls from `get-component-details`, and
5402
+ * an "Edit component source" action. Real-app features are gated by the
5403
+ * capabilities returned by the action; Alpine gets a lightweight read-only
5404
+ * view plus a Connect-Builder CTA.
5405
+ *
5406
+ * Matches the workbench artboard spec in DESIGN-STUDIO-PLAN.md §6.1.
5407
+ */
5408
+ function ComponentSection({
5409
+ designId,
5410
+ nodeId,
5411
+ sourceCapabilities = [],
5412
+ }: {
5413
+ designId: string;
5414
+ nodeId: string;
5415
+ /** Capability names advertised by the current source. */
5416
+ sourceCapabilities?: string[];
5417
+ }) {
5418
+ const queryClient = useQueryClient();
5419
+ const detailsKey = ["action", "get-component-details", { designId, nodeId }];
5420
+
5421
+ const { data, isLoading, error } = useActionQuery<ComponentDetailsResult>(
5422
+ "get-component-details",
5423
+ { designId, nodeId },
5424
+ );
5425
+
5426
+ const openSourceMutation = useActionMutation("open-component-source");
5427
+ const applyPropMutation = useActionMutation("apply-component-prop-edit");
5428
+
5429
+ // Persist a single prop change through apply-component-prop-edit. The canvas
5430
+ // re-renders from the freshly written design content (get-design refetch),
5431
+ // so no postMessage into the iframe is needed. The get-component-details
5432
+ // cache is optimistically patched so the control reflects the new value
5433
+ // immediately, then reconciled with the server on settle.
5434
+ const persistPropEdit = (
5435
+ edit:
5436
+ | { kind: "alpineData"; value: string }
5437
+ | { kind: "attribute"; attribute: string; value: string },
5438
+ optimistic: (prev: ComponentDetailsResult) => ComponentDetailsResult,
5439
+ ) => {
5440
+ queryClient.setQueryData<ComponentDetailsResult>(detailsKey, (prev) =>
5441
+ prev ? optimistic(prev) : prev,
5442
+ );
5443
+ applyPropMutation.mutate(
5444
+ { designId, nodeId, edit },
5445
+ {
5446
+ onSettled: () => {
5447
+ // Re-render the canvas from the new content and re-read the props.
5448
+ void queryClient.invalidateQueries({
5449
+ queryKey: ["action", "get-design"],
5450
+ });
5451
+ void queryClient.invalidateQueries({ queryKey: detailsKey });
5452
+ },
5453
+ },
5454
+ );
5455
+ };
5456
+
5457
+ // While loading, show a compact skeleton that matches the section width.
5458
+ if (isLoading) {
5459
+ return (
5460
+ <section className="shrink-0 border-t border-[var(--design-editor-control-border)] first:border-t-0">
5461
+ <div className="flex min-h-9 items-center gap-2 px-3">
5462
+ <div className="h-3 w-24 animate-pulse rounded bg-muted/50" />
5463
+ </div>
5464
+ <div className="space-y-1.5 px-3 pb-3 pt-0.5">
5465
+ <div className="h-5 w-full animate-pulse rounded bg-muted/40" />
5466
+ <div className="h-5 w-3/4 animate-pulse rounded bg-muted/40" />
5467
+ </div>
5468
+ </section>
5469
+ );
5470
+ }
5471
+
5472
+ // Hard error (node not found, no access, etc.) — collapse silently so
5473
+ // the rest of the inspector is not disrupted.
5474
+ if (error || !data) return null;
5475
+
5476
+ const {
5477
+ name,
5478
+ sourceType,
5479
+ sourceLocation,
5480
+ observedProps,
5481
+ persistedVariants,
5482
+ instance,
5483
+ capabilities,
5484
+ } = data;
5485
+
5486
+ // ── Editable prop model ───────────────────────────────────────────────────
5487
+ // Inline/Alpine designs persist through apply-component-prop-edit. Two write
5488
+ // surfaces:
5489
+ // • x-data keys → kind "alpineData" (rewrites the whole object)
5490
+ // • data-prop-* attrs → kind "attribute" (data-agent-native-prop-<kebab>)
5491
+ // Real-app sources keep the deeper source-prop controls gated as-is, so for
5492
+ // non-inline sources the controls are read-only here.
5493
+ const isInline = sourceType === "inline";
5494
+ const editingEnabled = isInline; // gated; real-app stays read-only for now
5495
+ const alpineData = parseAlpineDataObject(instance?.alpineData);
5496
+
5497
+ // Each editable row: name + current value + how it persists + its options.
5498
+ type PropRow = {
5499
+ name: string;
5500
+ value: string;
5501
+ /** Variant/enum options when the prop is a known group. */
5502
+ options?: string[];
5503
+ /** Persist surface for this prop. */
5504
+ surface: "alpineData" | "attribute";
5505
+ };
5506
+
5507
+ const rows: PropRow[] = [];
5508
+ const seen = new Set<string>();
5509
+
5510
+ // 1) Alpine x-data keys come first — they drive the live variant/state.
5511
+ if (alpineData) {
5512
+ for (const [key, value] of Object.entries(alpineData)) {
5513
+ rows.push({
5514
+ name: key,
5515
+ value,
5516
+ options: persistedVariants[key],
5517
+ surface: "alpineData",
5518
+ });
5519
+ seen.add(key);
5520
+ }
5521
+ }
5522
+
5523
+ // 2) data-agent-native-prop-* attributes not already covered by x-data.
5524
+ for (const prop of observedProps) {
5525
+ if (seen.has(prop.name)) continue;
5526
+ rows.push({
5527
+ name: prop.name,
5528
+ value: prop.value,
5529
+ options: persistedVariants[prop.name],
5530
+ surface: "attribute",
5531
+ });
5532
+ seen.add(prop.name);
5533
+ }
5534
+
5535
+ // 3) persistedVariant groups with no observed value yet (default to first).
5536
+ for (const [group, options] of Object.entries(persistedVariants)) {
5537
+ if (seen.has(group)) continue;
5538
+ rows.push({
5539
+ name: group,
5540
+ value: options[0] ?? "",
5541
+ options,
5542
+ surface: alpineData ? "alpineData" : "attribute",
5543
+ });
5544
+ seen.add(group);
5545
+ }
5546
+
5547
+ const hasRows = rows.length > 0;
5548
+
5549
+ // Build the apply-component-prop-edit payload + optimistic cache patch for a
5550
+ // single prop change.
5551
+ const commitProp = (row: PropRow, nextValue: string) => {
5552
+ if (!editingEnabled || nextValue === row.value) return;
5553
+
5554
+ if (row.surface === "alpineData") {
5555
+ const nextData = { ...(alpineData ?? {}), [row.name]: nextValue };
5556
+ const serialized = serializeAlpineDataObject(nextData);
5557
+ persistPropEdit({ kind: "alpineData", value: serialized }, (prev) => ({
5558
+ ...prev,
5559
+ instance: { ...(prev.instance ?? {}), alpineData: serialized },
5560
+ observedProps: prev.observedProps.map((p) =>
5561
+ p.name === row.name ? { ...p, value: nextValue } : p,
5562
+ ),
5563
+ }));
5564
+ } else {
5565
+ // camelCase prop name → data-agent-native-prop-<kebab>
5566
+ const kebab = row.name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
5567
+ persistPropEdit(
5568
+ {
5569
+ kind: "attribute",
5570
+ attribute: `data-agent-native-prop-${kebab}`,
5571
+ value: nextValue,
5572
+ },
5573
+ (prev) => {
5574
+ const exists = prev.observedProps.some((p) => p.name === row.name);
5575
+ return {
5576
+ ...prev,
5577
+ observedProps: exists
5578
+ ? prev.observedProps.map((p) =>
5579
+ p.name === row.name ? { ...p, value: nextValue } : p,
5580
+ )
5581
+ : [...prev.observedProps, { name: row.name, value: nextValue }],
5582
+ };
5583
+ },
5584
+ );
5585
+ }
5586
+ };
5587
+
5588
+ // ── Capability gates ──
5589
+ const canJumpToSource =
5590
+ capabilities.canResolveToFile &&
5591
+ Boolean(sourceLocation?.filePath) &&
5592
+ sourceCapabilities.includes("resolveNodeToFile");
5593
+
5594
+ // ── Source chip text ──
5595
+ const sourceChip = sourceLocation?.exportName
5596
+ ? `${sourceLocation.exportName} — ${sourceLocation.filePath}`
5597
+ : (sourceLocation?.filePath ?? null);
5598
+
5599
+ return (
5600
+ <section
5601
+ className="shrink-0 border-t border-[var(--design-editor-control-border)] first:border-t-0"
5602
+ data-testid="component-section"
5603
+ >
5604
+ {/* ── Section header ── */}
5605
+ <div className="flex min-h-9 items-center gap-2 px-3">
5606
+ {/* Accent diamond matching the workbench artboard component rows */}
5607
+ <span
5608
+ className="size-2 shrink-0 rotate-45 rounded-[2px] bg-[var(--design-editor-accent-color)]"
5609
+ aria-hidden="true"
5610
+ />
5611
+ <h3 className="min-w-0 flex-1 truncate text-[11px] font-semibold text-foreground">
5612
+ {name}
5613
+ </h3>
5614
+ {/* Jump-to-source action */}
5615
+ <Tooltip>
5616
+ <TooltipTrigger asChild>
5617
+ <Button
5618
+ type="button"
5619
+ variant="ghost"
5620
+ size="icon"
5621
+ className="size-6 rounded-md text-muted-foreground hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
5622
+ disabled={!canJumpToSource}
5623
+ aria-label={
5624
+ "Edit component source" /* i18n-ignore design inspector action */
5625
+ }
5626
+ onClick={() => {
5627
+ openSourceMutation.mutate({ designId, nodeId });
5628
+ }}
5629
+ >
5630
+ <IconExternalLink className="size-3.5" />
5631
+ </Button>
5632
+ </TooltipTrigger>
5633
+ <TooltipContent>
5634
+ {
5635
+ canJumpToSource
5636
+ ? "Edit component source" /* i18n-ignore design inspector action */
5637
+ : (capabilities.ctaMessage ??
5638
+ "Connect Builder to jump to component source") /* i18n-ignore design inspector tooltip */
5639
+ }
5640
+ </TooltipContent>
5641
+ </Tooltip>
5642
+ </div>
5643
+
5644
+ {/* ── Body ── */}
5645
+ <div className="space-y-1.5 px-3 pb-3 pt-0.5 text-[11px]">
5646
+ {/* Source path chip */}
5647
+ {sourceChip && (
5648
+ <div
5649
+ className="flex items-center gap-1 rounded bg-[var(--design-editor-control-bg)] px-2 py-1"
5650
+ title={sourceChip}
5651
+ >
5652
+ <IconCode className="size-3 shrink-0 text-muted-foreground/60" />
5653
+ <span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
5654
+ {sourceChip}
5655
+ </span>
5656
+ </div>
5657
+ )}
5658
+
5659
+ {/* Typed prop controls. Inline/Alpine designs are editable and persist
5660
+ through apply-component-prop-edit; real-app sources are read-only
5661
+ until the deeper source-prop controls land. */}
5662
+ {hasRows && (
5663
+ <div className="space-y-1">
5664
+ <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">
5665
+ {"Props" /* i18n-ignore design inspector label */}
5666
+ </p>
5667
+ {rows.map((row) => {
5668
+ const hasOptions = (row.options?.length ?? 0) > 0;
5669
+ const isBoolean = !hasOptions && isBooleanPropValue(row.value);
5670
+ return (
5671
+ <div key={row.name} className="flex items-center gap-1.5">
5672
+ <Label className="w-[64px] shrink-0 truncate text-[11px] font-medium capitalize text-muted-foreground">
5673
+ {row.name}
5674
+ </Label>
5675
+ {hasOptions ? (
5676
+ // Dropdown for variant / enum groups.
5677
+ <Select
5678
+ value={row.value || row.options![0] || ""}
5679
+ onValueChange={(v) => commitProp(row, v)}
5680
+ disabled={!editingEnabled}
5681
+ >
5682
+ <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)]">
5683
+ <SelectValue />
5684
+ </SelectTrigger>
5685
+ <SelectContent>
5686
+ {row.options!.map((opt) => (
5687
+ <SelectItem
5688
+ key={opt}
5689
+ value={opt}
5690
+ className="text-[11px]"
5691
+ >
5692
+ {opt}
5693
+ </SelectItem>
5694
+ ))}
5695
+ </SelectContent>
5696
+ </Select>
5697
+ ) : isBoolean ? (
5698
+ // Toggle for boolean props.
5699
+ <div className="flex min-w-0 flex-1 items-center">
5700
+ <Switch
5701
+ checked={row.value.trim().toLowerCase() === "true"}
5702
+ onCheckedChange={(checked) =>
5703
+ commitProp(row, checked ? "true" : "false")
5704
+ }
5705
+ disabled={!editingEnabled}
5706
+ className="h-4 w-7 [&>span]:size-3 [&>span]:data-[state=checked]:translate-x-3"
5707
+ aria-label={
5708
+ row.name /* i18n-ignore dynamic prop name */
5709
+ }
5710
+ />
5711
+ </div>
5712
+ ) : (
5713
+ // Text input for string props (e.g. a label).
5714
+ <Input
5715
+ defaultValue={row.value}
5716
+ key={`${row.name}:${row.value}`}
5717
+ disabled={!editingEnabled}
5718
+ onBlur={(e) => commitProp(row, e.target.value)}
5719
+ onKeyDown={(e) => {
5720
+ if (e.key === "Enter") {
5721
+ e.preventDefault();
5722
+ e.currentTarget.blur();
5723
+ }
5724
+ }}
5725
+ 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)]"
5726
+ />
5727
+ )}
5728
+ </div>
5729
+ );
5730
+ })}
5731
+ </div>
5732
+ )}
5733
+
5734
+ {/* Connect-Builder CTA (inline, only when real-app features are gated) */}
5735
+ {capabilities.ctaRequired && (
5736
+ <MakeItRealCard
5737
+ designId={designId}
5738
+ featureLabel="component source jump and full prop controls"
5739
+ />
5740
+ )}
5741
+ </div>
5742
+ </section>
5743
+ );
5744
+ }
5745
+
4360
5746
  export function EditPanel({
4361
5747
  selectedElement,
4362
5748
  pageStyles = {},
@@ -4371,14 +5757,35 @@ export function EditPanel({
4371
5757
  onRequestTweaks,
4372
5758
  onStyleChange,
4373
5759
  onStylesChange,
5760
+ onAutoLayoutConvert,
4374
5761
  onExport,
4375
5762
  exporting = false,
5763
+ readOnly = false,
5764
+ fileId,
5765
+ filename,
5766
+ designId,
5767
+ onTokensApplied,
5768
+ statesPanelProps,
5769
+ reviewPanelProps,
5770
+ componentNodeId,
5771
+ sourceCapabilities = [],
5772
+ onCreateComponent,
5773
+ defaultComponentName = "Component",
5774
+ inspectCode,
4376
5775
  }: EditPanelProps) {
4377
5776
  const t = useT();
5777
+ const [createComponentOpen, setCreateComponentOpen] = useState(false);
4378
5778
  const [exportSettings, setExportSettings] = useState<ExportSettingsValue>(
4379
5779
  DEFAULT_EXPORT_SETTINGS,
4380
5780
  );
4381
5781
  const [showExportPreview, setShowExportPreview] = useState(false);
5782
+ // Sub-tab within the Tweaks area: "tokens" (TokensPanel face) or "tweaks"
5783
+ // (raw TweakDefinition controls). Always default to the existing "tweaks"
5784
+ // controls so opening Tweaks lands on the user's TweakDefinition controls
5785
+ // (and the + affordance) rather than auto-firing token indexing. "Tokens"
5786
+ // remains available as a sub-tab the user can click when designId is set.
5787
+ const [tweaksSubTab, setTweaksSubTab] = useState<TweaksSubTab>("tweaks");
5788
+
4382
5789
  const selectedElementKey = selectedElement
4383
5790
  ? elementIdentityKey(selectedElement)
4384
5791
  : "none";
@@ -4395,12 +5802,66 @@ export function EditPanel({
4395
5802
  },
4396
5803
  [onTweakChange],
4397
5804
  );
5805
+ // The "Add Tweaks" (+) flow lives only on the "tweaks" sub-tab. Switch to it
5806
+ // before opening the prompt so the + button and the newly added control are
5807
+ // visible (avoids leaving the user on the Tokens sub-tab where + isn't shown).
5808
+ const handleRequestTweaks = useCallback(
5809
+ (anchor: HTMLElement) => {
5810
+ setTweaksSubTab("tweaks");
5811
+ onRequestTweaks?.(anchor);
5812
+ },
5813
+ [onRequestTweaks],
5814
+ );
4398
5815
 
4399
5816
  useEffect(() => {
4400
5817
  setExportSettings(DEFAULT_EXPORT_SETTINGS);
4401
5818
  setShowExportPreview(false);
4402
5819
  }, [selectedElementKey]);
4403
5820
 
5821
+ // Scroll guard: suppress the click that fires immediately after a scroll
5822
+ // gesture ends (rubber-band or normal scroll). Using onScroll instead of
5823
+ // onPointerDown avoids side-effects like Radix DismissableLayer detecting a
5824
+ // "pointerdown outside" and closing open popovers — which, during an
5825
+ // over-scroll bounce, could briefly un-shield the canvas and allow a stray
5826
+ // pointer event to deselect the selected canvas element (R3 regression).
5827
+ const scrolledRecentlyRef = useRef(false);
5828
+ const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
5829
+
5830
+ if (readOnly) {
5831
+ return (
5832
+ <div
5833
+ className={cn(
5834
+ "shrink-0 bg-[var(--design-editor-panel-bg)]",
5835
+ "flex h-full min-h-0 flex-col overflow-hidden",
5836
+ )}
5837
+ style={{ width }}
5838
+ >
5839
+ <div className="flex min-h-8 shrink-0 items-center border-b border-border/90 px-3">
5840
+ <h2 className="min-w-0 truncate text-[12px] font-semibold text-foreground">
5841
+ {t("editPanel.properties")}
5842
+ </h2>
5843
+ </div>
5844
+ <div className="flex min-h-0 flex-1 items-center justify-center px-5">
5845
+ <div
5846
+ className="max-w-[188px] text-center"
5847
+ aria-live="polite"
5848
+ aria-busy="true"
5849
+ >
5850
+ <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">
5851
+ <IconLock className="size-4" aria-hidden="true" />
5852
+ </div>
5853
+ <h3 className="text-[12px] font-semibold leading-snug text-foreground">
5854
+ {t("designEditor.inspectorLockedTitle")}
5855
+ </h3>
5856
+ <p className="mt-1.5 text-[11px] leading-snug text-muted-foreground/70">
5857
+ {t("designEditor.inspectorLockedDescription")}
5858
+ </p>
5859
+ </div>
5860
+ </div>
5861
+ </div>
5862
+ );
5863
+ }
5864
+
4404
5865
  return (
4405
5866
  <div
4406
5867
  className={cn(
@@ -4417,13 +5878,109 @@ export function EditPanel({
4417
5878
 
4418
5879
  {activeTab === "design" ? (
4419
5880
  <>
4420
- <SelectionHeader element={selectedElement} />
5881
+ <SelectionHeader
5882
+ element={selectedElement}
5883
+ onCreateComponent={
5884
+ onCreateComponent && selectedElement
5885
+ ? () => setCreateComponentOpen(true)
5886
+ : undefined
5887
+ }
5888
+ inspectCodePopover={
5889
+ inspectCode && selectedElement
5890
+ ? (trigger) => (
5891
+ <InspectCodePopover trigger={trigger} data={inspectCode} />
5892
+ )
5893
+ : undefined
5894
+ }
5895
+ />
5896
+ {onCreateComponent && (
5897
+ <CreateComponentDialog
5898
+ open={createComponentOpen}
5899
+ onOpenChange={setCreateComponentOpen}
5900
+ defaultName={defaultComponentName}
5901
+ onSubmit={onCreateComponent}
5902
+ />
5903
+ )}
5904
+
5905
+ <div
5906
+ className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain"
5907
+ onScroll={() => {
5908
+ // Mark that a scroll just happened so the click that some
5909
+ // browsers fire at the end of a scroll gesture (or after an
5910
+ // overscroll/rubber-band bounce) is suppressed. Crucially this
5911
+ // runs on the scroll event — NOT on pointerdown — so it never
5912
+ // triggers Radix's DismissableLayer "outside pointerdown"
5913
+ // detection, which would close open inspector popovers and, once
5914
+ // the shield is removed, allow a stray canvas pointer event to
5915
+ // deselect the selected element (the R3 overscroll regression).
5916
+ scrolledRecentlyRef.current = true;
5917
+ if (scrollTimerRef.current !== null) {
5918
+ clearTimeout(scrollTimerRef.current);
5919
+ }
5920
+ scrollTimerRef.current = setTimeout(() => {
5921
+ scrolledRecentlyRef.current = false;
5922
+ scrollTimerRef.current = null;
5923
+ }, 300);
5924
+ }}
5925
+ onClickCapture={(e) => {
5926
+ // Suppress spurious clicks (e.g. color-picker opening) that
5927
+ // fire immediately after a scroll gesture ends. The 300ms
5928
+ // window from the last scroll event covers both the synchronous
5929
+ // scroll-end click and the delayed synthetic click that mobile
5930
+ // browsers generate after a touch-scroll ends.
5931
+ if (!scrolledRecentlyRef.current) return;
5932
+ scrolledRecentlyRef.current = false;
5933
+ if (scrollTimerRef.current !== null) {
5934
+ clearTimeout(scrollTimerRef.current);
5935
+ scrollTimerRef.current = null;
5936
+ }
5937
+ e.stopPropagation();
5938
+ e.preventDefault();
5939
+ }}
5940
+ onKeyDown={(e) => {
5941
+ // Trap Tab within the inspector panel so it never focuses the
5942
+ // canvas iframe. When the canvas iframe gains focus it forwards
5943
+ // a synthetic Tab keydown to the parent window, which is picked
5944
+ // up by the design-editor hotkey handler as "cycle file" and
5945
+ // causes apparent deselection / overview-mode switch (bug: Tab
5946
+ // in a numeric field deselected the canvas element).
5947
+ if (e.key !== "Tab") return;
5948
+ const panel = e.currentTarget;
5949
+ const focusable = Array.from(
5950
+ panel.querySelectorAll<HTMLElement>(
5951
+ 'input, button, select, textarea, [tabindex]:not([tabindex="-1"])',
5952
+ ),
5953
+ ).filter(
5954
+ (el) =>
5955
+ !el.hasAttribute("disabled") &&
5956
+ el.tabIndex !== -1 &&
5957
+ !el.closest('[aria-hidden="true"]'),
5958
+ );
5959
+ if (focusable.length === 0) return;
5960
+ e.preventDefault();
5961
+ const current = document.activeElement as HTMLElement | null;
5962
+ const idx = current ? focusable.indexOf(current) : -1;
5963
+ const next = e.shiftKey
5964
+ ? focusable[(idx - 1 + focusable.length) % focusable.length]
5965
+ : focusable[(idx + 1) % focusable.length];
5966
+ next?.focus();
5967
+ }}
5968
+ >
5969
+ {/* §6.1 Component section — shown at the top when a component
5970
+ instance is selected. Requires designId + componentNodeId. */}
5971
+ {designId && componentNodeId && (
5972
+ <ComponentSection
5973
+ designId={designId}
5974
+ nodeId={componentNodeId}
5975
+ sourceCapabilities={sourceCapabilities}
5976
+ />
5977
+ )}
4421
5978
 
4422
- <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
4423
5979
  {!selectedElement && (
4424
5980
  <PageProperties
4425
5981
  styles={pageStyles}
4426
5982
  onStyleChange={onStyleChange}
5983
+ onStylesChange={onStylesChange}
4427
5984
  />
4428
5985
  )}
4429
5986
 
@@ -4437,6 +5994,7 @@ export function EditPanel({
4437
5994
  element={selectedElement}
4438
5995
  onStyleChange={onStyleChange}
4439
5996
  onStylesChange={onStylesChange}
5997
+ onAutoLayoutConvert={onAutoLayoutConvert}
4440
5998
  />
4441
5999
  <AppearanceProperties
4442
6000
  element={selectedElement}
@@ -4473,6 +6031,17 @@ export function EditPanel({
4473
6031
  onStyleChange={onStyleChange}
4474
6032
  />
4475
6033
  ) : null}
6034
+ {/* AI edit request block — collapsible, collapsed by default */}
6035
+ <section className="shrink-0 border-t border-[var(--design-editor-control-border)]">
6036
+ <InspectorAiActions
6037
+ selector={selectedElement.selector}
6038
+ sourceId={selectedElement.sourceId}
6039
+ designId={designId}
6040
+ fileId={fileId}
6041
+ filename={filename}
6042
+ canEdit={!readOnly}
6043
+ />
6044
+ </section>
4476
6045
  </>
4477
6046
  )}
4478
6047
  {onExport ? (
@@ -4507,40 +6076,212 @@ export function EditPanel({
4507
6076
  ) : null}
4508
6077
  </PanelSection>
4509
6078
  ) : null}
6079
+
6080
+ {/* §6.4 States & Responsive — contextual section in Design tab.
6081
+ Collapsed by default so existing Design content is not buried.
6082
+ Only mounts when designId is provided so there is something to
6083
+ load; without it the panel would fire an action with no id. */}
6084
+ {designId && statesPanelProps ? (
6085
+ <PanelSection
6086
+ title={"States" /* i18n-ignore design inspector section */}
6087
+ defaultCollapsed
6088
+ actions={
6089
+ <Tooltip>
6090
+ <TooltipTrigger asChild>
6091
+ <Button
6092
+ type="button"
6093
+ variant="ghost"
6094
+ size="icon"
6095
+ className="size-6 rounded-md text-muted-foreground hover:text-foreground"
6096
+ aria-label={
6097
+ "Breakpoints & states" /* i18n-ignore design inspector action */
6098
+ }
6099
+ >
6100
+ <IconDeviceDesktop className="size-3.5" />
6101
+ </Button>
6102
+ </TooltipTrigger>
6103
+ <TooltipContent>
6104
+ {
6105
+ "Breakpoints & states" /* i18n-ignore design inspector action */
6106
+ }
6107
+ </TooltipContent>
6108
+ </Tooltip>
6109
+ }
6110
+ >
6111
+ <StatesPanel designId={designId} {...statesPanelProps} />
6112
+ {/* §6.4 / §6.6 — live captures (real running-app data, route
6113
+ props, API responses) are a real-app capability. When
6114
+ canCapture is false (inline source) surface a compact
6115
+ "Make it real" CTA so the user knows it's one step away. */}
6116
+ {!statesPanelProps.canCapture && (
6117
+ <MakeItRealCard
6118
+ designId={designId}
6119
+ featureLabel="live data captures and real-app states"
6120
+ />
6121
+ )}
6122
+ </PanelSection>
6123
+ ) : null}
6124
+
6125
+ {/* §6.5 Review — contextual section in Design tab.
6126
+ Collapsed by default. Renders when reviewPanelProps is provided,
6127
+ no designId check needed since ReviewPanel is statically fed. */}
6128
+ {reviewPanelProps ? (
6129
+ <PanelSection
6130
+ title={"Review" /* i18n-ignore design inspector section */}
6131
+ defaultCollapsed
6132
+ actions={
6133
+ <Tooltip>
6134
+ <TooltipTrigger asChild>
6135
+ <Button
6136
+ type="button"
6137
+ variant="ghost"
6138
+ size="icon"
6139
+ className="size-6 rounded-md text-muted-foreground hover:text-foreground"
6140
+ aria-label={
6141
+ "Accessibility & visual diff" /* i18n-ignore design inspector action */
6142
+ }
6143
+ >
6144
+ <IconShieldCheck className="size-3.5" />
6145
+ </Button>
6146
+ </TooltipTrigger>
6147
+ <TooltipContent>
6148
+ {
6149
+ "Accessibility & visual diff" /* i18n-ignore design inspector action */
6150
+ }
6151
+ </TooltipContent>
6152
+ </Tooltip>
6153
+ }
6154
+ >
6155
+ {/* ReviewPanel manages its own scroll; no extra wrapper needed. */}
6156
+ <ReviewPanel {...reviewPanelProps} />
6157
+ </PanelSection>
6158
+ ) : null}
4510
6159
  </div>
4511
6160
  </>
4512
6161
  ) : activeTab === "tweaks" ? (
4513
6162
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
4514
- <div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
4515
- <h3 className="min-w-0 flex-1 truncate text-[13px] font-semibold text-foreground">
4516
- {t("designEditor.tweaks")}
4517
- </h3>
4518
- {onRequestTweaks ? (
4519
- <Tooltip>
4520
- <TooltipTrigger asChild>
4521
- <Button
4522
- type="button"
4523
- variant="ghost"
4524
- size="icon"
4525
- className="size-7 rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
4526
- aria-label={t("designEditor.addTweaks")}
4527
- onClick={(event) => onRequestTweaks(event.currentTarget)}
4528
- >
4529
- <IconPlus className="size-3.5" />
4530
- </Button>
4531
- </TooltipTrigger>
4532
- <TooltipContent>{t("designEditor.addTweaks")}</TooltipContent>
4533
- </Tooltip>
4534
- ) : null}
4535
- </div>
6163
+ {/* Tweaks tab header: when designId is present show a Tokens | Tweaks
6164
+ sub-tab switcher so the TokensPanel is the first-class face of
6165
+ this area per §6.2. When there is no designId only the raw tweaks
6166
+ title is shown (original behaviour). */}
6167
+ {designId ? (
6168
+ <div className="flex h-9 shrink-0 items-center gap-0.5 border-b border-border/90 px-2">
6169
+ <Button
6170
+ type="button"
6171
+ variant="ghost"
6172
+ size="sm"
6173
+ className={cn(
6174
+ "h-6 rounded-md px-2 text-[11px] font-semibold text-muted-foreground transition-colors hover:text-foreground",
6175
+ tweaksSubTab === "tokens" &&
6176
+ "bg-[var(--design-editor-panel-raised-bg)] text-foreground",
6177
+ )}
6178
+ onClick={() => setTweaksSubTab("tokens")}
6179
+ >
6180
+ <IconPalette className="mr-1 size-3" />
6181
+ {"Tokens" /* i18n-ignore design inspector sub-tab */}
6182
+ </Button>
6183
+ <Button
6184
+ type="button"
6185
+ variant="ghost"
6186
+ size="sm"
6187
+ className={cn(
6188
+ "h-6 rounded-md px-2 text-[11px] font-semibold text-muted-foreground transition-colors hover:text-foreground",
6189
+ tweaksSubTab === "tweaks" &&
6190
+ "bg-[var(--design-editor-panel-raised-bg)] text-foreground",
6191
+ )}
6192
+ onClick={() => setTweaksSubTab("tweaks")}
6193
+ >
6194
+ {t("designEditor.tweaks")}
6195
+ </Button>
6196
+ </div>
6197
+ ) : (
6198
+ <div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
6199
+ <h3 className="min-w-0 flex-1 truncate text-[13px] font-semibold text-foreground">
6200
+ {t("designEditor.tweaks")}
6201
+ </h3>
6202
+ {onRequestTweaks ? (
6203
+ <Tooltip>
6204
+ <TooltipTrigger asChild>
6205
+ <Button
6206
+ type="button"
6207
+ variant="ghost"
6208
+ size="icon"
6209
+ className="size-7 rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
6210
+ aria-label={t("designEditor.addTweaks")}
6211
+ onClick={(event) =>
6212
+ handleRequestTweaks(event.currentTarget)
6213
+ }
6214
+ >
6215
+ <IconPlus className="size-3.5" />
6216
+ </Button>
6217
+ </TooltipTrigger>
6218
+ <TooltipContent>{t("designEditor.addTweaks")}</TooltipContent>
6219
+ </Tooltip>
6220
+ ) : null}
6221
+ </div>
6222
+ )}
6223
+
4536
6224
  <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
4537
- <TweaksPanelContent
4538
- tweaks={tweaks}
4539
- values={tweakValues}
4540
- onChange={handleTweakChange}
4541
- onRequestTweaks={onRequestTweaks}
4542
- className="px-3 py-3"
4543
- />
6225
+ {designId && tweaksSubTab === "tokens" ? (
6226
+ /* §6.2 Tokens face — first-class view of the Tweaks area.
6227
+ TokensPanel calls apply-design-token-edit which routes through
6228
+ the Tweaks loop (designs.data.tweakSelections persist). */
6229
+ <>
6230
+ <TokensPanel
6231
+ designId={designId}
6232
+ onTokensApplied={onTokensApplied}
6233
+ />
6234
+ {/* §5 / §6.6 — token write-back to source is a real-app
6235
+ capability. On inline sources (sourceCapabilities lacks
6236
+ "writeFile") show a compact "Make it real" CTA after the
6237
+ token list so the user knows write-back is one step away. */}
6238
+ {!sourceCapabilities.includes("writeFile") && (
6239
+ <div className="px-3 pb-3 pt-1">
6240
+ <MakeItRealCard
6241
+ designId={designId}
6242
+ featureLabel="token write-back to source files"
6243
+ />
6244
+ </div>
6245
+ )}
6246
+ </>
6247
+ ) : (
6248
+ /* Raw TweakDefinition controls — shown when no designId, or when
6249
+ the user switches to the Tweaks sub-tab. */
6250
+ <>
6251
+ {designId && onRequestTweaks ? (
6252
+ /* When in sub-tab mode, show the add-tweaks action in the
6253
+ scrollable content area above the raw controls. */
6254
+ <div className="flex items-center justify-end px-3 pt-2">
6255
+ <Tooltip>
6256
+ <TooltipTrigger asChild>
6257
+ <Button
6258
+ type="button"
6259
+ variant="ghost"
6260
+ size="icon"
6261
+ className="size-7 rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
6262
+ aria-label={t("designEditor.addTweaks")}
6263
+ onClick={(event) =>
6264
+ handleRequestTweaks(event.currentTarget)
6265
+ }
6266
+ >
6267
+ <IconPlus className="size-3.5" />
6268
+ </Button>
6269
+ </TooltipTrigger>
6270
+ <TooltipContent>
6271
+ {t("designEditor.addTweaks")}
6272
+ </TooltipContent>
6273
+ </Tooltip>
6274
+ </div>
6275
+ ) : null}
6276
+ <TweaksPanelContent
6277
+ tweaks={tweaks}
6278
+ values={tweakValues}
6279
+ onChange={handleTweakChange}
6280
+ onRequestTweaks={!designId ? handleRequestTweaks : undefined}
6281
+ className="px-3 py-3"
6282
+ />
6283
+ </>
6284
+ )}
4544
6285
  </div>
4545
6286
  </div>
4546
6287
  ) : extensionContext ? (