@agent-native/core 0.80.7 → 0.80.9

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 (302) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +18 -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/mcp.ts +10 -0
  16. package/corpus/core/src/cli/skills.ts +58 -12
  17. package/corpus/core/src/client/AssistantChat.tsx +70 -27
  18. package/corpus/core/src/client/analytics.ts +3 -1
  19. package/corpus/core/src/client/error-format.ts +25 -0
  20. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
  21. package/corpus/core/src/client/guided-questions.tsx +32 -4
  22. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
  23. package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
  24. package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
  25. package/corpus/core/src/client/session-replay.ts +34 -0
  26. package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
  27. package/corpus/core/src/coding-tools/index.ts +1 -1
  28. package/corpus/core/src/mcp/index.ts +2 -0
  29. package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
  30. package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
  31. package/corpus/core/src/mcp-client/index.ts +9 -0
  32. package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
  33. package/corpus/core/src/onboarding/types.ts +7 -0
  34. package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
  35. package/corpus/core/src/server/agents-bundle.ts +35 -7
  36. package/corpus/core/src/styles/agent-native.css +5 -0
  37. package/corpus/core/src/templates/default/app/global.css +35 -35
  38. package/corpus/templates/analytics/AGENTS.md +6 -0
  39. package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
  40. package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
  41. package/corpus/templates/analytics/app/global.css +144 -40
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
  44. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  45. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
  46. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
  47. package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  49. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
  50. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
  51. package/corpus/templates/assets/app/global.css +99 -35
  52. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  53. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
  54. package/corpus/templates/assets/app/routes/library.tsx +4 -4
  55. package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
  56. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  57. package/corpus/templates/brain/app/global.css +63 -35
  58. package/corpus/templates/brain/app/routes/search.tsx +1 -1
  59. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  60. package/corpus/templates/brain/app/routes/sources.tsx +2 -2
  61. package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
  62. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
  63. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
  64. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
  65. package/corpus/templates/calendar/app/global.css +69 -35
  66. package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  67. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/chat/app/global.css +35 -35
  69. package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  70. package/corpus/templates/clips/AGENTS.md +13 -0
  71. package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
  72. package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
  73. package/corpus/templates/clips/actions/view-screen.ts +2 -2
  74. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  75. package/corpus/templates/clips/app/global.css +47 -35
  76. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
  77. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
  78. package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
  80. package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
  81. package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
  82. package/corpus/templates/clips/desktop/src/app.tsx +290 -1
  83. package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
  84. package/corpus/templates/clips/desktop/src/styles.css +67 -52
  85. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
  87. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
  88. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
  89. package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
  90. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +69 -16
  91. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
  92. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  93. package/corpus/templates/content/app/global.css +29 -15
  94. package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  95. package/corpus/templates/content/changelog/2026-06-29-read-only-pages-no-longer-try-to-autosave-or-open-editor-onl.md +6 -0
  96. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
  97. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  98. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
  99. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
  100. package/corpus/templates/design/AGENTS.md +24 -14
  101. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
  102. package/corpus/templates/design/actions/apply-visual-edit.ts +24 -4
  103. package/corpus/templates/design/actions/edit-design.ts +6 -3
  104. package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
  105. package/corpus/templates/design/actions/generate-design.ts +6 -21
  106. package/corpus/templates/design/actions/generate-screens.ts +48 -5
  107. package/corpus/templates/design/actions/get-design.ts +1 -0
  108. package/corpus/templates/design/actions/present-design-variants.ts +355 -30
  109. package/corpus/templates/design/actions/update-design.ts +47 -10
  110. package/corpus/templates/design/actions/update-file.ts +33 -8
  111. package/corpus/templates/design/actions/view-screen.ts +89 -31
  112. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +389 -71
  113. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
  114. package/corpus/templates/design/app/components/design/EditPanel.tsx +596 -95
  115. package/corpus/templates/design/app/components/design/LayersPanel.tsx +428 -221
  116. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1197 -228
  117. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +94 -217
  118. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +28 -14
  119. package/corpus/templates/design/app/components/design/index.ts +0 -1
  120. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +228 -170
  121. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +140 -13
  122. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +39 -8
  123. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -2
  124. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +75 -5
  125. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
  126. package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
  127. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +17 -4
  128. package/corpus/templates/design/app/components/design/types.ts +9 -0
  129. package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
  130. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +448 -259
  131. package/corpus/templates/design/app/global.css +77 -49
  132. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  133. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +8 -1
  134. package/corpus/templates/design/app/i18n/zh-TW.ts +4 -0
  135. package/corpus/templates/design/app/i18n-data.ts +55 -3
  136. package/corpus/templates/design/app/pages/DesignEditor.tsx +2783 -658
  137. package/corpus/templates/design/app/root.tsx +1 -1
  138. package/corpus/templates/design/changelog/2026-06-29-agent-edits-to-a-design-are-no-longer-occasionally-dropped-o.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-29-box-shadow-and-gradient-colors-keep-named-css-colors-like-re.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-29-cut-cmd-ctrl-x-now-removes-the-selected-element-to-the-clipb.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-29-deleting-with-multiple-layers-selected-in-the-layers-panel-n.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
  147. package/corpus/templates/design/changelog/2026-06-29-duplicating-a-screen-now-gives-the-copy-its-own-layer-ids-so.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-29-hiding-and-re-showing-a-gradient-fill-now-preserves-each-sto.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-29-picking-a-design-direction-now-submits-to-the-agent-right-aw.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-29-style-changes-that-set-several-properties-at-once-fixed-size.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
  171. package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
  172. package/corpus/templates/design/changelog/2026-06-29-the-editor-no-longer-crashes-when-a-screen-ends-up-with-dupl.md +6 -0
  173. package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
  174. package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
  175. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
  176. package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
  177. package/corpus/templates/design/e2e/README.md +4 -3
  178. package/corpus/templates/design/e2e/global-setup.ts +24 -23
  179. package/corpus/templates/design/e2e/helpers.ts +122 -11
  180. package/corpus/templates/design/playwright.config.ts +19 -4
  181. package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
  182. package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
  183. package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
  184. package/corpus/templates/design/shared/code-layer.ts +125 -22
  185. package/corpus/templates/design/shared/color-utils.ts +159 -0
  186. package/corpus/templates/design/shared/generation-session.ts +42 -0
  187. package/corpus/templates/dispatch/app/global.css +79 -35
  188. package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
  189. package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  190. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
  191. package/corpus/templates/forms/app/global.css +64 -35
  192. package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
  193. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
  194. package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
  195. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
  196. package/corpus/templates/macros/app/global.css +25 -9
  197. package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  198. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
  199. package/corpus/templates/mail/app/global.css +42 -36
  200. package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
  201. package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
  202. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
  203. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
  204. package/corpus/templates/plan/app/global.css +74 -42
  205. package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
  206. package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
  207. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
  208. package/corpus/templates/slides/app/global.css +49 -33
  209. package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  210. package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
  211. package/corpus/templates/videos/app/global.css +35 -35
  212. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  213. package/dist/cli/mcp.d.ts.map +1 -1
  214. package/dist/cli/mcp.js +10 -0
  215. package/dist/cli/mcp.js.map +1 -1
  216. package/dist/cli/skills.d.ts.map +1 -1
  217. package/dist/cli/skills.js +58 -12
  218. package/dist/cli/skills.js.map +1 -1
  219. package/dist/client/AssistantChat.d.ts.map +1 -1
  220. package/dist/client/AssistantChat.js +54 -24
  221. package/dist/client/AssistantChat.js.map +1 -1
  222. package/dist/client/analytics.d.ts.map +1 -1
  223. package/dist/client/analytics.js +2 -1
  224. package/dist/client/analytics.js.map +1 -1
  225. package/dist/client/error-format.d.ts.map +1 -1
  226. package/dist/client/error-format.js +17 -0
  227. package/dist/client/error-format.js.map +1 -1
  228. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  229. package/dist/client/extensions/ExtensionViewer.js +3 -4
  230. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  231. package/dist/client/guided-questions.d.ts +4 -0
  232. package/dist/client/guided-questions.d.ts.map +1 -1
  233. package/dist/client/guided-questions.js +16 -7
  234. package/dist/client/guided-questions.js.map +1 -1
  235. package/dist/client/onboarding/OnboardingPanel.js +26 -1
  236. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  237. package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
  238. package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
  239. package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
  240. package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
  241. package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
  242. package/dist/client/resources/use-builtin-capabilities.js +2 -1
  243. package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
  244. package/dist/client/session-replay.d.ts.map +1 -1
  245. package/dist/client/session-replay.js +31 -0
  246. package/dist/client/session-replay.js.map +1 -1
  247. package/dist/client/sharing/ShareButton.d.ts +14 -0
  248. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  249. package/dist/client/sharing/ShareButton.js +36 -7
  250. package/dist/client/sharing/ShareButton.js.map +1 -1
  251. package/dist/coding-tools/index.js +1 -1
  252. package/dist/coding-tools/index.js.map +1 -1
  253. package/dist/collab/routes.d.ts +1 -1
  254. package/dist/mcp/index.d.ts +2 -0
  255. package/dist/mcp/index.d.ts.map +1 -1
  256. package/dist/mcp/index.js +1 -0
  257. package/dist/mcp/index.js.map +1 -1
  258. package/dist/mcp/screen-memory-stdio.d.ts +6 -0
  259. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
  260. package/dist/mcp/screen-memory-stdio.js +213 -0
  261. package/dist/mcp/screen-memory-stdio.js.map +1 -0
  262. package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
  263. package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
  264. package/dist/mcp-client/builtin-capabilities.js +10 -0
  265. package/dist/mcp-client/builtin-capabilities.js.map +1 -1
  266. package/dist/mcp-client/index.d.ts +1 -0
  267. package/dist/mcp-client/index.d.ts.map +1 -1
  268. package/dist/mcp-client/index.js +1 -0
  269. package/dist/mcp-client/index.js.map +1 -1
  270. package/dist/mcp-client/screen-memory-local.d.ts +61 -0
  271. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
  272. package/dist/mcp-client/screen-memory-local.js +336 -0
  273. package/dist/mcp-client/screen-memory-local.js.map +1 -0
  274. package/dist/onboarding/types.d.ts +7 -0
  275. package/dist/onboarding/types.d.ts.map +1 -1
  276. package/dist/onboarding/types.js.map +1 -1
  277. package/dist/resources/handlers.d.ts +2 -2
  278. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  279. package/dist/server/agent-chat-plugin.js +26 -0
  280. package/dist/server/agent-chat-plugin.js.map +1 -1
  281. package/dist/server/agents-bundle.d.ts.map +1 -1
  282. package/dist/server/agents-bundle.js +22 -7
  283. package/dist/server/agents-bundle.js.map +1 -1
  284. package/dist/server/transcribe-voice.d.ts +1 -1
  285. package/dist/styles/agent-native.css +5 -0
  286. package/dist/templates/default/app/global.css +35 -35
  287. package/docs/content/locales/ar-SA/template-design.mdx +6 -0
  288. package/docs/content/locales/de-DE/template-design.mdx +8 -0
  289. package/docs/content/locales/es-ES/template-design.mdx +10 -2
  290. package/docs/content/locales/fr-FR/template-design.mdx +10 -2
  291. package/docs/content/locales/hi-IN/template-design.mdx +7 -0
  292. package/docs/content/locales/ja-JP/template-design.mdx +7 -0
  293. package/docs/content/locales/ko-KR/template-design.mdx +7 -0
  294. package/docs/content/locales/pt-BR/template-design.mdx +9 -2
  295. package/docs/content/locales/zh-CN/template-design.mdx +6 -0
  296. package/docs/content/locales/zh-TW/template-design.mdx +6 -0
  297. package/docs/content/template-design.mdx +9 -2
  298. package/package.json +1 -1
  299. package/src/templates/default/app/global.css +35 -35
  300. package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
  301. package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
  302. package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -258
