@ctrl-ui/react 0.8.6 → 0.8.8

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 (376) hide show
  1. package/dist/action-bar.d.ts +15 -7
  2. package/dist/action-bar.js +39 -19
  3. package/dist/activity.js +7 -15
  4. package/dist/audio-recorder.d.ts +14 -4
  5. package/dist/audio-recorder.js +82 -29
  6. package/dist/audio-visualizer.js +1 -1
  7. package/dist/blocks/coding-agent.js +54 -57
  8. package/dist/blocks/design-canvas-data.d.ts +32 -0
  9. package/dist/blocks/design-canvas-data.js +100 -0
  10. package/dist/blocks/design-canvas-inspector.d.ts +8 -0
  11. package/dist/blocks/design-canvas-inspector.js +238 -0
  12. package/dist/blocks/design-canvas.d.ts +21 -0
  13. package/dist/blocks/design-canvas.js +452 -0
  14. package/dist/blocks/discussion.d.ts +28 -0
  15. package/dist/blocks/discussion.js +67 -0
  16. package/dist/blocks/file-explorer-data.d.ts +1 -0
  17. package/dist/blocks/file-explorer-data.js +4 -1
  18. package/dist/blocks/file-explorer.d.ts +9 -1
  19. package/dist/blocks/file-explorer.js +461 -192
  20. package/dist/blocks/settings.d.ts +2 -1
  21. package/dist/blocks/settings.js +101 -79
  22. package/dist/blocks/team-chat.d.ts +12 -0
  23. package/dist/blocks/team-chat.js +26 -0
  24. package/dist/blocks/theme-toggle.js +23 -31
  25. package/dist/chat-composer-attachment.d.ts +12 -3
  26. package/dist/chat-composer-attachment.js +61 -19
  27. package/dist/chat-composer-editor/extensions/mention.d.ts +5 -1
  28. package/dist/chat-composer-editor/extensions/mention.js +58 -27
  29. package/dist/chat-composer-editor/ghost.js +2 -4
  30. package/dist/chat-composer-editor/types.d.ts +5 -0
  31. package/dist/chat-composer-editor.d.ts +1 -1
  32. package/dist/chat-composer-editor.js +71 -28
  33. package/dist/chat-composer.d.ts +13 -32
  34. package/dist/chat-composer.js +54 -13
  35. package/dist/chat-layout.d.ts +19 -0
  36. package/dist/chat-layout.js +96 -32
  37. package/dist/chat-message/social.d.ts +26 -0
  38. package/dist/chat-message/social.js +122 -0
  39. package/dist/chat-message.d.ts +28 -29
  40. package/dist/chat-message.js +106 -25
  41. package/dist/context.d.ts +2 -2
  42. package/dist/context.js +38 -23
  43. package/dist/dynamic-notification-canvas-loop.d.ts +24 -0
  44. package/dist/dynamic-notification-canvas-loop.js +113 -0
  45. package/dist/dynamic-notification-glass.js +23 -114
  46. package/dist/dynamic-notification-liquid.js +14 -118
  47. package/dist/dynamic-notification.d.ts +16 -8
  48. package/dist/dynamic-notification.js +85 -22
  49. package/dist/email/email-brand.d.ts +7 -4
  50. package/dist/email/email-brand.js +9 -6
  51. package/dist/email/email-code.d.ts +3 -2
  52. package/dist/email/email-code.js +11 -9
  53. package/dist/email/email-footer.d.ts +5 -3
  54. package/dist/email/email-footer.js +11 -5
  55. package/dist/email/email.d.ts +31 -9
  56. package/dist/email/email.js +50 -26
  57. package/dist/email/templates.d.ts +10 -9
  58. package/dist/email/templates.js +102 -38
  59. package/dist/email/theme.js +1 -1
  60. package/dist/environment-variables.d.ts +4 -2
  61. package/dist/environment-variables.js +52 -32
  62. package/dist/filter-bar/context.d.ts +32 -0
  63. package/dist/filter-bar/context.js +11 -0
  64. package/dist/filter-bar/editor.d.ts +14 -0
  65. package/dist/filter-bar/editor.js +216 -0
  66. package/dist/filter-bar/model.d.ts +10 -0
  67. package/dist/filter-bar/model.js +57 -0
  68. package/dist/filter-bar/motion.d.ts +10 -0
  69. package/dist/filter-bar/motion.js +84 -0
  70. package/dist/filter-bar/parts.d.ts +19 -0
  71. package/dist/filter-bar/parts.js +244 -0
  72. package/dist/filter-bar/types.d.ts +54 -0
  73. package/dist/filter-bar/types.js +0 -0
  74. package/dist/filter-bar.d.ts +27 -0
  75. package/dist/filter-bar.js +310 -0
  76. package/dist/hooks/use-audio-recorder.js +12 -6
  77. package/dist/hooks/use-chat-composer.d.ts +21 -7
  78. package/dist/hooks/use-chat-composer.js +32 -5
  79. package/dist/hooks/use-chat-message.d.ts +28 -2
  80. package/dist/hooks/use-chat-message.js +41 -10
  81. package/dist/hooks/use-chat-thread-scroll.js +43 -7
  82. package/dist/hooks/use-color-area.d.ts +3 -3
  83. package/dist/hooks/use-color-area.js +13 -12
  84. package/dist/hooks/use-copy-to-clipboard.d.ts +2 -1
  85. package/dist/hooks/use-copy-to-clipboard.js +20 -20
  86. package/dist/hooks/use-dropzone.d.ts +7 -4
  87. package/dist/hooks/use-dropzone.js +34 -3
  88. package/dist/hooks/use-dynamic-notification.d.ts +6 -2
  89. package/dist/hooks/use-dynamic-notification.js +26 -10
  90. package/dist/hooks/use-mobile.d.ts +3 -2
  91. package/dist/hooks/use-mobile.js +8 -4
  92. package/dist/hooks/use-textarea-trigger-menu.js +2 -1
  93. package/dist/hooks/use-trigger-menu.d.ts +54 -0
  94. package/dist/hooks/use-trigger-menu.js +102 -47
  95. package/dist/hooks/use-user-ask.d.ts +2 -0
  96. package/dist/hooks/use-user-ask.js +35 -41
  97. package/dist/inline-attachment.d.ts +6 -2
  98. package/dist/inline-attachment.js +44 -29
  99. package/dist/inline-citation.d.ts +12 -5
  100. package/dist/inline-citation.js +25 -16
  101. package/dist/knob-contracts/audio-recorder-knobs.d.ts +1 -1
  102. package/dist/knob-contracts/audio-recorder-knobs.js +2 -1
  103. package/dist/knob-contracts/audio-visualizer-knobs.d.ts +1 -1
  104. package/dist/knob-contracts/audio-visualizer-knobs.js +2 -1
  105. package/dist/knob-contracts/avatar-knobs.d.ts +1 -1
  106. package/dist/knob-contracts/avatar-knobs.js +6 -1
  107. package/dist/knob-contracts/badge-knobs.d.ts +1 -1
  108. package/dist/knob-contracts/badge-knobs.js +2 -1
  109. package/dist/knob-contracts/breadcrumb-knobs.d.ts +4 -0
  110. package/dist/knob-contracts/breadcrumb-knobs.js +8 -0
  111. package/dist/knob-contracts/button-knobs.d.ts +1 -1
  112. package/dist/knob-contracts/button-knobs.js +1 -0
  113. package/dist/knob-contracts/chat-layout-knobs.d.ts +1 -1
  114. package/dist/knob-contracts/chat-layout-knobs.js +2 -1
  115. package/dist/knob-contracts/chat-message-knobs.d.ts +1 -1
  116. package/dist/knob-contracts/chat-message-knobs.js +8 -1
  117. package/dist/knob-contracts/choice-knobs.d.ts +1 -1
  118. package/dist/knob-contracts/choice-knobs.js +1 -0
  119. package/dist/knob-contracts/code-diff-knobs.d.ts +1 -1
  120. package/dist/knob-contracts/code-diff-knobs.js +2 -0
  121. package/dist/knob-contracts/code-knobs.d.ts +1 -1
  122. package/dist/knob-contracts/code-knobs.js +2 -1
  123. package/dist/knob-contracts/collapsible-knobs.d.ts +1 -1
  124. package/dist/knob-contracts/collapsible-knobs.js +1 -1
  125. package/dist/knob-contracts/dynamic-notification-knobs.d.ts +1 -1
  126. package/dist/knob-contracts/dynamic-notification-knobs.js +2 -0
  127. package/dist/knob-contracts/emoji-picker-knobs.d.ts +4 -0
  128. package/dist/knob-contracts/emoji-picker-knobs.js +10 -0
  129. package/dist/knob-contracts/field-knobs.d.ts +1 -1
  130. package/dist/knob-contracts/field-knobs.js +5 -1
  131. package/dist/knob-contracts/filter-bar-knobs.d.ts +4 -0
  132. package/dist/knob-contracts/filter-bar-knobs.js +15 -0
  133. package/dist/knob-contracts/infinite-canvas-knobs.d.ts +1 -1
  134. package/dist/knob-contracts/infinite-canvas-knobs.js +3 -0
  135. package/dist/knob-contracts/markdown-knobs.d.ts +1 -1
  136. package/dist/knob-contracts/markdown-knobs.js +3 -1
  137. package/dist/knob-contracts/page-layout-knobs.d.ts +1 -1
  138. package/dist/knob-contracts/page-layout-knobs.js +4 -1
  139. package/dist/knob-contracts/popup-knobs.d.ts +1 -1
  140. package/dist/knob-contracts/popup-knobs.js +28 -0
  141. package/dist/knob-contracts/range-knobs.d.ts +1 -1
  142. package/dist/knob-contracts/range-knobs.js +1 -0
  143. package/dist/knob-contracts/rich-tooltip-knobs.d.ts +1 -1
  144. package/dist/knob-contracts/rich-tooltip-knobs.js +2 -1
  145. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  146. package/dist/knob-contracts/sidebar-knobs.js +27 -2
  147. package/dist/knob-contracts/spinner-knobs.d.ts +1 -1
  148. package/dist/knob-contracts/spinner-knobs.js +1 -1
  149. package/dist/knob-contracts/stepper-knobs.d.ts +1 -1
  150. package/dist/knob-contracts/stepper-knobs.js +4 -1
  151. package/dist/knob-contracts/switch-knobs.d.ts +1 -1
  152. package/dist/knob-contracts/switch-knobs.js +5 -0
  153. package/dist/knob-contracts/table-of-contents-knobs.d.ts +1 -1
  154. package/dist/knob-contracts/table-of-contents-knobs.js +4 -5
  155. package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
  156. package/dist/knob-contracts/tabs-knobs.js +1 -0
  157. package/dist/knob-contracts/tree-knobs.d.ts +1 -1
  158. package/dist/knob-contracts/tree-knobs.js +2 -1
  159. package/dist/lib/cn.js +0 -4
  160. package/dist/lib/contrast.d.ts +1 -0
  161. package/dist/lib/contrast.js +17 -5
  162. package/dist/lib/dropzone-validation.js +41 -28
  163. package/dist/lib/env-file.d.ts +1 -1
  164. package/dist/lib/env-file.js +5 -5
  165. package/dist/lib/motion.d.ts +4 -0
  166. package/dist/lib/motion.js +15 -0
  167. package/dist/lib/trigger-detect.js +6 -8
  168. package/dist/markdown-block.js +13 -8
  169. package/dist/popup-parts.d.ts +8 -0
  170. package/dist/popup-parts.js +43 -0
  171. package/dist/scripts/contrast-eval.js +5 -4
  172. package/dist/source-badge.js +3 -1
  173. package/dist/styles/code.css +0 -2
  174. package/dist/styles/effects.css +6 -32
  175. package/dist/styles/index.css +14 -8
  176. package/dist/styles/recipes/accordion.css +9 -6
  177. package/dist/styles/recipes/action-bar.css +0 -15
  178. package/dist/styles/recipes/activity.css +5 -11
  179. package/dist/styles/recipes/alert.css +4 -4
  180. package/dist/styles/recipes/audio-recorder.css +15 -3
  181. package/dist/styles/recipes/audio-visualizer.css +13 -2
  182. package/dist/styles/recipes/avatar.css +78 -1
  183. package/dist/styles/recipes/badge.css +14 -4
  184. package/dist/styles/recipes/breadcrumb.css +77 -0
  185. package/dist/styles/recipes/button-group.css +6 -8
  186. package/dist/styles/recipes/button-states.css +66 -0
  187. package/dist/styles/recipes/button.css +26 -85
  188. package/dist/styles/recipes/calendar.css +17 -23
  189. package/dist/styles/recipes/card.css +2 -2
  190. package/dist/styles/recipes/chat-composer-attachment.css +9 -3
  191. package/dist/styles/recipes/chat-composer.css +21 -16
  192. package/dist/styles/recipes/chat-layout.css +64 -2
  193. package/dist/styles/recipes/chat-message-social.css +123 -0
  194. package/dist/styles/recipes/chat-message.css +157 -37
  195. package/dist/styles/recipes/choice.css +25 -21
  196. package/dist/styles/recipes/code-diff.css +51 -26
  197. package/dist/styles/recipes/code.css +44 -18
  198. package/dist/styles/recipes/collapsible.css +43 -1
  199. package/dist/styles/recipes/color-picker-details.css +3 -4
  200. package/dist/styles/recipes/color-picker-values.css +9 -8
  201. package/dist/styles/recipes/color-picker.css +23 -4
  202. package/dist/styles/recipes/context.css +3 -3
  203. package/dist/styles/recipes/dockable-panel.css +18 -7
  204. package/dist/styles/recipes/dropzone.css +7 -8
  205. package/dist/styles/recipes/dynamic-notification-input.css +8 -5
  206. package/dist/styles/recipes/dynamic-notification-motion.css +12 -21
  207. package/dist/styles/recipes/dynamic-notification.css +40 -26
  208. package/dist/styles/recipes/emoji-picker.css +132 -0
  209. package/dist/styles/recipes/empty.css +5 -4
  210. package/dist/styles/recipes/environment-variables.css +4 -15
  211. package/dist/styles/recipes/field-parts.css +89 -21
  212. package/dist/styles/recipes/field.css +83 -26
  213. package/dist/styles/recipes/filter-bar.css +161 -0
  214. package/dist/styles/recipes/gradient-editor.css +18 -7
  215. package/dist/styles/recipes/infinite-canvas.css +51 -4
  216. package/dist/styles/recipes/inline-attachment.css +9 -4
  217. package/dist/styles/recipes/inline-citation.css +10 -4
  218. package/dist/styles/recipes/item.css +23 -9
  219. package/dist/styles/recipes/markdown-block.css +5 -4
  220. package/dist/styles/recipes/markdown.css +68 -26
  221. package/dist/styles/recipes/morphing-panel.css +8 -8
  222. package/dist/styles/recipes/page-layout.css +36 -10
  223. package/dist/styles/recipes/pagination.css +5 -6
  224. package/dist/styles/recipes/phone-input.css +12 -8
  225. package/dist/styles/recipes/popup-controls.css +175 -26
  226. package/dist/styles/recipes/popup-drawer.css +35 -44
  227. package/dist/styles/recipes/popup-lists.css +139 -29
  228. package/dist/styles/recipes/popup-motion.css +4 -45
  229. package/dist/styles/recipes/popup-toast.css +55 -26
  230. package/dist/styles/recipes/popup.css +149 -153
  231. package/dist/styles/recipes/range.css +55 -19
  232. package/dist/styles/recipes/resizable.css +36 -7
  233. package/dist/styles/recipes/rich-tooltip.css +24 -34
  234. package/dist/styles/recipes/scroll-area.css +8 -1
  235. package/dist/styles/recipes/sidebar-menu.css +149 -34
  236. package/dist/styles/recipes/sidebar-parts.css +154 -0
  237. package/dist/styles/recipes/sidebar.css +187 -128
  238. package/dist/styles/recipes/source-badge.css +1 -1
  239. package/dist/styles/recipes/spinner.css +12 -1
  240. package/dist/styles/recipes/stepper.css +31 -13
  241. package/dist/styles/recipes/switch.css +68 -23
  242. package/dist/styles/recipes/table-of-contents.css +49 -41
  243. package/dist/styles/recipes/table.css +14 -10
  244. package/dist/styles/recipes/tabs.css +38 -12
  245. package/dist/styles/recipes/task-list.css +7 -14
  246. package/dist/styles/recipes/thread-rail.css +42 -19
  247. package/dist/styles/recipes/timeline.css +2 -0
  248. package/dist/styles/recipes/toolbar.css +27 -25
  249. package/dist/styles/recipes/track-highlight.css +12 -8
  250. package/dist/styles/recipes/transcript-divider.css +2 -2
  251. package/dist/styles/recipes/tree.css +31 -4
  252. package/dist/styles/recipes/user-ask-details.css +0 -1
  253. package/dist/styles/recipes/user-ask.css +4 -4
  254. package/dist/styles/theme.css +287 -107
  255. package/dist/task-list.d.ts +4 -1
  256. package/dist/task-list.js +24 -8
  257. package/dist/thread-rail.d.ts +1 -1
  258. package/dist/thread-rail.js +61 -19
  259. package/dist/transcript-divider.js +2 -1
  260. package/dist/ui/alert.d.ts +3 -1
  261. package/dist/ui/alert.js +14 -8
  262. package/dist/ui/autocomplete.js +7 -1
  263. package/dist/ui/avatar.d.ts +8 -0
  264. package/dist/ui/avatar.js +23 -3
  265. package/dist/ui/badge.js +1 -1
  266. package/dist/ui/breadcrumb.d.ts +27 -0
  267. package/dist/ui/breadcrumb.js +88 -0
  268. package/dist/ui/button.d.ts +2 -1
  269. package/dist/ui/button.js +1 -1
  270. package/dist/ui/calendar.js +2 -2
  271. package/dist/ui/card.d.ts +3 -1
  272. package/dist/ui/card.js +12 -7
  273. package/dist/ui/checkbox-group.js +1 -1
  274. package/dist/ui/checkbox.d.ts +2 -0
  275. package/dist/ui/code-diff.d.ts +1 -1
  276. package/dist/ui/code-diff.js +71 -27
  277. package/dist/ui/code.d.ts +5 -3
  278. package/dist/ui/code.js +27 -28
  279. package/dist/ui/color-picker.d.ts +5 -2
  280. package/dist/ui/color-picker.js +84 -36
  281. package/dist/ui/combobox-disabled-selection.d.ts +1 -2
  282. package/dist/ui/combobox-disabled-selection.js +7 -8
  283. package/dist/ui/combobox.d.ts +5 -23
  284. package/dist/ui/combobox.js +36 -16
  285. package/dist/ui/command.d.ts +8 -2
  286. package/dist/ui/command.js +39 -15
  287. package/dist/ui/context-menu.js +3 -0
  288. package/dist/ui/dialog.d.ts +2 -1
  289. package/dist/ui/dialog.js +3 -22
  290. package/dist/ui/dockable-panel-geometry.d.ts +1 -0
  291. package/dist/ui/dockable-panel-geometry.js +7 -1
  292. package/dist/ui/dockable-panel.d.ts +9 -6
  293. package/dist/ui/dockable-panel.js +81 -33
  294. package/dist/ui/drawer.d.ts +7 -4
  295. package/dist/ui/drawer.js +35 -22
  296. package/dist/ui/dropdown-menu.js +4 -3
  297. package/dist/ui/dropzone.d.ts +5 -3
  298. package/dist/ui/dropzone.js +78 -17
  299. package/dist/ui/emoji-picker.d.ts +18 -0
  300. package/dist/ui/emoji-picker.js +136 -0
  301. package/dist/ui/empty.d.ts +3 -1
  302. package/dist/ui/empty.js +12 -7
  303. package/dist/ui/field.js +1 -1
  304. package/dist/ui/gradient-editor.d.ts +1 -1
  305. package/dist/ui/gradient-editor.js +37 -21
  306. package/dist/ui/infinite-canvas.d.ts +13 -1
  307. package/dist/ui/infinite-canvas.js +139 -20
  308. package/dist/ui/input-group.d.ts +2 -4
  309. package/dist/ui/input-group.js +13 -14
  310. package/dist/ui/input-otp.d.ts +1 -1
  311. package/dist/ui/input-otp.js +32 -15
  312. package/dist/ui/input.js +11 -2
  313. package/dist/ui/item.d.ts +2 -1
  314. package/dist/ui/item.js +11 -7
  315. package/dist/ui/live-status.d.ts +7 -0
  316. package/dist/ui/live-status.js +17 -0
  317. package/dist/ui/markdown-elements.d.ts +6 -0
  318. package/dist/ui/markdown-elements.js +42 -9
  319. package/dist/ui/menubar.js +2 -2
  320. package/dist/ui/morphing-panel.js +2 -1
  321. package/dist/ui/native-select.d.ts +1 -1
  322. package/dist/ui/native-select.js +24 -11
  323. package/dist/ui/number-field.d.ts +1 -1
  324. package/dist/ui/number-field.js +13 -2
  325. package/dist/ui/page-layout.d.ts +2 -2
  326. package/dist/ui/page-layout.js +2 -4
  327. package/dist/ui/pagination.d.ts +7 -5
  328. package/dist/ui/pagination.js +19 -13
  329. package/dist/ui/phone-input.d.ts +1 -0
  330. package/dist/ui/phone-input.js +56 -58
  331. package/dist/ui/radio-group.d.ts +3 -0
  332. package/dist/ui/radio-group.js +11 -1
  333. package/dist/ui/resizable.d.ts +20 -2
  334. package/dist/ui/resizable.js +130 -8
  335. package/dist/ui/responsive-dialog.d.ts +1 -1
  336. package/dist/ui/responsive-dialog.js +9 -23
  337. package/dist/ui/rich-tooltip.d.ts +10 -4
  338. package/dist/ui/rich-tooltip.js +81 -33
  339. package/dist/ui/scroll-area.d.ts +4 -1
  340. package/dist/ui/scroll-area.js +44 -28
  341. package/dist/ui/select.d.ts +2 -1
  342. package/dist/ui/select.js +57 -32
  343. package/dist/ui/sidebar-menu.d.ts +5 -1
  344. package/dist/ui/sidebar-menu.js +31 -33
  345. package/dist/ui/sidebar-provider.d.ts +10 -1
  346. package/dist/ui/sidebar-provider.js +38 -13
  347. package/dist/ui/sidebar-resize-rail.js +7 -3
  348. package/dist/ui/sidebar.d.ts +11 -6
  349. package/dist/ui/sidebar.js +46 -23
  350. package/dist/ui/slider.js +31 -22
  351. package/dist/ui/spinner.d.ts +3 -2
  352. package/dist/ui/spinner.js +3 -2
  353. package/dist/ui/stepper.d.ts +8 -1
  354. package/dist/ui/stepper.js +74 -25
  355. package/dist/ui/switch.d.ts +1 -0
  356. package/dist/ui/table-of-contents.d.ts +5 -4
  357. package/dist/ui/table-of-contents.js +143 -52
  358. package/dist/ui/table.js +15 -9
  359. package/dist/ui/textarea.d.ts +1 -1
  360. package/dist/ui/textarea.js +20 -9
  361. package/dist/ui/timeline.d.ts +3 -1
  362. package/dist/ui/timeline.js +17 -9
  363. package/dist/ui/toast.d.ts +11 -5
  364. package/dist/ui/toast.js +100 -54
  365. package/dist/ui/toggle.d.ts +1 -1
  366. package/dist/ui/toggle.js +2 -1
  367. package/dist/ui/toolbar.d.ts +3 -2
  368. package/dist/ui/toolbar.js +4 -2
  369. package/dist/ui/tooltip.js +3 -20
  370. package/dist/ui/tree.js +120 -80
  371. package/dist/ui/trigger-menu.js +7 -2
  372. package/dist/user-ask.d.ts +10 -5
  373. package/dist/user-ask.js +31 -11
  374. package/package.json +5 -1
  375. package/dist/hooks/use-keyboard-navigation.d.ts +0 -3
  376. package/dist/hooks/use-keyboard-navigation.js +0 -22
