@adres/mgda-ui 0.0.0-stage → 0.0.2

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,170 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronDown } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+
7
+ interface StepContextValue {
8
+ open: boolean
9
+ setOpen: (open: boolean) => void
10
+ contentId: string
11
+ }
12
+
13
+ const StepContext = React.createContext<StepContextValue | null>(null)
14
+
15
+ function useStep(part: string) {
16
+ const context = React.useContext(StepContext)
17
+ if (!context) throw new Error(`${part} must be used inside ChainOfThoughtStep`)
18
+ return context
19
+ }
20
+
21
+ export type ChainOfThoughtProps = React.HTMLAttributes<HTMLDivElement>
22
+
23
+ /**
24
+ * ChainOfThought - the reasoning behind an assistant answer as a vertical
25
+ * chain of steps. Each step is a one-line trigger that expands to its detail,
26
+ * and a thin rule joins one step to the next.
27
+ *
28
+ * @example
29
+ * <ChainOfThought>
30
+ * <ChainOfThoughtStep>
31
+ * <ChainOfThoughtTrigger>Searching Yas Island listings</ChainOfThoughtTrigger>
32
+ * <ChainOfThoughtContent>
33
+ * <ChainOfThoughtItem>Found 42 townhouses under AED 5M</ChainOfThoughtItem>
34
+ * </ChainOfThoughtContent>
35
+ * </ChainOfThoughtStep>
36
+ * </ChainOfThought>
37
+ */
38
+ export const ChainOfThought = React.forwardRef<HTMLDivElement, ChainOfThoughtProps>(
39
+ ({ className, ...props }, ref) => (
40
+ <div ref={ref} data-slot="chain-of-thought" className={cn("ax:flex ax:flex-col", className)} {...props} />
41
+ ),
42
+ )
43
+ ChainOfThought.displayName = "ChainOfThought"
44
+
45
+ export interface ChainOfThoughtStepProps extends React.HTMLAttributes<HTMLDivElement> {
46
+ open?: boolean
47
+ defaultOpen?: boolean
48
+ onOpenChange?: (open: boolean) => void
49
+ }
50
+
51
+ /** One step in the chain. Closed by default; control it with `open` and `onOpenChange`. */
52
+ export const ChainOfThoughtStep = React.forwardRef<HTMLDivElement, ChainOfThoughtStepProps>(
53
+ ({ open: openProp, defaultOpen = false, onOpenChange, className, children, ...props }, ref) => {
54
+ const contentId = React.useId()
55
+ const [uncontrolled, setUncontrolled] = React.useState(defaultOpen)
56
+ const open = openProp ?? uncontrolled
57
+ const setOpen = React.useCallback(
58
+ (next: boolean) => {
59
+ if (openProp === undefined) setUncontrolled(next)
60
+ onOpenChange?.(next)
61
+ },
62
+ [openProp, onOpenChange],
63
+ )
64
+
65
+ return (
66
+ <StepContext.Provider value={{ open, setOpen, contentId }}>
67
+ <div
68
+ ref={ref}
69
+ data-slot="chain-of-thought-step"
70
+ data-state={open ? "open" : "closed"}
71
+ className={cn("ax:group/cot-step", className)}
72
+ {...props}
73
+ >
74
+ {children}
75
+ <div className="ax:flex ax:w-4 ax:justify-center ax:group-last/cot-step:hidden" aria-hidden>
76
+ <div className="ax:h-sm ax:w-px ax:bg-border" />
77
+ </div>
78
+ </div>
79
+ </StepContext.Provider>
80
+ )
81
+ },
82
+ )
83
+ ChainOfThoughtStep.displayName = "ChainOfThoughtStep"
84
+
85
+ export interface ChainOfThoughtTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
86
+ /** Replaces the default dot. Swaps to a chevron on hover and focus unless `swapIconOnHover` is false. */
87
+ icon?: React.ReactNode
88
+ swapIconOnHover?: boolean
89
+ }
90
+
91
+ /** The step's one-line label; toggles its content. */
92
+ export const ChainOfThoughtTrigger = React.forwardRef<HTMLButtonElement, ChainOfThoughtTriggerProps>(
93
+ ({ icon, swapIconOnHover = true, className, children, onClick, ...props }, ref) => {
94
+ const { open, setOpen, contentId } = useStep("ChainOfThoughtTrigger")
95
+ const swap = Boolean(icon) && swapIconOnHover
96
+ const chevron = cn("ax:size-4 ax:transition-transform ax:motion-reduce:transition-none", open && "ax:rotate-180")
97
+
98
+ return (
99
+ <button
100
+ ref={ref}
101
+ type="button"
102
+ aria-expanded={open}
103
+ aria-controls={contentId}
104
+ onClick={(event) => {
105
+ onClick?.(event)
106
+ if (!event.defaultPrevented) setOpen(!open)
107
+ }}
108
+ className={cn(
109
+ "ax:group/cot-trigger ax:flex ax:items-center ax:gap-sm ax:rounded-xs ax:text-start ax:t-body-sm ax:text-muted-foreground ax:transition-colors",
110
+ "ax:hover:text-foreground ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring",
111
+ className,
112
+ )}
113
+ {...props}
114
+ >
115
+ <span className="ax:relative ax:inline-flex ax:size-4 ax:shrink-0 ax:items-center ax:justify-center ax:[&_svg]:size-4" aria-hidden>
116
+ {icon ? (
117
+ <span className={cn("ax:inline-flex ax:transition-opacity", swap && "ax:group-hover/cot-trigger:opacity-0 ax:group-focus-visible/cot-trigger:opacity-0")}>
118
+ {icon}
119
+ </span>
120
+ ) : (
121
+ <span className="ax:size-2 ax:rounded-full ax:bg-current" />
122
+ )}
123
+ {swap && (
124
+ <ChevronDown
125
+ className={cn(chevron, "ax:absolute ax:opacity-0 ax:group-hover/cot-trigger:opacity-100 ax:group-focus-visible/cot-trigger:opacity-100")}
126
+ />
127
+ )}
128
+ </span>
129
+ <span className="ax:min-w-0">{children}</span>
130
+ {!swap && <ChevronDown className={cn(chevron, "ax:shrink-0")} aria-hidden />}
131
+ </button>
132
+ )
133
+ },
134
+ )
135
+ ChainOfThoughtTrigger.displayName = "ChainOfThoughtTrigger"
136
+
137
+ export type ChainOfThoughtContentProps = React.HTMLAttributes<HTMLDivElement>
138
+
139
+ /** The step's detail, beside a rule that carries on to the next step. Rendered only while open. */
140
+ export const ChainOfThoughtContent = React.forwardRef<HTMLDivElement, ChainOfThoughtContentProps>(
141
+ ({ className, children, ...props }, ref) => {
142
+ const { open, contentId } = useStep("ChainOfThoughtContent")
143
+ if (!open) return null
144
+ return (
145
+ <div
146
+ ref={ref}
147
+ id={contentId}
148
+ data-slot="chain-of-thought-content"
149
+ className={cn("ax:grid ax:grid-cols-[1rem_minmax(0,1fr)] ax:gap-x-sm", className)}
150
+ {...props}
151
+ >
152
+ <div className="ax:flex ax:justify-center" aria-hidden>
153
+ <div className="ax:w-px ax:bg-border ax:group-last/cot-step:bg-transparent" />
154
+ </div>
155
+ <div className="ax:mt-sm ax:flex ax:min-w-0 ax:flex-col ax:gap-sm">{children}</div>
156
+ </div>
157
+ )
158
+ },
159
+ )
160
+ ChainOfThoughtContent.displayName = "ChainOfThoughtContent"
161
+
162
+ export type ChainOfThoughtItemProps = React.HTMLAttributes<HTMLDivElement>
163
+
164
+ /** One line of detail inside a step: a finding, a source, or a short note. */
165
+ export const ChainOfThoughtItem = React.forwardRef<HTMLDivElement, ChainOfThoughtItemProps>(
166
+ ({ className, ...props }, ref) => (
167
+ <div ref={ref} data-slot="chain-of-thought-item" className={cn("ax:t-body-sm ax:text-muted-foreground", className)} {...props} />
168
+ ),
169
+ )
170
+ ChainOfThoughtItem.displayName = "ChainOfThoughtItem"
@@ -0,0 +1,47 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { ChatContainerContent, ChatContainerRoot, ChatScrollButton } from "./chat-container"
4
+ import { ChatMessage } from "./chat-message"
5
+ import { Button } from "./button"
6
+
7
+ const meta: Meta<typeof ChatContainerRoot> = {
8
+ title: "Components/ChatContainer",
9
+ component: ChatContainerRoot,
10
+ tags: ["autodocs"],
11
+ decorators: [(Story) => <div style={{ maxWidth: 560 }}><Story /></div>],
12
+ }
13
+
14
+ export default meta
15
+ type Story = StoryObj<typeof ChatContainerRoot>
16
+
17
+ const TURNS = [
18
+ "Any townhouses on Yas Island under AED 5M?",
19
+ "Yes. 42 townhouses in Yas Acres and Noya are under AED 5M, and 11 have a private garden.",
20
+ "Which is cheapest?",
21
+ "The lowest asking price is AED 4.2M for a 3-bed in Yas Acres.",
22
+ "How does that compare with recent sales?",
23
+ "The median registered sale in Yas Acres over the last 12 months is AED 4.6M, so it is priced below recent sales.",
24
+ ]
25
+
26
+ function Conversation() {
27
+ const [turns, setTurns] = React.useState(TURNS)
28
+ return (
29
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
30
+ <ChatContainerRoot className="ax:h-80 ax:rounded-md ax:border">
31
+ <ChatContainerContent className="ax:gap-lg ax:p-md">
32
+ {turns.map((text, index) => (
33
+ <ChatMessage key={index} from={index % 2 === 0 ? "user" : "assistant"}>{text}</ChatMessage>
34
+ ))}
35
+ </ChatContainerContent>
36
+ <ChatScrollButton className="ax:absolute ax:bottom-sm ax:left-1/2 ax:-translate-x-1/2" />
37
+ </ChatContainerRoot>
38
+ <div>
39
+ <Button variant="outline" size="sm" onClick={() => setTurns((prev) => [...prev, "Another reply from the assistant."])}>
40
+ Add reply
41
+ </Button>
42
+ </div>
43
+ </div>
44
+ )
45
+ }
46
+
47
+ export const Default: Story = { render: () => <Conversation /> }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { ChatContainerContent, ChatContainerRoot, ChatScrollButton, useChatContainer } from "./chat-container"
5
+
6
+ describe("ChatContainer", () => {
7
+ it("renders a focusable log named Conversation by default", () => {
8
+ const html = renderToStaticMarkup(
9
+ <ChatContainerRoot>
10
+ <ChatContainerContent>Hello</ChatContainerContent>
11
+ </ChatContainerRoot>,
12
+ )
13
+ expect(html).toContain('role="log"')
14
+ expect(html).toContain('aria-label="Conversation"')
15
+ expect(html).toContain('tabindex="0"')
16
+ })
17
+
18
+ it("lets the caller rename the log", () => {
19
+ const html = renderToStaticMarkup(
20
+ <ChatContainerRoot>
21
+ <ChatContainerContent aria-label="Chat with Adres">Hello</ChatContainerContent>
22
+ </ChatContainerRoot>,
23
+ )
24
+ expect(html).toContain('aria-label="Chat with Adres"')
25
+ })
26
+
27
+ it("starts at the bottom, so the scroll button is hidden and out of the tab order", () => {
28
+ const html = renderToStaticMarkup(
29
+ <ChatContainerRoot>
30
+ <ChatScrollButton />
31
+ </ChatContainerRoot>,
32
+ )
33
+ expect(html).toContain('aria-hidden="true"')
34
+ expect(html).toContain('tabindex="-1"')
35
+ expect(html).toContain('aria-label="Scroll to latest"')
36
+ })
37
+
38
+ it("exposes the scroll state through useChatContainer", () => {
39
+ function Probe() {
40
+ const { isAtBottom, hasUnseen } = useChatContainer()
41
+ return <span>{`${isAtBottom}-${hasUnseen}`}</span>
42
+ }
43
+ expect(renderToStaticMarkup(<ChatContainerRoot><Probe /></ChatContainerRoot>)).toContain("true-false")
44
+ })
45
+
46
+ it("throws when a part is used outside the root", () => {
47
+ expect(() => renderToStaticMarkup(<ChatScrollButton />)).toThrow(/ChatContainerRoot/)
48
+ })
49
+ })
@@ -0,0 +1,222 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ArrowDown } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+ import { Button, type ButtonProps } from "./button"
7
+
8
+ /** How close to the bottom, in px, still counts as reading the latest. */
9
+ const STICK_THRESHOLD = 48
10
+
11
+ export interface ChatContainerState {
12
+ /** True while the log follows new content: the reader is at the bottom or jumped back to it. */
13
+ isAtBottom: boolean
14
+ /** True when content arrived while the reader was scrolled up. Clears at the bottom. */
15
+ hasUnseen: boolean
16
+ /** Scrolls to the latest and resumes following. Call it when the reader sends a message. */
17
+ scrollToBottom: (behavior?: ScrollBehavior) => void
18
+ }
19
+
20
+ interface ChatContainerContextValue extends ChatContainerState {
21
+ scrollRef: React.RefObject<HTMLDivElement | null>
22
+ onScroll: () => void
23
+ }
24
+
25
+ const ChatContainerContext = React.createContext<ChatContainerContextValue | null>(null)
26
+
27
+ function useChatContainerContext(part: string) {
28
+ const context = React.useContext(ChatContainerContext)
29
+ if (!context) throw new Error(`${part} must be used inside ChatContainerRoot`)
30
+ return context
31
+ }
32
+
33
+ /** Scroll state for the nearest ChatContainerRoot, for custom controls and composers. */
34
+ export function useChatContainer(): ChatContainerState {
35
+ const { isAtBottom, hasUnseen, scrollToBottom } = useChatContainerContext("useChatContainer")
36
+ return { isAtBottom, hasUnseen, scrollToBottom }
37
+ }
38
+
39
+ function distanceFromBottom(el: HTMLElement) {
40
+ return el.scrollHeight - el.scrollTop - el.clientHeight
41
+ }
42
+
43
+ export interface ChatContainerRootProps extends React.HTMLAttributes<HTMLDivElement> {}
44
+
45
+ /**
46
+ * ChatContainer - a conversation log that sticks to the latest message. It
47
+ * follows new and streaming content while the reader is at the bottom, stays
48
+ * put once they scroll up, and offers a button back down.
49
+ *
50
+ * @example
51
+ * <ChatContainerRoot className="ax:h-[480px]">
52
+ * <ChatContainerContent className="ax:gap-lg ax:p-md">{messages}</ChatContainerContent>
53
+ * <ChatScrollButton className="ax:absolute ax:bottom-sm ax:left-1/2 ax:-translate-x-1/2" />
54
+ * </ChatContainerRoot>
55
+ */
56
+ export const ChatContainerRoot = React.forwardRef<HTMLDivElement, ChatContainerRootProps>(
57
+ ({ className, children, ...props }, ref) => {
58
+ const scrollRef = React.useRef<HTMLDivElement>(null)
59
+ const following = React.useRef(true)
60
+ const lastScrollTop = React.useRef(0)
61
+ const lastScrollHeight = React.useRef(0)
62
+ const [isAtBottom, setIsAtBottom] = React.useState(true)
63
+ const [hasUnseen, setHasUnseen] = React.useState(false)
64
+
65
+ const scrollToBottom = React.useCallback((behavior: ScrollBehavior = "smooth") => {
66
+ const el = scrollRef.current
67
+ if (!el) return
68
+ following.current = true
69
+ setIsAtBottom(true)
70
+ setHasUnseen(false)
71
+ el.scrollTo({ top: el.scrollHeight, behavior })
72
+ }, [])
73
+
74
+ // Only a scroll up stops following, so a smooth scroll down never flickers the button back.
75
+ const onScroll = React.useCallback(() => {
76
+ const el = scrollRef.current
77
+ if (!el) return
78
+ const near = distanceFromBottom(el) < STICK_THRESHOLD
79
+ if (near) {
80
+ following.current = true
81
+ setHasUnseen(false)
82
+ } else if (el.scrollTop < lastScrollTop.current) {
83
+ following.current = false
84
+ }
85
+ lastScrollTop.current = el.scrollTop
86
+ setIsAtBottom(following.current)
87
+ }, [])
88
+
89
+ React.useLayoutEffect(() => {
90
+ const el = scrollRef.current
91
+ if (!el) return
92
+ el.scrollTop = el.scrollHeight
93
+ lastScrollTop.current = el.scrollTop
94
+ lastScrollHeight.current = el.scrollHeight
95
+ }, [])
96
+
97
+ // Mutations catch new turns and streamed text; resizes catch the viewport changing.
98
+ React.useEffect(() => {
99
+ const el = scrollRef.current
100
+ if (!el) return
101
+ const update = () => {
102
+ const grew = el.scrollHeight > lastScrollHeight.current
103
+ lastScrollHeight.current = el.scrollHeight
104
+ if (following.current) {
105
+ el.scrollTop = el.scrollHeight
106
+ lastScrollTop.current = el.scrollTop
107
+ } else if (grew) {
108
+ setHasUnseen(true)
109
+ }
110
+ }
111
+ const mutations = new MutationObserver(update)
112
+ mutations.observe(el, { childList: true, subtree: true, characterData: true })
113
+ const resizes = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update)
114
+ resizes?.observe(el)
115
+ return () => {
116
+ mutations.disconnect()
117
+ resizes?.disconnect()
118
+ }
119
+ }, [])
120
+
121
+ const value = React.useMemo(
122
+ () => ({ isAtBottom, hasUnseen, scrollToBottom, scrollRef, onScroll }),
123
+ [isAtBottom, hasUnseen, scrollToBottom, onScroll],
124
+ )
125
+
126
+ return (
127
+ <ChatContainerContext.Provider value={value}>
128
+ <div
129
+ ref={ref}
130
+ data-slot="chat-container"
131
+ className={cn("ax:relative ax:flex ax:min-h-0 ax:flex-col", className)}
132
+ {...props}
133
+ >
134
+ {children}
135
+ </div>
136
+ </ChatContainerContext.Provider>
137
+ )
138
+ },
139
+ )
140
+ ChatContainerRoot.displayName = "ChatContainerRoot"
141
+
142
+ export interface ChatContainerContentProps extends React.HTMLAttributes<HTMLDivElement> {}
143
+
144
+ /** The scrolling log that holds the turns. Named "Conversation" unless you pass `aria-label`. */
145
+ export const ChatContainerContent = React.forwardRef<HTMLDivElement, ChatContainerContentProps>(
146
+ ({ className, onScroll: onScrollProp, ...props }, ref) => {
147
+ const { scrollRef, onScroll } = useChatContainerContext("ChatContainerContent")
148
+ const setRefs = React.useCallback(
149
+ (node: HTMLDivElement | null) => {
150
+ scrollRef.current = node
151
+ if (typeof ref === "function") ref(node)
152
+ else if (ref) ref.current = node
153
+ },
154
+ [ref, scrollRef],
155
+ )
156
+
157
+ return (
158
+ <div
159
+ ref={setRefs}
160
+ data-slot="chat-container-content"
161
+ role="log"
162
+ aria-label="Conversation"
163
+ tabIndex={0}
164
+ onScroll={(event) => {
165
+ onScroll()
166
+ onScrollProp?.(event)
167
+ }}
168
+ className={cn(
169
+ "ax:flex ax:min-h-0 ax:flex-1 ax:flex-col ax:overflow-y-auto",
170
+ "ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-inset",
171
+ className,
172
+ )}
173
+ {...props}
174
+ />
175
+ )
176
+ },
177
+ )
178
+ ChatContainerContent.displayName = "ChatContainerContent"
179
+
180
+ export interface ChatScrollButtonProps extends ButtonProps {}
181
+
182
+ /**
183
+ * Jumps back to the latest. Shown only while the reader is scrolled up: an
184
+ * arrow on its own, or "New messages" once content arrives. Position it yourself.
185
+ */
186
+ export const ChatScrollButton = React.forwardRef<HTMLButtonElement, ChatScrollButtonProps>(
187
+ ({ className, variant = "outline", size, children, onClick, ...props }, ref) => {
188
+ const { isAtBottom, hasUnseen, scrollToBottom } = useChatContainerContext("ChatScrollButton")
189
+ const label = hasUnseen ? "New messages" : null
190
+
191
+ return (
192
+ <Button
193
+ ref={ref}
194
+ type="button"
195
+ data-slot="chat-scroll-button"
196
+ variant={variant}
197
+ size={size ?? (children || label ? "sm" : "icon-sm")}
198
+ aria-label={children || label ? undefined : "Scroll to latest"}
199
+ aria-hidden={isAtBottom || undefined}
200
+ tabIndex={isAtBottom ? -1 : undefined}
201
+ onClick={(event) => {
202
+ onClick?.(event)
203
+ if (!event.defaultPrevented) scrollToBottom()
204
+ }}
205
+ className={cn(
206
+ "ax:rounded-full ax:bg-background ax:shadow-sm ax:transition-[opacity,translate] ax:duration-150 ax:motion-reduce:transition-none",
207
+ isAtBottom ? "ax:pointer-events-none ax:translate-y-sm ax:opacity-0" : "ax:opacity-100",
208
+ className,
209
+ )}
210
+ {...props}
211
+ >
212
+ {children ?? (
213
+ <>
214
+ <ArrowDown />
215
+ {label}
216
+ </>
217
+ )}
218
+ </Button>
219
+ )
220
+ },
221
+ )
222
+ ChatScrollButton.displayName = "ChatScrollButton"
@@ -0,0 +1,52 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { ChatAvatar, ChatDivider, ChatMessage } from "./chat-message"
4
+ import { MessageActions } from "./message-actions"
5
+ import { Text } from "./text"
6
+ import { TooltipProvider } from "./tooltip"
7
+
8
+ const QUESTION = "Any townhouses on Yas Island under AED 5M with a private garden?"
9
+ const ANSWER = "Yes. Yas Acres has three-bed townhouses with private gardens from AED 4.2M, and two are listed this week."
10
+
11
+ const meta: Meta<typeof ChatMessage> = {
12
+ title: "Components/ChatMessage",
13
+ component: ChatMessage,
14
+ tags: ["autodocs"],
15
+ decorators: [(Story) => <TooltipProvider><div style={{ maxWidth: 640 }}><Story /></div></TooltipProvider>],
16
+ argTypes: {
17
+ from: { control: "inline-radio", options: ["assistant", "user"] },
18
+ status: { control: "inline-radio", options: ["sent", "failed"] },
19
+ onRetry: { control: false },
20
+ },
21
+ args: { from: "assistant", children: ANSWER, meta: "10:42" },
22
+ }
23
+
24
+ export default meta
25
+ type Story = StoryObj<typeof ChatMessage>
26
+
27
+ export const Default: Story = {}
28
+
29
+ export const Conversation: Story = {
30
+ render: () => (
31
+ <div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
32
+ <ChatDivider label="Today" />
33
+ <ChatMessage from="user" meta="Delivered · 10:41">{QUESTION}</ChatMessage>
34
+ <ChatMessage from="assistant" actions={<MessageActions copyText={ANSWER} onRegenerate={() => undefined} revealOnHover />} meta="10:42">
35
+ {ANSWER}
36
+ </ChatMessage>
37
+ </div>
38
+ ),
39
+ }
40
+
41
+ export const FailedUserMessage: Story = {
42
+ args: { from: "user", status: "failed", children: QUESTION, onRetry: () => undefined },
43
+ }
44
+
45
+ export const WithAvatar: Story = { args: { avatar: <ChatAvatar /> } }
46
+
47
+ export const CustomContent: Story = {
48
+ args: {
49
+ children: <Text size="2" color="muted">Searched 3 sources, compared 12 listings</Text>,
50
+ meta: undefined,
51
+ },
52
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { ChatAvatar, ChatDivider, ChatMessage } from "./chat-message"
5
+
6
+ describe("ChatMessage", () => {
7
+ it("names each turn by who wrote it", () => {
8
+ expect(renderToStaticMarkup(<ChatMessage from="assistant">Hi</ChatMessage>)).toContain('aria-label="Assistant message"')
9
+ expect(renderToStaticMarkup(<ChatMessage from="user">Hi</ChatMessage>)).toContain('aria-label="Your message"')
10
+ })
11
+
12
+ it("replaces the meta line with a retry action when a user message failed", () => {
13
+ const html = renderToStaticMarkup(
14
+ <ChatMessage from="user" status="failed" meta="Delivered · 10:41" onRetry={() => undefined}>Hi</ChatMessage>,
15
+ )
16
+ expect(html).toContain("Not delivered.")
17
+ expect(html).toMatch(/<button type="button"[^>]*>Retry<\/button>/)
18
+ expect(html).not.toContain("Delivered · 10:41")
19
+ })
20
+
21
+ it("shows no avatar on assistant turns unless one is passed", () => {
22
+ expect(renderToStaticMarkup(<ChatMessage from="assistant">Hi</ChatMessage>)).not.toContain("<svg")
23
+ expect(renderToStaticMarkup(<ChatMessage from="assistant" avatar={<ChatAvatar />}>Hi</ChatMessage>)).toContain("<svg")
24
+ })
25
+
26
+ it("puts assistant text on the page and user text in a bubble", () => {
27
+ expect(renderToStaticMarkup(<ChatMessage from="assistant">Hi</ChatMessage>)).not.toContain("ax:bg-muted")
28
+ expect(renderToStaticMarkup(<ChatMessage from="user">Hi</ChatMessage>)).toContain("ax:bg-muted")
29
+ })
30
+ })
31
+
32
+ describe("ChatDivider", () => {
33
+ it("is a labelled separator", () => {
34
+ expect(renderToStaticMarkup(<ChatDivider label="Today" />)).toMatch(/role="separator" aria-label="Today"/)
35
+ })
36
+ })