@agent-native/core 0.80.10 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +419 -19
  6. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  7. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  8. package/corpus/core/src/client/chat/index.ts +1 -0
  9. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  10. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  11. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  12. package/corpus/core/src/client/index.ts +1 -0
  13. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  15. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  17. package/corpus/core/src/db/client.ts +29 -2
  18. package/corpus/core/src/provider-api/index.ts +29 -0
  19. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  20. package/corpus/core/src/styles/agent-native.css +54 -0
  21. package/corpus/core/src/usage/store.ts +2 -2
  22. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  23. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  24. package/corpus/templates/analytics/app/global.css +8 -0
  25. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  26. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  27. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  29. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  30. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  31. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  36. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  38. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  39. package/corpus/templates/assets/AGENTS.md +4 -0
  40. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  42. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  43. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  44. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  46. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  47. package/corpus/templates/assets/app/global.css +15 -0
  48. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  49. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  50. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  51. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  52. package/corpus/templates/assets/app/routes/library.tsx +233 -52
  53. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  54. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  56. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  57. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  58. package/corpus/templates/assets/shared/api.ts +4 -0
  59. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  60. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  61. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  64. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  66. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  67. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  68. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  69. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  70. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  71. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  72. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  76. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  77. package/corpus/templates/design/AGENTS.md +27 -0
  78. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  79. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  80. package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
  81. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  82. package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
  83. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  84. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  85. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  86. package/corpus/templates/design/actions/delete-design.ts +20 -0
  87. package/corpus/templates/design/actions/export-zip.ts +23 -8
  88. package/corpus/templates/design/actions/generate-design.ts +53 -1
  89. package/corpus/templates/design/actions/get-component-details.ts +21 -26
  90. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  91. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  92. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  93. package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
  94. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  95. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  96. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  97. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  98. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  99. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  100. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  101. package/corpus/templates/design/actions/navigate.ts +18 -3
  102. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  103. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  104. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  105. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  106. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  107. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  108. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  109. package/corpus/templates/design/actions/run-design-audit.ts +74 -10
  110. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
  118. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
  120. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
  121. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
  122. package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
  123. package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
  124. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  125. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  126. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  127. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  128. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  129. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  131. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  132. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  133. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  134. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  136. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  137. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  138. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  139. package/corpus/templates/design/app/global.css +7 -0
  140. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  141. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  142. package/corpus/templates/design/app/i18n-data.ts +369 -0
  143. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
  145. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  146. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  171. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  172. package/corpus/templates/design/e2e/helpers.ts +168 -65
  173. package/corpus/templates/design/package.json +3 -1
  174. package/corpus/templates/design/server/db/schema.ts +23 -0
  175. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  176. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  177. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  178. package/corpus/templates/design/server/plugins/db.ts +39 -0
  179. package/corpus/templates/design/server/register-secrets.ts +38 -0
  180. package/corpus/templates/design/shared/board-file.ts +228 -0
  181. package/corpus/templates/design/shared/board-objects.ts +288 -0
  182. package/corpus/templates/design/shared/code-layer.ts +52 -0
  183. package/corpus/templates/design/shared/component-model.ts +35 -0
  184. package/corpus/templates/design/shared/motion-compiler.ts +137 -15
  185. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  186. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  187. package/corpus/templates/design/tsconfig.json +2 -1
  188. package/corpus/templates/slides/AGENTS.md +9 -0
  189. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  190. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  191. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  192. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  193. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  194. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  195. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  196. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  197. package/corpus/templates/slides/package.json +1 -0
  198. package/corpus/templates/slides/vite.config.ts +1 -0
  199. package/dist/agent/production-agent.d.ts.map +1 -1
  200. package/dist/agent/production-agent.js +94 -10
  201. package/dist/agent/production-agent.js.map +1 -1
  202. package/dist/cli/design-connect.d.ts +25 -0
  203. package/dist/cli/design-connect.d.ts.map +1 -1
  204. package/dist/cli/design-connect.js +357 -18
  205. package/dist/cli/design-connect.js.map +1 -1
  206. package/dist/client/AssistantChat.d.ts +14 -0
  207. package/dist/client/AssistantChat.d.ts.map +1 -1
  208. package/dist/client/AssistantChat.js +115 -22
  209. package/dist/client/AssistantChat.js.map +1 -1
  210. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  211. package/dist/client/ConnectBuilderCard.js +1 -0
  212. package/dist/client/ConnectBuilderCard.js.map +1 -1
  213. package/dist/client/chat/index.d.ts +1 -1
  214. package/dist/client/chat/index.d.ts.map +1 -1
  215. package/dist/client/chat/index.js +1 -1
  216. package/dist/client/chat/index.js.map +1 -1
  217. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  218. package/dist/client/chat/run-recovery.js +5 -5
  219. package/dist/client/chat/run-recovery.js.map +1 -1
  220. package/dist/client/chat-view-transition.d.ts +6 -0
  221. package/dist/client/chat-view-transition.d.ts.map +1 -1
  222. package/dist/client/chat-view-transition.js +23 -2
  223. package/dist/client/chat-view-transition.js.map +1 -1
  224. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  225. package/dist/client/composer/PromptComposer.js +3 -2
  226. package/dist/client/composer/PromptComposer.js.map +1 -1
  227. package/dist/client/index.d.ts +1 -1
  228. package/dist/client/index.d.ts.map +1 -1
  229. package/dist/client/index.js +1 -1
  230. package/dist/client/index.js.map +1 -1
  231. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  232. package/dist/client/org/OrgSwitcher.js +11 -4
  233. package/dist/client/org/OrgSwitcher.js.map +1 -1
  234. package/dist/client/sharing/ShareButton.js +3 -3
  235. package/dist/client/sharing/ShareButton.js.map +1 -1
  236. package/dist/client/sse-event-processor.d.ts.map +1 -1
  237. package/dist/client/sse-event-processor.js +10 -0
  238. package/dist/client/sse-event-processor.js.map +1 -1
  239. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  240. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  241. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  242. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  243. package/dist/collab/awareness.d.ts +2 -2
  244. package/dist/collab/awareness.d.ts.map +1 -1
  245. package/dist/collab/routes.d.ts +2 -2
  246. package/dist/db/client.d.ts +1 -0
  247. package/dist/db/client.d.ts.map +1 -1
  248. package/dist/db/client.js +28 -2
  249. package/dist/db/client.js.map +1 -1
  250. package/dist/notifications/routes.d.ts +3 -3
  251. package/dist/observability/routes.d.ts +3 -3
  252. package/dist/progress/routes.d.ts +1 -1
  253. package/dist/provider-api/index.d.ts +5 -4
  254. package/dist/provider-api/index.d.ts.map +1 -1
  255. package/dist/provider-api/index.js +28 -0
  256. package/dist/provider-api/index.js.map +1 -1
  257. package/dist/resources/handlers.d.ts +3 -3
  258. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  259. package/dist/server/core-routes-plugin.d.ts +25 -0
  260. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  261. package/dist/server/core-routes-plugin.js +43 -18
  262. package/dist/server/core-routes-plugin.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +54 -0
  265. package/dist/usage/store.js +2 -2
  266. package/dist/usage/store.js.map +1 -1
  267. package/package.json +1 -1