@@ -4,7 +4,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  //#region src/hooks/use-dropzone.ts
5
5
  const registeredDropzoneRoots = /* @__PURE__ */ new WeakSet();
6
6
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
7
- function useDropzone({ value: controlledValue, defaultValue = [], onValueChange, policy, disabled = false, drag = true, preventDropOnDocument = true, trackDocumentDrag = false, getFilesFromEvent, onDrop, onError } = {}) {
7
+ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange, policy, disabled = false, drag = true, paste = true, preventDropOnDocument = true, trackDocumentDrag = false, getFilesFromEvent, onDrop, onError } = {}) {
8
8
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
9
9
  const [fileRejections, setFileRejections] = useState([]);
10
10
  const [isDragActive, setIsDragActive] = useState(false);
@@ -25,12 +25,15 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
25
25
  const disabledRef = useRef(disabled);
26
26
  const dragEnabled = drag && !disabled;
27
27
  const dragEnabledRef = useRef(dragEnabled);
28
+ const pasteEnabled = paste && !disabled;
29
+ const pasteEnabledRef = useRef(pasteEnabled);
28
30
  const policyRef = useRef(policy);
29
31
  const customExtractionRef = useRef(Boolean(getFilesFromEvent));
30
32
  useIsomorphicLayoutEffect(() => {
31
33
  currentValueRef.current = currentValue;
32
34
  disabledRef.current = disabled;
33
35
  dragEnabledRef.current = dragEnabled;
36
+ pasteEnabledRef.current = pasteEnabled;
34
37
  policyRef.current = policy;
35
38
  customExtractionRef.current = Boolean(getFilesFromEvent);
36
39
  });
@@ -136,6 +139,10 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
136
139
  removedFiles
137
140
  });
