@agent-native/core 0.80.8 → 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 (281) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -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/error-format.ts +25 -0
  18. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
  19. package/corpus/core/src/client/guided-questions.tsx +32 -4
  20. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
  21. package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
  22. package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
  23. package/corpus/core/src/client/session-replay.ts +34 -0
  24. package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
  25. package/corpus/core/src/coding-tools/index.ts +1 -1
  26. package/corpus/core/src/mcp/index.ts +2 -0
  27. package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
  28. package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
  29. package/corpus/core/src/mcp-client/index.ts +9 -0
  30. package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
  31. package/corpus/core/src/onboarding/types.ts +7 -0
  32. package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
  33. package/corpus/core/src/server/agents-bundle.ts +35 -7
  34. package/corpus/core/src/styles/agent-native.css +5 -0
  35. package/corpus/core/src/templates/default/app/global.css +35 -35
  36. package/corpus/templates/analytics/AGENTS.md +6 -0
  37. package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
  38. package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
  39. package/corpus/templates/analytics/app/global.css +144 -40
  40. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
  41. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
  44. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
  45. package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  47. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
  48. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
  49. package/corpus/templates/assets/app/global.css +99 -35
  50. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  51. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
  52. package/corpus/templates/assets/app/routes/library.tsx +4 -4
  53. package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
  54. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  55. package/corpus/templates/brain/app/global.css +63 -35
  56. package/corpus/templates/brain/app/routes/search.tsx +1 -1
  57. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  58. package/corpus/templates/brain/app/routes/sources.tsx +2 -2
  59. package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
  60. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
  61. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
  62. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
  63. package/corpus/templates/calendar/app/global.css +69 -35
  64. package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  65. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  66. package/corpus/templates/chat/app/global.css +35 -35
  67. package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  68. package/corpus/templates/clips/AGENTS.md +13 -0
  69. package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
  70. package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
  71. package/corpus/templates/clips/actions/view-screen.ts +2 -2
  72. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  73. package/corpus/templates/clips/app/global.css +47 -35
  74. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
  75. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
  76. package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
  78. package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
  79. package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
  80. package/corpus/templates/clips/desktop/src/app.tsx +290 -1
  81. package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
  82. package/corpus/templates/clips/desktop/src/styles.css +67 -52
  83. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
  86. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
  87. package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
  88. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
  89. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  90. package/corpus/templates/content/app/global.css +29 -15
  91. package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  92. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
  93. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  94. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
  95. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
  96. package/corpus/templates/design/AGENTS.md +20 -12
  97. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
  98. package/corpus/templates/design/actions/apply-visual-edit.ts +24 -4
  99. package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
  100. package/corpus/templates/design/actions/generate-design.ts +6 -21
  101. package/corpus/templates/design/actions/generate-screens.ts +43 -2
  102. package/corpus/templates/design/actions/get-design.ts +1 -0
  103. package/corpus/templates/design/actions/present-design-variants.ts +355 -30
  104. package/corpus/templates/design/actions/view-screen.ts +75 -31
  105. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +329 -69
  106. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
  107. package/corpus/templates/design/app/components/design/EditPanel.tsx +479 -77
  108. package/corpus/templates/design/app/components/design/LayersPanel.tsx +380 -216
  109. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1050 -196
  110. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
  111. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
  112. package/corpus/templates/design/app/components/design/index.ts +0 -1
  113. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +225 -133
  114. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +122 -11
  115. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +24 -5
  116. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +57 -1
  117. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
  118. package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
  119. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +12 -5
  120. package/corpus/templates/design/app/components/design/types.ts +9 -0
  121. package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
  122. package/corpus/templates/design/app/global.css +77 -49
  123. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  124. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +8 -1
  125. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
  126. package/corpus/templates/design/app/i18n-data.ts +45 -3
  127. package/corpus/templates/design/app/pages/DesignEditor.tsx +2602 -657
  128. package/corpus/templates/design/app/root.tsx +1 -1
  129. package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
  134. package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
  159. package/corpus/templates/design/e2e/README.md +3 -3
  160. package/corpus/templates/design/e2e/global-setup.ts +11 -22
  161. package/corpus/templates/design/e2e/helpers.ts +122 -11
  162. package/corpus/templates/design/playwright.config.ts +9 -3
  163. package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
  164. package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
  165. package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
  166. package/corpus/templates/design/shared/code-layer.ts +125 -22
  167. package/corpus/templates/design/shared/generation-session.ts +41 -0
  168. package/corpus/templates/dispatch/app/global.css +79 -35
  169. package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
  170. package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  171. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
  172. package/corpus/templates/forms/app/global.css +64 -35
  173. package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
  174. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
  175. package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
  176. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
  177. package/corpus/templates/macros/app/global.css +25 -9
  178. package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  179. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
  180. package/corpus/templates/mail/app/global.css +42 -36
  181. package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
  182. package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
  183. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
  184. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
  185. package/corpus/templates/plan/app/global.css +74 -42
  186. package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
  187. package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
  188. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
  189. package/corpus/templates/slides/app/global.css +49 -33
  190. package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  191. package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
  192. package/corpus/templates/videos/app/global.css +35 -35
  193. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  194. package/dist/cli/mcp.d.ts.map +1 -1
  195. package/dist/cli/mcp.js +10 -0
  196. package/dist/cli/mcp.js.map +1 -1
  197. package/dist/cli/skills.d.ts.map +1 -1
  198. package/dist/cli/skills.js +58 -12
  199. package/dist/cli/skills.js.map +1 -1
  200. package/dist/client/error-format.d.ts.map +1 -1
  201. package/dist/client/error-format.js +17 -0
  202. package/dist/client/error-format.js.map +1 -1
  203. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  204. package/dist/client/extensions/ExtensionViewer.js +3 -4
  205. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  206. package/dist/client/guided-questions.d.ts +4 -0
  207. package/dist/client/guided-questions.d.ts.map +1 -1
  208. package/dist/client/guided-questions.js +16 -7
  209. package/dist/client/guided-questions.js.map +1 -1
  210. package/dist/client/onboarding/OnboardingPanel.js +26 -1
  211. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  212. package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
  213. package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
  214. package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
  215. package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
  216. package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
  217. package/dist/client/resources/use-builtin-capabilities.js +2 -1
  218. package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
  219. package/dist/client/session-replay.d.ts.map +1 -1
  220. package/dist/client/session-replay.js +31 -0
  221. package/dist/client/session-replay.js.map +1 -1
  222. package/dist/client/sharing/ShareButton.d.ts +14 -0
  223. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  224. package/dist/client/sharing/ShareButton.js +36 -7
  225. package/dist/client/sharing/ShareButton.js.map +1 -1
  226. package/dist/coding-tools/index.js +1 -1
  227. package/dist/coding-tools/index.js.map +1 -1
  228. package/dist/collab/routes.d.ts +1 -1
  229. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  230. package/dist/mcp/index.d.ts +2 -0
  231. package/dist/mcp/index.d.ts.map +1 -1
  232. package/dist/mcp/index.js +1 -0
  233. package/dist/mcp/index.js.map +1 -1
  234. package/dist/mcp/screen-memory-stdio.d.ts +6 -0
  235. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
  236. package/dist/mcp/screen-memory-stdio.js +213 -0
  237. package/dist/mcp/screen-memory-stdio.js.map +1 -0
  238. package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
  239. package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
  240. package/dist/mcp-client/builtin-capabilities.js +10 -0
  241. package/dist/mcp-client/builtin-capabilities.js.map +1 -1
  242. package/dist/mcp-client/index.d.ts +1 -0
  243. package/dist/mcp-client/index.d.ts.map +1 -1
  244. package/dist/mcp-client/index.js +1 -0
  245. package/dist/mcp-client/index.js.map +1 -1
  246. package/dist/mcp-client/screen-memory-local.d.ts +61 -0
  247. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
  248. package/dist/mcp-client/screen-memory-local.js +336 -0
  249. package/dist/mcp-client/screen-memory-local.js.map +1 -0
  250. package/dist/notifications/routes.d.ts +2 -2
  251. package/dist/observability/routes.d.ts +8 -8
  252. package/dist/onboarding/types.d.ts +7 -0
  253. package/dist/onboarding/types.d.ts.map +1 -1
  254. package/dist/onboarding/types.js.map +1 -1
  255. package/dist/resources/handlers.d.ts +2 -2
  256. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  257. package/dist/server/agent-chat-plugin.js +26 -0
  258. package/dist/server/agent-chat-plugin.js.map +1 -1
  259. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  260. package/dist/server/agents-bundle.d.ts.map +1 -1
  261. package/dist/server/agents-bundle.js +22 -7
  262. package/dist/server/agents-bundle.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +5 -0
  265. package/dist/templates/default/app/global.css +35 -35
  266. package/docs/content/locales/ar-SA/template-design.mdx +6 -0
  267. package/docs/content/locales/de-DE/template-design.mdx +8 -0
  268. package/docs/content/locales/es-ES/template-design.mdx +10 -2
  269. package/docs/content/locales/fr-FR/template-design.mdx +10 -2
  270. package/docs/content/locales/hi-IN/template-design.mdx +7 -0
  271. package/docs/content/locales/ja-JP/template-design.mdx +7 -0
  272. package/docs/content/locales/ko-KR/template-design.mdx +7 -0
  273. package/docs/content/locales/pt-BR/template-design.mdx +9 -2
  274. package/docs/content/locales/zh-CN/template-design.mdx +6 -0
  275. package/docs/content/locales/zh-TW/template-design.mdx +6 -0
  276. package/docs/content/template-design.mdx +9 -2
  277. package/package.json +1 -1
  278. package/src/templates/default/app/global.css +35 -35
  279. package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
  280. package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
  281. package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -342
