@ctrl-ui/react 0.8.7 → 0.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) 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 +39 -39
  8. package/dist/blocks/design-canvas.d.ts +5 -1
  9. package/dist/blocks/design-canvas.js +198 -92
  10. package/dist/blocks/discussion.d.ts +28 -0
  11. package/dist/blocks/discussion.js +67 -0
  12. package/dist/blocks/file-explorer-data.d.ts +1 -0
  13. package/dist/blocks/file-explorer-data.js +4 -1
  14. package/dist/blocks/file-explorer.d.ts +9 -1
  15. package/dist/blocks/file-explorer.js +461 -192
  16. package/dist/blocks/settings.d.ts +2 -1
  17. package/dist/blocks/settings.js +101 -79
  18. package/dist/blocks/team-chat.d.ts +12 -0
  19. package/dist/blocks/team-chat.js +26 -0
  20. package/dist/blocks/theme-toggle.js +23 -31
  21. package/dist/chat-composer-attachment.d.ts +4 -1
  22. package/dist/chat-composer-attachment.js +35 -10
  23. package/dist/chat-composer-editor/extensions/mention.d.ts +5 -1
  24. package/dist/chat-composer-editor/extensions/mention.js +59 -28
  25. package/dist/chat-composer-editor/ghost.js +2 -4
  26. package/dist/chat-composer-editor/types.d.ts +5 -0
  27. package/dist/chat-composer-editor.d.ts +1 -1
  28. package/dist/chat-composer-editor.js +57 -30
  29. package/dist/chat-composer.d.ts +13 -32
  30. package/dist/chat-composer.js +52 -12
  31. package/dist/chat-layout.d.ts +19 -0
  32. package/dist/chat-layout.js +91 -40
  33. package/dist/chat-message/social.d.ts +26 -0
  34. package/dist/chat-message/social.js +122 -0
  35. package/dist/chat-message.d.ts +28 -29
  36. package/dist/chat-message.js +106 -25
  37. package/dist/context.d.ts +2 -2
  38. package/dist/context.js +38 -23
  39. package/dist/dynamic-notification-canvas-loop.d.ts +24 -0
  40. package/dist/dynamic-notification-canvas-loop.js +113 -0
  41. package/dist/dynamic-notification-glass.js +23 -114
  42. package/dist/dynamic-notification-liquid.js +14 -118
  43. package/dist/dynamic-notification.d.ts +16 -8
  44. package/dist/dynamic-notification.js +85 -22
  45. package/dist/email/email-brand.d.ts +7 -4
  46. package/dist/email/email-brand.js +9 -6
  47. package/dist/email/email-code.d.ts +3 -2
  48. package/dist/email/email-code.js +11 -9
  49. package/dist/email/email-footer.d.ts +5 -3
  50. package/dist/email/email-footer.js +11 -5
  51. package/dist/email/email.d.ts +31 -9
  52. package/dist/email/email.js +50 -26
  53. package/dist/email/templates.d.ts +10 -9
  54. package/dist/email/templates.js +102 -38
  55. package/dist/email/theme.js +1 -1
  56. package/dist/environment-variables.d.ts +4 -2
  57. package/dist/environment-variables.js +52 -32
  58. package/dist/filter-bar/editor.js +20 -8
  59. package/dist/filter-bar/model.d.ts +1 -0
  60. package/dist/filter-bar/model.js +10 -1
  61. package/dist/filter-bar/motion.js +4 -4
  62. package/dist/filter-bar/parts.js +19 -8
  63. package/dist/filter-bar/types.d.ts +1 -0
  64. package/dist/filter-bar.js +19 -13
  65. package/dist/hooks/use-audio-recorder.js +12 -6
  66. package/dist/hooks/use-chat-composer.d.ts +19 -7
  67. package/dist/hooks/use-chat-composer.js +32 -5
  68. package/dist/hooks/use-chat-message.d.ts +28 -2
  69. package/dist/hooks/use-chat-message.js +41 -10
  70. package/dist/hooks/use-chat-thread-scroll.js +43 -7
  71. package/dist/hooks/use-color-area.d.ts +3 -3
  72. package/dist/hooks/use-color-area.js +13 -12
  73. package/dist/hooks/use-copy-to-clipboard.d.ts +2 -1
  74. package/dist/hooks/use-copy-to-clipboard.js +20 -20
  75. package/dist/hooks/use-dynamic-notification.d.ts +6 -2
  76. package/dist/hooks/use-dynamic-notification.js +26 -10
  77. package/dist/hooks/use-mobile.d.ts +3 -2
  78. package/dist/hooks/use-mobile.js +8 -4
  79. package/dist/hooks/use-textarea-trigger-menu.js +2 -1
  80. package/dist/hooks/use-trigger-menu.d.ts +54 -0
  81. package/dist/hooks/use-trigger-menu.js +102 -47
  82. package/dist/hooks/use-user-ask.d.ts +2 -0
  83. package/dist/hooks/use-user-ask.js +35 -41
  84. package/dist/inline-attachment.d.ts +6 -2
  85. package/dist/inline-attachment.js +36 -15
  86. package/dist/inline-citation.d.ts +12 -5
  87. package/dist/inline-citation.js +25 -16
  88. package/dist/knob-contracts/audio-recorder-knobs.d.ts +1 -1
  89. package/dist/knob-contracts/audio-recorder-knobs.js +2 -1
  90. package/dist/knob-contracts/audio-visualizer-knobs.d.ts +1 -1
  91. package/dist/knob-contracts/audio-visualizer-knobs.js +2 -1
  92. package/dist/knob-contracts/avatar-knobs.d.ts +1 -1
  93. package/dist/knob-contracts/avatar-knobs.js +6 -1
  94. package/dist/knob-contracts/badge-knobs.d.ts +1 -1
  95. package/dist/knob-contracts/badge-knobs.js +2 -1
  96. package/dist/knob-contracts/breadcrumb-knobs.d.ts +4 -0
  97. package/dist/knob-contracts/breadcrumb-knobs.js +8 -0
  98. package/dist/knob-contracts/button-knobs.d.ts +1 -1
  99. package/dist/knob-contracts/button-knobs.js +1 -0
  100. package/dist/knob-contracts/chat-message-knobs.d.ts +1 -1
  101. package/dist/knob-contracts/chat-message-knobs.js +8 -1
  102. package/dist/knob-contracts/code-diff-knobs.d.ts +1 -1
  103. package/dist/knob-contracts/code-diff-knobs.js +2 -0
  104. package/dist/knob-contracts/code-knobs.d.ts +1 -1
  105. package/dist/knob-contracts/code-knobs.js +2 -1
  106. package/dist/knob-contracts/dynamic-notification-knobs.d.ts +1 -1
  107. package/dist/knob-contracts/dynamic-notification-knobs.js +2 -0
  108. package/dist/knob-contracts/emoji-picker-knobs.d.ts +4 -0
  109. package/dist/knob-contracts/emoji-picker-knobs.js +10 -0
  110. package/dist/knob-contracts/field-knobs.d.ts +1 -1
  111. package/dist/knob-contracts/field-knobs.js +5 -1
  112. package/dist/knob-contracts/filter-bar-knobs.d.ts +1 -1
  113. package/dist/knob-contracts/filter-bar-knobs.js +1 -2
  114. package/dist/knob-contracts/markdown-knobs.d.ts +1 -1
  115. package/dist/knob-contracts/markdown-knobs.js +3 -1
  116. package/dist/knob-contracts/page-layout-knobs.d.ts +1 -1
  117. package/dist/knob-contracts/page-layout-knobs.js +2 -1
  118. package/dist/knob-contracts/popup-knobs.d.ts +1 -1
  119. package/dist/knob-contracts/popup-knobs.js +27 -0
  120. package/dist/knob-contracts/range-knobs.d.ts +1 -1
  121. package/dist/knob-contracts/range-knobs.js +1 -0
  122. package/dist/knob-contracts/rich-tooltip-knobs.d.ts +1 -1
  123. package/dist/knob-contracts/rich-tooltip-knobs.js +2 -1
  124. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  125. package/dist/knob-contracts/sidebar-knobs.js +27 -2
  126. package/dist/knob-contracts/spinner-knobs.d.ts +1 -1
  127. package/dist/knob-contracts/spinner-knobs.js +1 -1
  128. package/dist/knob-contracts/stepper-knobs.d.ts +1 -1
  129. package/dist/knob-contracts/stepper-knobs.js +4 -1
  130. package/dist/knob-contracts/switch-knobs.d.ts +1 -1
  131. package/dist/knob-contracts/switch-knobs.js +5 -0
  132. package/dist/knob-contracts/table-of-contents-knobs.d.ts +1 -1
  133. package/dist/knob-contracts/table-of-contents-knobs.js +2 -1
  134. package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
  135. package/dist/knob-contracts/tabs-knobs.js +1 -0
  136. package/dist/knob-contracts/tree-knobs.d.ts +1 -1
  137. package/dist/knob-contracts/tree-knobs.js +2 -1
  138. package/dist/lib/cn.js +0 -4
  139. package/dist/lib/contrast.d.ts +1 -0
  140. package/dist/lib/contrast.js +17 -5
  141. package/dist/lib/dropzone-validation.js +41 -28
  142. package/dist/lib/env-file.d.ts +1 -1
  143. package/dist/lib/env-file.js +5 -5
  144. package/dist/lib/motion.d.ts +4 -0
  145. package/dist/lib/motion.js +15 -0
  146. package/dist/lib/trigger-detect.js +6 -8
  147. package/dist/markdown-block.js +2 -1
  148. package/dist/popup-parts.d.ts +8 -0
  149. package/dist/popup-parts.js +43 -0
  150. package/dist/source-badge.js +3 -1
  151. package/dist/styles/code.css +0 -2
  152. package/dist/styles/effects.css +6 -32
  153. package/dist/styles/index.css +10 -5
  154. package/dist/styles/recipes/accordion.css +9 -6
  155. package/dist/styles/recipes/action-bar.css +0 -15
  156. package/dist/styles/recipes/activity.css +5 -11
  157. package/dist/styles/recipes/alert.css +4 -4
  158. package/dist/styles/recipes/audio-recorder.css +15 -3
  159. package/dist/styles/recipes/audio-visualizer.css +13 -2
  160. package/dist/styles/recipes/avatar.css +78 -1
  161. package/dist/styles/recipes/badge.css +14 -4
  162. package/dist/styles/recipes/breadcrumb.css +77 -0
  163. package/dist/styles/recipes/button-group.css +5 -7
  164. package/dist/styles/recipes/button-states.css +66 -0
  165. package/dist/styles/recipes/button.css +24 -83
  166. package/dist/styles/recipes/calendar.css +17 -23
  167. package/dist/styles/recipes/card.css +2 -2
  168. package/dist/styles/recipes/chat-composer-attachment.css +9 -3
  169. package/dist/styles/recipes/chat-composer.css +7 -10
  170. package/dist/styles/recipes/chat-layout.css +35 -0
  171. package/dist/styles/recipes/chat-message-social.css +123 -0
  172. package/dist/styles/recipes/chat-message.css +157 -37
  173. package/dist/styles/recipes/choice.css +18 -21
  174. package/dist/styles/recipes/code-diff.css +46 -33
  175. package/dist/styles/recipes/code.css +36 -16
  176. package/dist/styles/recipes/collapsible.css +7 -2
  177. package/dist/styles/recipes/color-picker-details.css +3 -4
  178. package/dist/styles/recipes/color-picker-values.css +9 -8
  179. package/dist/styles/recipes/color-picker.css +11 -2
  180. package/dist/styles/recipes/context.css +3 -3
  181. package/dist/styles/recipes/dockable-panel.css +7 -4
  182. package/dist/styles/recipes/dropzone.css +7 -8
  183. package/dist/styles/recipes/dynamic-notification-input.css +8 -5
  184. package/dist/styles/recipes/dynamic-notification-motion.css +12 -21
  185. package/dist/styles/recipes/dynamic-notification.css +40 -26
  186. package/dist/styles/recipes/emoji-picker.css +132 -0
  187. package/dist/styles/recipes/empty.css +5 -4
  188. package/dist/styles/recipes/environment-variables.css +4 -15
  189. package/dist/styles/recipes/field-parts.css +30 -24
  190. package/dist/styles/recipes/field.css +61 -26
  191. package/dist/styles/recipes/filter-bar.css +18 -31
  192. package/dist/styles/recipes/gradient-editor.css +18 -7
  193. package/dist/styles/recipes/infinite-canvas.css +0 -1
  194. package/dist/styles/recipes/inline-attachment.css +3 -3
  195. package/dist/styles/recipes/inline-citation.css +10 -4
  196. package/dist/styles/recipes/item.css +23 -9
  197. package/dist/styles/recipes/markdown-block.css +5 -4
  198. package/dist/styles/recipes/markdown.css +68 -26
  199. package/dist/styles/recipes/morphing-panel.css +8 -8
  200. package/dist/styles/recipes/page-layout.css +20 -12
  201. package/dist/styles/recipes/pagination.css +4 -5
  202. package/dist/styles/recipes/phone-input.css +12 -8
  203. package/dist/styles/recipes/popup-controls.css +166 -33
  204. package/dist/styles/recipes/popup-drawer.css +3 -14
  205. package/dist/styles/recipes/popup-lists.css +128 -27
  206. package/dist/styles/recipes/popup-motion.css +4 -45
  207. package/dist/styles/recipes/popup-toast.css +55 -26
  208. package/dist/styles/recipes/popup.css +139 -141
  209. package/dist/styles/recipes/range.css +54 -18
  210. package/dist/styles/recipes/resizable.css +2 -7
  211. package/dist/styles/recipes/rich-tooltip.css +24 -34
  212. package/dist/styles/recipes/scroll-area.css +8 -1
  213. package/dist/styles/recipes/sidebar-menu.css +166 -34
  214. package/dist/styles/recipes/sidebar-parts.css +192 -0
  215. package/dist/styles/recipes/sidebar.css +188 -128
  216. package/dist/styles/recipes/source-badge.css +1 -1
  217. package/dist/styles/recipes/spinner.css +8 -1
  218. package/dist/styles/recipes/stepper.css +31 -13
  219. package/dist/styles/recipes/switch.css +68 -23
  220. package/dist/styles/recipes/table-of-contents.css +9 -1
  221. package/dist/styles/recipes/table.css +14 -10
  222. package/dist/styles/recipes/tabs.css +17 -12
  223. package/dist/styles/recipes/task-list.css +7 -14
  224. package/dist/styles/recipes/thread-rail.css +42 -19
  225. package/dist/styles/recipes/timeline.css +2 -0
  226. package/dist/styles/recipes/toolbar.css +27 -25
  227. package/dist/styles/recipes/track-highlight.css +10 -7
  228. package/dist/styles/recipes/tree.css +31 -4
  229. package/dist/styles/recipes/user-ask-details.css +0 -1
  230. package/dist/styles/recipes/user-ask.css +4 -4
  231. package/dist/styles/theme.css +37 -16
  232. package/dist/task-list.d.ts +4 -1
  233. package/dist/task-list.js +24 -8
  234. package/dist/thread-rail.d.ts +1 -1
  235. package/dist/thread-rail.js +61 -19
  236. package/dist/transcript-divider.js +2 -1
  237. package/dist/ui/alert.d.ts +3 -1
  238. package/dist/ui/alert.js +14 -8
  239. package/dist/ui/autocomplete.js +3 -1
  240. package/dist/ui/avatar.d.ts +8 -0
  241. package/dist/ui/avatar.js +23 -3
  242. package/dist/ui/badge.js +1 -1
  243. package/dist/ui/breadcrumb.d.ts +27 -0
  244. package/dist/ui/breadcrumb.js +88 -0
  245. package/dist/ui/button.d.ts +2 -1
  246. package/dist/ui/button.js +1 -1
  247. package/dist/ui/calendar.js +2 -2
  248. package/dist/ui/card.d.ts +3 -1
  249. package/dist/ui/card.js +12 -7
  250. package/dist/ui/checkbox.d.ts +2 -0
  251. package/dist/ui/code-diff.d.ts +1 -1
  252. package/dist/ui/code-diff.js +71 -27
  253. package/dist/ui/code.d.ts +5 -3
  254. package/dist/ui/code.js +26 -27
  255. package/dist/ui/color-picker.d.ts +5 -2
  256. package/dist/ui/color-picker.js +84 -36
  257. package/dist/ui/combobox-disabled-selection.d.ts +1 -2
  258. package/dist/ui/combobox-disabled-selection.js +7 -9
  259. package/dist/ui/combobox.js +13 -5
  260. package/dist/ui/command.d.ts +8 -2
  261. package/dist/ui/command.js +30 -14
  262. package/dist/ui/context-menu.js +3 -0
  263. package/dist/ui/dialog.d.ts +2 -1
  264. package/dist/ui/dialog.js +3 -22
  265. package/dist/ui/dockable-panel-geometry.d.ts +1 -0
  266. package/dist/ui/dockable-panel-geometry.js +7 -1
  267. package/dist/ui/dockable-panel.d.ts +9 -6
  268. package/dist/ui/dockable-panel.js +63 -23
  269. package/dist/ui/dropdown-menu.js +4 -3
  270. package/dist/ui/dropzone.d.ts +2 -2
  271. package/dist/ui/dropzone.js +75 -16
  272. package/dist/ui/emoji-picker.d.ts +18 -0
  273. package/dist/ui/emoji-picker.js +136 -0
  274. package/dist/ui/empty.d.ts +3 -1
  275. package/dist/ui/empty.js +12 -7
  276. package/dist/ui/gradient-editor.d.ts +1 -1
  277. package/dist/ui/gradient-editor.js +37 -21
  278. package/dist/ui/infinite-canvas.d.ts +3 -2
  279. package/dist/ui/infinite-canvas.js +63 -19
  280. package/dist/ui/input-group.d.ts +0 -3
  281. package/dist/ui/input-group.js +3 -13
  282. package/dist/ui/input-otp.d.ts +1 -1
  283. package/dist/ui/input-otp.js +32 -15
  284. package/dist/ui/input.js +4 -4
  285. package/dist/ui/item.d.ts +2 -1
  286. package/dist/ui/item.js +11 -7
  287. package/dist/ui/live-status.d.ts +7 -0
  288. package/dist/ui/live-status.js +17 -0
  289. package/dist/ui/markdown-elements.d.ts +6 -0
  290. package/dist/ui/markdown-elements.js +31 -1
  291. package/dist/ui/menubar.js +2 -2
  292. package/dist/ui/morphing-panel.js +2 -1
  293. package/dist/ui/native-select.d.ts +1 -1
  294. package/dist/ui/native-select.js +24 -11
  295. package/dist/ui/page-layout.d.ts +2 -2
  296. package/dist/ui/page-layout.js +2 -4
  297. package/dist/ui/pagination.d.ts +7 -5
  298. package/dist/ui/pagination.js +19 -13
  299. package/dist/ui/phone-input.d.ts +1 -0
  300. package/dist/ui/phone-input.js +56 -58
  301. package/dist/ui/radio-group.d.ts +2 -0
  302. package/dist/ui/resizable.d.ts +3 -2
  303. package/dist/ui/resizable.js +5 -3
  304. package/dist/ui/responsive-dialog.d.ts +1 -1
  305. package/dist/ui/responsive-dialog.js +4 -21
  306. package/dist/ui/rich-tooltip.d.ts +10 -4
  307. package/dist/ui/rich-tooltip.js +81 -33
  308. package/dist/ui/scroll-area.d.ts +1 -1
  309. package/dist/ui/scroll-area.js +43 -28
  310. package/dist/ui/sidebar-menu.d.ts +5 -1
  311. package/dist/ui/sidebar-menu.js +34 -34
  312. package/dist/ui/sidebar-provider.d.ts +10 -1
  313. package/dist/ui/sidebar-provider.js +38 -13
  314. package/dist/ui/sidebar-resize-rail.js +7 -3
  315. package/dist/ui/sidebar.d.ts +11 -6
  316. package/dist/ui/sidebar.js +48 -24
  317. package/dist/ui/slider.js +31 -22
  318. package/dist/ui/spinner.d.ts +3 -2
  319. package/dist/ui/spinner.js +3 -2
  320. package/dist/ui/stepper.d.ts +8 -1
  321. package/dist/ui/stepper.js +74 -25
  322. package/dist/ui/switch.d.ts +1 -0
  323. package/dist/ui/table-of-contents.d.ts +2 -2
  324. package/dist/ui/table-of-contents.js +20 -8
  325. package/dist/ui/table.js +4 -2
  326. package/dist/ui/textarea.d.ts +1 -1
  327. package/dist/ui/textarea.js +20 -9
  328. package/dist/ui/timeline.d.ts +3 -1
  329. package/dist/ui/timeline.js +17 -9
  330. package/dist/ui/toast.d.ts +11 -5
  331. package/dist/ui/toast.js +100 -54
  332. package/dist/ui/toolbar.d.ts +2 -1
  333. package/dist/ui/toolbar.js +3 -1
  334. package/dist/ui/tooltip.js +3 -20
  335. package/dist/ui/tree.js +120 -80
  336. package/dist/ui/trigger-menu.js +1 -0
  337. package/dist/user-ask.d.ts +10 -5
  338. package/dist/user-ask.js +31 -11
  339. package/package.json +5 -1
  340. package/dist/hooks/use-keyboard-navigation.d.ts +0 -3
  341. package/dist/hooks/use-keyboard-navigation.js +0 -22