138
141
  }
142
+ function removeRejection(file) {
143
+ if (disabledRef.current || processingRef.current) return;
144
+ setFileRejections((current) => current.filter((rejection) => rejection.file !== file));
145
+ }
139
146
  function clearRejections() {
140
147
  if (disabledRef.current || processingRef.current || fileRejections.length === 0) return;
141
148
  setFileRejections([]);
@@ -153,7 +160,7 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
153
160
  if (fileRejections.length > 0) setFileRejections([]);
154
161
  }
155
162
  function getRootProps(props = {}) {
156
- const { onDragEnter, onDragOver, onDragLeave, onDragEnd, onDrop: callerOnDrop, ref, ...rootProps } = props;
163
+ const { onDragEnter, onDragOver, onDragLeave, onDragEnd, onDrop: callerOnDrop, onPaste, ref, ...rootProps } = props;
157
164
  return {
158
165
  ...rootProps,
159
166
  ref: (node) => {
@@ -207,6 +214,14 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
207
214
  if (event.defaultPrevented || !dragEnabledRef.current) return;
208
215
  event.preventDefault();
209
216
  intake(event, "drop");
217
+ },
218
+ onPaste: (event) => {
219
+ onPaste?.(event);
220
+ if (!isFilePaste(event.clipboardData)) return;
221
+ event.stopPropagation();
222
+ if (event.defaultPrevented || !pasteEnabledRef.current) return;
223
+ event.preventDefault();
224
+ intake(event, "paste");
210
225
  }
211
226
  };
212
227
  }
