@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,284 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { TrendingDown, TrendingUp } from "lucide-react"
5
+ import { Card, CardContent } from "./card"
6
+ import { Skeleton } from "./skeleton"
7
+ import { cn } from "../lib/cn"
8
+
9
+ export type KpiCardSize = "mini" | "sm" | "md" | "lg"
10
+
11
+ // ─── Group context ────────────────────────────────────────────────────────────
12
+ //
13
+ // `KpiCardGroup compact` merges its children into a single visual block:
14
+ // the cards lose their individual rounding/shadow and overlap their borders
15
+ // (`-mt-px` on mobile, `-ms-px` on desktop). Children read this via context
16
+ // so the parent grouping is fully declarative; consumers don't need to thread
17
+ // per-card props.
18
+
19
+ interface KpiCardGroupContextValue {
20
+ compact: boolean
21
+ columns?: 2 | 3 | 4
22
+ }
23
+
24
+ const KpiCardGroupContext = React.createContext<KpiCardGroupContextValue>({ compact: false })
25
+
26
+ export interface KpiCardGroupProps extends React.HTMLAttributes<HTMLDivElement> {
27
+ /** Merge children into a borderless single-block layout. */
28
+ compact?: boolean
29
+ /** Lay the cards out in a fixed N-column grid instead of a single flex row.
30
+ * With `compact`, the grid renders as a seamless bordered "table" - 1px
31
+ * hairline dividers between cells inside a rounded outer frame. Give a card
32
+ * `sm:col-span-*` to make a row span fewer, wider columns (e.g. a 4-column
33
+ * grid whose second row is two `col-span-2` cells). */
34
+ columns?: 2 | 3 | 4
35
+ }
36
+
37
+ const GRID_COLS: Record<2 | 3 | 4, string> = {
38
+ 2: "ax:sm:grid-cols-2",
39
+ 3: "ax:sm:grid-cols-3",
40
+ 4: "ax:sm:grid-cols-4",
41
+ }
42
+
43
+ export const KpiCardGroup = React.forwardRef<HTMLDivElement, KpiCardGroupProps>(
44
+ ({ compact = false, columns, className, children, ...props }, ref) => (
45
+ <KpiCardGroupContext.Provider value={{ compact, columns }}>
46
+ <div
47
+ ref={ref}
48
+ className={cn(
49
+ columns
50
+ ? compact
51
+ // Seamless bordered "table": the container paints the seams:
52
+ // `bg-border` shows through the `gap-px` as 1px dividers, clipped
53
+ // to a rounded outer frame. Cards inside drop their own chrome.
54
+ ? cn("ax:grid ax:grid-cols-1 ax:gap-px ax:overflow-hidden ax:rounded-md ax:border ax:border-border ax:bg-border", GRID_COLS[columns])
55
+ // Non-compact: a plain N-column grid of separate cards.
56
+ : cn("ax:grid ax:gap-md ax:grid-cols-1", GRID_COLS[columns])
57
+ : compact
58
+ // Stacks vertically on mobile, becomes a horizontal row at `sm` and up.
59
+ ? "ax:flex ax:flex-col ax:sm:flex-row"
60
+ : "ax:grid ax:gap-md ax:sm:grid-cols-2 ax:lg:grid-cols-4",
61
+ className,
62
+ )}
63
+ {...props}
64
+ >
65
+ {children}
66
+ </div>
67
+ </KpiCardGroupContext.Provider>
68
+ ),
69
+ )
70
+ KpiCardGroup.displayName = "KpiCardGroup"
71
+
72
+ // ─── KpiCard ──────────────────────────────────────────────────────────────────
73
+
74
+ export interface KpiCardProps extends React.HTMLAttributes<HTMLDivElement> {
75
+ /** Overline label sitting above the value. */
76
+ title: string
77
+ /** The metric itself. Strings are useful for pre-formatted values
78
+ * ("76 %", "1.2k"). Numbers get a `0` fallback for the empty state. */
79
+ value: string | number
80
+ /** Optional period-over-period delta as a percentage. Positive → success
81
+ * pill with an up-arrow; negative → destructive pill with a down-arrow.
82
+ * `0` and `undefined` both render no delta - zero isn't a trend. */
83
+ change?: number
84
+ /** Optional suffix appended to the value (e.g. "%", "h"). Rendered in a
85
+ * smaller muted style so it doesn't compete with the number. */
86
+ suffix?: string
87
+ /** Optional topic icon - rendered muted, top-end of the card. */
88
+ icon?: React.ReactNode
89
+ /** Optional context line under the value (e.g. "vs last 30 days"). */
90
+ description?: React.ReactNode
91
+ /** Visual density. `md` is the default. */
92
+ size?: KpiCardSize
93
+ /** While true, swaps the title/value/delta for skeleton placeholders. */
94
+ loading?: boolean
95
+ }
96
+
97
+ // ─── Size tokens ──────────────────────────────────────────────────────────────
98
+ //
99
+ // Trend value uses raw `text-* font-bold` (not `t-h*`) on purpose: the
100
+ // typography tokens hard-code `text-foreground-strong`, which would override
101
+ // the semantic `text-success-strong` / `text-destructive-strong` color on the trend pill.
102
+
103
+ const CARD_PADDING: Record<KpiCardSize, string> = {
104
+ mini: "ax:p-md", // 16px (same footprint as sm; mini shrinks the value, not the card)
105
+ sm: "ax:p-md", // 16px
106
+ md: "ax:p-lg", // 20px
107
+ lg: "ax:p-xl", // 24px
108
+ }
109
+
110
+ const VALUE_CLASS: Record<KpiCardSize, string> = {
111
+ mini: "ax:t-h2", // text-lg (18px) - a compact metric
112
+ sm: "ax:t-h1", // text-2xl (24px)
113
+ md: "ax:t-display-md", // text-3xl (30px)
114
+ lg: "ax:t-display-lg", // text-4xl (36px)
115
+ }
116
+
117
+ // Vertical gap between the label, value, and description. `mini` halves the
118
+ // default (gap-sm = 12px → 6px) to sit its smaller value tight to the label.
119
+ const CONTENT_GAP: Record<KpiCardSize, string> = {
120
+ mini: "ax:gap-1.5", // 6px
121
+ sm: "ax:gap-0", // 12px
122
+ md: "ax:gap-0",
123
+ lg: "ax:gap-0",
124
+ }
125
+
126
+ const SUFFIX_CLASS: Record<KpiCardSize, string> = {
127
+ mini: "ax:t-caption",
128
+ sm: "ax:t-body-sm",
129
+ md: "ax:t-h5",
130
+ lg: "ax:t-h4",
131
+ }
132
+
133
+ const TREND_CLASS: Record<KpiCardSize, string> = {
134
+ mini: "ax:text-xs ax:font-semibold",
135
+ sm: "ax:text-sm ax:font-semibold",
136
+ md: "ax:text-base ax:font-semibold",
137
+ lg: "ax:text-lg ax:font-semibold",
138
+ }
139
+
140
+ const TREND_ICON_CLASS: Record<KpiCardSize, string> = {
141
+ mini: "ax:size-3",
142
+ sm: "ax:size-3.5",
143
+ md: "ax:size-4",
144
+ lg: "ax:size-5",
145
+ }
146
+
147
+ const ICON_CLASS: Record<KpiCardSize, string> = {
148
+ mini: "ax:size-3 ax:[&>svg]:size-3",
149
+ sm: "ax:size-3.5 ax:[&>svg]:size-3.5",
150
+ md: "ax:size-4 ax:[&>svg]:size-4",
151
+ lg: "ax:size-5 ax:[&>svg]:size-5",
152
+ }
153
+
154
+ const SKELETON_VALUE_HEIGHT: Record<KpiCardSize, string> = {
155
+ mini: "ax:h-5",
156
+ sm: "ax:h-6",
157
+ md: "ax:h-7",
158
+ lg: "ax:h-9",
159
+ }
160
+
161
+ // Compact-group layout: cards round only their outer edges and overlap
162
+ // borders by 1px so the seam between two cards renders as a single line
163
+ // rather than a doubled-up border.
164
+ const COMPACT_CARD_LAYOUT = [
165
+ "ax:flex-1 ax:min-w-0",
166
+ // Drop shadow + per-card rounding when inside a compact group. `bg-card`
167
+ // restores the solid fill that the flat `level0` elevation clears, so the
168
+ // merged block keeps its surface while staying shadowless.
169
+ "ax:rounded-none ax:shadow-none ax:bg-card",
170
+ // Mobile (stacked): round top of first card, bottom of last; overlap rows.
171
+ "ax:first:rounded-t-md ax:last:rounded-b-md",
172
+ "ax:[&:not(:first-child)]:-mt-px",
173
+ // Desktop (row): round inline-start of first, inline-end of last;
174
+ // overlap columns instead of rows.
175
+ "ax:sm:first:rounded-t-none ax:sm:last:rounded-b-none",
176
+ "ax:sm:first:rounded-s-md ax:sm:last:rounded-e-md",
177
+ "ax:sm:[&:not(:first-child)]:mt-0 ax:sm:[&:not(:first-child)]:-ms-px",
178
+ ].join(" ")
179
+
180
+ // Grid-table layout (compact + columns): cells carry no chrome of their own:
181
+ // the group's rounded frame and gap-px hairlines draw every edge, so a card is
182
+ // just a filled rectangle.
183
+ const GRID_TABLE_CARD_LAYOUT = "ax:min-w-0 ax:border-0 ax:rounded-none ax:shadow-none ax:bg-card"
184
+
185
+ /** Single-metric card with optional trend delta. A common dashboard
186
+ * primitive: pairs a big number with a label and (optionally) a
187
+ * period-over-period change. The empty state ("No data yet") activates
188
+ * only when the numeric value is `0` and no `change` is provided. */
189
+ export const KpiCard = React.forwardRef<HTMLDivElement, KpiCardProps>(
190
+ (
191
+ {
192
+ title,
193
+ value,
194
+ change,
195
+ suffix,
196
+ icon,
197
+ description,
198
+ size = "md",
199
+ loading,
200
+ className,
201
+ ...props
202
+ },
203
+ ref,
204
+ ) => {
205
+ const { compact, columns } = React.useContext(KpiCardGroupContext)
206
+ const gridTable = compact && columns != null
207
+
208
+ const numericValue = typeof value === "number" ? value : Number(value)
209
+ const isEmpty = numericValue === 0
210
+ const hasChange = change !== undefined && change !== 0 && !isEmpty
211
+ const isPositive = (change ?? 0) > 0
212
+
213
+ // When an onClick is provided the card becomes interactive: resting
214
+ // shadow-xs lifts to shadow-md on hover, cursor flips to pointer.
215
+ // Compact cards are excluded: they have no individual shadow.
216
+ const isClickable = !compact && typeof props.onClick === "function"
217
+
218
+ const cardClasses = cn(
219
+ CARD_PADDING[size],
220
+ gridTable ? GRID_TABLE_CARD_LAYOUT : compact && COMPACT_CARD_LAYOUT,
221
+ className,
222
+ )
223
+
224
+ if (loading) {
225
+ return (
226
+ <Card ref={ref} elevation={compact ? "level0" : "level2"} interactive={isClickable} className={cardClasses} {...props}>
227
+ <CardContent className={cn("ax:p-0 ax:flex ax:flex-col", CONTENT_GAP[size])}>
228
+ <Skeleton className="ax:h-3 ax:w-24" />
229
+ <div className="ax:flex ax:items-end ax:justify-between ax:gap-sm">
230
+ <Skeleton className={cn(SKELETON_VALUE_HEIGHT[size], "ax:w-32")} />
231
+ <Skeleton className="ax:h-4 ax:w-12" />
232
+ </div>
233
+ </CardContent>
234
+ </Card>
235
+ )
236
+ }
237
+
238
+ return (
239
+ <Card ref={ref} elevation={compact ? "level0" : "level2"} interactive={isClickable} className={cardClasses} {...props}>
240
+ <CardContent className={cn("ax:p-0 ax:flex ax:flex-col", CONTENT_GAP[size])}>
241
+ <div className="ax:flex ax:items-start ax:justify-between ax:gap-sm">
242
+ <p className="ax:t-overline ax:text-muted-foreground">{title}</p>
243
+ {icon && (
244
+ <span className={cn("ax:shrink-0 ax:text-muted-foreground ax:[&>svg]:shrink-0", ICON_CLASS[size])}>
245
+ {icon}
246
+ </span>
247
+ )}
248
+ </div>
249
+
250
+ <div className={cn("ax:flex ax:items-end ax:gap-sm", compact ? "ax:justify-start" : "ax:justify-between")}>
251
+ <p className={cn(VALUE_CLASS[size], "ax:font-semibold ax:text-foreground")}>
252
+ {isEmpty ? "0" : value}
253
+ {suffix && <span className={cn(SUFFIX_CLASS[size], "ax:text-muted-foreground ax:ms-1")}>{suffix}</span>}
254
+ </p>
255
+
256
+ {hasChange ? (
257
+ <div
258
+ className={cn(
259
+ "ax:flex ax:items-center ax:gap-1",
260
+ TREND_CLASS[size],
261
+ isPositive ? "ax:text-success-foreground" : "ax:text-destructive-foreground",
262
+ )}
263
+ >
264
+ {isPositive ? (
265
+ <TrendingUp className={cn(TREND_ICON_CLASS[size], "ax:shrink-0")} />
266
+ ) : (
267
+ <TrendingDown className={cn(TREND_ICON_CLASS[size], "ax:shrink-0")} />
268
+ )}
269
+ <span>{Math.abs(change ?? 0)}%</span>
270
+ </div>
271
+ ) : isEmpty ? (
272
+ <span className="ax:t-caption ax:text-muted-foreground">No data yet</span>
273
+ ) : null}
274
+ </div>
275
+
276
+ {description && !isEmpty && (
277
+ <p className="ax:t-caption ax:text-muted-foreground">{description}</p>
278
+ )}
279
+ </CardContent>
280
+ </Card>
281
+ )
282
+ },
283
+ )
284
+ KpiCard.displayName = "KpiCard"
@@ -0,0 +1,22 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { ListNav, ListNavItem } from "./list-nav"
4
+
5
+ const meta: Meta<typeof ListNav> = {
6
+ title: "Components/ListNav",
7
+ component: ListNav,
8
+ tags: ["autodocs"],
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof ListNav>
13
+
14
+ export const Default: Story = {
15
+ render: () => (
16
+ <ListNav title="Settings">
17
+ <ListNavItem active>General</ListNavItem>
18
+ <ListNavItem>Notifications</ListNavItem>
19
+ <ListNavItem>Billing</ListNavItem>
20
+ </ListNav>
21
+ ),
22
+ }
@@ -0,0 +1,145 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Slot, Slottable } from "@radix-ui/react-slot"
5
+ import { cn } from "../lib/cn"
6
+
7
+ export type ListNavSize = "base" | "lg"
8
+
9
+ // Size is shared from ListNav to its items via context, so `<ListNav size="lg">`
10
+ // sets the density for the whole list; a single item can still override it.
11
+ interface ListNavContextValue {
12
+ size: ListNavSize
13
+ }
14
+ const ListNavContext = React.createContext<ListNavContextValue>({ size: "base" })
15
+
16
+ // Per-size item padding + type. `lg` widens the row into a comfortable ~44px
17
+ // tap target for touchscreens; `base` stays compact for dense desktop lists.
18
+ const ITEM_SIZE: Record<ListNavSize, string> = {
19
+ base: "ax:px-xs ax:py-2xs ax:t-label-sm",
20
+ lg: "ax:px-sm ax:py-sm ax:t-label-md",
21
+ }
22
+ // Leading icon (and the inset placeholder) column width per size.
23
+ const ICON_BOX: Record<ListNavSize, string> = {
24
+ base: "ax:size-4",
25
+ lg: "ax:size-5",
26
+ }
27
+
28
+ // ─── ListNav ──────────────────────────────────────────────────────────────────
29
+
30
+ export interface ListNavProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
31
+ /** Optional eyebrow-style title rendered above the list (uses `t-overline`). */
32
+ title?: React.ReactNode
33
+ /** Row density. `base` (default) is compact; `lg` adds padding for a larger,
34
+ * touch-friendly tap target. Cascades to all items via context. */
35
+ size?: ListNavSize
36
+ }
37
+
38
+ /**
39
+ * ListNav - vertical list of selectable items with an optional eyebrow title.
40
+ *
41
+ * Use for in-page section jumpers, dense filter lists, or any sidebar-adjacent
42
+ * nav where items don't need an icon column. For app-shell navigation with
43
+ * brand, collapse, and tooltips, reach for `Sidebar` instead.
44
+ *
45
+ * Compose multiple `ListNav`s vertically when grouping is needed: each owns
46
+ * its own title.
47
+ */
48
+ export const ListNav = React.forwardRef<HTMLElement, ListNavProps>(
49
+ ({ className, title, size = "base", children, ...props }, ref) => (
50
+ <ListNavContext.Provider value={{ size }}>
51
+ <nav
52
+ ref={ref}
53
+ className={cn("ax:flex ax:flex-col", className)}
54
+ {...props}
55
+ >
56
+ {title && (
57
+ // Title x-padding tracks the item padding so it stays flush with rows.
58
+ <p className={cn("ax:t-overline ax:text-foreground-strong ax:pt-sm ax:pb-sm", size === "lg" ? "ax:px-sm" : "ax:px-xs")}>
59
+ {title}
60
+ </p>
61
+ )}
62
+ <div className="ax:flex ax:flex-col">{children}</div>
63
+ </nav>
64
+ </ListNavContext.Provider>
65
+ )
66
+ )
67
+ ListNav.displayName = "ListNav"
68
+
69
+ // ─── ListNavItem ──────────────────────────────────────────────────────────────
70
+
71
+ export interface ListNavItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
72
+ active?: boolean
73
+ /** Optional leading icon. */
74
+ icon?: React.ReactNode
75
+ /**
76
+ * Reserves the leading icon column even when no icon is provided. Use when
77
+ * mixing items with and without icons in the same list, so labels stay
78
+ * aligned. Has no effect if `icon` is set.
79
+ */
80
+ inset?: boolean
81
+ /** Trailing slot - typically a Badge, count, or shortcut hint. */
82
+ trailing?: React.ReactNode
83
+ /**
84
+ * Render as the child element (e.g. a Next.js `<Link>`). The child receives
85
+ * the className, ref, and event props so it behaves as a list-nav row while
86
+ * keeping its own navigation behaviour.
87
+ */
88
+ asChild?: boolean
89
+ /** Overrides the row density inherited from `ListNav` for this item. */
90
+ size?: ListNavSize
91
+ }
92
+
93
+ export const ListNavItem = React.forwardRef<HTMLButtonElement, ListNavItemProps>(
94
+ ({ className, active, icon, inset, trailing, asChild, type, size: sizeProp, children, ...props }, ref) => {
95
+ const { size: ctxSize } = React.useContext(ListNavContext)
96
+ const size = sizeProp ?? ctxSize
97
+ const Comp = asChild ? Slot : "button"
98
+ // `type="button"` only makes sense on a real <button>; if `asChild` wraps
99
+ // an <a> or <Link>, applying it would emit invalid HTML.
100
+ const buttonOnly = asChild ? {} : { type: type ?? "button" }
101
+
102
+ return (
103
+ <Comp
104
+ ref={ref as React.Ref<HTMLButtonElement>}
105
+ aria-current={active ? "page" : undefined}
106
+ className={cn(
107
+ "ax:w-full ax:text-start ax:flex ax:items-center ax:gap-sm ax:rounded-(--input-radius) ax:cursor-pointer",
108
+ ITEM_SIZE[size],
109
+ "ax:transition-colors",
110
+ "ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-2 ax:focus-visible:ring-offset-background",
111
+ active
112
+ // `bg-primary-weakest` auto-inverts to a deep tint in dark; keep it
113
+ // translucent there (/25). `text-primary-foreground` flips near-white in
114
+ // dark (washed-out), so use a bright branded sage (`-strong`) instead.
115
+ ? "ax:bg-primary-muted ax:text-primary-foreground ax:dark:text-primary-strong ax:font-medium"
116
+ : "ax:text-muted-foreground ax:hover:text-foreground ax:hover:bg-muted",
117
+ className,
118
+ )}
119
+ {...buttonOnly}
120
+ {...props}
121
+ >
122
+ {icon ? (
123
+ <span className={cn("ax:shrink-0 ax:flex ax:items-center ax:justify-center ax:[&>svg]:size-full ax:[&>svg]:shrink-0", ICON_BOX[size])}>
124
+ {icon}
125
+ </span>
126
+ ) : inset ? (
127
+ // Empty placeholder occupying the icon column so labels stay aligned
128
+ // with sibling rows that DO have an icon.
129
+ <span aria-hidden="true" className={cn("ax:shrink-0", ICON_BOX[size])} />
130
+ ) : null}
131
+ {asChild ? (
132
+ // Slot needs exactly one slottable child: the consumer's element (e.g. an <a>)
133
+ // receives the icon and trailing siblings around its own content.
134
+ <Slottable>{children}</Slottable>
135
+ ) : (
136
+ // leading-snug (not the row's t-label-sm leading-none): `truncate` clips
137
+ // with overflow-hidden, and a 1.0 line-height would shave descenders.
138
+ <span className="ax:flex-1 ax:truncate ax:leading-snug">{children}</span>
139
+ )}
140
+ {trailing && <span className="ax:shrink-0 ax:flex ax:items-center">{trailing}</span>}
141
+ </Comp>
142
+ )
143
+ }
144
+ )
145
+ ListNavItem.displayName = "ListNavItem"
@@ -0,0 +1,23 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { LogoCloud } from "./logo-cloud"
4
+
5
+ const meta: Meta<typeof LogoCloud> = {
6
+ title: "Components/LogoCloud",
7
+ component: LogoCloud,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ eyebrow: "Trusted by teams at",
11
+ items: [
12
+ { name: "Acme", logo: <span className="ax:t-h5">Acme</span> },
13
+ { name: "Globex", logo: <span className="ax:t-h5">Globex</span> },
14
+ { name: "Initech", logo: <span className="ax:t-h5">Initech</span> },
15
+ { name: "Umbrella", logo: <span className="ax:t-h5">Umbrella</span> },
16
+ ],
17
+ },
18
+ }
19
+
20
+ export default meta
21
+ type Story = StoryObj<typeof LogoCloud>
22
+
23
+ export const Default: Story = {}
@@ -0,0 +1,42 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/cn"
3
+
4
+ export interface LogoCloudItem {
5
+ name: string
6
+ logo: React.ReactNode
7
+ }
8
+
9
+ export interface LogoCloudProps extends React.HTMLAttributes<HTMLElement> {
10
+ eyebrow?: React.ReactNode
11
+ items: LogoCloudItem[]
12
+ }
13
+
14
+ export function LogoCloud({ className, eyebrow, items, ...props }: LogoCloudProps) {
15
+ return (
16
+ <section className={cn("ax:w-full ax:px-xl ax:md:px-3xl ax:py-4xl ax:md:py-5xl", className)} {...props}>
17
+ <div className="ax:mx-auto ax:max-w-420">
18
+ {eyebrow && (
19
+ <p className="ax:text-center ax:t-overline ax:text-muted-foreground ax:mb-2xl">
20
+ {eyebrow}
21
+ </p>
22
+ )}
23
+ <div
24
+ className={cn(
25
+ "ax:grid ax:grid-cols-2 ax:gap-x-2xl ax:gap-y-xl ax:items-center",
26
+ "ax:sm:grid-cols-3 ax:md:grid-cols-4 ax:lg:grid-cols-6"
27
+ )}
28
+ >
29
+ {items.map((item) => (
30
+ <div
31
+ key={item.name}
32
+ title={item.name}
33
+ className="ax:flex ax:items-center ax:justify-center ax:opacity-50 ax:grayscale ax:hover:opacity-80 ax:hover:grayscale-0 ax:transition-[opacity,filter] ax:transition-smooth"
34
+ >
35
+ {item.logo}
36
+ </div>
37
+ ))}
38
+ </div>
39
+ </div>
40
+ </section>
41
+ )
42
+ }
@@ -0,0 +1,25 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { MediaGallery, type MediaItem } from "./media-gallery"
4
+
5
+ const media: MediaItem[] = [
6
+ { type: "image", src: "https://picsum.photos/800/600?random=1", label: "Property exterior" },
7
+ { type: "image", src: "https://picsum.photos/800/600?random=2", label: "Living room" },
8
+ { type: "image", src: "https://picsum.photos/800/600?random=3", label: "Kitchen" },
9
+ ]
10
+
11
+ const meta: Meta<typeof MediaGallery> = {
12
+ title: "Components/MediaGallery",
13
+ component: MediaGallery,
14
+ tags: ["autodocs"],
15
+ args: {
16
+ title: "Gallery",
17
+ media,
18
+ thumbnailCount: 3,
19
+ },
20
+ }
21
+
22
+ export default meta
23
+ type Story = StoryObj<typeof MediaGallery>
24
+
25
+ export const Default: Story = {}