@@ -14,6 +14,7 @@ import type { ShaderDescriptor } from "@shared/shader-presets";
14
14
  import { IconChevronDown, IconColorPicker } from "@tabler/icons-react";
15
15
  import {
16
16
  useEffect,
17
+ useMemo,
17
18
  useRef,
18
19
  useState,
19
20
  type JSX,
@@ -130,6 +131,7 @@ export interface DesignColorPickerProps {
130
131
  value: string;
131
132
  onChange: (value: string) => void;
132
133
  onPaintValueChange?: (value: string) => void;
134
+ onImageFillChange?: (value: ImageFillValue) => void;
133
135
  label?: string;
134
136
  opacity?: number;
135
137
  onOpacityChange?: (opacity: number) => void;
@@ -191,6 +193,87 @@ interface HsvaColor {
191
193
 
192
194
  const FALLBACK_COLOR: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
193
195
 
196
+ // ─── Extended CSS color parser ──────────────────────────────────────────────────
197
+ //
198
+ // `parseCssColor` from color-utils handles hex, comma-separated rgb/rgba, and
199
+ // hsl/hsla. Browsers increasingly emit modern CSS Level 4 formats from
200
+ // getComputedStyle: space-separated `rgb(R G B)`, `rgb(R G B / A)`, and
201
+ // opaque formats like `oklch(...)` or `color(display-p3 ...)`.
202
+ //
203
+ // This local wrapper extends the parser to cover those cases so that colors
204
+ // arriving from the canvas's computed-style bridge are always usable.
205
+
206
+ const MODERN_RGB_PATTERN =
207
+ /^rgba?\(\s*([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)(?:\s*\/\s*([0-9.]+%?))?\s*\)$/i;
208
+
209
+ /** Canvas element reused across calls for DOM-based color resolution. */
210
+ let _resolverCanvas: HTMLCanvasElement | null = null;
211
+ let _resolverCtx: CanvasRenderingContext2D | null = null;
212
+
213
+ /**
214
+ * Parses a CSS color string into RgbaColor, extending the base parser with:
215
+ * - Modern space-separated `rgb(R G B)` / `rgb(R G B / A)` syntax
216
+ * - Opaque formats (oklch, color, etc.) resolved via a hidden canvas
217
+ *
218
+ * Falls back to null if the value is unparseable and the DOM is unavailable.
219
+ */
220
+ function parseCssColorExtended(value: string): RgbaColor | null {
221
+ // 1. Try the standard parser first (handles hex, comma rgb/rgba, hsl/hsla).
222
+ const standard = parseCssColor(value);
223
+ if (standard) return standard;
224
+
225
+ const trimmed = value.trim();
226
+ if (!trimmed || trimmed === "transparent" || trimmed === "none") return null;
227
+
228
+ // 2. Modern space-separated rgb/rgba — CSS Level 4.
229
+ const modernRgb = trimmed.match(MODERN_RGB_PATTERN);
230
+ if (modernRgb) {
231
+ const parseAlphaLocal = (v: string | undefined): number => {
232
+ if (!v) return 1;
233
+ if (v.endsWith("%"))
234
+ return Math.max(0, Math.min(1, Number(v.slice(0, -1)) / 100));
235
+ return Math.max(0, Math.min(1, Number(v)));
236
+ };
237
+ return {
238
+ r: Math.round(Math.max(0, Math.min(255, Number(modernRgb[1])))),
239
+ g: Math.round(Math.max(0, Math.min(255, Number(modernRgb[2])))),
240
+ b: Math.round(Math.max(0, Math.min(255, Number(modernRgb[3])))),
241
+ a: parseAlphaLocal(modernRgb[4]),
242
+ };
243
+ }
244
+
245
+ // 3. DOM-based resolver for oklch, color(display-p3 ...), hsl (modern), etc.
246
+ // Uses a hidden 1×1 canvas to resolve any valid CSS color to rgb().
247
+ if (typeof document === "undefined") return null;
248
+ try {
249
+ if (!_resolverCanvas) {
250
+ _resolverCanvas = document.createElement("canvas");
251
+ _resolverCanvas.width = 1;
252
+ _resolverCanvas.height = 1;
253
+ }
254
+ if (!_resolverCtx) {
255
+ _resolverCtx = _resolverCanvas.getContext("2d", {
256
+ willReadFrequently: true,
257
+ });
258
+ }
259
+ const ctx = _resolverCtx;
260
+ if (!ctx) return null;
261
+ // Detect invalid color values: save fillStyle before and after assignment.
262
+ // If the browser rejects the value, fillStyle won't change.
263
+ const prev = ctx.fillStyle;
264
+ ctx.fillStyle = trimmed;
265
+ const next = ctx.fillStyle; // browser normalises to rgb/hex on accept
266
+ // If the value was rejected, fillStyle stays at the previous value.
267
+ if (next === prev) return null;
268
+ ctx.clearRect(0, 0, 1, 1);
269
+ ctx.fillRect(0, 0, 1, 1);
270
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
271
+ return { r, g, b, a: a / 255 };
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+
194
277
  const DEFAULT_LABELS: DesignColorPickerLabels = {
195
278
  trigger: "Open color picker", // i18n-ignore fallback component label
196
279
  hex: "Hex", // i18n-ignore fallback component label
@@ -575,6 +658,7 @@ export function DesignColorPicker({
575
658
  value,
576
659
  onChange,
577
660
  onPaintValueChange,
661
+ onImageFillChange,
578
662
  label: _label,
579
663
  opacity,
580
664
  onOpacityChange,
@@ -605,7 +689,7 @@ export function DesignColorPicker({
605
689
  className,
606
690
  }: DesignColorPickerProps) {
607
691
  const copy = { ...DEFAULT_LABELS, ...labels };
608
- const color = parseCssColor(value) ?? FALLBACK_COLOR;
692
+ const color = parseCssColorExtended(value) ?? FALLBACK_COLOR;
609
693
  const hsv = rgbaToHsv(color);
610
694
  const hsl = rgbaToHsl(color);
611
695
  const effectiveOpacity = opacity ?? alphaToOpacity(color.a);
@@ -651,16 +735,24 @@ export function DesignColorPicker({
651
735
 
652
736
  // Resolve the active gradient: prefer EditPanel-driven props; otherwise parse
653
737
  // the live CSS value, falling back to local edit state.
654
- const parsedGradient = parseGradientCss(value, gradientType ?? "linear");
738
+ const parsedGradient = useMemo(
739
+ () => parseGradientCss(value, gradientType ?? "linear"),
740
+ [gradientType, value],
741
+ );
742
+ const fallbackGradient = useMemo(
743
+ () =>
744
+ GRADIENT_TYPES.has(effectivePaintType)
745
+ ? defaultGradient(
746
+ effectivePaintType as GradientKind,
747
+ toCssColor(color) || "#000000",
748
+ )
749
+ : null,
750
+ [color.r, color.g, color.b, color.a, effectivePaintType],
751
+ );
655
752
  const activeGradient: GradientValue | null = GRADIENT_TYPES.has(
656
753
  effectivePaintType,
657
754
  )
658
- ? (localGradient ??
659
- parsedGradient ??
660
- defaultGradient(
661
- effectivePaintType as GradientKind,
662
- toCssColor(color) || "#000000",
663
- ))
755
+ ? (localGradient ?? parsedGradient ?? fallbackGradient)
664
756
  : null;
665
757
 
666
758
  useEffect(() => {
@@ -754,7 +846,8 @@ export function DesignColorPicker({
754
846
 
755
847
  // The 2D field edits the selected gradient stop's color when in gradient mode.
756
848
  const fieldColor: RgbaColor = activeGradient
757
- ? (parseCssColor(selectedStop?.color ?? "#000000") ?? FALLBACK_COLOR)
849
+ ? (parseCssColorExtended(selectedStop?.color ?? "#000000") ??
850
+ FALLBACK_COLOR)
758
851
  : color;
759
852
  const rawFieldHsv = rgbaToHsv(fieldColor);
760
853
  // Preserve the last non-zero hue so dragging through gray doesn't lose it.
@@ -771,7 +864,7 @@ export function DesignColorPicker({
771
864
  const selectedStopColor = selectedStop?.color;
772
865
  useEffect(() => {
773
866
  if (!activeGradient || !selectedStopColor) return;
774
- const parsed = parseCssColor(selectedStopColor);
867
+ const parsed = parseCssColorExtended(selectedStopColor);
775
868
  if (parsed) setHexDraft(toDisplayHex(parsed));
776
869
  // eslint-disable-next-line react-hooks/exhaustive-deps
777
870
  }, [selectedStopColor, selectedStopId]);
@@ -807,6 +900,10 @@ export function DesignColorPicker({
807
900
 
808
901
  const emitImageFill = (next: ImageFillValue) => {
809
902
  setImageFill(next);
903
+ if (onImageFillChange) {
904
+ onImageFillChange(next);
905
+ return;
906
+ }
810
907
  emitPaintValue(imageFillToCss(next));
811
908
  };
812
909
 
@@ -860,6 +957,10 @@ export function DesignColorPicker({
860
957
  return;
861
958
  }
862
959
  if (nextType === "image") {
960
+ if (onImageFillChange && imageFill.url) {
961
+ onImageFillChange(imageFill);
962
+ return;
963
+ }
863
964
  emitPaintValue(imageFill.url ? imageFillToCss(imageFill) : "transparent");
864
965
  return;
865
966
  }
@@ -889,7 +990,17 @@ export function DesignColorPicker({
889
990
  setPicking(true);
890
991
  try {
891
992
  const result = await new EyeDropperCtor().open();
892
- if (result.sRGBHex) onChange(result.sRGBHex);
993
+ if (result.sRGBHex) {
994
+ if (activeGradient) {
995
+ // In gradient mode, route to the selected stop (preserving its alpha)
996
+ // like every other color edit — don't replace the whole gradient with
997
+ // a solid hex.
998
+ const parsed = parseCssColor(result.sRGBHex);
999
+ if (parsed) emitStopColor({ ...parsed, a: fieldColor.a });
1000
+ } else {
1001
+ onChange(result.sRGBHex);
1002
+ }
1003
+ }
893
1004
  } catch {
894
1005
  // Browser cancels are expected; keep the current color.
895
1006
  } finally {
@@ -1050,8 +1161,18 @@ export function DesignColorPicker({
1050
1161
  align="start"
1051
1162
  sideOffset={8}
1052
1163
  className="z-[10000] w-[252px] p-0 shadow-xl"
1164
+ // Keep the picker open when the style change triggered by a paint-type
1165
+ // switch causes the canvas to re-project the element. Without this,
1166
+ // Radix treats the resulting focus shift as an "interact outside" event
1167
+ // and closes the popover before the type switch is visible.
1168
+ onInteractOutside={(e) => {
1169
+ // Allow closing only for genuine pointer clicks on the canvas area
1170
+ // (the user clicked somewhere else). Programmatic focus changes from
1171
+ // the canvas bridge (element re-projection) should not close the picker.
1172
+ if (e.type === "focusoutside") e.preventDefault();
1173
+ }}
1053
1174
  >
1054
- <div className="overflow-hidden rounded-md bg-popover text-popover-foreground">
1175
+ <div className="rounded-md bg-popover text-popover-foreground">
1055
1176
  {view === "shader" ? (
1056
1177
  <ShaderFillsPanel
1057
1178
  descriptor={shaderDescriptor ?? undefined}
@@ -1845,8 +1966,14 @@ export function inferPaintType(
1845
1966
  if (
1846
1967
  lower.startsWith("radial-gradient") ||
1847
1968
  lower.startsWith("repeating-radial-gradient")
1848
- )
1969
+ ) {
1970
+ // Diamond fills serialize as a radial gradient (either "closest-corner"
1971
+ // from EditPanel or "ellipse closest-side" from GradientEditor). Recognize
1972
+ // both so a diamond doesn't silently become a plain radial on reselect.
1973
+ if (/closest-corner/.test(lower) || /ellipse\s+closest-side/.test(lower))
1974
+ return "diamond";
1849
1975
  return "radial";
1976
+ }
1850
1977
  if (
1851
1978
  lower.startsWith("conic-gradient") ||
1852
1979
  lower.startsWith("repeating-conic-gradient")
@@ -57,6 +57,14 @@ const DEFAULT_LABELS: ExportSettingsPanelLabels = {
57
57
 
58
58
  const DEFAULT_FORMATS: ExportFormat[] = ["png", "jpg", "svg", "pdf", "webp"];
59
59
 
60
+ const controlChromeClass =
61
+ "border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-foreground shadow-none hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground focus:ring-1 focus:ring-[var(--design-editor-accent-color)] focus:ring-offset-0 focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)] focus-visible:ring-offset-0";
62
+
63
+ // Export scale bounds — must match the clamp in DesignEditor's PNG/SVG export
64
+ // handlers so the panel never promises a range the exporter won't render.
65
+ const MIN_EXPORT_SCALE = 0.1;
66
+ const MAX_EXPORT_SCALE = 4;
67
+
60
68
  /** Preset scale options shown in the scale dropdown — matches the design editor's presets */
61
69
  const SCALE_PRESETS: { label: string; value: ExportScale }[] = [
62
70
  { label: "0.5x", value: "0.5" },
@@ -130,7 +138,12 @@ function ScaleSelect({
130
138
  }
131
139
  }}
132
140
  >
133
- <SelectTrigger className="h-6 w-14 shrink-0 px-1.5 text-[11px]">
141
+ <SelectTrigger
142
+ className={cn(
143
+ "h-6 w-14 shrink-0 px-1.5 text-[11px]",
144
+ controlChromeClass,
145
+ )}
146
+ >
134
147
  <SelectValue />
135
148
  </SelectTrigger>
136
149
  <SelectContent>
@@ -262,7 +275,7 @@ export function ExportSettingsPanel({
262
275
  variant="outline"
263
276
  disabled={isDisabled}
264
277
  onClick={handleExport}
265
- className="h-6 w-full px-2 text-[11px]"
278
+ className={cn("h-6 w-full px-2 text-[11px]", controlChromeClass)}
266
279
  >
267
280
  <IconDownload className="size-3.5" />
268
281
  {copy.export}
@@ -297,13 +310,20 @@ function ExportRow({
297
310
  type="number"
298
311
  value={row.scale}
299
312
  disabled={isDisabled}
300
- min={0.01}
301
- max={100}
313
+ // Bounds match the exporter's real clamp ([0.1, 4]); see
314
+ // DesignEditor handleDownload* . Previously the field accepted up to
315
+ // 100x but the exporter silently clamped to 4x with no feedback.
316
+ min={MIN_EXPORT_SCALE}
317
+ max={MAX_EXPORT_SCALE}
302
318
  step={0.5}
303
319
  onChange={(e) => {
304
320
  const parsed = parseFloat(e.target.value);
305
321
  if (!Number.isNaN(parsed) && parsed > 0) {
306
- onPatchRow(row.id, { scale: parsed });
322
+ const clamped = Math.min(
323
+ MAX_EXPORT_SCALE,
324
+ Math.max(MIN_EXPORT_SCALE, parsed),
325
+ );
326
+ onPatchRow(row.id, { scale: clamped });
307
327
  }
308
328
  }}
309
329
  onBlur={() => {
@@ -312,7 +332,10 @@ function ExportRow({
312
332
  onPatchRow(row.id, { customScale: false });
313
333
  }
314
334
  }}
315
- className="h-6 w-14 shrink-0 px-1.5 text-[11px] tabular-nums"
335
+ className={cn(
336
+ "h-6 w-14 shrink-0 px-1.5 text-[11px] tabular-nums",
337
+ controlChromeClass,
338
+ )}
316
339
  aria-label="Scale"
317
340
  // eslint-disable-next-line jsx-a11y/no-autofocus
318
341
  autoFocus
@@ -340,7 +363,12 @@ function ExportRow({
340
363
  onPatchRow(row.id, { format: format as ExportFormat })
341
364
  }
342
365
  >
343
- <SelectTrigger className="h-6 min-w-0 flex-1 px-1.5 text-[11px]">
366
+ <SelectTrigger
367
+ className={cn(
368
+ "h-6 min-w-0 flex-1 px-1.5 text-[11px]",
369
+ controlChromeClass,
370
+ )}
371
+ >
344
372
  <SelectValue />
345
373
  </SelectTrigger>
346
374
  <SelectContent>
@@ -364,7 +392,10 @@ function ExportRow({
364
392
  disabled={isDisabled}
365
393
  onChange={(e) => onPatchRow(row.id, { suffix: e.target.value })}
366
394
  placeholder={labels.suffix}
367
- className="h-6 min-w-0 flex-1 px-1.5 text-[11px]"
395
+ className={cn(
396
+ "h-6 min-w-0 flex-1 px-1.5 text-[11px]",
397
+ controlChromeClass,
398
+ )}
368
399
  aria-label={labels.suffix}
369
400
  />
370
401
 
@@ -167,10 +167,12 @@ export function parseGradientCss(
167
167
  }
168
168
  } else if (fn === "radial") {
169
169
  kind =
170
- /ellipse\s+closest-side/i.test(first) || fallbackKind === "diamond"
170
+ /ellipse\s+closest-side/i.test(first) ||
171
+ /closest-corner/i.test(first) ||
172
+ fallbackKind === "diamond"
171
173
  ? "diamond"
172
174
  : "radial";
173
- if (/circle|ellipse|at\s/i.test(first)) stopStart = 1;
175
+ if (/circle|ellipse|at\s|closest-/i.test(first)) stopStart = 1;
174
176
  } else if (fn === "conic") {
175
177
  kind = "angular";
176
178
  const angleMatch = first.match(ANGLE_RE);
@@ -34,6 +34,16 @@ const FIT_MODES: Array<{ mode: ImageFitMode; label: string }> = [
34
34
 
35
35
  // ─── CSS serialization ─────────────────────────────────────────────────────────
36
36
 
37
+ /**
38
+ * Escape a URL for embedding inside a double-quoted CSS url("...") token.
39
+ * Only `"` and `\` are CSS-significant inside a double-quoted string; newlines
40
+ * must also be stripped. Everything else (parens, commas, single-quotes, etc.)
41
+ * is safe inside double quotes, so we leave it alone to keep the URL intact.
42
+ */
43
+ function escapeForQuotedUrl(url: string): string {
44
+ return url.replace(/\\/g, "\\\\").replace(/"/g, "%22").replace(/\r?\n/g, "");
45
+ }
46
+
37
47
  const CHECKER_A = "#d4d4d4";
38
48
  const CHECKERBOARD_IMAGE = `linear-gradient(45deg, ${CHECKER_A} 25%, transparent 25%), linear-gradient(-45deg, ${CHECKER_A} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${CHECKER_A} 75%), linear-gradient(-45deg, transparent 75%, ${CHECKER_A} 75%)`;
39
49
  const FIT_MARKER_RE =
@@ -55,7 +65,7 @@ function imageFitMarker(fit: ImageFitMode): string {
55
65
  export function imageFillToCss(value: ImageFillValue): string {
56
66
  const url = value.url.trim();
57
67
  if (!url) return "transparent";
58
- const safeUrl = url.replace(/["')]/g, encodeURIComponent);
68
+ const safeUrl = escapeForQuotedUrl(url);
59
69
  const image = `url("${safeUrl}")`;
60
70
  switch (value.fit) {
61
71
  case "fit":
@@ -70,13 +80,70 @@ export function imageFillToCss(value: ImageFillValue): string {
70
80
  }
71
81
  }
72
82
 
73
- const URL_RE = /url\((['"]?)([^'")]+)\1\)/i;
83
+ export function imageFillToBackgroundStyles(
84
+ value: ImageFillValue,
85
+ ): Record<
86
+ | "backgroundImage"
87
+ | "backgroundSize"
88
+ | "backgroundRepeat"
89
+ | "backgroundPosition",
90
+ string
91
+ > {
92
+ const url = value.url.trim();
93
+ if (!url) {
94
+ return {
95
+ backgroundImage: "none",
96
+ backgroundSize: "auto",
97
+ backgroundRepeat: "no-repeat",
98
+ backgroundPosition: "center",
99
+ };
100
+ }
101
+ const safeUrl = url.replace(/["')]/g, encodeURIComponent);
102
+ const backgroundImage = `url("${safeUrl}")`;
103
+ switch (value.fit) {
104
+ case "fit":
105
+ return {
106
+ backgroundImage,
107
+ backgroundSize: "contain",
108
+ backgroundRepeat: "no-repeat",
109
+ backgroundPosition: "center",
110
+ };
111
+ case "tile":
112
+ return {
113
+ backgroundImage,
114
+ backgroundSize: "auto",
115
+ backgroundRepeat: "repeat",
116
+ backgroundPosition: "top left",
117
+ };
118
+ case "crop":
119
+ return {
120
+ backgroundImage,
121
+ backgroundSize: "cover",
122
+ backgroundRepeat: "no-repeat",
123
+ backgroundPosition: "center",
124
+ };
125
+ case "fill":
126
+ default:
127
+ return {
128
+ backgroundImage,
129
+ backgroundSize: "cover",
130
+ backgroundRepeat: "no-repeat",
131
+ backgroundPosition: "center",
132
+ };
133
+ }
134
+ }
135
+
136
+ // Matches url() in three forms:
137
+ // group 1 — double-quoted: url("...anything...")
138
+ // group 2 — single-quoted: url('...anything...')
139
+ // group 3 — unquoted: url(...no-parens-or-quotes...)
140
+ const URL_RE = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)'"]*?))\s*\)/i;
74
141
 
75
142
  /** Extract the URL + fit mode from a CSS background value, if present. */
76
143
  export function parseImageFillCss(value: string): ImageFillValue | null {
77
144
  const match = value.match(URL_RE);
78
145
  if (!match) return null;
79
- const url = match[2];
146
+ const url = (match[1] ?? match[2] ?? match[3] ?? "").trim();
80
147
  const marker = value.match(FIT_MARKER_RE)?.[1] as ImageFitMode | undefined;
81
148
  if (marker) return { url, fit: marker };
82
149
  // Heuristic fallback when no marker comment is present (e.g. CSS pasted from
@@ -141,7 +208,7 @@ export function ImageFillControls({
141
208
  className="relative h-24 w-full overflow-hidden rounded-md border border-border/60"
142
209
  style={{
143
210
  backgroundImage: value.url
144
- ? `url("${value.url.trim().replace(/["')]/g, encodeURIComponent)}")`
211
+ ? `url("${escapeForQuotedUrl(value.url.trim())}")`
145
212
  : CHECKERBOARD_IMAGE,
146
213
  backgroundSize: value.url
147
214
  ? value.fit === "fit"
@@ -235,7 +302,10 @@ export function ImageFillControls({
235
302
  onValueChange={(v) => onChange({ ...value, fit: v as ImageFitMode })}
236
303
  disabled={disabled}
237
304
  >
238
- <SelectTrigger className="h-6 w-full rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] shadow-none focus:ring-0 focus:ring-offset-0 focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-3 [&>svg]:shrink-0">
305
+ <SelectTrigger
306
+ aria-label={"Fill" /* i18n-ignore image fit selector */}
307
+ className="h-6 w-full rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] shadow-none focus:ring-0 focus:ring-offset-0 focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-3 [&>svg]:shrink-0"
308
+ >
239
309
  <SelectValue />
240
310
  </SelectTrigger>
241
311
  <SelectContent className="text-[11px]">
@@ -4,9 +4,9 @@ import {
4
4
  useId,
5
5
  useRef,
6
6
  useState,
7
- type ComponentType,
8
7
  type KeyboardEvent,
9
8
  type PointerEvent,
9
+ type ReactNode,
10
10
  } from "react";
11
11
 
12
12
  import { Input } from "@/components/ui/input";
@@ -26,7 +26,7 @@ import {
26
26
  type ScrubExpressionOptions,
27
27
  } from "./scrub-input-utils";
28
28
 
29
- type ScrubInputIcon = ComponentType<{ className?: string }>;
29
+ type ScrubInputIcon = (props: { className?: string }) => ReactNode;
30
30
 
31
31
  export interface ScrubInputChangeMeta {
32
32
  source: "commit" | "keyboard" | "scrub";
@@ -39,13 +39,14 @@ export interface ScrubInputProps extends ScrubExpressionOptions {
39
39
  onChange: (value: number, meta: ScrubInputChangeMeta) => void;
40
40
  id?: string;
41
41
  step?: number;
42
- icon?: ScrubInputIcon;
42
+ icon?: ScrubInputIcon | null;
43
43
  disabled?: boolean;
44
44
  placeholder?: string;
45
45
  className?: string;
46
46
  inputClassName?: string;
47
47
  labelClassName?: string;
48
48
  ariaLabel?: string;
49
+ tooltipLabel?: string;
49
50
  }
50
51
 
51
52
  export function ScrubInput({
@@ -65,12 +66,17 @@ export function ScrubInput({
65
66
  inputClassName,
66
67
  labelClassName,
67
68
  ariaLabel,
69
+ tooltipLabel,
68
70
  }: ScrubInputProps) {
69
71
  const generatedId = useId();
70
72
  const inputId = id ?? generatedId;
71
73
  const [draft, setDraft] = useState(() =>
72
74
  formatScrubValue(value, { unit, precision }),
73
75
  );
76
+ // Track the latest draft in a ref so commitDraft always reads the most
77
+ // up-to-date value even if the blur event fires before the React state
78
+ // update has been committed to the render tree (concurrent mode / batching).
79
+ const draftRef = useRef(draft);
74
80
  const [focused, setFocused] = useState(false);
75
81
  const [dragging, setDragging] = useState(false);
76
82
  const inputRef = useRef<HTMLInputElement>(null);
@@ -83,27 +89,41 @@ export function ScrubInput({
83
89
  });
84
90
 
85
91
  useEffect(() => {
86
- if (!focused) setDraft(formatScrubValue(value, { unit, precision }));
92
+ if (!focused) {
93
+ const formatted = formatScrubValue(value, { unit, precision });
94
+ draftRef.current = formatted;
95
+ setDraft(formatted);
96
+ }
87
97
  }, [focused, precision, unit, value]);
88
98
 
89
99
  const options = { unit, min, max, precision };
100
+ const resolvedTooltipLabel = tooltipLabel ?? ariaLabel ?? label;
90
101
 
91
102
  const setNextValue = (nextValue: number, meta: ScrubInputChangeMeta) => {
92
103
  const normalized = normalizeScrubNumber(nextValue, options);
93
104
  onChange(normalized, meta);
94
- setDraft(formatScrubValue(normalized, options));
105
+ const formatted = formatScrubValue(normalized, options);
106
+ draftRef.current = formatted;
107
+ setDraft(formatted);
95
108
  };
96
109
 
97
110
  const commitDraft = () => {
98
- const parsed = parseScrubExpression(draft, value, options);
111
+ // Always read from the ref so we use the latest typed value even if the
112
+ // React render with the updated draft state hasn't committed yet (e.g.
113
+ // when blur fires in the same synchronous batch as the last onChange).
114
+ const currentDraft = draftRef.current;
115
+ const parsed = parseScrubExpression(currentDraft, value, options);
99
116
  if (!parsed) {
100
- setDraft(formatScrubValue(value, options));
117
+ const reverted = formatScrubValue(value, options);
118
+ draftRef.current = reverted;
119
+ setDraft(reverted);
101
120
  return;
102
121
  }
103
122
 
123
+ draftRef.current = parsed.normalized;
104
124
  setDraft(parsed.normalized);
105
125
  if (parsed.value !== value) {
106
- onChange(parsed.value, { source: "commit", expression: draft });
126
+ onChange(parsed.value, { source: "commit", expression: currentDraft });
107
127
  }
108
128
  };
109
129
 
@@ -131,7 +151,9 @@ export function ScrubInput({
131
151
 
132
152
  if (event.key === "Escape") {
133
153
  event.preventDefault();
134
- setDraft(formatScrubValue(value, options));
154
+ const reverted = formatScrubValue(value, options);
155
+ draftRef.current = reverted;
156
+ setDraft(reverted);
135
157
  skipNextBlurCommitRef.current = true;
136
158
  event.currentTarget.blur();
137
159
  }
@@ -202,11 +224,11 @@ export function ScrubInput({
202
224
  labelClassName,
203
225
  )}
204
226
  >
205
- <Icon className="size-3 shrink-0" />
227
+ {Icon ? <Icon className="size-3 shrink-0" /> : null}
206
228
  <span className="truncate">{label}</span>
207
229
  </Label>
208
230
  </TooltipTrigger>
209
- <TooltipContent>{`${label} — drag to scrub · ↑↓ step · Shift ×10 · ⌥ fine`}</TooltipContent>
231
+ <TooltipContent>{resolvedTooltipLabel}</TooltipContent>
210
232
  </Tooltip>
211
233
  <Input
212
234
  ref={inputRef}
@@ -228,7 +250,10 @@ export function ScrubInput({
228
250
  }
229
251
  commitDraft();
230
252
  }}
231
- onChange={(event) => setDraft(event.target.value)}
253
+ onChange={(event) => {
254
+ draftRef.current = event.target.value;
255
+ setDraft(event.target.value);
256
+ }}
232
257
  onKeyDown={handleKeyDown}
233
258
  className={cn(
234
259
  // Compact design-editor: h-6, 11px tabular text, ring-1 with no offset.
@@ -60,6 +60,7 @@ export {
60
60
  } from "./GradientEditor";
61
61
  export {
62
62
  ImageFillControls,
63
+ imageFillToBackgroundStyles,
63
64
  imageFillToCss,
64
65
  parseImageFillCss,
65
66
  type ImageFillControlsProps,
@@ -58,10 +58,23 @@ export function formatScrubValue(
58
58
  options: Pick<ScrubExpressionOptions, "precision" | "unit"> = {},
59
59
  ): string {
60
60
  const normalized = normalizeScrubNumber(value, options);
61
- const numeric =
62
- Number.isFinite(options.precision) && options.precision! >= 0
63
- ? normalized.toFixed(options.precision).replace(/\.?0+$/, "")
64
- : String(normalized);
61
+ let numeric: string;
62
+ if (Number.isFinite(options.precision) && options.precision! >= 0) {
63
+ const fixed = normalized.toFixed(options.precision!);
64
+ if (options.unit) {
65
+ // For fields with units (px, %, °, etc.) collapse all trailing zeros
66
+ // including the decimal point: "12.30px" → "12.3px", "10.00px" → "10px".
67
+ // Leave integer strings alone; otherwise precision 0 turns "100" into "1".
68
+ numeric = fixed.includes(".") ? fixed.replace(/\.?0+$/, "") : fixed;
69
+ } else {
70
+ // For unitless fields (e.g. line-height), preserve at least one decimal
71
+ // digit so values like "2.0" stay "2.0" rather than collapsing to "2".
72
+ // Only strip redundant trailing zeros beyond the first decimal digit.
73
+ numeric = fixed.includes(".") ? fixed.replace(/(?<=\.\d)0+$/, "") : fixed;
74
+ }
75
+ } else {
76
+ numeric = String(normalized);
77
+ }
65
78
  return `${numeric}${options.unit ?? ""}`;
66
79
  }
67
80
 
@@ -36,6 +36,15 @@ export interface ElementInfo {
36
36
 
37
37
  export type DeviceFrameType = "none" | "desktop" | "tablet" | "mobile";
38
38
 
39
+ export const DEVICE_FRAME_VIEWPORTS = {
40
+ desktop: { width: 1280, height: 800 },
41
+ tablet: { width: 768, height: 1024 },
42
+ mobile: { width: 390, height: 844 },
43
+ } as const satisfies Record<
44
+ Exclude<DeviceFrameType, "none">,
45
+ { width: number; height: number }
46
+ >;
47
+
39
48
  export interface ViewportTab {
40
49
  id: string;
41
50
  filename: string;