@adres/mgda-ui 0.0.0-stage → 0.0.1

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 (325) hide show
  1. package/brand-knowledge.json +451 -0
  2. package/knowledge/accordion.md +135 -0
  3. package/knowledge/activity-rings.md +123 -0
  4. package/knowledge/alert.md +184 -0
  5. package/knowledge/answer-actions.md +217 -0
  6. package/knowledge/aspect-ratio.md +82 -0
  7. package/knowledge/avatar.md +158 -0
  8. package/knowledge/badge.md +172 -0
  9. package/knowledge/bar-graph.md +167 -0
  10. package/knowledge/bento-grid.md +115 -0
  11. package/knowledge/blockquote.md +153 -0
  12. package/knowledge/box-list.md +177 -0
  13. package/knowledge/bullet-list.md +128 -0
  14. package/knowledge/button-group.md +179 -0
  15. package/knowledge/button.md +231 -0
  16. package/knowledge/calendar.md +168 -0
  17. package/knowledge/card.md +321 -0
  18. package/knowledge/cards-nav.md +170 -0
  19. package/knowledge/chain-of-thought.md +164 -0
  20. package/knowledge/chat-container.md +135 -0
  21. package/knowledge/chat-message.md +224 -0
  22. package/knowledge/checkbox-card.md +172 -0
  23. package/knowledge/checkbox.md +111 -0
  24. package/knowledge/citations.md +241 -0
  25. package/knowledge/combobox.md +265 -0
  26. package/knowledge/countdown-pill.md +153 -0
  27. package/knowledge/cta.md +118 -0
  28. package/knowledge/data-list.md +173 -0
  29. package/knowledge/data-table.md +220 -0
  30. package/knowledge/dialog.md +249 -0
  31. package/knowledge/drawer.md +236 -0
  32. package/knowledge/dropdown-menu.md +238 -0
  33. package/knowledge/dropzone.md +153 -0
  34. package/knowledge/email-chips-input.md +161 -0
  35. package/knowledge/empty-state.md +106 -0
  36. package/knowledge/entity-header.md +166 -0
  37. package/knowledge/feature-grid.md +120 -0
  38. package/knowledge/feedback-bar.md +127 -0
  39. package/knowledge/footer.md +116 -0
  40. package/knowledge/heading.md +189 -0
  41. package/knowledge/hero.md +361 -0
  42. package/knowledge/input-button.md +187 -0
  43. package/knowledge/input-group.md +185 -0
  44. package/knowledge/input.md +256 -0
  45. package/knowledge/inset.md +177 -0
  46. package/knowledge/kpi-card.md +206 -0
  47. package/knowledge/list-nav.md +181 -0
  48. package/knowledge/logo-cloud.md +102 -0
  49. package/knowledge/media-gallery.md +158 -0
  50. package/knowledge/message-actions.md +175 -0
  51. package/knowledge/nav-sidebar.md +225 -0
  52. package/knowledge/number-stepper.md +159 -0
  53. package/knowledge/page-header.md +109 -0
  54. package/knowledge/pagination.md +225 -0
  55. package/knowledge/popover.md +159 -0
  56. package/knowledge/pricing.md +132 -0
  57. package/knowledge/progress.md +156 -0
  58. package/knowledge/prompt-input.md +337 -0
  59. package/knowledge/prompt-suggestions.md +168 -0
  60. package/knowledge/quota-bar.md +160 -0
  61. package/knowledge/quote.md +116 -0
  62. package/knowledge/radio-card.md +172 -0
  63. package/knowledge/radio-group.md +135 -0
  64. package/knowledge/reasoning.md +142 -0
  65. package/knowledge/search.md +131 -0
  66. package/knowledge/section-nav.md +162 -0
  67. package/knowledge/segmented-control.md +181 -0
  68. package/knowledge/select-button.md +205 -0
  69. package/knowledge/select.md +210 -0
  70. package/knowledge/separator.md +75 -0
  71. package/knowledge/shimmer-text.md +140 -0
  72. package/knowledge/sidebar.md +227 -0
  73. package/knowledge/skeleton.md +142 -0
  74. package/knowledge/slider.md +133 -0
  75. package/knowledge/slideshow.md +159 -0
  76. package/knowledge/source.md +156 -0
  77. package/knowledge/spinner.md +120 -0
  78. package/knowledge/stat-strip.md +128 -0
  79. package/knowledge/stats.md +117 -0
  80. package/knowledge/step-indicator.md +157 -0
  81. package/knowledge/switch.md +88 -0
  82. package/knowledge/table.md +190 -0
  83. package/knowledge/tabs.md +169 -0
  84. package/knowledge/testimonials.md +115 -0
  85. package/knowledge/text-link.md +192 -0
  86. package/knowledge/text.md +194 -0
  87. package/knowledge/thinking-bar.md +115 -0
  88. package/knowledge/thinking-steps.md +140 -0
  89. package/knowledge/toaster.md +127 -0
  90. package/knowledge/toggle.md +113 -0
  91. package/knowledge/tool-call-card.md +241 -0
  92. package/knowledge/tooltip.md +120 -0
  93. package/knowledge/top-nav.md +138 -0
  94. package/knowledge.json +19854 -0
  95. package/package.json +94 -4
  96. package/src/components/accordion.stories.tsx +46 -0
  97. package/src/components/accordion.tsx +74 -0
  98. package/src/components/activity-rings.stories.tsx +73 -0
  99. package/src/components/activity-rings.tsx +121 -0
  100. package/src/components/alert.stories.tsx +43 -0
  101. package/src/components/alert.tsx +132 -0
  102. package/src/components/answer-actions.stories.tsx +54 -0
  103. package/src/components/answer-actions.test.tsx +37 -0
  104. package/src/components/answer-actions.tsx +313 -0
  105. package/src/components/aspect-ratio.stories.tsx +32 -0
  106. package/src/components/aspect-ratio.tsx +5 -0
  107. package/src/components/avatar.stories.tsx +40 -0
  108. package/src/components/avatar.tsx +154 -0
  109. package/src/components/badge.stories.tsx +27 -0
  110. package/src/components/badge.tsx +63 -0
  111. package/src/components/bar-graph.stories.tsx +28 -0
  112. package/src/components/bar-graph.tsx +204 -0
  113. package/src/components/bento-grid.stories.tsx +23 -0
  114. package/src/components/bento-grid.tsx +67 -0
  115. package/src/components/blockquote.stories.tsx +28 -0
  116. package/src/components/blockquote.tsx +88 -0
  117. package/src/components/box-list.stories.tsx +25 -0
  118. package/src/components/box-list.tsx +108 -0
  119. package/src/components/bullet-list.stories.tsx +22 -0
  120. package/src/components/bullet-list.tsx +101 -0
  121. package/src/components/button-group.stories.tsx +31 -0
  122. package/src/components/button-group.tsx +156 -0
  123. package/src/components/button.stories.tsx +66 -0
  124. package/src/components/button.test.ts +17 -0
  125. package/src/components/button.tsx +139 -0
  126. package/src/components/calendar.stories.tsx +25 -0
  127. package/src/components/calendar.tsx +367 -0
  128. package/src/components/card.stories.tsx +44 -0
  129. package/src/components/card.tsx +392 -0
  130. package/src/components/cards-nav.stories.tsx +35 -0
  131. package/src/components/cards-nav.tsx +115 -0
  132. package/src/components/chain-of-thought.stories.tsx +67 -0
  133. package/src/components/chain-of-thought.test.tsx +50 -0
  134. package/src/components/chain-of-thought.tsx +170 -0
  135. package/src/components/chat-container.stories.tsx +47 -0
  136. package/src/components/chat-container.test.tsx +49 -0
  137. package/src/components/chat-container.tsx +222 -0
  138. package/src/components/chat-message.stories.tsx +52 -0
  139. package/src/components/chat-message.test.tsx +36 -0
  140. package/src/components/chat-message.tsx +163 -0
  141. package/src/components/checkbox-card.stories.tsx +27 -0
  142. package/src/components/checkbox-card.tsx +200 -0
  143. package/src/components/checkbox.stories.tsx +26 -0
  144. package/src/components/checkbox.tsx +61 -0
  145. package/src/components/citations.stories.tsx +64 -0
  146. package/src/components/citations.test.tsx +61 -0
  147. package/src/components/citations.tsx +220 -0
  148. package/src/components/combobox.stories.tsx +184 -0
  149. package/src/components/combobox.test.tsx +82 -0
  150. package/src/components/combobox.tsx +407 -0
  151. package/src/components/countdown-pill.stories.tsx +24 -0
  152. package/src/components/countdown-pill.tsx +129 -0
  153. package/src/components/cta.stories.tsx +29 -0
  154. package/src/components/cta.tsx +52 -0
  155. package/src/components/data-list.stories.tsx +50 -0
  156. package/src/components/data-list.test.tsx +35 -0
  157. package/src/components/data-list.tsx +181 -0
  158. package/src/components/data-table.stories.tsx +29 -0
  159. package/src/components/data-table.tsx +200 -0
  160. package/src/components/dialog.stories.tsx +42 -0
  161. package/src/components/dialog.tsx +200 -0
  162. package/src/components/drawer.stories.tsx +48 -0
  163. package/src/components/drawer.tsx +385 -0
  164. package/src/components/dropdown-menu.stories.tsx +41 -0
  165. package/src/components/dropdown-menu.tsx +252 -0
  166. package/src/components/dropzone.stories.tsx +19 -0
  167. package/src/components/dropzone.tsx +250 -0
  168. package/src/components/email-chips-input.stories.tsx +135 -0
  169. package/src/components/email-chips-input.tsx +210 -0
  170. package/src/components/empty-state.stories.tsx +22 -0
  171. package/src/components/empty-state.tsx +35 -0
  172. package/src/components/entity-header.stories.tsx +43 -0
  173. package/src/components/entity-header.test.tsx +35 -0
  174. package/src/components/entity-header.tsx +92 -0
  175. package/src/components/feature-grid.stories.tsx +25 -0
  176. package/src/components/feature-grid.tsx +87 -0
  177. package/src/components/feedback-bar.stories.tsx +25 -0
  178. package/src/components/feedback-bar.test.tsx +24 -0
  179. package/src/components/feedback-bar.tsx +62 -0
  180. package/src/components/footer.stories.tsx +23 -0
  181. package/src/components/footer.tsx +78 -0
  182. package/src/components/heading.stories.tsx +45 -0
  183. package/src/components/heading.test.tsx +22 -0
  184. package/src/components/heading.tsx +77 -0
  185. package/src/components/hero.stories.tsx +38 -0
  186. package/src/components/hero.tsx +498 -0
  187. package/src/components/input-button.stories.tsx +19 -0
  188. package/src/components/input-button.tsx +112 -0
  189. package/src/components/input-group.stories.tsx +38 -0
  190. package/src/components/input-group.tsx +157 -0
  191. package/src/components/input.stories.tsx +31 -0
  192. package/src/components/input.tsx +284 -0
  193. package/src/components/inset.stories.tsx +29 -0
  194. package/src/components/inset.tsx +70 -0
  195. package/src/components/kpi-card.stories.tsx +29 -0
  196. package/src/components/kpi-card.tsx +284 -0
  197. package/src/components/list-nav.stories.tsx +22 -0
  198. package/src/components/list-nav.tsx +145 -0
  199. package/src/components/logo-cloud.stories.tsx +23 -0
  200. package/src/components/logo-cloud.tsx +42 -0
  201. package/src/components/media-gallery.stories.tsx +25 -0
  202. package/src/components/media-gallery.tsx +347 -0
  203. package/src/components/message-actions.stories.tsx +45 -0
  204. package/src/components/message-actions.test.tsx +32 -0
  205. package/src/components/message-actions.tsx +119 -0
  206. package/src/components/nav-sidebar.stories.tsx +58 -0
  207. package/src/components/nav-sidebar.test.tsx +54 -0
  208. package/src/components/nav-sidebar.tsx +462 -0
  209. package/src/components/number-stepper.stories.tsx +19 -0
  210. package/src/components/number-stepper.tsx +158 -0
  211. package/src/components/page-header.stories.tsx +25 -0
  212. package/src/components/page-header.tsx +64 -0
  213. package/src/components/pagination.stories.tsx +110 -0
  214. package/src/components/pagination.test.tsx +46 -0
  215. package/src/components/pagination.tsx +144 -0
  216. package/src/components/popover.stories.tsx +26 -0
  217. package/src/components/popover.tsx +62 -0
  218. package/src/components/pricing.stories.tsx +42 -0
  219. package/src/components/pricing.tsx +130 -0
  220. package/src/components/progress.stories.tsx +22 -0
  221. package/src/components/progress.tsx +189 -0
  222. package/src/components/prompt-input.stories.tsx +62 -0
  223. package/src/components/prompt-input.test.tsx +106 -0
  224. package/src/components/prompt-input.tsx +343 -0
  225. package/src/components/prompt-suggestions.stories.tsx +53 -0
  226. package/src/components/prompt-suggestions.test.tsx +37 -0
  227. package/src/components/prompt-suggestions.tsx +122 -0
  228. package/src/components/quota-bar.stories.tsx +19 -0
  229. package/src/components/quota-bar.tsx +191 -0
  230. package/src/components/quote.stories.tsx +18 -0
  231. package/src/components/quote.tsx +43 -0
  232. package/src/components/radio-card.stories.tsx +27 -0
  233. package/src/components/radio-card.tsx +225 -0
  234. package/src/components/radio-group.stories.tsx +32 -0
  235. package/src/components/radio-group.tsx +68 -0
  236. package/src/components/reasoning.stories.tsx +50 -0
  237. package/src/components/reasoning.test.tsx +48 -0
  238. package/src/components/reasoning.tsx +143 -0
  239. package/src/components/search.stories.tsx +18 -0
  240. package/src/components/search.tsx +133 -0
  241. package/src/components/section-nav.stories.tsx +59 -0
  242. package/src/components/section-nav.test.tsx +36 -0
  243. package/src/components/section-nav.tsx +146 -0
  244. package/src/components/segmented-control.stories.tsx +22 -0
  245. package/src/components/segmented-control.tsx +301 -0
  246. package/src/components/select-button.stories.tsx +27 -0
  247. package/src/components/select-button.tsx +174 -0
  248. package/src/components/select.stories.tsx +27 -0
  249. package/src/components/select.tsx +198 -0
  250. package/src/components/separator.stories.tsx +22 -0
  251. package/src/components/separator.tsx +23 -0
  252. package/src/components/shimmer-text.stories.tsx +23 -0
  253. package/src/components/shimmer-text.test.tsx +35 -0
  254. package/src/components/shimmer-text.tsx +43 -0
  255. package/src/components/sidebar.stories.tsx +25 -0
  256. package/src/components/sidebar.tsx +234 -0
  257. package/src/components/skeleton.stories.tsx +22 -0
  258. package/src/components/skeleton.tsx +96 -0
  259. package/src/components/slider.stories.tsx +20 -0
  260. package/src/components/slider.tsx +176 -0
  261. package/src/components/slideshow.stories.tsx +30 -0
  262. package/src/components/slideshow.tsx +291 -0
  263. package/src/components/source.stories.tsx +32 -0
  264. package/src/components/source.test.tsx +40 -0
  265. package/src/components/source.tsx +161 -0
  266. package/src/components/spinner.stories.tsx +19 -0
  267. package/src/components/spinner.tsx +76 -0
  268. package/src/components/stat-strip.stories.tsx +36 -0
  269. package/src/components/stat-strip.test.tsx +25 -0
  270. package/src/components/stat-strip.tsx +85 -0
  271. package/src/components/stats.stories.tsx +24 -0
  272. package/src/components/stats.tsx +83 -0
  273. package/src/components/step-indicator.stories.tsx +18 -0
  274. package/src/components/step-indicator.test.tsx +18 -0
  275. package/src/components/step-indicator.tsx +286 -0
  276. package/src/components/switch.stories.tsx +26 -0
  277. package/src/components/switch.tsx +32 -0
  278. package/src/components/table.stories.tsx +38 -0
  279. package/src/components/table.tsx +220 -0
  280. package/src/components/tabs.stories.tsx +25 -0
  281. package/src/components/tabs.tsx +276 -0
  282. package/src/components/testimonials.stories.tsx +22 -0
  283. package/src/components/testimonials.tsx +109 -0
  284. package/src/components/text-link.stories.tsx +47 -0
  285. package/src/components/text-link.test.tsx +18 -0
  286. package/src/components/text-link.tsx +88 -0
  287. package/src/components/text.stories.tsx +49 -0
  288. package/src/components/text.tsx +105 -0
  289. package/src/components/thinking-bar.stories.tsx +19 -0
  290. package/src/components/thinking-bar.test.tsx +27 -0
  291. package/src/components/thinking-bar.tsx +64 -0
  292. package/src/components/thinking-steps.stories.tsx +44 -0
  293. package/src/components/thinking-steps.test.tsx +42 -0
  294. package/src/components/thinking-steps.tsx +92 -0
  295. package/src/components/toaster.stories.tsx +80 -0
  296. package/src/components/toaster.tsx +36 -0
  297. package/src/components/toggle.stories.tsx +21 -0
  298. package/src/components/toggle.tsx +103 -0
  299. package/src/components/tool-call-card.stories.tsx +97 -0
  300. package/src/components/tool-call-card.test.tsx +88 -0
  301. package/src/components/tool-call-card.tsx +231 -0
  302. package/src/components/tooltip.stories.tsx +25 -0
  303. package/src/components/tooltip.tsx +43 -0
  304. package/src/components/top-nav.stories.tsx +20 -0
  305. package/src/components/top-nav.tsx +76 -0
  306. package/src/docs/Colors.stories.tsx +192 -0
  307. package/src/docs/GettingStarted.mdx +72 -0
  308. package/src/docs/Mcp.mdx +52 -0
  309. package/src/docs/Typography.stories.tsx +114 -0
  310. package/src/hooks/use-in-view.ts +32 -0
  311. package/src/hooks/use-reduced-motion.ts +26 -0
  312. package/src/index.ts +343 -0
  313. package/src/lib/cn.test.ts +67 -0
  314. package/src/lib/cn.ts +81 -0
  315. package/src/lib/motion.ts +109 -0
  316. package/src/lib/prefix-utilities.test.ts +56 -0
  317. package/src/lib/prefix-utilities.ts +50 -0
  318. package/src/styles/base.css +489 -0
  319. package/src/styles/motion.css +69 -0
  320. package/src/styles/overrides.css +44 -0
  321. package/src/styles/typography.css +173 -0
  322. package/src/theme/theme.css +391 -0
  323. package/src/theme/tokens.ts +29 -0
  324. package/src/theme/types.ts +26 -0
  325. package/README.md +0 -3