@@ -326,6 +341,7 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
326
341
  multiple: policy?.multiple ?? true,
327
342
  disabled,
328
343
  dragEnabled,
344
+ pasteEnabled,
329
345
  visualState,
330
346
  isDragActive,
331
347
  isDragAccept: visualState === "accept",
@@ -339,6 +355,7 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
339
355
  open,
340
356
  removeFile,
341
357
  clearFiles,
358
+ removeRejection,
342
359
  clearRejections,
343
360
  reset,
344
361
  getRootProps,
@@ -346,11 +363,25 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
346
363
  };
347
364
  }
348
365
  function defaultGetFilesFromEvent(event) {
349
- return "dataTransfer" in event ? Array.from(event.dataTransfer.files) : Array.from(event.currentTarget.files ?? []);
366
+ if ("clipboardData" in event) return getClipboardFiles(event.clipboardData);
367
+ if ("dataTransfer" in event) return Array.from(event.dataTransfer.files);
368
+ return Array.from(event.currentTarget.files ?? []);
369
+ }
370
+ /** Safari exposes a pasted screenshot through `items` only, so `files` alone loses it. */
371
+ function getClipboardFiles(clipboardData) {
372
+ const files = Array.from(clipboardData.files);
373
+ if (files.length > 0) return files;
374
+ return Array.from(clipboardData.items).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((file) => file !== null);
350
375
  }