@@ -11,6 +11,7 @@ import {
11
11
  rgbaToHex,
12
12
  withColorOpacity,
13
13
  } from "@shared/color-utils";
14
+ import { propNameToDataAttribute } from "@shared/component-model";
14
15
  import {
15
16
  IconAlignCenter,
16
17
  IconAlignJustified,
@@ -51,7 +52,6 @@ import {
51
52
  IconLock,
52
53
  IconMaximize,
53
54
  IconMinus,
54
- IconPalette,
55
55
  IconPhoto,
56
56
  IconPlus,
57
57
  IconShadow,
@@ -69,6 +69,7 @@ import {
69
69
  type ReactNode,
70
70
  } from "react";
71
71
  import { useParams } from "react-router";
72
+ import { toast } from "sonner";
72
73
 
73
74
  import { Button } from "@/components/ui/button";
74
75
  import {
@@ -109,10 +110,6 @@ import {
109
110
  } from "@/components/ui/tooltip";
110
111
  import { cn } from "@/lib/utils";
111
112
 
112
- import {
113
- DesignExtensionsPanel,
114
- type DesignExtensionSlotContext,
115
- } from "./DesignExtensionsPanel";
116
113
  import {
117
114
  AutoLayoutMatrix,
118
115
  ConstraintsWidget,
@@ -141,18 +138,10 @@ import { ReviewPanel } from "./ReviewPanel";
141
138
  import type { ReviewPanelProps } from "./ReviewPanel";
142
139
  import { StatesPanel } from "./StatesPanel";
143
140
  import type { StatesPanelProps } from "./StatesPanel";
144
- import { TokensPanel } from "./TokensPanel";
145
141
  import { TweaksPanelContent } from "./TweaksPanel";
146
142
  import type { ElementInfo } from "./types";
147
143
 
148
- export type InspectorTab = "design" | "tweaks" | "extensions";
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";
144
+ export type InspectorTab = "design" | "tweaks";
156
145
 
157
146
  interface EditPanelProps {
158
147
  selectedElement: ElementInfo | null;
@@ -164,7 +153,6 @@ interface EditPanelProps {
164
153
  onActiveTabChange?: (tab: InspectorTab) => void;
165
154
  tweaks?: TweakDefinition[];
166
155
  tweakValues?: Record<string, string | number | boolean>;
167
- extensionContext?: DesignExtensionSlotContext;
168
156
  onTweakChange?: (id: string, value: string | number | boolean) => void;
169
157
  onRequestTweaks?: (anchor: HTMLElement) => void;
170
158
  onStyleChange: (property: string, value: string) => void;
@@ -180,12 +168,25 @@ interface EditPanelProps {
180
168
  fileId?: string;
181
169
  /** Active file name (e.g. "index.html") — forwarded to InspectorAiActions. */
182
170
  filename?: string;
171
+ /** Latest active file HTML, used to compose rapid sequential source edits. */
172
+ activeContent?: string;
173
+ /** Server revision for activeContent. */
174
+ activeFileUpdatedAt?: string | null;
183
175
  // -------------------------------------------------------------------------
184
176
  // Design Studio panels (§6.2, §6.4, §6.5)
185
177
  // Pass `designId` to unlock Tokens, States, and Review sections.
186
178
  // -------------------------------------------------------------------------
187
179
  /** The active design's id — required to mount Tokens / States / Review. */
188
180
  designId?: string;
181
+ /**
182
+ * Called after a component prop edit returns the patched source so the parent
183
+ * editor can sync local/Yjs content instead of waiting for query invalidation.
184
+ */
185
+ onComponentPropApplied?: (
186
+ fileId: string,
187
+ content: string,
188
+ updatedAt?: string,
189
+ ) => void;
189
190
  /**
190
191
  * Called after a token edit is applied so the parent can push the resolved
191
192
  * CSS-var map into the iframe via the tweak-values postMessage.
@@ -355,6 +356,9 @@ function ColorInput({
355
356
  value,
356
357
  onChange,
357
358
  backgroundImage,
359
+ backgroundSize,
360
+ backgroundRepeat,
361
+ backgroundPosition,
358
362
  onBackgroundImageChange,
359
363
  onImageFillChange,
360
364
  blendMode,
@@ -367,6 +371,9 @@ function ColorInput({
367
371
  value: string;
368
372
  onChange: (value: string) => void;
369
373
  backgroundImage?: string;
374
+ backgroundSize?: string;
375
+ backgroundRepeat?: string;
376
+ backgroundPosition?: string;
370
377
  onBackgroundImageChange?: (value: string) => void;
371
378
  onImageFillChange?: (value: ImageFillValue) => void;
372
379
  blendMode?: string;
@@ -385,6 +392,9 @@ function ColorInput({
385
392
  }, [value]);
386
393
 
387
394
  const backgroundLayers = splitCssLayers(backgroundImage || "");
395
+ const backgroundSizeLayers = splitCssLayers(backgroundSize || "");
396
+ const backgroundRepeatLayers = splitCssLayers(backgroundRepeat || "");
397
+ const backgroundPositionLayers = splitCssLayers(backgroundPosition || "");
388
398
  const selectedLayerIndex = fillLayerIndex(selectedFillId);
389
399
  const selectedGradient =
390
400
  selectedLayerIndex !== null
@@ -619,6 +629,8 @@ function ColorInput({
619
629
  selectedLayerIndex !== null
620
630
  ? (backgroundLayers[selectedLayerIndex] ?? draft ?? value ?? "#000000")
621
631
  : draft || "#000000";
632
+ const selectedBackgroundLayerValue = (layers: string[]): string | undefined =>
633
+ selectedLayerIndex !== null ? layers[selectedLayerIndex] : undefined;
622
634
  const handlePaintTypeChange = (type: DesignPaintType) => {
623
635
  const selectedLayer = fillLayerIndex(selectedFillId);
624
636
  if (type === "solid") {
@@ -679,6 +691,12 @@ function ColorInput({
679
691
  supportsLayeredFills ? handlePaintValueChange : undefined
680
692
  }
681
693
  onImageFillChange={onImageFillChange}
694
+ backgroundImage={selectedBackgroundLayerValue(backgroundLayers)}
695
+ backgroundSize={selectedBackgroundLayerValue(backgroundSizeLayers)}
696
+ backgroundRepeat={selectedBackgroundLayerValue(backgroundRepeatLayers)}
697
+ backgroundPosition={selectedBackgroundLayerValue(
698
+ backgroundPositionLayers,
699
+ )}
682
700
  blendMode={blendMode}
683
701
  onBlendModeChange={onBlendModeChange}
684
702
  showBlendMode={Boolean(onBlendModeChange)}
@@ -2149,6 +2167,292 @@ export function serializeAlpineDataObject(obj: Record<string, string>): string {
2149
2167
  return parts.length ? `{ ${parts.join(", ")} }` : "{}";
2150
2168
  }
2151
2169
 
2170
+ /**
2171
+ * Format a single editable prop value as an `x-data` literal: bare for
2172
+ * boolean / number values, single-quoted (with escaping) for strings.
2173
+ *
2174
+ * Pure — exported for tests.
2175
+ */
2176
+ export function alpineDataValueLiteral(value: string): string {
2177
+ const isBoolean = value === "true" || value === "false";
2178
+ const isNumber = /^-?\d+(\.\d+)?$/.test(value);
2179
+ return isBoolean || isNumber ? value : `'${value.replace(/'/g, "\\'")}'`;
2180
+ }
2181
+
2182
+ /**
2183
+ * Surgically replace a single top-level key's value inside an Alpine `x-data`
2184
+ * object literal, preserving everything else byte-for-byte — methods
2185
+ * (`toggle() { … }`), nested objects, escaped strings, quoted keys, comments,
2186
+ * and whitespace are all left untouched.
2187
+ *
2188
+ * Unlike a `parseAlpineDataObject` → mutate → `serializeAlpineDataObject`
2189
+ * round-trip (which only understands a flat object of simple literals and so
2190
+ * *drops* anything it can't model), this walks the original string, finds the
2191
+ * `key:` token at the top level (depth 0, not inside a string/comment), and
2192
+ * rewrites only the value literal that immediately follows it.
2193
+ *
2194
+ * Returns `null` when the key cannot be located surgically (e.g. the value is
2195
+ * an expression/function/object rather than a simple string/boolean/number, or
2196
+ * the literal isn't a `{ … }` object) so the caller can fail safe instead of
2197
+ * persisting a lossy rewrite.
2198
+ *
2199
+ * Pure — exported for tests.
2200
+ */
2201
+ export function replaceAlpineDataKeyValue(
2202
+ xData: string | null | undefined,
2203
+ key: string,
2204
+ nextValue: string,
2205
+ ): string | null {
2206
+ if (!xData) return null;
2207
+ const trimmed = xData.trim();
2208
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
2209
+
2210
+ const s = xData;
2211
+ const n = s.length;
2212
+ // Walk the whole string tracking nesting depth and skipping over strings,
2213
+ // template literals, regex-ish slashes are not handled (Alpine x-data does
2214
+ // not use them at the object-key level), and line / block comments. Only at
2215
+ // object depth 1 (directly inside the outermost `{ … }`) do we look for the
2216
+ // target `key :` token.
2217
+ let depth = 0;
2218
+ let i = 0;
2219
+
2220
+ /** Advance `i` past a quoted string starting at `i` (handles escapes). */
2221
+ const skipString = (quote: string): void => {
2222
+ i += 1; // opening quote
2223
+ while (i < n) {
2224
+ const c = s[i];
2225
+ if (c === "\\") {
2226
+ i += 2;
2227
+ continue;
2228
+ }
2229
+ if (c === quote) {
2230
+ i += 1;
2231
+ return;
2232
+ }
2233
+ i += 1;
2234
+ }
2235
+ };
2236
+
2237
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2238
+ // Bare identifier key at a token boundary: `key` then optional ws then `:`.
2239
+ const bareRe = new RegExp(`^(${escapedKey})(\\s*:\\s*)`);
2240
+ // Quoted key: `'key'` or `"key"` then optional ws then `:`.
2241
+ const quotedRe = new RegExp(`^(['"]${escapedKey}['"])(\\s*:\\s*)`);
2242
+
2243
+ while (i < n) {
2244
+ const c = s[i];
2245
+
2246
+ // At the top level, a `{` / `,` (or the string start) opens a fresh value
2247
+ // slot. Try to match the target key here *before* treating a quote as an
2248
+ // opaque string — this is how quoted keys (`'size': …`) are recognised.
2249
+ if (depth === 1) {
2250
+ const prev = lastNonSpaceBefore(s, i);
2251
+ if (prev === "{" || prev === ",") {
2252
+ const rest = s.slice(i);
2253
+ const m = bareRe.exec(rest) ?? quotedRe.exec(rest);
2254
+ if (m) {
2255
+ const valueStart = i + m[1].length + m[2].length;
2256
+ const valueEnd = simpleValueEnd(s, valueStart);
2257
+ if (valueEnd === null) return null; // value is not a simple literal
2258
+ return (
2259
+ s.slice(0, valueStart) +
2260
+ alpineDataValueLiteral(nextValue) +
2261
+ s.slice(valueEnd)
2262
+ );
2263
+ }
2264
+ }
2265
+ }
2266
+
2267
+ // Skip strings / template literals wholesale.
2268
+ if (c === '"' || c === "'" || c === "`") {
2269
+ skipString(c);
2270
+ continue;
2271
+ }
2272
+ // Skip comments.
2273
+ if (c === "/" && s[i + 1] === "/") {
2274
+ i += 2;
2275
+ while (i < n && s[i] !== "\n") i += 1;
2276
+ continue;
2277
+ }
2278
+ if (c === "/" && s[i + 1] === "*") {
2279
+ i += 2;
2280
+ while (i < n && !(s[i] === "*" && s[i + 1] === "/")) i += 1;
2281
+ i += 2;
2282
+ continue;
2283
+ }
2284
+
2285
+ if (c === "{" || c === "[" || c === "(") {
2286
+ depth += 1;
2287
+ i += 1;
2288
+ continue;
2289
+ }
2290
+ if (c === "}" || c === "]" || c === ")") {
2291
+ depth -= 1;
2292
+ i += 1;
2293
+ continue;
2294
+ }
2295
+
2296
+ i += 1;
2297
+ }
2298
+
2299
+ return null;
2300
+ }
2301
+
2302
+ /** Last non-whitespace char strictly before index `i` (or `""`). */
2303
+ function lastNonSpaceBefore(s: string, i: number): string {
2304
+ let j = i - 1;
2305
+ while (j >= 0 && /\s/.test(s[j]!)) j -= 1;
2306
+ return j >= 0 ? s[j]! : "";
2307
+ }
2308
+
2309
+ /**
2310
+ * Given the start index of a value in an `x-data` literal, return the index
2311
+ * just past a *simple* literal value (single/double-quoted string with
2312
+ * escapes, boolean, or number). Returns `null` when the value is anything else
2313
+ * (an expression, function, object, array, template literal, etc.) so the
2314
+ * caller can fail safe rather than mangle it.
2315
+ */
2316
+ function simpleValueEnd(s: string, start: number): number | null {
2317
+ const c = s[start];
2318
+ if (c === "'" || c === '"') {
2319
+ let i = start + 1;
2320
+ while (i < s.length) {
2321
+ if (s[i] === "\\") {
2322
+ i += 2;
2323
+ continue;
2324
+ }
2325
+ if (s[i] === c) return i + 1;
2326
+ i += 1;
2327
+ }
2328
+ return null; // unterminated string
2329
+ }
2330
+ // Boolean / number: read the bare token, then confirm it is exactly one.
2331
+ const m = /^[A-Za-z0-9_.+-]+/.exec(s.slice(start));
2332
+ if (!m) return null;
2333
+ const token = m[0];
2334
+ const isBoolean = token === "true" || token === "false";
2335
+ const isNumber = /^-?\d+(\.\d+)?$/.test(token);
2336
+ if (!isBoolean && !isNumber) return null;
2337
+ return start + token.length;
2338
+ }
2339
+
2340
+ /**
2341
+ * True when an `x-data` literal can be rebuilt from its flat parsed map with
2342
+ * no loss — i.e. there is nothing richer than the simple `key: literal` pairs
2343
+ * that `serializeAlpineDataObject` already round-trips. Used as the gate for
2344
+ * falling back to a full rebuild when a surgical single-key replace is not
2345
+ * possible (e.g. when adding a brand-new key).
2346
+ *
2347
+ * Returns `true` for an empty / absent literal (nothing to lose) and for a
2348
+ * flat object whose `parse → serialize` round-trip is semantically stable.
2349
+ * Returns `false` when the original holds methods, nested objects, comments,
2350
+ * or expressions that a rebuild would silently drop.
2351
+ *
2352
+ * Pure — exported for tests.
2353
+ */
2354
+ export function canRebuildAlpineDataLosslessly(
2355
+ xData: string | null | undefined,
2356
+ ): boolean {
2357
+ const trimmed = (xData ?? "").trim();
2358
+ // No object literal at all → there is nothing richer to preserve.
2359
+ if (!trimmed || trimmed === "{}" || trimmed === "{ }") return true;
2360
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
2361
+
2362
+ const parsed = parseAlpineDataObject(trimmed);
2363
+ if (!parsed) return false;
2364
+
2365
+ // Re-serialize and re-parse; if the round-trip is stable AND the parsed map
2366
+ // accounts for every top-level key actually present in the original, a
2367
+ // rebuild loses nothing.
2368
+ const reserialized = serializeAlpineDataObject(parsed);
2369
+ const reparsed = parseAlpineDataObject(reserialized);
2370
+ if (!reparsed) return false;
2371
+ const keysA = Object.keys(parsed).sort().join(",");
2372
+ const keysB = Object.keys(reparsed).sort().join(",");
2373
+ if (keysA !== keysB) return false;
2374
+
2375
+ // Guard against dropped content the flat parser ignores (e.g. a trailing
2376
+ // method): the number of top-level `key:` tokens in the original must match
2377
+ // the number of parsed keys. Count top-level `:` separators conservatively.
2378
+ return countTopLevelKeys(trimmed) === Object.keys(parsed).length;
2379
+ }
2380
+
2381
+ /**
2382
+ * Count top-level `key:` entries in an `x-data` object literal, skipping
2383
+ * strings, comments, and nested braces/brackets/parens. A method like
2384
+ * `toggle() { … }` is counted as a key too (its `:`-less form still occupies a
2385
+ * top-level slot), so a mismatch against the flat parser's key count reveals
2386
+ * dropped content.
2387
+ */
2388
+ function countTopLevelKeys(xData: string): number {
2389
+ const s = xData;
2390
+ const n = s.length;
2391
+ let depth = 0;
2392
+ let i = 0;
2393
+ let count = 0;
2394
+ let sawTokenInSlot = false;
2395
+
2396
+ const skipString = (quote: string): void => {
2397
+ i += 1;
2398
+ while (i < n) {
2399
+ if (s[i] === "\\") {
2400
+ i += 2;
2401
+ continue;
2402
+ }
2403
+ if (s[i] === quote) {
2404
+ i += 1;
2405
+ return;
2406
+ }
2407
+ i += 1;
2408
+ }
2409
+ };
2410
+
2411
+ while (i < n) {
2412
+ const c = s[i]!;
2413
+ if (c === '"' || c === "'" || c === "`") {
2414
+ if (depth === 1) sawTokenInSlot = true;
2415
+ skipString(c);
2416
+ continue;
2417
+ }
2418
+ if (c === "/" && s[i + 1] === "/") {
2419
+ i += 2;
2420
+ while (i < n && s[i] !== "\n") i += 1;
2421
+ continue;
2422
+ }
2423
+ if (c === "/" && s[i + 1] === "*") {
2424
+ i += 2;
2425
+ while (i < n && !(s[i] === "*" && s[i + 1] === "/")) i += 1;
2426
+ i += 2;
2427
+ continue;
2428
+ }
2429
+ if (c === "{" || c === "[" || c === "(") {
2430
+ depth += 1;
2431
+ i += 1;
2432
+ continue;
2433
+ }
2434
+ if (c === "}" || c === "]" || c === ")") {
2435
+ if (depth === 1 && c === "}" && sawTokenInSlot) {
2436
+ count += 1;
2437
+ sawTokenInSlot = false;
2438
+ }
2439
+ depth -= 1;
2440
+ i += 1;
2441
+ continue;
2442
+ }
2443
+ if (depth === 1) {
2444
+ if (c === ",") {
2445
+ if (sawTokenInSlot) count += 1;
2446
+ sawTokenInSlot = false;
2447
+ } else if (!/\s/.test(c)) {
2448
+ sawTokenInSlot = true;
2449
+ }
2450
+ }
2451
+ i += 1;
2452
+ }
2453
+ return count;
2454
+ }
2455
+
2152
2456
  /** A boolean-ish prop value (`"true"` / `"false"`), case-insensitive. */
2153
2457
  export function isBooleanPropValue(value: string): boolean {
2154
2458
  const v = value.trim().toLowerCase();
@@ -2370,12 +2674,6 @@ function InspectorTabsHeader({
2370
2674
  >
2371
2675
  {t("designEditor.tweaks")}
2372
2676
  </TabsTrigger>
2373
- <TabsTrigger
2374
- value="extensions"
2375
- className="h-6 rounded-md px-1.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
2376
- >
2377
- {t("designEditor.extensions")}
2378
- </TabsTrigger>
2379
2677
  </TabsList>
2380
2678
  </Tabs>
2381
2679
  {trailing ? <div className="shrink-0">{trailing}</div> : null}
@@ -2387,15 +2685,19 @@ function SectionIconButton({
2387
2685
  label,
2388
2686
  onClick,
2389
2687
  children,
2688
+ activateOnPointerDown = false,
2390
2689
  disabled = false,
2391
2690
  className,
2392
2691
  }: {
2393
2692
  label: string;
2394
2693
  onClick?: () => void;
2395
2694
  children: ReactNode;
2695
+ activateOnPointerDown?: boolean;
2396
2696
  disabled?: boolean;
2397
2697
  className?: string;
2398
2698
  }) {
2699
+ const pointerActivatedRef = useRef(false);
2700
+
2399
2701
  return (
2400
2702
  <Tooltip>
2401
2703
  <TooltipTrigger asChild>
@@ -2408,7 +2710,22 @@ function SectionIconButton({
2408
2710
  className,
2409
2711
  )}
2410
2712
  disabled={disabled}
2411
- onClick={onClick}
2713
+ onPointerDown={(event) => {
2714
+ if (!activateOnPointerDown || disabled || event.button !== 0) {
2715
+ return;
2716
+ }
2717
+ pointerActivatedRef.current = true;
2718
+ event.preventDefault();
2719
+ event.stopPropagation();
2720
+ onClick?.();
2721
+ }}
2722
+ onClick={() => {
2723
+ if (pointerActivatedRef.current) {
2724
+ pointerActivatedRef.current = false;
2725
+ return;
2726
+ }
2727
+ onClick?.();
2728
+ }}
2412
2729
  aria-label={label}
2413
2730
  >
2414
2731
  {children}
@@ -3103,7 +3420,10 @@ function PageProperties({
3103
3420
  label={t("editPanel.labels.background")}
3104
3421
  value={styles.backgroundColor || ""}
3105
3422
  onChange={(v) => onStyleChange("backgroundColor", v)}
3106
- backgroundImage={styles.backgroundImage || ""}
3423
+ backgroundImage={styles.backgroundImage}
3424
+ backgroundSize={styles.backgroundSize}
3425
+ backgroundRepeat={styles.backgroundRepeat}
3426
+ backgroundPosition={styles.backgroundPosition}
3107
3427
  onBackgroundImageChange={(v) => onStyleChange("backgroundImage", v)}
3108
3428
  onImageFillChange={(value) =>
3109
3429
  commitStylePatch(
@@ -4240,6 +4560,15 @@ function FillProperties({
4240
4560
  const backgroundLayers = isTextElement
4241
4561
  ? []
4242
4562
  : splitCssLayers(styles.backgroundImage || "");
4563
+ const backgroundSizeLayers = isTextElement
4564
+ ? []
4565
+ : splitCssLayers(styles.backgroundSize || "");
4566
+ const backgroundRepeatLayers = isTextElement
4567
+ ? []
4568
+ : splitCssLayers(styles.backgroundRepeat || "");
4569
+ const backgroundPositionLayers = isTextElement
4570
+ ? []
4571
+ : splitCssLayers(styles.backgroundPosition || "");
4243
4572
  const hasBackgroundLayer = !isTextElement && backgroundLayers.length > 0;
4244
4573
  const hasVisibleFill =
4245
4574
  isTextElement || colorHasVisibleAlpha(fillValue) || hasBackgroundLayer;
@@ -4378,6 +4707,7 @@ function FillProperties({
4378
4707
  : t("editPanel.labels.hideLayer")
4379
4708
  }
4380
4709
  onClick={handleFillVisibilityToggle}
4710
+ activateOnPointerDown
4381
4711
  >
4382
4712
  {isHidden ? (
4383
4713
  <IconEyeOff className="size-3.5" />
@@ -4492,6 +4822,10 @@ function FillProperties({
4492
4822
  );
4493
4823
  }}
4494
4824
  paintType={gradient?.type ?? "image"}
4825
+ backgroundImage={layer}
4826
+ backgroundSize={backgroundSizeLayers[index]}
4827
+ backgroundRepeat={backgroundRepeatLayers[index]}
4828
+ backgroundPosition={backgroundPositionLayers[index]}
4495
4829
  gradientType={gradient?.type}
4496
4830
  onGradientTypeChange={(type) => {
4497
4831
  if (!gradient) return;
@@ -5236,8 +5570,8 @@ function MakeItRealCard({
5236
5570
  // height so the inspector doesn't jump when the data arrives.
5237
5571
  if (isLoading || !data) {
5238
5572
  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" />
5573
+ <div className="flex h-7 items-center rounded-[5px] bg-[var(--design-editor-control-bg)] px-2">
5574
+ <div className="h-3 w-28 animate-pulse rounded bg-muted/40" />
5241
5575
  </div>
5242
5576
  );
5243
5577
  }
@@ -5282,85 +5616,86 @@ function MakeItRealCard({
5282
5616
 
5283
5617
  if (migrateResult?.status === "processing" && migrateResult.url) {
5284
5618
  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">
5619
+ <div className="flex items-center gap-2 rounded-[5px] border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 py-1.5">
5620
+ <IconLoader2 className="size-3.5 shrink-0 animate-spin text-[var(--design-editor-accent-color)]" />
5621
+ <p className="min-w-0 flex-1 truncate text-[10px] text-muted-foreground">
5290
5622
  {migrateResult.message ??
5291
- `Branch "${migrateResult.branchName}" is being generated.`}
5623
+ `Generating ${migrateResult.branchName ?? "React app"}.`}
5292
5624
  </p>
5293
5625
  <a
5294
5626
  href={migrateResult.url}
5295
5627
  target="_blank"
5296
5628
  rel="noopener noreferrer"
5297
- className="inline-flex items-center gap-1 text-[10px] font-medium text-[var(--design-editor-accent-color)] hover:underline"
5629
+ className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md px-1.5 text-[10px] font-semibold text-[var(--design-editor-accent-color)] hover:bg-[var(--design-editor-panel-raised-bg)]"
5298
5630
  >
5299
- {"Open in Builder" /* i18n-ignore make-it-real card */}
5631
+ {"Open" /* i18n-ignore make-it-real card */}
5300
5632
  <IconExternalLink className="size-2.5" />
5301
5633
  </a>
5302
5634
  </div>
5303
5635
  );
5304
5636
  }
5305
5637
 
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>
5638
+ const summary =
5639
+ cta.kind === "configure-project"
5640
+ ? `Configure Builder to enable ${featureLabel}.`
5641
+ : `Connect Builder to enable ${featureLabel}.`;
5642
+ const primaryLabel =
5643
+ cta.kind === "configure-project"
5644
+ ? "Configure" /* i18n-ignore make-it-real card */
5645
+ : "Connect"; /* i18n-ignore make-it-real card */
5324
5646
 
5325
- {/* Error */}
5326
- {migrateError ? (
5327
- <p className="text-[10px] text-destructive">
5328
- {migrateError instanceof Error
5329
- ? migrateError.message
5330
- : "Migration failed. Please try again."}
5647
+ return (
5648
+ <div className="space-y-1">
5649
+ <div className="flex items-center gap-2 rounded-[5px] border border-dashed border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)]/70 px-2 py-1.5">
5650
+ <span
5651
+ className="size-1.5 shrink-0 rounded-full bg-[var(--design-editor-accent-color)]"
5652
+ aria-hidden="true"
5653
+ />
5654
+ <p
5655
+ className="min-w-0 flex-1 truncate text-[10px] text-muted-foreground"
5656
+ title={summary}
5657
+ >
5658
+ {summary}
5331
5659
  </p>
5332
- ) : null}
5333
-
5334
- {/* Actions */}
5335
- <div className="flex items-center gap-1.5 flex-wrap">
5336
- <button
5660
+ <Button
5337
5661
  type="button"
5662
+ size="sm"
5338
5663
  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"
5664
+ title={cta.primaryAction}
5665
+ className="h-6 shrink-0 gap-1 rounded-md bg-[var(--design-editor-accent-color)] px-1.5 text-[10px] font-semibold text-white hover:bg-[var(--design-editor-accent-hover-color)]"
5340
5666
  >
5341
- {cta.primaryAction}
5667
+ {primaryLabel}
5342
5668
  <IconArrowRight className="size-2.5" />
5343
- </button>
5669
+ </Button>
5344
5670
 
5345
5671
  {/* When Builder is fully connected, also offer direct migration */}
5346
5672
  {data.connected && data.builderEnabled && (
5347
- <button
5673
+ <Button
5348
5674
  type="button"
5675
+ variant="ghost"
5676
+ size="sm"
5349
5677
  onClick={handleMigrate}
5350
5678
  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"
5679
+ className="h-6 shrink-0 gap-1 rounded-md px-1.5 text-[10px] font-semibold text-muted-foreground hover:text-foreground disabled:cursor-wait disabled:opacity-60"
5352
5680
  >
5353
5681
  {isPending ? (
5354
5682
  <>
5355
5683
  <IconLoader2 className="size-2.5 animate-spin" />
5356
- {"Migrating…" /* i18n-ignore make-it-real card */}
5684
+ {"Generating" /* i18n-ignore make-it-real card */}
5357
5685
  </>
5358
5686
  ) : (
5359
- <>{"Generate React app" /* i18n-ignore make-it-real card */}</>
5687
+ <>{"Generate" /* i18n-ignore make-it-real card */}</>
5360
5688
  )}
5361
- </button>
5689
+ </Button>
5362
5690
  )}
5363
5691
  </div>
5692
+ {migrateError ? (
5693
+ <p className="px-2 text-[10px] text-destructive">
5694
+ {migrateError instanceof Error
5695
+ ? migrateError.message
5696
+ : "Migration failed. Please try again."}
5697
+ </p>
5698
+ ) : null}
5364
5699
  </div>
5365
5700
  );
5366
5701
  }
@@ -5372,6 +5707,7 @@ function MakeItRealCard({
5372
5707
  * typed here; the action may return additional fields.
5373
5708
  */
5374
5709
  interface ComponentDetailsResult {
5710
+ nodeId: string;
5375
5711
  name: string;
5376
5712
  sourceType: string;
5377
5713
  observedProps: Array<{ name: string; value: string }>;
@@ -5405,32 +5741,79 @@ interface ComponentDetailsResult {
5405
5741
  *
5406
5742
  * Matches the workbench artboard spec in DESIGN-STUDIO-PLAN.md §6.1.
5407
5743
  */
5408
- function ComponentSection({
5744
+ export function ComponentSection({
5409
5745
  designId,
5746
+ fileId,
5747
+ activeContent,
5748
+ activeFileUpdatedAt,
5410
5749
  nodeId,
5750
+ onComponentPropApplied,
5411
5751
  sourceCapabilities = [],
5412
5752
  }: {
5413
5753
  designId: string;
5754
+ fileId?: string;
5755
+ activeContent?: string;
5756
+ activeFileUpdatedAt?: string | null;
5414
5757
  nodeId: string;
5758
+ onComponentPropApplied?: (
5759
+ fileId: string,
5760
+ content: string,
5761
+ updatedAt?: string,
5762
+ ) => void;
5415
5763
  /** Capability names advertised by the current source. */
5416
5764
  sourceCapabilities?: string[];
5417
5765
  }) {
5418
5766
  const queryClient = useQueryClient();
5419
- const detailsKey = ["action", "get-component-details", { designId, nodeId }];
5767
+ const detailsParams = { designId, nodeId, ...(fileId ? { fileId } : {}) };
5768
+ const detailsKey = ["action", "get-component-details", detailsParams];
5769
+ const latestSourceRef = useRef<{
5770
+ content: string;
5771
+ revision?: string | null;
5772
+ }>({
5773
+ content: activeContent ?? "",
5774
+ revision: activeFileUpdatedAt ?? null,
5775
+ });
5420
5776
 
5421
- const { data, isLoading, error } = useActionQuery<ComponentDetailsResult>(
5422
- "get-component-details",
5423
- { designId, nodeId },
5424
- );
5777
+ useEffect(() => {
5778
+ latestSourceRef.current = {
5779
+ content: activeContent ?? "",
5780
+ revision: activeFileUpdatedAt ?? null,
5781
+ };
5782
+ }, [activeContent, activeFileUpdatedAt, fileId, nodeId]);
5783
+
5784
+ const { data, isLoading, error, refetch } =
5785
+ useActionQuery<ComponentDetailsResult>(
5786
+ "get-component-details",
5787
+ detailsParams,
5788
+ { refetchOnMount: "always" },
5789
+ );
5425
5790
 
5426
5791
  const openSourceMutation = useActionMutation("open-component-source");
5427
5792
  const applyPropMutation = useActionMutation("apply-component-prop-edit");
5428
5793
 
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.
5794
+ const postComponentPropPreview = useCallback(
5795
+ (attribute: string, value: string) => {
5796
+ if (typeof document === "undefined") return;
5797
+
5798
+ const iframe = document.querySelector<HTMLIFrameElement>(
5799
+ "iframe[data-design-preview-iframe]",
5800
+ );
5801
+ iframe?.contentWindow?.postMessage(
5802
+ {
5803
+ type: "style-change",
5804
+ selector: data?.instance?.selector ?? "",
5805
+ nodeId: data?.instance?.nodeId ?? nodeId,
5806
+ attributeOverrides: { [attribute]: value },
5807
+ },
5808
+ "*",
5809
+ );
5810
+ },
5811
+ [data?.instance?.nodeId, data?.instance?.selector, nodeId],
5812
+ );
5813
+
5814
+ // Persist a single prop change through apply-component-prop-edit. Attribute
5815
+ // props also preview immediately in the iframe so the selected component
5816
+ // changes without waiting for the write/refetch round-trip.
5434
5817
  const persistPropEdit = (
5435
5818
  edit:
5436
5819
  | { kind: "alpineData"; value: string }
@@ -5440,20 +5823,90 @@ function ComponentSection({
5440
5823
  queryClient.setQueryData<ComponentDetailsResult>(detailsKey, (prev) =>
5441
5824
  prev ? optimistic(prev) : prev,
5442
5825
  );
5826
+ if (edit.kind === "attribute") {
5827
+ postComponentPropPreview(edit.attribute, edit.value);
5828
+ }
5829
+ const latestSource = latestSourceRef.current;
5443
5830
  applyPropMutation.mutate(
5444
- { designId, nodeId, edit },
5445
5831
  {
5832
+ designId,
5833
+ nodeId,
5834
+ ...(fileId ? { fileId } : {}),
5835
+ edit,
5836
+ ...(latestSource.content
5837
+ ? {
5838
+ source: {
5839
+ currentContent: latestSource.content,
5840
+ ...(latestSource.revision
5841
+ ? { revision: latestSource.revision }
5842
+ : {}),
5843
+ },
5844
+ }
5845
+ : {}),
5846
+ },
5847
+ {
5848
+ onSuccess: (result) => {
5849
+ const response = result as {
5850
+ content?: unknown;
5851
+ fileId?: unknown;
5852
+ updatedAt?: unknown;
5853
+ conflict?: unknown;
5854
+ error?: unknown;
5855
+ };
5856
+ if (response.conflict) {
5857
+ toast.error(
5858
+ typeof response.error === "string"
5859
+ ? response.error
5860
+ : "This file changed since this component prop edit was prepared. Refresh and try again.",
5861
+ );
5862
+ return;
5863
+ }
5864
+ if (
5865
+ typeof response.fileId === "string" &&
5866
+ typeof response.content === "string"
5867
+ ) {
5868
+ const updatedAt =
5869
+ typeof response.updatedAt === "string"
5870
+ ? response.updatedAt
5871
+ : undefined;
5872
+ latestSourceRef.current = {
5873
+ content: response.content,
5874
+ revision: updatedAt ?? latestSourceRef.current.revision,
5875
+ };
5876
+ onComponentPropApplied?.(
5877
+ response.fileId,
5878
+ response.content,
5879
+ updatedAt,
5880
+ );
5881
+ }
5882
+ },
5446
5883
  onSettled: () => {
5447
- // Re-render the canvas from the new content and re-read the props.
5448
5884
  void queryClient.invalidateQueries({
5449
5885
  queryKey: ["action", "get-design"],
5450
5886
  });
5451
5887
  void queryClient.invalidateQueries({ queryKey: detailsKey });
5888
+ void refetch();
5452
5889
  },
5453
5890
  },
5454
5891
  );
5455
5892
  };
5456
5893
 
5894
+ useEffect(() => {
5895
+ if (typeof document === "undefined") return;
5896
+
5897
+ const handleMessage = (event: MessageEvent) => {
5898
+ if (
5899
+ (event.data as { type?: unknown } | null)?.type === "element-select"
5900
+ ) {
5901
+ void refetch();
5902
+ }
5903
+ };
5904
+ window.addEventListener("message", handleMessage);
5905
+ return () => {
5906
+ window.removeEventListener("message", handleMessage);
5907
+ };
5908
+ }, [refetch]);
5909
+
5457
5910
  // While loading, show a compact skeleton that matches the section width.
5458
5911
  if (isLoading) {
5459
5912
  return (
@@ -5491,7 +5944,7 @@ function ComponentSection({
5491
5944
  // Real-app sources keep the deeper source-prop controls gated as-is, so for
5492
5945
  // non-inline sources the controls are read-only here.
5493
5946
  const isInline = sourceType === "inline";
5494
- const editingEnabled = isInline; // gated; real-app stays read-only for now
5947
+ const editingEnabled = isInline && capabilities.canEditProps; // gated; real-app stays read-only for now
5495
5948
  const alpineData = parseAlpineDataObject(instance?.alpineData);
5496
5949
 
5497
5950
  // Each editable row: name + current value + how it persists + its options.
@@ -5552,22 +6005,51 @@ function ComponentSection({
5552
6005
  if (!editingEnabled || nextValue === row.value) return;
5553
6006
 
5554
6007
  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
- }));
6008
+ // Surgically replace only the edited key's value inside the original
6009
+ // x-data string so methods, nested objects, escaped strings, quoted
6010
+ // keys, and whitespace survive byte-for-byte. A full
6011
+ // parse→mutate→serialize round-trip would drop anything
6012
+ // parseAlpineDataObject can't model (e.g. `toggle() { }`).
6013
+ const original = instance?.alpineData ?? "";
6014
+ const surgical = replaceAlpineDataKeyValue(original, row.name, nextValue);
6015
+
6016
+ let serialized: string;
6017
+ if (surgical != null) {
6018
+ serialized = surgical;
6019
+ } else if (canRebuildAlpineDataLosslessly(original)) {
6020
+ // The key isn't present yet (or there is no original literal). Rebuild
6021
+ // from the flat map — safe here precisely because the original holds
6022
+ // nothing richer than the flat literals serialize already preserves.
6023
+ const nextData = { ...(alpineData ?? {}), [row.name]: nextValue };
6024
+ serialized = serializeAlpineDataObject(nextData);
6025
+ } else {
6026
+ // The original carries content (methods / nested / expressions) we
6027
+ // can't rewrite for this key without dropping it. Fail safe: skip the
6028
+ // edit rather than persist a lossy rewrite, and tell the user why so
6029
+ // the change doesn't silently vanish.
6030
+ toast.error(
6031
+ // i18n-ignore
6032
+ "Can’t safely edit this prop inline — this component’s Alpine state is too complex. Edit the source instead.",
6033
+ );
6034
+ return;
6035
+ }
6036
+
6037
+ const nextSerialized = serialized;
6038
+ persistPropEdit(
6039
+ { kind: "alpineData", value: nextSerialized },
6040
+ (prev) => ({
6041
+ ...prev,
6042
+ instance: { ...(prev.instance ?? {}), alpineData: nextSerialized },
6043
+ observedProps: prev.observedProps.map((p) =>
6044
+ p.name === row.name ? { ...p, value: nextValue } : p,
6045
+ ),
6046
+ }),
6047
+ );
5564
6048
  } else {
5565
- // camelCase prop name → data-agent-native-prop-<kebab>
5566
- const kebab = row.name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
5567
6049
  persistPropEdit(
5568
6050
  {
5569
6051
  kind: "attribute",
5570
- attribute: `data-agent-native-prop-${kebab}`,
6052
+ attribute: propNameToDataAttribute(row.name),
5571
6053
  value: nextValue,
5572
6054
  },
5573
6055
  (prev) => {
@@ -5624,7 +6106,11 @@ function ComponentSection({
5624
6106
  "Edit component source" /* i18n-ignore design inspector action */
5625
6107
  }
5626
6108
  onClick={() => {
5627
- openSourceMutation.mutate({ designId, nodeId });
6109
+ openSourceMutation.mutate({
6110
+ designId,
6111
+ nodeId,
6112
+ ...(fileId ? { fileId } : {}),
6113
+ });
5628
6114
  }}
5629
6115
  >
5630
6116
  <IconExternalLink className="size-3.5" />
@@ -5667,6 +6153,7 @@ function ComponentSection({
5667
6153
  {rows.map((row) => {
5668
6154
  const hasOptions = (row.options?.length ?? 0) > 0;
5669
6155
  const isBoolean = !hasOptions && isBooleanPropValue(row.value);
6156
+ const disabled = !editingEnabled || applyPropMutation.isPending;
5670
6157
  return (
5671
6158
  <div key={row.name} className="flex items-center gap-1.5">
5672
6159
  <Label className="w-[64px] shrink-0 truncate text-[11px] font-medium capitalize text-muted-foreground">
@@ -5677,7 +6164,7 @@ function ComponentSection({
5677
6164
  <Select
5678
6165
  value={row.value || row.options![0] || ""}
5679
6166
  onValueChange={(v) => commitProp(row, v)}
5680
- disabled={!editingEnabled}
6167
+ disabled={disabled}
5681
6168
  >
5682
6169
  <SelectTrigger className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)]">
5683
6170
  <SelectValue />
@@ -5702,7 +6189,7 @@ function ComponentSection({
5702
6189
  onCheckedChange={(checked) =>
5703
6190
  commitProp(row, checked ? "true" : "false")
5704
6191
  }
5705
- disabled={!editingEnabled}
6192
+ disabled={disabled}
5706
6193
  className="h-4 w-7 [&>span]:size-3 [&>span]:data-[state=checked]:translate-x-3"
5707
6194
  aria-label={
5708
6195
  row.name /* i18n-ignore dynamic prop name */
@@ -5714,7 +6201,7 @@ function ComponentSection({
5714
6201
  <Input
5715
6202
  defaultValue={row.value}
5716
6203
  key={`${row.name}:${row.value}`}
5717
- disabled={!editingEnabled}
6204
+ disabled={disabled}
5718
6205
  onBlur={(e) => commitProp(row, e.target.value)}
5719
6206
  onKeyDown={(e) => {
5720
6207
  if (e.key === "Enter") {
@@ -5752,7 +6239,6 @@ export function EditPanel({
5752
6239
  onActiveTabChange,
5753
6240
  tweaks = [],
5754
6241
  tweakValues = {},
5755
- extensionContext,
5756
6242
  onTweakChange,
5757
6243
  onRequestTweaks,
5758
6244
  onStyleChange,
@@ -5763,8 +6249,10 @@ export function EditPanel({
5763
6249
  readOnly = false,
5764
6250
  fileId,
5765
6251
  filename,
6252
+ activeContent,
6253
+ activeFileUpdatedAt,
5766
6254
  designId,
5767
- onTokensApplied,
6255
+ onComponentPropApplied,
5768
6256
  statesPanelProps,
5769
6257
  reviewPanelProps,
5770
6258
  componentNodeId,
@@ -5779,12 +6267,6 @@ export function EditPanel({
5779
6267
  DEFAULT_EXPORT_SETTINGS,
5780
6268
  );
5781
6269
  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
6270
 
5789
6271
  const selectedElementKey = selectedElement
5790
6272
  ? elementIdentityKey(selectedElement)
@@ -5802,12 +6284,8 @@ export function EditPanel({
5802
6284
  },
5803
6285
  [onTweakChange],
5804
6286
  );
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
6287
  const handleRequestTweaks = useCallback(
5809
6288
  (anchor: HTMLElement) => {
5810
- setTweaksSubTab("tweaks");
5811
6289
  onRequestTweaks?.(anchor);
5812
6290
  },
5813
6291
  [onRequestTweaks],
@@ -5825,6 +6303,7 @@ export function EditPanel({
5825
6303
  // over-scroll bounce, could briefly un-shield the canvas and allow a stray
5826
6304
  // pointer event to deselect the selected canvas element (R3 regression).
5827
6305
  const scrolledRecentlyRef = useRef(false);
6306
+ const userScrollIntentRef = useRef(false);
5828
6307
  const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
5829
6308
 
5830
6309
  if (readOnly) {
@@ -5904,7 +6383,14 @@ export function EditPanel({
5904
6383
 
5905
6384
  <div
5906
6385
  className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain"
6386
+ onWheelCapture={() => {
6387
+ userScrollIntentRef.current = true;
6388
+ }}
6389
+ onTouchMoveCapture={() => {
6390
+ userScrollIntentRef.current = true;
6391
+ }}
5907
6392
  onScroll={() => {
6393
+ if (!userScrollIntentRef.current) return;
5908
6394
  // Mark that a scroll just happened so the click that some
5909
6395
  // browsers fire at the end of a scroll gesture (or after an
5910
6396
  // overscroll/rubber-band bounce) is suppressed. Crucially this
@@ -5919,6 +6405,7 @@ export function EditPanel({
5919
6405
  }
5920
6406
  scrollTimerRef.current = setTimeout(() => {
5921
6407
  scrolledRecentlyRef.current = false;
6408
+ userScrollIntentRef.current = false;
5922
6409
  scrollTimerRef.current = null;
5923
6410
  }, 300);
5924
6411
  }}
@@ -5930,6 +6417,7 @@ export function EditPanel({
5930
6417
  // browsers generate after a touch-scroll ends.
5931
6418
  if (!scrolledRecentlyRef.current) return;
5932
6419
  scrolledRecentlyRef.current = false;
6420
+ userScrollIntentRef.current = false;
5933
6421
  if (scrollTimerRef.current !== null) {
5934
6422
  clearTimeout(scrollTimerRef.current);
5935
6423
  scrollTimerRef.current = null;
@@ -5971,7 +6459,11 @@ export function EditPanel({
5971
6459
  {designId && componentNodeId && (
5972
6460
  <ComponentSection
5973
6461
  designId={designId}
6462
+ fileId={fileId}
6463
+ activeContent={activeContent}
6464
+ activeFileUpdatedAt={activeFileUpdatedAt}
5974
6465
  nodeId={componentNodeId}
6466
+ onComponentPropApplied={onComponentPropApplied}
5975
6467
  sourceCapabilities={sourceCapabilities}
5976
6468
  />
5977
6469
  )}
@@ -6160,132 +6652,41 @@ export function EditPanel({
6160
6652
  </>
6161
6653
  ) : activeTab === "tweaks" ? (
6162
6654
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
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
- )}
6655
+ <div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
6656
+ <h3 className="min-w-0 flex-1 truncate text-[13px] font-semibold text-foreground">
6657
+ {t("designEditor.tweaks")}
6658
+ </h3>
6659
+ {onRequestTweaks ? (
6660
+ <Tooltip>
6661
+ <TooltipTrigger asChild>
6662
+ <Button
6663
+ type="button"
6664
+ variant="ghost"
6665
+ size="icon"
6666
+ className="size-7 rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
6667
+ aria-label={t("designEditor.addTweaks")}
6668
+ onClick={(event) =>
6669
+ handleRequestTweaks(event.currentTarget)
6670
+ }
6671
+ >
6672
+ <IconPlus className="size-3.5" />
6673
+ </Button>
6674
+ </TooltipTrigger>
6675
+ <TooltipContent>{t("designEditor.addTweaks")}</TooltipContent>
6676
+ </Tooltip>
6677
+ ) : null}
6678
+ </div>
6223
6679
 
6224
6680
  <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
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
- )}
6681
+ <TweaksPanelContent
6682
+ tweaks={tweaks}
6683
+ values={tweakValues}
6684
+ onChange={handleTweakChange}
6685
+ onRequestTweaks={handleRequestTweaks}
6686
+ className="px-3 py-3"
6687
+ />
6285
6688
  </div>
6286
6689
  </div>
6287
- ) : extensionContext ? (
6288
- <DesignExtensionsPanel context={extensionContext} />
6289
6690
  ) : null}
6290
6691
  </div>
6291
6692
  );