@burtson-labs/ui 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/LICENSES/DEPENDENCIES.txt +1 -1
  2. package/README.md +15 -11
  3. package/dist/components/accordion.d.ts +3 -3
  4. package/dist/components/accordion.js +13 -10
  5. package/dist/components/accordion.js.map +1 -1
  6. package/dist/components/agent-run.d.ts +22 -0
  7. package/dist/components/agent-run.js +131 -0
  8. package/dist/components/agent-run.js.map +1 -0
  9. package/dist/components/alert-dialog.d.ts +12 -10
  10. package/dist/components/alert-dialog.js +29 -20
  11. package/dist/components/alert-dialog.js.map +1 -1
  12. package/dist/components/alert.d.ts +5 -3
  13. package/dist/components/alert.js +11 -7
  14. package/dist/components/alert.js.map +1 -1
  15. package/dist/components/app-shell.d.ts +7 -6
  16. package/dist/components/app-shell.js +21 -14
  17. package/dist/components/app-shell.js.map +1 -1
  18. package/dist/components/attachment.d.ts +3 -3
  19. package/dist/components/attachment.js +10 -7
  20. package/dist/components/attachment.js.map +1 -1
  21. package/dist/components/audio-player.d.ts +2 -2
  22. package/dist/components/audio-player.js +8 -6
  23. package/dist/components/audio-player.js.map +1 -1
  24. package/dist/components/avatar-upload.d.ts +19 -0
  25. package/dist/components/avatar-upload.js +359 -0
  26. package/dist/components/avatar-upload.js.map +1 -0
  27. package/dist/components/avatar.d.ts +3 -3
  28. package/dist/components/avatar.js +10 -7
  29. package/dist/components/avatar.js.map +1 -1
  30. package/dist/components/badge.d.ts +1 -1
  31. package/dist/components/badge.js +12 -10
  32. package/dist/components/badge.js.map +1 -1
  33. package/dist/components/breadcrumb.d.ts +8 -8
  34. package/dist/components/breadcrumb.js +24 -17
  35. package/dist/components/breadcrumb.js.map +1 -1
  36. package/dist/components/button.d.ts +8 -1
  37. package/dist/components/button.js +12 -3
  38. package/dist/components/button.js.map +1 -1
  39. package/dist/components/card.d.ts +8 -8
  40. package/dist/components/card.js +32 -23
  41. package/dist/components/card.js.map +1 -1
  42. package/dist/components/chat-history.d.ts +1 -1
  43. package/dist/components/chat-history.js +10 -9
  44. package/dist/components/chat-history.js.map +1 -1
  45. package/dist/components/chat-layout.d.ts +1 -1
  46. package/dist/components/chat-layout.js +7 -3
  47. package/dist/components/chat-layout.js.map +1 -1
  48. package/dist/components/checkbox-card.d.ts +2 -2
  49. package/dist/components/checkbox-card.js +9 -7
  50. package/dist/components/checkbox-card.js.map +1 -1
  51. package/dist/components/checkbox.d.ts +23 -2
  52. package/dist/components/checkbox.js +48 -6
  53. package/dist/components/checkbox.js.map +1 -1
  54. package/dist/components/collapsible.d.ts +2 -2
  55. package/dist/components/collapsible.js +7 -5
  56. package/dist/components/collapsible.js.map +1 -1
  57. package/dist/components/combobox.d.ts +14 -4
  58. package/dist/components/combobox.js +18 -8
  59. package/dist/components/combobox.js.map +1 -1
  60. package/dist/components/command.d.ts +68 -12
  61. package/dist/components/command.js +38 -25
  62. package/dist/components/command.js.map +1 -1
  63. package/dist/components/composer.d.ts +2 -2
  64. package/dist/components/composer.js +9 -7
  65. package/dist/components/composer.js.map +1 -1
  66. package/dist/components/connection-status.d.ts +3 -3
  67. package/dist/components/connection-status.js +18 -14
  68. package/dist/components/connection-status.js.map +1 -1
  69. package/dist/components/context-menu.d.ts +14 -13
  70. package/dist/components/context-menu.js +33 -23
  71. package/dist/components/context-menu.js.map +1 -1
  72. package/dist/components/conversation.d.ts +1 -1
  73. package/dist/components/conversation.js +6 -5
  74. package/dist/components/conversation.js.map +1 -1
  75. package/dist/components/copy-button.d.ts +1 -1
  76. package/dist/components/copy-button.js +3 -2
  77. package/dist/components/copy-button.js.map +1 -1
  78. package/dist/components/data-table.d.ts +7 -2
  79. package/dist/components/data-table.js +16 -42
  80. package/dist/components/data-table.js.map +1 -1
  81. package/dist/components/dialog.d.ts +18 -9
  82. package/dist/components/dialog.js +38 -22
  83. package/dist/components/dialog.js.map +1 -1
  84. package/dist/components/dropdown-menu.d.ts +14 -13
  85. package/dist/components/dropdown-menu.js +33 -23
  86. package/dist/components/dropdown-menu.js.map +1 -1
  87. package/dist/components/editor-tabs.d.ts +1 -1
  88. package/dist/components/editor-tabs.js +6 -5
  89. package/dist/components/editor-tabs.js.map +1 -1
  90. package/dist/components/empty-state.d.ts +5 -5
  91. package/dist/components/empty-state.js +16 -11
  92. package/dist/components/empty-state.js.map +1 -1
  93. package/dist/components/error-summary.d.ts +21 -0
  94. package/dist/components/error-summary.js +62 -0
  95. package/dist/components/error-summary.js.map +1 -0
  96. package/dist/components/field.d.ts +66 -7
  97. package/dist/components/field.js +151 -20
  98. package/dist/components/field.js.map +1 -1
  99. package/dist/components/form-actions.d.ts +18 -0
  100. package/dist/components/form-actions.js +30 -0
  101. package/dist/components/form-actions.js.map +1 -0
  102. package/dist/components/icon-button.d.ts +3 -1
  103. package/dist/components/icon-button.js +5 -3
  104. package/dist/components/icon-button.js.map +1 -1
  105. package/dist/components/input.d.ts +42 -7
  106. package/dist/components/input.js +53 -10
  107. package/dist/components/input.js.map +1 -1
  108. package/dist/components/kbd.d.ts +2 -2
  109. package/dist/components/kbd.js +7 -5
  110. package/dist/components/kbd.js.map +1 -1
  111. package/dist/components/label.d.ts +6 -1
  112. package/dist/components/label.js +10 -4
  113. package/dist/components/label.js.map +1 -1
  114. package/dist/components/markdown.d.ts +4 -3
  115. package/dist/components/markdown.js +7 -6
  116. package/dist/components/markdown.js.map +1 -1
  117. package/dist/components/menubar.d.ts +16 -14
  118. package/dist/components/menubar.js +39 -27
  119. package/dist/components/menubar.js.map +1 -1
  120. package/dist/components/message-actions.d.ts +3 -3
  121. package/dist/components/message-actions.js +15 -12
  122. package/dist/components/message-actions.js.map +1 -1
  123. package/dist/components/message.d.ts +1 -1
  124. package/dist/components/message.js +5 -3
  125. package/dist/components/message.js.map +1 -1
  126. package/dist/components/mobile-nav.d.ts +1 -1
  127. package/dist/components/mobile-nav.js +6 -5
  128. package/dist/components/mobile-nav.js.map +1 -1
  129. package/dist/components/native-select.d.ts +16 -0
  130. package/dist/components/native-select.js +33 -0
  131. package/dist/components/native-select.js.map +1 -0
  132. package/dist/components/navigation-menu.d.ts +12 -11
  133. package/dist/components/navigation-menu.js +32 -23
  134. package/dist/components/navigation-menu.js.map +1 -1
  135. package/dist/components/number-input.d.ts +23 -0
  136. package/dist/components/number-input.js +45 -0
  137. package/dist/components/number-input.js.map +1 -0
  138. package/dist/components/onboarding-checklist.d.ts +1 -1
  139. package/dist/components/onboarding-checklist.js +4 -3
  140. package/dist/components/onboarding-checklist.js.map +1 -1
  141. package/dist/components/page-header.d.ts +7 -6
  142. package/dist/components/page-header.js +22 -15
  143. package/dist/components/page-header.js.map +1 -1
  144. package/dist/components/pagination.d.ts +1 -1
  145. package/dist/components/pagination.js +3 -2
  146. package/dist/components/pagination.js.map +1 -1
  147. package/dist/components/popover.d.ts +13 -10
  148. package/dist/components/popover.js +23 -17
  149. package/dist/components/popover.js.map +1 -1
  150. package/dist/components/progress.d.ts +1 -1
  151. package/dist/components/progress.js +6 -5
  152. package/dist/components/progress.js.map +1 -1
  153. package/dist/components/radio-group.d.ts +22 -3
  154. package/dist/components/radio-group.js +51 -10
  155. package/dist/components/radio-group.js.map +1 -1
  156. package/dist/components/reasoning.d.ts +3 -3
  157. package/dist/components/reasoning.js +9 -7
  158. package/dist/components/reasoning.js.map +1 -1
  159. package/dist/components/scroll-area.d.ts +3 -2
  160. package/dist/components/scroll-area.js +10 -7
  161. package/dist/components/scroll-area.js.map +1 -1
  162. package/dist/components/secret-input.d.ts +3 -2
  163. package/dist/components/secret-input.js +6 -4
  164. package/dist/components/secret-input.js.map +1 -1
  165. package/dist/components/select.d.ts +11 -6
  166. package/dist/components/select.js +24 -18
  167. package/dist/components/select.js.map +1 -1
  168. package/dist/components/separator.d.ts +1 -1
  169. package/dist/components/separator.js +4 -3
  170. package/dist/components/separator.js.map +1 -1
  171. package/dist/components/sheet.d.ts +10 -8
  172. package/dist/components/sheet.js +31 -20
  173. package/dist/components/sheet.js.map +1 -1
  174. package/dist/components/skeleton.d.ts +2 -1
  175. package/dist/components/skeleton.js +6 -4
  176. package/dist/components/skeleton.js.map +1 -1
  177. package/dist/components/slider.d.ts +10 -6
  178. package/dist/components/slider.js +15 -7
  179. package/dist/components/slider.js.map +1 -1
  180. package/dist/components/source.d.ts +2 -2
  181. package/dist/components/source.js +9 -7
  182. package/dist/components/source.js.map +1 -1
  183. package/dist/components/spinner.d.ts +4 -2
  184. package/dist/components/spinner.js +4 -3
  185. package/dist/components/spinner.js.map +1 -1
  186. package/dist/components/stat-card.d.ts +27 -2
  187. package/dist/components/stat-card.js +69 -9
  188. package/dist/components/stat-card.js.map +1 -1
  189. package/dist/components/status.d.ts +7 -6
  190. package/dist/components/status.js +11 -5
  191. package/dist/components/status.js.map +1 -1
  192. package/dist/components/steps.d.ts +1 -1
  193. package/dist/components/steps.js +4 -3
  194. package/dist/components/steps.js.map +1 -1
  195. package/dist/components/switch.d.ts +46 -2
  196. package/dist/components/switch.js +85 -8
  197. package/dist/components/switch.js.map +1 -1
  198. package/dist/components/table.d.ts +29 -12
  199. package/dist/components/table.js +79 -34
  200. package/dist/components/table.js.map +1 -1
  201. package/dist/components/tabs.d.ts +9 -4
  202. package/dist/components/tabs.js +22 -13
  203. package/dist/components/tabs.js.map +1 -1
  204. package/dist/components/textarea.d.ts +6 -1
  205. package/dist/components/textarea.js +3 -3
  206. package/dist/components/textarea.js.map +1 -1
  207. package/dist/components/toast.d.ts +10 -6
  208. package/dist/components/toast.js +27 -18
  209. package/dist/components/toast.js.map +1 -1
  210. package/dist/components/toaster.d.ts +4 -2
  211. package/dist/components/toaster.js +6 -2
  212. package/dist/components/toaster.js.map +1 -1
  213. package/dist/components/tool-call.d.ts +3 -3
  214. package/dist/components/tool-call.js +16 -12
  215. package/dist/components/tool-call.js.map +1 -1
  216. package/dist/components/toolbar.d.ts +9 -4
  217. package/dist/components/toolbar.js +14 -9
  218. package/dist/components/toolbar.js.map +1 -1
  219. package/dist/components/tooltip.d.ts +3 -2
  220. package/dist/components/tooltip.js +7 -4
  221. package/dist/components/tooltip.js.map +1 -1
  222. package/dist/components/tour.js +2 -2
  223. package/dist/components/tour.js.map +1 -1
  224. package/dist/components/tree-view.d.ts +1 -1
  225. package/dist/components/tree-view.js +9 -5
  226. package/dist/components/tree-view.js.map +1 -1
  227. package/dist/components/voice-recorder.d.ts +1 -1
  228. package/dist/components/voice-recorder.js +4 -2
  229. package/dist/components/voice-recorder.js.map +1 -1
  230. package/dist/index.d.ts +8 -1
  231. package/dist/index.js +19 -12
  232. package/dist/lib/media.d.ts +18 -0
  233. package/dist/lib/media.js +34 -0
  234. package/dist/lib/media.js.map +1 -0
  235. package/dist/lib/utils.d.ts +32 -0
  236. package/dist/lib/utils.js +33 -1
  237. package/dist/lib/utils.js.map +1 -1
  238. package/dist/mui/index.js +20 -8
  239. package/dist/mui/index.js.map +1 -1
  240. package/dist/styles.css +1 -1
  241. package/dist/theme.css +36 -17
  242. package/dist/tokens.d.ts +78 -36
  243. package/dist/tokens.js +63 -32
  244. package/dist/tokens.js.map +1 -1
  245. package/package.json +5 -3