351
376
  function isFileDrag(dataTransfer) {
352
377
  return dataTransfer ? Array.from(dataTransfer.types).includes("Files") : false;
353
378
  }
379
+ /** Clipboards that also carry text belong to the focused editor: only a text-free paste is an attachment. */
380
+ function isFilePaste(clipboardData) {
381
+ if (!clipboardData) return false;
382
+ const types = Array.from(clipboardData.types);
383
+ return types.includes("Files") && !types.includes("text/plain");
384
+ }
354
385
  function getRegisteredDropzoneRoot(event) {
355
386
  for (const target of event.composedPath()) if (target && registeredDropzoneRoots.has(target)) return target;
356
387
  }
@@ -18,18 +18,22 @@ export type DynamicNotificationProps = Omit<ComponentProps<"div">, "onChange" |
18
18
  onReply?: (payload: DynamicNotificationReplyPayload) => void | Promise<void>;
19
19
  variant?: DynamicNotificationVariant;
20
20
  disabled?: boolean;
21
+ thinkingLabel?: string;
22
+ replyErrorLabel?: string;
21
23
  style?: CSSProperties & DynamicNotificationKnobStyle;
22
24
  };
23
25
  export type DynamicNotificationController = {
24
26
  open: boolean;
25
27
  disabled: boolean;
26
- setOpen: (nextOpen: boolean, reason: OpenChangeReason, event: Event, trigger?: Element) => void;
28
+ setOpen: (nextOpen: boolean, reason: OpenChangeReason, event: Event, trigger?: Element) => boolean;
27
29
  reply: string;
28
30
  setReply: (nextValue: string) => void;
29
31
  normalizedReply: string;
30
32
  canSubmit: boolean;
33
+ replyPending: boolean;
34
+ replyFailed: boolean;
31
35
  clear: () => void;
32
- submitReply: () => void;
36
+ submitReply: () => Promise<void>;
33
37
  handleReplySubmit: (event: FormSubmitEvent) => void;
34
38
  };
35
39
  export declare function useDynamicNotification({ open, defaultOpen, onOpenChange, replyValue, defaultReplyValue, onReplyValueChange, onReply, disabled }: Pick<DynamicNotificationProps, "open" | "defaultOpen" | "onOpenChange" | "replyValue" | "defaultReplyValue" | "onReplyValueChange" | "onReply" | "disabled">): DynamicNotificationController;
@@ -1,4 +1,4 @@
1
- import { useMemo, useState } from "react";
1
+ import { useMemo, useRef, useState } from "react";
2
2
  //#region src/hooks/use-dynamic-notification.ts
