@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,392 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { motion, type HTMLMotionProps } from "motion/react"
5
+ import { cn } from "../lib/cn"
6
+ import { useReducedMotion } from "../hooks/use-reduced-motion"
7
+ import { hover, transitions } from "../lib/motion"
8
+
9
+ export type CardVariant = "default" | "surface" | "ghost"
10
+ export type CardPadding = "none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"
11
+ export type CardGap = "sm" | "md" | "lg" | "xl"
12
+ export type CardElevation = "none" | "xs" | "sm" | "level0" | "level1" | "level2" | "level3"
13
+
14
+ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
15
+ /** Visual treatment of the card surface. */
16
+ variant?: CardVariant
17
+ /** Padding inside the card. All responsive variants scale from their mobile floor up to
18
+ * the named size at XL (1280px+), one step per breakpoint.
19
+ * Fixed: none · sm(12px) · md(16px)
20
+ * Responsive: lg(→20px) · xl(→24px) · 2xl(→32px) · 3xl(→40px) · 4xl(→48px) */
21
+ padding?: CardPadding
22
+ /** When set, makes the card a vertical flex column with the given gap between children.
23
+ * sm=12px md=16px lg=20px xl=24px */
24
+ gap?: CardGap
25
+ /** Surface elevation - bundles a background fill with a shadow into one level,
26
+ * overriding the variant's own background and shadow (the variant's border, if
27
+ * any, is kept). Reads the brand-themeable --elevation-* tokens.
28
+ * level0 - transparent, flat
29
+ * level1 - translucent surface (50%), flat
30
+ * level2 - solid surface + subtle shadow
31
+ * level3 - solid surface + stronger shadow
32
+ * Left unset, the card keeps its variant's default (brand-themeable) chrome. */
33
+ elevation?: CardElevation
34
+ /** When true, the card zooms subtly on hover. Disabled by reduced-motion preference. */
35
+ interactive?: boolean
36
+ /** Hover animation style when `interactive` is true.
37
+ * "scale" (default) - a light zoom (1.03×) that raises the shadow one level ·
38
+ * "lift" - translates up 4px */
39
+ hoverEffect?: "lift" | "scale"
40
+ /** When true, removes the border and shadow from the card. */
41
+ borderless?: boolean
42
+ /** Split the card into a muted header band with a content section beneath it.
43
+ * The card's own padding is dropped and each sub-component (CardHeader /
44
+ * CardContent / CardFooter) supplies its own padding instead; the header runs
45
+ * full-bleed with `bg-muted` + a bottom divider, its top corners rounded by
46
+ * the card's own radius. */
47
+ sectioned?: boolean
48
+ }
49
+
50
+ // When `sectioned`, the card goes padding-less and hands the padding scale to its
51
+ // sub-components via context so the muted header band can run full-bleed to the
52
+ // rounded edges while each section keeps consistent interior padding.
53
+ // `headerSize` mirrors the sectioned header's `size` (when set) so the content
54
+ // and footer can match the header's padding scale.
55
+ interface CardSectionContextValue { padding: CardPadding; headerSize?: CardHeaderSize }
56
+ const CardSectionContext = React.createContext<CardSectionContextValue | null>(null)
57
+
58
+ const CARD_BASE = "ax:rounded-md ax:text-card-foreground ax:overflow-hidden"
59
+
60
+ const CARD_VARIANT: Record<CardVariant, string> = {
61
+ default: "ax:border ax:bg-card ax:shadow-sm",
62
+ surface: "ax:border ax:border-border ax:bg-background",
63
+ ghost: "ax:bg-transparent",
64
+ }
65
+
66
+ // Just the border treatment of each variant, with no background or shadow. Used
67
+ // when `elevation` is set: the level supplies the surface (bg + shadow), so the
68
+ // variant's own bg/shadow are dropped rather than fighting the level's utilities.
69
+ const CARD_VARIANT_BORDER: Record<CardVariant, string> = {
70
+ default: "ax:border",
71
+ surface: "ax:border ax:border-border",
72
+ ghost: "",
73
+ }
74
+
75
+ const CARD_PADDING: Record<CardPadding, string> = {
76
+ none: "",
77
+ sm: "ax:p-sm", // 12px - fixed
78
+ md: "ax:p-md", // 16px - fixed
79
+ lg: "ax:p-md ax:md:p-lg", // 16 → 20px
80
+ xl: "ax:p-md ax:sm:p-lg ax:xl:p-xl", // 16 → 20 → 24px
81
+ "2xl": "ax:p-sm ax:sm:p-md ax:md:p-lg ax:lg:p-xl ax:xl:p-2xl", // 12 → 16 → 20 → 24 → 32px
82
+ "3xl": "ax:p-md ax:sm:p-lg ax:md:p-xl ax:lg:p-2xl ax:xl:p-3xl", // 16 → 20 → 24 → 32 → 40px
83
+ "4xl": "ax:p-lg ax:sm:p-xl ax:md:p-2xl ax:lg:p-3xl ax:xl:p-4xl", // 20 → 24 → 32 → 40 → 48px
84
+ }
85
+
86
+ const CARD_GAP: Record<CardGap, string> = {
87
+ sm: "ax:flex ax:flex-col ax:gap-sm",
88
+ md: "ax:flex ax:flex-col ax:gap-md",
89
+ lg: "ax:flex ax:flex-col ax:gap-lg",
90
+ xl: "ax:flex ax:flex-col ax:gap-xl",
91
+ }
92
+
93
+ // Surface elevation - fill + shadow per level, read from the brand-themeable
94
+ // --elevation-* tokens. Applied AFTER the variant classes in cn() so tailwind-merge
95
+ // lets the level win on background-color and box-shadow while the variant's border
96
+ // (if any) survives.
97
+ const CARD_ELEVATION: Record<CardElevation, string> = {
98
+ none: "ax:bg-card ax:shadow-none",
99
+ xs: "ax:bg-card ax:shadow-xs",
100
+ sm: "ax:bg-card ax:shadow-sm",
101
+ level0: "ax:bg-[var(--elevation-0-surface)] ax:shadow-[var(--elevation-0-shadow)]",
102
+ level1: "ax:bg-[var(--elevation-1-surface)] ax:shadow-[var(--elevation-1-shadow)]",
103
+ level2: "ax:bg-[var(--elevation-2-surface)] ax:shadow-[var(--elevation-2-shadow)]",
104
+ level3: "ax:bg-[var(--elevation-3-surface)] ax:shadow-[var(--elevation-3-shadow)]",
105
+ }
106
+
107
+ // Exposes the active padding value to Inset children so they can break out via negative margin.
108
+ const INSET_STYLE: Record<CardPadding, React.CSSProperties> = {
109
+ none: {},
110
+ sm: { "--inset-padding": "var(--space-sm)" } as React.CSSProperties,
111
+ md: { "--inset-padding": "var(--space-md)" } as React.CSSProperties,
112
+ lg: { "--inset-padding": "var(--space-lg)" } as React.CSSProperties,
113
+ xl: { "--inset-padding": "var(--space-xl)" } as React.CSSProperties,
114
+ "2xl": { "--inset-padding": "var(--space-2xl)" } as React.CSSProperties,
115
+ "3xl": { "--inset-padding": "var(--space-3xl)" } as React.CSSProperties,
116
+ "4xl": { "--inset-padding": "var(--space-4xl)" } as React.CSSProperties,
117
+ }
118
+
119
+ /** Utility for applying Card surface classes to non-div elements (e.g. motion.figure, a). */
120
+ export function cardCn(variant: CardVariant = "default", padding: CardPadding = "xl", extra?: string) {
121
+ return cn(CARD_BASE, CARD_VARIANT[variant], CARD_PADDING[padding], extra)
122
+ }
123
+
124
+ export const Card = React.forwardRef<HTMLDivElement, CardProps>(
125
+ ({ className, variant = "default", padding = "xl", gap, elevation, interactive, hoverEffect = "scale", borderless, sectioned, style, children, ...props }, ref) => {
126
+ const reduce = useReducedMotion()
127
+
128
+ // For non-interactive default cards, defer border-color and shadow to brand CSS variables
129
+ // so themes can control card chrome without touching Tailwind utilities.
130
+ // Interactive cards are excluded because hover:shadow-md (class) can't beat inline styles.
131
+ const cardBrandStyle: React.CSSProperties =
132
+ variant === "default" && !borderless && !elevation && !interactive
133
+ ? { borderColor: "var(--card-border-color)", boxShadow: "var(--card-shadow)" }
134
+ : {}
135
+
136
+ const mergedStyle = { ...INSET_STYLE[padding], ...cardBrandStyle, ...style }
137
+ const borderlessClass = borderless
138
+ ? elevation ? "ax:border-0" : "ax:border-0 ax:shadow-none"
139
+ : undefined
140
+ // Sectioned cards go padding-less: the header/content sections own their padding.
141
+ const paddingClass = sectioned ? "" : CARD_PADDING[padding]
142
+ const gapClass = sectioned ? undefined : (gap ? CARD_GAP[gap] : undefined)
143
+ // Mirror the sectioned CardBanner's `size` so content/footer match its padding.
144
+ let headerSize: CardHeaderSize | undefined
145
+ if (sectioned) {
146
+ React.Children.forEach(children, (child) => {
147
+ if (React.isValidElement(child) && (child.type as { displayName?: string })?.displayName === "CardBanner") {
148
+ headerSize = (child.props as CardBannerProps).size ?? "lg"
149
+ }
150
+ })
151
+ }
152
+ const renderChildren = sectioned
153
+ ? <CardSectionContext.Provider value={{ padding, headerSize }}>{children}</CardSectionContext.Provider>
154
+ : children
155
+ // When elevation is set it owns the surface (bg + shadow); keep only the
156
+ // variant's border so the level's utilities aren't fighting the variant's own
157
+ // background/shadow. Unset → the variant's full default chrome.
158
+ const surfaceClass = cn(
159
+ elevation
160
+ ? cn(CARD_VARIANT_BORDER[variant], CARD_ELEVATION[elevation])
161
+ : CARD_VARIANT[variant],
162
+ // Sectioned: paint the header surface on the card root so the card's
163
+ // rounded top corners read as the header, AND so the content container's
164
+ // rounded top corners reveal the header colour behind them.
165
+ sectioned && "ax:bg-neutral-weakest",
166
+ )
167
+
168
+ if (interactive && !reduce) {
169
+ // "scale" - card chrome grows, content stays at natural size via counter-scale.
170
+ // The outer div scales up (overlapping siblings); the inner div scales back down
171
+ // so text and icons remain visually unchanged.
172
+ if (hoverEffect === "scale") {
173
+ const SCALE = 1.03
174
+ return (
175
+ <motion.div
176
+ ref={ref as React.Ref<HTMLDivElement>}
177
+ style={mergedStyle}
178
+ className={cn(
179
+ CARD_BASE, surfaceClass, borderlessClass,
180
+ "ax:hover:shadow-md ax:transition-shadow ax:transition-snappy ax:cursor-pointer ax:relative ax:hover:z-10",
181
+ className
182
+ )}
183
+ whileHover={{ scale: SCALE }}
184
+ transition={transitions.spring}
185
+ {...(props as HTMLMotionProps<"div">)}
186
+ >
187
+ <motion.div
188
+ className={cn(paddingClass, gapClass, "ax:h-full ax:flex ax:flex-col")}
189
+ whileHover={{ scale: 1 / SCALE }}
190
+ transition={transitions.spring}
191
+ >
192
+ {renderChildren}
193
+ </motion.div>
194
+ </motion.div>
195
+ )
196
+ }
197
+
198
+ return (
199
+ <motion.div
200
+ ref={ref as React.Ref<HTMLDivElement>}
201
+ style={mergedStyle}
202
+ className={cn(
203
+ CARD_BASE, surfaceClass, paddingClass, gapClass, borderlessClass,
204
+ sectioned && "ax:flex ax:flex-col",
205
+ "ax:hover:shadow-md ax:transition-shadow ax:transition-snappy ax:cursor-pointer",
206
+ className
207
+ )}
208
+ whileHover={hover.lift}
209
+ transition={transitions.spring}
210
+ {...(props as HTMLMotionProps<"div">)}
211
+ >
212
+ {renderChildren}
213
+ </motion.div>
214
+ )
215
+ }
216
+
217
+ return (
218
+ <div
219
+ ref={ref}
220
+ style={mergedStyle}
221
+ className={cn(
222
+ CARD_BASE, surfaceClass, paddingClass, gapClass, borderlessClass,
223
+ sectioned && "ax:flex ax:flex-col",
224
+ interactive && "ax:cursor-pointer",
225
+ className
226
+ )}
227
+ {...props}
228
+ >
229
+ {renderChildren}
230
+ </div>
231
+ )
232
+ }
233
+ )
234
+ Card.displayName = "Card"
235
+
236
+ // ── CardHeader sizing ──────────────────────────────────────────────────────
237
+ export type CardHeaderSize = "xs" | "sm" | "md" | "lg" | "xl"
238
+
239
+ // Title type token per size - the header's `size` drives its CardTitle.
240
+ const HEADER_TITLE: Record<CardHeaderSize, string> = {
241
+ xs: "ax:t-h6", sm: "ax:t-h5", md: "ax:t-h4", lg: "ax:t-h3", xl: "ax:t-h2",
242
+ }
243
+ // Description type token per size - drives a nested CardDescription.
244
+ const HEADER_DESC: Record<CardHeaderSize, string> = {
245
+ xs: "ax:t-body-xs", sm: "ax:t-body-sm", md: "ax:t-body-sm", lg: "ax:t-body-md", xl: "ax:t-body-md",
246
+ }
247
+ // Padding when the header owns its box (a `sectioned` band): horizontal on the
248
+ // p-* scale, vertical on the tighter py-* scale.
249
+ const HEADER_PADDING: Record<CardHeaderSize, string> = {
250
+ xs: "ax:px-sm ax:py-xs", sm: "ax:px-md ax:py-sm", md: "ax:px-lg ax:py-md", lg: "ax:px-xl ax:py-lg", xl: "ax:px-2xl ax:py-xl",
251
+ }
252
+ // Bottom-only padding (non-sectioned: root handles top/sides, header adds the
253
+ // gap before the content).
254
+ const HEADER_PADDING_BOTTOM: Record<CardHeaderSize, string> = {
255
+ xs: "ax:pb-sm", sm: "ax:pb-md", md: "ax:pb-lg", lg: "ax:pb-xl", xl: "ax:pb-2xl",
256
+ }
257
+ // Content/footer padding when the sectioned header carries a `size`: all-sides
258
+ // on the header's horizontal scale so the sections line up under the header.
259
+ const SECTION_PADDING: Record<CardHeaderSize, string> = {
260
+ xs: "ax:p-sm", sm: "ax:p-md", md: "ax:p-lg", lg: "ax:p-xl", xl: "ax:p-2xl",
261
+ }
262
+
263
+ // Lets CardHeader's `size` drive the type tokens of a nested CardTitle /
264
+ // CardDescription. Defaults match the legacy (unsized) header.
265
+ interface CardHeaderType { title: string; description: string }
266
+ const CardHeaderTypeContext = React.createContext<CardHeaderType>({ title: "ax:t-h3", description: "ax:t-body-sm" })
267
+
268
+ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
269
+ /** Sizes the header - its bottom padding and the nested CardTitle /
270
+ * CardDescription type tokens. xs=t-h6 · sm=t-h5 · md=t-h4 · lg=t-h3 (default)
271
+ * · xl=t-h2. Leave unset to keep the legacy defaults (t-h3 / t-body-sm). */
272
+ size?: CardHeaderSize
273
+ }
274
+
275
+ // CardHeader - the default header: title + description stacked with a small gap,
276
+ // bottom padding before the content (root padding handles top/sides). For the
277
+ // darker full-bleed band of a `sectioned` card, use CardBanner instead.
278
+ export const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
279
+ ({ className, size, ...props }, ref) => {
280
+ // Explicit `size` drives the title/description type tokens; unset keeps the
281
+ // legacy defaults (t-h3 / t-body-sm).
282
+ const type = React.useMemo<CardHeaderType>(
283
+ () => size
284
+ ? { title: HEADER_TITLE[size], description: HEADER_DESC[size] }
285
+ : { title: "ax:t-h3", description: "ax:t-body-sm" },
286
+ [size],
287
+ )
288
+ return (
289
+ <CardHeaderTypeContext.Provider value={type}>
290
+ <div
291
+ ref={ref}
292
+ className={cn("ax:flex ax:flex-col ax:gap-sm", size ? HEADER_PADDING_BOTTOM[size] : "ax:pb-xl", className)}
293
+ {...props}
294
+ />
295
+ </CardHeaderTypeContext.Provider>
296
+ )
297
+ }
298
+ )
299
+ CardHeader.displayName = "CardHeader"
300
+
301
+ export interface CardBannerProps extends React.HTMLAttributes<HTMLDivElement> {
302
+ /** Sizes the band - its padding and the nested CardTitle / CardDescription
303
+ * type tokens (and the matching CardContent/CardFooter padding).
304
+ * xs=t-h6 · sm=t-h5 · md=t-h4 · lg=t-h3 (default) · xl=t-h2. */
305
+ size?: CardHeaderSize
306
+ }
307
+
308
+ // CardBanner - the darker, full-bleed header band for a `sectioned` card. Its top
309
+ // corners are rounded by the card's radius; the CardContent below tucks under it
310
+ // with rounded top corners that reveal this band's colour.
311
+ export const CardBanner = React.forwardRef<HTMLDivElement, CardBannerProps>(
312
+ ({ className, size = "lg", ...props }, ref) => {
313
+ const type = React.useMemo<CardHeaderType>(
314
+ () => ({ title: HEADER_TITLE[size], description: HEADER_DESC[size] }),
315
+ [size],
316
+ )
317
+ return (
318
+ <CardHeaderTypeContext.Provider value={type}>
319
+ <div
320
+ ref={ref}
321
+ className={cn(
322
+ "ax:flex ax:flex-col ax:bg-neutral-weakest/50 ax:dark:bg-neutral-weaker/50",
323
+ HEADER_PADDING[size],
324
+ className,
325
+ )}
326
+ {...props}
327
+ />
328
+ </CardHeaderTypeContext.Provider>
329
+ )
330
+ }
331
+ )
332
+ CardBanner.displayName = "CardBanner"
333
+
334
+ export const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
335
+ ({ className, ...props }, ref) => {
336
+ const { title } = React.useContext(CardHeaderTypeContext)
337
+ return (
338
+ <h3
339
+ ref={ref}
340
+ className={cn(title, "ax:text-foreground", className)}
341
+ {...props}
342
+ />
343
+ )
344
+ }
345
+ )
346
+ CardTitle.displayName = "CardTitle"
347
+
348
+ export const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
349
+ ({ className, ...props }, ref) => {
350
+ const { description } = React.useContext(CardHeaderTypeContext)
351
+ return (
352
+ <p ref={ref} className={cn(description, "ax:text-muted-foreground", className)} {...props} />
353
+ )
354
+ }
355
+ )
356
+ CardDescription.displayName = "CardDescription"
357
+
358
+ // CardContent - root padding handles all spacing; in a `sectioned` card it
359
+ // supplies its own padding instead.
360
+ export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
361
+ ({ className, ...props }, ref) => {
362
+ const section = React.useContext(CardSectionContext)
363
+ return (
364
+ <div
365
+ ref={ref}
366
+ className={cn("ax:t-body-sm ax:text-foreground", section && cn("ax:bg-card ax:rounded-t-lg ax:flex-1", section.headerSize ? SECTION_PADDING[section.headerSize] : CARD_PADDING[section.padding]), className)}
367
+ {...props}
368
+ />
369
+ )
370
+ }
371
+ )
372
+ CardContent.displayName = "CardContent"
373
+
374
+ // CardFooter - top padding separates it from CardContent above. In a `sectioned`
375
+ // card it becomes a divided band with its own padding.
376
+ export const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
377
+ ({ className, ...props }, ref) => {
378
+ const section = React.useContext(CardSectionContext)
379
+ return (
380
+ <div
381
+ ref={ref}
382
+ className={cn(
383
+ "ax:flex ax:items-center",
384
+ section ? cn("ax:bg-card ax:border-t ax:border-border", section.headerSize ? SECTION_PADDING[section.headerSize] : CARD_PADDING[section.padding]) : "ax:pt-xl",
385
+ className,
386
+ )}
387
+ {...props}
388
+ />
389
+ )
390
+ }
391
+ )
392
+ CardFooter.displayName = "CardFooter"
@@ -0,0 +1,35 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { CardsNav } from "./cards-nav"
4
+
5
+ const items = [
6
+ { title: "Documentation", description: "Guides and API references.", href: "#docs" },
7
+ { title: "Playground", description: "Try components live.", href: "#playground" },
8
+ { title: "Brands", description: "Explore brand themes.", href: "#brands" },
9
+ ]
10
+
11
+ const meta: Meta<typeof CardsNav> = {
12
+ title: "Components/CardsNav",
13
+ component: CardsNav,
14
+ tags: ["autodocs"],
15
+ args: { items },
16
+ }
17
+
18
+ export default meta
19
+ type Story = StoryObj<typeof CardsNav>
20
+
21
+ export const Default: Story = {
22
+ render: (args) => (
23
+ <div className="ax:grid ax:gap-md ax:sm:grid-cols-3 ax:max-w-3xl">
24
+ <CardsNav {...args} />
25
+ </div>
26
+ ),
27
+ }
28
+
29
+ export const WithOverline: Story = {
30
+ render: (args) => (
31
+ <div className="ax:grid ax:gap-md ax:sm:grid-cols-3 ax:max-w-3xl">
32
+ <CardsNav {...args} overline />
33
+ </div>
34
+ ),
35
+ }
@@ -0,0 +1,115 @@
1
+ import * as React from "react"
2
+ import { ArrowUpRight } from "lucide-react"
3
+ import { cn } from "../lib/cn"
4
+
5
+ export interface CardsNavItem {
6
+ /** Card heading. */
7
+ title: React.ReactNode
8
+ /** Optional supporting line under the title. */
9
+ description?: React.ReactNode
10
+ /** Destination passed to the link element (`as`). */
11
+ href: string
12
+ /** Custom overline content; falls back to a two-digit ordinal (01, 02, …)
13
+ * when the `overline` prop is on and this is omitted. */
14
+ overline?: React.ReactNode
15
+ /** Optional accent colour for the overline, as any CSS colour string (e.g.
16
+ * `"hsl(198 100% 44%)"`). Tints just the overline; leaves the rest neutral. */
17
+ accent?: string
18
+ }
19
+
20
+ export type CardsNavSize = "sm" | "md" | "lg"
21
+
22
+ /** Card padding per size. */
23
+ const SIZE: Record<CardsNavSize, string> = {
24
+ sm: "ax:p-lg",
25
+ md: "ax:p-xl",
26
+ lg: "ax:p-2xl",
27
+ }
28
+
29
+ /** Title + description type scale per text size. */
30
+ const TEXT: Record<CardsNavSize, { title: string; desc: string }> = {
31
+ sm: { title: "ax:t-h5", desc: "ax:t-body-sm" },
32
+ md: { title: "ax:t-h4", desc: "ax:t-body-sm" },
33
+ lg: { title: "ax:t-h3", desc: "ax:t-body-md" },
34
+ }
35
+
36
+ export interface CardsNavProps {
37
+ items: CardsNavItem[]
38
+ /** Element rendered for each card. Default `"a"`; pass a router `Link` (e.g.
39
+ * Next.js) for client-side navigation. */
40
+ as?: React.ElementType
41
+ /** Card padding. Default "md". */
42
+ size?: CardsNavSize
43
+ /** Title + description type scale. Default "md". */
44
+ textSize?: CardsNavSize
45
+ /** Show the top overline row - a per-item `overline` or, failing that, a
46
+ * two-digit ordinal. Default false. */
47
+ overline?: boolean
48
+ /** Open links in a new tab (adds `target="_blank"` + safe `rel`). Use for
49
+ * external destinations. Default false. */
50
+ newTab?: boolean
51
+ /** Extra classes applied to each card. */
52
+ className?: string
53
+ }
54
+
55
+ /**
56
+ * CardsNav - a set of navigational cards, each a title, optional supporting
57
+ * line and a trailing up-right arrow that leans on hover. Renders the cards as
58
+ * a fragment (no wrapper): place them in your own grid or flex container so the
59
+ * column count and gap stay with the layout.
60
+ *
61
+ * Framework-agnostic: cards render as `<a>` by default; pass `as={Link}` to use
62
+ * a router link. Turn on `overline` for a numbered / eyebrow'd set.
63
+ */
64
+ export function CardsNav({ items, as, size = "md", textSize = "md", overline = false, newTab = false, className }: CardsNavProps) {
65
+ const Comp = as ?? "a"
66
+ const text = TEXT[textSize]
67
+ const newTabProps = newTab ? { target: "_blank", rel: "noopener noreferrer" } : {}
68
+ return (
69
+ <>
70
+ {items.map((item, i) => {
71
+ // The arrow shares a row with whichever element leads the card (the
72
+ // overline when it's shown, otherwise the title) rather than floating
73
+ // alone in a top row.
74
+ const arrow = (
75
+ <ArrowUpRight className="ax:size-4 ax:shrink-0 ax:text-neutral-weaker ax:transition-[color,transform] ax:group-hover:-translate-y-0.5 ax:group-hover:translate-x-0.5 ax:group-hover:text-primary ax:rtl:-scale-x-100" />
76
+ )
77
+ return (
78
+ <Comp
79
+ key={i}
80
+ href={item.href}
81
+ {...newTabProps}
82
+ className={cn(
83
+ "ax:group ax:flex ax:flex-col ax:rounded-sm ax:border ax:border-border ax:bg-card ax:transition-colors ax:dark:bg-card/50 ax:hover:border-primary ax:hover:bg-card",
84
+ SIZE[size],
85
+ className,
86
+ )}
87
+ >
88
+ {overline ? (
89
+ <>
90
+ <div className="ax:flex ax:items-center ax:justify-between ax:gap-md">
91
+ <span
92
+ className="ax:t-eyebrow-small ax:tabular-nums ax:text-muted-foreground"
93
+ style={item.accent ? { color: item.accent } : undefined}
94
+ >
95
+ {item.overline ?? String(i + 1).padStart(2, "0")}
96
+ </span>
97
+ {arrow}
98
+ </div>
99
+ <span className={cn("ax:mt-xs ax:text-foreground ax:transition-colors ax:group-hover:text-primary-foreground", text.title)}>{item.title}</span>
100
+ </>
101
+ ) : (
102
+ <div className="ax:flex ax:items-start ax:justify-between ax:gap-md">
103
+ <span className={cn("ax:text-foreground ax:transition-colors ax:group-hover:text-primary-foreground", text.title)}>{item.title}</span>
104
+ {arrow}
105
+ </div>
106
+ )}
107
+ {item.description != null && (
108
+ <span className={cn("ax:mt-2xs ax:max-w-prose ax:text-muted-foreground", text.desc)}>{item.description}</span>
109
+ )}
110
+ </Comp>
111
+ )
112
+ })}
113
+ </>
114
+ )
115
+ }
@@ -0,0 +1,67 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Calculator, Lightbulb, Search } from "lucide-react"
4
+ import {
5
+ ChainOfThought, ChainOfThoughtContent, ChainOfThoughtItem, ChainOfThoughtStep, ChainOfThoughtTrigger,
6
+ } from "./chain-of-thought"
7
+
8
+ const meta: Meta<typeof ChainOfThought> = {
9
+ title: "Components/ChainOfThought",
10
+ component: ChainOfThought,
11
+ tags: ["autodocs"],
12
+ decorators: [(Story) => <div style={{ maxWidth: 560 }}><Story /></div>],
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof ChainOfThought>
17
+
18
+ export const Basic: Story = {
19
+ render: () => (
20
+ <ChainOfThought>
21
+ <ChainOfThoughtStep defaultOpen>
22
+ <ChainOfThoughtTrigger>Understanding the request</ChainOfThoughtTrigger>
23
+ <ChainOfThoughtContent>
24
+ <ChainOfThoughtItem>Townhouse on Yas Island, under AED 5M, with a private garden.</ChainOfThoughtItem>
25
+ </ChainOfThoughtContent>
26
+ </ChainOfThoughtStep>
27
+ <ChainOfThoughtStep>
28
+ <ChainOfThoughtTrigger>Searching listings</ChainOfThoughtTrigger>
29
+ <ChainOfThoughtContent>
30
+ <ChainOfThoughtItem>Found 42 townhouses in Yas Acres and Noya.</ChainOfThoughtItem>
31
+ <ChainOfThoughtItem>11 have a private garden.</ChainOfThoughtItem>
32
+ </ChainOfThoughtContent>
33
+ </ChainOfThoughtStep>
34
+ <ChainOfThoughtStep>
35
+ <ChainOfThoughtTrigger>Comparing prices</ChainOfThoughtTrigger>
36
+ <ChainOfThoughtContent>
37
+ <ChainOfThoughtItem>Lowest asking price is AED 4.2M in Yas Acres.</ChainOfThoughtItem>
38
+ </ChainOfThoughtContent>
39
+ </ChainOfThoughtStep>
40
+ </ChainOfThought>
41
+ ),
42
+ }
43
+
44
+ export const WithIcons: Story = {
45
+ render: () => (
46
+ <ChainOfThought>
47
+ <ChainOfThoughtStep>
48
+ <ChainOfThoughtTrigger icon={<Search />}>Searched 128 listings on Yas Island</ChainOfThoughtTrigger>
49
+ <ChainOfThoughtContent>
50
+ <ChainOfThoughtItem>42 townhouses under AED 5M, 11 with a private garden.</ChainOfThoughtItem>
51
+ </ChainOfThoughtContent>
52
+ </ChainOfThoughtStep>
53
+ <ChainOfThoughtStep>
54
+ <ChainOfThoughtTrigger icon={<Calculator />}>Compared registered sales</ChainOfThoughtTrigger>
55
+ <ChainOfThoughtContent>
56
+ <ChainOfThoughtItem>Median sale price in Yas Acres is AED 4.6M over the last 12 months.</ChainOfThoughtItem>
57
+ </ChainOfThoughtContent>
58
+ </ChainOfThoughtStep>
59
+ <ChainOfThoughtStep>
60
+ <ChainOfThoughtTrigger icon={<Lightbulb />}>Drew a conclusion</ChainOfThoughtTrigger>
61
+ <ChainOfThoughtContent>
62
+ <ChainOfThoughtItem>The AED 4.2M listing is priced below recent sales.</ChainOfThoughtItem>
63
+ </ChainOfThoughtContent>
64
+ </ChainOfThoughtStep>
65
+ </ChainOfThought>
66
+ ),
67
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import {
5
+ ChainOfThought, ChainOfThoughtContent, ChainOfThoughtItem, ChainOfThoughtStep, ChainOfThoughtTrigger,
6
+ } from "./chain-of-thought"
7
+
8
+ function step(props: React.ComponentProps<typeof ChainOfThoughtStep>, icon?: React.ReactNode) {
9
+ return (
10
+ <ChainOfThoughtStep {...props}>
11
+ <ChainOfThoughtTrigger icon={icon}>Searching listings</ChainOfThoughtTrigger>
12
+ <ChainOfThoughtContent>
13
+ <ChainOfThoughtItem>Found 42 townhouses</ChainOfThoughtItem>
14
+ </ChainOfThoughtContent>
15
+ </ChainOfThoughtStep>
16
+ )
17
+ }
18
+
19
+ describe("ChainOfThought", () => {
20
+ it("keeps a step closed by default", () => {
21
+ const html = renderToStaticMarkup(<ChainOfThought>{step({})}</ChainOfThought>)
22
+ expect(html).toContain('aria-expanded="false"')
23
+ expect(html).not.toContain("Found 42 townhouses")
24
+ })
25
+
26
+ it("opens with defaultOpen and points the trigger at its content", () => {
27
+ const html = renderToStaticMarkup(<ChainOfThought>{step({ defaultOpen: true })}</ChainOfThought>)
28
+ expect(html).toContain('aria-expanded="true"')
29
+ expect(html).toContain("Found 42 townhouses")
30
+ const controls = html.match(/aria-controls="([^"]+)"/)?.[1]
31
+ expect(controls).toBeTruthy()
32
+ expect(html).toContain(`id="${controls}"`)
33
+ })
34
+
35
+ it("lets a controlled open win over defaultOpen", () => {
36
+ const html = renderToStaticMarkup(<ChainOfThought>{step({ open: false, defaultOpen: true })}</ChainOfThought>)
37
+ expect(html).toContain('aria-expanded="false"')
38
+ })
39
+
40
+ it("shows a trailing chevron only when there is no icon to swap", () => {
41
+ const chevrons = (html: string) => html.match(/lucide-chevron-down/g)?.length ?? 0
42
+ expect(chevrons(renderToStaticMarkup(step({})))).toBe(1)
43
+ expect(chevrons(renderToStaticMarkup(step({}, <svg />)))).toBe(1)
44
+ expect(renderToStaticMarkup(step({}, <svg />))).toContain("ax:absolute")
45
+ })
46
+
47
+ it("throws when a trigger is used outside a step", () => {
48
+ expect(() => renderToStaticMarkup(<ChainOfThoughtTrigger>Orphan</ChainOfThoughtTrigger>)).toThrow(/ChainOfThoughtStep/)
49
+ })
50
+ })