@@ -1,8 +1,9 @@
1
1
  import * as React from 'react';
2
- declare function AppShell({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
3
- declare function AppShellHeader({ className, ...props }: React.ComponentProps<'header'>): React.JSX.Element;
4
- declare function AppShellBody({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
5
- declare function AppShellSidebar({ className, ...props }: React.ComponentProps<'aside'>): React.JSX.Element;
6
- declare function AppShellMain({ className, ...props }: React.ComponentProps<'main'>): React.JSX.Element;
7
- declare function AppShellContent({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
2
+ declare const AppShell: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
3
+ /** The top bar: sticky, over the page (z 40), clear of the notch. */
4
+ declare const AppShellHeader: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & React.RefAttributes<HTMLElement>>;
5
+ declare const AppShellBody: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
6
+ declare const AppShellSidebar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & React.RefAttributes<HTMLElement>>;
7
+ declare const AppShellMain: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & React.RefAttributes<HTMLElement>>;
8
+ declare const AppShellContent: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
8
9
  export { AppShell, AppShellBody, AppShellContent, AppShellHeader, AppShellMain, AppShellSidebar };
@@ -1,50 +1,57 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
- import "react";
3
+ import * as React from "react";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/components/app-shell.tsx
6
- function AppShell({ className, ...props }) {
6
+ var AppShell = React.forwardRef(function AppShell({ className, ...props }, ref) {
7
7
  return /* @__PURE__ */ jsx("div", {
8
+ ref,
8
9
  "data-slot": "app-shell",
9
10
  className: cn("grid min-h-dvh grid-rows-[auto_1fr] bg-background text-foreground", className),
10
11
  ...props
11
12
  });
12
- }
13
- function AppShellHeader({ className, ...props }) {
13
+ });
14
+ /** The top bar: sticky, over the page (z 40), clear of the notch. */
15
+ var AppShellHeader = React.forwardRef(function AppShellHeader({ className, ...props }, ref) {
14
16
  return /* @__PURE__ */ jsx("header", {
17
+ ref,
15
18
  "data-slot": "app-shell-header",
16
- className: cn("sticky top-0 z-40 flex h-12 items-center border-b border-border bg-background/90 px-3 backdrop-blur-xl sm:px-4", className),
19
+ className: cn("sticky top-0 z-40 flex h-12 items-center border-b border-border bg-background/90 px-3 pt-[env(safe-area-inset-top)] backdrop-blur-xl sm:px-4", className),
17
20
  ...props
18
21
  });
19
- }
20
- function AppShellBody({ className, ...props }) {
22
+ });
23
+ var AppShellBody = React.forwardRef(function AppShellBody({ className, ...props }, ref) {
21
24
  return /* @__PURE__ */ jsx("div", {
25
+ ref,
22
26
  "data-slot": "app-shell-body",
23
27
  className: cn("grid min-h-0 grid-cols-1", className),
24
28
  ...props
25
29
  });
26
- }
27
- function AppShellSidebar({ className, ...props }) {
30
+ });
31
+ var AppShellSidebar = React.forwardRef(function AppShellSidebar({ className, ...props }, ref) {
28
32
  return /* @__PURE__ */ jsx("aside", {
33
+ ref,
29
34
  "data-slot": "app-shell-sidebar",
30
35
  className: cn("min-h-0 border-r border-border bg-surface-muted/55", className),
31
36
  ...props
32
37
  });
33
- }
34
- function AppShellMain({ className, ...props }) {
38
+ });
39
+ var AppShellMain = React.forwardRef(function AppShellMain({ className, ...props }, ref) {
35
40
  return /* @__PURE__ */ jsx("main", {
41
+ ref,
36
42
  "data-slot": "app-shell-main",
37
43
  className: cn("min-w-0 bg-background", className),
38
44
  ...props
39
45
  });
40
- }
41
- function AppShellContent({ className, ...props }) {
46
+ });
47
+ var AppShellContent = React.forwardRef(function AppShellContent({ className, ...props }, ref) {
42
48
  return /* @__PURE__ */ jsx("div", {
49
+ ref,
43
50
  "data-slot": "app-shell-content",
44
51
  className: cn("mx-auto w-full max-w-[1600px] p-4 sm:p-6", className),
45
52
  ...props
46
53
  });
47
- }
54
+ });
48
55
  //#endregion
49
56
  export { AppShell, AppShellBody, AppShellContent, AppShellHeader, AppShellMain, AppShellSidebar };
50
57
 
@@ -1 +1 @@
1
- {"version":3,"file":"app-shell.js","names":[],"sources":["../../src/components/app-shell.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nfunction AppShell({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"app-shell\"\n className={cn('grid min-h-dvh grid-rows-[auto_1fr] bg-background text-foreground', className)}\n {...props}\n />\n );\n}\n\nfunction AppShellHeader({ className, ...props }: React.ComponentProps<'header'>) {\n return (\n <header\n data-slot=\"app-shell-header\"\n className={cn(\n 'sticky top-0 z-40 flex h-12 items-center border-b border-border bg-background/90 px-3 backdrop-blur-xl sm:px-4',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AppShellBody({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"app-shell-body\"\n className={cn('grid min-h-0 grid-cols-1', className)}\n {...props}\n />\n );\n}\n\nfunction AppShellSidebar({ className, ...props }: React.ComponentProps<'aside'>) {\n return (\n <aside\n data-slot=\"app-shell-sidebar\"\n className={cn('min-h-0 border-r border-border bg-surface-muted/55', className)}\n {...props}\n />\n );\n}\n\nfunction AppShellMain({ className, ...props }: React.ComponentProps<'main'>) {\n return (\n <main\n data-slot=\"app-shell-main\"\n className={cn('min-w-0 bg-background', className)}\n {...props}\n />\n );\n}\n\nfunction AppShellContent({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"app-shell-content\"\n className={cn('mx-auto w-full max-w-[1600px] p-4 sm:p-6', className)}\n {...props}\n />\n );\n}\n\nexport { AppShell, AppShellBody, AppShellContent, AppShellHeader, AppShellMain, AppShellSidebar };\n"],"mappings":";;;;;AAIA,SAAS,SAAS,EAAE,WAAW,GAAG,SAAsC;CACtE,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,qEAAqE,SAAS;EAC5F,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,eAAe,EAAE,WAAW,GAAG,SAAyC;CAC/E,OACE,oBAAC,UAAD;EACE,aAAU;EACV,WAAW,GACT,kHACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,SAAsC;CAC1E,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,4BAA4B,SAAS;EACnD,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,gBAAgB,EAAE,WAAW,GAAG,SAAwC;CAC/E,OACE,oBAAC,SAAD;EACE,aAAU;EACV,WAAW,GAAG,sDAAsD,SAAS;EAC7E,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,SAAuC;CAC3E,OACE,oBAAC,QAAD;EACE,aAAU;EACV,WAAW,GAAG,yBAAyB,SAAS;EAChD,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,gBAAgB,EAAE,WAAW,GAAG,SAAsC;CAC7E,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,4CAA4C,SAAS;EACnE,GAAI;CACL,CAAA;AAEL"}
1
+ {"version":3,"file":"app-shell.js","names":[],"sources":["../../src/components/app-shell.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nconst AppShell = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(function AppShell(\n { className, ...props },\n ref,\n) {\n return (\n <div\n ref={ref}\n data-slot=\"app-shell\"\n className={cn('grid min-h-dvh grid-rows-[auto_1fr] bg-background text-foreground', className)}\n {...props}\n />\n );\n});\n\n/** The top bar: sticky, over the page (z 40), clear of the notch. */\nconst AppShellHeader = React.forwardRef<HTMLElement, React.ComponentProps<'header'>>(\n function AppShellHeader({ className, ...props }, ref) {\n return (\n <header\n ref={ref}\n data-slot=\"app-shell-header\"\n className={cn(\n 'sticky top-0 z-40 flex h-12 items-center border-b border-border bg-background/90 px-3 pt-[env(safe-area-inset-top)] backdrop-blur-xl sm:px-4',\n className,\n )}\n {...props}\n />\n );\n },\n);\n\nconst AppShellBody = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(\n function AppShellBody({ className, ...props }, ref) {\n return (\n <div\n ref={ref}\n data-slot=\"app-shell-body\"\n className={cn('grid min-h-0 grid-cols-1', className)}\n {...props}\n />\n );\n },\n);\n\nconst AppShellSidebar = React.forwardRef<HTMLElement, React.ComponentProps<'aside'>>(\n function AppShellSidebar({ className, ...props }, ref) {\n return (\n <aside\n ref={ref}\n data-slot=\"app-shell-sidebar\"\n className={cn('min-h-0 border-r border-border bg-surface-muted/55', className)}\n {...props}\n />\n );\n },\n);\n\nconst AppShellMain = React.forwardRef<HTMLElement, React.ComponentProps<'main'>>(\n function AppShellMain({ className, ...props }, ref) {\n return (\n <main\n ref={ref}\n data-slot=\"app-shell-main\"\n className={cn('min-w-0 bg-background', className)}\n {...props}\n />\n );\n },\n);\n\nconst AppShellContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(\n function AppShellContent({ className, ...props }, ref) {\n return (\n <div\n ref={ref}\n data-slot=\"app-shell-content\"\n className={cn('mx-auto w-full max-w-[1600px] p-4 sm:p-6', className)}\n {...props}\n />\n );\n },\n);\n\nexport { AppShell, AppShellBody, AppShellContent, AppShellHeader, AppShellMain, AppShellSidebar };\n"],"mappings":";;;;;AAIA,IAAM,WAAW,MAAM,WAAwD,SAAS,SACtF,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAAC,OAAD;EACO;EACL,aAAU;EACV,WAAW,GAAG,qEAAqE,SAAS;EAC5F,GAAI;CACL,CAAA;AAEL,CAAC;;AAGD,IAAM,iBAAiB,MAAM,WAC3B,SAAS,eAAe,EAAE,WAAW,GAAG,SAAS,KAAK;CACpD,OACE,oBAAC,UAAD;EACO;EACL,aAAU;EACV,WAAW,GACT,gJACA,SACF;EACA,GAAI;CACL,CAAA;AAEL,CACF;AAEA,IAAM,eAAe,MAAM,WACzB,SAAS,aAAa,EAAE,WAAW,GAAG,SAAS,KAAK;CAClD,OACE,oBAAC,OAAD;EACO;EACL,aAAU;EACV,WAAW,GAAG,4BAA4B,SAAS;EACnD,GAAI;CACL,CAAA;AAEL,CACF;AAEA,IAAM,kBAAkB,MAAM,WAC5B,SAAS,gBAAgB,EAAE,WAAW,GAAG,SAAS,KAAK;CACrD,OACE,oBAAC,SAAD;EACO;EACL,aAAU;EACV,WAAW,GAAG,sDAAsD,SAAS;EAC7E,GAAI;CACL,CAAA;AAEL,CACF;AAEA,IAAM,eAAe,MAAM,WACzB,SAAS,aAAa,EAAE,WAAW,GAAG,SAAS,KAAK;CAClD,OACE,oBAAC,QAAD;EACO;EACL,aAAU;EACV,WAAW,GAAG,yBAAyB,SAAS;EAChD,GAAI;CACL,CAAA;AAEL,CACF;AAEA,IAAM,kBAAkB,MAAM,WAC5B,SAAS,gBAAgB,EAAE,WAAW,GAAG,SAAS,KAAK;CACrD,OACE,oBAAC,OAAD;EACO;EACL,aAAU;EACV,WAAW,GAAG,4CAA4C,SAAS;EACnE,GAAI;CACL,CAAA;AAEL,CACF"}
@@ -28,13 +28,13 @@ export interface AttachmentItemProps extends Omit<React.ComponentProps<'div'>, '
28
28
  * One attached file: name, size, state, progress, and remove/retry. State
29
29
  * changes are announced politely; a failure says why.
30
30
  */
31
- declare function AttachmentItem({ name, size, state, progress, error, preview, icon, onRemove, onRetry, layout, className, ...props }: AttachmentItemProps): React.JSX.Element;
31
+ declare const AttachmentItem: React.ForwardRefExoticComponent<Omit<AttachmentItemProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
32
32
  export interface AttachmentTrayProps extends React.ComponentProps<'div'> {
33
33
  /** Accessible name for the list. */
34
34
  label?: string;
35
35
  }
36
36
  /** Attachments waiting to send, in a row that scrolls sideways when full. */
37
- declare function AttachmentTray({ label, className, children, ...props }: AttachmentTrayProps): React.JSX.Element;
37
+ declare const AttachmentTray: React.ForwardRefExoticComponent<Omit<AttachmentTrayProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
38
38
  export interface UploadQueueFile {
39
39
  id: string;
40
40
  name: string;
@@ -52,5 +52,5 @@ export interface UploadQueueProps extends Omit<React.ComponentProps<'section'>,
52
52
  empty?: React.ReactNode;
53
53
  }
54
54
  /** A list of uploads with a summary: "2 of 4 ready, 1 failed". */
55
- declare function UploadQueue({ files, title, onRemove, onRetry, empty, className, ...props }: UploadQueueProps): React.JSX.Element;
55
+ declare const UploadQueue: React.ForwardRefExoticComponent<Omit<UploadQueueProps, "ref"> & React.RefAttributes<HTMLElement>>;
56
56
  export { AttachmentItem, AttachmentTray, UploadQueue };
@@ -5,10 +5,10 @@ import { Progress } from "./progress.js";
5
5
  import { Spinner } from "./spinner.js";
6
6
  import * as React from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
+ import X from "@burtson-labs/icons/react/x";
8
9
  import AlertCircle from "@burtson-labs/icons/react/alert-circle";
9
10
  import FileText from "@burtson-labs/icons/react/file-text";
10
11
  import RotateCw from "@burtson-labs/icons/react/rotate-cw";
11
- import X from "@burtson-labs/icons/react/x";
12
12
  //#region src/components/attachment.tsx
13
13
  var stateText = {
14
14
  queued: "Waiting",
@@ -40,12 +40,13 @@ var InTray = React.createContext(false);
40
40
  * One attached file: name, size, state, progress, and remove/retry. State
41
41
  * changes are announced politely; a failure says why.
42
42
  */
43
- function AttachmentItem({ name, size, state = "ready", progress, error, preview, icon, onRemove, onRetry, layout = "chip", className, ...props }) {
43
+ var AttachmentItem = React.forwardRef(function AttachmentItem({ name, size, state = "ready", progress, error, preview, icon, onRemove, onRetry, layout = "chip", className, ...props }, ref) {
44
44
  const inTray = React.useContext(InTray);
45
45
  const busy = state === "uploading" || state === "parsing";
46
46
  const failed = state === "failed";
47
47
  const meta = [size !== void 0 ? formatBytes(size) : null, stateText[state]].filter(Boolean).join(" · ");
48
48
  return /* @__PURE__ */ jsxs("div", {
49
+ ref,
49
50
  "data-slot": "attachment-item",
50
51
  "data-state": state,
51
52
  "data-layout": layout,
@@ -112,12 +113,13 @@ function AttachmentItem({ name, size, state = "ready", progress, error, preview,
112
113
  })
113
114
  ]
114
115
  });
115
- }
116
+ });
116
117
  /** Attachments waiting to send, in a row that scrolls sideways when full. */
117
- function AttachmentTray({ label = "Attachments", className, children, ...props }) {
118
+ var AttachmentTray = React.forwardRef(function AttachmentTray({ label = "Attachments", className, children, ...props }, ref) {
118
119
  return /* @__PURE__ */ jsx(InTray.Provider, {
119
120
  value: true,
120
121
  children: /* @__PURE__ */ jsx("div", {
122
+ ref,
121
123
  "data-slot": "attachment-tray",
122
124
  role: "list",
123
125
  "aria-label": label,
@@ -126,14 +128,15 @@ function AttachmentTray({ label = "Attachments", className, children, ...props }
126
128
  children
127
129
  })
128
130
  });
129
- }
131
+ });
130
132
  /** A list of uploads with a summary: "2 of 4 ready, 1 failed". */
131
- function UploadQueue({ files, title = "Uploads", onRemove, onRetry, empty, className, ...props }) {
133
+ var UploadQueue = React.forwardRef(function UploadQueue({ files, title = "Uploads", onRemove, onRetry, empty, className, ...props }, ref) {
132
134
  const headingId = React.useId();
133
135
  const ready = files.filter((f) => f.state === "ready").length;
134
136
  const failed = files.filter((f) => f.state === "failed").length;
135
137
  const summary = files.length ? `${ready} of ${files.length} ready${failed ? `, ${failed} failed` : ""}` : "";
136
138
  return /* @__PURE__ */ jsxs("section", {
139
+ ref,
137
140
  "data-slot": "upload-queue",
138
141
  "aria-labelledby": headingId,
139
142
  className: cn("grid gap-2", className),
@@ -170,7 +173,7 @@ function UploadQueue({ files, title = "Uploads", onRemove, onRetry, empty, class
170
173
  })
171
174
  })]
172
175
  });
173
- }
176
+ });
174
177
  //#endregion
175
178
  export { AttachmentItem, AttachmentTray, UploadQueue, formatBytes };
176
179
 
@@ -1 +1 @@
1
- {"version":3,"file":"attachment.js","names":[],"sources":["../../src/components/attachment.tsx"],"sourcesContent":["import AlertCircle from '@burtson-labs/icons/react/alert-circle';\nimport FileText from '@burtson-labs/icons/react/file-text';\nimport RotateCw from '@burtson-labs/icons/react/rotate-cw';\nimport X from '@burtson-labs/icons/react/x';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\nimport { Progress } from './progress';\nimport { Spinner } from './spinner';\n\n/**\n * Where a file is on its way into a message. The app owns the upload and the\n * parsing; these components only show the state it reports.\n */\nexport type AttachmentState = 'queued' | 'uploading' | 'parsing' | 'ready' | 'failed';\n\nconst stateText: Record<AttachmentState, string> = {\n queued: 'Waiting',\n uploading: 'Uploading',\n parsing: 'Reading',\n ready: 'Ready',\n failed: 'Failed',\n};\n\n/** 1536 → \"1.5 KB\". */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;\n}\n\nconst InTray = React.createContext(false);\n\nexport interface AttachmentItemProps extends Omit<React.ComponentProps<'div'>, 'children'> {\n name: string;\n /** Bytes. */\n size?: number;\n state?: AttachmentState;\n /** 0–100 while uploading or reading. Leave out for an indeterminate bar. */\n progress?: number;\n /** Why it failed, in words the person can act on. */\n error?: string;\n /** A thumbnail URL for images. */\n preview?: string;\n /** Replaces the file icon. */\n icon?: React.ReactNode;\n onRemove?: () => void;\n onRetry?: () => void;\n /** `chip` sits in a composer; `row` fills an upload list. */\n layout?: 'chip' | 'row';\n}\n\n/**\n * One attached file: name, size, state, progress, and remove/retry. State\n * changes are announced politely; a failure says why.\n */\nfunction AttachmentItem({\n name,\n size,\n state = 'ready',\n progress,\n error,\n preview,\n icon,\n onRemove,\n onRetry,\n layout = 'chip',\n className,\n ...props\n}: AttachmentItemProps) {\n const inTray = React.useContext(InTray);\n const busy = state === 'uploading' || state === 'parsing';\n const failed = state === 'failed';\n const meta = [size !== undefined ? formatBytes(size) : null, stateText[state]]\n .filter(Boolean)\n .join(' · ');\n return (\n <div\n data-slot=\"attachment-item\"\n data-state={state}\n data-layout={layout}\n role={inTray ? 'listitem' : undefined}\n aria-busy={busy || undefined}\n className={cn(\n 'group/attachment relative flex min-w-0 items-center gap-2.5 rounded-md border bg-surface text-sm',\n layout === 'chip' ? 'w-60 max-w-full shrink-0 p-1.5 pr-1' : 'w-full p-2.5',\n failed && 'border-destructive/40 bg-destructive/5',\n className,\n )}\n {...props}\n >\n <div\n className={cn(\n 'grid shrink-0 place-items-center overflow-hidden rounded-sm bg-muted text-muted-foreground [&_svg]:size-4',\n layout === 'chip' ? 'size-9' : 'size-10',\n )}\n >\n {preview ? (\n <img src={preview} alt=\"\" className=\"size-full object-cover\" />\n ) : failed ? (\n <AlertCircle className=\"text-destructive\" aria-hidden />\n ) : busy ? (\n <Spinner className=\"size-4\" role=\"presentation\" aria-hidden />\n ) : (\n (icon ?? <FileText aria-hidden />)\n )}\n </div>\n <div className=\"grid min-w-0 flex-1 gap-1\">\n <span className=\"truncate font-medium text-foreground\" title={name}>\n {name}\n </span>\n <span\n aria-live=\"polite\"\n className={cn('truncate text-xs', failed ? 'text-destructive' : 'text-muted-foreground')}\n >\n <span className=\"sr-only\">{name}: </span>\n {failed && error ? error : meta}\n </span>\n {busy && (\n <Progress value={progress} aria-label={`${stateText[state]} ${name}`} className=\"h-1\" />\n )}\n </div>\n {failed && onRetry && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Retry ${name}`}\n title=\"Retry\"\n onClick={onRetry}\n >\n <RotateCw />\n </Button>\n )}\n {onRemove && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Remove ${name}`}\n title=\"Remove\"\n onClick={onRemove}\n >\n <X />\n </Button>\n )}\n </div>\n );\n}\n\nexport interface AttachmentTrayProps extends React.ComponentProps<'div'> {\n /** Accessible name for the list. */\n label?: string;\n}\n\n/** Attachments waiting to send, in a row that scrolls sideways when full. */\nfunction AttachmentTray({\n label = 'Attachments',\n className,\n children,\n ...props\n}: AttachmentTrayProps) {\n return (\n <InTray.Provider value={true}>\n <div\n data-slot=\"attachment-tray\"\n role=\"list\"\n aria-label={label}\n className={cn('flex max-w-full gap-1.5 overflow-x-auto pb-0.5', className)}\n {...props}\n >\n {children}\n </div>\n </InTray.Provider>\n );\n}\n\nexport interface UploadQueueFile {\n id: string;\n name: string;\n size?: number;\n state: AttachmentState;\n progress?: number;\n error?: string;\n}\n\nexport interface UploadQueueProps extends Omit<React.ComponentProps<'section'>, 'title'> {\n files: UploadQueueFile[];\n title?: React.ReactNode;\n onRemove?: (id: string) => void;\n onRetry?: (id: string) => void;\n /** Shown when there are no files. */\n empty?: React.ReactNode;\n}\n\n/** A list of uploads with a summary: \"2 of 4 ready, 1 failed\". */\nfunction UploadQueue({\n files,\n title = 'Uploads',\n onRemove,\n onRetry,\n empty,\n className,\n ...props\n}: UploadQueueProps) {\n const headingId = React.useId();\n const ready = files.filter((f) => f.state === 'ready').length;\n const failed = files.filter((f) => f.state === 'failed').length;\n const summary = files.length\n ? `${ready} of ${files.length} ready${failed ? `, ${failed} failed` : ''}`\n : '';\n return (\n <section\n data-slot=\"upload-queue\"\n aria-labelledby={headingId}\n className={cn('grid gap-2', className)}\n {...props}\n >\n <div className=\"flex items-baseline justify-between gap-3\">\n <h3 id={headingId} className=\"text-sm font-semibold\">\n {title}\n </h3>\n <span className=\"text-xs text-muted-foreground tabular-nums\">{summary}</span>\n </div>\n {files.length === 0 ? (\n empty ? (\n <div className=\"text-sm text-muted-foreground\">{empty}</div>\n ) : null\n ) : (\n <InTray.Provider value={true}>\n <div role=\"list\" aria-labelledby={headingId} className=\"grid gap-1.5\">\n {files.map((f) => (\n <AttachmentItem\n key={f.id}\n layout=\"row\"\n name={f.name}\n size={f.size}\n state={f.state}\n progress={f.progress}\n error={f.error}\n onRemove={onRemove ? () => onRemove(f.id) : undefined}\n onRetry={onRetry ? () => onRetry(f.id) : undefined}\n />\n ))}\n </div>\n </InTray.Provider>\n )}\n </section>\n );\n}\n\nexport { AttachmentItem, AttachmentTray, UploadQueue };\n"],"mappings":"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"}
1
+ {"version":3,"file":"attachment.js","names":[],"sources":["../../src/components/attachment.tsx"],"sourcesContent":["import AlertCircle from '@burtson-labs/icons/react/alert-circle';\nimport FileText from '@burtson-labs/icons/react/file-text';\nimport RotateCw from '@burtson-labs/icons/react/rotate-cw';\nimport X from '@burtson-labs/icons/react/x';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\nimport { Progress } from './progress';\nimport { Spinner } from './spinner';\n\n/**\n * Where a file is on its way into a message. The app owns the upload and the\n * parsing; these components only show the state it reports.\n */\nexport type AttachmentState = 'queued' | 'uploading' | 'parsing' | 'ready' | 'failed';\n\nconst stateText: Record<AttachmentState, string> = {\n queued: 'Waiting',\n uploading: 'Uploading',\n parsing: 'Reading',\n ready: 'Ready',\n failed: 'Failed',\n};\n\n/** 1536 → \"1.5 KB\". */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;\n}\n\nconst InTray = React.createContext(false);\n\nexport interface AttachmentItemProps extends Omit<React.ComponentProps<'div'>, 'children'> {\n name: string;\n /** Bytes. */\n size?: number;\n state?: AttachmentState;\n /** 0–100 while uploading or reading. Leave out for an indeterminate bar. */\n progress?: number;\n /** Why it failed, in words the person can act on. */\n error?: string;\n /** A thumbnail URL for images. */\n preview?: string;\n /** Replaces the file icon. */\n icon?: React.ReactNode;\n onRemove?: () => void;\n onRetry?: () => void;\n /** `chip` sits in a composer; `row` fills an upload list. */\n layout?: 'chip' | 'row';\n}\n\n/**\n * One attached file: name, size, state, progress, and remove/retry. State\n * changes are announced politely; a failure says why.\n */\nconst AttachmentItem = React.forwardRef<HTMLDivElement, AttachmentItemProps>(\n function AttachmentItem(\n {\n name,\n size,\n state = 'ready',\n progress,\n error,\n preview,\n icon,\n onRemove,\n onRetry,\n layout = 'chip',\n className,\n ...props\n },\n ref,\n ) {\n const inTray = React.useContext(InTray);\n const busy = state === 'uploading' || state === 'parsing';\n const failed = state === 'failed';\n const meta = [size !== undefined ? formatBytes(size) : null, stateText[state]]\n .filter(Boolean)\n .join(' · ');\n return (\n <div\n ref={ref}\n data-slot=\"attachment-item\"\n data-state={state}\n data-layout={layout}\n role={inTray ? 'listitem' : undefined}\n aria-busy={busy || undefined}\n className={cn(\n 'group/attachment relative flex min-w-0 items-center gap-2.5 rounded-md border bg-surface text-sm',\n layout === 'chip' ? 'w-60 max-w-full shrink-0 p-1.5 pr-1' : 'w-full p-2.5',\n failed && 'border-destructive/40 bg-destructive/5',\n className,\n )}\n {...props}\n >\n <div\n className={cn(\n 'grid shrink-0 place-items-center overflow-hidden rounded-sm bg-muted text-muted-foreground [&_svg]:size-4',\n layout === 'chip' ? 'size-9' : 'size-10',\n )}\n >\n {preview ? (\n <img src={preview} alt=\"\" className=\"size-full object-cover\" />\n ) : failed ? (\n <AlertCircle className=\"text-destructive\" aria-hidden />\n ) : busy ? (\n <Spinner className=\"size-4\" role=\"presentation\" aria-hidden />\n ) : (\n (icon ?? <FileText aria-hidden />)\n )}\n </div>\n <div className=\"grid min-w-0 flex-1 gap-1\">\n <span className=\"truncate font-medium text-foreground\" title={name}>\n {name}\n </span>\n <span\n aria-live=\"polite\"\n className={cn(\n 'truncate text-xs',\n failed ? 'text-destructive' : 'text-muted-foreground',\n )}\n >\n <span className=\"sr-only\">{name}: </span>\n {failed && error ? error : meta}\n </span>\n {busy && (\n <Progress value={progress} aria-label={`${stateText[state]} ${name}`} className=\"h-1\" />\n )}\n </div>\n {failed && onRetry && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Retry ${name}`}\n title=\"Retry\"\n onClick={onRetry}\n >\n <RotateCw />\n </Button>\n )}\n {onRemove && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Remove ${name}`}\n title=\"Remove\"\n onClick={onRemove}\n >\n <X />\n </Button>\n )}\n </div>\n );\n },\n);\n\nexport interface AttachmentTrayProps extends React.ComponentProps<'div'> {\n /** Accessible name for the list. */\n label?: string;\n}\n\n/** Attachments waiting to send, in a row that scrolls sideways when full. */\nconst AttachmentTray = React.forwardRef<HTMLDivElement, AttachmentTrayProps>(\n function AttachmentTray({ label = 'Attachments', className, children, ...props }, ref) {\n return (\n <InTray.Provider value={true}>\n <div\n ref={ref}\n data-slot=\"attachment-tray\"\n role=\"list\"\n aria-label={label}\n className={cn('flex max-w-full gap-1.5 overflow-x-auto pb-0.5', className)}\n {...props}\n >\n {children}\n </div>\n </InTray.Provider>\n );\n },\n);\n\nexport interface UploadQueueFile {\n id: string;\n name: string;\n size?: number;\n state: AttachmentState;\n progress?: number;\n error?: string;\n}\n\nexport interface UploadQueueProps extends Omit<React.ComponentProps<'section'>, 'title'> {\n files: UploadQueueFile[];\n title?: React.ReactNode;\n onRemove?: (id: string) => void;\n onRetry?: (id: string) => void;\n /** Shown when there are no files. */\n empty?: React.ReactNode;\n}\n\n/** A list of uploads with a summary: \"2 of 4 ready, 1 failed\". */\nconst UploadQueue = React.forwardRef<HTMLElement, UploadQueueProps>(function UploadQueue(\n { files, title = 'Uploads', onRemove, onRetry, empty, className, ...props },\n ref,\n) {\n const headingId = React.useId();\n const ready = files.filter((f) => f.state === 'ready').length;\n const failed = files.filter((f) => f.state === 'failed').length;\n const summary = files.length\n ? `${ready} of ${files.length} ready${failed ? `, ${failed} failed` : ''}`\n : '';\n return (\n <section\n ref={ref}\n data-slot=\"upload-queue\"\n aria-labelledby={headingId}\n className={cn('grid gap-2', className)}\n {...props}\n >\n <div className=\"flex items-baseline justify-between gap-3\">\n <h3 id={headingId} className=\"text-sm font-semibold\">\n {title}\n </h3>\n <span className=\"text-xs text-muted-foreground tabular-nums\">{summary}</span>\n </div>\n {files.length === 0 ? (\n empty ? (\n <div className=\"text-sm text-muted-foreground\">{empty}</div>\n ) : null\n ) : (\n <InTray.Provider value={true}>\n <div role=\"list\" aria-labelledby={headingId} className=\"grid gap-1.5\">\n {files.map((f) => (\n <AttachmentItem\n key={f.id}\n layout=\"row\"\n name={f.name}\n size={f.size}\n state={f.state}\n progress={f.progress}\n error={f.error}\n onRemove={onRemove ? () => onRemove(f.id) : undefined}\n onRetry={onRetry ? () => onRetry(f.id) : undefined}\n />\n ))}\n </div>\n </InTray.Provider>\n )}\n </section>\n );\n});\n\nexport { AttachmentItem, AttachmentTray, UploadQueue };\n"],"mappings":"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"}
@@ -33,7 +33,7 @@ export interface AudioPlayerProps extends Omit<React.ComponentProps<'div'>, 'tit
33
33
  * time, speed (1×, 1.5×, 2×), download and a transcript. The waveform is a
34
34
  * slider for screen readers.
35
35
  */
36
- declare function AudioPlayer({ src, peaks, title, duration: knownDuration, transcript, downloadName, variant, onPlayChange, className, ...props }: AudioPlayerProps): React.JSX.Element;
36
+ declare const AudioPlayer: React.ForwardRefExoticComponent<Omit<AudioPlayerProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
37
37
  /** A voice note inside a message: the compact AudioPlayer. */
38
- declare function VoiceMessage(props: Omit<AudioPlayerProps, 'variant'>): React.JSX.Element;
38
+ declare const VoiceMessage: React.ForwardRefExoticComponent<Omit<Omit<AudioPlayerProps, "variant">, "ref"> & React.RefAttributes<HTMLDivElement>>;
39
39
  export { AudioPlayer, VoiceMessage };
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { cn } from "../lib/utils.js";
2
+ import { cn, focusRingClasses } from "../lib/utils.js";
3
3
  import { Button } from "./button.js";
4
4
  import * as React from "react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -63,7 +63,7 @@ var SPEEDS = [
63
63
  * time, speed (1×, 1.5×, 2×), download and a transcript. The waveform is a
64
64
  * slider for screen readers.
65
65
  */
66
- function AudioPlayer({ src, peaks, title = "Audio", duration: knownDuration, transcript, downloadName, variant = "default", onPlayChange, className, ...props }) {
66
+ var AudioPlayer = React.forwardRef(function AudioPlayer({ src, peaks, title = "Audio", duration: knownDuration, transcript, downloadName, variant = "default", onPlayChange, className, ...props }, ref) {
67
67
  const audio = React.useRef(null);
68
68
  const [playing, setPlaying] = React.useState(false);
69
69
  const [current, setCurrent] = React.useState(0);
@@ -117,6 +117,7 @@ function AudioPlayer({ src, peaks, title = "Audio", duration: knownDuration, tra
117
117
  };
118
118
  const bars = peaks && peaks.length ? peaks : null;
119
119
  return /* @__PURE__ */ jsxs("div", {
120
+ ref,
120
121
  "data-slot": "audio-player",
121
122
  "data-variant": variant,
122
123
  className: cn("grid min-w-0 gap-2 rounded-lg border bg-surface text-foreground", compact ? "w-full max-w-sm p-2" : "p-3", className),
@@ -169,7 +170,7 @@ function AudioPlayer({ src, peaks, title = "Audio", duration: knownDuration, tra
169
170
  if (!rect.width || !total) return;
170
171
  seek((e.clientX - rect.left) / rect.width * total);
171
172
  },
172
- className: cn("relative flex min-w-0 flex-1 cursor-pointer items-center rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20", compact ? "h-7" : "h-9"),
173
+ className: cn("relative flex min-w-0 flex-1 cursor-pointer items-center rounded-sm", focusRingClasses, compact ? "h-7" : "h-9", "pointer-coarse:h-11"),
173
174
  children: bars ? /* @__PURE__ */ jsx("div", {
174
175
  "aria-hidden": true,
175
176
  className: "flex h-full w-full items-center gap-[2px]",
@@ -236,15 +237,16 @@ function AudioPlayer({ src, peaks, title = "Audio", duration: knownDuration, tra
236
237
  })
237
238
  ]
238
239
  });
239
- }
240
+ });
240
241
  /** A voice note inside a message: the compact AudioPlayer. */
241
- function VoiceMessage(props) {
242
+ var VoiceMessage = React.forwardRef(function VoiceMessage(props, ref) {
242
243
  return /* @__PURE__ */ jsx(AudioPlayer, {
244
+ ref,
243
245
  title: "Voice message",
244
246
  ...props,
245
247
  variant: "compact"
246
248
  });
247
- }
249
+ });
248
250
  //#endregion
249
251
  export { AudioPlayer, VoiceMessage, computePeaks, formatDuration, peaksFromChannelData };
250
252
 
@@ -1 +1 @@
1
- {"version":3,"file":"audio-player.js","names":[],"sources":["../../src/components/audio-player.tsx"],"sourcesContent":["import Download from '@burtson-labs/icons/react/download';\nimport FileText from '@burtson-labs/icons/react/file-text';\nimport Pause from '@burtson-labs/icons/react/pause';\nimport Play from '@burtson-labs/icons/react/play';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\n\n/** 83.4 → \"1:23\"; 3723 → \"1:02:03\". */\nexport function formatDuration(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) return '0:00';\n const s = Math.floor(seconds);\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const sec = String(s % 60).padStart(2, '0');\n return h ? `${h}:${String(m).padStart(2, '0')}:${sec}` : `${m}:${sec}`;\n}\n\n/**\n * Reduce decoded audio to `bars` peak heights between 0 and 1 (the loudest\n * sample in each slice, normalised to the loudest slice).\n */\nexport function peaksFromChannelData(data: Float32Array, bars = 48): number[] {\n if (!data.length || bars < 1) return [];\n const size = Math.max(1, Math.floor(data.length / bars));\n const peaks: number[] = [];\n for (let b = 0; b < bars; b++) {\n let max = 0;\n const end = Math.min(data.length, (b + 1) * size);\n for (let i = b * size; i < end; i++) {\n const v = Math.abs(data[i] ?? 0);\n if (v > max) max = v;\n }\n peaks.push(max);\n }\n const top = Math.max(...peaks) || 1;\n return peaks.map((p) => p / top);\n}\n\n/**\n * Decode an audio file or URL with Web Audio and return its peaks for\n * AudioPlayer. Browser only; for long files compute peaks on the server.\n */\nexport async function computePeaks(source: Blob | string, bars = 48): Promise<number[]> {\n const Ctx =\n globalThis.AudioContext ??\n (globalThis as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctx) throw new Error('Web Audio is not available in this browser.');\n const buf =\n typeof source === 'string'\n ? await (await fetch(source)).arrayBuffer()\n : await source.arrayBuffer();\n const ctx = new Ctx();\n try {\n const audio = await ctx.decodeAudioData(buf);\n return peaksFromChannelData(audio.getChannelData(0), bars);\n } finally {\n void ctx.close();\n }\n}\n\nconst SPEEDS = [1, 1.5, 2] as const;\n\nexport interface AudioPlayerProps extends Omit<React.ComponentProps<'div'>, 'title'> {\n src: string;\n /** Waveform heights 0–1 (see computePeaks). Leave out for a plain progress bar. */\n peaks?: number[];\n /** Name read out with the controls, e.g. \"Voice note from Dana\". */\n title?: string;\n /** Known length in seconds, shown before the file has loaded. */\n duration?: number;\n /** Text of the recording; adds a Transcript toggle. */\n transcript?: React.ReactNode;\n /** Adds a download link with this file name. */\n downloadName?: string;\n /** `compact` fits inside a message bubble. */\n variant?: 'default' | 'compact';\n onPlayChange?: (playing: boolean) => void;\n}\n\n/**\n * Plays one recording: play and pause, a waveform you can click or drive\n * with the arrow keys (5 seconds a step, Home and End), elapsed and total\n * time, speed (1×, 1.5×, 2×), download and a transcript. The waveform is a\n * slider for screen readers.\n */\nfunction AudioPlayer({\n src,\n peaks,\n title = 'Audio',\n duration: knownDuration,\n transcript,\n downloadName,\n variant = 'default',\n onPlayChange,\n className,\n ...props\n}: AudioPlayerProps) {\n const audio = React.useRef<HTMLAudioElement>(null);\n const [playing, setPlaying] = React.useState(false);\n const [current, setCurrent] = React.useState(0);\n const [duration, setDuration] = React.useState(knownDuration ?? 0);\n const [speed, setSpeed] = React.useState<(typeof SPEEDS)[number]>(1);\n const [showTranscript, setShowTranscript] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const transcriptId = React.useId();\n const compact = variant === 'compact';\n const total = duration || knownDuration || 0;\n const ratio = total ? Math.min(1, current / total) : 0;\n\n React.useEffect(() => {\n if (audio.current) audio.current.playbackRate = speed;\n }, [speed]);\n\n const seek = (t: number) => {\n const el = audio.current;\n const clamped = Math.max(0, Math.min(total || t, t));\n if (el) el.currentTime = clamped;\n setCurrent(clamped);\n };\n\n const togglePlay = async () => {\n const el = audio.current;\n if (!el) return;\n if (el.paused) {\n try {\n await el.play();\n } catch {\n setFailed(true);\n }\n } else el.pause();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n const step = { ArrowRight: 5, ArrowUp: 5, ArrowLeft: -5, ArrowDown: -5 }[e.key];\n if (step !== undefined) {\n e.preventDefault();\n seek(current + step);\n } else if (e.key === 'Home') {\n e.preventDefault();\n seek(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n seek(total);\n } else if (e.key === ' ' || e.key === 'Enter') {\n e.preventDefault();\n void togglePlay();\n }\n };\n\n const bars = peaks && peaks.length ? peaks : null;\n\n return (\n <div\n data-slot=\"audio-player\"\n data-variant={variant}\n className={cn(\n 'grid min-w-0 gap-2 rounded-lg border bg-surface text-foreground',\n compact ? 'w-full max-w-sm p-2' : 'p-3',\n className,\n )}\n {...props}\n >\n {/* eslint-disable-next-line jsx-a11y/media-has-caption -- the transcript prop is the text alternative */}\n <audio\n ref={audio}\n src={src}\n preload=\"metadata\"\n onLoadedMetadata={(e) => {\n const d = e.currentTarget.duration;\n if (Number.isFinite(d)) setDuration(d);\n }}\n onTimeUpdate={(e) => setCurrent(e.currentTarget.currentTime)}\n onPlay={() => {\n setPlaying(true);\n onPlayChange?.(true);\n }}\n onPause={() => {\n setPlaying(false);\n onPlayChange?.(false);\n }}\n onEnded={() => setCurrent(0)}\n onError={() => setFailed(true)}\n />\n <div className=\"flex min-w-0 items-center gap-2\">\n <Button\n type=\"button\"\n size={compact ? 'icon-sm' : 'icon'}\n variant={playing ? 'secondary' : 'default'}\n aria-label={playing ? `Pause ${title}` : `Play ${title}`}\n onClick={() => void togglePlay()}\n disabled={failed}\n className=\"shrink-0 rounded-full pointer-coarse:size-11\"\n >\n {playing ? <Pause className=\"fill-current\" /> : <Play className=\"fill-current\" />}\n </Button>\n <div\n role=\"slider\"\n tabIndex={failed ? -1 : 0}\n aria-label={`Seek ${title}`}\n aria-valuemin={0}\n aria-valuemax={Math.round(total)}\n aria-valuenow={Math.round(current)}\n aria-valuetext={`${formatDuration(current)} of ${formatDuration(total)}`}\n onKeyDown={onKeyDown}\n onClick={(e) => {\n const rect = e.currentTarget.getBoundingClientRect();\n if (!rect.width || !total) return;\n seek(((e.clientX - rect.left) / rect.width) * total);\n }}\n className={cn(\n 'relative flex min-w-0 flex-1 cursor-pointer items-center rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20',\n compact ? 'h-7' : 'h-9',\n )}\n >\n {bars ? (\n <div aria-hidden className=\"flex h-full w-full items-center gap-[2px]\">\n {bars.map((p, i) => (\n <span\n key={i}\n className={cn(\n 'min-w-[2px] flex-1 rounded-full transition-colors duration-[var(--duration-fast)] ease-[var(--ease-standard)]',\n (i + 0.5) / bars.length <= ratio ? 'bg-brand' : 'bg-border-strong',\n )}\n style={{ height: `${Math.max(12, Math.round(p * 100))}%` }}\n />\n ))}\n </div>\n ) : (\n <div aria-hidden className=\"h-1.5 w-full overflow-hidden rounded-full bg-muted\">\n <div className=\"h-full rounded-full bg-brand\" style={{ width: `${ratio * 100}%` }} />\n </div>\n )}\n </div>\n <span className=\"shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums\">\n {playing || current ? formatDuration(current) : formatDuration(total)}\n </span>\n </div>\n {failed ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n This recording could not be played.\n </p>\n ) : (\n <div className=\"flex flex-wrap items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={`Playback speed ${speed}×`}\n onClick={() => setSpeed(SPEEDS[(SPEEDS.indexOf(speed) + 1) % SPEEDS.length] ?? 1)}\n className=\"font-mono tabular-nums\"\n >\n {speed}×\n </Button>\n {transcript && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-expanded={showTranscript}\n aria-controls={transcriptId}\n onClick={() => setShowTranscript((v) => !v)}\n >\n <FileText /> Transcript\n </Button>\n )}\n {downloadName && (\n <Button variant=\"ghost\" size=\"xs\" asChild>\n <a href={src} download={downloadName}>\n <Download /> Download\n </a>\n </Button>\n )}\n </div>\n )}\n {transcript && showTranscript && (\n <div\n id={transcriptId}\n className=\"animate-in rounded-md bg-muted/60 p-2.5 text-sm leading-6 text-muted-foreground\"\n >\n {transcript}\n </div>\n )}\n </div>\n );\n}\n\n/** A voice note inside a message: the compact AudioPlayer. */\nfunction VoiceMessage(props: Omit<AudioPlayerProps, 'variant'>) {\n return <AudioPlayer title=\"Voice message\" {...props} variant=\"compact\" />;\n}\n\nexport { AudioPlayer, VoiceMessage };\n"],"mappings":"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"}
1
+ {"version":3,"file":"audio-player.js","names":[],"sources":["../../src/components/audio-player.tsx"],"sourcesContent":["import Download from '@burtson-labs/icons/react/download';\nimport FileText from '@burtson-labs/icons/react/file-text';\nimport Pause from '@burtson-labs/icons/react/pause';\nimport Play from '@burtson-labs/icons/react/play';\nimport * as React from 'react';\n\nimport { cn, focusRingClasses } from '../lib/utils';\n\nimport { Button } from './button';\n\n/** 83.4 → \"1:23\"; 3723 → \"1:02:03\". */\nexport function formatDuration(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) return '0:00';\n const s = Math.floor(seconds);\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const sec = String(s % 60).padStart(2, '0');\n return h ? `${h}:${String(m).padStart(2, '0')}:${sec}` : `${m}:${sec}`;\n}\n\n/**\n * Reduce decoded audio to `bars` peak heights between 0 and 1 (the loudest\n * sample in each slice, normalised to the loudest slice).\n */\nexport function peaksFromChannelData(data: Float32Array, bars = 48): number[] {\n if (!data.length || bars < 1) return [];\n const size = Math.max(1, Math.floor(data.length / bars));\n const peaks: number[] = [];\n for (let b = 0; b < bars; b++) {\n let max = 0;\n const end = Math.min(data.length, (b + 1) * size);\n for (let i = b * size; i < end; i++) {\n const v = Math.abs(data[i] ?? 0);\n if (v > max) max = v;\n }\n peaks.push(max);\n }\n const top = Math.max(...peaks) || 1;\n return peaks.map((p) => p / top);\n}\n\n/**\n * Decode an audio file or URL with Web Audio and return its peaks for\n * AudioPlayer. Browser only; for long files compute peaks on the server.\n */\nexport async function computePeaks(source: Blob | string, bars = 48): Promise<number[]> {\n const Ctx =\n globalThis.AudioContext ??\n (globalThis as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctx) throw new Error('Web Audio is not available in this browser.');\n const buf =\n typeof source === 'string'\n ? await (await fetch(source)).arrayBuffer()\n : await source.arrayBuffer();\n const ctx = new Ctx();\n try {\n const audio = await ctx.decodeAudioData(buf);\n return peaksFromChannelData(audio.getChannelData(0), bars);\n } finally {\n void ctx.close();\n }\n}\n\nconst SPEEDS = [1, 1.5, 2] as const;\n\nexport interface AudioPlayerProps extends Omit<React.ComponentProps<'div'>, 'title'> {\n src: string;\n /** Waveform heights 0–1 (see computePeaks). Leave out for a plain progress bar. */\n peaks?: number[];\n /** Name read out with the controls, e.g. \"Voice note from Dana\". */\n title?: string;\n /** Known length in seconds, shown before the file has loaded. */\n duration?: number;\n /** Text of the recording; adds a Transcript toggle. */\n transcript?: React.ReactNode;\n /** Adds a download link with this file name. */\n downloadName?: string;\n /** `compact` fits inside a message bubble. */\n variant?: 'default' | 'compact';\n onPlayChange?: (playing: boolean) => void;\n}\n\n/**\n * Plays one recording: play and pause, a waveform you can click or drive\n * with the arrow keys (5 seconds a step, Home and End), elapsed and total\n * time, speed (1×, 1.5×, 2×), download and a transcript. The waveform is a\n * slider for screen readers.\n */\nconst AudioPlayer = React.forwardRef<HTMLDivElement, AudioPlayerProps>(function AudioPlayer(\n {\n src,\n peaks,\n title = 'Audio',\n duration: knownDuration,\n transcript,\n downloadName,\n variant = 'default',\n onPlayChange,\n className,\n ...props\n },\n ref,\n) {\n const audio = React.useRef<HTMLAudioElement>(null);\n const [playing, setPlaying] = React.useState(false);\n const [current, setCurrent] = React.useState(0);\n const [duration, setDuration] = React.useState(knownDuration ?? 0);\n const [speed, setSpeed] = React.useState<(typeof SPEEDS)[number]>(1);\n const [showTranscript, setShowTranscript] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const transcriptId = React.useId();\n const compact = variant === 'compact';\n const total = duration || knownDuration || 0;\n const ratio = total ? Math.min(1, current / total) : 0;\n\n React.useEffect(() => {\n if (audio.current) audio.current.playbackRate = speed;\n }, [speed]);\n\n const seek = (t: number) => {\n const el = audio.current;\n const clamped = Math.max(0, Math.min(total || t, t));\n if (el) el.currentTime = clamped;\n setCurrent(clamped);\n };\n\n const togglePlay = async () => {\n const el = audio.current;\n if (!el) return;\n if (el.paused) {\n try {\n await el.play();\n } catch {\n setFailed(true);\n }\n } else el.pause();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n const step = { ArrowRight: 5, ArrowUp: 5, ArrowLeft: -5, ArrowDown: -5 }[e.key];\n if (step !== undefined) {\n e.preventDefault();\n seek(current + step);\n } else if (e.key === 'Home') {\n e.preventDefault();\n seek(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n seek(total);\n } else if (e.key === ' ' || e.key === 'Enter') {\n e.preventDefault();\n void togglePlay();\n }\n };\n\n const bars = peaks && peaks.length ? peaks : null;\n\n return (\n <div\n ref={ref}\n data-slot=\"audio-player\"\n data-variant={variant}\n className={cn(\n 'grid min-w-0 gap-2 rounded-lg border bg-surface text-foreground',\n compact ? 'w-full max-w-sm p-2' : 'p-3',\n className,\n )}\n {...props}\n >\n {/* eslint-disable-next-line jsx-a11y/media-has-caption -- the transcript prop is the text alternative */}\n <audio\n ref={audio}\n src={src}\n preload=\"metadata\"\n onLoadedMetadata={(e) => {\n const d = e.currentTarget.duration;\n if (Number.isFinite(d)) setDuration(d);\n }}\n onTimeUpdate={(e) => setCurrent(e.currentTarget.currentTime)}\n onPlay={() => {\n setPlaying(true);\n onPlayChange?.(true);\n }}\n onPause={() => {\n setPlaying(false);\n onPlayChange?.(false);\n }}\n onEnded={() => setCurrent(0)}\n onError={() => setFailed(true)}\n />\n <div className=\"flex min-w-0 items-center gap-2\">\n <Button\n type=\"button\"\n size={compact ? 'icon-sm' : 'icon'}\n variant={playing ? 'secondary' : 'default'}\n aria-label={playing ? `Pause ${title}` : `Play ${title}`}\n onClick={() => void togglePlay()}\n disabled={failed}\n className=\"shrink-0 rounded-full pointer-coarse:size-11\"\n >\n {playing ? <Pause className=\"fill-current\" /> : <Play className=\"fill-current\" />}\n </Button>\n <div\n role=\"slider\"\n tabIndex={failed ? -1 : 0}\n aria-label={`Seek ${title}`}\n aria-valuemin={0}\n aria-valuemax={Math.round(total)}\n aria-valuenow={Math.round(current)}\n aria-valuetext={`${formatDuration(current)} of ${formatDuration(total)}`}\n onKeyDown={onKeyDown}\n onClick={(e) => {\n const rect = e.currentTarget.getBoundingClientRect();\n if (!rect.width || !total) return;\n seek(((e.clientX - rect.left) / rect.width) * total);\n }}\n className={cn(\n 'relative flex min-w-0 flex-1 cursor-pointer items-center rounded-sm',\n focusRingClasses,\n compact ? 'h-7' : 'h-9',\n 'pointer-coarse:h-11',\n )}\n >\n {bars ? (\n <div aria-hidden className=\"flex h-full w-full items-center gap-[2px]\">\n {bars.map((p, i) => (\n <span\n key={i}\n className={cn(\n 'min-w-[2px] flex-1 rounded-full transition-colors duration-[var(--duration-fast)] ease-[var(--ease-standard)]',\n (i + 0.5) / bars.length <= ratio ? 'bg-brand' : 'bg-border-strong',\n )}\n style={{ height: `${Math.max(12, Math.round(p * 100))}%` }}\n />\n ))}\n </div>\n ) : (\n <div aria-hidden className=\"h-1.5 w-full overflow-hidden rounded-full bg-muted\">\n <div className=\"h-full rounded-full bg-brand\" style={{ width: `${ratio * 100}%` }} />\n </div>\n )}\n </div>\n <span className=\"shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums\">\n {playing || current ? formatDuration(current) : formatDuration(total)}\n </span>\n </div>\n {failed ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n This recording could not be played.\n </p>\n ) : (\n <div className=\"flex flex-wrap items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={`Playback speed ${speed}×`}\n onClick={() => setSpeed(SPEEDS[(SPEEDS.indexOf(speed) + 1) % SPEEDS.length] ?? 1)}\n className=\"font-mono tabular-nums\"\n >\n {speed}×\n </Button>\n {transcript && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-expanded={showTranscript}\n aria-controls={transcriptId}\n onClick={() => setShowTranscript((v) => !v)}\n >\n <FileText /> Transcript\n </Button>\n )}\n {downloadName && (\n <Button variant=\"ghost\" size=\"xs\" asChild>\n <a href={src} download={downloadName}>\n <Download /> Download\n </a>\n </Button>\n )}\n </div>\n )}\n {transcript && showTranscript && (\n <div\n id={transcriptId}\n className=\"animate-in rounded-md bg-muted/60 p-2.5 text-sm leading-6 text-muted-foreground\"\n >\n {transcript}\n </div>\n )}\n </div>\n );\n});\n\n/** A voice note inside a message: the compact AudioPlayer. */\nconst VoiceMessage = React.forwardRef<HTMLDivElement, Omit<AudioPlayerProps, 'variant'>>(\n function VoiceMessage(props, ref) {\n return <AudioPlayer ref={ref} title=\"Voice message\" {...props} variant=\"compact\" />;\n },\n);\n\nexport { AudioPlayer, VoiceMessage };\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ export interface AvatarUploadProps extends Omit<React.ComponentProps<'div'>, 'onError'> {
3
+ src?: string;
4
+ /** A stored photo may exist even when its preview cannot be loaded. */
5
+ hasPhoto?: boolean;
6
+ /** The person's name, used to label the current photo. */
7
+ name: string;
8
+ fallback?: React.ReactNode;
9
+ /** Receives a square JPEG. Resolve only after it has been saved; reject to offer retry. */
10
+ onUpload: (file: File) => Promise<void>;
11
+ onRemove?: () => Promise<void>;
12
+ disabled?: boolean;
13
+ /** Maximum size of the original file. Default: 10 MiB. */
14
+ maxBytes?: number;
15
+ /** Exported square dimension. Default: 512px. */
16
+ outputSize?: number;
17
+ }
18
+ /** Choose, position and save a profile photo. No storage or networking is built in. */
19
+ export declare function AvatarUpload({ src, hasPhoto, name, fallback, onUpload, onRemove, disabled, maxBytes, outputSize, className, ...props }: AvatarUploadProps): React.JSX.Element;