@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,276 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
5
+ import { cn } from "../lib/cn"
6
+
7
+ // ─── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export type TabsSize = "sm" | "md" | "lg"
10
+ export type TabsOrientation = "horizontal" | "vertical"
11
+
12
+ // Sizing mirrors SegmentedControl + Button exactly so all three primitives can
13
+ // sit side-by-side in a toolbar without visual conflict. Heights:
14
+ // sm → 32px (h-2xl) md → 40px (h-3xl) lg → 48px (h-4xl)
15
+ const LIST_HEIGHT: Record<TabsSize, string> = {
16
+ sm: "ax:h-2xl",
17
+ md: "ax:h-3xl",
18
+ lg: "ax:h-4xl",
19
+ }
20
+ const TRIGGER_PADDING: Record<TabsSize, string> = {
21
+ sm: "ax:px-sm",
22
+ md: "ax:px-md",
23
+ lg: "ax:px-xl",
24
+ }
25
+ const TRIGGER_TEXT: Record<TabsSize, string> = {
26
+ sm: "ax:t-label-sm",
27
+ md: "ax:t-label-sm",
28
+ lg: "ax:t-label-lg",
29
+ }
30
+ // Vertical triggers own their height (no fixed list height), so they need
31
+ // their own vertical padding. Horizontal triggers get height from LIST_HEIGHT.
32
+ const TRIGGER_PADDING_Y: Record<TabsSize, string> = {
33
+ sm: "ax:py-xs", // 8px
34
+ md: "ax:py-sm", // 12px
35
+ lg: "ax:py-md", // 16px
36
+ }
37
+
38
+ // ─── Context ──────────────────────────────────────────────────────────────────
39
+
40
+ interface TabsContextValue {
41
+ size: TabsSize
42
+ orientation: TabsOrientation
43
+ currentValue: string
44
+ registerTrigger: (value: string, el: HTMLButtonElement | null) => void
45
+ triggerRefs: React.MutableRefObject<Map<string, HTMLButtonElement>>
46
+ }
47
+
48
+ const TabsContext = React.createContext<TabsContextValue>({
49
+ size: "md",
50
+ orientation: "horizontal",
51
+ currentValue: "",
52
+ registerTrigger: () => {},
53
+ triggerRefs: { current: new Map() },
54
+ })
55
+
56
+ // ─── Root ─────────────────────────────────────────────────────────────────────
57
+
58
+ export interface TabsProps
59
+ extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root> {
60
+ size?: TabsSize
61
+ /** `horizontal` (default) is a segmented row; `vertical` stacks the triggers
62
+ * in a left-aligned column (labels may wrap). */
63
+ orientation?: TabsOrientation
64
+ }
65
+
66
+ export const Tabs = React.forwardRef<
67
+ React.ElementRef<typeof TabsPrimitive.Root>,
68
+ TabsProps
69
+ >(({ size = "md", orientation = "horizontal", value, defaultValue, onValueChange, children, ...props }, ref) => {
70
+ // Trigger refs are kept in a Map so TabsList can measure the active one for
71
+ // the floating indicator. The Map identity is stable across renders.
72
+ const triggerRefs = React.useRef(new Map<string, HTMLButtonElement>())
73
+
74
+ // Mirror Radix's value internally so the indicator has something to point at
75
+ // even when the consumer hasn't provided value/onValueChange (uncontrolled).
76
+ const [internalValue, setInternalValue] = React.useState<string>(
77
+ value ?? defaultValue ?? ""
78
+ )
79
+ const currentValue = value ?? internalValue
80
+
81
+ const handleValueChange = React.useCallback(
82
+ (v: string) => {
83
+ setInternalValue(v)
84
+ onValueChange?.(v)
85
+ },
86
+ [onValueChange]
87
+ )
88
+
89
+ const registerTrigger = React.useCallback(
90
+ (val: string, el: HTMLButtonElement | null) => {
91
+ if (el) triggerRefs.current.set(val, el)
92
+ else triggerRefs.current.delete(val)
93
+ },
94
+ []
95
+ )
96
+
97
+ const ctx = React.useMemo<TabsContextValue>(
98
+ () => ({ size, orientation, currentValue, registerTrigger, triggerRefs }),
99
+ [size, orientation, currentValue, registerTrigger]
100
+ )
101
+
102
+ return (
103
+ <TabsContext.Provider value={ctx}>
104
+ <TabsPrimitive.Root
105
+ ref={ref}
106
+ orientation={orientation}
107
+ value={value}
108
+ defaultValue={defaultValue}
109
+ onValueChange={handleValueChange}
110
+ {...props}
111
+ >
112
+ {children}
113
+ </TabsPrimitive.Root>
114
+ </TabsContext.Provider>
115
+ )
116
+ })
117
+ Tabs.displayName = TabsPrimitive.Root.displayName
118
+
119
+ // ─── List ─────────────────────────────────────────────────────────────────────
120
+ //
121
+ // Hosts the floating indicator: a single absolute-positioned div that slides
122
+ // behind the active trigger. Position is measured via getBoundingClientRect on
123
+ // every value change and on parent resize (ResizeObserver). The transition is
124
+ // suppressed on the very first measurement so the indicator doesn't slide in
125
+ // from 0,0 on mount.
126
+
127
+ export const TabsList = React.forwardRef<
128
+ React.ElementRef<typeof TabsPrimitive.List>,
129
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
130
+ >(({ className, children, ...props }, ref) => {
131
+ const { size, orientation, currentValue, triggerRefs } = React.useContext(TabsContext)
132
+
133
+ const rootRef = React.useRef<HTMLDivElement>(null)
134
+ const positioned = React.useRef(false)
135
+ const [indicatorStyle, setIndicatorStyle] = React.useState<React.CSSProperties>({ opacity: 0 })
136
+
137
+ const updateIndicator = React.useCallback((val: string) => {
138
+ const el = triggerRefs.current.get(val)
139
+ const root = rootRef.current
140
+ if (!el || !root) return
141
+
142
+ const rootRect = root.getBoundingClientRect()
143
+ const itemRect = el.getBoundingClientRect()
144
+
145
+ setIndicatorStyle({
146
+ left: itemRect.left - rootRect.left,
147
+ width: itemRect.width,
148
+ top: itemRect.top - rootRect.top,
149
+ height: itemRect.height,
150
+ opacity: 1,
151
+ transition: positioned.current
152
+ ? "left 150ms ease, width 150ms ease, top 150ms ease, height 150ms ease"
153
+ : undefined,
154
+ })
155
+ positioned.current = true
156
+ }, [triggerRefs])
157
+
158
+ React.useLayoutEffect(() => {
159
+ updateIndicator(currentValue)
160
+ }, [currentValue, updateIndicator])
161
+
162
+ React.useEffect(() => {
163
+ const root = rootRef.current
164
+ if (!root) return
165
+ const observer = new ResizeObserver(() => updateIndicator(currentValue))
166
+ observer.observe(root)
167
+ return () => observer.disconnect()
168
+ }, [currentValue, updateIndicator])
169
+
170
+ // Merge the forwarded ref with the local root ref so consumers can still
171
+ // attach their own ref while we measure the same node.
172
+ const mergedRef = React.useCallback(
173
+ (node: HTMLDivElement | null) => {
174
+ (rootRef as React.MutableRefObject<HTMLDivElement | null>).current = node
175
+ if (typeof ref === "function") ref(node)
176
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
177
+ },
178
+ [ref]
179
+ )
180
+
181
+ return (
182
+ <TabsPrimitive.List
183
+ ref={mergedRef}
184
+ className={cn(
185
+ "ax:relative ax:items-stretch ax:bg-muted ax:rounded-(--input-radius) ax:gap-px",
186
+ orientation === "vertical"
187
+ // Stacks full-width in a column; height is content-driven. Set a width
188
+ // on this element (or its container) to let long labels wrap.
189
+ ? "ax:flex ax:flex-col ax:w-full"
190
+ : cn("ax:inline-flex", LIST_HEIGHT[size]),
191
+ className
192
+ )}
193
+ {...props}
194
+ >
195
+ {/* Floating indicator - slides behind the active trigger */}
196
+ <div
197
+ aria-hidden="true"
198
+ // Dark mode: a raised neutral-stronger thumb, lighter than the `bg-muted`
199
+ // track, so the active tab pops off the track (mirrors the light-mode
200
+ // white-on-grey lift). Matches SegmentedControl. Light stays --card.
201
+ className="ax:absolute ax:bg-card ax:dark:bg-neutral-weaker ax:shadow-sm ax:rounded-(--input-radius) ax:pointer-events-none"
202
+ style={indicatorStyle}
203
+ />
204
+ {children}
205
+ </TabsPrimitive.List>
206
+ )
207
+ })
208
+ TabsList.displayName = TabsPrimitive.List.displayName
209
+
210
+ // ─── Trigger ──────────────────────────────────────────────────────────────────
211
+
212
+ export const TabsTrigger = React.forwardRef<
213
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
214
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
215
+ >(({ className, value, children, ...props }, ref) => {
216
+ const { size, orientation, currentValue, registerTrigger } = React.useContext(TabsContext)
217
+ const isActive = currentValue === value
218
+ const localRef = React.useRef<HTMLButtonElement>(null)
219
+
220
+ React.useImperativeHandle(ref, () => localRef.current!)
221
+
222
+ React.useLayoutEffect(() => {
223
+ registerTrigger(value, localRef.current)
224
+ return () => registerTrigger(value, null)
225
+ }, [value, registerTrigger])
226
+
227
+ return (
228
+ <TabsPrimitive.Trigger
229
+ ref={localRef}
230
+ value={value}
231
+ className={cn(
232
+ "ax:relative ax:z-10 ax:flex ax:items-center ax:cursor-pointer ax:select-none",
233
+ "ax:rounded-(--input-radius) ax:leading-snug",
234
+ "ax:transition-colors ax:transition-snappy",
235
+ "ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-1 ax:focus-visible:ring-offset-muted",
236
+ "ax:disabled:cursor-not-allowed ax:disabled:opacity-50",
237
+ TRIGGER_PADDING[size],
238
+ TRIGGER_TEXT[size],
239
+ // Horizontal: centered single line. Vertical: full-width, start-aligned,
240
+ // owns its vertical padding and may wrap (hence leading-snug above).
241
+ orientation === "vertical"
242
+ ? cn("ax:w-full ax:justify-start ax:text-start", TRIGGER_PADDING_Y[size])
243
+ : "ax:justify-center ax:whitespace-nowrap",
244
+ isActive ? "ax:text-primary-foreground" : "ax:text-muted-foreground ax:hover:text-foreground",
245
+ className
246
+ )}
247
+ {...props}
248
+ >
249
+ {children}
250
+ </TabsPrimitive.Trigger>
251
+ )
252
+ })
253
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
254
+
255
+ // ─── Content ──────────────────────────────────────────────────────────────────
256
+
257
+ export const TabsContent = React.forwardRef<
258
+ React.ElementRef<typeof TabsPrimitive.Content>,
259
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
260
+ >(({ className, ...props }, ref) => {
261
+ const { orientation } = React.useContext(TabsContext)
262
+ return (
263
+ <TabsPrimitive.Content
264
+ ref={ref}
265
+ className={cn(
266
+ // Horizontal stacks content under the list; vertical places it beside
267
+ // the list, so spacing is left to the consumer's row layout.
268
+ orientation === "vertical" ? "" : "ax:mt-4",
269
+ "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",
270
+ className
271
+ )}
272
+ {...props}
273
+ />
274
+ )
275
+ })
276
+ TabsContent.displayName = TabsPrimitive.Content.displayName
@@ -0,0 +1,22 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Testimonials } from "./testimonials"
4
+
5
+ const meta: Meta<typeof Testimonials> = {
6
+ title: "Components/Testimonials",
7
+ component: Testimonials,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ eyebrow: "Testimonials",
11
+ title: "Loved by teams everywhere",
12
+ items: [
13
+ { quote: "Adres helped us ship a consistent UI across three brands.", name: "Sarah Chen", role: "Design Lead", company: "Madhmoun", rating: 5 },
14
+ { quote: "The component library saved us months of work.", name: "Omar Ali", role: "Engineering Manager", company: "ThinkProp", rating: 5 },
15
+ ],
16
+ },
17
+ }
18
+
19
+ export default meta
20
+ type Story = StoryObj<typeof Testimonials>
21
+
22
+ export const Default: Story = {}
@@ -0,0 +1,109 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Star } from "lucide-react"
5
+ import { motion } from "motion/react"
6
+ import { cn } from "../lib/cn"
7
+ import { cardCn } from "./card"
8
+ import { useReducedMotion } from "../hooks/use-reduced-motion"
9
+ import { stagger, variants, viewport as defaultViewport } from "../lib/motion"
10
+
11
+ export interface Testimonial {
12
+ quote: string
13
+ name: string
14
+ role?: string
15
+ company?: string
16
+ avatar?: string
17
+ rating?: number
18
+ }
19
+
20
+ export interface TestimonialsProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
21
+ eyebrow?: React.ReactNode
22
+ title?: React.ReactNode
23
+ items: Testimonial[]
24
+ columns?: 2 | 3
25
+ }
26
+
27
+ export function Testimonials({ className, eyebrow, title, items, columns = 3, ...props }: TestimonialsProps) {
28
+ const reduce = useReducedMotion()
29
+
30
+ return (
31
+ <section className={cn("ax:w-full ax:px-xl ax:md:px-3xl ax:py-5xl ax:md:py-6xl", className)} {...props}>
32
+ <div className="ax:mx-auto ax:max-w-7xl">
33
+ {(eyebrow || title) && (
34
+ <div className="ax:max-w-2xl ax:mb-4xl ax:space-y-sm">
35
+ {eyebrow && (
36
+ <span className="ax:inline-block ax:t-overline ax:text-primary">
37
+ {eyebrow}
38
+ </span>
39
+ )}
40
+ {title && (
41
+ <h2 className="ax:t-display-md ax:md:t-display-lg ax:text-foreground ax:[text-wrap:balance]">
42
+ {title}
43
+ </h2>
44
+ )}
45
+ </div>
46
+ )}
47
+
48
+ <motion.div
49
+ className={cn(
50
+ "ax:grid ax:gap-xl",
51
+ columns === 2 ? "ax:md:grid-cols-2" : "ax:md:grid-cols-2 ax:lg:grid-cols-3"
52
+ )}
53
+ {...(reduce ? {} : {
54
+ variants: stagger(0, 0.07),
55
+ initial: "hidden",
56
+ whileInView: "visible",
57
+ viewport: defaultViewport,
58
+ })}
59
+ >
60
+ {items.map((item, i) => (
61
+ <motion.figure
62
+ key={i}
63
+ {...(reduce ? {} : { variants: variants.fadeUp })}
64
+ className={cardCn("default", "xl", "ax:flex ax:flex-col ax:gap-md")}
65
+ >
66
+ {item.rating && (
67
+ <div className="ax:flex ax:gap-0.5">
68
+ {Array.from({ length: 5 }).map((_, j) => (
69
+ <Star
70
+ key={j}
71
+ className={cn(
72
+ "ax:size-4",
73
+ j < item.rating! ? "ax:fill-warning ax:text-warning-strong" : "ax:fill-muted ax:text-muted"
74
+ )}
75
+ />
76
+ ))}
77
+ </div>
78
+ )}
79
+ <blockquote className="ax:flex-1 ax:t-body-sm ax:text-foreground">
80
+ "{item.quote}"
81
+ </blockquote>
82
+ <figcaption className="ax:flex ax:items-center ax:gap-sm">
83
+ {item.avatar ? (
84
+ <img
85
+ src={item.avatar}
86
+ alt={item.name}
87
+ className="ax:size-9 ax:rounded-full ax:object-cover ax:bg-muted"
88
+ />
89
+ ) : (
90
+ <div className="ax:size-9 ax:rounded-full ax:bg-primary/10 ax:text-primary ax:flex ax:items-center ax:justify-center ax:t-label-sm">
91
+ {item.name.charAt(0)}
92
+ </div>
93
+ )}
94
+ <div>
95
+ <div className="ax:t-label-sm ax:text-foreground">{item.name}</div>
96
+ {(item.role || item.company) && (
97
+ <div className="ax:t-caption ax:text-muted-foreground">
98
+ {[item.role, item.company].filter(Boolean).join(" · ")}
99
+ </div>
100
+ )}
101
+ </div>
102
+ </figcaption>
103
+ </motion.figure>
104
+ ))}
105
+ </motion.div>
106
+ </div>
107
+ </section>
108
+ )
109
+ }
@@ -0,0 +1,47 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { TextLink } from "./text-link"
4
+ import { Text } from "./text"
5
+
6
+ const meta: Meta<typeof TextLink> = {
7
+ title: "Components/TextLink",
8
+ component: TextLink,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ color: { control: "select", options: ["primary", "default", "muted"] },
12
+ underline: { control: "select", options: ["hover", "always", "none"] },
13
+ weight: { control: "select", options: ["regular", "medium"] },
14
+ },
15
+ args: {
16
+ href: "#",
17
+ children: "Forgot password?",
18
+ },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof TextLink>
23
+
24
+ export const Default: Story = {}
25
+
26
+ /** Inherits the surrounding size when `size` is omitted, so it never shifts the line. */
27
+ export const InSentence: Story = {
28
+ render: (args) => (
29
+ <Text as="p" size="2" color="muted">
30
+ Don&apos;t have an account? <TextLink {...args}>Create one</TextLink>
31
+ </Text>
32
+ ),
33
+ }
34
+
35
+ export const Colors: Story = {
36
+ render: () => (
37
+ <div style={{ display: "flex", gap: "1.5rem" }}>
38
+ <TextLink href="#">Primary</TextLink>
39
+ <TextLink href="#" color="default">Default</TextLink>
40
+ <TextLink href="#" color="muted">Muted</TextLink>
41
+ </div>
42
+ ),
43
+ }
44
+
45
+ export const External: Story = {
46
+ args: { href: "https://example.com", external: true, underline: "always", children: "Read the privacy policy" },
47
+ }
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { TextLink } from "./text-link"
5
+
6
+ describe("TextLink", () => {
7
+ it("adds safe new-tab attributes and a screen-reader hint when external", () => {
8
+ const html = renderToStaticMarkup(<TextLink href="https://example.com" external>Docs</TextLink>)
9
+ expect(html).toContain('target="_blank"')
10
+ expect(html).toContain('rel="noopener noreferrer"')
11
+ expect(html).toContain("(opens in a new tab)")
12
+ })
13
+
14
+ it("inherits the surrounding text size unless size is set", () => {
15
+ expect(renderToStaticMarkup(<TextLink href="#">Link</TextLink>)).not.toContain("ax:t-body")
16
+ expect(renderToStaticMarkup(<TextLink href="#" size="2">Link</TextLink>)).toContain("ax:t-body-sm")
17
+ })
18
+ })
@@ -0,0 +1,88 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Slot } from "@radix-ui/react-slot"
5
+ import { tv, type VariantProps } from "tailwind-variants"
6
+ import { cn } from "../lib/cn"
7
+
8
+ export type TextLinkColor = "primary" | "default" | "muted"
9
+ export type TextLinkUnderline = "hover" | "always" | "none"
10
+ export type TextLinkSize = "1" | "2" | "3" | "4" | "5"
11
+
12
+ /**
13
+ * Inline text link. Unlike Button's `link` variant it carries no height or
14
+ * padding, so it sits inside a sentence without shifting the line. `size`
15
+ * shares Text's body scale and is omitted by default so the link inherits the
16
+ * surrounding text size.
17
+ */
18
+ export const textLinkVariants = tv({
19
+ base: [
20
+ "ax:cursor-pointer ax:rounded-xs ax:underline-offset-4 ax:decoration-1",
21
+ "ax:transition-colors ax:transition-snappy",
22
+ "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",
23
+ ],
24
+ variants: {
25
+ color: {
26
+ primary: "ax:text-primary-foreground",
27
+ default: "ax:text-foreground",
28
+ muted: "ax:text-muted-foreground ax:hover:text-foreground",
29
+ },
30
+ underline: {
31
+ hover: "ax:no-underline ax:hover:underline",
32
+ always: "ax:underline",
33
+ none: "ax:no-underline",
34
+ },
35
+ size: {
36
+ "1": "ax:t-body-xs",
37
+ "2": "ax:t-body-sm",
38
+ "3": "ax:t-body-md",
39
+ "4": "ax:t-body-lg",
40
+ "5": "ax:t-body-xl",
41
+ },
42
+ weight: {
43
+ regular: "ax:font-normal",
44
+ medium: "ax:font-medium",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ color: "primary",
49
+ underline: "hover",
50
+ weight: "medium",
51
+ },
52
+ }, { twMerge: false })
53
+
54
+ export interface TextLinkProps
55
+ extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "color">,
56
+ VariantProps<typeof textLinkVariants> {
57
+ /** Render the child instead of an `<a>`, e.g. a framework router link. */
58
+ asChild?: boolean
59
+ /** Opens in a new tab with `rel="noopener noreferrer"` and announces that to screen readers. */
60
+ external?: boolean
61
+ }
62
+
63
+ export const TextLink = React.forwardRef<HTMLAnchorElement, TextLinkProps>(
64
+ ({ className, color, underline, size, weight, asChild = false, external = false, children, ...props }, ref) => {
65
+ const Comp = asChild ? Slot : "a"
66
+ const externalProps = external ? { target: "_blank", rel: "noopener noreferrer" } : {}
67
+ return (
68
+ <Comp
69
+ ref={ref}
70
+ data-slot="text-link"
71
+ className={cn(textLinkVariants({ color, underline, size, weight }), className)}
72
+ {...externalProps}
73
+ {...props}
74
+ >
75
+ {/* Slot needs a single child to merge into, so the external hint only renders on a plain <a>. */}
76
+ {external && !asChild ? (
77
+ <>
78
+ {children}
79
+ <span className="ax:sr-only"> (opens in a new tab)</span>
80
+ </>
81
+ ) : (
82
+ children
83
+ )}
84
+ </Comp>
85
+ )
86
+ },
87
+ )
88
+ TextLink.displayName = "TextLink"
@@ -0,0 +1,49 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Text, type TextSize } from "./text"
4
+
5
+ const meta: Meta<typeof Text> = {
6
+ title: "Components/Text",
7
+ component: Text,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ children: "The quick brown fox jumps over the lazy dog.",
11
+ size: "3",
12
+ },
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof Text>
17
+
18
+ export const Default: Story = {}
19
+
20
+ export const Muted: Story = {
21
+ args: { color: "muted", size: "2" },
22
+ }
23
+
24
+ const SIZE_TO_T_CLASS: Record<TextSize, string> = {
25
+ "1": "ax:t-body-xs",
26
+ "2": "ax:t-body-sm",
27
+ "3": "ax:t-body-md",
28
+ "4": "ax:t-body-lg",
29
+ "5": "ax:t-body-xl",
30
+ "6": "ax:t-h1",
31
+ "7": "ax:t-display-sm",
32
+ "8": "ax:t-display-md",
33
+ "9": "ax:t-display-xl",
34
+ }
35
+
36
+ export const Scale: Story = {
37
+ render: () => (
38
+ <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
39
+ {Object.entries(SIZE_TO_T_CLASS).map(([size, tClass]) => (
40
+ <div key={size} style={{ display: "flex", alignItems: "baseline", gap: "1rem" }}>
41
+ <code style={{ minWidth: "8rem" }}>
42
+ size=&quot;{size}&quot; -&gt; {tClass}
43
+ </code>
44
+ <Text size={size as TextSize}>The quick brown fox jumps over the lazy dog.</Text>
45
+ </div>
46
+ ))}
47
+ </div>
48
+ ),
49
+ }