@agent-native/core 0.79.27 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +26 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +41 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +51 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +50 -0
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +49 -0
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +44 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +46 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +44 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +48 -0
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +36 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +38 -0
  13. package/corpus/core/docs/content/template-design.mdx +45 -0
  14. package/corpus/core/package.json +2 -1
  15. package/corpus/core/src/agent/engine/index.ts +1 -0
  16. package/corpus/core/src/agent/engine/registry.ts +40 -0
  17. package/corpus/core/src/agent/production-agent.ts +6 -2
  18. package/corpus/core/src/cli/design-connect.ts +490 -0
  19. package/corpus/core/src/cli/index.ts +16 -0
  20. package/corpus/core/src/cli/skills.ts +126 -3
  21. package/corpus/core/src/client/AssistantChat.tsx +43 -7
  22. package/corpus/core/src/client/agent-chat.ts +27 -3
  23. package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +1 -1
  24. package/corpus/core/src/client/blocks/library/diagram.tsx +2 -2
  25. package/corpus/core/src/client/blocks/library/wireframe.tsx +1 -1
  26. package/corpus/core/src/client/chat/message-components.tsx +1 -1
  27. package/corpus/core/src/client/chat-model-groups.ts +1 -2
  28. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +33 -66
  29. package/corpus/core/src/client/composer/TiptapComposer.tsx +99 -24
  30. package/corpus/core/src/client/composer/useVoiceDictation.ts +429 -260
  31. package/corpus/core/src/client/frame-protocol.ts +17 -2
  32. package/corpus/core/src/client/rich-markdown-editor/RegistryBlockNode.tsx +2 -2
  33. package/corpus/core/src/client/sharing/ShareButton.tsx +20 -15
  34. package/corpus/core/src/client/use-agent-engine-configured.ts +80 -28
  35. package/corpus/core/src/server/transcribe-voice.ts +57 -22
  36. package/corpus/core/src/shared/mcp-embed-headers.ts +23 -1
  37. package/corpus/core/src/styles/agent-native.css +56 -7
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  39. package/corpus/core/src/vite/client.ts +2 -0
  40. package/corpus/core/src/voice/index.ts +18 -0
  41. package/corpus/core/src/voice/voice-cleanup-prompt.ts +38 -0
  42. package/corpus/core/src/voice/voice-context.ts +217 -0
  43. package/corpus/core/src/voice/voice-replacements.ts +75 -0
  44. package/corpus/templates/analytics/AGENTS.md +4 -3
  45. package/corpus/templates/analytics/actions/list-session-recordings.ts +6 -1
  46. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +557 -400
  47. package/corpus/templates/analytics/app/global.css +0 -4
  48. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  49. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +33 -8
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +7 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +7 -0
  53. package/corpus/templates/analytics/changelog/2026-06-28-ask-tab-now-matches-the-darker-app-background.md +6 -0
  54. package/corpus/templates/analytics/changelog/2026-06-28-dashboard-email-reports-now-use-cleaner-light-mode-snapshots.md +6 -0
  55. package/corpus/templates/analytics/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  56. package/corpus/templates/analytics/changelog/2026-06-28-sessions-now-show-only-signed-in-recordings-with-playable-re.md +6 -0
  57. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  58. package/corpus/templates/analytics/server/lib/dashboard-report.ts +29 -7
  59. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +5 -2
  60. package/corpus/templates/analytics/server/lib/session-replay.ts +24 -4
  61. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -4
  62. package/corpus/templates/assets/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  63. package/corpus/templates/brain/app/components/layout/Layout.tsx +29 -2
  64. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +131 -42
  65. package/corpus/templates/brain/app/i18n/zh-TW.ts +2 -0
  66. package/corpus/templates/brain/app/i18n-data.ts +20 -0
  67. package/corpus/templates/brain/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  68. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +29 -1
  69. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +417 -299
  70. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  71. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  72. package/corpus/templates/calendar/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  73. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +6 -19
  74. package/corpus/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  75. package/corpus/templates/clips/AGENTS.md +4 -0
  76. package/corpus/templates/clips/actions/cleanup-transcript.ts +39 -9
  77. package/corpus/templates/clips/actions/finalize-meeting.ts +81 -12
  78. package/corpus/templates/clips/actions/lib/agents-md-context.ts +4 -1
  79. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -3
  80. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +1 -1
  81. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  82. package/corpus/templates/clips/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  83. package/corpus/templates/clips/desktop/src/lib/personal-vocabulary.ts +13 -8
  84. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +60 -1
  85. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1 -1
  86. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -1
  87. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -5
  88. package/corpus/templates/content/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  89. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +137 -0
  90. package/corpus/templates/design/AGENTS.md +95 -5
  91. package/corpus/templates/design/DESIGN.md +82 -0
  92. package/corpus/templates/design/DEVELOPING.md +43 -0
  93. package/corpus/templates/design/actions/add-localhost-screens.ts +426 -0
  94. package/corpus/templates/design/actions/apply-shader.ts +277 -0
  95. package/corpus/templates/design/actions/apply-visual-edit.ts +303 -0
  96. package/corpus/templates/design/actions/connect-localhost.ts +183 -0
  97. package/corpus/templates/design/actions/edit-design.ts +195 -42
  98. package/corpus/templates/design/actions/generate-design.ts +71 -1
  99. package/corpus/templates/design/actions/generate-screens.ts +193 -0
  100. package/corpus/templates/design/actions/get-code-layer-projection.ts +191 -0
  101. package/corpus/templates/design/actions/get-shader.ts +119 -0
  102. package/corpus/templates/design/actions/list-localhost-connections.ts +95 -0
  103. package/corpus/templates/design/actions/navigate.ts +55 -2
  104. package/corpus/templates/design/actions/view-screen.ts +68 -1
  105. package/corpus/templates/design/agent-native.app-skill.json +10 -0
  106. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +761 -0
  107. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +2057 -103
  108. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +74 -38
  109. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +399 -0
  110. package/corpus/templates/design/app/components/design/EditPanel.tsx +3185 -439
  111. package/corpus/templates/design/app/components/design/LayersPanel.tsx +1124 -0
  112. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +3116 -147
  113. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +700 -13
  114. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +107 -63
  115. package/corpus/templates/design/app/components/design/index.ts +0 -5
  116. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +286 -0
  117. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +699 -0
  118. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +461 -0
  119. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +166 -0
  120. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +1420 -0
  121. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +420 -0
  122. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +215 -0
  123. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +71 -0
  124. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +518 -0
  125. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +106 -0
  126. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +361 -0
  127. package/corpus/templates/design/app/components/design/inspector/index.ts +60 -0
  128. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +201 -0
  129. package/corpus/templates/design/app/components/design/types.ts +23 -0
  130. package/corpus/templates/design/app/components/layout/Layout.tsx +24 -13
  131. package/corpus/templates/design/app/components/layout/Sidebar.tsx +4 -4
  132. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +474 -36
  133. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +22 -5
  134. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +24 -11
  135. package/corpus/templates/design/app/global.css +121 -3
  136. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -0
  137. package/corpus/templates/design/app/hooks/use-navigation-state.ts +130 -8
  138. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +329 -0
  139. package/corpus/templates/design/app/i18n/zh-TW.ts +264 -0
  140. package/corpus/templates/design/app/i18n-data.ts +2790 -1
  141. package/corpus/templates/design/app/pages/DesignEditor.tsx +5108 -851
  142. package/corpus/templates/design/changelog/2026-06-28-annotate-mode-now-combines-drawing-and-comment-pins-in-one-c.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-28-design-editor-panel-borders-are-cleaner-so-the-canvas-edge-s.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-28-drawing-prompts-now-reliably-open-and-submit-to-the-design-a.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-28-editor-app-menus-now-use-an-outline-agent-native-mark-with-b.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-28-layer-selection-now-updates-chat-context-without-opening-the.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-28-layer-selections-now-preserve-hidden-and-locked-canvas-state.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-handles-zooming-drawing-opening-and-dele.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-selects-screens-on-click-and-opens-them-.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-28-screen-overview-zooms-into-the-active-screen-with-smoother-e.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-28-selected-element-chips-in-chat-are-shorter-and-easier-to-sca.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-28-the-design-editor-toolbar-is-more-compact-and-the-app-menu-m.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-at-the-top-of-the-design-sidebar-instead-of-.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-in-the-right-inspector-beside-the-design-tab.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-29-active-sidebar-rows-use-soft-highlights.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-29-agents-can-now-open-screen-overview-focus-screens-and-add.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-edit-broad-copy-changes-more-reliably.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-29-design-editor-extensions-now-run-inline-beside-the-artboard.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-29-sidebars-use-darker-chrome-and-chat-warns-immediately.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-29-visual-edit-can-open-localhost-routes-as-url-backed-screens.md +6 -0
  165. package/corpus/templates/design/package.json +1 -0
  166. package/corpus/templates/design/server/db/schema.ts +26 -0
  167. package/corpus/templates/design/server/lib/design-snapshot.ts +1 -1
  168. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -0
  169. package/corpus/templates/design/shared/api.ts +27 -0
  170. package/corpus/templates/design/shared/canvas-frames.ts +103 -0
  171. package/corpus/templates/design/shared/canvas-math.ts +1058 -0
  172. package/corpus/templates/design/shared/code-layer.ts +2212 -0
  173. package/corpus/templates/design/shared/color-utils.ts +180 -0
  174. package/corpus/templates/design/shared/generation-session.ts +61 -0
  175. package/corpus/templates/design/shared/shader-presets.ts +782 -0
  176. package/corpus/templates/design/shared/shader-safety.ts +137 -0
  177. package/corpus/templates/design/shared/source-mode.ts +237 -0
  178. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +149 -6
  179. package/corpus/templates/forms/app/i18n/ar-SA.ts +2 -0
  180. package/corpus/templates/forms/app/i18n/de-DE.ts +2 -0
  181. package/corpus/templates/forms/app/i18n/en-US.ts +2 -0
  182. package/corpus/templates/forms/app/i18n/es-ES.ts +2 -0
  183. package/corpus/templates/forms/app/i18n/fr-FR.ts +2 -0
  184. package/corpus/templates/forms/app/i18n/hi-IN.ts +2 -0
  185. package/corpus/templates/forms/app/i18n/ja-JP.ts +2 -0
  186. package/corpus/templates/forms/app/i18n/ko-KR.ts +2 -0
  187. package/corpus/templates/forms/app/i18n/pt-BR.ts +2 -0
  188. package/corpus/templates/forms/app/i18n/zh-CN.ts +2 -0
  189. package/corpus/templates/forms/app/i18n/zh-TW.ts +2 -0
  190. package/corpus/templates/forms/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  191. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +2 -2
  192. package/corpus/templates/macros/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  193. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +408 -247
  194. package/corpus/templates/mail/app/i18n/ar-SA.ts +4 -0
  195. package/corpus/templates/mail/app/i18n/de-DE.ts +4 -0
  196. package/corpus/templates/mail/app/i18n/en-US.ts +4 -0
  197. package/corpus/templates/mail/app/i18n/es-ES.ts +4 -0
  198. package/corpus/templates/mail/app/i18n/fr-FR.ts +4 -0
  199. package/corpus/templates/mail/app/i18n/hi-IN.ts +4 -0
  200. package/corpus/templates/mail/app/i18n/ja-JP.ts +4 -0
  201. package/corpus/templates/mail/app/i18n/ko-KR.ts +4 -0
  202. package/corpus/templates/mail/app/i18n/pt-BR.ts +4 -0
  203. package/corpus/templates/mail/app/i18n/zh-CN.ts +4 -0
  204. package/corpus/templates/mail/app/i18n/zh-TW.ts +4 -0
  205. package/corpus/templates/mail/changelog/2026-06-28-pinned-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  206. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +6 -8
  207. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +1 -1
  208. package/corpus/templates/plan/app/global.css +61 -78
  209. package/corpus/templates/plan/app/pages/PlansPage.tsx +8 -0
  210. package/corpus/templates/plan/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  211. package/corpus/templates/plan/changelog/2026-06-28-plans-use-the-shared-neutral-light-and-dark-theme.md +6 -0
  212. package/corpus/templates/plan/changelog/2026-06-28-sending-open-feedback-from-a-plan-now-opens-the-inline-plan-.md +6 -0
  213. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
  214. package/corpus/templates/slides/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  215. package/corpus/templates/videos/app/components/Sidebar.tsx +2 -2
  216. package/corpus/templates/videos/app/components/layout/Layout.tsx +29 -2
  217. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +113 -30
  218. package/corpus/templates/videos/app/i18n/ar-SA.ts +2 -0
  219. package/corpus/templates/videos/app/i18n/de-DE.ts +2 -0
  220. package/corpus/templates/videos/app/i18n/en-US.ts +2 -0
  221. package/corpus/templates/videos/app/i18n/es-ES.ts +2 -0
  222. package/corpus/templates/videos/app/i18n/fr-FR.ts +2 -0
  223. package/corpus/templates/videos/app/i18n/hi-IN.ts +2 -0
  224. package/corpus/templates/videos/app/i18n/ja-JP.ts +2 -0
  225. package/corpus/templates/videos/app/i18n/ko-KR.ts +2 -0
  226. package/corpus/templates/videos/app/i18n/pt-BR.ts +2 -0
  227. package/corpus/templates/videos/app/i18n/zh-CN.ts +2 -0
  228. package/corpus/templates/videos/app/i18n/zh-TW.ts +2 -0
  229. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  230. package/dist/agent/engine/index.d.ts +1 -1
  231. package/dist/agent/engine/index.d.ts.map +1 -1
  232. package/dist/agent/engine/index.js +1 -1
  233. package/dist/agent/engine/index.js.map +1 -1
  234. package/dist/agent/engine/registry.d.ts +9 -0
  235. package/dist/agent/engine/registry.d.ts.map +1 -1
  236. package/dist/agent/engine/registry.js +37 -0
  237. package/dist/agent/engine/registry.js.map +1 -1
  238. package/dist/agent/production-agent.d.ts.map +1 -1
  239. package/dist/agent/production-agent.js +4 -3
  240. package/dist/agent/production-agent.js.map +1 -1
  241. package/dist/cli/design-connect.d.ts +52 -0
  242. package/dist/cli/design-connect.d.ts.map +1 -0
  243. package/dist/cli/design-connect.js +409 -0
  244. package/dist/cli/design-connect.js.map +1 -0
  245. package/dist/cli/index.js +15 -0
  246. package/dist/cli/index.js.map +1 -1
  247. package/dist/cli/skills.d.ts +3 -0
  248. package/dist/cli/skills.d.ts.map +1 -1
  249. package/dist/cli/skills.js +121 -3
  250. package/dist/cli/skills.js.map +1 -1
  251. package/dist/client/AssistantChat.d.ts.map +1 -1
  252. package/dist/client/AssistantChat.js +38 -8
  253. package/dist/client/AssistantChat.js.map +1 -1
  254. package/dist/client/agent-chat.d.ts +6 -0
  255. package/dist/client/agent-chat.d.ts.map +1 -1
  256. package/dist/client/agent-chat.js +17 -3
  257. package/dist/client/agent-chat.js.map +1 -1
  258. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  259. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  260. package/dist/client/blocks/library/diagram.js +1 -1
  261. package/dist/client/blocks/library/diagram.js.map +1 -1
  262. package/dist/client/blocks/library/wireframe.js +1 -1
  263. package/dist/client/blocks/library/wireframe.js.map +1 -1
  264. package/dist/client/chat/message-components.js +1 -1
  265. package/dist/client/chat/message-components.js.map +1 -1
  266. package/dist/client/chat-model-groups.d.ts.map +1 -1
  267. package/dist/client/chat-model-groups.js +1 -2
  268. package/dist/client/chat-model-groups.js.map +1 -1
  269. package/dist/client/components/LiveCursorOverlay.d.ts +1 -2
  270. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  271. package/dist/client/components/LiveCursorOverlay.js +19 -40
  272. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  273. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  274. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  275. package/dist/client/composer/TiptapComposer.js +86 -24
  276. package/dist/client/composer/TiptapComposer.js.map +1 -1
  277. package/dist/client/composer/useVoiceDictation.d.ts +9 -0
  278. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  279. package/dist/client/composer/useVoiceDictation.js +154 -32
  280. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  281. package/dist/client/frame-protocol.d.ts +17 -2
  282. package/dist/client/frame-protocol.d.ts.map +1 -1
  283. package/dist/client/frame-protocol.js.map +1 -1
  284. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +2 -2
  285. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  286. package/dist/client/sharing/ShareButton.d.ts +2 -0
  287. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  288. package/dist/client/sharing/ShareButton.js +7 -6
  289. package/dist/client/sharing/ShareButton.js.map +1 -1
  290. package/dist/client/use-agent-engine-configured.d.ts +5 -0
  291. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  292. package/dist/client/use-agent-engine-configured.js +51 -25
  293. package/dist/client/use-agent-engine-configured.js.map +1 -1
  294. package/dist/collab/awareness.d.ts +2 -2
  295. package/dist/collab/awareness.d.ts.map +1 -1
  296. package/dist/collab/routes.d.ts +1 -1
  297. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  298. package/dist/notifications/routes.d.ts +3 -3
  299. package/dist/observability/routes.d.ts +8 -8
  300. package/dist/progress/routes.d.ts +1 -1
  301. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  302. package/dist/server/transcribe-voice.d.ts.map +1 -1
  303. package/dist/server/transcribe-voice.js +40 -24
  304. package/dist/server/transcribe-voice.js.map +1 -1
  305. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  306. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  307. package/dist/shared/mcp-embed-headers.js +20 -1
  308. package/dist/shared/mcp-embed-headers.js.map +1 -1
  309. package/dist/styles/agent-native.css +56 -7
  310. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  311. package/dist/vite/client.d.ts.map +1 -1
  312. package/dist/vite/client.js +2 -0
  313. package/dist/vite/client.js.map +1 -1
  314. package/dist/voice/index.d.ts +5 -0
  315. package/dist/voice/index.d.ts.map +1 -0
  316. package/dist/voice/index.js +4 -0
  317. package/dist/voice/index.js.map +1 -0
  318. package/dist/voice/voice-cleanup-prompt.d.ts +6 -0
  319. package/dist/voice/voice-cleanup-prompt.d.ts.map +1 -0
  320. package/dist/voice/voice-cleanup-prompt.js +26 -0
  321. package/dist/voice/voice-cleanup-prompt.js.map +1 -0
  322. package/dist/voice/voice-context.d.ts +25 -0
  323. package/dist/voice/voice-context.d.ts.map +1 -0
  324. package/dist/voice/voice-context.js +156 -0
  325. package/dist/voice/voice-context.js.map +1 -0
  326. package/dist/voice/voice-replacements.d.ts +4 -0
  327. package/dist/voice/voice-replacements.d.ts.map +1 -0
  328. package/dist/voice/voice-replacements.js +52 -0
  329. package/dist/voice/voice-replacements.js.map +1 -0
  330. package/docs/content/locales/ar-SA/template-design.mdx +41 -0
  331. package/docs/content/locales/de-DE/template-design.mdx +51 -0
  332. package/docs/content/locales/es-ES/template-design.mdx +50 -0
  333. package/docs/content/locales/fr-FR/template-design.mdx +49 -0
  334. package/docs/content/locales/hi-IN/template-design.mdx +44 -0
  335. package/docs/content/locales/ja-JP/template-design.mdx +46 -0
  336. package/docs/content/locales/ko-KR/template-design.mdx +44 -0
  337. package/docs/content/locales/pt-BR/template-design.mdx +48 -0
  338. package/docs/content/locales/zh-CN/template-design.mdx +36 -0
  339. package/docs/content/locales/zh-TW/template-design.mdx +38 -0
  340. package/docs/content/template-design.mdx +45 -0
  341. package/package.json +2 -1
  342. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  343. package/corpus/templates/design/app/components/design/DesignToolbar.tsx +0 -339
  344. package/corpus/templates/design/app/components/design/PresentMode.tsx +0 -68
  345. package/corpus/templates/design/app/components/design/ViewportTabs.tsx +0 -149
  346. package/corpus/templates/design/app/components/design/ZoomControls.tsx +0 -157