3
3
  function createOpenChangeEventDetails(reason, event, trigger) {
4
4
  let canceled = false;
@@ -39,13 +39,17 @@ function useDynamicNotification({ open, defaultOpen = false, onOpenChange, reply
39
39
  onValueChange: onReplyValueChange
40
40
  });
41
41
  const normalizedReply = reply.trim();
42
- const canSubmit = normalizedReply.length > 0 && !disabled;
42
+ const [replyPending, setReplyPending] = useState(false);
43
+ const [replyFailed, setReplyFailed] = useState(false);
44
+ const replyInFlight = useRef(false);
45
+ const canSubmit = normalizedReply.length > 0 && !disabled && !replyPending;
43
46
  const setOpen = useMemo(() => (nextOpen, reason, event, trigger) => {
44
- if (disabled || nextOpen === isOpen) return;
47
+ if (disabled || nextOpen === isOpen) return false;
45
48
  const details = createOpenChangeEventDetails(reason, event, trigger);
46
49
  onOpenChange?.(nextOpen, details);
47
- if (details.isCanceled) return;
50
+ if (details.isCanceled) return false;
48
51
  if (!isControlled) setInternalOpen(nextOpen);
52
+ return true;
49
53
  }, [
50
54
  disabled,
51
55
  isControlled,
@@ -55,12 +59,22 @@ function useDynamicNotification({ open, defaultOpen = false, onOpenChange, reply
55
59
  const clear = useMemo(() => () => {
56
60
  setReply("");
57
61
  }, [setReply]);
58
- const submitReply = useMemo(() => () => {
59
- if (!canSubmit) return;
60
- onReply?.({
61
- value: normalizedReply,
62
- clear
63
- });
62
+ const submitReply = useMemo(() => async () => {
63
+ if (!canSubmit || replyInFlight.current) return;
64
+ replyInFlight.current = true;
65
+ setReplyPending(true);
66
+ setReplyFailed(false);
67
+ try {
68
+ await onReply?.({
69
+ value: normalizedReply,
70
+ clear
71
+ });
72
+ } catch {
73
+ setReplyFailed(true);
74
+ } finally {
75
+ replyInFlight.current = false;
76
+ setReplyPending(false);
77
+ }
64
78
  }, [
65
79
  canSubmit,
66
80
  clear,
@@ -75,6 +89,8 @@ function useDynamicNotification({ open, defaultOpen = false, onOpenChange, reply
75
89
  setReply,
76
90
  normalizedReply,
77
91
  canSubmit,
92
+ replyPending,
93
+ replyFailed,
78
94
  clear,
79
95
  submitReply,
80
96
  handleReplySubmit: useMemo(() => (event) => {
@@ -1,4 +1,5 @@
1
1
  //#region src/hooks/use-mobile.d.ts
2
- /** True below `breakpoint` px (Tailwind `md` by default); pass another breakpoint to match layout that switches elsewhere. */
3
- export declare function useIsMobile(breakpoint?: number): boolean;
2
+ type Breakpoint = number | `--${string}`;
3
+ /** True below `breakpoint`: px (Tailwind `md` by default) or a theme breakpoint token such as `--breakpoint-lg`, so JS and the matching Tailwind variant share one value. */
4
+ export declare function useIsMobile(breakpoint?: Breakpoint): boolean;
4
5
  //#endregion
@@ -5,14 +5,18 @@ const viewportStores = /* @__PURE__ */ new Map();
5
5
  function viewportStore(breakpoint) {
6
6
  const cached = viewportStores.get(breakpoint);
7
7
  if (cached) return cached;
8
- const mobileQuery = `(max-width: ${breakpoint - 1}px)`;
8
+ let resolvedQuery;
9
+ function mobileQuery() {
10
+ resolvedQuery ??= typeof breakpoint === "number" ? `(width < ${breakpoint}px)` : `(width < ${getComputedStyle(document.documentElement).getPropertyValue(breakpoint).trim()})`;
11
+ return resolvedQuery;
12
+ }
9
13
  const store = {
10
14
  subscribe(onChange) {
11
- const query = window.matchMedia(mobileQuery);
15
+ const query = window.matchMedia(mobileQuery());
12
16
  query.addEventListener("change", onChange);
13
17
  return () => query.removeEventListener("change", onChange);
14
18
  },
15
- getSnapshot: () => window.matchMedia(mobileQuery).matches
19
+ getSnapshot: () => window.matchMedia(mobileQuery()).matches
16
20
  };
17
21
  viewportStores.set(breakpoint, store);
18
22
  return store;
@@ -20,7 +24,7 @@ function viewportStore(breakpoint) {
20
24
  function getServerViewportSnapshot() {
21
25
  return false;
22
26
  }
23
- /** True below `breakpoint` px (Tailwind `md` by default); pass another breakpoint to match layout that switches elsewhere. */
27
+ /** True below `breakpoint`: px (Tailwind `md` by default) or a theme breakpoint token such as `--breakpoint-lg`, so JS and the matching Tailwind variant share one value. */
24
28
  function useIsMobile(breakpoint = MOBILE_BREAKPOINT) {
25
29
  const store = viewportStore(breakpoint);
26
30
  return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerViewportSnapshot);
@@ -1,6 +1,6 @@
1
1
  "use client";
2
+ import { isComposingKey, useTriggerMenu } from "./use-trigger-menu.js";
2
3
  import { caretRectInTextarea, detectTrigger } from "../lib/trigger-detect.js";
3
- import { useTriggerMenu } from "./use-trigger-menu.js";
4
4
  import { useEffect, useEffectEvent } from "react";
5
5
  //#region src/hooks/use-textarea-trigger-menu.ts
6
6
  function replaceRange(element, start, end, text) {
@@ -36,6 +36,7 @@ function useTextareaTriggerMenu(ref, options) {
36
36
  controller.report(match, match === null ? null : caretRectInTextarea(element, match.start));
37
37
  });
38
38
  const handleKeyDown = useEffectEvent((event) => {
39
+ if (isComposingKey(event)) return;
39
40
  if (controller.open && controller.handleKeyDown(event.key)) event.preventDefault();
40
41
  });
41
42
  const closeAfterBlur = useEffectEvent(() => {
@@ -27,6 +27,16 @@ export type UseTriggerMenuOptions<Item extends TriggerMenuItemData> = {
27
27
  triggers: readonly TriggerConfig<Item>[];
28
28
  onCommit: (item: Item, trigger: TriggerConfig<Item>, match: TriggerMatch) => void;
29
29
  };
30
+ /**
31
+ * Popup attributes for the element that keeps focus (textarea or editor) while the menu is up. It stays a
32
+ * multiline textbox (a <textarea> can't take role="combobox"), so open state is conveyed by `aria-controls`.
33
+ */
34
+ export type TriggerMenuInputAria = {
35
+ "aria-autocomplete": "list";
36
+ "aria-haspopup": "listbox";
37
+ "aria-controls"?: string;
38
+ "aria-activedescendant"?: string;
39
+ };
30
40
  export type TriggerMenuController<Item extends TriggerMenuItemData> = {
31
41
  open: boolean;
32
42
  items: readonly Item[];
@@ -34,6 +44,11 @@ export type TriggerMenuController<Item extends TriggerMenuItemData> = {
34
44
  anchorRect: DOMRect | null;
35
45
  activeChar: string | null;
36
46
  query: string;
47
+ /** Id for `TriggerMenuList`; the input's `aria-controls` points at it. */
48
+ listId: string;
49
+ /** Id for the option at `index`; the input's `aria-activedescendant` points at the active one. */
50
+ optionId: (index: number) => string;
51
+ inputAria: TriggerMenuInputAria;
37
52
  /** Binding → engine: publish current trigger match (or null) and caret rect to anchor to. */
38
53
  report: (match: TriggerMatch | null, anchorRect: DOMRect | null) => void;
39
54
  /** Binding → engine: route keydown while open. Returns true when engine consumed key. */
@@ -44,5 +59,44 @@ export type TriggerMenuController<Item extends TriggerMenuItemData> = {
44
59
  close: () => void;
45
60
  setOpen: (open: boolean) => void;
46
61
  };
62
+ type DismissedMatch = {
63
+ char: string;
64
+ start: number;
65
+ query: string;
66
+ };
67
+ export type TriggerMenuState = {
68
+ open: boolean;
69
+ char: string | null;
70
+ query: string;
71
+ anchorRect: DOMRect | null;
72
+ activeIndex: number;
73
+ dismissed: DismissedMatch | null;
74
+ };
75
+ export type TriggerMenuAction = {
76
+ type: "report";
77
+ match: TriggerMatch | null;
78
+ anchorRect: DOMRect | null;
79
+ } | {
80
+ type: "close";
81
+ dismissed: DismissedMatch | null;
82
+ } | {
83
+ type: "move";
84
+ delta: number;
85
+ count: number;
86
+ } | {
87
+ type: "activate";
88
+ index: number;
89
+ };
90
+ export declare const CLOSED_TRIGGER_MENU: TriggerMenuState;
91
+ /** A dismissed token stays closed until its query changes, so keyup/click re-detection after Escape can't reopen it. */
92
+ export declare function triggerMenuReducer(state: TriggerMenuState, action: TriggerMenuAction): TriggerMenuState;
93
+ /** Keys an IME is still composing must never reach the menu or the composer's submit. */
94
+ export declare function isComposingKey(event: {
95
+ isComposing: boolean;
96
+ keyCode: number;
97
+ }): boolean;
98
+ export type TriggerMenuKeyIntent = "next" | "previous" | "commit" | "dismiss" | "pass";
99
+ /** What a key means to an open menu; an empty list passes Enter/Tab through so newline, submit and Tab-out still work. */
100
+ export declare function triggerMenuKeyIntent(key: string, open: boolean, itemCount: number): TriggerMenuKeyIntent;
47
101
  export declare function useTriggerMenu<Item extends TriggerMenuItemData>({ triggers, onCommit }: UseTriggerMenuOptions<Item>): TriggerMenuController<Item>;
48
102
  //#endregion
@@ -1,12 +1,48 @@
1
- import { useRef, useState } from "react";
1
+ import { useId, useReducer, useRef } from "react";
2
2
  //#region src/hooks/use-trigger-menu.ts
3
- const CLOSED = {
3
+ const CLOSED_TRIGGER_MENU = {
4
4
  open: false,
5
5
  char: null,
6
6
  query: "",
7
7
  anchorRect: null,
8
- activeIndex: 0
8
+ activeIndex: 0,
9
+ dismissed: null
9
10
  };
11
+ /** A dismissed token stays closed until its query changes, so keyup/click re-detection after Escape can't reopen it. */
12
+ function triggerMenuReducer(state, action) {
13
+ switch (action.type) {
14
+ case "report": {
15
+ const { match, anchorRect } = action;
16
+ if (match === null) return { ...CLOSED_TRIGGER_MENU };
17
+ const { dismissed } = state;
18
+ if (dismissed !== null && dismissed.char === match.char && dismissed.start === match.start && dismissed.query === match.query) return {
19
+ ...CLOSED_TRIGGER_MENU,
20
+ dismissed
21
+ };
22
+ const activeIndex = state.open && state.char === match.char && state.query === match.query ? state.activeIndex : 0;
23
+ return {
24
+ open: true,
25
+ char: match.char,
26
+ query: match.query,
27
+ anchorRect,
28
+ activeIndex,
29
+ dismissed: null
30
+ };
31
+ }
32
+ case "close": return {
33
+ ...CLOSED_TRIGGER_MENU,
34
+ dismissed: action.dismissed
35
+ };
36
+ case "move": return {
37
+ ...state,
38
+ activeIndex: action.count === 0 ? 0 : (state.activeIndex + action.delta + action.count) % action.count
39
+ };
40
+ case "activate": return {
41
+ ...state,
42
+ activeIndex: action.index
43
+ };
44
+ }
45
+ }
10
46
  function matchesQuery(item, query) {
11
47
  if (query === "") return true;
12
48
  const needle = query.toLowerCase();
@@ -18,14 +54,45 @@ function resolveItems(trigger, query) {
18
54
  const filter = trigger.filter ?? matchesQuery;
19
55
  return trigger.items.filter((item) => filter(item, query));
20
56
  }
57
+ /** Keys an IME is still composing must never reach the menu or the composer's submit. */
58
+ function isComposingKey(event) {
59
+ return event.isComposing || event.keyCode === 229;
60
+ }
61
+ /** What a key means to an open menu; an empty list passes Enter/Tab through so newline, submit and Tab-out still work. */
62
+ function triggerMenuKeyIntent(key, open, itemCount) {
63
+ if (!open) return "pass";
64
+ if (key === "ArrowDown") return "next";
65
+ if (key === "ArrowUp") return "previous";
66
+ if (key === "Escape") return "dismiss";
67
+ if ((key === "Enter" || key === "Tab") && itemCount > 0) return "commit";
68
+ return "pass";
69
+ }
21
70
  function useTriggerMenu({ triggers, onCommit }) {
22
- const [state, setState] = useState(CLOSED);
71
+ const [state, dispatch] = useReducer(triggerMenuReducer, CLOSED_TRIGGER_MENU);
23
72
  const lastMatch = useRef(null);
73
+ const listId = useId();
24
74
  const activeTrigger = state.char === null ? null : triggers.find((trigger) => trigger.char === state.char) ?? null;
25
75
  const items = activeTrigger === null ? [] : resolveItems(activeTrigger, state.query);
76
+ const optionId = (index) => `${listId}-option-${index}`;
77
+ const hasActiveItem = state.open && state.activeIndex < items.length;
78
+ function dismiss() {
79
+ const match = lastMatch.current;
80
+ lastMatch.current = null;
81
+ dispatch({
82
+ type: "close",
83
+ dismissed: match === null ? null : {
84
+ char: match.char,
85
+ start: match.start,
86
+ query: match.query
87
+ }
88
+ });
89
+ }
26
90
  function close() {
27
91
  lastMatch.current = null;
28
- setState(CLOSED);
92
+ dispatch({
93
+ type: "close",
94
+ dismissed: null
95
+ });
29
96
  }
30
97
  function select(item) {
31
98
  const match = lastMatch.current;
@@ -33,55 +100,35 @@ function useTriggerMenu({ triggers, onCommit }) {
33
100
  close();
34
101
  }
35
102
  function report(match, anchorRect) {
36
- lastMatch.current = match;
37
- const open = match !== null && triggers.some((trigger) => trigger.char === match.char);
38
- setState((current) => {
39
- if (!open || match === null) return CLOSED;
40
- const activeIndex = current.char === match.char && current.query === match.query ? current.activeIndex : 0;
41
- return {
42
- open: true,
43
- char: match.char,
44
- query: match.query,
45
- anchorRect,
46
- activeIndex
47
- };
103
+ const known = match !== null && triggers.some((trigger) => trigger.char === match.char);
104
+ lastMatch.current = known ? match : null;
105
+ dispatch({
106
+ type: "report",
107
+ match: known ? match : null,
108
+ anchorRect
48
109
  });
49
110
  }
50
111
  function handleKeyDown(key) {
51
- if (!state.open) return false;
52
- if (key === "ArrowDown") {
53
- setState((current) => ({
54
- ...current,
55
- activeIndex: items.length === 0 ? 0 : (current.activeIndex + 1) % items.length
56
- }));
57
- return true;
58
- }
59
- if (key === "ArrowUp") {
60
- setState((current) => ({
61
- ...current,
62
- activeIndex: items.length === 0 ? 0 : (current.activeIndex - 1 + items.length) % items.length
63
- }));
64
- return true;
65
- }
66
- if (key === "Enter" || key === "Tab") {
112
+ const intent = triggerMenuKeyIntent(key, state.open, items.length);
113
+ if (intent === "next" || intent === "previous") dispatch({
114
+ type: "move",
115
+ delta: intent === "next" ? 1 : -1,
116
+ count: items.length
117
+ });
118
+ else if (intent === "commit") {
67
119
  const item = items[state.activeIndex];
68
120
  if (item && !item.disabled) select(item);
69
- return true;
70
- }
71
- if (key === "Escape") {
72
- close();
73
- return true;
74
- }
75
- return false;
121
+ } else if (intent === "dismiss") dismiss();
122
+ return intent !== "pass";
76
123
  }
77
124
  function setOpen(open) {
78
- if (!open) close();
125
+ if (!open) dismiss();
79
126
  }
80
- function setActiveIndex(activeIndex) {
81
- setState((current) => ({
82
- ...current,
83
- activeIndex
84
- }));
127
+ function setActiveIndex(index) {
128
+ dispatch({
129
+ type: "activate",
130
+ index
131
+ });
85
132
  }
86
133
  return {
87
134
  open: state.open,
@@ -90,6 +137,14 @@ function useTriggerMenu({ triggers, onCommit }) {
90
137
  anchorRect: state.anchorRect,
91
138
  activeChar: state.char,
92
139
  query: state.query,
140
+ listId,
141
+ optionId,
142
+ inputAria: {
143
+ "aria-autocomplete": "list",
144
+ "aria-haspopup": "listbox",
145
+ "aria-controls": state.open ? listId : void 0,
146
+ "aria-activedescendant": hasActiveItem ? optionId(state.activeIndex) : void 0
147
+ },
93
148
  report,
94
149
  handleKeyDown,
95
150
  setActiveIndex,
@@ -99,4 +154,4 @@ function useTriggerMenu({ triggers, onCommit }) {
99
154
  };
100
155
  }
101
156
  //#endregion
102
- export { useTriggerMenu };
157
+ export { CLOSED_TRIGGER_MENU, isComposingKey, triggerMenuKeyIntent, triggerMenuReducer, useTriggerMenu };
@@ -6,6 +6,7 @@ export type UserAskProps = Omit<ComponentProps<"section">, "style"> & {
6
6
  children?: ReactNode;
7
7
  onComplete?: (answers: UserAskAnswers) => void;
8
8
  onDismiss?: () => void;
9
+ questionStatusLabel?: (current: number, total: number, title: string) => string;
9
10
  style?: CSSProperties & UserAskKnobStyle;
10
11
  };
11
12
  export type UserAskQuestionEntry = {
@@ -36,6 +37,7 @@ export declare function useUserAsk({ onComplete, onDismiss }: Pick<UserAskProps,
36
37
  allowsMultiple: (questionId: string) => boolean;
37
38
  selectionFor: (questionId: string) => string;
38
39
  isSelected: (questionId: string, optionKey: string) => boolean;
40
+ isTabStop: (questionId: string, optionKey: string) => boolean;
39
41
  select: (questionId: string, optionKey: string) => void;
40
42
  freeformTextFor: (questionId: string) => string;
41
43
  setFreeformText: (questionId: string, text: string) => void;
@@ -8,6 +8,12 @@ function upsert(entries, entry) {
8
8
  next[index] = entry;
9
9
  return next;
10
10
  }
11
+ const arrowDeltas = {
12
+ ArrowDown: 1,
13
+ ArrowRight: 1,
14
+ ArrowUp: -1,
15
+ ArrowLeft: -1
16
+ };
11
17
  function useUserAsk({ onComplete, onDismiss }) {
12
18
  const [questions, setQuestions] = useState([]);
13
19
  const [optionsByQuestion, setOptionsByQuestion] = useState({});
@@ -114,21 +120,11 @@ function useUserAsk({ onComplete, onDismiss }) {
114
120
  function dismiss() {
115
121
  onDismiss?.();
116
122
  }
117
- function moveSelection(delta) {
118
- if (!activeQuestion) return;
119
- const options = optionsFor(activeQuestion.id);
120
- if (!options.some((option) => !option.disabled)) return;
121
- const currentKey = selectionFor(activeQuestion.id);
122
- const currentIndex = options.findIndex((option) => option.key === currentKey);
123
- let start = currentIndex;
124
- if (currentIndex === -1) start = delta > 0 ? -1 : 0;
125
- for (let step = 1; step <= options.length; step += 1) {
126
- const candidate = options[(start + delta * step + options.length * step) % options.length];
127
- if (candidate && !candidate.disabled) {
128
- select(activeQuestion.id, candidate.key);
129
- return;
130
- }
131
- }
123
+ function isTabStop(questionId, optionKey) {
124
+ if (allowsMultiple(questionId)) return true;
125
+ const selectedKey = selectionFor(questionId);
126
+ if (selectedKey !== void 0) return selectedKey === optionKey;
127
+ return optionsFor(questionId).find((option) => !option.disabled)?.key === optionKey;
132
128
  }
133
129
  function selectDigit(digit) {
134
130
  if (!activeQuestion) return;
@@ -137,39 +133,28 @@ function useUserAsk({ onComplete, onDismiss }) {
137
133
  }
138
134
  function moveActiveOption(event, delta) {
139
135
  if (!activeQuestion) return;
140
- if (!allowsMultiple(activeQuestion.id)) {
141
- moveSelection(delta);
142
- return;
143
- }
144
136
  const rows = [...event.currentTarget.querySelector("[data-slot=\"question\"][data-active]")?.querySelectorAll("[data-slot=\"option\"]:not([disabled])") ?? []];
145
137
  if (rows.length === 0) return;
146
138
  const focused = rows.findIndex((candidate) => candidate.contains(document.activeElement));
147
139
  let start = focused;
148
140
  if (focused === -1) start = delta > 0 ? -1 : 0;
149
141
  const row = rows[(start + delta + rows.length) % rows.length];
150
- (row?.matches("button, input") ? row : row?.querySelector("input, button"))?.focus();
151
- }
152
- function handleKeyDown(event) {
153
- if (event.defaultPrevented) return;
154
- if (event.key === "Escape") {
155
- event.preventDefault();
156
- dismiss();
157
- return;
158
- }
159
- if (event.key === "Enter") {
142
+ const target = row?.matches("button, input") ? row : row?.querySelector("input, button");
143
+ target?.focus();
144
+ if (!allowsMultiple(activeQuestion.id) && target?.matches("button")) target.click();
145
+ }
146
+ function handleEnter(event) {
147
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return;
148
+ if (event.target instanceof Element && event.target.closest("button:not([role=\"radio\"]):not([role=\"checkbox\"]), a[href]")) return;
149
+ event.preventDefault();
150
+ advance();
151
+ }
152
+ function handleOptionKey(event) {
153
+ if (event.target instanceof Element && event.target.closest("input, textarea, [contenteditable]")) return;
154
+ const delta = arrowDeltas[event.key];
155
+ if (delta !== void 0) {
160
156
  event.preventDefault();
161
- advance();
162
- return;
163
- }
164
- if (event.target instanceof HTMLElement && event.target.closest("input, textarea, [contenteditable]")) return;
165
- if (event.key === "ArrowDown") {
166
- event.preventDefault();
167
- moveActiveOption(event, 1);
168
- return;
169
- }
170
- if (event.key === "ArrowUp") {
171
- event.preventDefault();
172
- moveActiveOption(event, -1);
157
+ moveActiveOption(event, delta);
173
158
  return;
174
159
  }
175
160
  if (/^[1-9]$/.test(event.key)) {
@@ -177,6 +162,14 @@ function useUserAsk({ onComplete, onDismiss }) {
177
162
  selectDigit(Number(event.key));
178
163
  }
179
164
  }
165
+ function handleKeyDown(event) {
166
+ if (event.defaultPrevented) return;
167
+ if (event.key === "Escape") {
168
+ event.preventDefault();
169
+ dismiss();
170
+ } else if (event.key === "Enter") handleEnter(event);
171
+ else handleOptionKey(event);
172
+ }
180
173
  return {
181
174
  questions,
182
175
  activeIndex,
@@ -191,6 +184,7 @@ function useUserAsk({ onComplete, onDismiss }) {
191
184
  allowsMultiple,
192
185
  selectionFor,
193
186
  isSelected,
187
+ isTabStop,
194
188
  select,
195
189
  freeformTextFor,
196
190
  setFreeformText,