@agent-native/core 0.92.10 → 0.92.12

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 (354) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +29 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +214 -17
  5. package/corpus/core/src/agent/run-manager.ts +72 -14
  6. package/corpus/core/src/agent/run-store.ts +152 -27
  7. package/corpus/core/src/cli/design-connect.ts +433 -151
  8. package/corpus/core/src/cli/skills.ts +14 -0
  9. package/corpus/core/src/client/AssistantChat.tsx +25 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +40 -6
  11. package/corpus/core/src/client/agent-chat-adapter.ts +56 -12
  12. package/corpus/core/src/client/agent-chat.ts +165 -1
  13. package/corpus/core/src/client/chat/index.ts +9 -1
  14. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +38 -2
  15. package/corpus/core/src/client/index.ts +5 -0
  16. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +18 -7
  17. package/corpus/core/src/collab/awareness-store.ts +23 -12
  18. package/corpus/core/src/collab/client.ts +16 -5
  19. package/corpus/core/src/collab/index.ts +4 -0
  20. package/corpus/core/src/collab/recent-edits.ts +65 -2
  21. package/corpus/core/src/index.browser.ts +1 -0
  22. package/corpus/core/src/server/agent-chat-plugin.ts +98 -11
  23. package/corpus/core/src/server/builder-design-systems.ts +30 -4
  24. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +1 -1
  25. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +12 -6
  26. package/corpus/templates/clips/AGENTS.md +4 -0
  27. package/corpus/templates/clips/actions/create-recording.ts +2 -0
  28. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +5 -3
  29. package/corpus/templates/clips/actions/request-transcript.ts +102 -33
  30. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -0
  31. package/corpus/templates/clips/app/components/player/share-dialog.tsx +59 -19
  32. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +75 -9
  33. package/corpus/templates/clips/app/components/player/video-player.tsx +17 -12
  34. package/corpus/templates/clips/app/hooks/use-player-shortcuts.ts +1 -0
  35. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
  36. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
  37. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  38. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
  39. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
  40. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
  41. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
  42. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
  43. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
  44. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
  45. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
  46. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -37
  47. package/corpus/templates/clips/app/routes/record.tsx +17 -4
  48. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  49. package/corpus/templates/clips/changelog/2026-07-09-desktop-popover-sits-closer-to-the-menu-bar.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-09-desktop-recording-optimization-progress-stays-visible-until-the-clip-opens.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-09-fixed-large-video-file-uploads-failing-before-their-resumabl.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-09-fixed-new-video-uploads-defaulting-to-private.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-09-long-recordings-now-get-enough-time-to-download-their-media-.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-09-meeting-notes-no-longer-lowers-your-microphone-volume-for-ot.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-07-09-on-mobile-tapping-a-playing-clip-reveals-playback-controls-w.md +6 -0
  56. package/corpus/templates/clips/changelog/2026-07-09-regenerate-any-clip-transcript-from-its-saved-recording-or-r.md +6 -0
  57. package/corpus/templates/clips/changelog/2026-07-09-slack-previews-now-show-the-clip-length-alongside-its-descri.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-07-10-fixed-copying-agent-responses-and-transcript-text-from-recor.md +6 -0
  59. package/corpus/templates/clips/desktop/src/app.tsx +12 -2
  60. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +8 -1
  61. package/corpus/templates/clips/desktop/src/lib/recorder.ts +8 -5
  62. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +5 -5
  63. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +3 -0
  64. package/corpus/templates/clips/desktop/src/styles.css +6 -3
  65. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +29 -11
  66. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  67. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +103 -21
  68. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +117 -39
  69. package/corpus/templates/clips/server/lib/slack-unfurls.ts +20 -1
  70. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +10 -4
  71. package/corpus/templates/clips/shared/recording-core.ts +12 -0
  72. package/corpus/templates/content/AGENTS.md +7 -6
  73. package/corpus/templates/content/actions/_builder-cms-read-client.ts +82 -7
  74. package/corpus/templates/content/actions/_database-source-utils.ts +149 -30
  75. package/corpus/templates/content/actions/_document-flush.ts +132 -17
  76. package/corpus/templates/content/actions/_notion-action-utils.ts +15 -0
  77. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +4 -7
  78. package/corpus/templates/content/actions/attach-content-database-source.ts +41 -17
  79. package/corpus/templates/content/actions/change-content-database-source-role.ts +46 -8
  80. package/corpus/templates/content/actions/create-and-link-notion-page.ts +2 -0
  81. package/corpus/templates/content/actions/link-notion-page.ts +2 -0
  82. package/corpus/templates/content/actions/pull-document.ts +5 -7
  83. package/corpus/templates/content/actions/pull-notion-page.ts +2 -0
  84. package/corpus/templates/content/actions/push-notion-page.ts +10 -0
  85. package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +2 -0
  86. package/corpus/templates/content/actions/suggest-source-join-key.ts +8 -1
  87. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +88 -23
  88. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +32 -2
  89. package/corpus/templates/content/app/hooks/use-notion.ts +6 -6
  90. package/corpus/templates/content/app/i18n/zh-TW.ts +1 -0
  91. package/corpus/templates/content/app/i18n-data.ts +18 -0
  92. package/corpus/templates/content/changelog/2026-07-09-builder-source-refreshes-now-keep-mapped-fields-such-as-topi.md +6 -0
  93. package/corpus/templates/content/changelog/2026-07-09-notion-conflict-choices-and-version-restores-preserve-docume.md +6 -0
  94. package/corpus/templates/content/server/lib/notion-sync.ts +80 -64
  95. package/corpus/templates/content/shared/api.ts +1 -0
  96. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +29 -2
  97. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +86 -0
  98. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +57 -0
  99. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +11 -0
  100. package/corpus/templates/design/AGENTS.md +45 -10
  101. package/corpus/templates/design/actions/add-localhost-screens.ts +66 -13
  102. package/corpus/templates/design/actions/apply-component-prop-edit.ts +45 -36
  103. package/corpus/templates/design/actions/apply-motion-edit.ts +25 -31
  104. package/corpus/templates/design/actions/apply-shader-fill.ts +29 -42
  105. package/corpus/templates/design/actions/detach-component-instance.ts +337 -0
  106. package/corpus/templates/design/actions/export-design-as-figma-svg.ts +219 -0
  107. package/corpus/templates/design/actions/generate-design.ts +79 -12
  108. package/corpus/templates/design/actions/generate-screens.ts +57 -12
  109. package/corpus/templates/design/actions/get-figma-styles.ts +115 -0
  110. package/corpus/templates/design/actions/go-to-main-component.ts +254 -0
  111. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +4 -3
  112. package/corpus/templates/design/actions/import-design-source.ts +4 -31
  113. package/corpus/templates/design/actions/import-figma-clipboard.ts +134 -0
  114. package/corpus/templates/design/actions/import-figma-frame.ts +95 -0
  115. package/corpus/templates/design/actions/index-design-system-with-builder.ts +16 -1
  116. package/corpus/templates/design/actions/list-design-components.ts +104 -0
  117. package/corpus/templates/design/actions/list-figma-library-assets.ts +1 -24
  118. package/corpus/templates/design/actions/open-component-source.ts +2 -4
  119. package/corpus/templates/design/actions/present-design-variants.ts +412 -16
  120. package/corpus/templates/design/actions/preview-source-edit.ts +14 -1
  121. package/corpus/templates/design/actions/run-design-audit.ts +25 -3
  122. package/corpus/templates/design/actions/show-design-questions.ts +10 -0
  123. package/corpus/templates/design/actions/swap-component-instance.ts +544 -0
  124. package/corpus/templates/design/actions/take-design-screenshot.ts +45 -60
  125. package/corpus/templates/design/actions/write-local-file.ts +25 -1
  126. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +152 -0
  127. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1077 -58
  128. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +5 -5
  129. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +73 -21
  130. package/corpus/templates/design/app/components/design/EditPanel.tsx +16 -11
  131. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +364 -0
  132. package/corpus/templates/design/app/components/design/LayersPanel.tsx +180 -53
  133. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +0 -2
  134. package/corpus/templates/design/app/components/design/MotionDock.tsx +144 -21
  135. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1152 -167
  136. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +27 -5
  137. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +23 -2
  138. package/corpus/templates/design/app/components/design/TokensPanel.tsx +63 -49
  139. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1293 -54
  140. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +7 -0
  141. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +6 -1
  142. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +23 -6
  143. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +7 -0
  144. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -0
  145. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +14 -2
  146. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +37 -2
  147. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open-guard.ts +25 -0
  148. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +19 -1
  149. package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +43 -5
  150. package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +27 -2
  151. package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +10 -1
  152. package/corpus/templates/design/app/components/design/code-workbench/store.tsx +19 -2
  153. package/corpus/templates/design/app/components/design/design-canvas/annotation-snapshot.ts +265 -0
  154. package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +16 -5
  155. package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +16 -2
  156. package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +7 -2
  157. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +3 -1
  158. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +78 -8
  159. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +451 -73
  160. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +12 -4
  161. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +398 -272
  162. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +41 -7
  163. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +16 -3
  164. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +24 -4
  165. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +222 -4
  166. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +171 -78
  167. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +45 -5
  168. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +299 -28
  169. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +120 -150
  170. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +53 -1
  171. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +284 -117
  172. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +81 -0
  173. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +19 -14
  174. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +7 -0
  175. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +155 -0
  176. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +268 -46
  177. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +350 -31
  178. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +13 -4
  179. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +233 -23
  180. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +72 -2
  181. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +51 -11
  182. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +84 -11
  183. package/corpus/templates/design/app/components/design/inspector/index.ts +3 -0
  184. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +552 -0
  185. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +157 -0
  186. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +53 -0
  187. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +184 -0
  188. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +403 -26
  189. package/corpus/templates/design/app/components/design/multi-screen/types.ts +20 -1
  190. package/corpus/templates/design/app/components/design/types.ts +33 -0
  191. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +59 -7
  192. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +119 -17
  193. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +275 -63
  194. package/corpus/templates/design/app/hooks/use-question-flow.ts +14 -3
  195. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +67 -10
  196. package/corpus/templates/design/app/i18n/zh-TW.ts +91 -1
  197. package/corpus/templates/design/app/i18n-data.ts +1217 -10
  198. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +577 -0
  199. package/corpus/templates/design/app/lib/agent-chat.ts +23 -0
  200. package/corpus/templates/design/app/lib/design-import.ts +24 -0
  201. package/corpus/templates/design/app/lib/figma-clipboard.ts +157 -0
  202. package/corpus/templates/design/app/lib/figma-svg-copy.ts +221 -0
  203. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +143 -30
  204. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +15 -0
  205. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +33 -0
  206. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +38 -0
  207. package/corpus/templates/design/app/pages/design-editor/overview-annotation-context.ts +157 -0
  208. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +62 -6
  209. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +245 -0
  210. package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +586 -0
  211. package/corpus/templates/design/app/pages/design-editor/screen-auto-layout.ts +169 -0
  212. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +201 -1
  213. package/corpus/templates/design/changelog/2026-07-09-added-underline-strikethrough-and-text-case-uppercase-lowerc.md +6 -0
  214. package/corpus/templates/design/changelog/2026-07-09-annotate-now-draws-across-the-all-screens-canvas-without-swi.md +6 -0
  215. package/corpus/templates/design/changelog/2026-07-09-auto-layout-children-can-now-be-dragged-into-freeform-screen.md +6 -0
  216. package/corpus/templates/design/changelog/2026-07-09-board-shapes-now-stay-visible-and-nest-correctly-across-the-.md +6 -0
  217. package/corpus/templates/design/changelog/2026-07-09-component-source-navigation-now-opens-reliably-from-the-insp.md +6 -0
  218. package/corpus/templates/design/changelog/2026-07-09-cross-screen-local-react-layer-moves-now-hand-off-exact-sour.md +6 -0
  219. package/corpus/templates/design/changelog/2026-07-09-cross-screen-moves-undo-and-redo-now-keep-both-screens-stabl.md +6 -0
  220. package/corpus/templates/design/changelog/2026-07-09-detach-a-component-instance-into-plain-editable-eleme.md +6 -0
  221. package/corpus/templates/design/changelog/2026-07-09-dragging-layers-into-auto-layout-now-respects-ignore-auto-la.md +6 -0
  222. package/corpus/templates/design/changelog/2026-07-09-drawing-annotations-sent-to-the-agent-now-include-a-rendered.md +6 -0
  223. package/corpus/templates/design/changelog/2026-07-09-editor-sidebars-now-use-figma-matched-default-widths-for-a-r.md +6 -0
  224. package/corpus/templates/design/changelog/2026-07-09-empty-frames-and-rectangles-now-expose-full-auto-layout-cont.md +6 -0
  225. package/corpus/templates/design/changelog/2026-07-09-export-any-design-screen-as-a-real-vector-svg-for-figma-with.md +6 -0
  226. package/corpus/templates/design/changelog/2026-07-09-grid-auto-layout-now-uses-true-responsive-rows-and-columns-w.md +6 -0
  227. package/corpus/templates/design/changelog/2026-07-09-hidden-and-locked-screens-now-behave-consistently-between-la.md +6 -0
  228. package/corpus/templates/design/changelog/2026-07-09-image-fills-now-preserve-valid-urls-and-layered-fit-settings.md +6 -0
  229. package/corpus/templates/design/changelog/2026-07-09-import-figma-frames-as-pixel-accurate-design-screens-via-a-s.md +6 -0
  230. package/corpus/templates/design/changelog/2026-07-09-inspect-code-now-hides-internal-styling-metadata-and-wraps-l.md +6 -0
  231. package/corpus/templates/design/changelog/2026-07-09-inspector-blur-and-shadow-effects-now-preserve-other-effects.md +6 -0
  232. package/corpus/templates/design/changelog/2026-07-09-jump-to-a-components-other-instances-from-the-inspecto.md +6 -0
  233. package/corpus/templates/design/changelog/2026-07-09-keyboard-shortcuts-are-now-discoverable-in-a-figma-style-bot.md +6 -0
  234. package/corpus/templates/design/changelog/2026-07-09-layer-selection-now-keeps-cmd-or-ctrl-toggles-and-shift-rang.md +6 -0
  235. package/corpus/templates/design/changelog/2026-07-09-layers-dropped-into-auto-layout-now-participate-correctly-in.md +6 -0
  236. package/corpus/templates/design/changelog/2026-07-09-local-live-edit-screens-now-enter-and-leave-full-view-withou.md +6 -0
  237. package/corpus/templates/design/changelog/2026-07-09-local-preview-urls-now-live-in-the-inspector-instead-of-taki.md +6 -0
  238. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-can-now-be-hidden-and-locked-from-layers-.md +6 -0
  239. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-now-keep-exact-source-provenance-and-supp.md +6 -0
  240. package/corpus/templates/design/changelog/2026-07-09-local-react-visual-editing-now-loads-reliably-and-shows-live.md +6 -0
  241. package/corpus/templates/design/changelog/2026-07-09-paste-from-figma-cmd-v-now-imports-exact-editable-nodes-when.md +6 -0
  242. package/corpus/templates/design/changelog/2026-07-09-pressing-escape-after-selecting-a-screen-now-reveals-page-pr.md +6 -0
  243. package/corpus/templates/design/changelog/2026-07-09-right-click-overlapping-or-nested-objects-to-choose-the-exac.md +6 -0
  244. package/corpus/templates/design/changelog/2026-07-09-scale-edge-stretch-and-center-constraints-now-preserve-layer.md +6 -0
  245. package/corpus/templates/design/changelog/2026-07-09-screen-rows-can-now-be-reordered-in-layers-with-matching-can.md +6 -0
  246. package/corpus/templates/design/changelog/2026-07-09-screen-selection-stays-responsive-in-designs-with-hundreds-o.md +6 -0
  247. package/corpus/templates/design/changelog/2026-07-09-shift-a-now-enables-auto-layout-on-a-selected-screen-with-fl.md +6 -0
  248. package/corpus/templates/design/changelog/2026-07-09-swap-a-component-instance-for-another-from-the-libra.md +6 -0
  249. package/corpus/templates/design/changelog/2026-07-10-drawing-annotations-now-stay-available-to-retry-when-the.md +6 -0
  250. package/corpus/templates/design/e2e/global-setup.ts +3 -6
  251. package/corpus/templates/design/e2e/helpers.ts +27 -6
  252. package/corpus/templates/design/server/lib/design-to-figma-svg.ts +1412 -0
  253. package/corpus/templates/design/server/lib/figma-clipboard-match.ts +196 -0
  254. package/corpus/templates/design/server/lib/figma-node-import.ts +311 -0
  255. package/corpus/templates/design/server/lib/figma-node-to-html.ts +1334 -0
  256. package/corpus/templates/design/server/lib/figma-paste-fallback.ts +64 -0
  257. package/corpus/templates/design/server/lib/playwright-runtime.ts +78 -0
  258. package/corpus/templates/design/server/plugins/db.ts +41 -0
  259. package/corpus/templates/design/server/source-workspace.ts +64 -0
  260. package/corpus/templates/design/shared/board-file.ts +13 -2
  261. package/corpus/templates/design/shared/canvas-math.ts +231 -33
  262. package/corpus/templates/design/shared/code-layer.ts +122 -8
  263. package/corpus/templates/design/shared/component-library.ts +161 -0
  264. package/corpus/templates/design/shared/db-conflict.ts +27 -0
  265. package/corpus/templates/design/shared/design-source-capabilities.ts +2 -1
  266. package/corpus/templates/design/shared/figma-url.ts +149 -0
  267. package/corpus/templates/design/shared/pen-path.ts +16 -7
  268. package/corpus/templates/design/shared/source-mode.ts +1 -0
  269. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +7 -19
  270. package/corpus/templates/plan/changelog/2026-07-09-canvas-panning-now-keeps-zoom-fixed-even-at-the-edges.md +6 -0
  271. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +16 -1
  272. package/corpus/templates/slides/app/context/DeckContext.tsx +311 -70
  273. package/corpus/templates/slides/changelog/2026-07-09-fixed-the-slide-rail-going-permanently-stale-during-a-long-a.md +6 -0
  274. package/dist/agent/production-agent.d.ts +82 -7
  275. package/dist/agent/production-agent.d.ts.map +1 -1
  276. package/dist/agent/production-agent.js +180 -17
  277. package/dist/agent/production-agent.js.map +1 -1
  278. package/dist/agent/run-manager.d.ts.map +1 -1
  279. package/dist/agent/run-manager.js +63 -14
  280. package/dist/agent/run-manager.js.map +1 -1
  281. package/dist/agent/run-store.d.ts +46 -0
  282. package/dist/agent/run-store.d.ts.map +1 -1
  283. package/dist/agent/run-store.js +129 -22
  284. package/dist/agent/run-store.js.map +1 -1
  285. package/dist/cli/design-connect.d.ts +6 -0
  286. package/dist/cli/design-connect.d.ts.map +1 -1
  287. package/dist/cli/design-connect.js +336 -139
  288. package/dist/cli/design-connect.js.map +1 -1
  289. package/dist/cli/skills.d.ts.map +1 -1
  290. package/dist/cli/skills.js +14 -0
  291. package/dist/cli/skills.js.map +1 -1
  292. package/dist/client/AssistantChat.d.ts +2 -0
  293. package/dist/client/AssistantChat.d.ts.map +1 -1
  294. package/dist/client/AssistantChat.js +19 -3
  295. package/dist/client/AssistantChat.js.map +1 -1
  296. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  297. package/dist/client/MultiTabAssistantChat.js +38 -18
  298. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  299. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  300. package/dist/client/agent-chat-adapter.js +48 -12
  301. package/dist/client/agent-chat-adapter.js.map +1 -1
  302. package/dist/client/agent-chat.d.ts +60 -0
  303. package/dist/client/agent-chat.d.ts.map +1 -1
  304. package/dist/client/agent-chat.js +122 -1
  305. package/dist/client/agent-chat.js.map +1 -1
  306. package/dist/client/chat/index.d.ts +1 -1
  307. package/dist/client/chat/index.d.ts.map +1 -1
  308. package/dist/client/chat/index.js +1 -1
  309. package/dist/client/chat/index.js.map +1 -1
  310. package/dist/client/extensions/EmbeddedExtension.d.ts +12 -0
  311. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  312. package/dist/client/extensions/EmbeddedExtension.js +36 -2
  313. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  314. package/dist/client/index.d.ts +1 -1
  315. package/dist/client/index.d.ts.map +1 -1
  316. package/dist/client/index.js +1 -1
  317. package/dist/client/index.js.map +1 -1
  318. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  319. package/dist/client/rich-markdown-editor/useCollabReconcile.js +18 -6
  320. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  321. package/dist/collab/awareness-store.d.ts +5 -0
  322. package/dist/collab/awareness-store.d.ts.map +1 -1
  323. package/dist/collab/awareness-store.js +19 -12
  324. package/dist/collab/awareness-store.js.map +1 -1
  325. package/dist/collab/awareness.d.ts +2 -2
  326. package/dist/collab/awareness.d.ts.map +1 -1
  327. package/dist/collab/client.d.ts.map +1 -1
  328. package/dist/collab/client.js +13 -5
  329. package/dist/collab/client.js.map +1 -1
  330. package/dist/collab/index.d.ts +1 -0
  331. package/dist/collab/index.d.ts.map +1 -1
  332. package/dist/collab/index.js +1 -0
  333. package/dist/collab/index.js.map +1 -1
  334. package/dist/collab/recent-edits.d.ts +3 -1
  335. package/dist/collab/recent-edits.d.ts.map +1 -1
  336. package/dist/collab/recent-edits.js +57 -2
  337. package/dist/collab/recent-edits.js.map +1 -1
  338. package/dist/collab/struct-routes.d.ts +1 -1
  339. package/dist/index.browser.d.ts +1 -1
  340. package/dist/index.browser.d.ts.map +1 -1
  341. package/dist/index.browser.js +1 -1
  342. package/dist/index.browser.js.map +1 -1
  343. package/dist/observability/routes.d.ts +5 -5
  344. package/dist/progress/routes.d.ts +1 -1
  345. package/dist/resources/handlers.d.ts +3 -3
  346. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  347. package/dist/server/agent-chat-plugin.js +87 -10
  348. package/dist/server/agent-chat-plugin.js.map +1 -1
  349. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  350. package/dist/server/builder-design-systems.d.ts +15 -0
  351. package/dist/server/builder-design-systems.d.ts.map +1 -1
  352. package/dist/server/builder-design-systems.js +9 -4
  353. package/dist/server/builder-design-systems.js.map +1 -1
  354. package/package.json +1 -1