@@ -1,6 +1,6 @@
1
1
  import { FormSubmitEvent } from "../control-props.js";
2
- import { ChatComposerKnobStyle } from "../knob-contracts/chat-composer-knobs.js";
3
2
  import { ChatDensity } from "./use-chat-message.js";
3
+ import { ChatComposerKnobStyle } from "../knob-contracts/chat-composer-knobs.js";
4
4
  import { CSSProperties, ComponentProps, ReactNode } from "react";
5
5
  //#region src/hooks/use-chat-composer.d.ts
6
6
  export type ChatComposerSubmitPayload = {
@@ -8,6 +8,7 @@ export type ChatComposerSubmitPayload = {
8
8
  clear: () => void;
9
9
  mentions?: MentionItem[];
10
10
  };
11
+ export type ChatComposerSubmitKey = "enter" | "mod-enter";
11
12
  export type ChatComposerProps = Omit<ComponentProps<"form">, "onSubmit" | "style"> & {
12
13
  children?: ReactNode;
13
14
  value?: string;
@@ -19,6 +20,7 @@ export type ChatComposerProps = Omit<ComponentProps<"form">, "onSubmit" | "style
19
20
  disabled?: boolean;
20
21
  /** Let an empty message send, for composers where an attachment or recording carries the turn. */
21
22
  allowEmptySubmit?: boolean;
23
+ submitKey?: ChatComposerSubmitKey;
22
24
  style?: CSSProperties & ChatComposerKnobStyle;
23
25
  };
24
26
  export type MentionItem = {
@@ -29,23 +31,27 @@ export type MentionItem = {
29
31
  type ComposerKeyEvent = {
30
32
  key: string;
31
33
  shiftKey: boolean;
34
+ metaKey: boolean;
35
+ ctrlKey: boolean;
32
36
  defaultPrevented: boolean;
33
37
  nativeEvent: {
34
38
  isComposing: boolean;
39
+ keyCode: number;
35
40
  };
36
41
  preventDefault: () => void;
37
42
  };
38
- export declare function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state, density, disabled, allowEmptySubmit, trackSends }: Pick<ChatComposerProps, "value" | "defaultValue" | "onValueChange" | "onSubmit" | "state" | "density" | "disabled" | "allowEmptySubmit"> & {
39
- /** Count successful submits — only enabled when something reads counter (send-layer anchor), so idle apps pay no extra state update. */
40
- trackSends?: boolean;
41
- }): {
43
+ type InsertText = (text: string) => void;
44
+ export type RegisterInsertionTarget = (insert: InsertText) => () => void;
45
+ export type ChatComposerController = {
42
46
  value: string;
43
- setValue: (nextValue: string) => void;
47
+ setValue: (value: string) => void;
44
48
  normalizedValue: string;
45
- state: "disabled" | "idle" | "submitting";
49
+ state: NonNullable<ChatComposerProps["state"]>;
46
50
  density: ChatDensity;
51
+ submitKey: ChatComposerSubmitKey;
47
52
  isCompact: boolean;
48
53
  isDisabled: boolean;
54
+ isLocked: boolean;
49
55
  canSubmit: boolean;
50
56
  rows: number;
51
57
  sendCount: number;
@@ -53,5 +59,11 @@ export declare function useChatComposer({ value, defaultValue, onValueChange, on
53
59
  submit: (extra?: Partial<ChatComposerSubmitPayload>) => void;
54
60
  handleSubmit: (event: FormSubmitEvent) => void;
55
61
  handleKeyDown: (event: ComposerKeyEvent) => void;
62
+ insertText: InsertText;
63
+ registerInsertionTarget: RegisterInsertionTarget;
56
64
  };
65
+ export declare function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state, density, disabled, allowEmptySubmit, submitKey, trackSends }: Pick<ChatComposerProps, "value" | "defaultValue" | "onValueChange" | "onSubmit" | "state" | "density" | "disabled" | "allowEmptySubmit" | "submitKey"> & {
66
+ /** Count successful submits — only enabled when something reads counter (send-layer anchor), so idle apps pay no extra state update. */
67
+ trackSends?: boolean;
68
+ }): ChatComposerController;
57
69
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { isComposingKey } from "./use-trigger-menu.js";
1
2
  import { useState } from "react";
2
3
  //#region src/hooks/use-chat-composer.ts
3
4
  function useControllableText({ value, defaultValue = "", onValueChange }) {
@@ -10,16 +11,30 @@ function useControllableText({ value, defaultValue = "", onValueChange }) {
10
11
  }
11
12
  return [currentValue, setValue];
12
13
  }
13
- function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state = "idle", density = "comfortable", disabled = false, allowEmptySubmit = false, trackSends = false }) {
14
+ function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state = "idle", density = "comfortable", disabled = false, allowEmptySubmit = false, submitKey = "enter", trackSends = false }) {
14
15
  const [inputValue, setInputValue] = useControllableText({
15
16
  value,
16
17
  defaultValue,
17
18
  onValueChange
18
19
  });
19
20
  const [sendCount, setSendCount] = useState(0);
21
+ const [insertion] = useState(() => {
22
+ let target = null;
23
+ const register = (insert) => {
24
+ target = insert;
25
+ return () => {
26
+ if (target === insert) target = null;
27
+ };
28
+ };
29
+ return {
30
+ register,
31
+ current: () => target
32
+ };
33
+ });
20
34
  const normalizedValue = inputValue.trim();
21
- const isDisabled = disabled || state === "disabled" || state === "submitting";
22
- const canSubmit = (normalizedValue.length > 0 || allowEmptySubmit) && !isDisabled;
35
+ const isDisabled = disabled || state === "disabled";
36
+ const isLocked = state === "submitting";
37
+ const canSubmit = (normalizedValue.length > 0 || allowEmptySubmit) && !isDisabled && !isLocked;
23
38
  const isCompact = density === "compact";
24
39
  function clear() {
25
40
  setInputValue("");
@@ -38,25 +53,37 @@ function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state =
38
53
  submit();
39
54
  }
40
55
  function handleKeyDown(event) {
41
- if (event.defaultPrevented || event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
56
+ if (event.defaultPrevented || event.key !== "Enter" || event.shiftKey || isComposingKey(event.nativeEvent)) return;
57
+ const modifierHeld = event.metaKey || event.ctrlKey;
58
+ if (submitKey === "mod-enter" && !modifierHeld) return;
42
59
  event.preventDefault();
43
60
  submit();
44
61
  }
62
+ function insertText(text) {
63
+ if (isDisabled || isLocked) return;
64
+ const insertAtCaret = insertion.current();
65
+ if (insertAtCaret) insertAtCaret(text);
66
+ else setInputValue(`${inputValue}${text}`);
67
+ }
45
68
  return {
46
69
  value: inputValue,
47
70
  setValue: setInputValue,
48
71
  normalizedValue,
49
72
  state,
50
73
  density,
74
+ submitKey,
51
75
  isCompact,
52
76
  isDisabled,
77
+ isLocked,
53
78
  canSubmit,
54
79
  rows: isCompact ? 2 : 4,
55
80
  sendCount,
56
81
  clear,
57
82
  submit,
58
83
  handleSubmit,
59
- handleKeyDown
84
+ handleKeyDown,
85
+ insertText,
86
+ registerInsertionTarget: insertion.register
60
87
  };
61
88
  }
62
89
  //#endregion
@@ -1,13 +1,25 @@
1
1
  import { ChatMessageKnobStyle } from "../knob-contracts/chat-message-knobs.js";
2
2
  import { CSSProperties, ComponentProps } from "react";
3
3
  //#region src/hooks/use-chat-message.d.ts
4
- export type ChatRole = "user" | "assistant" | "system" | "tool";
4
+ export type ChatRole = "user" | "assistant" | "participant" | "system" | "tool";
5
5
  export type ChatDensity = "compact" | "comfortable";
6
6
  export type ChatState = "idle" | "streaming" | "pending" | "error";
7
+ export type ChatMessageLayout = "bubble" | "flat";
8
+ /** Transition announcements a message publishes to its thread. */
9
+ export type ChatMessageStatusLabels = {
10
+ pending: string;
11
+ replied: string;
12
+ error: string;
13
+ };
7
14
  export type ChatMessageProps = ComponentProps<"article"> & {
8
15
  from: ChatRole;
9
16
  state?: ChatState;
10
17
  density?: ChatDensity;
18
+ layout?: ChatMessageLayout;
19
+ continuation?: boolean;
20
+ /** Accessible name of the message, so the speaker isn't conveyed by alignment and colour alone. */
21
+ authorLabel?: string;
22
+ statusLabels?: Partial<ChatMessageStatusLabels>;
11
23
  } & {
12
24
  style?: CSSProperties & ChatMessageKnobStyle;
13
25
  };
@@ -15,14 +27,28 @@ export type ChatMessageContext = {
15
27
  from: ChatRole;
16
28
  state: ChatState;
17
29
  density: ChatDensity;
30
+ layout: ChatMessageLayout;
31
+ continuation: boolean;
18
32
  isUser: boolean;
19
33
  isAssistant: boolean;
34
+ isParticipant: boolean;
20
35
  isSystem: boolean;
21
36
  isTool: boolean;
22
37
  isCompact: boolean;
23
38
  isStreaming: boolean;
24
39
  isPending: boolean;
25
40
  isError: boolean;
41
+ isEndAligned: boolean;
42
+ isBubble: boolean;
26
43
  };
27
- export declare function useChatMessage({ from, state, density }: Pick<ChatMessageProps, "from" | "state" | "density">): ChatMessageContext;
44
+ export declare const chatAuthorLabels: Record<ChatRole, string>;
45
+ export declare const chatMessageStatusLabels: ChatMessageStatusLabels;
46
+ export declare const ChatThreadAnnounceContext: import("react").Context<(message: string) => void>;
47
+ /** Publishes a state transition (never streamed text) to the thread's one polite status region; a no-op outside `ChatThread`. */
48
+ export declare function useChatThreadAnnounce(): (message: string) => void;
49
+ export declare const ChatMessageStateProvider: import("react").Provider<ChatMessageContext | null>;
50
+ export declare function useChatMessageContext(): ChatMessageContext;
51
+ /** The announcement for a state change, or null when the change isn't worth announcing. */
52
+ export declare function chatMessageTransition(previous: ChatState, next: ChatState, labels: ChatMessageStatusLabels): string | null;
53
+ export declare function useChatMessage({ from, state, density, layout, continuation }: Pick<ChatMessageProps, "from" | "state" | "density" | "layout" | "continuation">): ChatMessageContext;
28
54
  //#endregion
@@ -1,25 +1,56 @@
1
+ import { createContext, useContext } from "react";
1
2
  //#region src/hooks/use-chat-message.ts
2
- function getChatMessageContext({ from, state, density }) {
3
+ const chatAuthorLabels = {
4
+ user: "You",
5
+ assistant: "Assistant",
6
+ participant: "Participant",
7
+ system: "System",
8
+ tool: "Tool"
9
+ };
10
+ const chatMessageStatusLabels = {
11
+ pending: "Assistant is replying",
12
+ replied: "Assistant replied",
13
+ error: "Reply failed"
14
+ };
15
+ const ChatThreadAnnounceContext = createContext(() => {});
16
+ /** Publishes a state transition (never streamed text) to the thread's one polite status region; a no-op outside `ChatThread`. */
17
+ function useChatThreadAnnounce() {
18
+ return useContext(ChatThreadAnnounceContext);
19
+ }
20
+ const ChatMessageStateContext = createContext(null);
21
+ const ChatMessageStateProvider = ChatMessageStateContext.Provider;
22
+ function useChatMessageContext() {
23
+ const context = useContext(ChatMessageStateContext);
24
+ if (!context) throw new Error("ChatMessage compound components must be rendered inside <ChatMessage>.");
25
+ return context;
26
+ }
27
+ /** The announcement for a state change, or null when the change isn't worth announcing. */
28
+ function chatMessageTransition(previous, next, labels) {
29
+ if (previous === next) return null;
30
+ if (next === "pending") return labels.pending;
31
+ if (next === "error") return labels.error;
32
+ if (next === "idle" && (previous === "pending" || previous === "streaming")) return labels.replied;
33
+ return null;
34
+ }
35
+ function useChatMessage({ from, state = "idle", density = "comfortable", layout = "bubble", continuation = false }) {
3
36
  return {
4
37
  from,
5
38
  state,
6
39
  density,
40
+ layout,
41
+ continuation,
7
42
  isUser: from === "user",
8
43
  isAssistant: from === "assistant",
44
+ isParticipant: from === "participant",
9
45
  isSystem: from === "system",
10
46
  isTool: from === "tool",
11
47
  isCompact: density === "compact",
12
48
  isStreaming: state === "streaming",
13
49
  isPending: state === "pending",
14
- isError: state === "error"
50
+ isError: state === "error",
51
+ isEndAligned: from === "user" && layout === "bubble",
52
+ isBubble: layout === "bubble" && (from === "user" || from === "participant")
15
53
  };
16
54
  }
17
- function useChatMessage({ from, state = "idle", density = "comfortable" }) {
18
- return getChatMessageContext({
19
- from,
20
- state,
21
- density
22
- });
23
- }
24
55
  //#endregion
25
- export { useChatMessage };
56
+ export { ChatMessageStateProvider, ChatThreadAnnounceContext, chatAuthorLabels, chatMessageStatusLabels, chatMessageTransition, useChatMessage, useChatMessageContext, useChatThreadAnnounce };
@@ -1,14 +1,29 @@
1
+ import { prefersReducedMotion } from "../lib/motion.js";
1
2
  import { useEffect, useRef, useState } from "react";
2
3
  //#region src/hooks/use-chat-thread-scroll.ts
3
4
  const BOTTOM_THRESHOLD = 24;
4
5
  const SETTLE_TIMEOUT = 700;
6
+ function firstVisibleChild(list, viewportTop) {
7
+ const children = list.children;
8
+ let low = 0;
9
+ let high = children.length - 1;
10
+ let found;
11
+ while (low <= high) {
12
+ const middle = low + high >> 1;
13
+ if (children[middle].getBoundingClientRect().bottom > viewportTop) {
14
+ found = children[middle];
15
+ high = middle - 1;
16
+ } else low = middle + 1;
17
+ }
18
+ return found;
19
+ }
5
20
  function useChatThreadScroll() {
6
21
  const viewportRef = useRef(null);
7
22
  const contentRef = useRef(null);
8
23
  const pinnedRef = useRef(true);
9
24
  const [atBottom, setAtBottom] = useState(true);
10
25
  const settlingRef = useRef(void 0);
11
- function scrollToBottom(behavior = "smooth") {
26
+ function scrollToBottom(behavior) {
12
27
  const viewport = viewportRef.current;
13
28
  if (!viewport) return;
14
29
  pinnedRef.current = true;
@@ -19,13 +34,29 @@ function useChatThreadScroll() {
19
34
  }, SETTLE_TIMEOUT);
20
35
  viewport.scrollTo({
21
36
  top: viewport.scrollHeight,
22
- behavior
37
+ behavior: behavior ?? (prefersReducedMotion(viewport) ? "auto" : "smooth")
23
38
  });
24
39
  }
25
40
  useEffect(() => {
26
41
  const viewport = viewportRef.current;
27
42
  const content = contentRef.current;
28
- if (!viewport || !content) return;
43
+ const messageList = content?.firstElementChild;
44
+ if (!viewport || !content || !messageList) return;
45
+ let anchor;
46
+ function rememberAnchor() {
47
+ if (!viewport || !messageList) return;
48
+ const viewportTop = viewport.getBoundingClientRect().top;
49
+ const element = firstVisibleChild(messageList, viewportTop);
50
+ anchor = element ? {
51
+ element,
52
+ offset: element.getBoundingClientRect().top - viewportTop
53
+ } : void 0;
54
+ }
55
+ function keepAnchorInPlace() {
56
+ if (!viewport || !anchor?.element.isConnected) return;
57
+ const drift = anchor.element.getBoundingClientRect().top - viewport.getBoundingClientRect().top - anchor.offset;
58
+ if (drift !== 0) viewport.scrollTop += drift;
59
+ }
29
60
  function settle() {
30
61
  clearTimeout(settlingRef.current);
31
62
  settlingRef.current = void 0;
@@ -37,21 +68,26 @@ function useChatThreadScroll() {
37
68
  pinnedRef.current = distance <= BOTTOM_THRESHOLD;
38
69
  setAtBottom(pinnedRef.current);
39
70
  }
71
+ function handleScroll() {
72
+ rememberAnchor();
73
+ readPosition();
74
+ }
40
75
  const observer = new ResizeObserver(() => {
41
76
  if (!viewport) return;
42
77
  if (pinnedRef.current) viewport.scrollTop = viewport.scrollHeight;
43
- else readPosition();
78
+ else keepAnchorInPlace();
79
+ handleScroll();
44
80
  });
45
81
  observer.observe(content);
46
82
  observer.observe(viewport);
47
- viewport.addEventListener("scroll", readPosition, { passive: true });
83
+ viewport.addEventListener("scroll", handleScroll, { passive: true });
48
84
  viewport.addEventListener("scrollend", settle);
49
85
  viewport.scrollTop = viewport.scrollHeight;
50
- readPosition();
86
+ handleScroll();
51
87
  return () => {
52
88
  observer.disconnect();
53
89
  clearTimeout(settlingRef.current);
54
- viewport.removeEventListener("scroll", readPosition);
90
+ viewport.removeEventListener("scroll", handleScroll);
55
91
  viewport.removeEventListener("scrollend", settle);
56
92
  };
57
93
  }, []);
@@ -8,10 +8,10 @@ export type ColorAreaRect = {
8
8
  width: number;
9
9
  height: number;
10
10
  };
11
- export type ColorAreaPointer = {
12
- areaRef: RefObject<HTMLDivElement | null>;
11
+ export type ColorAreaPointer<Element extends HTMLElement> = {
12
+ areaRef: RefObject<Element | null>;
13
13
  dragging: boolean;
14
14
  onPointerDown: (event: PointerEvent) => void;
15
15
  };
16
- export declare function useColorArea(onChange: (offset: ColorAreaOffset, rect: ColorAreaRect) => void): ColorAreaPointer;
16
+ export declare function useColorArea<Element extends HTMLElement = HTMLDivElement>(onChange: (offset: ColorAreaOffset, rect: ColorAreaRect) => void, targetRef?: RefObject<Element | null>): ColorAreaPointer<Element>;
17
17
  //#endregion
@@ -1,41 +1,42 @@
1
1
  "use client";
2
2
  import { useEffect, useEffectEvent, useRef, useState } from "react";
3
3
  //#region src/hooks/use-color-area.ts
4
- function useColorArea(onChange) {
5
- const areaRef = useRef(null);
4
+ function useColorArea(onChange, targetRef) {
5
+ const ownRef = useRef(null);
6
+ const areaRef = targetRef ?? ownRef;
6
7
  const [dragging, setDragging] = useState(false);
7
8
  const emitDragChange = useEffectEvent(onChange);
8
9
  useEffect(() => {
9
10
  if (!dragging) return;
10
- function emit(clientX, clientY) {
11
+ function onMove(event) {
11
12
  const el = areaRef.current;
12
13
  if (!el) return;
13
14
  const rect = el.getBoundingClientRect();
14
15
  emitDragChange({
15
- x: clientX - rect.left,
16
- y: clientY - rect.top
16
+ x: event.clientX - rect.left,
17
+ y: event.clientY - rect.top
17
18
  }, {
18
19
  width: rect.width,
19
20
  height: rect.height
20
21
  });
21
22
  }
22
- function onMove(event) {
23
- emit(event.clientX, event.clientY);
24
- }
25
- function onUp() {
23
+ function onEnd() {
26
24
  setDragging(false);
27
25
  }
28
26
  const previousSelect = document.body.style.userSelect;
29
27
  document.body.style.userSelect = "none";
30
28
  window.addEventListener("pointermove", onMove);
31
- window.addEventListener("pointerup", onUp);
29
+ window.addEventListener("pointerup", onEnd);
30
+ window.addEventListener("pointercancel", onEnd);
32
31
  return () => {
33
32
  window.removeEventListener("pointermove", onMove);
34
- window.removeEventListener("pointerup", onUp);
33
+ window.removeEventListener("pointerup", onEnd);
34
+ window.removeEventListener("pointercancel", onEnd);
35
35
  document.body.style.userSelect = previousSelect;
36
36
  };
37
- }, [dragging]);
37
+ }, [dragging, areaRef]);
38
38
  function onPointerDown(event) {
39
+ if (event.button !== 0) return;
39
40
  const el = areaRef.current;
40
41
  if (el) {
41
42
  const rect = el.getBoundingClientRect();
@@ -5,8 +5,9 @@ export type UseCopyToClipboardOptions = {
5
5
  onCopy?: (value: string) => void;
6
6
  onCopyError?: (error: unknown) => void;
7
7
  };
8
+ export type CopyStatus = "idle" | "copied" | "failed";
8
9
  export type UseCopyToClipboardResult = {
9
- isCopied: boolean;
10
+ status: CopyStatus;
10
11
  copyToClipboard: (value: string) => Promise<boolean>;
11
12
  resetCopied: () => void;
12
13
  };
@@ -17,15 +17,15 @@ function copyViaSelection(value) {
17
17
  textarea.setAttribute("readonly", "");
18
18
  textarea.setAttribute("aria-hidden", "true");
19
19
  textarea.style.position = "fixed";
20
- textarea.style.top = "0";
21
- textarea.style.left = "0";
20
+ textarea.style.insetBlockStart = "0";
21
+ textarea.style.insetInlineStart = "0";
22
22
  textarea.style.width = "1px";
23
23
  textarea.style.height = "1px";
24
24
  textarea.style.opacity = "0";
25
25
  textarea.style.pointerEvents = "none";
26
26
  document.body.appendChild(textarea);
27
27
  try {
28
- textarea.focus();
28
+ textarea.focus({ preventScroll: true });
29
29
  textarea.select();
30
30
  textarea.setSelectionRange(0, value.length);
31
31
  return document.execCommand("copy");
@@ -41,7 +41,7 @@ async function copyTextToClipboard(value) {
41
41
  return copyViaSelection(value);
42
42
  }
43
43
  function useCopyToClipboard({ text, copiedDuration = 1200, onCopy, onCopyError } = {}) {
44
- const [isCopied, setIsCopied] = useState(false);
44
+ const [status, setStatus] = useState("idle");
45
45
  const resetTimeout = useRef(null);
46
46
  useEffect(() => {
47
47
  const timeoutRef = resetTimeout;
@@ -52,38 +52,38 @@ function useCopyToClipboard({ text, copiedDuration = 1200, onCopy, onCopyError }
52
52
  function resetCopied() {
53
53
  if (resetTimeout.current) window.clearTimeout(resetTimeout.current);
54
54
  resetTimeout.current = null;
55
- setIsCopied(false);
55
+ setStatus("idle");
56
+ }
57
+ function settle(next) {
58
+ setStatus(next);
59
+ if (resetTimeout.current) window.clearTimeout(resetTimeout.current);
60
+ resetTimeout.current = next === "copied" ? window.setTimeout(() => setStatus("idle"), copiedDuration) : null;
61
+ return next === "copied";
56
62
  }
57
63
  async function copyToClipboard(value) {
58
64
  if (!await copyTextToClipboard(value)) {
59
65
  onCopyError?.(/* @__PURE__ */ new Error("Unable to copy text"));
60
- setIsCopied(false);
61
- return false;
66
+ return settle("failed");
62
67
  }
63
68
  try {
64
69
  onCopy?.(value);
65
70
  } catch (error) {
66
71
  onCopyError?.(error);
67
- setIsCopied(false);
68
- return false;
72
+ return settle("failed");
69
73
  }
70
- setIsCopied(true);
71
- if (resetTimeout.current) window.clearTimeout(resetTimeout.current);
72
- resetTimeout.current = window.setTimeout(() => setIsCopied(false), copiedDuration);
73
- return true;
74
+ return settle("copied");
74
75
  }
75
76
  function handleCopy() {
76
77
  return text !== void 0 ? copyToClipboard(text) : Promise.resolve(false);
77
78
  }
78
- return text === void 0 ? {
79
- isCopied,
79
+ const state = {
80
+ status,
80
81
  copyToClipboard,
81
82
  resetCopied
82
- } : {
83
- isCopied,
84
- copyToClipboard,
85
- handleCopy,
86
- resetCopied
83
+ };
84
+ return text === void 0 ? state : {
85
+ ...state,
86
+ handleCopy
87
87
  };
88
88
  }
89
89
  //#endregion
@@ -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);