@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,48 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { Reasoning, ReasoningContent, ReasoningTrigger } from "./reasoning"
5
+
6
+ const contentTag = (html: string) => html.match(/<div[^>]*data-slot="reasoning-content"[^>]*>/)?.[0] ?? ""
7
+
8
+ function render(props: React.ComponentProps<typeof Reasoning>) {
9
+ return renderToStaticMarkup(
10
+ <Reasoning {...props}>
11
+ <ReasoningTrigger>Reasoning</ReasoningTrigger>
12
+ <ReasoningContent>Compared 42 sales</ReasoningContent>
13
+ </Reasoning>,
14
+ )
15
+ }
16
+
17
+ describe("Reasoning", () => {
18
+ it("starts closed, with the content inert and hidden from assistive tech", () => {
19
+ const html = render({})
20
+ expect(html).toContain('aria-expanded="false"')
21
+ expect(contentTag(html)).toContain('aria-hidden="true"')
22
+ expect(contentTag(html)).toContain("inert")
23
+ })
24
+
25
+ it("starts open while streaming and shimmers the label", () => {
26
+ const html = render({ streaming: true })
27
+ expect(html).toContain('aria-expanded="true"')
28
+ expect(contentTag(html)).not.toContain("aria-hidden")
29
+ expect(contentTag(html)).not.toContain("inert")
30
+ expect(html).toContain('data-slot="shimmer-text"')
31
+ })
32
+
33
+ it("opens with defaultOpen and points the trigger at the content", () => {
34
+ const html = render({ defaultOpen: true })
35
+ const controls = html.match(/aria-controls="([^"]+)"/)?.[1]
36
+ expect(controls).toBeTruthy()
37
+ expect(html).toContain(`id="${controls}"`)
38
+ expect(html).toContain('data-state="open"')
39
+ })
40
+
41
+ it("lets a controlled open win over streaming", () => {
42
+ expect(render({ open: false, streaming: true })).toContain('aria-expanded="false"')
43
+ })
44
+
45
+ it("throws when a part is used outside Reasoning", () => {
46
+ expect(() => renderToStaticMarkup(<ReasoningTrigger>Orphan</ReasoningTrigger>)).toThrow(/Reasoning/)
47
+ })
48
+ })
@@ -0,0 +1,143 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronDown } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+ import { ShimmerText } from "./shimmer-text"
7
+
8
+ interface ReasoningContextValue {
9
+ open: boolean
10
+ setOpen: (open: boolean) => void
11
+ streaming: boolean
12
+ contentId: string
13
+ }
14
+
15
+ const ReasoningContext = React.createContext<ReasoningContextValue | null>(null)
16
+
17
+ function useReasoning(part: string) {
18
+ const context = React.useContext(ReasoningContext)
19
+ if (!context) throw new Error(`${part} must be used inside Reasoning`)
20
+ return context
21
+ }
22
+
23
+ export interface ReasoningProps extends React.HTMLAttributes<HTMLDivElement> {
24
+ open?: boolean
25
+ defaultOpen?: boolean
26
+ onOpenChange?: (open: boolean) => void
27
+ /** True while the model is still reasoning: the panel opens, then closes when it turns false. */
28
+ streaming?: boolean
29
+ }
30
+
31
+ /**
32
+ * Reasoning - the model's thinking behind an answer, in a panel that opens
33
+ * while it streams and folds away once the answer is ready.
34
+ *
35
+ * @example
36
+ * <Reasoning streaming={thinking}>
37
+ * <ReasoningTrigger>Show reasoning</ReasoningTrigger>
38
+ * <ReasoningContent>{reasoningText}</ReasoningContent>
39
+ * </Reasoning>
40
+ */
41
+ export const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
42
+ ({ open: openProp, defaultOpen = false, onOpenChange, streaming = false, className, children, ...props }, ref) => {
43
+ const contentId = React.useId()
44
+ const [uncontrolled, setUncontrolled] = React.useState(defaultOpen || streaming)
45
+ const open = openProp ?? uncontrolled
46
+
47
+ const setOpen = React.useCallback(
48
+ (next: boolean) => {
49
+ if (openProp === undefined) setUncontrolled(next)
50
+ onOpenChange?.(next)
51
+ },
52
+ [openProp, onOpenChange],
53
+ )
54
+
55
+ // Follow streaming edges only, so a manual toggle mid-stream sticks until the next edge.
56
+ const wasStreaming = React.useRef(streaming)
57
+ React.useEffect(() => {
58
+ if (streaming === wasStreaming.current) return
59
+ wasStreaming.current = streaming
60
+ setOpen(streaming)
61
+ }, [streaming, setOpen])
62
+
63
+ const value = React.useMemo(() => ({ open, setOpen, streaming, contentId }), [open, setOpen, streaming, contentId])
64
+
65
+ return (
66
+ <ReasoningContext.Provider value={value}>
67
+ <div ref={ref} data-slot="reasoning" data-state={open ? "open" : "closed"} className={className} {...props}>
68
+ {children}
69
+ </div>
70
+ </ReasoningContext.Provider>
71
+ )
72
+ },
73
+ )
74
+ Reasoning.displayName = "Reasoning"
75
+
76
+ export interface ReasoningTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
77
+
78
+ /** Toggles the panel. The label shimmers while the model is still reasoning. */
79
+ export const ReasoningTrigger = React.forwardRef<HTMLButtonElement, ReasoningTriggerProps>(
80
+ ({ className, children, onClick, ...props }, ref) => {
81
+ const { open, setOpen, streaming, contentId } = useReasoning("ReasoningTrigger")
82
+ return (
83
+ <button
84
+ ref={ref}
85
+ type="button"
86
+ aria-expanded={open}
87
+ aria-controls={contentId}
88
+ onClick={(event) => {
89
+ onClick?.(event)
90
+ if (!event.defaultPrevented) setOpen(!open)
91
+ }}
92
+ className={cn(
93
+ "ax:flex ax:items-center ax:gap-1 ax:rounded-xs ax:t-body-sm ax:text-muted-foreground ax:transition-colors",
94
+ "ax:hover:text-foreground ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring",
95
+ className,
96
+ )}
97
+ {...props}
98
+ >
99
+ {streaming ? <ShimmerText className="ax:font-medium">{children}</ShimmerText> : <span>{children}</span>}
100
+ <ChevronDown className={cn("ax:size-4 ax:transition-transform ax:motion-reduce:transition-none", open && "ax:rotate-180")} aria-hidden />
101
+ </button>
102
+ )
103
+ },
104
+ )
105
+ ReasoningTrigger.displayName = "ReasoningTrigger"
106
+
107
+ export interface ReasoningContentProps extends React.HTMLAttributes<HTMLDivElement> {
108
+ /** Extra classes for the inner text block, inside the animated wrapper. */
109
+ contentClassName?: string
110
+ }
111
+
112
+ /**
113
+ * The reasoning text beside a thin rule. Grows and shrinks with the panel;
114
+ * while closed it stays in the DOM but is inert and hidden from assistive tech.
115
+ */
116
+ export const ReasoningContent = React.forwardRef<HTMLDivElement, ReasoningContentProps>(
117
+ ({ className, contentClassName, children, ...props }, ref) => {
118
+ const { open, contentId } = useReasoning("ReasoningContent")
119
+ return (
120
+ <div
121
+ ref={ref}
122
+ id={contentId}
123
+ data-slot="reasoning-content"
124
+ data-state={open ? "open" : "closed"}
125
+ aria-hidden={!open || undefined}
126
+ inert={!open}
127
+ className={cn(
128
+ "ax:grid ax:transition-[grid-template-rows] ax:duration-150 ax:ease-out ax:motion-reduce:transition-none",
129
+ open ? "ax:grid-rows-[1fr]" : "ax:grid-rows-[0fr]",
130
+ className,
131
+ )}
132
+ {...props}
133
+ >
134
+ <div className="ax:min-h-0 ax:overflow-hidden">
135
+ <div className={cn("ax:mt-sm ax:border-s-2 ax:border-border ax:ps-md ax:t-body-sm ax:text-muted-foreground", contentClassName)}>
136
+ {children}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ )
141
+ },
142
+ )
143
+ ReasoningContent.displayName = "ReasoningContent"
@@ -0,0 +1,18 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Search } from "./search"
4
+
5
+ const meta: Meta<typeof Search> = {
6
+ title: "Components/Search",
7
+ component: Search,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ placeholder: "Search students…",
11
+ className: "ax:w-65",
12
+ },
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof Search>
17
+
18
+ export const Default: Story = {}
@@ -0,0 +1,133 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Search as SearchIcon, X as ClearIcon } from "lucide-react"
5
+ import { Input, TextFieldRoot, TextFieldSlot, type InputSize } from "./input"
6
+ import { cn } from "../lib/cn"
7
+
8
+ export interface SearchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> {
9
+ /** Visual density - passes through to the underlying `TextFieldRoot` /
10
+ * `Input`. Defaults to `md`. */
11
+ size?: InputSize
12
+ /** Marks the input as invalid (red border + ring). */
13
+ invalid?: boolean
14
+ /** Optional className applied to the outer `TextFieldRoot`. Useful for
15
+ * setting width (e.g. `"ax:w-65"`) or alignment helpers (`"ax:ms-auto"`). */
16
+ className?: string
17
+ /**
18
+ * Show a trailing clear button when the input has a value. Defaults to
19
+ * `true` for controlled inputs (when `value` is set). Pass `false` to
20
+ * suppress entirely.
21
+ *
22
+ * When clicked, the input is cleared by calling either:
23
+ * • `onClear` (if provided), or
24
+ * • `onChange` with a synthetic event whose `target.value` is `""`.
25
+ * The latter keeps controlled callers working without an extra prop.
26
+ */
27
+ clearable?: boolean
28
+ /** Called when the user clicks the clear button. Takes precedence over the
29
+ * default behaviour of dispatching an empty-string change event. */
30
+ onClear?: () => void
31
+ }
32
+
33
+ /**
34
+ * Search - a styled search input with a leading magnifying-glass icon and an
35
+ * optional trailing clear button.
36
+ *
37
+ * A thin composition over `TextFieldRoot` + `TextFieldSlot` + `Input` that
38
+ * codifies the search-field pattern in one component, so every product gets
39
+ * the same look (icon, sizing, focus ring, clear affordance) without
40
+ * re-assembling the three primitives at every call site.
41
+ *
42
+ * Accepts all standard input props (`value`, `defaultValue`, `onChange`,
43
+ * `placeholder`, `aria-label`, `name`, `disabled`, etc.) via spread.
44
+ *
45
+ * @example
46
+ * <Search placeholder="Search students…" aria-label="Search" />
47
+ *
48
+ * @example
49
+ * <Search
50
+ * value={query}
51
+ * onChange={(e) => setQuery(e.target.value)}
52
+ * onClear={() => setQuery("")}
53
+ * placeholder="Search…"
54
+ * className="ax:w-65"
55
+ * />
56
+ */
57
+ export const Search = React.forwardRef<HTMLInputElement, SearchProps>(
58
+ (
59
+ {
60
+ size = "md",
61
+ invalid,
62
+ className,
63
+ clearable,
64
+ onClear,
65
+ value,
66
+ onChange,
67
+ placeholder = "Search…",
68
+ "aria-label": ariaLabel = "Search",
69
+ ...inputProps
70
+ },
71
+ ref,
72
+ ) => {
73
+ const innerRef = React.useRef<HTMLInputElement>(null)
74
+ React.useImperativeHandle(ref, () => innerRef.current as HTMLInputElement)
75
+
76
+ // Show the clear button when:
77
+ // • clearable is explicitly true, OR
78
+ // • clearable is undefined AND we're in controlled mode with a value.
79
+ const hasValue = typeof value === "string" && value.length > 0
80
+ const showClear = clearable === true || (clearable === undefined && hasValue)
81
+
82
+ function handleClear() {
83
+ if (onClear) {
84
+ onClear()
85
+ } else if (onChange && innerRef.current) {
86
+ // Synthesise a change event so controlled callers update their state
87
+ // without needing an extra `onClear` prop.
88
+ const input = innerRef.current
89
+ const setter = Object.getOwnPropertyDescriptor(
90
+ window.HTMLInputElement.prototype,
91
+ "value",
92
+ )?.set
93
+ setter?.call(input, "")
94
+ input.dispatchEvent(new Event("input", { bubbles: true }))
95
+ }
96
+ innerRef.current?.focus()
97
+ }
98
+
99
+ return (
100
+ <TextFieldRoot size={size} invalid={invalid} className={cn(className)}>
101
+ {/* TextFieldRoot already applies the horizontal padding (px-sm/px-md);
102
+ the slot must not add its own or the icon gets doubled left inset. */}
103
+ <TextFieldSlot>
104
+ <SearchIcon className="ax:size-4" />
105
+ </TextFieldSlot>
106
+ <Input
107
+ ref={innerRef}
108
+ type="search"
109
+ // The trailing clear button replaces the browser's own, which would otherwise show alongside it.
110
+ className="ax:[&::-webkit-search-cancel-button]:appearance-none ax:[&::-webkit-search-decoration]:appearance-none"
111
+ value={value}
112
+ onChange={onChange}
113
+ placeholder={placeholder}
114
+ aria-label={ariaLabel}
115
+ {...inputProps}
116
+ />
117
+ {showClear && (
118
+ <TextFieldSlot>
119
+ <button
120
+ type="button"
121
+ onClick={handleClear}
122
+ aria-label="Clear search"
123
+ className="ax:text-muted-foreground ax:hover:text-foreground ax:transition-colors ax:rounded-sm ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring"
124
+ >
125
+ <ClearIcon className="ax:size-4" />
126
+ </button>
127
+ </TextFieldSlot>
128
+ )}
129
+ </TextFieldRoot>
130
+ )
131
+ },
132
+ )
133
+ Search.displayName = "Search"
@@ -0,0 +1,59 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { SectionNav } from "./section-nav"
4
+
5
+ const meta: Meta<typeof SectionNav> = {
6
+ title: "Components/SectionNav",
7
+ component: SectionNav,
8
+ tags: ["autodocs"],
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof SectionNav>
13
+
14
+ const items = [
15
+ { id: "overview", label: "Overview" },
16
+ { id: "eligibility", label: "Eligibility" },
17
+ { id: "how-it-works", label: "How it works" },
18
+ { id: "support", label: "Support" },
19
+ ]
20
+
21
+ export const Default: Story = {
22
+ render: () => (
23
+ <div className="ax:flex ax:gap-xl">
24
+ <div className="ax:w-56 ax:shrink-0">
25
+ <div className="ax:sticky ax:top-md">
26
+ <SectionNav items={items} />
27
+ </div>
28
+ </div>
29
+ <div className="ax:flex-1 ax:space-y-xl">
30
+ {items.map((item) => (
31
+ <section key={item.id} id={item.id} className="ax:min-h-[60vh]">
32
+ <h2 className="ax:t-h2">{item.label}</h2>
33
+ </section>
34
+ ))}
35
+ </div>
36
+ </div>
37
+ ),
38
+ }
39
+
40
+ export const InScrollContainer: Story = {
41
+ render: () => (
42
+ <div className="ax:flex ax:h-96 ax:gap-xl">
43
+ <div className="ax:w-56 ax:shrink-0">
44
+ <SectionNav items={items} scrollRoot="#section-nav-story-root" />
45
+ </div>
46
+ <div id="section-nav-story-root" className="ax:flex-1 ax:space-y-xl ax:overflow-y-auto">
47
+ {items.map((item) => (
48
+ <section key={item.id} id={item.id} className="ax:min-h-80">
49
+ <h2 className="ax:t-h2">{item.label}</h2>
50
+ </section>
51
+ ))}
52
+ </div>
53
+ </div>
54
+ ),
55
+ }
56
+
57
+ export const Responsive: Story = {
58
+ render: () => <SectionNav items={items} layout="responsive" />,
59
+ }
@@ -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 { SectionNav } from "./section-nav"
5
+
6
+ const items = [
7
+ { id: "one", label: "One" },
8
+ { id: "two", label: "Two" },
9
+ ]
10
+
11
+ describe("SectionNav", () => {
12
+ it("renders a labelled nav with an anchor per section", () => {
13
+ const html = renderToStaticMarkup(<SectionNav items={items} />)
14
+ expect(html).toContain('aria-label="Page sections"')
15
+ expect(html).toContain('href="#one"')
16
+ expect(html).toContain('href="#two"')
17
+ })
18
+
19
+ it("marks the first section as current before any scrolling", () => {
20
+ const html = renderToStaticMarkup(<SectionNav items={items} />)
21
+ expect(html.match(/aria-current="page"/g)).toHaveLength(1)
22
+ expect(html).toContain("ax:border-primary")
23
+ })
24
+
25
+ it("shows the default title and lets it be hidden", () => {
26
+ expect(renderToStaticMarkup(<SectionNav items={items} />)).toContain("Jump to")
27
+ expect(renderToStaticMarkup(<SectionNav items={items} title={null} />)).not.toContain("Jump to")
28
+ })
29
+
30
+ it("adds the horizontal-row classes only for the responsive layout", () => {
31
+ const vertical = renderToStaticMarkup(<SectionNav items={items} />)
32
+ const responsive = renderToStaticMarkup(<SectionNav items={items} layout="responsive" />)
33
+ expect(vertical).not.toContain("ax:overflow-x-auto")
34
+ expect(responsive).toContain("ax:overflow-x-auto")
35
+ })
36
+ })
@@ -0,0 +1,146 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../lib/cn"
5
+ import { ListNav, ListNavItem } from "./list-nav"
6
+
7
+ export interface SectionNavItem {
8
+ /** The `id` of the section element this item jumps to. */
9
+ id: string
10
+ label: React.ReactNode
11
+ }
12
+
13
+ export interface SectionNavProps extends Omit<React.HTMLAttributes<HTMLElement>, "title" | "onSelect"> {
14
+ /** Sections to list, in page order. Each `id` must match an element on the page. */
15
+ items: SectionNavItem[]
16
+ /** Eyebrow title above the list. Pass `null` to hide it. */
17
+ title?: React.ReactNode
18
+ /**
19
+ * The scrolling container the sections live in: a CSS selector, an element,
20
+ * or `null` (default) for the window.
21
+ */
22
+ scrollRoot?: string | HTMLElement | null
23
+ /**
24
+ * How far down the scroll root (0-1) a section's top must pass to count as
25
+ * the current one. Default `0.24`.
26
+ */
27
+ activationRatio?: number
28
+ /**
29
+ * `vertical` (default) is a stacked list at every width. `responsive` runs as
30
+ * a horizontally scrolling row below `xl` and a stacked list from `xl` up,
31
+ * hiding the title in the row.
32
+ */
33
+ layout?: "vertical" | "responsive"
34
+ /** Fires when the current section changes, from scrolling or from a click. */
35
+ onActiveChange?: (id: string) => void
36
+ }
37
+
38
+ function resolveScrollRoot(scrollRoot: SectionNavProps["scrollRoot"]): HTMLElement | null {
39
+ if (typeof scrollRoot === "string") return document.querySelector<HTMLElement>(scrollRoot)
40
+ return scrollRoot ?? null
41
+ }
42
+
43
+ /**
44
+ * SectionNav - an in-page "Jump to" menu with scroll-spy. Lists the page's
45
+ * sections as anchor links, marks the one currently in view with a primary
46
+ * left border and text, and follows the page as it scrolls.
47
+ *
48
+ * Built on `ListNav`: each item is an `<a href="#id">`, so it still works as a
49
+ * plain anchor without JavaScript. Use `scrollRoot` when the page scrolls
50
+ * inside a container rather than the window.
51
+ */
52
+ export const SectionNav = React.forwardRef<HTMLElement, SectionNavProps>(
53
+ (
54
+ {
55
+ items,
56
+ title = "Jump to",
57
+ scrollRoot = null,
58
+ activationRatio = 0.24,
59
+ layout = "vertical",
60
+ onActiveChange,
61
+ className,
62
+ "aria-label": ariaLabel = "Page sections",
63
+ ...props
64
+ },
65
+ ref,
66
+ ) => {
67
+ const [activeId, setActiveId] = React.useState(items[0]?.id ?? "")
68
+ const onActiveChangeRef = React.useRef(onActiveChange)
69
+ onActiveChangeRef.current = onActiveChange
70
+
71
+ const select = React.useCallback((id: string) => {
72
+ setActiveId((current) => {
73
+ if (current === id) return current
74
+ onActiveChangeRef.current?.(id)
75
+ return id
76
+ })
77
+ }, [])
78
+
79
+ React.useEffect(() => {
80
+ const root = resolveScrollRoot(scrollRoot)
81
+ if (typeof scrollRoot === "string" && !root) return
82
+
83
+ const scrollTarget: HTMLElement | Window = root ?? window
84
+
85
+ const update = () => {
86
+ const rootTop = root ? root.getBoundingClientRect().top : 0
87
+ const rootHeight = root ? root.clientHeight : window.innerHeight
88
+ const activationLine = rootTop + rootHeight * activationRatio
89
+ let nextId = items[0]?.id ?? ""
90
+ let nearestPassedTop = Number.NEGATIVE_INFINITY
91
+
92
+ for (const item of items) {
93
+ const section = document.getElementById(item.id)
94
+ if (!section) continue
95
+ const sectionTop = section.getBoundingClientRect().top
96
+ if (sectionTop <= activationLine && sectionTop > nearestPassedTop) {
97
+ nearestPassedTop = sectionTop
98
+ nextId = item.id
99
+ }
100
+ }
101
+ select(nextId)
102
+ }
103
+
104
+ scrollTarget.addEventListener("scroll", update, { passive: true })
105
+ update()
106
+ return () => scrollTarget.removeEventListener("scroll", update)
107
+ }, [items, scrollRoot, activationRatio, select])
108
+
109
+ return (
110
+ <ListNav
111
+ ref={ref}
112
+ title={title ?? undefined}
113
+ aria-label={ariaLabel}
114
+ className={cn(
115
+ layout === "responsive" && [
116
+ "ax:overflow-x-auto ax:pb-sm ax:[&>div]:flex-row ax:[&>div]:gap-2xs ax:[&>p]:hidden",
117
+ "ax:xl:overflow-visible ax:xl:pb-0 ax:xl:[&>div]:flex-col ax:xl:[&>div]:gap-0 ax:xl:[&>p]:block ax:xl:[&>p]:pt-0",
118
+ ],
119
+ className,
120
+ )}
121
+ {...props}
122
+ >
123
+ {items.map((item) => {
124
+ const active = item.id === activeId
125
+ return (
126
+ <ListNavItem
127
+ key={item.id}
128
+ asChild
129
+ active={active}
130
+ className={cn(
131
+ "ax:rounded-none ax:border-s-2 ax:py-2xs ax:pl-md ax:ml-sm",
132
+ layout === "responsive" && "ax:shrink-0 ax:xl:whitespace-normal",
133
+ active ? "ax:border-primary-foreground ax:bg-transparent" : "ax:border-border/50",
134
+ )}
135
+ >
136
+ <a href={`#${item.id}`} onClick={() => select(item.id)} className="ax:min-w-0">
137
+ <span className="ax:flex-1 ax:truncate ax:leading-snug">{item.label}</span>
138
+ </a>
139
+ </ListNavItem>
140
+ )
141
+ })}
142
+ </ListNav>
143
+ )
144
+ },
145
+ )
146
+ SectionNav.displayName = "SectionNav"
@@ -0,0 +1,22 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { SegmentedControl, SegmentedControlItem } from "./segmented-control"
4
+
5
+ const meta: Meta<typeof SegmentedControl> = {
6
+ title: "Components/SegmentedControl",
7
+ component: SegmentedControl,
8
+ tags: ["autodocs"],
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof SegmentedControl>
13
+
14
+ export const Default: Story = {
15
+ render: () => (
16
+ <SegmentedControl defaultValue="day">
17
+ <SegmentedControlItem value="day">Day</SegmentedControlItem>
18
+ <SegmentedControlItem value="week">Week</SegmentedControlItem>
19
+ <SegmentedControlItem value="month">Month</SegmentedControlItem>
20
+ </SegmentedControl>
21
+ ),
22
+ }