@@ -15,13 +15,15 @@ export type HorizontalConstraint =
15
15
  | "right"
16
16
  | "left-right"
17
17
  | "center"
18
- | "scale";
18
+ | "scale"
19
+ | "mixed";
19
20
  export type VerticalConstraint =
20
21
  | "top"
21
22
  | "bottom"
22
23
  | "top-bottom"
23
24
  | "center"
24
- | "scale";
25
+ | "scale"
26
+ | "mixed";
25
27
 
26
28
  export interface ConstraintsValue {
27
29
  horizontal: HorizontalConstraint;
@@ -40,6 +42,7 @@ export interface ConstraintsWidgetLabels {
40
42
  topBottom: string;
41
43
  center: string;
42
44
  scale: string;
45
+ mixed: string;
43
46
  }
44
47
 
45
48
  export interface ConstraintsWidgetProps {
@@ -71,6 +74,7 @@ const DEFAULT_LABELS: ConstraintsWidgetLabels = {
71
74
  topBottom: "Top and bottom", // i18n-ignore fallback component label
72
75
  center: "Center", // i18n-ignore fallback component label
73
76
  scale: "Scale", // i18n-ignore fallback component label
77
+ mixed: "Mixed", // i18n-ignore fallback component label
74
78
  };
75
79
 
76
80
  // ── pin-box geometry ────────────────────────────────────────────────────────
@@ -479,6 +483,10 @@ function ConstraintSelect({
479
483
  { value: "center", label: labels.center },
480
484
  { value: "scale", label: labels.scale },
481
485
  ];
486
+ const renderedOptions =
487
+ value === "mixed"
488
+ ? [{ value: "mixed", label: labels.mixed, disabled: true }, ...options]
489
+ : options;
482
490
 
483
491
  return (
484
492
  <Select
@@ -492,14 +500,15 @@ function ConstraintSelect({
492
500
  className="h-8 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[12px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)] focus:ring-offset-0 [&>svg]:size-3"
493
501
  aria-label={axis === "horizontal" ? labels.horizontal : labels.vertical}
494
502
  >
495
- <SelectValue />
503
+ {value === "mixed" ? labels.mixed : <SelectValue />}
496
504
  </SelectTrigger>
497
505
  <SelectContent className="z-[270]">
498
506
  <SelectGroup>
499
- {options.map((option) => (
507
+ {renderedOptions.map((option) => (
500
508
  <SelectItem
501
509
  key={option.value}
502
510
  value={option.value}
511
+ disabled={"disabled" in option && option.disabled}
503
512
  className="text-[12px]"
504
513
  >
505
514
  {option.label}
@@ -691,22 +691,17 @@ export function DesignColorPicker({
691
691
  blendMode,
692
692
  onBlendModeChange,
693
693
  showBlendMode = false,
694
- fillRows: _fillRows,
695
- selectedFillId: _selectedFillId,
696
- onFillSelect: _onFillSelect,
697
- onFillChange: _onFillChange,
698
- onAddFill: _onAddFill,
699
- onRemoveFill: _onRemoveFill,
694
+ // Note: fillRows/selectedFillId/onFillSelect/onFillChange/onAddFill/
695
+ // onRemoveFill and gradientStops/selectedStopId/onGradientStopSelect/
696
+ // onGradientStopChange/onAddGradientStop/onRemoveGradientStop are
697
+ // deliberately NOT destructured here — see the "These interfaces remain…"
698
+ // comment on DesignColorPickerProps above. The popover doesn't render a
699
+ // fills/gradient-stops list (fill-properties.tsx in edit-panel owns that
700
+ // UI directly), so binding them to local variables here was dead code.
700
701
  paintType,
701
702
  onPaintTypeChange,
702
703
  gradientType,
703
704
  onGradientTypeChange,
704
- gradientStops: _gradientStops,
705
- selectedStopId: _selectedStopId,
706
- onGradientStopSelect: _onGradientStopSelect,
707
- onGradientStopChange: _onGradientStopChange,
708
- onAddGradientStop: _onAddGradientStop,
709
- onRemoveGradientStop: _onRemoveGradientStop,
710
705
  documentColors,
711
706
  supportedPaintTypes,
712
707
  shaderContext,
@@ -752,6 +747,18 @@ export function DesignColorPicker({
752
747
  const [hexDraft, setHexDraft] = useState(() => toDisplayHex(color));
753
748
  const hexDraftRef = useRef(hexDraft);
754
749
  const [open, setOpen] = useState(false);
750
+ // Snapshot of value/opacity/paintType captured the instant the popover
751
+ // opens, so Escape can cancel the whole editing session — matching Figma:
752
+ // dragging hue/sat/alpha/gradient stops live-previews the color, but
753
+ // Escaping out reverts everything back to how it was before the popover
754
+ // opened, not just whatever field happens to be focused. Re-snapshotted
755
+ // only on the open transition (see the effect below), never while already
756
+ // open, so it doesn't chase the user's own edits.
757
+ const openSnapshotRef = useRef({
758
+ value,
759
+ opacity: effectiveOpacity,
760
+ paintType,
761
+ });
755
762
  const [picking, setPicking] = useState(false);
756
763
  const skipNextHexBlurCommitRef = useRef(false);
757
764
  // Preserve the last non-zero hue so dragging through an achromatic point
@@ -835,6 +842,16 @@ export function DesignColorPicker({
835
842
  setHexDraft(nextHex);
836
843
  }, [color.r, color.g, color.b]);
837
844
 
845
+ useEffect(() => {
846
+ if (open) {
847
+ openSnapshotRef.current = { value, opacity: effectiveOpacity, paintType };
848
+ }
849
+ // Deliberately only depends on `open`: this must capture the value as of
850
+ // the open transition, not re-run on every edit made while already open
851
+ // (that would defeat the point of an Escape-to-cancel snapshot).
852
+ // eslint-disable-next-line react-hooks/exhaustive-deps
853
+ }, [open]);
854
+
838
855
  // The local override (the user's explicit paint-type click) persists for the
839
856
  // life of the open popover so EditPanel bouncing `paintType` back to solid
840
857
  // can't wipe a just-selected gradient/image/shader. A new element selection
@@ -1015,15 +1032,31 @@ export function DesignColorPicker({
1015
1032
  onImageFillChange(next);
1016
1033
  return;
1017
1034
  }
1035
+ // ImageFillControls has no drag surface of its own (URL entry, file
1036
+ // pick, and fit selection are each already a discrete, complete action),
1037
+ // so — unlike the SV field / hue / alpha tracks — every onChange call
1038
+ // here is itself a final commit, never a live-preview tick.
1018
1039
  emitPaintValue(imageFillToCss(next));
1040
+ notifyChangeComplete();
1019
1041
  };
1020
1042
 
1021
1043
  // ── Shader editing ──────────────────────────────────────────────────────────────
1022
1044
 
1023
- const emitShader = (descriptor: ShaderDescriptor, css: string) => {
1045
+ // Cheap live preview fires on every ShaderFillsPanel tuning tick. Must
1046
+ // not call notifyChangeComplete (that's the expensive-commit signal), or
1047
+ // every drag tick would dirty undo history the same way an un-split
1048
+ // onChange/onChangeComplete would for color/gradient dragging.
1049
+ const previewShader = (descriptor: ShaderDescriptor, css: string) => {
1050
+ setShaderDescriptor(descriptor);
1051
+ emitPaintValue(css);
1052
+ };
1053
+
1054
+ // Fires once per gesture/discrete pick (see ShaderFillsPanel's onCommit).
1055
+ const commitShader = (descriptor: ShaderDescriptor, css: string) => {
1024
1056
  setShaderDescriptor(descriptor);
1025
1057
  onShaderChange?.(descriptor, css);
1026
1058
  emitPaintValue(css);
1059
+ notifyChangeComplete();
1027
1060
  };
1028
1061
 
1029
1062
  // ── Paint-type switching (does real work for every type) ──────────────────────
@@ -1134,6 +1167,29 @@ export function DesignColorPicker({
1134
1167
 
1135
1168
  const hasEyeDropper = hasEyeDropperSupport();
1136
1169
 
1170
+ // Cancels the whole editing session back to the snapshot captured when the
1171
+ // popover opened — the Escape-key contract (matches Figma: any live-preview
1172
+ // dragging done while the popover was open gets thrown away, not just
1173
+ // whatever field currently has focus). Resets every local override so the
1174
+ // effective paint type / gradient / shader recompute cleanly from the
1175
+ // reverted props on the next render.
1176
+ const revertToOpenSnapshot = () => {
1177
+ if (disabled) return;
1178
+ const snapshot = openSnapshotRef.current;
1179
+ setLocalPaintType(null);
1180
+ setLocalGradient(null);
1181
+ setSelectedStopId("");
1182
+ setShaderDescriptor(null);
1183
+ setView("picker");
1184
+ if (onPaintTypeChange && snapshot.paintType !== undefined) {
1185
+ onPaintTypeChange(snapshot.paintType);
1186
+ }
1187
+ if (onOpacityChange) onOpacityChange(snapshot.opacity);
1188
+ lastEmittedValueRef.current = snapshot.value;
1189
+ onChange(snapshot.value);
1190
+ onChangeComplete?.(snapshot.value);
1191
+ };
1192
+
1137
1193
  // ── Value row inputs by mode ─────────────────────────────────────────────────
1138
1194
 
1139
1195
  function renderValueInputs() {
@@ -1185,6 +1241,7 @@ export function DesignColorPicker({
1185
1241
  max={255}
1186
1242
  disabled={disabled}
1187
1243
  onChange={(next) => emitFieldColor({ ...fieldColor, [ch]: next })}
1244
+ onCommit={notifyChangeComplete}
1188
1245
  />
1189
1246
  ))}
1190
1247
  </div>
@@ -1200,6 +1257,7 @@ export function DesignColorPicker({
1200
1257
  max={360}
1201
1258
  disabled={disabled}
1202
1259
  onChange={(h) => emitFieldHsl({ ...fieldHsl, h })}
1260
+ onCommit={notifyChangeComplete}
1203
1261
  />
1204
1262
  <ScrubbyNumberInput
1205
1263
  aria-label={copy.saturation}
@@ -1208,6 +1266,7 @@ export function DesignColorPicker({
1208
1266
  max={100}
1209
1267
  disabled={disabled}
1210
1268
  onChange={(s) => emitFieldHsl({ ...fieldHsl, s })}
1269
+ onCommit={notifyChangeComplete}
1211
1270
  />
1212
1271
  <ScrubbyNumberInput
1213
1272
  aria-label={copy.lightness}
@@ -1216,6 +1275,7 @@ export function DesignColorPicker({
1216
1275
  max={100}
1217
1276
  disabled={disabled}
1218
1277
  onChange={(l) => emitFieldHsl({ ...fieldHsl, l })}
1278
+ onCommit={notifyChangeComplete}
1219
1279
  />
1220
1280
  </div>
1221
1281
  );
@@ -1230,6 +1290,7 @@ export function DesignColorPicker({
1230
1290
  max={360}
1231
1291
  disabled={disabled}
1232
1292
  onChange={(h) => emitFieldHsv({ ...fieldHsv, h })}
1293
+ onCommit={notifyChangeComplete}
1233
1294
  />
1234
1295
  <ScrubbyNumberInput
1235
1296
  aria-label={copy.saturation}
@@ -1238,6 +1299,7 @@ export function DesignColorPicker({
1238
1299
  max={100}
1239
1300
  disabled={disabled}
1240
1301
  onChange={(s) => emitFieldHsv({ ...fieldHsv, s })}
1302
+ onCommit={notifyChangeComplete}
1241
1303
  />
1242
1304
  <ScrubbyNumberInput
1243
1305
  aria-label={copy.brightness}
@@ -1246,6 +1308,7 @@ export function DesignColorPicker({
1246
1308
  max={100}
1247
1309
  disabled={disabled}
1248
1310
  onChange={(v) => emitFieldHsv({ ...fieldHsv, v })}
1311
+ onCommit={notifyChangeComplete}
1249
1312
  />
1250
1313
  </div>
1251
1314
  );
@@ -1298,6 +1361,11 @@ export function DesignColorPicker({
1298
1361
  // re-projection can't close the popover. Genuine pointer clicks
1299
1362
  // outside still close it via the default onInteractOutside behavior.
1300
1363
  onFocusOutside={(e) => e.preventDefault()}
1364
+ // Escape cancels the whole editing session (see revertToOpenSnapshot)
1365
+ // and then still closes the popover via Radix's default dismiss
1366
+ // behavior — this only reverts the color/opacity/paint-type state,
1367
+ // it doesn't call `e.preventDefault()`, so the close still happens.
1368
+ onEscapeKeyDown={revertToOpenSnapshot}
1301
1369
  >
1302
1370
  <div className="rounded-md bg-popover text-popover-foreground">
1303
1371
  {view === "shader" && glslShaderContext ? (
@@ -1320,7 +1388,8 @@ export function DesignColorPicker({
1320
1388
  descriptor={shaderDescriptor ?? undefined}
1321
1389
  applyContext={shaderContext}
1322
1390
  disabled={disabled}
1323
- onApply={emitShader}
1391
+ onApply={previewShader}
1392
+ onCommit={commitShader}
1324
1393
  onBack={() => {
1325
1394
  setView("picker");
1326
1395
  if (effectivePaintType === "shader") {
@@ -1452,19 +1521,23 @@ export function DesignColorPicker({
1452
1521
  if (e.key === "Enter") {
1453
1522
  e.preventDefault();
1454
1523
  const url = e.currentTarget.value.trim();
1455
- if (url)
1524
+ if (url) {
1456
1525
  emitPaintValue(
1457
1526
  `url("${url}") center / cover no-repeat`,
1458
1527
  );
1528
+ notifyChangeComplete();
1529
+ }
1459
1530
  e.currentTarget.blur();
1460
1531
  }
1461
1532
  }}
1462
1533
  onBlur={(e) => {
1463
1534
  const url = e.currentTarget.value.trim();
1464
- if (url)
1535
+ if (url) {
1465
1536
  emitPaintValue(
1466
1537
  `url("${url}") center / cover no-repeat`,
1467
1538
  );
1539
+ notifyChangeComplete();
1540
+ }
1468
1541
  }}
1469
1542
  />
1470
1543
  </div>
@@ -1479,6 +1552,7 @@ export function DesignColorPicker({
1479
1552
  disabled={disabled}
1480
1553
  onSelectStop={setSelectedStopId}
1481
1554
  onChange={emitGradient}
1555
+ onCommit={notifyChangeComplete}
1482
1556
  />
1483
1557
  </div>
1484
1558
  )}
@@ -1660,6 +1734,7 @@ export function DesignColorPicker({
1660
1734
  setOpacity(next);
1661
1735
  }
1662
1736
  }}
1737
+ onCommit={notifyChangeComplete}
1663
1738
  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"
1664
1739
  compact
1665
1740
  />
@@ -1959,7 +2034,7 @@ function SaturationBrightnessField({
1959
2034
  }}
1960
2035
  onKeyDown={stepWithKeyboard}
1961
2036
  className={cn(
1962
- "relative h-48 w-full cursor-crosshair overflow-hidden outline-none",
2037
+ "relative h-48 w-full touch-none cursor-crosshair overflow-hidden outline-none",
1963
2038
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
1964
2039
  "active:cursor-grabbing",
1965
2040
  disabled && "cursor-not-allowed opacity-60",
@@ -2080,7 +2155,7 @@ function ColorTrack({
2080
2155
  if (ended.shouldCommit) onCommit?.();
2081
2156
  }}
2082
2157
  className={cn(
2083
- "relative h-3.5 cursor-pointer rounded-full border border-border/60 outline-none",
2158
+ "relative h-3.5 touch-none cursor-pointer rounded-full border border-border/60 outline-none",
2084
2159
  "ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
2085
2160
  "active:cursor-grabbing",
2086
2161
  disabled && "cursor-not-allowed opacity-60",
@@ -2097,7 +2172,14 @@ function ColorTrack({
2097
2172
  }
2098
2173
 
2099
2174
  /**
2100
- * A number input with select-on-focus and Esc-to-revert.
2175
+ * A number input with select-on-focus, Esc-to-revert, arrow-key nudging, and
2176
+ * Figma-style click-drag "scrubbing": pressing down and dragging left/right
2177
+ * decrements/increments the value continuously (Shift = 10x coarser step,
2178
+ * matching the same shift convention as arrow-key nudges and the SV/hue/alpha
2179
+ * tracks) without needing to type. A short press-release with no meaningful
2180
+ * movement is treated as an ordinary click so focus + select-on-focus (and
2181
+ * therefore typing) keep working exactly as before.
2182
+ *
2101
2183
  * The `compact` prop removes inner padding for use inside bordered wrappers.
2102
2184
  */
2103
2185
  function ScrubbyNumberInput({
@@ -2107,6 +2189,7 @@ function ScrubbyNumberInput({
2107
2189
  max,
2108
2190
  disabled,
2109
2191
  onChange,
2192
+ onCommit,
2110
2193
  className,
2111
2194
  compact = false,
2112
2195
  }: {
@@ -2116,12 +2199,22 @@ function ScrubbyNumberInput({
2116
2199
  max: number;
2117
2200
  disabled: boolean;
2118
2201
  onChange: (value: number) => void;
2202
+ /**
2203
+ * Fires once per discrete, complete edit: a blur/Enter commit (only when
2204
+ * the draft actually parsed — not on a revert), each arrow-key step, and
2205
+ * once at the end of a scrub drag (not per pointermove tick). Mirrors the
2206
+ * `onCommit` contract used by SaturationBrightnessField/ColorTrack so every
2207
+ * draggable/steppable control in this picker notifies
2208
+ * `onChangeComplete` exactly once per gesture.
2209
+ */
2210
+ onCommit?: () => void;
2119
2211
  className?: string;
2120
2212
  compact?: boolean;
2121
2213
  }) {
2122
2214
  const [draft, setDraft] = useState<string>(() => String(value));
2123
2215
  const draftRef = useRef(draft);
2124
2216
  const skipBlurRef = useRef(false);
2217
+ const scrubRef = useRef<ScrubGestureState>(SCRUB_GESTURE_IDLE);
2125
2218
 
2126
2219
  useEffect(() => {
2127
2220
  const nextDraft = String(value);
@@ -2138,6 +2231,7 @@ function ScrubbyNumberInput({
2138
2231
  return;
2139
2232
  }
2140
2233
  onChange(clamp(parsed, min, max));
2234
+ onCommit?.();
2141
2235
  };
2142
2236
 
2143
2237
  return (
@@ -2149,8 +2243,15 @@ function ScrubbyNumberInput({
2149
2243
  max={max}
2150
2244
  disabled={disabled}
2151
2245
  className={cn(
2152
- "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",
2246
+ "h-6 w-full touch-none rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-center !text-[11px] tabular-nums",
2153
2247
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
2248
+ // Figma's numeric fields never show the native up/down spinner —
2249
+ // hide it in both engines so this reads as a plain scrubbable value,
2250
+ // not a browser default number input. (The spinner buttons also
2251
+ // bypassed this component's onChange/onCommit contract entirely,
2252
+ // since native stepper clicks only mutate the draft string, not the
2253
+ // color — hiding them removes that dead, contract-violating path.)
2254
+ "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
2154
2255
  compact && "border-0 shadow-none focus-visible:ring-0",
2155
2256
  className,
2156
2257
  )}
@@ -2179,6 +2280,7 @@ function ScrubbyNumberInput({
2179
2280
  const parsed = Number(draftRef.current);
2180
2281
  const base = Number.isFinite(parsed) ? parsed : value;
2181
2282
  onChange(clamp(base + step, min, max));
2283
+ onCommit?.();
2182
2284
  }
2183
2285
  if (e.key === "ArrowDown") {
2184
2286
  e.preventDefault();
@@ -2186,6 +2288,7 @@ function ScrubbyNumberInput({
2186
2288
  const parsed = Number(draftRef.current);
2187
2289
  const base = Number.isFinite(parsed) ? parsed : value;
2188
2290
  onChange(clamp(base - step, min, max));
2291
+ onCommit?.();
2189
2292
  }
2190
2293
  }}
2191
2294
  onBlur={() => {
@@ -2195,6 +2298,58 @@ function ScrubbyNumberInput({
2195
2298
  }
2196
2299
  commit();
2197
2300
  }}
2301
+ onPointerDown={(e) => {
2302
+ if (disabled) return;
2303
+ // Don't preventDefault here — a plain click (no subsequent movement
2304
+ // past the threshold) must still focus the input normally so typing
2305
+ // keeps working. Pointer capture just ensures a real drag keeps
2306
+ // routing to this element even once the cursor leaves its bounds.
2307
+ scrubRef.current = startScrubGesture(e.clientX, value);
2308
+ e.currentTarget.setPointerCapture(e.pointerId);
2309
+ }}
2310
+ onPointerMove={(e) => {
2311
+ if (disabled || !scrubRef.current.active) return;
2312
+ const deltaX = e.clientX - scrubRef.current.startX;
2313
+ if (!scrubRef.current.dragging) {
2314
+ if (Math.abs(deltaX) < SCRUB_DRAG_THRESHOLD_PX) return;
2315
+ scrubRef.current = { ...scrubRef.current, dragging: true };
2316
+ // Now that this is a genuine drag (not a click), stop the browser
2317
+ // from turning the pointer move into a text-selection drag.
2318
+ window.getSelection?.()?.removeAllRanges();
2319
+ }
2320
+ e.preventDefault();
2321
+ onChange(
2322
+ computeScrubbedValue(
2323
+ scrubRef.current.startValue,
2324
+ deltaX,
2325
+ min,
2326
+ max,
2327
+ e.shiftKey,
2328
+ ),
2329
+ );
2330
+ }}
2331
+ onPointerUp={(e) => {
2332
+ const wasDragging = scrubRef.current.dragging;
2333
+ scrubRef.current = SCRUB_GESTURE_IDLE;
2334
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
2335
+ e.currentTarget.releasePointerCapture(e.pointerId);
2336
+ }
2337
+ if (wasDragging) {
2338
+ onCommit?.();
2339
+ // A drag ended without focusing the field for text entry — match
2340
+ // Figma (dragging a number field doesn't leave it in edit mode).
2341
+ skipBlurRef.current = true;
2342
+ e.currentTarget.blur();
2343
+ }
2344
+ }}
2345
+ onPointerCancel={(e) => {
2346
+ const wasDragging = scrubRef.current.dragging;
2347
+ scrubRef.current = SCRUB_GESTURE_IDLE;
2348
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
2349
+ e.currentTarget.releasePointerCapture(e.pointerId);
2350
+ }
2351
+ if (wasDragging) onCommit?.();
2352
+ }}
2198
2353
  />
2199
2354
  );
2200
2355
  }
@@ -2230,6 +2385,61 @@ export function endPointerGesture(state: PointerGestureState): {
2230
2385
  return { state: POINTER_GESTURE_IDLE, shouldCommit: state };
2231
2386
  }
2232
2387
 
2388
+ // ─── Click-drag "scrub" tracking for ScrubbyNumberInput ────────────────────────
2389
+ //
2390
+ // A plain pointerdown+pointerup with no meaningful movement must behave like
2391
+ // an ordinary click (focus the input, select-on-focus, allow typing) — it
2392
+ // only becomes a scrub once the pointer has moved past a small pixel
2393
+ // threshold, matching Figma's numeric fields (click to type, click-drag to
2394
+ // scrub). `dragging` flips true exactly once per gesture, the first time the
2395
+ // threshold is crossed.
2396
+
2397
+ const SCRUB_DRAG_THRESHOLD_PX = 3;
2398
+ /** Pixels of drag per 1 unit of value change at the normal (non-Shift) rate. */
2399
+ const SCRUB_PIXELS_PER_STEP = 4;
2400
+
2401
+ export interface ScrubGestureState {
2402
+ /** True from pointerdown until the matching pointerup/pointercancel. */
2403
+ active: boolean;
2404
+ /** True once the drag has crossed the click-vs-drag threshold. */
2405
+ dragging: boolean;
2406
+ startX: number;
2407
+ startValue: number;
2408
+ }
2409
+
2410
+ export const SCRUB_GESTURE_IDLE: ScrubGestureState = {
2411
+ active: false,
2412
+ dragging: false,
2413
+ startX: 0,
2414
+ startValue: 0,
2415
+ };
2416
+
2417
+ /** Call on pointerdown: starts tracking a new potential scrub gesture. */
2418
+ export function startScrubGesture(
2419
+ startX: number,
2420
+ startValue: number,
2421
+ ): ScrubGestureState {
2422
+ return { active: true, dragging: false, startX, startValue };
2423
+ }
2424
+
2425
+ /**
2426
+ * Pure math for one scrub tick: converts total horizontal drag distance
2427
+ * (from the gesture's start position) into a new clamped value. Shift scales
2428
+ * the rate 10x coarser, matching the same shift convention as arrow-key
2429
+ * nudges and the SV/hue/alpha track keyboard steps.
2430
+ */
2431
+ export function computeScrubbedValue(
2432
+ startValue: number,
2433
+ deltaX: number,
2434
+ min: number,
2435
+ max: number,
2436
+ shiftKey: boolean,
2437
+ ): number {
2438
+ const rate = shiftKey ? 10 : 1;
2439
+ const delta = Math.round(deltaX / SCRUB_PIXELS_PER_STEP) * rate;
2440
+ return clamp(startValue + delta, min, max);
2441
+ }
2442
+
2233
2443
  // ─── Utilities ─────────────────────────────────────────────────────────────────
2234
2444
 
2235
2445
  export function inferPaintType(
@@ -2388,7 +2598,7 @@ function alphaTrackBackground(color: RgbaColor): string {
2388
2598
  return `${CHECKERBOARD_IMAGE}, linear-gradient(90deg, rgba(${color.r}, ${color.g}, ${color.b}, 0), rgba(${color.r}, ${color.g}, ${color.b}, 1))`;
2389
2599
  }
2390
2600
 
2391
- function rgbaToHsv(color: RgbaColor): HsvaColor {
2601
+ export function rgbaToHsv(color: RgbaColor): HsvaColor {
2392
2602
  const r = clampFloat(color.r / 255, 0, 1);
2393
2603
  const g = clampFloat(color.g / 255, 0, 1);
2394
2604
  const b = clampFloat(color.b / 255, 0, 1);
@@ -2413,7 +2623,7 @@ function rgbaToHsv(color: RgbaColor): HsvaColor {
2413
2623
  };
2414
2624
  }
2415
2625
 
2416
- function hsvToRgba(color: HsvaColor): RgbaColor {
2626
+ export function hsvToRgba(color: HsvaColor): RgbaColor {
2417
2627
  const h = ((color.h % 360) + 360) % 360;
2418
2628
  const s = clampFloat(color.s, 0, 100) / 100;
2419
2629
  const v = clampFloat(color.v, 0, 100) / 100;
@@ -2449,7 +2659,7 @@ function clampFloat(value: number, min: number, max: number): number {
2449
2659
  return Math.max(min, Math.min(max, value));
2450
2660
  }
2451
2661
 
2452
- function hasHexAlpha(value: string): boolean {
2662
+ export function hasHexAlpha(value: string): boolean {
2453
2663
  return /^#?(?:[0-9a-f]{4}|[0-9a-f]{8})$/i.test(value.trim());
2454
2664
  }
2455
2665