@adres/mgda-ui 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/brand-knowledge.json +451 -0
  2. package/knowledge/accordion.md +135 -0
  3. package/knowledge/activity-rings.md +123 -0
  4. package/knowledge/alert.md +184 -0
  5. package/knowledge/answer-actions.md +217 -0
  6. package/knowledge/aspect-ratio.md +82 -0
  7. package/knowledge/avatar.md +158 -0
  8. package/knowledge/badge.md +172 -0
  9. package/knowledge/bar-graph.md +167 -0
  10. package/knowledge/bento-grid.md +115 -0
  11. package/knowledge/blockquote.md +153 -0
  12. package/knowledge/box-list.md +177 -0
  13. package/knowledge/bullet-list.md +128 -0
  14. package/knowledge/button-group.md +179 -0
  15. package/knowledge/button.md +231 -0
  16. package/knowledge/calendar.md +168 -0
  17. package/knowledge/card.md +321 -0
  18. package/knowledge/cards-nav.md +170 -0
  19. package/knowledge/chain-of-thought.md +164 -0
  20. package/knowledge/chat-container.md +135 -0
  21. package/knowledge/chat-message.md +224 -0
  22. package/knowledge/checkbox-card.md +172 -0
  23. package/knowledge/checkbox.md +111 -0
  24. package/knowledge/citations.md +241 -0
  25. package/knowledge/combobox.md +265 -0
  26. package/knowledge/countdown-pill.md +153 -0
  27. package/knowledge/cta.md +118 -0
  28. package/knowledge/data-list.md +173 -0
  29. package/knowledge/data-table.md +220 -0
  30. package/knowledge/dialog.md +249 -0
  31. package/knowledge/drawer.md +236 -0
  32. package/knowledge/dropdown-menu.md +238 -0
  33. package/knowledge/dropzone.md +153 -0
  34. package/knowledge/email-chips-input.md +161 -0
  35. package/knowledge/empty-state.md +106 -0
  36. package/knowledge/entity-header.md +166 -0
  37. package/knowledge/feature-grid.md +120 -0
  38. package/knowledge/feedback-bar.md +127 -0
  39. package/knowledge/footer.md +116 -0
  40. package/knowledge/heading.md +189 -0
  41. package/knowledge/hero.md +361 -0
  42. package/knowledge/input-button.md +187 -0
  43. package/knowledge/input-group.md +185 -0
  44. package/knowledge/input.md +256 -0
  45. package/knowledge/inset.md +177 -0
  46. package/knowledge/kpi-card.md +206 -0
  47. package/knowledge/list-nav.md +181 -0
  48. package/knowledge/logo-cloud.md +102 -0
  49. package/knowledge/media-gallery.md +158 -0
  50. package/knowledge/message-actions.md +175 -0
  51. package/knowledge/nav-sidebar.md +225 -0
  52. package/knowledge/number-stepper.md +159 -0
  53. package/knowledge/page-header.md +109 -0
  54. package/knowledge/pagination.md +225 -0
  55. package/knowledge/popover.md +159 -0
  56. package/knowledge/pricing.md +132 -0
  57. package/knowledge/progress.md +156 -0
  58. package/knowledge/prompt-input.md +337 -0
  59. package/knowledge/prompt-suggestions.md +168 -0
  60. package/knowledge/quota-bar.md +160 -0
  61. package/knowledge/quote.md +116 -0
  62. package/knowledge/radio-card.md +172 -0
  63. package/knowledge/radio-group.md +135 -0
  64. package/knowledge/reasoning.md +142 -0
  65. package/knowledge/search.md +131 -0
  66. package/knowledge/section-nav.md +162 -0
  67. package/knowledge/segmented-control.md +181 -0
  68. package/knowledge/select-button.md +205 -0
  69. package/knowledge/select.md +210 -0
  70. package/knowledge/separator.md +75 -0
  71. package/knowledge/shimmer-text.md +140 -0
  72. package/knowledge/sidebar.md +227 -0
  73. package/knowledge/skeleton.md +142 -0
  74. package/knowledge/slider.md +133 -0
  75. package/knowledge/slideshow.md +159 -0
  76. package/knowledge/source.md +156 -0
  77. package/knowledge/spinner.md +120 -0
  78. package/knowledge/stat-strip.md +128 -0
  79. package/knowledge/stats.md +117 -0
  80. package/knowledge/step-indicator.md +157 -0
  81. package/knowledge/switch.md +88 -0
  82. package/knowledge/table.md +190 -0
  83. package/knowledge/tabs.md +169 -0
  84. package/knowledge/testimonials.md +115 -0
  85. package/knowledge/text-link.md +192 -0
  86. package/knowledge/text.md +194 -0
  87. package/knowledge/thinking-bar.md +115 -0
  88. package/knowledge/thinking-steps.md +140 -0
  89. package/knowledge/toaster.md +127 -0
  90. package/knowledge/toggle.md +113 -0
  91. package/knowledge/tool-call-card.md +241 -0
  92. package/knowledge/tooltip.md +120 -0
  93. package/knowledge/top-nav.md +138 -0
  94. package/knowledge.json +19854 -0
  95. package/package.json +94 -4
  96. package/src/components/accordion.stories.tsx +46 -0
  97. package/src/components/accordion.tsx +74 -0
  98. package/src/components/activity-rings.stories.tsx +73 -0
  99. package/src/components/activity-rings.tsx +121 -0
  100. package/src/components/alert.stories.tsx +43 -0
  101. package/src/components/alert.tsx +132 -0
  102. package/src/components/answer-actions.stories.tsx +54 -0
  103. package/src/components/answer-actions.test.tsx +37 -0
  104. package/src/components/answer-actions.tsx +313 -0
  105. package/src/components/aspect-ratio.stories.tsx +32 -0
  106. package/src/components/aspect-ratio.tsx +5 -0
  107. package/src/components/avatar.stories.tsx +40 -0
  108. package/src/components/avatar.tsx +154 -0
  109. package/src/components/badge.stories.tsx +27 -0
  110. package/src/components/badge.tsx +63 -0
  111. package/src/components/bar-graph.stories.tsx +28 -0
  112. package/src/components/bar-graph.tsx +204 -0
  113. package/src/components/bento-grid.stories.tsx +23 -0
  114. package/src/components/bento-grid.tsx +67 -0
  115. package/src/components/blockquote.stories.tsx +28 -0
  116. package/src/components/blockquote.tsx +88 -0
  117. package/src/components/box-list.stories.tsx +25 -0
  118. package/src/components/box-list.tsx +108 -0
  119. package/src/components/bullet-list.stories.tsx +22 -0
  120. package/src/components/bullet-list.tsx +101 -0
  121. package/src/components/button-group.stories.tsx +31 -0
  122. package/src/components/button-group.tsx +156 -0
  123. package/src/components/button.stories.tsx +66 -0
  124. package/src/components/button.test.ts +17 -0
  125. package/src/components/button.tsx +139 -0
  126. package/src/components/calendar.stories.tsx +25 -0
  127. package/src/components/calendar.tsx +367 -0
  128. package/src/components/card.stories.tsx +44 -0
  129. package/src/components/card.tsx +392 -0
  130. package/src/components/cards-nav.stories.tsx +35 -0
  131. package/src/components/cards-nav.tsx +115 -0
  132. package/src/components/chain-of-thought.stories.tsx +67 -0
  133. package/src/components/chain-of-thought.test.tsx +50 -0
  134. package/src/components/chain-of-thought.tsx +170 -0
  135. package/src/components/chat-container.stories.tsx +47 -0
  136. package/src/components/chat-container.test.tsx +49 -0
  137. package/src/components/chat-container.tsx +222 -0
  138. package/src/components/chat-message.stories.tsx +52 -0
  139. package/src/components/chat-message.test.tsx +36 -0
  140. package/src/components/chat-message.tsx +163 -0
  141. package/src/components/checkbox-card.stories.tsx +27 -0
  142. package/src/components/checkbox-card.tsx +200 -0
  143. package/src/components/checkbox.stories.tsx +26 -0
  144. package/src/components/checkbox.tsx +61 -0
  145. package/src/components/citations.stories.tsx +64 -0
  146. package/src/components/citations.test.tsx +61 -0
  147. package/src/components/citations.tsx +220 -0
  148. package/src/components/combobox.stories.tsx +184 -0
  149. package/src/components/combobox.test.tsx +82 -0
  150. package/src/components/combobox.tsx +407 -0
  151. package/src/components/countdown-pill.stories.tsx +24 -0
  152. package/src/components/countdown-pill.tsx +129 -0
  153. package/src/components/cta.stories.tsx +29 -0
  154. package/src/components/cta.tsx +52 -0
  155. package/src/components/data-list.stories.tsx +50 -0
  156. package/src/components/data-list.test.tsx +35 -0
  157. package/src/components/data-list.tsx +181 -0
  158. package/src/components/data-table.stories.tsx +29 -0
  159. package/src/components/data-table.tsx +200 -0
  160. package/src/components/dialog.stories.tsx +42 -0
  161. package/src/components/dialog.tsx +200 -0
  162. package/src/components/drawer.stories.tsx +48 -0
  163. package/src/components/drawer.tsx +385 -0
  164. package/src/components/dropdown-menu.stories.tsx +41 -0
  165. package/src/components/dropdown-menu.tsx +252 -0
  166. package/src/components/dropzone.stories.tsx +19 -0
  167. package/src/components/dropzone.tsx +250 -0
  168. package/src/components/email-chips-input.stories.tsx +135 -0
  169. package/src/components/email-chips-input.tsx +210 -0
  170. package/src/components/empty-state.stories.tsx +22 -0
  171. package/src/components/empty-state.tsx +35 -0
  172. package/src/components/entity-header.stories.tsx +43 -0
  173. package/src/components/entity-header.test.tsx +35 -0
  174. package/src/components/entity-header.tsx +92 -0
  175. package/src/components/feature-grid.stories.tsx +25 -0
  176. package/src/components/feature-grid.tsx +87 -0
  177. package/src/components/feedback-bar.stories.tsx +25 -0
  178. package/src/components/feedback-bar.test.tsx +24 -0
  179. package/src/components/feedback-bar.tsx +62 -0
  180. package/src/components/footer.stories.tsx +23 -0
  181. package/src/components/footer.tsx +78 -0
  182. package/src/components/heading.stories.tsx +45 -0
  183. package/src/components/heading.test.tsx +22 -0
  184. package/src/components/heading.tsx +77 -0
  185. package/src/components/hero.stories.tsx +38 -0
  186. package/src/components/hero.tsx +498 -0
  187. package/src/components/input-button.stories.tsx +19 -0
  188. package/src/components/input-button.tsx +112 -0
  189. package/src/components/input-group.stories.tsx +38 -0
  190. package/src/components/input-group.tsx +157 -0
  191. package/src/components/input.stories.tsx +31 -0
  192. package/src/components/input.tsx +284 -0
  193. package/src/components/inset.stories.tsx +29 -0
  194. package/src/components/inset.tsx +70 -0
  195. package/src/components/kpi-card.stories.tsx +29 -0
  196. package/src/components/kpi-card.tsx +284 -0
  197. package/src/components/list-nav.stories.tsx +22 -0
  198. package/src/components/list-nav.tsx +145 -0
  199. package/src/components/logo-cloud.stories.tsx +23 -0
  200. package/src/components/logo-cloud.tsx +42 -0
  201. package/src/components/media-gallery.stories.tsx +25 -0
  202. package/src/components/media-gallery.tsx +347 -0
  203. package/src/components/message-actions.stories.tsx +45 -0
  204. package/src/components/message-actions.test.tsx +32 -0
  205. package/src/components/message-actions.tsx +119 -0
  206. package/src/components/nav-sidebar.stories.tsx +58 -0
  207. package/src/components/nav-sidebar.test.tsx +54 -0
  208. package/src/components/nav-sidebar.tsx +462 -0
  209. package/src/components/number-stepper.stories.tsx +19 -0
  210. package/src/components/number-stepper.tsx +158 -0
  211. package/src/components/page-header.stories.tsx +25 -0
  212. package/src/components/page-header.tsx +64 -0
  213. package/src/components/pagination.stories.tsx +110 -0
  214. package/src/components/pagination.test.tsx +46 -0
  215. package/src/components/pagination.tsx +144 -0
  216. package/src/components/popover.stories.tsx +26 -0
  217. package/src/components/popover.tsx +62 -0
  218. package/src/components/pricing.stories.tsx +42 -0
  219. package/src/components/pricing.tsx +130 -0
  220. package/src/components/progress.stories.tsx +22 -0
  221. package/src/components/progress.tsx +189 -0
  222. package/src/components/prompt-input.stories.tsx +62 -0
  223. package/src/components/prompt-input.test.tsx +106 -0
  224. package/src/components/prompt-input.tsx +343 -0
  225. package/src/components/prompt-suggestions.stories.tsx +53 -0
  226. package/src/components/prompt-suggestions.test.tsx +37 -0
  227. package/src/components/prompt-suggestions.tsx +122 -0
  228. package/src/components/quota-bar.stories.tsx +19 -0
  229. package/src/components/quota-bar.tsx +191 -0
  230. package/src/components/quote.stories.tsx +18 -0
  231. package/src/components/quote.tsx +43 -0
  232. package/src/components/radio-card.stories.tsx +27 -0
  233. package/src/components/radio-card.tsx +225 -0
  234. package/src/components/radio-group.stories.tsx +32 -0
  235. package/src/components/radio-group.tsx +68 -0
  236. package/src/components/reasoning.stories.tsx +50 -0
  237. package/src/components/reasoning.test.tsx +48 -0
  238. package/src/components/reasoning.tsx +143 -0
  239. package/src/components/search.stories.tsx +18 -0
  240. package/src/components/search.tsx +133 -0
  241. package/src/components/section-nav.stories.tsx +59 -0
  242. package/src/components/section-nav.test.tsx +36 -0
  243. package/src/components/section-nav.tsx +146 -0
  244. package/src/components/segmented-control.stories.tsx +22 -0
  245. package/src/components/segmented-control.tsx +301 -0
  246. package/src/components/select-button.stories.tsx +27 -0
  247. package/src/components/select-button.tsx +174 -0
  248. package/src/components/select.stories.tsx +27 -0
  249. package/src/components/select.tsx +198 -0
  250. package/src/components/separator.stories.tsx +22 -0
  251. package/src/components/separator.tsx +23 -0
  252. package/src/components/shimmer-text.stories.tsx +23 -0
  253. package/src/components/shimmer-text.test.tsx +35 -0
  254. package/src/components/shimmer-text.tsx +43 -0
  255. package/src/components/sidebar.stories.tsx +25 -0
  256. package/src/components/sidebar.tsx +234 -0
  257. package/src/components/skeleton.stories.tsx +22 -0
  258. package/src/components/skeleton.tsx +96 -0
  259. package/src/components/slider.stories.tsx +20 -0
  260. package/src/components/slider.tsx +176 -0
  261. package/src/components/slideshow.stories.tsx +30 -0
  262. package/src/components/slideshow.tsx +291 -0
  263. package/src/components/source.stories.tsx +32 -0
  264. package/src/components/source.test.tsx +40 -0
  265. package/src/components/source.tsx +161 -0
  266. package/src/components/spinner.stories.tsx +19 -0
  267. package/src/components/spinner.tsx +76 -0
  268. package/src/components/stat-strip.stories.tsx +36 -0
  269. package/src/components/stat-strip.test.tsx +25 -0
  270. package/src/components/stat-strip.tsx +85 -0
  271. package/src/components/stats.stories.tsx +24 -0
  272. package/src/components/stats.tsx +83 -0
  273. package/src/components/step-indicator.stories.tsx +18 -0
  274. package/src/components/step-indicator.test.tsx +18 -0
  275. package/src/components/step-indicator.tsx +286 -0
  276. package/src/components/switch.stories.tsx +26 -0
  277. package/src/components/switch.tsx +32 -0
  278. package/src/components/table.stories.tsx +38 -0
  279. package/src/components/table.tsx +220 -0
  280. package/src/components/tabs.stories.tsx +25 -0
  281. package/src/components/tabs.tsx +276 -0
  282. package/src/components/testimonials.stories.tsx +22 -0
  283. package/src/components/testimonials.tsx +109 -0
  284. package/src/components/text-link.stories.tsx +47 -0
  285. package/src/components/text-link.test.tsx +18 -0
  286. package/src/components/text-link.tsx +88 -0
  287. package/src/components/text.stories.tsx +49 -0
  288. package/src/components/text.tsx +105 -0
  289. package/src/components/thinking-bar.stories.tsx +19 -0
  290. package/src/components/thinking-bar.test.tsx +27 -0
  291. package/src/components/thinking-bar.tsx +64 -0
  292. package/src/components/thinking-steps.stories.tsx +44 -0
  293. package/src/components/thinking-steps.test.tsx +42 -0
  294. package/src/components/thinking-steps.tsx +92 -0
  295. package/src/components/toaster.stories.tsx +80 -0
  296. package/src/components/toaster.tsx +36 -0
  297. package/src/components/toggle.stories.tsx +21 -0
  298. package/src/components/toggle.tsx +103 -0
  299. package/src/components/tool-call-card.stories.tsx +97 -0
  300. package/src/components/tool-call-card.test.tsx +88 -0
  301. package/src/components/tool-call-card.tsx +231 -0
  302. package/src/components/tooltip.stories.tsx +25 -0
  303. package/src/components/tooltip.tsx +43 -0
  304. package/src/components/top-nav.stories.tsx +20 -0
  305. package/src/components/top-nav.tsx +76 -0
  306. package/src/docs/Colors.stories.tsx +192 -0
  307. package/src/docs/GettingStarted.mdx +72 -0
  308. package/src/docs/Mcp.mdx +52 -0
  309. package/src/docs/Typography.stories.tsx +114 -0
  310. package/src/hooks/use-in-view.ts +32 -0
  311. package/src/hooks/use-reduced-motion.ts +26 -0
  312. package/src/index.ts +343 -0
  313. package/src/lib/cn.test.ts +67 -0
  314. package/src/lib/cn.ts +81 -0
  315. package/src/lib/motion.ts +109 -0
  316. package/src/lib/prefix-utilities.test.ts +56 -0
  317. package/src/lib/prefix-utilities.ts +50 -0
  318. package/src/styles/base.css +489 -0
  319. package/src/styles/motion.css +69 -0
  320. package/src/styles/overrides.css +44 -0
  321. package/src/styles/typography.css +173 -0
  322. package/src/theme/theme.css +391 -0
  323. package/src/theme/tokens.ts +29 -0
  324. package/src/theme/types.ts +26 -0
  325. package/README.md +0 -3
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { DataList, DataListItem, DataListLabel, DataListValue } from "./data-list"
5
+
6
+ const list = (props: React.ComponentProps<typeof DataList>) => renderToStaticMarkup(
7
+ <DataList {...props}>
8
+ <DataListItem><DataListLabel>Trade licence</DataListLabel><DataListValue>2120561384412A</DataListValue></DataListItem>
9
+ </DataList>,
10
+ )
11
+
12
+ describe("DataList grid", () => {
13
+ it("lays items out in columns without dividers", () => {
14
+ const html = list({ orientation: "grid", columns: 4 })
15
+ expect(html).toContain("ax:grid ax:gap-x-lg ax:grid-cols-2 ax:sm:grid-cols-4")
16
+ expect(html).not.toContain("ax:divide-y")
17
+ })
18
+
19
+ it("stacks label above value and drops the horizontal label width", () => {
20
+ const html = list({ orientation: "grid" })
21
+ expect(html).not.toContain("ax:sm:flex-row")
22
+ expect(html).not.toContain("ax:sm:min-w-32")
23
+ })
24
+
25
+ it("keeps the divided list as the default", () => {
26
+ expect(list({})).toContain("ax:divide-y")
27
+ })
28
+
29
+ it("shares one label column across horizontal rows from sm up", () => {
30
+ const html = list({})
31
+ expect(html).toContain("ax:sm:grid ax:sm:grid-cols-[auto_1fr]")
32
+ expect(html).toContain("ax:sm:grid-cols-subgrid")
33
+ expect(html).toContain("ax:sm:gap-x-sm")
34
+ })
35
+ })
@@ -0,0 +1,181 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../lib/cn"
5
+
6
+ export type DataListSize = "sm" | "md" | "lg"
7
+ export type DataListOrientation = "horizontal" | "vertical" | "grid"
8
+ export type DataListColumns = 2 | 3 | 4
9
+
10
+ interface DataListContextValue {
11
+ size: DataListSize
12
+ orientation: DataListOrientation
13
+ }
14
+
15
+ const DataListContext = React.createContext<DataListContextValue>({
16
+ size: "md",
17
+ orientation: "horizontal",
18
+ })
19
+
20
+ // ─── Root ─────────────────────────────────────────────────────────────────────
21
+
22
+ export interface DataListProps extends React.HTMLAttributes<HTMLDListElement> {
23
+ size?: DataListSize
24
+ /** `grid` lays items out in columns, label above value, without dividers -
25
+ * for short facts such as licence numbers or key dates. */
26
+ orientation?: DataListOrientation
27
+ /** Columns from `sm` up when `orientation="grid"`; two below `sm`. */
28
+ columns?: DataListColumns
29
+ }
30
+
31
+ const GRID_COLUMNS: Record<DataListColumns, string> = {
32
+ 2: "ax:grid-cols-2",
33
+ 3: "ax:grid-cols-2 ax:sm:grid-cols-3",
34
+ 4: "ax:grid-cols-2 ax:sm:grid-cols-4",
35
+ }
36
+
37
+ export const DataList = React.forwardRef<HTMLDListElement, DataListProps>(
38
+ ({ className, size = "md", orientation = "horizontal", columns = 2, ...props }, ref) => (
39
+ <DataListContext.Provider value={{ size, orientation }}>
40
+ <dl
41
+ ref={ref}
42
+ className={cn(
43
+ orientation === "grid"
44
+ ? cn("ax:grid ax:gap-x-lg", GRID_COLUMNS[columns])
45
+ : cn(
46
+ "ax:divide-y ax:divide-border",
47
+ // Rows share these column tracks via `grid-cols-subgrid` on each
48
+ // DataListItem, so the label column's `auto` track sizes to the
49
+ // single widest label across every row, not a per-row guess.
50
+ orientation === "horizontal" && "ax:sm:grid ax:sm:grid-cols-[auto_1fr]",
51
+ ),
52
+ className,
53
+ )}
54
+ {...props}
55
+ />
56
+ </DataListContext.Provider>
57
+ )
58
+ )
59
+ DataList.displayName = "DataList"
60
+
61
+ // ─── Item ─────────────────────────────────────────────────────────────────────
62
+ //
63
+ // The middle (md) row uses `py-2.5` (a non-token half-step) so its visual
64
+ // rhythm matches the Table component's `md` row exactly. sm/lg map cleanly
65
+ // to the spacing scale.
66
+
67
+ const ITEM_PADDING: Record<DataListSize, string> = {
68
+ sm: "ax:py-xs", // 8px
69
+ md: "ax:py-2.5", // 10px (intentional half-step, matches Table md)
70
+ lg: "ax:py-sm", // 12px
71
+ }
72
+
73
+ // Label↔value gap when the item is stacked (vertical, and horizontal on mobile).
74
+ const ITEM_GAP: Record<DataListSize, string> = {
75
+ sm: "ax:gap-2xs", // 4px
76
+ md: "ax:gap-2xs", // 4px
77
+ lg: "ax:gap-xs", // 8px
78
+ }
79
+
80
+ // Label↔value gap in the row layout (horizontal from `sm` up) - a bit more
81
+ // than the stacked gap, since side-by-side text reads better with clearer
82
+ // separation than text stacked label-above-value does.
83
+ const ITEM_GAP_X: Record<DataListSize, string> = {
84
+ sm: "ax:sm:gap-x-xs", // 8px
85
+ md: "ax:sm:gap-x-sm", // 12px
86
+ lg: "ax:sm:gap-x-md", // 16px
87
+ }
88
+
89
+ export const DataListItem = React.forwardRef<
90
+ HTMLDivElement,
91
+ React.HTMLAttributes<HTMLDivElement>
92
+ >(({ className, ...props }, ref) => {
93
+ const { size, orientation } = React.useContext(DataListContext)
94
+ return (
95
+ <div
96
+ ref={ref}
97
+ className={cn(
98
+ "ax:flex ax:flex-col",
99
+ // Horizontal is responsive: stacked (label above value) on mobile, then
100
+ // a baseline-aligned row from `sm` up. The row uses `grid-cols-subgrid`
101
+ // so its label/value cells adopt the DataList's shared column tracks
102
+ // instead of sizing independently per row.
103
+ orientation === "horizontal" && "ax:sm:grid ax:sm:grid-cols-subgrid ax:sm:col-span-full ax:sm:items-baseline",
104
+ ITEM_PADDING[size],
105
+ ITEM_GAP[size],
106
+ orientation === "horizontal" && ITEM_GAP_X[size],
107
+ className
108
+ )}
109
+ {...props}
110
+ />
111
+ )
112
+ })
113
+ DataListItem.displayName = "DataListItem"
114
+
115
+ // ─── Label ────────────────────────────────────────────────────────────────────
116
+ //
117
+ // The label column's actual width comes from the DataListItem's subgrid (see
118
+ // above): it's an `auto` track, so it sizes to the single widest label across
119
+ // every row. `min-width` here is just a floor for when every label is short,
120
+ // so the column never gets uncomfortably narrow. It applies only from `sm` up;
121
+ // while stacked on mobile the label is a full-width block above the value, so
122
+ // it needs no minimum.
123
+
124
+ const LABEL_TEXT: Record<DataListSize, string> = {
125
+ sm: "ax:t-caption",
126
+ md: "ax:t-body-sm",
127
+ lg: "ax:t-body-sm",
128
+ }
129
+
130
+ const LABEL_MIN_WIDTH: Record<DataListSize, string> = {
131
+ sm: "ax:sm:min-w-25", // 100px
132
+ md: "ax:sm:min-w-32", // 128px
133
+ lg: "ax:sm:min-w-36", // 144px
134
+ }
135
+
136
+ export const DataListLabel = React.forwardRef<
137
+ HTMLElement,
138
+ React.HTMLAttributes<HTMLElement>
139
+ >(({ className, ...props }, ref) => {
140
+ const { size, orientation } = React.useContext(DataListContext)
141
+ return (
142
+ <dt
143
+ ref={ref as React.Ref<HTMLElement>}
144
+ className={cn(
145
+ "ax:text-muted-foreground",
146
+ LABEL_TEXT[size],
147
+ orientation === "horizontal" && LABEL_MIN_WIDTH[size],
148
+ className
149
+ )}
150
+ {...props}
151
+ />
152
+ )
153
+ })
154
+ DataListLabel.displayName = "DataListLabel"
155
+
156
+ // ─── Value ────────────────────────────────────────────────────────────────────
157
+
158
+ const VALUE_TEXT: Record<DataListSize, string> = {
159
+ sm: "ax:t-caption",
160
+ md: "ax:t-body-sm",
161
+ lg: "ax:t-body-md",
162
+ }
163
+
164
+ export const DataListValue = React.forwardRef<
165
+ HTMLElement,
166
+ React.HTMLAttributes<HTMLElement>
167
+ >(({ className, ...props }, ref) => {
168
+ const { size } = React.useContext(DataListContext)
169
+ return (
170
+ <dd
171
+ ref={ref as React.Ref<HTMLElement>}
172
+ className={cn(
173
+ "ax:text-foreground ax:font-medium ax:min-w-0",
174
+ VALUE_TEXT[size],
175
+ className
176
+ )}
177
+ {...props}
178
+ />
179
+ )
180
+ })
181
+ DataListValue.displayName = "DataListValue"
@@ -0,0 +1,29 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { DataTable, rowStyle, type ColumnDef } from "./data-table"
4
+
5
+ type Row = { name: string; role: string; status: string }
6
+
7
+ const columns: ColumnDef<Row>[] = [
8
+ { key: "name", header: "Name", accessor: (r) => r.name, sortable: true },
9
+ { key: "role", header: "Role", accessor: (r) => r.role },
10
+ { key: "status", header: "Status", accessor: (r) => <span className={rowStyle.muted}>{r.status}</span> },
11
+ ]
12
+
13
+ const data: Row[] = [
14
+ { name: "Aisha Khan", role: "Admin", status: "Active" },
15
+ { name: "Marco Silva", role: "Editor", status: "Active" },
16
+ { name: "Leila Hassan", role: "Viewer", status: "Invited" },
17
+ ]
18
+
19
+ const meta: Meta<typeof DataTable<Row>> = {
20
+ title: "Components/DataTable",
21
+ component: DataTable,
22
+ tags: ["autodocs"],
23
+ args: { columns, data },
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj<typeof DataTable<Row>>
28
+
29
+ export const Default: Story = {}
@@ -0,0 +1,200 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+
7
+ export type SortDirection = "asc" | "desc" | null
8
+ export type DataTableSize = "sm" | "md" | "lg"
9
+ export type DataTableAlign = "start" | "center" | "end"
10
+
11
+ export interface ColumnDef<T> {
12
+ key: string
13
+ header: React.ReactNode
14
+ accessor: (row: T) => React.ReactNode
15
+ sortable?: boolean
16
+ /** Logical inline alignment. `start` and `end` flip in RTL. */
17
+ align?: DataTableAlign
18
+ className?: string
19
+ }
20
+
21
+ export interface DataTableProps<T> extends Omit<React.HTMLAttributes<HTMLDivElement>, "onClick"> {
22
+ columns: ColumnDef<T>[]
23
+ data: T[]
24
+ /** Visual density. Matches the Table component's size scale. */
25
+ size?: DataTableSize
26
+ /** Key-accessor for React list rendering. Defaults to row index. */
27
+ rowKey?: (row: T, index: number) => string | number
28
+ /** Per-row className. Lets callers visually mark rows (placeholders, status-specific tinting, etc.). */
29
+ rowClassName?: (row: T, index: number) => string | undefined
30
+ /** Row click handler. When provided, rows become clickable (cursor + role).
31
+ * Interactive children (buttons, links, menu triggers) should stopPropagation
32
+ * if they need to bypass the row click. */
33
+ onRowClick?: (row: T, index: number) => void
34
+ /** Shown when data is empty. */
35
+ emptyMessage?: string
36
+ /** Hide the header row entirely. */
37
+ headless?: boolean
38
+ }
39
+
40
+ // Matches the Table component's CELL_PADDING map exactly so the two stay in
41
+ // visual sync: `py-1.5` and `py-2.5` are intentional half-steps.
42
+ const CELL_PADDING: Record<DataTableSize, string> = {
43
+ sm: "ax:px-sm ax:py-1.5",
44
+ md: "ax:px-md ax:py-2.5",
45
+ lg: "ax:px-lg ax:py-md",
46
+ }
47
+
48
+ // Font-size + line-height per density. Applied to every <td> so column
49
+ // accessors inherit the right base and only need to layer weight/colour.
50
+ const CELL_TYPOGRAPHY: Record<DataTableSize, string> = {
51
+ sm: "ax:text-xs ax:leading-tight",
52
+ md: "ax:text-sm ax:leading-tight",
53
+ lg: "ax:text-base ax:leading-tight",
54
+ }
55
+
56
+ // Named row-text variants for column accessors. They intentionally carry no
57
+ // font-size or line-height: those come from the table's size prop above.
58
+ export const rowStyle = {
59
+ title: "ax:font-medium ax:text-foreground-strong",
60
+ default: "ax:font-normal ax:text-foreground",
61
+ muted: "ax:font-normal ax:text-muted-foreground",
62
+ } as const
63
+
64
+ function SortIcon({ direction }: { direction: SortDirection }) {
65
+ if (direction === "asc") return <ChevronUp className="ax:size-3.5 ax:shrink-0" />
66
+ if (direction === "desc") return <ChevronDown className="ax:size-3.5 ax:shrink-0" />
67
+ return <ChevronsUpDown className="ax:size-3.5 ax:shrink-0 ax:text-muted-foreground/50" />
68
+ }
69
+
70
+ function alignClass(align?: DataTableAlign): string {
71
+ if (align === "center") return "ax:text-center"
72
+ if (align === "end") return "ax:text-end"
73
+ return "ax:text-start"
74
+ }
75
+
76
+ export function DataTable<T>({
77
+ columns,
78
+ data,
79
+ size = "md",
80
+ className,
81
+ rowKey,
82
+ rowClassName,
83
+ onRowClick,
84
+ emptyMessage = "No results.",
85
+ headless = false,
86
+ ...props
87
+ }: DataTableProps<T>) {
88
+ const [sortKey, setSortKey] = React.useState<string | null>(null)
89
+ const [sortDir, setSortDir] = React.useState<SortDirection>(null)
90
+
91
+ const handleSort = (key: string) => {
92
+ if (sortKey !== key) {
93
+ setSortKey(key)
94
+ setSortDir("asc")
95
+ return
96
+ }
97
+ if (sortDir === "asc") { setSortDir("desc"); return }
98
+ if (sortDir === "desc") { setSortKey(null); setSortDir(null); return }
99
+ setSortDir("asc")
100
+ }
101
+
102
+ const sortedData = React.useMemo(() => {
103
+ if (!sortKey || !sortDir) return data
104
+ const col = columns.find((c) => c.key === sortKey)
105
+ if (!col) return data
106
+ return [...data].sort((a, b) => {
107
+ const av = col.accessor(a)
108
+ const bv = col.accessor(b)
109
+ const as = String(av ?? "")
110
+ const bs = String(bv ?? "")
111
+ const cmp = as.localeCompare(bs, undefined, { numeric: true })
112
+ return sortDir === "asc" ? cmp : -cmp
113
+ })
114
+ }, [data, sortKey, sortDir, columns])
115
+
116
+ const cellPad = CELL_PADDING[size]
117
+
118
+ return (
119
+ <div
120
+ data-slot="data-table"
121
+ className={cn("ax:w-full ax:overflow-auto ax:rounded-lg ax:border ax:border-border ax:bg-card", className)}
122
+ {...props}
123
+ >
124
+ <table className="ax:w-full">
125
+ {!headless && (
126
+ <thead className="ax:sticky ax:top-0 ax:z-10 ax:border-b ax:border-border ax:bg-muted/50">
127
+ <tr>
128
+ {columns.map((col) => (
129
+ <th
130
+ key={col.key}
131
+ className={cn(
132
+ cellPad,
133
+ CELL_TYPOGRAPHY[size],
134
+ "ax:font-medium ax:text-muted-foreground",
135
+ alignClass(col.align),
136
+ col.sortable && "ax:cursor-pointer ax:select-none ax:hover:text-foreground ax:transition-colors",
137
+ col.className
138
+ )}
139
+ onClick={col.sortable ? () => handleSort(col.key) : undefined}
140
+ aria-sort={
141
+ sortKey === col.key
142
+ ? sortDir === "asc"
143
+ ? "ascending"
144
+ : "descending"
145
+ : undefined
146
+ }
147
+ >
148
+ <span className="ax:inline-flex ax:items-center ax:gap-1.5">
149
+ {col.header}
150
+ {col.sortable && (
151
+ <SortIcon direction={sortKey === col.key ? sortDir : null} />
152
+ )}
153
+ </span>
154
+ </th>
155
+ ))}
156
+ </tr>
157
+ </thead>
158
+ )}
159
+ <tbody className="ax:divide-y ax:divide-border">
160
+ {sortedData.length === 0 ? (
161
+ <tr>
162
+ <td
163
+ colSpan={columns.length}
164
+ className={cn(cellPad, "ax:text-center ax:text-muted-foreground ax:py-2xl")}
165
+ >
166
+ {emptyMessage}
167
+ </td>
168
+ </tr>
169
+ ) : (
170
+ sortedData.map((row, i) => (
171
+ <tr
172
+ key={rowKey ? rowKey(row, i) : i}
173
+ className={cn(
174
+ "ax:hover:bg-muted/40 ax:transition-colors",
175
+ onRowClick && "ax:cursor-pointer",
176
+ rowClassName?.(row, i),
177
+ )}
178
+ onClick={onRowClick ? () => onRowClick(row, i) : undefined}
179
+ >
180
+ {columns.map((col) => (
181
+ <td
182
+ key={col.key}
183
+ className={cn(
184
+ cellPad,
185
+ CELL_TYPOGRAPHY[size],
186
+ alignClass(col.align),
187
+ col.className
188
+ )}
189
+ >
190
+ {col.accessor(row)}
191
+ </td>
192
+ ))}
193
+ </tr>
194
+ ))
195
+ )}
196
+ </tbody>
197
+ </table>
198
+ </div>
199
+ )
200
+ }
@@ -0,0 +1,42 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Button } from "./button"
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ DialogTrigger,
12
+ } from "./dialog"
13
+
14
+ const meta: Meta<typeof Dialog> = {
15
+ title: "Components/Dialog",
16
+ component: Dialog,
17
+ tags: ["autodocs"],
18
+ }
19
+
20
+ export default meta
21
+ type Story = StoryObj<typeof Dialog>
22
+
23
+ export const Default: Story = {
24
+ render: () => (
25
+ <Dialog>
26
+ <DialogTrigger asChild>
27
+ <Button>Open dialog</Button>
28
+ </DialogTrigger>
29
+ <DialogContent>
30
+ <DialogHeader>
31
+ <DialogTitle>Edit profile</DialogTitle>
32
+ <DialogDescription>Make changes to your profile here.</DialogDescription>
33
+ </DialogHeader>
34
+ <p className="ax:t-body-sm ax:text-muted-foreground">Dialog body content.</p>
35
+ <DialogFooter>
36
+ <Button variant="outline">Cancel</Button>
37
+ <Button>Save</Button>
38
+ </DialogFooter>
39
+ </DialogContent>
40
+ </Dialog>
41
+ ),
42
+ }
@@ -0,0 +1,200 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+ import { cn } from "../lib/cn"
7
+
8
+ // ─── Types ────────────────────────────────────────────────────────────────────
9
+
10
+ export type DialogSize = "sm" | "md" | "lg"
11
+
12
+ const DialogSizeContext = React.createContext<DialogSize>("md")
13
+
14
+ // ─── Size tokens ──────────────────────────────────────────────────────────────
15
+
16
+ const MAX_WIDTH: Record<DialogSize, string> = {
17
+ sm: "ax:max-w-xs", // 320px - compact confirmations / alerts
18
+ md: "ax:max-w-md", // 448px - DEFAULT - typical form dialogs
19
+ lg: "ax:max-w-3xl", // 768px - wide layouts / multi-column content
20
+ }
21
+
22
+ const PADDING: Record<DialogSize, string> = {
23
+ sm: "ax:p-lg", // 20px
24
+ md: "ax:p-xl", // 24px
25
+ lg: "ax:p-2xl", // 32px
26
+ }
27
+
28
+ const GAP: Record<DialogSize, string> = {
29
+ sm: "ax:gap-sm", // 12px
30
+ md: "ax:gap-md", // 16px
31
+ lg: "ax:gap-xl", // 24px
32
+ }
33
+
34
+ const TITLE_SIZE: Record<DialogSize, string> = {
35
+ sm: "ax:t-h3",
36
+ md: "ax:t-h3",
37
+ lg: "ax:t-h2",
38
+ }
39
+
40
+ const DESCRIPTION_SIZE: Record<DialogSize, string> = {
41
+ sm: "ax:t-body-sm",
42
+ md: "ax:t-body-sm",
43
+ lg: "ax:t-body-md",
44
+ }
45
+
46
+ const CLOSE_ICON: Record<DialogSize, string> = {
47
+ sm: "ax:size-4",
48
+ md: "ax:size-4",
49
+ lg: "ax:size-5",
50
+ }
51
+
52
+ // Footer top-margin scales with size so the gap above the action row feels
53
+ // proportional to the dialog's interior padding.
54
+ const FOOTER_MT: Record<DialogSize, string> = {
55
+ sm: "ax:mt-sm",
56
+ md: "ax:mt-md",
57
+ lg: "ax:mt-md",
58
+ }
59
+
60
+ // ─── Primitives (unchanged) ───────────────────────────────────────────────────
61
+
62
+ export const Dialog = DialogPrimitive.Root
63
+ export const DialogTrigger = DialogPrimitive.Trigger
64
+ export const DialogPortal = DialogPrimitive.Portal
65
+ export const DialogClose = DialogPrimitive.Close
66
+
67
+ // ─── Overlay ─────────────────────────────────────────────────────────────────
68
+
69
+ export const DialogOverlay = React.forwardRef<
70
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
71
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
72
+ >(({ className, ...props }, ref) => (
73
+ <DialogPrimitive.Overlay
74
+ ref={ref}
75
+ className={cn(
76
+ // `--foreground` flips light in dark mode, which would tint the scrim
77
+ // whitish, so use a black scrim in dark at the same opacity + blur.
78
+ "ax:fixed ax:inset-0 ax:z-[1100] ax:bg-foreground/40 ax:dark:bg-black/40 ax:backdrop-blur-sm",
79
+ "ax:data-[state=open]:animate-in ax:data-[state=closed]:animate-out",
80
+ "ax:data-[state=open]:fade-in-0 ax:data-[state=closed]:fade-out-0",
81
+ "ax:[animation-duration:var(--motion-base)] ax:[animation-timing-function:var(--motion-ease-out)]",
82
+ "ax:motion-reduce:animate-none",
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ ))
88
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
89
+
90
+ // ─── Content ─────────────────────────────────────────────────────────────────
91
+
92
+ export interface DialogContentProps
93
+ extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
94
+ size?: DialogSize
95
+ }
96
+
97
+ export const DialogContent = React.forwardRef<
98
+ React.ElementRef<typeof DialogPrimitive.Content>,
99
+ DialogContentProps
100
+ >(({ className, children, size = "md", ...props }, ref) => (
101
+ <DialogSizeContext.Provider value={size}>
102
+ <DialogPortal>
103
+ <DialogOverlay />
104
+ <DialogPrimitive.Content
105
+ ref={ref}
106
+ className={cn(
107
+ "ax:fixed ax:left-1/2 ax:top-1/2 ax:z-[1100] ax:grid ax:w-full ax:-translate-x-1/2 ax:-translate-y-1/2",
108
+ "ax:border ax:border-border ax:bg-card ax:shadow-lg ax:rounded-lg",
109
+ MAX_WIDTH[size],
110
+ PADDING[size],
111
+ GAP[size],
112
+ "ax:data-[state=open]:animate-in ax:data-[state=closed]:animate-out",
113
+ "ax:data-[state=open]:fade-in-0 ax:data-[state=closed]:fade-out-0",
114
+ "ax:data-[state=open]:zoom-in-[0.96] ax:data-[state=closed]:zoom-out-[0.96]",
115
+ "ax:data-[state=open]:slide-in-from-bottom-2 ax:data-[state=closed]:slide-out-to-bottom-1",
116
+ "ax:[animation-duration:var(--motion-base)] ax:[animation-timing-function:var(--motion-ease-out)]",
117
+ "ax:motion-reduce:animate-none",
118
+ className
119
+ )}
120
+ {...props}
121
+ >
122
+ {children}
123
+ <DialogPrimitive.Close
124
+ className={cn(
125
+ "ax:absolute ax:right-md ax:top-md ax:rounded-sm ax:opacity-70 ax:transition-opacity",
126
+ "ax:hover:opacity-100 ax:focus:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-2",
127
+ "ax:disabled:pointer-events-none"
128
+ )}
129
+ >
130
+ <X className={CLOSE_ICON[size]} aria-hidden="true" />
131
+ <span className="ax:sr-only">Close</span>
132
+ </DialogPrimitive.Close>
133
+ </DialogPrimitive.Content>
134
+ </DialogPortal>
135
+ </DialogSizeContext.Provider>
136
+ ))
137
+ DialogContent.displayName = DialogPrimitive.Content.displayName
138
+
139
+ // ─── Header / Footer ─────────────────────────────────────────────────────────
140
+
141
+ export const DialogHeader = React.forwardRef<
142
+ HTMLDivElement,
143
+ React.HTMLAttributes<HTMLDivElement>
144
+ >(({ className, ...props }, ref) => (
145
+ <div ref={ref} className={cn("ax:flex ax:flex-col ax:gap-md ax:text-left", className)} {...props} />
146
+ ))
147
+ DialogHeader.displayName = "DialogHeader"
148
+
149
+ export const DialogFooter = React.forwardRef<
150
+ HTMLDivElement,
151
+ React.HTMLAttributes<HTMLDivElement>
152
+ >(({ className, ...props }, ref) => {
153
+ const size = React.useContext(DialogSizeContext)
154
+ return (
155
+ <div
156
+ ref={ref}
157
+ className={cn(
158
+ "ax:flex ax:flex-col-reverse ax:gap-xs ax:sm:flex-row ax:sm:justify-end",
159
+ FOOTER_MT[size],
160
+ className
161
+ )}
162
+ {...props}
163
+ />
164
+ )
165
+ })
166
+ DialogFooter.displayName = "DialogFooter"
167
+
168
+ // ─── Title ───────────────────────────────────────────────────────────────────
169
+
170
+ export const DialogTitle = React.forwardRef<
171
+ React.ElementRef<typeof DialogPrimitive.Title>,
172
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
173
+ >(({ className, ...props }, ref) => {
174
+ const size = React.useContext(DialogSizeContext)
175
+ return (
176
+ <DialogPrimitive.Title
177
+ ref={ref}
178
+ className={cn(TITLE_SIZE[size], className)}
179
+ {...props}
180
+ />
181
+ )
182
+ })
183
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
184
+
185
+ // ─── Description ─────────────────────────────────────────────────────────────
186
+
187
+ export const DialogDescription = React.forwardRef<
188
+ React.ElementRef<typeof DialogPrimitive.Description>,
189
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
190
+ >(({ className, ...props }, ref) => {
191
+ const size = React.useContext(DialogSizeContext)
192
+ return (
193
+ <DialogPrimitive.Description
194
+ ref={ref}
195
+ className={cn("ax:text-foreground", DESCRIPTION_SIZE[size], className)}
196
+ {...props}
197
+ />
198
+ )
199
+ })
200
+ DialogDescription.displayName = DialogPrimitive.Description.displayName