@@ -151,6 +151,12 @@ export interface AutoLayoutMatrixProps {
151
151
  axis: AutoLayoutSizingAxis,
152
152
  sizing: AutoLayoutSizing,
153
153
  ) => void;
154
+ /**
155
+ * Invoked when the user directly edits the resolved size (scrub or type) in
156
+ * fixed-sizing mode. `value` is the new size in CSS pixels. Optional —
157
+ * when omitted the resolved-size display is read-only (mode-picker only).
158
+ */
159
+ onChildSizeChange?: (axis: AutoLayoutSizingAxis, value: number) => void;
154
160
  /**
155
161
  * Set or clear a min/max constraint on an axis. `value === null` clears it.
156
162
  * Optional — when omitted the "Add min/max…" rows and constraint sub-rows are
@@ -244,6 +250,7 @@ export function AutoLayoutMatrix({
244
250
  onChildMinMaxChange,
245
251
  onApplyVariable,
246
252
  onDisplayChange,
253
+ onChildSizeChange,
247
254
  availableChildSizing,
248
255
  labels,
249
256
  disabled = false,
@@ -371,6 +378,11 @@ export function AutoLayoutMatrix({
371
378
  labels={copy}
372
379
  disabled={disabled}
373
380
  onChange={(next) => onChildSizingChange("horizontal", next)}
381
+ onSizeChange={
382
+ onChildSizeChange
383
+ ? (px) => onChildSizeChange("horizontal", px)
384
+ : undefined
385
+ }
374
386
  onMinMaxChange={onChildMinMaxChange}
375
387
  onApplyVariable={onApplyVariable}
376
388
  />
@@ -387,6 +399,11 @@ export function AutoLayoutMatrix({
387
399
  labels={copy}
388
400
  disabled={disabled}
389
401
  onChange={(next) => onChildSizingChange("vertical", next)}
402
+ onSizeChange={
403
+ onChildSizeChange
404
+ ? (px) => onChildSizeChange("vertical", px)
405
+ : undefined
406
+ }
390
407
  onMinMaxChange={onChildMinMaxChange}
391
408
  onApplyVariable={onApplyVariable}
392
409
  />
@@ -417,36 +434,35 @@ export function AutoLayoutMatrix({
417
434
  </div>
418
435
  </div>
419
436
 
420
- {/* ── Alignment + Gap (single label row) ── */}
421
- <div className="grid grid-cols-[78px_1fr] items-start gap-3">
422
- {/* Left: Alignment label + compact 3×3 matrix (no border box) */}
423
- <div className="space-y-1.5">
424
- <ControlLabel>
425
- {"Alignment" /* i18n-ignore design inspector label */}
426
- </ControlLabel>
427
- <CompactAlignmentMatrix
428
- value={value.alignment}
429
- onChange={onAlignmentChange}
430
- direction={value.direction}
431
- disabled={disabled || isBlock}
432
- onDistribute={onDistribute}
433
- />
434
- </div>
437
+ {!isBlock ? (
438
+ <div className="grid grid-cols-[78px_1fr] items-start gap-3">
439
+ <div className="space-y-1.5">
440
+ <ControlLabel>
441
+ {"Alignment" /* i18n-ignore design inspector label */}
442
+ </ControlLabel>
443
+ <CompactAlignmentMatrix
444
+ value={value.alignment}
445
+ onChange={onAlignmentChange}
446
+ direction={value.direction}
447
+ disabled={disabled}
448
+ onDistribute={onDistribute}
449
+ />
450
+ </div>
435
451
 
436
- {/* Right: Gap label + [icon] value [▾] + sliders */}
437
- <div className="space-y-1.5">
438
- <ControlLabel>{copy.gap}</ControlLabel>
439
- <GapField
440
- value={value.gap}
441
- onGapChange={onGapChange}
442
- onDistribute={onDistribute}
443
- label={copy.gap}
444
- disabled={disabled || isBlock}
445
- direction={value.direction}
446
- gapMode={value.spaceBetween ? "auto" : "fixed"}
447
- />
452
+ <div className="space-y-1.5">
453
+ <ControlLabel>{copy.gap}</ControlLabel>
454
+ <GapField
455
+ value={value.gap}
456
+ onGapChange={onGapChange}
457
+ onDistribute={onDistribute}
458
+ label={copy.gap}
459
+ disabled={disabled}
460
+ direction={value.direction}
461
+ gapMode={value.spaceBetween ? "auto" : "fixed"}
462
+ />
463
+ </div>
448
464
  </div>
449
- </div>
465
+ ) : null}
450
466
 