@@ -0,0 +1,343 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ArrowUp, FileText, Image as ImageIcon, Upload, X } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+ import { Button, type ButtonProps } from "./button"
7
+ import { Textarea } from "./input"
8
+ import { Progress } from "./progress"
9
+ import { Text, type TextColor } from "./text"
10
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
11
+
12
+ export type PromptInputSize = "sm" | "md"
13
+
14
+ const SIZES = {
15
+ md: {
16
+ root: "ax:rounded-xl",
17
+ field: "ax:min-h-11 ax:px-md ax:pt-sm",
18
+ maxHeight: 8 * 24,
19
+ row: "ax:mt-lg ax:px-sm ax:pb-sm",
20
+ edge: "ax:px-sm ax:pt-xs",
21
+ hint: "ax:px-md ax:pb-sm",
22
+ button: "ax:size-9 ax:[&_svg]:size-4.5",
23
+ },
24
+ // For narrow chat columns and side panels.
25
+ sm: {
26
+ root: "ax:rounded-lg",
27
+ field: "ax:min-h-9 ax:px-sm ax:pt-xs",
28
+ maxHeight: 6 * 20,
29
+ row: "ax:mt-xs ax:px-xs ax:pb-xs",
30
+ edge: "ax:px-xs ax:pt-xs",
31
+ hint: "ax:px-sm ax:pb-xs",
32
+ button: "ax:size-8 ax:[&_svg]:size-4",
33
+ },
34
+ } as const
35
+
36
+ const PromptInputSizeContext = React.createContext<PromptInputSize>("md")
37
+
38
+ export type CharacterCountTone = Extract<TextColor, "muted" | "warning" | "destructive">
39
+
40
+ /** Hidden below 80% of the limit, then muted, warning from 90%, destructive once over. */
41
+ export function characterCountTone(length: number, max: number): CharacterCountTone | null {
42
+ if (length < max * 0.8) return null
43
+ if (length > max) return "destructive"
44
+ return length >= max * 0.9 ? "warning" : "muted"
45
+ }
46
+
47
+ export function formatFileSize(bytes: number) {
48
+ return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`
49
+ }
50
+
51
+ /** True when a drag carries files, so text or link drags pass through untouched. */
52
+ export function isFileDrag(types: readonly string[]) {
53
+ return types.includes("Files")
54
+ }
55
+
56
+ export interface PromptInputActionProps extends Omit<ButtonProps, "size" | "asChild"> {
57
+ /** Accessible name, and the tooltip unless `tooltip` is set. */
58
+ label: string
59
+ /** Longer tooltip text, e.g. accepted file types. */
60
+ tooltip?: React.ReactNode
61
+ /** Renders as a toggle: sets `aria-pressed`. */
62
+ pressed?: boolean
63
+ }
64
+
65
+ /** A round icon button with a tooltip for the composer's action row (attach, voice). Sized by its PromptInput. */
66
+ export const PromptInputAction = React.forwardRef<HTMLButtonElement, PromptInputActionProps>(
67
+ ({ label, tooltip, pressed, variant = "outline", className, ...props }, ref) => {
68
+ const size = React.useContext(PromptInputSizeContext)
69
+ return (
70
+ <Tooltip>
71
+ <TooltipTrigger asChild>
72
+ <Button
73
+ ref={ref}
74
+ type="button"
75
+ variant={variant}
76
+ size="icon"
77
+ aria-label={label}
78
+ aria-pressed={pressed}
79
+ className={cn("ax:rounded-full", SIZES[size].button, className)}
80
+ {...props}
81
+ />
82
+ </TooltipTrigger>
83
+ <TooltipContent>{tooltip ?? label}</TooltipContent>
84
+ </Tooltip>
85
+ )
86
+ },
87
+ )
88
+ PromptInputAction.displayName = "PromptInputAction"
89
+
90
+ export interface PromptInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSubmit" | "defaultValue"> {
91
+ value?: string
92
+ defaultValue?: string
93
+ onValueChange?: (value: string) => void
94
+ /** Called with the trimmed text on Enter or Send. The field then clears and keeps focus. */
95
+ onSubmit: (text: string) => void
96
+ /** True while the assistant answers; Send becomes Stop. */
97
+ busy?: boolean
98
+ onStop?: () => void
99
+ disabled?: boolean
100
+ /** Shown under the field when `disabled`, e.g. why the assistant is unavailable. */
101
+ disabledReason?: string
102
+ placeholder?: string
103
+ /** Accessible name of the field. */
104
+ label?: string
105
+ /** Characters allowed; the counter appears from 80% and sending is blocked when over. */
106
+ maxLength?: number
107
+ /** Lets an empty field send, e.g. when files are attached. */
108
+ hasAttachments?: boolean
109
+ /** Extra gate on sending, e.g. false while uploads are in progress. */
110
+ canSubmit?: boolean
111
+ /** Muted note under the field when nothing more urgent applies, e.g. "Waiting for uploads to finish." */
112
+ hint?: React.ReactNode
113
+ /** Above the field: usually a list of `AttachmentChip`s. */
114
+ attachments?: React.ReactNode
115
+ /** Start of the action row: attach, tools. Use `PromptInputAction` for icon buttons. */
116
+ toolbar?: React.ReactNode
117
+ /** End of the action row, before Send: voice input, status badges. */
118
+ endActions?: React.ReactNode
119
+ /** Receives files pasted into the field; without it, pasting files does nothing special. */
120
+ onFilesPaste?: (files: File[]) => void
121
+ /** Receives files dropped on the composer, which shows a drop overlay while dragging. Ignored while `disabled`. */
122
+ onFilesDrop?: (files: File[]) => void
123
+ /** Extra line in the drop overlay, e.g. accepted types and size limit. */
124
+ dropHint?: React.ReactNode
125
+ /** `sm` is a tighter composer for narrow chat columns and side panels. */
126
+ size?: PromptInputSize
127
+ }
128
+
129
+ /**
130
+ * PromptInput - the composer for an AI feature: an auto-growing field (up to
131
+ * about 8 lines), Enter to send and Shift+Enter for a new line, Send or Stop,
132
+ * a character limit, and slots for attachments, tools and voice.
133
+ *
134
+ * @example
135
+ * <PromptInput onSubmit={send} busy={replying} onStop={stop} toolbar={<AttachButton />} />
136
+ */
137
+ export const PromptInput = React.forwardRef<HTMLDivElement, PromptInputProps>(
138
+ (
139
+ {
140
+ value: valueProp, defaultValue = "", onValueChange, onSubmit, busy = false, onStop, disabled = false,
141
+ disabledReason, placeholder = "Ask anything", label = "Prompt", maxLength = 2000, hasAttachments = false,
142
+ canSubmit = true, hint: extraHint, attachments, toolbar, endActions, onFilesPaste, onFilesDrop, dropHint, size = "md", className, ...props
143
+ },
144
+ ref,
145
+ ) => {
146
+ const id = React.useId()
147
+ const [uncontrolled, setUncontrolled] = React.useState(defaultValue)
148
+ const text = valueProp ?? uncontrolled
149
+ const fieldRef = React.useRef<HTMLTextAreaElement>(null)
150
+ const [dragging, setDragging] = React.useState(false)
151
+ const sizes = SIZES[size]
152
+ const droppable = Boolean(onFilesDrop) && !disabled
153
+
154
+ function setText(next: string) {
155
+ if (valueProp === undefined) setUncontrolled(next)
156
+ onValueChange?.(next)
157
+ }
158
+
159
+ React.useLayoutEffect(() => {
160
+ const field = fieldRef.current
161
+ if (!field) return
162
+ field.style.height = "auto"
163
+ field.style.height = `${Math.min(field.scrollHeight, sizes.maxHeight)}px`
164
+ }, [text, sizes.maxHeight])
165
+
166
+ const tooLong = text.length > maxLength
167
+ const hasContent = text.trim().length > 0 || hasAttachments
168
+ const canSend = hasContent && !tooLong && canSubmit && !disabled && !busy
169
+ const tone = characterCountTone(text.length, maxLength)
170
+
171
+ function submit() {
172
+ if (!canSend) return
173
+ onSubmit(text.trim())
174
+ setText("")
175
+ fieldRef.current?.focus()
176
+ }
177
+
178
+ const hint = disabled
179
+ ? disabledReason
180
+ : tooLong
181
+ ? `Shorten your message by ${(text.length - maxLength).toLocaleString("en-US")} characters to send it.`
182
+ : extraHint
183
+
184
+ return (
185
+ <PromptInputSizeContext.Provider value={size}>
186
+ <div
187
+ ref={ref}
188
+ data-slot="prompt-input"
189
+ data-size={size}
190
+ className={cn(
191
+ sizes.root,
192
+ "ax:relative ax:flex ax:cursor-text ax:flex-col ax:border ax:border-input ax:bg-popover ax:pt-1 ax:shadow-xs ax:transition-[border-color,box-shadow]",
193
+ "ax:focus-within:border-ring ax:focus-within:ring-2 ax:focus-within:ring-ring/20",
194
+ tooLong && "ax:border-destructive ax:ring-2 ax:ring-destructive/20 ax:focus-within:border-destructive ax:focus-within:ring-destructive/20",
195
+ disabled && "ax:cursor-not-allowed ax:opacity-60",
196
+ className,
197
+ )}
198
+ onClick={(event) => {
199
+ if (!disabled && event.target === event.currentTarget) fieldRef.current?.focus()
200
+ }}
201
+ onDragOver={droppable ? (event) => {
202
+ if (!isFileDrag(event.dataTransfer.types)) return
203
+ event.preventDefault()
204
+ setDragging(true)
205
+ } : undefined}
206
+ onDragLeave={droppable ? (event) => {
207
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDragging(false)
208
+ } : undefined}
209
+ onDrop={droppable ? (event) => {
210
+ if (!isFileDrag(event.dataTransfer.types)) return
211
+ event.preventDefault()
212
+ setDragging(false)
213
+ onFilesDrop?.([...event.dataTransfer.files])
214
+ } : undefined}
215
+ {...props}
216
+ >
217
+ {attachments && <div className={sizes.edge}>{attachments}</div>}
218
+
219
+ <Textarea
220
+ ref={fieldRef}
221
+ value={text}
222
+ disabled={disabled}
223
+ onChange={(event) => setText(event.target.value)}
224
+ onPaste={(event) => {
225
+ const files = [...event.clipboardData.files]
226
+ if (files.length && onFilesPaste) {
227
+ event.preventDefault()
228
+ onFilesPaste(files)
229
+ }
230
+ }}
231
+ onKeyDown={(event) => {
232
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
233
+ event.preventDefault()
234
+ submit()
235
+ }
236
+ }}
237
+ rows={1}
238
+ resize="none"
239
+ placeholder={placeholder}
240
+ aria-label={label}
241
+ aria-invalid={tooLong || undefined}
242
+ aria-describedby={hint ? `${id}-hint` : undefined}
243
+ // The container owns the border, focus ring and invalid ring; the field stays bare inside it.
244
+ className={cn(sizes.field, "ax:border-0 ax:bg-transparent ax:text-sm ax:leading-snug ax:shadow-none ax:focus-visible:ring-0 ax:focus-visible:ring-offset-0 ax:aria-[invalid=true]:ring-0 ax:disabled:bg-transparent")}
245
+ />
246
+
247
+ <div className={cn("ax:flex ax:items-center ax:justify-between ax:gap-xs", sizes.row)}>
248
+ <div className="ax:flex ax:items-center ax:gap-xs">{toolbar}</div>
249
+ <div className="ax:flex ax:items-center ax:gap-xs">
250
+ {tone && (
251
+ <Text size="1" color={tone} weight="medium" className="ax:tabular-nums" aria-live="polite">
252
+ {text.length.toLocaleString("en-US")} / {maxLength.toLocaleString("en-US")}
253
+ </Text>
254
+ )}
255
+ {endActions}
256
+ {busy ? (
257
+ <Button size="icon" aria-label="Stop generating" onClick={onStop} className={cn("ax:rounded-full", sizes.button)}>
258
+ <span className="ax:size-3 ax:rounded-xs ax:bg-current" aria-hidden />
259
+ </Button>
260
+ ) : (
261
+ <Button size="icon" aria-label="Send message" disabled={!canSend} onClick={submit} className={cn("ax:rounded-full", sizes.button)}>
262
+ <ArrowUp />
263
+ </Button>
264
+ )}
265
+ </div>
266
+ </div>
267
+
268
+ {hint && (
269
+ <Text as="p" id={`${id}-hint`} size="1" color={tooLong ? "destructive" : "muted"} className={sizes.hint}>
270
+ {hint}
271
+ </Text>
272
+ )}
273
+
274
+ {droppable && dragging && (
275
+ <div className="ax:pointer-events-none ax:absolute ax:inset-0 ax:flex ax:flex-col ax:items-center ax:justify-center ax:gap-xs ax:rounded-[inherit] ax:border-2 ax:border-dashed ax:border-primary ax:bg-background/90">
276
+ <Upload className="ax:size-5 ax:text-primary-foreground" aria-hidden />
277
+ <Text size="2" weight="medium">Drop files to attach</Text>
278
+ {dropHint && <Text size="1" color="muted">{dropHint}</Text>}
279
+ </div>
280
+ )}
281
+ </div>
282
+ </PromptInputSizeContext.Provider>
283
+ )
284
+ },
285
+ )
286
+ PromptInput.displayName = "PromptInput"
287
+
288
+ export interface AttachmentChipProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
289
+ name: string
290
+ /** Bytes; shown once the upload finishes. */
291
+ size: number
292
+ kind?: "image" | "file"
293
+ /** Thumbnail for images. */
294
+ previewUrl?: string
295
+ /** 0-100 while uploading; omit or 100 when done. */
296
+ progress?: number
297
+ /** Replaces size and progress, and tints the chip. */
298
+ error?: string
299
+ onRemove: () => void
300
+ }
301
+
302
+ /** One file attached to a prompt: thumbnail or icon, name, then upload progress, size or the error. */
303
+ export const AttachmentChip = React.forwardRef<HTMLDivElement, AttachmentChipProps>(
304
+ ({ name, size, kind = "file", previewUrl, progress = 100, error, onRemove, className, ...props }, ref) => {
305
+ const uploading = !error && progress < 100
306
+ return (
307
+ <div
308
+ ref={ref}
309
+ data-slot="attachment-chip"
310
+ className={cn(
311
+ "ax:flex ax:w-64 ax:items-center ax:gap-sm ax:rounded-md ax:border ax:border-border ax:bg-card ax:p-xs",
312
+ error && "ax:border-destructive-weaker ax:bg-destructive-weakest",
313
+ className,
314
+ )}
315
+ {...props}
316
+ >
317
+ <div className="ax:flex ax:size-10 ax:shrink-0 ax:items-center ax:justify-center ax:overflow-hidden ax:rounded ax:bg-muted ax:text-muted-foreground">
318
+ {previewUrl ? (
319
+ <img src={previewUrl} alt="" className="ax:size-full ax:object-cover" />
320
+ ) : kind === "image" ? (
321
+ <ImageIcon className="ax:size-4" aria-hidden />
322
+ ) : (
323
+ <FileText className="ax:size-4" aria-hidden />
324
+ )}
325
+ </div>
326
+ <div className="ax:flex ax:min-w-0 ax:flex-1 ax:flex-col ax:gap-1">
327
+ <Text size="2" weight="medium" truncate>{name}</Text>
328
+ {error ? (
329
+ <Text size="1" color="destructive" truncate>{error}</Text>
330
+ ) : uploading ? (
331
+ <Progress value={progress} size="sm" aria-label={`Uploading ${name}`} />
332
+ ) : (
333
+ <Text size="1" color="muted">{formatFileSize(size)}</Text>
334
+ )}
335
+ </div>
336
+ <Button variant="ghost" size="icon-sm" aria-label={`Remove ${name}`} onClick={onRemove} className="ax:shrink-0 ax:rounded-full">
337
+ <X />
338
+ </Button>
339
+ </div>
340
+ )
341
+ },
342
+ )
343
+ AttachmentChip.displayName = "AttachmentChip"
@@ -0,0 +1,53 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Building2, Calculator, Home, MapPin } from "lucide-react"
4
+ import { StarterPrompts, SuggestionChips } from "./prompt-suggestions"
5
+
6
+ const FOLLOW_UPS = [
7
+ "Compare with Saadiyat Lagoons",
8
+ "Show only 4-bed townhouses",
9
+ "What are the service charges?",
10
+ "Book a viewing this weekend",
11
+ ]
12
+
13
+ const meta: Meta<typeof SuggestionChips> = {
14
+ title: "Components/SuggestionChips",
15
+ component: SuggestionChips,
16
+ tags: ["autodocs"],
17
+ argTypes: { onPick: { control: false }, onRefresh: { control: false } },
18
+ args: { suggestions: FOLLOW_UPS, onPick: () => undefined },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof SuggestionChips>
23
+
24
+ export const Default: Story = {}
25
+
26
+ export const Wrapped: Story = { args: { wrap: true } }
27
+
28
+ export const Loading: Story = { args: { loading: true, onRefresh: () => undefined } }
29
+
30
+ export const InsertTemplates: Story = {
31
+ args: {
32
+ insert: true,
33
+ wrap: true,
34
+ label: "Prompt templates",
35
+ suggestions: ["Find [bedrooms]-bed homes in [community]", "What's the price per sq ft in [community]?"],
36
+ },
37
+ }
38
+
39
+ export const Starters: Story = {
40
+ render: () => (
41
+ <div style={{ maxWidth: 640 }}>
42
+ <StarterPrompts
43
+ onPick={() => undefined}
44
+ starters={[
45
+ { icon: Home, title: "Find a home", prompt: "Show me 3-bed villas in Yas Island under AED 6M" },
46
+ { icon: Calculator, title: "Check affordability", prompt: "What can I afford with AED 40,000 a month?" },
47
+ { icon: MapPin, title: "Compare communities", prompt: "Compare Saadiyat and Al Reem for families" },
48
+ { icon: Building2, title: "Track a project", prompt: "When is Saadiyat Lagoons phase 2 handing over?" },
49
+ ]}
50
+ />
51
+ </div>
52
+ ),
53
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { StarterPrompts, SuggestionChips } from "./prompt-suggestions"
5
+
6
+ const pick = () => undefined
7
+
8
+ describe("SuggestionChips", () => {
9
+ it("renders one button per prompt inside a labelled group", () => {
10
+ const html = renderToStaticMarkup(<SuggestionChips suggestions={["One", "Two"]} onPick={pick} label="Follow-ups" />)
11
+ expect(html).toMatch(/role="group" aria-label="Follow-ups" aria-busy="false"/)
12
+ expect(html.match(/<button/g)).toHaveLength(2)
13
+ })
14
+
15
+ it("swaps chips for skeletons and marks the group busy while loading", () => {
16
+ const html = renderToStaticMarkup(<SuggestionChips suggestions={["One", "Two"]} onPick={pick} loading />)
17
+ expect(html).toContain('aria-busy="true"')
18
+ expect(html).not.toContain("<button")
19
+ expect(html).toContain("Loading suggestions")
20
+ })
21
+
22
+ it("labels insert-mode chips as edits", () => {
23
+ const html = renderToStaticMarkup(<SuggestionChips suggestions={["Find homes"]} onPick={pick} insert />)
24
+ expect(html).toContain('aria-label="Edit prompt: Find homes"')
25
+ })
26
+ })
27
+
28
+ describe("StarterPrompts", () => {
29
+ it("shows each starter's title and full prompt", () => {
30
+ const html = renderToStaticMarkup(
31
+ <StarterPrompts starters={[{ title: "Find a home", prompt: "Show me villas" }]} onPick={pick} />,
32
+ )
33
+ expect(html).toContain('aria-label="Ways to start"')
34
+ expect(html).toContain("Find a home")
35
+ expect(html).toContain("Show me villas")
36
+ })
37
+ })
@@ -0,0 +1,122 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { PenLine, RefreshCw } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+ import { Button } from "./button"
7
+ import { Skeleton } from "./skeleton"
8
+ import { Text } from "./text"
9
+
10
+ export interface SuggestionChipsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
11
+ suggestions: string[]
12
+ onPick: (prompt: string) => void
13
+ /** Accessible name of the chip group. */
14
+ label?: string
15
+ /** Swaps each chip for a skeleton and marks the group busy. */
16
+ loading?: boolean
17
+ /** E.g. while the assistant replies. */
18
+ disabled?: boolean
19
+ /** Chips put the prompt in the composer for editing instead of sending it. */
20
+ insert?: boolean
21
+ /** Adds an "Other suggestions" button under the row. */
22
+ onRefresh?: () => void
23
+ /** Wrap onto several lines instead of one horizontally scrolling row. */
24
+ wrap?: boolean
25
+ }
26
+
27
+ /**
28
+ * SuggestionChips - a row of prompt chips. Each chip is a button labelled with
29
+ * the full prompt it sends.
30
+ *
31
+ * @example
32
+ * <SuggestionChips suggestions={followUps} onPick={send} onRefresh={nextRound} />
33
+ */
34
+ export const SuggestionChips = React.forwardRef<HTMLDivElement, SuggestionChipsProps>(
35
+ ({ suggestions, onPick, label = "Suggested prompts", loading = false, disabled = false, insert = false, onRefresh, wrap = false, className, ...props }, ref) => (
36
+ <div ref={ref} data-slot="suggestion-chips" className={cn("ax:flex ax:flex-col ax:gap-xs", className)} {...props}>
37
+ <div
38
+ role="group"
39
+ aria-label={label}
40
+ aria-busy={loading}
41
+ className={cn("ax:flex ax:gap-xs", wrap ? "ax:flex-wrap" : "ax:overflow-x-auto ax:pb-1")}
42
+ >
43
+ {loading
44
+ ? suggestions.map((prompt) => <Skeleton key={prompt} className="ax:h-3xl ax:w-44 ax:shrink-0 ax:rounded-full" />)
45
+ : suggestions.map((prompt) => (
46
+ <Button
47
+ key={prompt}
48
+ variant="outline"
49
+ size="sm"
50
+ disabled={disabled}
51
+ onClick={() => onPick(prompt)}
52
+ aria-label={insert ? `Edit prompt: ${prompt}` : undefined}
53
+ className="ax:shrink-0 ax:rounded-full ax:font-normal"
54
+ >
55
+ {insert && <PenLine />}
56
+ {prompt}
57
+ </Button>
58
+ ))}
59
+ </div>
60
+ {/* Outside the scrolling row, so it never ends up hidden past the last chip. */}
61
+ {onRefresh && (
62
+ <Button variant="ghost" size="sm" disabled={disabled || loading} onClick={onRefresh} className="ax:self-start ax:rounded-full">
63
+ <RefreshCw />
64
+ Other suggestions
65
+ </Button>
66
+ )}
67
+ <span className="ax:sr-only" aria-live="polite">{loading ? "Loading suggestions" : ""}</span>
68
+ </div>
69
+ ),
70
+ )
71
+ SuggestionChips.displayName = "SuggestionChips"
72
+
73
+ export interface StarterPromptItem {
74
+ /** Decorative; the title names the starter. */
75
+ icon?: React.ElementType
76
+ /** Short, scannable name. */
77
+ title: string
78
+ /** The full prompt sent when picked. */
79
+ prompt: string
80
+ }
81
+
82
+ export interface StarterPromptsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
83
+ starters: StarterPromptItem[]
84
+ onPick: (prompt: string) => void
85
+ /** Accessible name of the group. */
86
+ label?: string
87
+ }
88
+
89
+ /**
90
+ * StarterPrompts - empty-state starters in a two-column grid: a short title
91
+ * for scanning, the full prompt underneath.
92
+ */
93
+ export const StarterPrompts = React.forwardRef<HTMLDivElement, StarterPromptsProps>(
94
+ ({ starters, onPick, label = "Ways to start", className, ...props }, ref) => (
95
+ <div
96
+ ref={ref}
97
+ data-slot="starter-prompts"
98
+ role="group"
99
+ aria-label={label}
100
+ className={cn("ax:grid ax:gap-sm ax:sm:grid-cols-2", className)}
101
+ {...props}
102
+ >
103
+ {starters.map(({ icon: Icon, title, prompt }) => (
104
+ <Button
105
+ key={title}
106
+ variant="outline"
107
+ onClick={() => onPick(prompt)}
108
+ // Icon in its own column so the prompt lines up under the title, not the icon.
109
+ className={cn(
110
+ "ax:grid ax:h-auto ax:items-center ax:justify-start ax:gap-x-xs ax:gap-y-1 ax:whitespace-normal ax:rounded-md ax:p-sm ax:text-start ax:shadow-none ax:hover:bg-muted",
111
+ Icon ? "ax:grid-cols-[auto_minmax(0,1fr)]" : "ax:grid-cols-1",
112
+ )}
113
+ >
114
+ {Icon && <Icon className="ax:text-primary-foreground" aria-hidden />}
115
+ <Text size="2" weight="medium">{title}</Text>
116
+ <Text size="1" color="muted" wrap="pretty" className={cn("ax:font-normal", Icon && "ax:col-start-2")}>{prompt}</Text>
117
+ </Button>
118
+ ))}
119
+ </div>
120
+ ),
121
+ )
122
+ StarterPrompts.displayName = "StarterPrompts"
@@ -0,0 +1,19 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { QuotaBar } from "./quota-bar"
4
+
5
+ const meta: Meta<typeof QuotaBar> = {
6
+ title: "Components/QuotaBar",
7
+ component: QuotaBar,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ used: 72,
11
+ total: 100,
12
+ unit: "GB",
13
+ },
14
+ }
15
+
16
+ export default meta
17
+ type Story = StoryObj<typeof QuotaBar>
18
+
19
+ export const Default: Story = {}