@@ -0,0 +1,1420 @@
1
+ import {
2
+ alphaToOpacity,
3
+ parseCssColor,
4
+ rgbaToCss,
5
+ rgbaToHex,
6
+ rgbaToHsl,
7
+ hslToRgba,
8
+ opacityToAlpha,
9
+ withColorOpacity,
10
+ type HslaColor,
11
+ type RgbaColor,
12
+ } from "@shared/color-utils";
13
+ import { IconChevronDown, IconColorPicker, IconX } from "@tabler/icons-react";
14
+ import {
15
+ useEffect,
16
+ useRef,
17
+ useState,
18
+ type JSX,
19
+ type KeyboardEvent,
20
+ type PointerEvent,
21
+ } from "react";
22
+
23
+ import { Input } from "@/components/ui/input";
24
+ import {
25
+ Popover,
26
+ PopoverContent,
27
+ PopoverTrigger,
28
+ } from "@/components/ui/popover";
29
+ import {
30
+ Select,
31
+ SelectContent,
32
+ SelectItem,
33
+ SelectTrigger,
34
+ SelectValue,
35
+ } from "@/components/ui/select";
36
+ import {
37
+ Tooltip,
38
+ TooltipContent,
39
+ TooltipTrigger,
40
+ } from "@/components/ui/tooltip";
41
+ import { cn } from "@/lib/utils";
42
+
43
+ // ─── Public types ──────────────────────────────────────────────────────────────
44
+
45
+ export type FigmaColorMode = "hex" | "rgb" | "hsl" | "hsb";
46
+ export type FigmaGradientType = "linear" | "radial" | "angular" | "diamond";
47
+ export type FigmaFillType = "solid" | "gradient" | "image";
48
+ export type FigmaPaintType =
49
+ | "solid"
50
+ | "linear"
51
+ | "radial"
52
+ | "angular"
53
+ | "diamond"
54
+ | "image"
55
+ | "video"
56
+ | "none";
57
+
58
+ // These interfaces remain so EditPanel's prop types don't break, even though
59
+ // the popover no longer renders the fills/gradient-stops list.
60
+ export interface FigmaFillRow {
61
+ id: string;
62
+ label: string;
63
+ value: string;
64
+ type: FigmaFillType;
65
+ opacity?: number;
66
+ swatch?: string;
67
+ selected?: boolean;
68
+ }
69
+
70
+ export interface FigmaFillRowPatch {
71
+ value?: string;
72
+ opacity?: number;
73
+ }
74
+
75
+ export interface FigmaGradientStop {
76
+ id: string;
77
+ color: string;
78
+ position: number;
79
+ opacity?: number;
80
+ label?: string;
81
+ }
82
+
83
+ export interface FigmaGradientStopPatch {
84
+ color?: string;
85
+ position?: number;
86
+ opacity?: number;
87
+ }
88
+
89
+ export interface FigmaColorPickerLabels {
90
+ trigger: string;
91
+ hex: string;
92
+ red: string;
93
+ green: string;
94
+ blue: string;
95
+ hue: string;
96
+ saturation: string;
97
+ saturationBrightness: string;
98
+ lightness: string;
99
+ brightness: string;
100
+ opacity: string;
101
+ blendMode: string;
102
+ fills: string;
103
+ addFill: string;
104
+ removeFill: string;
105
+ gradientType: string;
106
+ gradientStops: string;
107
+ addStop: string;
108
+ removeStop: string;
109
+ stopPosition: string;
110
+ linear: string;
111
+ radial: string;
112
+ angular: string;
113
+ diamond: string;
114
+ }
115
+
116
+ export interface FigmaColorPickerProps {
117
+ value: string;
118
+ onChange: (value: string) => void;
119
+ label?: string;
120
+ opacity?: number;
121
+ onOpacityChange?: (opacity: number) => void;
122
+ blendMode?: string;
123
+ onBlendModeChange?: (mode: string) => void;
124
+ showBlendMode?: boolean;
125
+ // Accepted but unused in the popover — list management lives in the sidebar.
126
+ fillRows?: FigmaFillRow[];
127
+ selectedFillId?: string;
128
+ onFillSelect?: (id: string) => void;
129
+ onFillChange?: (id: string, patch: FigmaFillRowPatch) => void;
130
+ onAddFill?: () => void;
131
+ onRemoveFill?: (id: string) => void;
132
+ paintType?: FigmaPaintType;
133
+ onPaintTypeChange?: (type: FigmaPaintType) => void;
134
+ gradientType?: FigmaGradientType;
135
+ onGradientTypeChange?: (type: FigmaGradientType) => void;
136
+ // Accepted but unused in the popover — gradient stop handles belong on canvas.
137
+ gradientStops?: FigmaGradientStop[];
138
+ selectedStopId?: string;
139
+ onGradientStopSelect?: (id: string) => void;
140
+ onGradientStopChange?: (id: string, patch: FigmaGradientStopPatch) => void;
141
+ onAddGradientStop?: () => void;
142
+ onRemoveGradientStop?: (id: string) => void;
143
+ labels?: Partial<FigmaColorPickerLabels>;
144
+ disabled?: boolean;
145
+ className?: string;
146
+ }
147
+
148
+ // ─── Internal types ────────────────────────────────────────────────────────────
149
+
150
+ interface HsvaColor {
151
+ h: number;
152
+ s: number;
153
+ v: number;
154
+ a: number;
155
+ }
156
+
157
+ // ─── Constants ─────────────────────────────────────────────────────────────────
158
+
159
+ const FALLBACK_COLOR: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
160
+
161
+ const DEFAULT_LABELS: FigmaColorPickerLabels = {
162
+ trigger: "Open color picker", // i18n-ignore fallback component label
163
+ hex: "Hex", // i18n-ignore fallback component label
164
+ red: "R", // i18n-ignore fallback component label
165
+ green: "G", // i18n-ignore fallback component label
166
+ blue: "B", // i18n-ignore fallback component label
167
+ hue: "H", // i18n-ignore fallback component label
168
+ saturation: "S", // i18n-ignore fallback component label
169
+ saturationBrightness: "Saturation and brightness", // i18n-ignore fallback component label
170
+ lightness: "L", // i18n-ignore fallback component label
171
+ brightness: "B", // i18n-ignore fallback component label
172
+ opacity: "Opacity", // i18n-ignore fallback component label
173
+ blendMode: "Blend", // i18n-ignore fallback component label
174
+ fills: "Fills", // i18n-ignore fallback component label
175
+ addFill: "Add fill", // i18n-ignore fallback component label
176
+ removeFill: "Remove fill", // i18n-ignore fallback component label
177
+ gradientType: "Type", // i18n-ignore fallback component label
178
+ gradientStops: "Gradient stops", // i18n-ignore fallback component label
179
+ addStop: "Add stop", // i18n-ignore fallback component label
180
+ removeStop: "Remove stop", // i18n-ignore fallback component label
181
+ stopPosition: "Position", // i18n-ignore fallback component label
182
+ linear: "Linear", // i18n-ignore fallback component label
183
+ radial: "Radial", // i18n-ignore fallback component label
184
+ angular: "Angular", // i18n-ignore fallback component label
185
+ diamond: "Diamond", // i18n-ignore fallback component label
186
+ };
187
+
188
+ // Figma-matching checkerboard: explicit light/dark tiles for legibility.
189
+ const CHECKER_A = "#d4d4d4";
190
+ const CHECKER_B = "#a3a3a3";
191
+ 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%)`;
192
+
193
+ // ─── Paint-type icon SVGs (Tabler style, distinct per type) ────────────────────
194
+
195
+ function IconSolid({ className }: { className?: string }) {
196
+ return (
197
+ <svg
198
+ viewBox="0 0 24 24"
199
+ fill="none"
200
+ stroke="currentColor"
201
+ strokeWidth={2}
202
+ strokeLinecap="round"
203
+ strokeLinejoin="round"
204
+ className={className}
205
+ >
206
+ <rect
207
+ x="4"
208
+ y="4"
209
+ width="16"
210
+ height="16"
211
+ rx="2"
212
+ fill="currentColor"
213
+ stroke="none"
214
+ />
215
+ </svg>
216
+ );
217
+ }
218
+
219
+ function IconLinearGradient({ className }: { className?: string }) {
220
+ return (
221
+ <svg
222
+ viewBox="0 0 24 24"
223
+ fill="none"
224
+ stroke="currentColor"
225
+ strokeWidth={2}
226
+ strokeLinecap="round"
227
+ strokeLinejoin="round"
228
+ className={className}
229
+ >
230
+ <defs>
231
+ <linearGradient id="lg-ico" x1="0" y1="0" x2="1" y2="0">
232
+ <stop offset="0%" stopColor="currentColor" stopOpacity="0" />
233
+ <stop offset="100%" stopColor="currentColor" stopOpacity="1" />
234
+ </linearGradient>
235
+ </defs>
236
+ <rect
237
+ x="4"
238
+ y="4"
239
+ width="16"
240
+ height="16"
241
+ rx="2"
242
+ fill="url(#lg-ico)"
243
+ stroke="currentColor"
244
+ strokeOpacity="0.5"
245
+ />
246
+ </svg>
247
+ );
248
+ }
249
+
250
+ function IconRadialGradient({ className }: { className?: string }) {
251
+ return (
252
+ <svg
253
+ viewBox="0 0 24 24"
254
+ fill="none"
255
+ stroke="currentColor"
256
+ strokeWidth={2}
257
+ strokeLinecap="round"
258
+ strokeLinejoin="round"
259
+ className={className}
260
+ >
261
+ <defs>
262
+ <radialGradient id="rg-ico" cx="50%" cy="50%" r="50%">
263
+ <stop offset="0%" stopColor="currentColor" stopOpacity="1" />
264
+ <stop offset="100%" stopColor="currentColor" stopOpacity="0" />
265
+ </radialGradient>
266
+ </defs>
267
+ <rect
268
+ x="4"
269
+ y="4"
270
+ width="16"
271
+ height="16"
272
+ rx="2"
273
+ fill="url(#rg-ico)"
274
+ stroke="currentColor"
275
+ strokeOpacity="0.5"
276
+ />
277
+ </svg>
278
+ );
279
+ }
280
+
281
+ function IconAngularGradient({ className }: { className?: string }) {
282
+ return (
283
+ <svg
284
+ viewBox="0 0 24 24"
285
+ fill="none"
286
+ stroke="currentColor"
287
+ strokeWidth={2}
288
+ strokeLinecap="round"
289
+ strokeLinejoin="round"
290
+ className={className}
291
+ >
292
+ <defs>
293
+ <linearGradient id="ag-ico" x1="0" y1="0" x2="1" y2="1">
294
+ <stop offset="0%" stopColor="currentColor" stopOpacity="1" />
295
+ <stop offset="100%" stopColor="currentColor" stopOpacity="0" />
296
+ </linearGradient>
297
+ </defs>
298
+ <rect
299
+ x="4"
300
+ y="4"
301
+ width="16"
302
+ height="16"
303
+ rx="2"
304
+ fill="url(#ag-ico)"
305
+ stroke="currentColor"
306
+ strokeOpacity="0.5"
307
+ />
308
+ <line
309
+ x1="12"
310
+ y1="4"
311
+ x2="12"
312
+ y2="20"
313
+ stroke="currentColor"
314
+ strokeOpacity="0.4"
315
+ strokeDasharray="2 2"
316
+ />
317
+ <line
318
+ x1="4"
319
+ y1="12"
320
+ x2="20"
321
+ y2="12"
322
+ stroke="currentColor"
323
+ strokeOpacity="0.4"
324
+ strokeDasharray="2 2"
325
+ />
326
+ </svg>
327
+ );
328
+ }
329
+
330
+ function IconDiamondGradient({ className }: { className?: string }) {
331
+ return (
332
+ <svg
333
+ viewBox="0 0 24 24"
334
+ fill="none"
335
+ stroke="currentColor"
336
+ strokeWidth={2}
337
+ strokeLinecap="round"
338
+ strokeLinejoin="round"
339
+ className={className}
340
+ >
341
+ <defs>
342
+ <radialGradient
343
+ id="dg-ico"
344
+ cx="50%"
345
+ cy="50%"
346
+ r="50%"
347
+ gradientTransform="scale(1, 1)"
348
+ >
349
+ <stop offset="0%" stopColor="currentColor" stopOpacity="1" />
350
+ <stop offset="100%" stopColor="currentColor" stopOpacity="0" />
351
+ </radialGradient>
352
+ </defs>
353
+ <polygon
354
+ points="12,4 20,12 12,20 4,12"
355
+ fill="url(#dg-ico)"
356
+ stroke="currentColor"
357
+ strokeOpacity="0.5"
358
+ />
359
+ </svg>
360
+ );
361
+ }
362
+
363
+ function IconImageFill({ className }: { className?: string }) {
364
+ return (
365
+ <svg
366
+ viewBox="0 0 24 24"
367
+ fill="none"
368
+ stroke="currentColor"
369
+ strokeWidth={2}
370
+ strokeLinecap="round"
371
+ strokeLinejoin="round"
372
+ className={className}
373
+ >
374
+ <rect x="3" y="3" width="18" height="18" rx="2" />
375
+ <path d="m3 16 5-5 4 4 3-3 6 6" />
376
+ <circle cx="8.5" cy="8.5" r="1.5" />
377
+ </svg>
378
+ );
379
+ }
380
+
381
+ function IconVideoFill({ className }: { className?: string }) {
382
+ return (
383
+ <svg
384
+ viewBox="0 0 24 24"
385
+ fill="none"
386
+ stroke="currentColor"
387
+ strokeWidth={2}
388
+ strokeLinecap="round"
389
+ strokeLinejoin="round"
390
+ className={className}
391
+ >
392
+ {/* Frame border */}
393
+ <rect x="3" y="5" width="18" height="14" rx="2" />
394
+ {/* Play triangle — filled, no stroke for clarity at small size */}
395
+ <polygon points="10,9 10,15 16,12" fill="currentColor" stroke="none" />
396
+ </svg>
397
+ );
398
+ }
399
+
400
+ function IconNoneFill({ className }: { className?: string }) {
401
+ return (
402
+ <svg
403
+ viewBox="0 0 24 24"
404
+ fill="none"
405
+ stroke="currentColor"
406
+ strokeWidth={2}
407
+ strokeLinecap="round"
408
+ strokeLinejoin="round"
409
+ className={className}
410
+ >
411
+ <circle cx="12" cy="12" r="9" />
412
+ <line x1="5.636" y1="5.636" x2="18.364" y2="18.364" />
413
+ </svg>
414
+ );
415
+ }
416
+
417
+ // ─── Paint type definitions (only supported types rendered) ────────────────────
418
+
419
+ const PAINT_TYPES: Array<{
420
+ type: FigmaPaintType;
421
+ label: string;
422
+ Icon: (props: { className?: string }) => JSX.Element;
423
+ }> = [
424
+ { type: "solid", label: "Solid", Icon: IconSolid }, // i18n-ignore paint type label
425
+ { type: "linear", label: "Linear", Icon: IconLinearGradient }, // i18n-ignore paint type label
426
+ { type: "radial", label: "Radial", Icon: IconRadialGradient }, // i18n-ignore paint type label
427
+ { type: "angular", label: "Angular", Icon: IconAngularGradient }, // i18n-ignore paint type label
428
+ { type: "diamond", label: "Diamond", Icon: IconDiamondGradient }, // i18n-ignore paint type label
429
+ { type: "image", label: "Image", Icon: IconImageFill }, // i18n-ignore paint type label
430
+ { type: "video", label: "Video", Icon: IconVideoFill }, // i18n-ignore paint type label
431
+ { type: "none", label: "None", Icon: IconNoneFill }, // i18n-ignore paint type label
432
+ ];
433
+
434
+ // ─── Document swatch placeholder (shown until real doc colors are provided) ────
435
+
436
+ const PLACEHOLDER_SWATCHES = [
437
+ "#1f2937",
438
+ "#6b7280",
439
+ "#ffffff",
440
+ "#a3a3a3",
441
+ "#f8fafc",
442
+ "#111111",
443
+ "#000000",
444
+ "#a8a29e",
445
+ "#22c1e8",
446
+ "#f08ce3",
447
+ "#737373",
448
+ "#050505",
449
+ "#23d4b5",
450
+ "#d9d9d2",
451
+ "#e6d05c",
452
+ "#9ca3af",
453
+ "#020617",
454
+ "#171717",
455
+ "#d4d4d8",
456
+ "#27272a",
457
+ ];
458
+
459
+ // ─── Main component ────────────────────────────────────────────────────────────
460
+
461
+ export function FigmaColorPicker({
462
+ value,
463
+ onChange,
464
+ label: _label,
465
+ opacity,
466
+ onOpacityChange,
467
+ blendMode: _blendMode,
468
+ onBlendModeChange: _onBlendModeChange,
469
+ showBlendMode: _showBlendMode,
470
+ fillRows: _fillRows,
471
+ selectedFillId: _selectedFillId,
472
+ onFillSelect: _onFillSelect,
473
+ onFillChange: _onFillChange,
474
+ onAddFill: _onAddFill,
475
+ onRemoveFill: _onRemoveFill,
476
+ paintType,
477
+ onPaintTypeChange,
478
+ gradientType: _gradientType,
479
+ onGradientTypeChange: _onGradientTypeChange,
480
+ gradientStops: _gradientStops,
481
+ selectedStopId: _selectedStopId,
482
+ onGradientStopSelect: _onGradientStopSelect,
483
+ onGradientStopChange: _onGradientStopChange,
484
+ onAddGradientStop: _onAddGradientStop,
485
+ onRemoveGradientStop: _onRemoveGradientStop,
486
+ labels,
487
+ disabled = false,
488
+ className,
489
+ }: FigmaColorPickerProps) {
490
+ const copy = { ...DEFAULT_LABELS, ...labels };
491
+ const color = parseCssColor(value) ?? FALLBACK_COLOR;
492
+ const hsv = rgbaToHsv(color);
493
+ const hsl = rgbaToHsl(color);
494
+ const effectiveOpacity = opacity ?? alphaToOpacity(color.a);
495
+
496
+ const [mode, setMode] = useState<FigmaColorMode>("hex");
497
+ const [hexDraft, setHexDraft] = useState(() => toDisplayHex(color));
498
+ const [open, setOpen] = useState(false);
499
+ const [sourceTab, setSourceTab] = useState<"custom" | "libraries">("custom");
500
+ const [picking, setPicking] = useState(false);
501
+ const skipNextHexBlurCommitRef = useRef(false);
502
+
503
+ const effectivePaintType =
504
+ paintType ?? inferPaintType(value, effectiveOpacity);
505
+
506
+ useEffect(() => {
507
+ setHexDraft(toDisplayHex(color));
508
+ }, [color.r, color.g, color.b]);
509
+
510
+ // ── Emit helpers ────────────────────────────────────────────────────────────
511
+
512
+ const emitColor = (nextColor: RgbaColor, nextOpacity = effectiveOpacity) => {
513
+ onChange(rgbaToCss(withColorOpacity(nextColor, nextOpacity)));
514
+ };
515
+
516
+ const emitColorFromHsv = (nextHsv: HsvaColor) => {
517
+ emitColor(hsvToRgba({ ...nextHsv, a: opacityToAlpha(effectiveOpacity) }));
518
+ };
519
+
520
+ const emitColorFromHsl = (nextHsl: HslaColor) => {
521
+ emitColor(hslToRgba({ ...nextHsl, a: opacityToAlpha(effectiveOpacity) }));
522
+ };
523
+
524
+ const commitHex = () => {
525
+ const parsed = parseCssColor(`#${hexDraft.replace(/^#/, "")}`);
526
+ if (!parsed) {
527
+ setHexDraft(toDisplayHex(color));
528
+ return;
529
+ }
530
+ const hexIncludesAlpha = hasHexAlpha(hexDraft);
531
+ const nextOpacity = hexIncludesAlpha
532
+ ? alphaToOpacity(parsed.a)
533
+ : effectiveOpacity;
534
+ if (hexIncludesAlpha && onOpacityChange) onOpacityChange(nextOpacity);
535
+ emitColor(parsed, nextOpacity);
536
+ };
537
+
538
+ const setOpacity = (nextOpacity: number) => {
539
+ if (onOpacityChange) onOpacityChange(nextOpacity);
540
+ else onChange(rgbaToCss(withColorOpacity(color, nextOpacity)));
541
+ };
542
+
543
+ const setPaintType = (nextType: FigmaPaintType) => {
544
+ if (disabled) return;
545
+ if (onPaintTypeChange) {
546
+ onPaintTypeChange(nextType);
547
+ return;
548
+ }
549
+ if (nextType === "none") {
550
+ onChange("transparent");
551
+ return;
552
+ }
553
+ if (nextType === "solid") {
554
+ emitColor(color, effectiveOpacity > 0 ? effectiveOpacity : 100);
555
+ return;
556
+ }
557
+ };
558
+
559
+ const pickScreenColor = async () => {
560
+ const EyeDropperCtor = (
561
+ window as unknown as {
562
+ EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> };
563
+ }
564
+ ).EyeDropper;
565
+ if (!EyeDropperCtor || disabled) return;
566
+ setPicking(true);
567
+ try {
568
+ const result = await new EyeDropperCtor().open();
569
+ if (result.sRGBHex) onChange(result.sRGBHex);
570
+ } catch {
571
+ // Browser cancels are expected; keep the current color.
572
+ } finally {
573
+ setPicking(false);
574
+ }
575
+ };
576
+
577
+ const hasEyeDropper = typeof window !== "undefined" && "EyeDropper" in window;
578
+
579
+ // ── Value row inputs by mode ─────────────────────────────────────────────────
580
+
581
+ function renderValueInputs() {
582
+ if (mode === "hex") {
583
+ return (
584
+ <Input
585
+ value={hexDraft}
586
+ disabled={disabled}
587
+ aria-label={copy.hex}
588
+ spellCheck={false}
589
+ className="h-6 min-w-0 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] tabular-nums uppercase"
590
+ onChange={(e) => setHexDraft(e.target.value)}
591
+ onFocus={(e) => e.target.select()}
592
+ onKeyDown={(e) => {
593
+ if (e.key === "Enter") {
594
+ e.preventDefault();
595
+ commitHex();
596
+ skipNextHexBlurCommitRef.current = true;
597
+ e.currentTarget.blur();
598
+ }
599
+ if (e.key === "Escape") {
600
+ setHexDraft(toDisplayHex(color));
601
+ skipNextHexBlurCommitRef.current = true;
602
+ e.currentTarget.blur();
603
+ }
604
+ }}
605
+ onBlur={() => {
606
+ if (skipNextHexBlurCommitRef.current) {
607
+ skipNextHexBlurCommitRef.current = false;
608
+ return;
609
+ }
610
+ commitHex();
611
+ }}
612
+ />
613
+ );
614
+ }
615
+ if (mode === "rgb") {
616
+ return (
617
+ <div className="flex gap-1">
618
+ {(["r", "g", "b"] as const).map((ch) => (
619
+ <ScrubbyNumberInput
620
+ key={ch}
621
+ aria-label={ch.toUpperCase()}
622
+ value={color[ch]}
623
+ min={0}
624
+ max={255}
625
+ disabled={disabled}
626
+ onChange={(next) => emitColor({ ...color, [ch]: next })}
627
+ />
628
+ ))}
629
+ </div>
630
+ );
631
+ }
632
+ if (mode === "hsl") {
633
+ return (
634
+ <div className="flex gap-1">
635
+ <ScrubbyNumberInput
636
+ aria-label={copy.hue}
637
+ value={hsl.h}
638
+ min={0}
639
+ max={360}
640
+ disabled={disabled}
641
+ onChange={(h) => emitColorFromHsl({ ...hsl, h })}
642
+ />
643
+ <ScrubbyNumberInput
644
+ aria-label={copy.saturation}
645
+ value={hsl.s}
646
+ min={0}
647
+ max={100}
648
+ disabled={disabled}
649
+ onChange={(s) => emitColorFromHsl({ ...hsl, s })}
650
+ />
651
+ <ScrubbyNumberInput
652
+ aria-label={copy.lightness}
653
+ value={hsl.l}
654
+ min={0}
655
+ max={100}
656
+ disabled={disabled}
657
+ onChange={(l) => emitColorFromHsl({ ...hsl, l })}
658
+ />
659
+ </div>
660
+ );
661
+ }
662
+ // hsb
663
+ return (
664
+ <div className="flex gap-1">
665
+ <ScrubbyNumberInput
666
+ aria-label={copy.hue}
667
+ value={hsv.h}
668
+ min={0}
669
+ max={360}
670
+ disabled={disabled}
671
+ onChange={(h) => emitColorFromHsv({ ...hsv, h })}
672
+ />
673
+ <ScrubbyNumberInput
674
+ aria-label={copy.saturation}
675
+ value={hsv.s}
676
+ min={0}
677
+ max={100}
678
+ disabled={disabled}
679
+ onChange={(s) => emitColorFromHsv({ ...hsv, s })}
680
+ />
681
+ <ScrubbyNumberInput
682
+ aria-label={copy.brightness}
683
+ value={hsv.v}
684
+ min={0}
685
+ max={100}
686
+ disabled={disabled}
687
+ onChange={(v) => emitColorFromHsv({ ...hsv, v })}
688
+ />
689
+ </div>
690
+ );
691
+ }
692
+
693
+ // ─────────────────────────────────────────────────────────────────────────────
694
+
695
+ return (
696
+ <div className={cn("space-y-1.5", className)}>
697
+ <Popover open={open} onOpenChange={setOpen}>
698
+ <PopoverTrigger asChild>
699
+ {/* Trigger: compact swatch + hex + opacity% — matches Figma's fill row */}
700
+ <button
701
+ type="button"
702
+ disabled={disabled}
703
+ aria-label={copy.trigger}
704
+ className={cn(
705
+ "flex h-6 w-full items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] shadow-none",
706
+ "hover:bg-[var(--design-editor-panel-raised-bg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
707
+ disabled && "pointer-events-none opacity-50",
708
+ )}
709
+ >
710
+ {/* Flat swatch chip — no shadow-inner (Figma is flat) */}
711
+ <span
712
+ className="size-4 shrink-0 rounded-[3px] border border-border/60"
713
+ style={swatchStyle(rgbaToCss(color))}
714
+ />
715
+ <span className="min-w-0 flex-1 truncate text-left tabular-nums uppercase text-[11px]">
716
+ {toDisplayHex(color)}
717
+ </span>
718
+ <span className="tabular-nums text-muted-foreground text-[11px]">
719
+ {effectiveOpacity}%
720
+ </span>
721
+ </button>
722
+ </PopoverTrigger>
723
+
724
+ {/* Figma popover: ~240px wide, uniform 12px padding, tight controls */}
725
+ <PopoverContent
726
+ side="left"
727
+ align="start"
728
+ sideOffset={8}
729
+ className="z-[10000] w-[252px] p-0 shadow-xl"
730
+ >
731
+ <div className="overflow-hidden rounded-md bg-popover text-popover-foreground">
732
+ {/* ── Header: Custom | Libraries + ✕ ────────────────────────── */}
733
+ <div className="flex items-center justify-between px-3 py-1.5">
734
+ <div className="flex items-center gap-2">
735
+ {(["custom", "libraries"] as const).map((tab) => (
736
+ <button
737
+ key={tab}
738
+ type="button"
739
+ role="tab"
740
+ aria-selected={sourceTab === tab}
741
+ onClick={() => setSourceTab(tab)}
742
+ className={cn(
743
+ "cursor-pointer rounded px-1.5 py-0.5 text-[11px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
744
+ sourceTab === tab
745
+ ? "bg-[var(--design-editor-control-bg)] text-foreground shadow-[inset_0_0_0_1px_var(--design-editor-control-border)]"
746
+ : "text-muted-foreground hover:text-foreground",
747
+ )}
748
+ >
749
+ {
750
+ tab === "custom"
751
+ ? "Custom" /* i18n-ignore Figma picker tab */
752
+ : "Libraries" /* i18n-ignore Figma picker tab */
753
+ }
754
+ </button>
755
+ ))}
756
+ </div>
757
+ <Tooltip>
758
+ <TooltipTrigger asChild>
759
+ <button
760
+ type="button"
761
+ aria-label={
762
+ "Close" /* i18n-ignore Figma picker close label */
763
+ }
764
+ onClick={() => setOpen(false)}
765
+ className="flex size-5 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
766
+ >
767
+ <IconX className="size-3" />
768
+ </button>
769
+ </TooltipTrigger>
770
+ <TooltipContent>{"Close" /* i18n-ignore */}</TooltipContent>
771
+ </Tooltip>
772
+ </div>
773
+
774
+ {sourceTab === "custom" ? (
775
+ <>
776
+ {/* ── Paint-type icon row ──────────────────────────────────── */}
777
+ <div className="border-t border-border/70 px-2 py-1.5">
778
+ <div className="grid grid-cols-8 gap-px">
779
+ {PAINT_TYPES.map(({ type, label, Icon }) => {
780
+ const isActive = effectivePaintType === type;
781
+ const isGradientType =
782
+ type === "linear" ||
783
+ type === "radial" ||
784
+ type === "angular" ||
785
+ type === "diamond";
786
+ const gradientDisabled =
787
+ isGradientType &&
788
+ !onPaintTypeChange &&
789
+ !_onGradientTypeChange &&
790
+ !_onAddGradientStop;
791
+ const imageDisabled =
792
+ (type === "image" || type === "video") &&
793
+ !onPaintTypeChange;
794
+ const isDisabled =
795
+ disabled || gradientDisabled || imageDisabled;
796
+ return (
797
+ <Tooltip key={type}>
798
+ <TooltipTrigger asChild>
799
+ <button
800
+ type="button"
801
+ aria-label={label}
802
+ aria-pressed={isActive}
803
+ disabled={isDisabled}
804
+ onClick={() => setPaintType(type)}
805
+ className={cn(
806
+ "flex h-6 w-full cursor-pointer items-center justify-center rounded-sm transition-colors",
807
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
808
+ "active:scale-95",
809
+ isActive
810
+ ? "bg-primary/10 text-primary shadow-[inset_0_0_0_1px_var(--primary)]"
811
+ : "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
812
+ isDisabled && "pointer-events-none opacity-40",
813
+ )}
814
+ >
815
+ <Icon className="size-3.5" />
816
+ </button>
817
+ </TooltipTrigger>
818
+ <TooltipContent>{label}</TooltipContent>
819
+ </Tooltip>
820
+ );
821
+ })}
822
+ </div>
823
+ </div>
824
+
825
+ {/* ── 2D Saturation/Brightness field (edge-to-edge, no side padding) */}
826
+ <div className="border-t border-border/70">
827
+ <SaturationBrightnessField
828
+ hsv={hsv}
829
+ label={copy.saturationBrightness}
830
+ disabled={disabled}
831
+ onChange={emitColorFromHsv}
832
+ />
833
+ </div>
834
+
835
+ {/* ── Eyedropper + Hue slider / Swatch + Alpha slider ─────── */}
836
+ {/* Left gutter spans both rows with dropper centered vertically */}
837
+ <div className="mt-2.5 px-3">
838
+ <div className="grid grid-cols-[1.5rem_1fr] items-center gap-x-2">
839
+ {/* Eyedropper centered across the two slider rows via row-span-2 */}
840
+ <div className="row-span-2 flex items-center justify-center">
841
+ <Tooltip>
842
+ <TooltipTrigger asChild>
843
+ <button
844
+ type="button"
845
+ aria-label={
846
+ "Pick color" /* i18n-ignore browser eyedropper label */
847
+ }
848
+ disabled={disabled || !hasEyeDropper}
849
+ onClick={() => void pickScreenColor()}
850
+ className={cn(
851
+ "flex size-6 cursor-pointer items-center justify-center rounded-sm transition-colors",
852
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
853
+ picking
854
+ ? "bg-primary/10 text-primary ring-1 ring-primary/50"
855
+ : "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
856
+ (disabled || !hasEyeDropper) &&
857
+ "pointer-events-none opacity-40",
858
+ )}
859
+ >
860
+ <IconColorPicker className="size-4" />
861
+ </button>
862
+ </TooltipTrigger>
863
+ <TooltipContent>
864
+ {
865
+ hasEyeDropper
866
+ ? "Pick color" // i18n-ignore browser eyedropper label
867
+ : "Not supported in this browser" // i18n-ignore browser eyedropper disabled label
868
+ }
869
+ </TooltipContent>
870
+ </Tooltip>
871
+ </div>
872
+
873
+ {/* Hue track */}
874
+ <ColorTrack
875
+ label={copy.hue}
876
+ value={hsv.h}
877
+ min={0}
878
+ max={360}
879
+ disabled={disabled}
880
+ backgroundImage="linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)"
881
+ onChange={(next) =>
882
+ emitColorFromHsv({ ...hsv, h: next === 360 ? 0 : next })
883
+ }
884
+ />
885
+
886
+ {/* Current-color swatch left of alpha (matches Figma's layout) */}
887
+ <div className="flex items-center gap-2">
888
+ <span
889
+ className="size-[18px] shrink-0 rounded-[3px] border border-border/60"
890
+ style={swatchStyle(rgbaToCss(color))}
891
+ />
892
+ {/* Alpha track fills remaining width */}
893
+ <div className="flex-1">
894
+ <ColorTrack
895
+ label={copy.opacity}
896
+ value={effectiveOpacity}
897
+ min={0}
898
+ max={100}
899
+ disabled={disabled}
900
+ backgroundImage={alphaTrackBackground(color)}
901
+ backgroundSize="8px 8px, 8px 8px, 8px 8px, 8px 8px, 100% 100%"
902
+ backgroundPosition="0 0, 0 4px, 4px -4px, -4px 0, 0 0"
903
+ onChange={setOpacity}
904
+ />
905
+ </div>
906
+ </div>
907
+ </div>
908
+ </div>
909
+
910
+ {/* ── Value row: [Hex ▾] [value input(s)] [opacity %] ─────── */}
911
+ <div className="mt-2.5 px-3 pb-3">
912
+ <div className="grid grid-cols-[4.5rem_1fr_3rem] items-center gap-1">
913
+ {/* Model dropdown */}
914
+ <Select
915
+ value={mode}
916
+ onValueChange={(v) => setMode(v as FigmaColorMode)}
917
+ disabled={disabled}
918
+ >
919
+ <SelectTrigger className="h-6 w-[4.5rem] rounded-md border-transparent bg-transparent px-1.5 text-[11px] font-semibold shadow-none hover:bg-[var(--design-editor-control-bg)] focus:ring-0 focus:ring-offset-0 focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-3 [&>svg]:shrink-0">
920
+ <SelectValue />
921
+ </SelectTrigger>
922
+ <SelectContent className="text-[11px]">
923
+ <SelectItem value="hex" className="text-[11px]">
924
+ Hex
925
+ </SelectItem>{" "}
926
+ {/* i18n-ignore color mode */}
927
+ <SelectItem value="rgb" className="text-[11px]">
928
+ RGB
929
+ </SelectItem>{" "}
930
+ {/* i18n-ignore color mode */}
931
+ <SelectItem value="hsl" className="text-[11px]">
932
+ HSL
933
+ </SelectItem>{" "}
934
+ {/* i18n-ignore color mode */}
935
+ <SelectItem value="hsb" className="text-[11px]">
936
+ HSB
937
+ </SelectItem>{" "}
938
+ {/* i18n-ignore color mode */}
939
+ </SelectContent>
940
+ </Select>
941
+
942
+ {/* Value field(s) — adapts to mode */}
943
+ {renderValueInputs()}
944
+
945
+ {/* Opacity % field */}
946
+ <div className="flex h-6 overflow-hidden rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)]">
947
+ <ScrubbyNumberInput
948
+ aria-label={copy.opacity}
949
+ value={effectiveOpacity}
950
+ min={0}
951
+ max={100}
952
+ disabled={disabled}
953
+ onChange={setOpacity}
954
+ className="h-full min-w-0 flex-1 rounded-none border-0 bg-transparent px-1 text-[11px] tabular-nums shadow-none focus-visible:ring-0"
955
+ compact
956
+ />
957
+ <span className="flex w-4 shrink-0 items-center justify-center border-l border-border/60 text-[10px] text-muted-foreground">
958
+ %
959
+ </span>
960
+ </div>
961
+ </div>
962
+ </div>
963
+
964
+ {/* ── "On this page" source + swatches ────────────────────── */}
965
+ <div className="border-t border-border/70 px-3 py-2.5">
966
+ {/* Source selector styled as a full-width bordered dropdown */}
967
+ <button
968
+ type="button"
969
+ className="mb-2 flex h-6 w-full items-center justify-between rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-left text-[11px] text-foreground hover:bg-[var(--design-editor-panel-raised-bg)]"
970
+ >
971
+ {"On this page" /* i18n-ignore Figma picker source */}
972
+ <IconChevronDown className="size-3 text-muted-foreground" />
973
+ </button>
974
+
975
+ {/* Swatch grid: flat chips (no shadow-inner), 8 columns */}
976
+ <div className="grid grid-cols-8 gap-1">
977
+ {PLACEHOLDER_SWATCHES.map((swatch, index) => {
978
+ const isCurrentColor =
979
+ rgbaToHex(parseCssColor(swatch) ?? FALLBACK_COLOR) ===
980
+ rgbaToHex(color);
981
+ return (
982
+ <Tooltip key={`${swatch}-${index}`}>
983
+ <TooltipTrigger asChild>
984
+ <button
985
+ type="button"
986
+ disabled={disabled}
987
+ aria-label={swatch}
988
+ aria-pressed={isCurrentColor}
989
+ className={cn(
990
+ "size-5 rounded-sm border transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
991
+ isCurrentColor
992
+ ? "border-primary ring-1 ring-primary"
993
+ : "border-border/60",
994
+ )}
995
+ style={swatchStyle(swatch)}
996
+ onClick={() =>
997
+ emitColor(parseCssColor(swatch) ?? color)
998
+ }
999
+ />
1000
+ </TooltipTrigger>
1001
+ <TooltipContent>{swatch}</TooltipContent>
1002
+ </Tooltip>
1003
+ );
1004
+ })}
1005
+ </div>
1006
+ </div>
1007
+ </>
1008
+ ) : (
1009
+ /* ── Libraries tab: honest empty state ────────────────────── */
1010
+ <div className="border-t border-border/70 p-3">
1011
+ <div className="rounded-md border border-dashed border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-3 py-5 text-center">
1012
+ <p className="text-[11px] font-medium text-foreground">
1013
+ {
1014
+ "No color libraries connected" /* i18n-ignore Figma picker library empty state */
1015
+ }
1016
+ </p>
1017
+ <p className="mt-1 text-[10px] text-muted-foreground">
1018
+ {
1019
+ "Connect a library in the Assets panel to browse colors here." /* i18n-ignore Figma picker library hint */
1020
+ }
1021
+ </p>
1022
+ </div>
1023
+ </div>
1024
+ )}
1025
+ </div>
1026
+ </PopoverContent>
1027
+ </Popover>
1028
+ </div>
1029
+ );
1030
+ }
1031
+
1032
+ // ─── Sub-components ────────────────────────────────────────────────────────────
1033
+
1034
+ function SaturationBrightnessField({
1035
+ hsv,
1036
+ label,
1037
+ disabled,
1038
+ onChange,
1039
+ }: {
1040
+ hsv: HsvaColor;
1041
+ label: string;
1042
+ disabled: boolean;
1043
+ onChange: (color: HsvaColor) => void;
1044
+ }) {
1045
+ const fieldRef = useRef<HTMLDivElement>(null);
1046
+ const draggingRef = useRef(false);
1047
+ const hueColor = rgbaToCss(hsvToRgba({ h: hsv.h, s: 100, v: 100, a: 1 }));
1048
+
1049
+ const updateFromPointer = (event: PointerEvent<HTMLDivElement>) => {
1050
+ const rect = fieldRef.current?.getBoundingClientRect();
1051
+ if (!rect) return;
1052
+ const nextSaturation = ((event.clientX - rect.left) / rect.width) * 100;
1053
+ const nextBrightness =
1054
+ 100 - ((event.clientY - rect.top) / rect.height) * 100;
1055
+ onChange({
1056
+ ...hsv,
1057
+ s: clamp(nextSaturation, 0, 100),
1058
+ v: clamp(nextBrightness, 0, 100),
1059
+ });
1060
+ };
1061
+
1062
+ const stepWithKeyboard = (event: KeyboardEvent<HTMLDivElement>) => {
1063
+ if (disabled) return;
1064
+ const step = event.shiftKey ? 10 : 1;
1065
+ if (event.key === "ArrowRight") {
1066
+ event.preventDefault();
1067
+ onChange({ ...hsv, s: clamp(hsv.s + step, 0, 100) });
1068
+ }
1069
+ if (event.key === "ArrowLeft") {
1070
+ event.preventDefault();
1071
+ onChange({ ...hsv, s: clamp(hsv.s - step, 0, 100) });
1072
+ }
1073
+ if (event.key === "ArrowUp") {
1074
+ event.preventDefault();
1075
+ onChange({ ...hsv, v: clamp(hsv.v + step, 0, 100) });
1076
+ }
1077
+ if (event.key === "ArrowDown") {
1078
+ event.preventDefault();
1079
+ onChange({ ...hsv, v: clamp(hsv.v - step, 0, 100) });
1080
+ }
1081
+ };
1082
+
1083
+ return (
1084
+ <div
1085
+ ref={fieldRef}
1086
+ tabIndex={disabled ? -1 : 0}
1087
+ aria-label={label}
1088
+ aria-disabled={disabled}
1089
+ onPointerDown={(event) => {
1090
+ if (disabled) return;
1091
+ draggingRef.current = true;
1092
+ event.currentTarget.setPointerCapture(event.pointerId);
1093
+ updateFromPointer(event);
1094
+ }}
1095
+ onPointerMove={(event) => {
1096
+ if (!draggingRef.current || disabled) return;
1097
+ updateFromPointer(event);
1098
+ }}
1099
+ onPointerUp={(event) => {
1100
+ draggingRef.current = false;
1101
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1102
+ event.currentTarget.releasePointerCapture(event.pointerId);
1103
+ }
1104
+ }}
1105
+ onPointerCancel={() => {
1106
+ draggingRef.current = false;
1107
+ }}
1108
+ onKeyDown={stepWithKeyboard}
1109
+ className={cn(
1110
+ "relative h-48 w-full cursor-crosshair overflow-hidden outline-none",
1111
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
1112
+ "active:cursor-grabbing",
1113
+ disabled && "cursor-not-allowed opacity-60",
1114
+ )}
1115
+ style={{
1116
+ backgroundImage: `linear-gradient(to top, #000 0%, transparent 100%), linear-gradient(to right, #fff 0%, ${hueColor} 100%)`,
1117
+ }}
1118
+ >
1119
+ {/* Handle: size-4, white ring, consistent foreground shadow */}
1120
+ <span
1121
+ className="pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1px_hsl(var(--foreground)/0.6)]"
1122
+ style={{
1123
+ left: `${hsv.s}%`,
1124
+ top: `${100 - hsv.v}%`,
1125
+ }}
1126
+ />
1127
+ </div>
1128
+ );
1129
+ }
1130
+
1131
+ function ColorTrack({
1132
+ label,
1133
+ value,
1134
+ min,
1135
+ max,
1136
+ disabled,
1137
+ backgroundImage,
1138
+ backgroundSize,
1139
+ backgroundPosition,
1140
+ onChange,
1141
+ }: {
1142
+ label: string;
1143
+ value: number;
1144
+ min: number;
1145
+ max: number;
1146
+ disabled: boolean;
1147
+ backgroundImage: string;
1148
+ backgroundSize?: string;
1149
+ backgroundPosition?: string;
1150
+ onChange: (value: number) => void;
1151
+ }) {
1152
+ const trackRef = useRef<HTMLDivElement>(null);
1153
+ const draggingRef = useRef(false);
1154
+ const percent = ((value - min) / (max - min)) * 100;
1155
+
1156
+ const updateFromPointer = (event: PointerEvent<HTMLDivElement>) => {
1157
+ const rect = trackRef.current?.getBoundingClientRect();
1158
+ if (!rect) return;
1159
+ const next = min + ((event.clientX - rect.left) / rect.width) * (max - min);
1160
+ onChange(clamp(next, min, max));
1161
+ };
1162
+
1163
+ const stepWithKeyboard = (event: KeyboardEvent<HTMLDivElement>) => {
1164
+ if (disabled) return;
1165
+ const step = event.shiftKey ? 10 : 1;
1166
+ if (event.key === "ArrowRight" || event.key === "ArrowUp") {
1167
+ event.preventDefault();
1168
+ onChange(clamp(value + step, min, max));
1169
+ }
1170
+ if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
1171
+ event.preventDefault();
1172
+ onChange(clamp(value - step, min, max));
1173
+ }
1174
+ if (event.key === "Home") {
1175
+ event.preventDefault();
1176
+ onChange(min);
1177
+ }
1178
+ if (event.key === "End") {
1179
+ event.preventDefault();
1180
+ onChange(max);
1181
+ }
1182
+ };
1183
+
1184
+ return (
1185
+ <div
1186
+ ref={trackRef}
1187
+ role="slider"
1188
+ tabIndex={disabled ? -1 : 0}
1189
+ aria-label={label}
1190
+ aria-valuemin={min}
1191
+ aria-valuemax={max}
1192
+ aria-valuenow={Math.round(value)}
1193
+ aria-disabled={disabled}
1194
+ onKeyDown={stepWithKeyboard}
1195
+ onPointerDown={(event) => {
1196
+ if (disabled) return;
1197
+ draggingRef.current = true;
1198
+ event.currentTarget.setPointerCapture(event.pointerId);
1199
+ updateFromPointer(event);
1200
+ }}
1201
+ onPointerMove={(event) => {
1202
+ if (!draggingRef.current || disabled) return;
1203
+ updateFromPointer(event);
1204
+ }}
1205
+ onPointerUp={(event) => {
1206
+ draggingRef.current = false;
1207
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1208
+ event.currentTarget.releasePointerCapture(event.pointerId);
1209
+ }
1210
+ }}
1211
+ onPointerCancel={() => {
1212
+ draggingRef.current = false;
1213
+ }}
1214
+ className={cn(
1215
+ "relative h-3.5 cursor-pointer rounded-full border border-border/60 outline-none",
1216
+ "ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
1217
+ "active:cursor-grabbing",
1218
+ disabled && "cursor-not-allowed opacity-60",
1219
+ )}
1220
+ style={{ backgroundImage, backgroundSize, backgroundPosition }}
1221
+ >
1222
+ {/* Thumb overhangs the track slightly, matching Figma */}
1223
+ <span
1224
+ className="pointer-events-none absolute top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1px_hsl(var(--foreground)/0.6)]"
1225
+ style={{ left: `${clamp(percent, 0, 100)}%` }}
1226
+ />
1227
+ </div>
1228
+ );
1229
+ }
1230
+
1231
+ /**
1232
+ * A number input with select-on-focus and Esc-to-revert.
1233
+ * The `compact` prop removes inner padding for use inside bordered wrappers.
1234
+ */
1235
+ function ScrubbyNumberInput({
1236
+ "aria-label": ariaLabel,
1237
+ value,
1238
+ min,
1239
+ max,
1240
+ disabled,
1241
+ onChange,
1242
+ className,
1243
+ compact = false,
1244
+ }: {
1245
+ "aria-label": string;
1246
+ value: number;
1247
+ min: number;
1248
+ max: number;
1249
+ disabled: boolean;
1250
+ onChange: (value: number) => void;
1251
+ className?: string;
1252
+ compact?: boolean;
1253
+ }) {
1254
+ const [draft, setDraft] = useState<string>(() => String(value));
1255
+ const skipBlurRef = useRef(false);
1256
+
1257
+ useEffect(() => {
1258
+ setDraft(String(value));
1259
+ }, [value]);
1260
+
1261
+ const commit = () => {
1262
+ const parsed = Number(draft);
1263
+ if (!Number.isFinite(parsed)) {
1264
+ setDraft(String(value));
1265
+ return;
1266
+ }
1267
+ onChange(clamp(parsed, min, max));
1268
+ };
1269
+
1270
+ return (
1271
+ <input
1272
+ type="number"
1273
+ aria-label={ariaLabel}
1274
+ value={draft}
1275
+ min={min}
1276
+ max={max}
1277
+ disabled={disabled}
1278
+ className={cn(
1279
+ "h-6 w-full rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-center text-[11px] tabular-nums",
1280
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1281
+ compact && "border-0 shadow-none focus-visible:ring-0",
1282
+ className,
1283
+ )}
1284
+ onChange={(e) => setDraft(e.target.value)}
1285
+ onFocus={(e) => e.target.select()}
1286
+ onKeyDown={(e) => {
1287
+ if (e.key === "Enter") {
1288
+ e.preventDefault();
1289
+ commit();
1290
+ skipBlurRef.current = true;
1291
+ e.currentTarget.blur();
1292
+ }
1293
+ if (e.key === "Escape") {
1294
+ setDraft(String(value));
1295
+ skipBlurRef.current = true;
1296
+ e.currentTarget.blur();
1297
+ }
1298
+ if (e.key === "ArrowUp") {
1299
+ e.preventDefault();
1300
+ const step = e.shiftKey ? 10 : 1;
1301
+ onChange(clamp(value + step, min, max));
1302
+ }
1303
+ if (e.key === "ArrowDown") {
1304
+ e.preventDefault();
1305
+ const step = e.shiftKey ? 10 : 1;
1306
+ onChange(clamp(value - step, min, max));
1307
+ }
1308
+ }}
1309
+ onBlur={() => {
1310
+ if (skipBlurRef.current) {
1311
+ skipBlurRef.current = false;
1312
+ return;
1313
+ }
1314
+ commit();
1315
+ }}
1316
+ />
1317
+ );
1318
+ }
1319
+
1320
+ // ─── Utilities ─────────────────────────────────────────────────────────────────
1321
+
1322
+ function inferPaintType(value: string, opacity: number): FigmaPaintType {
1323
+ const parsed = parseCssColor(value);
1324
+ if (opacity <= 0 || parsed?.a === 0 || value.trim() === "transparent") {
1325
+ return "none";
1326
+ }
1327
+ return "solid";
1328
+ }
1329
+
1330
+ /** Show hex without the leading # for the display field (matches Figma). */
1331
+ function toDisplayHex(color: RgbaColor): string {
1332
+ return rgbaToHex(color).replace(/^#/, "").toUpperCase();
1333
+ }
1334
+
1335
+ function swatchStyle(value: string): {
1336
+ backgroundColor?: string;
1337
+ backgroundImage?: string;
1338
+ backgroundSize?: string;
1339
+ backgroundPosition?: string;
1340
+ } {
1341
+ const parsed = parseCssColor(value);
1342
+ if (parsed && parsed.a < 1) {
1343
+ return {
1344
+ backgroundImage: `${CHECKERBOARD_IMAGE}, linear-gradient(${rgbaToCss(parsed)}, ${rgbaToCss(parsed)})`,
1345
+ backgroundSize: "8px 8px, 8px 8px, 8px 8px, 8px 8px, 100% 100%",
1346
+ backgroundPosition: "0 0, 0 4px, 4px -4px, -4px 0, 0 0",
1347
+ };
1348
+ }
1349
+ if (parsed) return { backgroundColor: rgbaToCss(parsed) };
1350
+ return { backgroundImage: value || "none" };
1351
+ }
1352
+
1353
+ function alphaTrackBackground(color: RgbaColor): string {
1354
+ return `${CHECKERBOARD_IMAGE}, linear-gradient(90deg, rgba(${color.r}, ${color.g}, ${color.b}, 0), rgba(${color.r}, ${color.g}, ${color.b}, 1))`;
1355
+ }
1356
+
1357
+ function rgbaToHsv(color: RgbaColor): HsvaColor {
1358
+ const r = clampFloat(color.r / 255, 0, 1);
1359
+ const g = clampFloat(color.g / 255, 0, 1);
1360
+ const b = clampFloat(color.b / 255, 0, 1);
1361
+ const max = Math.max(r, g, b);
1362
+ const min = Math.min(r, g, b);
1363
+ const delta = max - min;
1364
+
1365
+ let h = 0;
1366
+ if (delta !== 0) {
1367
+ if (max === r) h = ((g - b) / delta) % 6;
1368
+ else if (max === g) h = (b - r) / delta + 2;
1369
+ else h = (r - g) / delta + 4;
1370
+ h *= 60;
1371
+ if (h < 0) h += 360;
1372
+ }
1373
+
1374
+ return {
1375
+ h: Math.round(h),
1376
+ s: max === 0 ? 0 : Math.round((delta / max) * 100),
1377
+ v: Math.round(max * 100),
1378
+ a: color.a,
1379
+ };
1380
+ }
1381
+
1382
+ function hsvToRgba(color: HsvaColor): RgbaColor {
1383
+ const h = ((color.h % 360) + 360) % 360;
1384
+ const s = clampFloat(color.s, 0, 100) / 100;
1385
+ const v = clampFloat(color.v, 0, 100) / 100;
1386
+ const chroma = v * s;
1387
+ const x = chroma * (1 - Math.abs(((h / 60) % 2) - 1));
1388
+ const m = v - chroma;
1389
+
1390
+ let r = 0,
1391
+ g = 0,
1392
+ b = 0;
1393
+ if (h < 60) [r, g, b] = [chroma, x, 0];
1394
+ else if (h < 120) [r, g, b] = [x, chroma, 0];
1395
+ else if (h < 180) [r, g, b] = [0, chroma, x];
1396
+ else if (h < 240) [r, g, b] = [0, x, chroma];
1397
+ else if (h < 300) [r, g, b] = [x, 0, chroma];
1398
+ else [r, g, b] = [chroma, 0, x];
1399
+
1400
+ return {
1401
+ r: clamp(Math.round((r + m) * 255), 0, 255),
1402
+ g: clamp(Math.round((g + m) * 255), 0, 255),
1403
+ b: clamp(Math.round((b + m) * 255), 0, 255),
1404
+ a: clampFloat(color.a, 0, 1),
1405
+ };
1406
+ }
1407
+
1408
+ function clamp(value: number, min: number, max: number): number {
1409
+ if (!Number.isFinite(value)) return min;
1410
+ return Math.max(min, Math.min(max, Math.round(value)));
1411
+ }
1412
+
1413
+ function clampFloat(value: number, min: number, max: number): number {
1414
+ if (!Number.isFinite(value)) return min;
1415
+ return Math.max(min, Math.min(max, value));
1416
+ }
1417
+
1418
+ function hasHexAlpha(value: string): boolean {
1419
+ return /^#?(?:[0-9a-f]{4}|[0-9a-f]{8})$/i.test(value.trim());
1420
+ }