451
467
  {/* ── Padding ── */}
452
468
  <div className="space-y-1.5">
@@ -860,16 +876,15 @@ function GapField({
860
876
  {/* [gap-icon] value [▾] in one control surface */}
861
877
  <div
862
878
  className={cn(
863
- "flex h-7 min-w-0 flex-1 items-center rounded-md bg-[var(--design-editor-control-bg)] pl-1.5",
879
+ "flex h-7 min-w-0 flex-1 items-center rounded-md bg-[var(--design-editor-control-bg)]",
864
880
  disabled && "opacity-40",
865
881
  )}
866
882
  >
867
- <span className="flex shrink-0 items-center text-muted-foreground">
868
- <IconGap className="size-3.5" />
869
- </span>
870
883
  <ScrubInput
871
884
  label={label}
872
885
  ariaLabel={label}
886
+ tooltipLabel={label}
887
+ icon={IconGap}
873
888
  value={value}
874
889
  onChange={(next) => onGapChange(next)}
875
890
  unit="px"
@@ -878,7 +893,7 @@ function GapField({
878
893
  precision={1}
879
894
  disabled={disabled}
880
895
  className="min-w-0 flex-1 gap-0"
881
- labelClassName="hidden"
896
+ labelClassName="h-7 w-6 justify-center gap-0 rounded-l-md text-muted-foreground [&>span]:hidden"
882
897
  inputClassName="h-6 border-0 bg-transparent px-1 text-[11px] shadow-none focus-visible:ring-0"
883
898
  />
884
899
  {onDistribute != null ? (
@@ -972,16 +987,15 @@ function PaddingField({
972
987
  return (
973
988
  <div
974
989
  className={cn(
975
- "flex h-7 min-w-0 items-center rounded-md bg-[var(--design-editor-control-bg)] pl-1.5",
990
+ "flex h-7 min-w-0 items-center rounded-md bg-[var(--design-editor-control-bg)]",
976
991
  disabled && "opacity-40",
977
992
  )}
978
993
  >
979
- <span className="flex shrink-0 items-center text-muted-foreground">
980
- <Icon className="size-3.5" />
981
- </span>
982
994
  <ScrubInput
983
995
  label={ariaLabel}
984
996
  ariaLabel={ariaLabel}
997
+ tooltipLabel={ariaLabel}
998
+ icon={Icon}
985
999
  value={value}
986
1000
  onChange={(next) => onChange(next)}
987
1001
  unit="px"
@@ -990,7 +1004,7 @@ function PaddingField({
990
1004
  precision={1}
991
1005
  disabled={disabled}
992
1006
  className="min-w-0 flex-1 gap-0"
993
- labelClassName="hidden"
1007
+ labelClassName="h-7 w-6 justify-center gap-0 rounded-l-md text-muted-foreground [&>span]:hidden"
994
1008
  inputClassName="h-6 border-0 bg-transparent px-1 text-[11px] shadow-none focus-visible:ring-0"
995
1009
  />
996
1010
  </div>
@@ -1055,6 +1069,12 @@ export interface SizingFieldProps {
1055
1069
  labels?: Partial<AutoLayoutMatrixLabels>;
1056
1070
  disabled: boolean;
1057
1071
  onChange: (value: AutoLayoutSizing) => void;
1072
+ /**
1073
+ * Invoked when the user directly scrubs or types a new pixel value while in
1074
+ * "fixed" sizing mode. When omitted the numeric display is read-only and the
1075
+ * entire trigger is a mode-picker dropdown (legacy behaviour).
1076
+ */
1077
+ onSizeChange?: (px: number) => void;
1058
1078
  onMinMaxChange?: (
1059
1079
  axis: AutoLayoutSizingAxis,
1060
1080
  kind: "min" | "max",
@@ -1085,6 +1105,7 @@ export function SizingField({
1085
1105
  labels: labelOverrides,
1086
1106
  disabled,
1087
1107
  onChange,
1108
+ onSizeChange,
1088
1109
  onMinMaxChange,
1089
1110
  onApplyVariable,
1090
1111
  }: SizingFieldProps) {
@@ -1108,6 +1129,9 @@ export function SizingField({
1108
1129
  const minLabel = isWidth ? labels.minWidth : labels.minHeight;
1109
1130
  const maxLabel = isWidth ? labels.maxWidth : labels.maxHeight;
1110
1131
 
1132
+ // Whether we're in editable fixed mode (ScrubInput shown for size).
1133
+ const isEditableFixed = value === "fixed" && onSizeChange != null;
1134
+
1111
1135
  const openEditor = (kind: "min" | "max") => {
1112
1136
  // Commit immediately so the shown row always reflects real state and
1113
1137
  // persists across selection changes, remounts, and parent re-renders.
@@ -1116,105 +1140,173 @@ export function SizingField({
1116
1140
  onMinMaxChange?.(sizingAxis, kind, seedValue);
1117
1141
  };
1118
1142
 
1119
- return (
1120
- <div className="flex min-w-0 flex-col gap-1">
1121
- <DropdownMenu>
1122
- <Tooltip>
1123
- <TooltipTrigger asChild>
1124
- <DropdownMenuTrigger asChild>
1125
- <button
1126
- type="button"
1127
- aria-label={`${axis} ${Math.round(resolvedSize ?? 0)} ${labels[value]}`}
1128
- disabled={disabled}
1129
- className={cn(
1130
- "flex h-7 w-full items-center gap-1 overflow-hidden rounded-md px-1.5",
1131
- "bg-[var(--design-editor-control-bg)] text-[11px]",
1132
- "hover:bg-[var(--design-editor-panel-raised-bg)]",
1133
- "disabled:pointer-events-none disabled:opacity-40",
1134
- "focus:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color,hsl(var(--primary)))]",
1135
- )}
1136
- >
1137
- {/* Axis letter */}
1138
- <span className="shrink-0 text-muted-foreground">{axis}</span>
1139
- {/* Resolved size */}
1140
- <span className="min-w-0 flex-1 truncate text-left tabular-nums text-foreground">
1141
- {Math.round(resolvedSize ?? 0)}
1142
- </span>
1143
- {/* Mode word (Hug/Fill only) */}
1144
- {showWord ? (
1145
- <span className="shrink-0 truncate text-muted-foreground">
1146
- {labels[value]}
1147
- </span>
1148
- ) : null}
1149
- {/* Caret */}
1150
- <span className="flex shrink-0 items-center text-muted-foreground">
1151
- <ChevronDownMini />
1152
- </span>
1153
- </button>
1154
- </DropdownMenuTrigger>
1155
- </TooltipTrigger>
1156
- <TooltipContent>{`${axis} · ${labels[value]}`}</TooltipContent>
1157
- </Tooltip>
1158
- <DropdownMenuContent
1159
- align="start"
1160
- className="min-w-[180px] text-[12px]"
1161
- sideOffset={4}
1162
- >
1163
- {/* ── Modes ── */}
1164
- <SizingMenuItem
1165
- icon={<IconSizingFixed />}
1166
- label={labels.fixed}
1167
- active={value === "fixed"}
1168
- onSelect={() => onChange("fixed")}
1169
- />
1170
- {canHug ? (
1171
- <SizingMenuItem
1172
- icon={<IconSizingHug />}
1173
- label={labels.hugContents}
1174
- active={value === "hug"}
1175
- onSelect={() => onChange("hug")}
1176
- />
1177
- ) : null}
1178
- {canFill ? (
1179
- <SizingMenuItem
1180
- icon={<IconSizingFill />}
1181
- label={labels.fillContainer}
1182
- active={value === "fill"}
1183
- onSelect={() => onChange("fill")}
1184
- />
1185
- ) : null}
1186
-
1187
- {/* ── Min / Max ── */}
1188
- {onMinMaxChange ? (
1189
- <>
1190
- <DropdownMenuSeparator />
1191
- <SizingMenuItem
1192
- icon={<IconSizingMin />}
1193
- label={addMinLabel}
1194
- active={hasMin}
1195
- disabled={hasMin}
1196
- onSelect={() => openEditor("min")}
1197
- />
1198
- <SizingMenuItem
1199
- icon={<IconSizingMax />}
1200
- label={addMaxLabel}
1201
- active={hasMax}
1202
- disabled={hasMax}
1203
- onSelect={() => openEditor("max")}
1204
- />
1205
- </>
1206
- ) : null}
1143
+ // Shared dropdown content (mode picker menu).
1144
+ const dropdownContent = (
1145
+ <DropdownMenuContent
1146
+ align="start"
1147
+ className="min-w-[180px] text-[12px]"
1148
+ sideOffset={4}
1149
+ >
1150
+ {/* ── Modes ── */}
1151
+ <SizingMenuItem
1152
+ icon={<IconSizingFixed />}
1153
+ label={labels.fixed}
1154
+ active={value === "fixed"}
1155
+ onSelect={() => onChange("fixed")}
1156
+ />
1157
+ {canHug ? (
1158
+ <SizingMenuItem
1159
+ icon={<IconSizingHug />}
1160
+ label={labels.hugContents}
1161
+ active={value === "hug"}
1162
+ onSelect={() => onChange("hug")}
1163
+ />
1164
+ ) : null}
1165
+ {canFill ? (
1166
+ <SizingMenuItem
1167
+ icon={<IconSizingFill />}
1168
+ label={labels.fillContainer}
1169
+ active={value === "fill"}
1170
+ onSelect={() => onChange("fill")}
1171
+ />
1172
+ ) : null}
1207
1173
 
1208
- {/* ── Variable ── */}
1174
+ {/* ── Min / Max ── */}
1175
+ {onMinMaxChange ? (
1176
+ <>
1209
1177
  <DropdownMenuSeparator />
1210
1178
  <SizingMenuItem
1211
- icon={<IconSizingVariable />}
1212
- label={labels.applyVariable}
1213
- disabled={!onApplyVariable}
1214
- onSelect={() => onApplyVariable?.(sizingAxis)}
1179
+ icon={<IconSizingMin />}
1180
+ label={addMinLabel}
1181
+ active={hasMin}
1182
+ disabled={hasMin}
1183
+ onSelect={() => openEditor("min")}
1215
1184
  />
1216
- </DropdownMenuContent>
1217
- </DropdownMenu>
1185
+ <SizingMenuItem
1186
+ icon={<IconSizingMax />}
1187
+ label={addMaxLabel}
1188
+ active={hasMax}
1189
+ disabled={hasMax}
1190
+ onSelect={() => openEditor("max")}
1191
+ />
1192
+ </>
1193
+ ) : null}
1194
+
1195
+ {/* ── Variable ── */}
1196
+ <DropdownMenuSeparator />
1197
+ <SizingMenuItem
1198
+ icon={<IconSizingVariable />}
1199
+ label={labels.applyVariable}
1200
+ disabled={!onApplyVariable}
1201
+ onSelect={() => onApplyVariable?.(sizingAxis)}
1202
+ />
1203
+ </DropdownMenuContent>
1204
+ );
1205
+
1206
+ return (
1207
+ <div className="flex min-w-0 flex-col gap-1">
1208
+ {isEditableFixed ? (
1209
+ /*
1210
+ * Editable fixed mode: split the trigger into two zones —
1211
+ * [axis letter | ScrubInput (numeric) | ▾ mode-picker caret]
1212
+ * The ScrubInput occupies the center and lets the user type or
1213
+ * drag-to-scrub the size in px. The chevron opens the mode dropdown.
1214
+ */
1215
+ <DropdownMenu>
1216
+ <div
1217
+ className={cn(
1218
+ "flex h-7 w-full items-center overflow-hidden rounded-md",
1219
+ "bg-[var(--design-editor-control-bg)] text-[11px]",
1220
+ disabled && "pointer-events-none opacity-40",
1221
+ )}
1222
+ >
1223
+ {/* Scrub-editable size value */}
1224
+ <ScrubInput
1225
+ label={axis}
1226
+ ariaLabel={`${axis} size in pixels`}
1227
+ tooltipLabel={`${axis} size`}
1228
+ icon={null}
1229
+ value={Math.round(resolvedSize ?? 0)}
1230
+ onChange={(next) => onSizeChange!(Math.max(1, Math.round(next)))}
1231
+ unit="px"
1232
+ min={1}
1233
+ step={1}
1234
+ precision={0}
1235
+ disabled={disabled}
1236
+ className="min-w-0 flex-1 gap-0"
1237
+ labelClassName="h-7 w-5 justify-center gap-0 rounded-l-md px-0 text-muted-foreground"
1238
+ inputClassName="h-6 border-0 bg-transparent px-1 text-[11px] shadow-none focus-visible:ring-0"
1239
+ />
1240
+ {/* Caret opens the mode picker */}
1241
+ <Tooltip>
1242
+ <TooltipTrigger asChild>
1243
+ <DropdownMenuTrigger asChild>
1244
+ <button
1245
+ type="button"
1246
+ aria-label={`${axis} sizing mode — ${labels[value]}`}
1247
+ disabled={disabled}
1248
+ className={cn(
1249
+ "flex h-7 w-6 shrink-0 items-center justify-center rounded-r-md",
1250
+ "text-muted-foreground hover:text-foreground",
1251
+ "focus:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color,hsl(var(--primary)))]",
1252
+ )}
1253
+ >
1254
+ <ChevronDownMini />
1255
+ </button>
1256
+ </DropdownMenuTrigger>
1257
+ </TooltipTrigger>
1258
+ <TooltipContent>
1259
+ {`${axis} · ${labels[value]} — click to change sizing mode`}
1260
+ </TooltipContent>
1261
+ </Tooltip>
1262
+ </div>
1263
+ {dropdownContent}
1264
+ </DropdownMenu>
1265
+ ) : (
1266
+ /*
1267
+ * Non-editable / hug / fill mode: keep the original single-button
1268
+ * dropdown trigger showing [axis | resolved size | mode word | ▾].
1269
+ */
1270
+ <DropdownMenu>
1271
+ <Tooltip>
1272
+ <TooltipTrigger asChild>
1273
+ <DropdownMenuTrigger asChild>
1274
+ <button
1275
+ type="button"
1276
+ aria-label={`${axis} ${Math.round(resolvedSize ?? 0)} ${labels[value]}`}
1277
+ disabled={disabled}
1278
+ className={cn(
1279
+ "flex h-7 w-full items-center gap-1 overflow-hidden rounded-md px-1.5",
1280
+ "bg-[var(--design-editor-control-bg)] text-[11px]",
1281
+ "hover:bg-[var(--design-editor-panel-raised-bg)]",
1282
+ "disabled:pointer-events-none disabled:opacity-40",
1283
+ "focus:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color,hsl(var(--primary)))]",
1284
+ )}
1285
+ >
1286
+ {/* Axis letter */}
1287
+ <span className="shrink-0 text-muted-foreground">{axis}</span>
1288
+ {/* Resolved size */}
1289
+ <span className="min-w-0 flex-1 truncate text-left tabular-nums text-foreground">
1290
+ {Math.round(resolvedSize ?? 0)}
1291
+ </span>
1292
+ {/* Mode word (Hug/Fill only) */}
1293
+ {showWord ? (
1294
+ <span className="shrink-0 truncate text-muted-foreground">
1295
+ {labels[value]}
1296
+ </span>
1297
+ ) : null}
1298
+ {/* Caret */}
1299
+ <span className="flex shrink-0 items-center text-muted-foreground">
1300
+ <ChevronDownMini />
1301
+ </span>
1302
+ </button>
1303
+ </DropdownMenuTrigger>
1304
+ </TooltipTrigger>
1305
+ <TooltipContent>{`${axis} · ${labels[value]} — click to change sizing mode`}</TooltipContent>
1306
+ </Tooltip>
1307
+ {dropdownContent}
1308
+ </DropdownMenu>
1309
+ )}
1218
1310
 
1219
1311
  {/* ── Constraint sub-rows ── */}
1220
1312
  {hasMin ? (
@@ -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
  }
@@ -1060,8 +1161,18 @@ export function DesignColorPicker({
1060
1161
  align="start"
1061
1162
  sideOffset={8}
1062
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
+ }}
1063
1174
  >
1064
- <div className="overflow-hidden rounded-md bg-popover text-popover-foreground">
1175
+ <div className="rounded-md bg-popover text-popover-foreground">
1065
1176
  {view === "shader" ? (
1066
1177
  <ShaderFillsPanel
1067
1178
  descriptor={shaderDescriptor ?? undefined}