@adres/mgda-ui 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/brand-knowledge.json +451 -0
  2. package/knowledge/accordion.md +135 -0
  3. package/knowledge/activity-rings.md +123 -0
  4. package/knowledge/alert.md +184 -0
  5. package/knowledge/answer-actions.md +217 -0
  6. package/knowledge/aspect-ratio.md +82 -0
  7. package/knowledge/avatar.md +158 -0
  8. package/knowledge/badge.md +172 -0
  9. package/knowledge/bar-graph.md +167 -0
  10. package/knowledge/bento-grid.md +115 -0
  11. package/knowledge/blockquote.md +153 -0
  12. package/knowledge/box-list.md +177 -0
  13. package/knowledge/bullet-list.md +128 -0
  14. package/knowledge/button-group.md +179 -0
  15. package/knowledge/button.md +231 -0
  16. package/knowledge/calendar.md +168 -0
  17. package/knowledge/card.md +321 -0
  18. package/knowledge/cards-nav.md +170 -0
  19. package/knowledge/chain-of-thought.md +164 -0
  20. package/knowledge/chat-container.md +135 -0
  21. package/knowledge/chat-message.md +224 -0
  22. package/knowledge/checkbox-card.md +172 -0
  23. package/knowledge/checkbox.md +111 -0
  24. package/knowledge/citations.md +241 -0
  25. package/knowledge/combobox.md +265 -0
  26. package/knowledge/countdown-pill.md +153 -0
  27. package/knowledge/cta.md +118 -0
  28. package/knowledge/data-list.md +173 -0
  29. package/knowledge/data-table.md +220 -0
  30. package/knowledge/dialog.md +249 -0
  31. package/knowledge/drawer.md +236 -0
  32. package/knowledge/dropdown-menu.md +238 -0
  33. package/knowledge/dropzone.md +153 -0
  34. package/knowledge/email-chips-input.md +161 -0
  35. package/knowledge/empty-state.md +106 -0
  36. package/knowledge/entity-header.md +166 -0
  37. package/knowledge/feature-grid.md +120 -0
  38. package/knowledge/feedback-bar.md +127 -0
  39. package/knowledge/footer.md +116 -0
  40. package/knowledge/heading.md +189 -0
  41. package/knowledge/hero.md +361 -0
  42. package/knowledge/input-button.md +187 -0
  43. package/knowledge/input-group.md +185 -0
  44. package/knowledge/input.md +256 -0
  45. package/knowledge/inset.md +177 -0
  46. package/knowledge/kpi-card.md +206 -0
  47. package/knowledge/list-nav.md +181 -0
  48. package/knowledge/logo-cloud.md +102 -0
  49. package/knowledge/media-gallery.md +158 -0
  50. package/knowledge/message-actions.md +175 -0
  51. package/knowledge/nav-sidebar.md +225 -0
  52. package/knowledge/number-stepper.md +159 -0
  53. package/knowledge/page-header.md +109 -0
  54. package/knowledge/pagination.md +225 -0
  55. package/knowledge/popover.md +159 -0
  56. package/knowledge/pricing.md +132 -0
  57. package/knowledge/progress.md +156 -0
  58. package/knowledge/prompt-input.md +337 -0
  59. package/knowledge/prompt-suggestions.md +168 -0
  60. package/knowledge/quota-bar.md +160 -0
  61. package/knowledge/quote.md +116 -0
  62. package/knowledge/radio-card.md +172 -0
  63. package/knowledge/radio-group.md +135 -0
  64. package/knowledge/reasoning.md +142 -0
  65. package/knowledge/search.md +131 -0
  66. package/knowledge/section-nav.md +162 -0
  67. package/knowledge/segmented-control.md +181 -0
  68. package/knowledge/select-button.md +205 -0
  69. package/knowledge/select.md +210 -0
  70. package/knowledge/separator.md +75 -0
  71. package/knowledge/shimmer-text.md +140 -0
  72. package/knowledge/sidebar.md +227 -0
  73. package/knowledge/skeleton.md +142 -0
  74. package/knowledge/slider.md +133 -0
  75. package/knowledge/slideshow.md +159 -0
  76. package/knowledge/source.md +156 -0
  77. package/knowledge/spinner.md +120 -0
  78. package/knowledge/stat-strip.md +128 -0
  79. package/knowledge/stats.md +117 -0
  80. package/knowledge/step-indicator.md +157 -0
  81. package/knowledge/switch.md +88 -0
  82. package/knowledge/table.md +190 -0
  83. package/knowledge/tabs.md +169 -0
  84. package/knowledge/testimonials.md +115 -0
  85. package/knowledge/text-link.md +192 -0
  86. package/knowledge/text.md +194 -0
  87. package/knowledge/thinking-bar.md +115 -0
  88. package/knowledge/thinking-steps.md +140 -0
  89. package/knowledge/toaster.md +127 -0
  90. package/knowledge/toggle.md +113 -0
  91. package/knowledge/tool-call-card.md +241 -0
  92. package/knowledge/tooltip.md +120 -0
  93. package/knowledge/top-nav.md +138 -0
  94. package/knowledge.json +19854 -0
  95. package/package.json +94 -4
  96. package/src/components/accordion.stories.tsx +46 -0
  97. package/src/components/accordion.tsx +74 -0
  98. package/src/components/activity-rings.stories.tsx +73 -0
  99. package/src/components/activity-rings.tsx +121 -0
  100. package/src/components/alert.stories.tsx +43 -0
  101. package/src/components/alert.tsx +132 -0
  102. package/src/components/answer-actions.stories.tsx +54 -0
  103. package/src/components/answer-actions.test.tsx +37 -0
  104. package/src/components/answer-actions.tsx +313 -0
  105. package/src/components/aspect-ratio.stories.tsx +32 -0
  106. package/src/components/aspect-ratio.tsx +5 -0
  107. package/src/components/avatar.stories.tsx +40 -0
  108. package/src/components/avatar.tsx +154 -0
  109. package/src/components/badge.stories.tsx +27 -0
  110. package/src/components/badge.tsx +63 -0
  111. package/src/components/bar-graph.stories.tsx +28 -0
  112. package/src/components/bar-graph.tsx +204 -0
  113. package/src/components/bento-grid.stories.tsx +23 -0
  114. package/src/components/bento-grid.tsx +67 -0
  115. package/src/components/blockquote.stories.tsx +28 -0
  116. package/src/components/blockquote.tsx +88 -0
  117. package/src/components/box-list.stories.tsx +25 -0
  118. package/src/components/box-list.tsx +108 -0
  119. package/src/components/bullet-list.stories.tsx +22 -0
  120. package/src/components/bullet-list.tsx +101 -0
  121. package/src/components/button-group.stories.tsx +31 -0
  122. package/src/components/button-group.tsx +156 -0
  123. package/src/components/button.stories.tsx +66 -0
  124. package/src/components/button.test.ts +17 -0
  125. package/src/components/button.tsx +139 -0
  126. package/src/components/calendar.stories.tsx +25 -0
  127. package/src/components/calendar.tsx +367 -0
  128. package/src/components/card.stories.tsx +44 -0
  129. package/src/components/card.tsx +392 -0
  130. package/src/components/cards-nav.stories.tsx +35 -0
  131. package/src/components/cards-nav.tsx +115 -0
  132. package/src/components/chain-of-thought.stories.tsx +67 -0
  133. package/src/components/chain-of-thought.test.tsx +50 -0
  134. package/src/components/chain-of-thought.tsx +170 -0
  135. package/src/components/chat-container.stories.tsx +47 -0
  136. package/src/components/chat-container.test.tsx +49 -0
  137. package/src/components/chat-container.tsx +222 -0
  138. package/src/components/chat-message.stories.tsx +52 -0
  139. package/src/components/chat-message.test.tsx +36 -0
  140. package/src/components/chat-message.tsx +163 -0
  141. package/src/components/checkbox-card.stories.tsx +27 -0
  142. package/src/components/checkbox-card.tsx +200 -0
  143. package/src/components/checkbox.stories.tsx +26 -0
  144. package/src/components/checkbox.tsx +61 -0
  145. package/src/components/citations.stories.tsx +64 -0
  146. package/src/components/citations.test.tsx +61 -0
  147. package/src/components/citations.tsx +220 -0
  148. package/src/components/combobox.stories.tsx +184 -0
  149. package/src/components/combobox.test.tsx +82 -0
  150. package/src/components/combobox.tsx +407 -0
  151. package/src/components/countdown-pill.stories.tsx +24 -0
  152. package/src/components/countdown-pill.tsx +129 -0
  153. package/src/components/cta.stories.tsx +29 -0
  154. package/src/components/cta.tsx +52 -0
  155. package/src/components/data-list.stories.tsx +50 -0
  156. package/src/components/data-list.test.tsx +35 -0
  157. package/src/components/data-list.tsx +181 -0
  158. package/src/components/data-table.stories.tsx +29 -0
  159. package/src/components/data-table.tsx +200 -0
  160. package/src/components/dialog.stories.tsx +42 -0
  161. package/src/components/dialog.tsx +200 -0
  162. package/src/components/drawer.stories.tsx +48 -0
  163. package/src/components/drawer.tsx +385 -0
  164. package/src/components/dropdown-menu.stories.tsx +41 -0
  165. package/src/components/dropdown-menu.tsx +252 -0
  166. package/src/components/dropzone.stories.tsx +19 -0
  167. package/src/components/dropzone.tsx +250 -0
  168. package/src/components/email-chips-input.stories.tsx +135 -0
  169. package/src/components/email-chips-input.tsx +210 -0
  170. package/src/components/empty-state.stories.tsx +22 -0
  171. package/src/components/empty-state.tsx +35 -0
  172. package/src/components/entity-header.stories.tsx +43 -0
  173. package/src/components/entity-header.test.tsx +35 -0
  174. package/src/components/entity-header.tsx +92 -0
  175. package/src/components/feature-grid.stories.tsx +25 -0
  176. package/src/components/feature-grid.tsx +87 -0
  177. package/src/components/feedback-bar.stories.tsx +25 -0
  178. package/src/components/feedback-bar.test.tsx +24 -0
  179. package/src/components/feedback-bar.tsx +62 -0
  180. package/src/components/footer.stories.tsx +23 -0
  181. package/src/components/footer.tsx +78 -0
  182. package/src/components/heading.stories.tsx +45 -0
  183. package/src/components/heading.test.tsx +22 -0
  184. package/src/components/heading.tsx +77 -0
  185. package/src/components/hero.stories.tsx +38 -0
  186. package/src/components/hero.tsx +498 -0
  187. package/src/components/input-button.stories.tsx +19 -0
  188. package/src/components/input-button.tsx +112 -0
  189. package/src/components/input-group.stories.tsx +38 -0
  190. package/src/components/input-group.tsx +157 -0
  191. package/src/components/input.stories.tsx +31 -0
  192. package/src/components/input.tsx +284 -0
  193. package/src/components/inset.stories.tsx +29 -0
  194. package/src/components/inset.tsx +70 -0
  195. package/src/components/kpi-card.stories.tsx +29 -0
  196. package/src/components/kpi-card.tsx +284 -0
  197. package/src/components/list-nav.stories.tsx +22 -0
  198. package/src/components/list-nav.tsx +145 -0
  199. package/src/components/logo-cloud.stories.tsx +23 -0
  200. package/src/components/logo-cloud.tsx +42 -0
  201. package/src/components/media-gallery.stories.tsx +25 -0
  202. package/src/components/media-gallery.tsx +347 -0
  203. package/src/components/message-actions.stories.tsx +45 -0
  204. package/src/components/message-actions.test.tsx +32 -0
  205. package/src/components/message-actions.tsx +119 -0
  206. package/src/components/nav-sidebar.stories.tsx +58 -0
  207. package/src/components/nav-sidebar.test.tsx +54 -0
  208. package/src/components/nav-sidebar.tsx +462 -0
  209. package/src/components/number-stepper.stories.tsx +19 -0
  210. package/src/components/number-stepper.tsx +158 -0
  211. package/src/components/page-header.stories.tsx +25 -0
  212. package/src/components/page-header.tsx +64 -0
  213. package/src/components/pagination.stories.tsx +110 -0
  214. package/src/components/pagination.test.tsx +46 -0
  215. package/src/components/pagination.tsx +144 -0
  216. package/src/components/popover.stories.tsx +26 -0
  217. package/src/components/popover.tsx +62 -0
  218. package/src/components/pricing.stories.tsx +42 -0
  219. package/src/components/pricing.tsx +130 -0
  220. package/src/components/progress.stories.tsx +22 -0
  221. package/src/components/progress.tsx +189 -0
  222. package/src/components/prompt-input.stories.tsx +62 -0
  223. package/src/components/prompt-input.test.tsx +106 -0
  224. package/src/components/prompt-input.tsx +343 -0
  225. package/src/components/prompt-suggestions.stories.tsx +53 -0
  226. package/src/components/prompt-suggestions.test.tsx +37 -0
  227. package/src/components/prompt-suggestions.tsx +122 -0
  228. package/src/components/quota-bar.stories.tsx +19 -0
  229. package/src/components/quota-bar.tsx +191 -0
  230. package/src/components/quote.stories.tsx +18 -0
  231. package/src/components/quote.tsx +43 -0
  232. package/src/components/radio-card.stories.tsx +27 -0
  233. package/src/components/radio-card.tsx +225 -0
  234. package/src/components/radio-group.stories.tsx +32 -0
  235. package/src/components/radio-group.tsx +68 -0
  236. package/src/components/reasoning.stories.tsx +50 -0
  237. package/src/components/reasoning.test.tsx +48 -0
  238. package/src/components/reasoning.tsx +143 -0
  239. package/src/components/search.stories.tsx +18 -0
  240. package/src/components/search.tsx +133 -0
  241. package/src/components/section-nav.stories.tsx +59 -0
  242. package/src/components/section-nav.test.tsx +36 -0
  243. package/src/components/section-nav.tsx +146 -0
  244. package/src/components/segmented-control.stories.tsx +22 -0
  245. package/src/components/segmented-control.tsx +301 -0
  246. package/src/components/select-button.stories.tsx +27 -0
  247. package/src/components/select-button.tsx +174 -0
  248. package/src/components/select.stories.tsx +27 -0
  249. package/src/components/select.tsx +198 -0
  250. package/src/components/separator.stories.tsx +22 -0
  251. package/src/components/separator.tsx +23 -0
  252. package/src/components/shimmer-text.stories.tsx +23 -0
  253. package/src/components/shimmer-text.test.tsx +35 -0
  254. package/src/components/shimmer-text.tsx +43 -0
  255. package/src/components/sidebar.stories.tsx +25 -0
  256. package/src/components/sidebar.tsx +234 -0
  257. package/src/components/skeleton.stories.tsx +22 -0
  258. package/src/components/skeleton.tsx +96 -0
  259. package/src/components/slider.stories.tsx +20 -0
  260. package/src/components/slider.tsx +176 -0
  261. package/src/components/slideshow.stories.tsx +30 -0
  262. package/src/components/slideshow.tsx +291 -0
  263. package/src/components/source.stories.tsx +32 -0
  264. package/src/components/source.test.tsx +40 -0
  265. package/src/components/source.tsx +161 -0
  266. package/src/components/spinner.stories.tsx +19 -0
  267. package/src/components/spinner.tsx +76 -0
  268. package/src/components/stat-strip.stories.tsx +36 -0
  269. package/src/components/stat-strip.test.tsx +25 -0
  270. package/src/components/stat-strip.tsx +85 -0
  271. package/src/components/stats.stories.tsx +24 -0
  272. package/src/components/stats.tsx +83 -0
  273. package/src/components/step-indicator.stories.tsx +18 -0
  274. package/src/components/step-indicator.test.tsx +18 -0
  275. package/src/components/step-indicator.tsx +286 -0
  276. package/src/components/switch.stories.tsx +26 -0
  277. package/src/components/switch.tsx +32 -0
  278. package/src/components/table.stories.tsx +38 -0
  279. package/src/components/table.tsx +220 -0
  280. package/src/components/tabs.stories.tsx +25 -0
  281. package/src/components/tabs.tsx +276 -0
  282. package/src/components/testimonials.stories.tsx +22 -0
  283. package/src/components/testimonials.tsx +109 -0
  284. package/src/components/text-link.stories.tsx +47 -0
  285. package/src/components/text-link.test.tsx +18 -0
  286. package/src/components/text-link.tsx +88 -0
  287. package/src/components/text.stories.tsx +49 -0
  288. package/src/components/text.tsx +105 -0
  289. package/src/components/thinking-bar.stories.tsx +19 -0
  290. package/src/components/thinking-bar.test.tsx +27 -0
  291. package/src/components/thinking-bar.tsx +64 -0
  292. package/src/components/thinking-steps.stories.tsx +44 -0
  293. package/src/components/thinking-steps.test.tsx +42 -0
  294. package/src/components/thinking-steps.tsx +92 -0
  295. package/src/components/toaster.stories.tsx +80 -0
  296. package/src/components/toaster.tsx +36 -0
  297. package/src/components/toggle.stories.tsx +21 -0
  298. package/src/components/toggle.tsx +103 -0
  299. package/src/components/tool-call-card.stories.tsx +97 -0
  300. package/src/components/tool-call-card.test.tsx +88 -0
  301. package/src/components/tool-call-card.tsx +231 -0
  302. package/src/components/tooltip.stories.tsx +25 -0
  303. package/src/components/tooltip.tsx +43 -0
  304. package/src/components/top-nav.stories.tsx +20 -0
  305. package/src/components/top-nav.tsx +76 -0
  306. package/src/docs/Colors.stories.tsx +192 -0
  307. package/src/docs/GettingStarted.mdx +72 -0
  308. package/src/docs/Mcp.mdx +52 -0
  309. package/src/docs/Typography.stories.tsx +114 -0
  310. package/src/hooks/use-in-view.ts +32 -0
  311. package/src/hooks/use-reduced-motion.ts +26 -0
  312. package/src/index.ts +343 -0
  313. package/src/lib/cn.test.ts +67 -0
  314. package/src/lib/cn.ts +81 -0
  315. package/src/lib/motion.ts +109 -0
  316. package/src/lib/prefix-utilities.test.ts +56 -0
  317. package/src/lib/prefix-utilities.ts +50 -0
  318. package/src/styles/base.css +489 -0
  319. package/src/styles/motion.css +69 -0
  320. package/src/styles/overrides.css +44 -0
  321. package/src/styles/typography.css +173 -0
  322. package/src/theme/theme.css +391 -0
  323. package/src/theme/tokens.ts +29 -0
  324. package/src/theme/types.ts +26 -0
  325. package/README.md +0 -3
@@ -0,0 +1,407 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
5
+ import { Search as SearchIcon, X as ClearIcon } from "lucide-react"
6
+ import { Spinner } from "./spinner"
7
+ import { Input, TextFieldRoot, TextFieldSlot, type InputSize } from "./input"
8
+ import { cn } from "../lib/cn"
9
+
10
+ export interface ComboboxItem {
11
+ /** Unique, stable identifier. Also what `value` / `onValueChange` carry. */
12
+ value: string
13
+ label: string
14
+ /** Secondary text shown at the end of the row, e.g. a price or count. */
15
+ description?: string
16
+ /** Leading visual, e.g. an icon or a small Avatar. Mark decorative icons aria-hidden. */
17
+ icon?: React.ReactNode
18
+ /** Rows with the same group render under one heading, in first-seen order. */
19
+ group?: string
20
+ /** Extra text matched by the default filter but not shown. */
21
+ keywords?: string
22
+ disabled?: boolean
23
+ }
24
+
25
+ export type ComboboxSize = InputSize
26
+
27
+ export interface ComboboxProps {
28
+ items: ComboboxItem[]
29
+ /** Selected item's value. Set it (with onValueChange) for a picker that shows the choice in the input. */
30
+ value?: string | null
31
+ onValueChange?: (value: string | null) => void
32
+ /** Called on every pick. Without `value`, the input clears after a pick (search launcher). */
33
+ onSelect?: (item: ComboboxItem) => void
34
+ /** Controlled query. Leave unset to let the combobox own it. */
35
+ query?: string
36
+ onQueryChange?: (query: string) => void
37
+ /** Return true to keep an item. Default: case-insensitive match on label, description and keywords. */
38
+ filter?: (item: ComboboxItem, query: string) => boolean
39
+ /** Shown instead of the item list while the query is empty, e.g. recent searches. */
40
+ startContent?: React.ReactNode
41
+ /** Shown when the query matches nothing. Receives the trimmed query. */
42
+ emptyMessage?: React.ReactNode | ((query: string) => React.ReactNode)
43
+ /** Show a pending row instead of results, e.g. while fetching remote items. */
44
+ loading?: boolean
45
+ loadingMessage?: React.ReactNode
46
+ /** Cap the rows shown per group (or overall when ungrouped). */
47
+ maxPerGroup?: number
48
+ /** Bold the matched part of each label. Default true. */
49
+ highlightMatches?: boolean
50
+ placeholder?: string
51
+ /** Accessible name. Required unless a <Label htmlFor={id}> names the input. */
52
+ "aria-label"?: string
53
+ id?: string
54
+ name?: string
55
+ size?: ComboboxSize
56
+ invalid?: boolean
57
+ disabled?: boolean
58
+ className?: string
59
+ }
60
+
61
+ export function defaultComboboxFilter(item: ComboboxItem, query: string) {
62
+ const haystack = `${item.label} ${item.description ?? ""} ${item.keywords ?? ""}`.toLowerCase()
63
+ return haystack.includes(query.toLowerCase())
64
+ }
65
+
66
+ /** Groups items in first-seen order, capping each group. Ungrouped items share one unnamed group. */
67
+ export function groupComboboxItems(items: ComboboxItem[], maxPerGroup = Infinity) {
68
+ const groups: { name: string | undefined; items: ComboboxItem[] }[] = []
69
+ for (const item of items) {
70
+ let group = groups.find((entry) => entry.name === item.group)
71
+ if (!group) {
72
+ group = { name: item.group, items: [] }
73
+ groups.push(group)
74
+ }
75
+ if (group.items.length < maxPerGroup) group.items.push(item)
76
+ }
77
+ return groups
78
+ }
79
+
80
+ /** Splits a label around the first case-insensitive match so it can be emphasised. */
81
+ export function splitComboboxMatch(text: string, query: string) {
82
+ const index = query ? text.toLowerCase().indexOf(query.toLowerCase()) : -1
83
+ if (index < 0) return null
84
+ return { before: text.slice(0, index), match: text.slice(index, index + query.length), after: text.slice(index + query.length) }
85
+ }
86
+
87
+ /** Next enabled index in `direction`, wrapping around; -1 when nothing is enabled. */
88
+ export function nextEnabledIndex(items: ComboboxItem[], from: number, direction: 1 | -1) {
89
+ for (let step = 1; step <= items.length; step++) {
90
+ const index = (from + direction * step + items.length * step) % items.length
91
+ if (!items[index]?.disabled) return index
92
+ }
93
+ return -1
94
+ }
95
+
96
+ const SIZE_TEXT: Record<ComboboxSize, string> = {
97
+ sm: "ax:t-label-sm",
98
+ md: "ax:t-body-sm",
99
+ lg: "ax:t-body-md",
100
+ }
101
+
102
+ function Highlight({ text, query }: { text: string; query: string }) {
103
+ const parts = splitComboboxMatch(text, query)
104
+ if (!parts) return <>{text}</>
105
+ return (
106
+ <>
107
+ {parts.before}
108
+ <strong className="ax:font-semibold ax:text-foreground-strong">{parts.match}</strong>
109
+ {parts.after}
110
+ </>
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Combobox - a text input with a filtered, keyboard-navigable list of options.
116
+ *
117
+ * Follows the WAI-ARIA combobox pattern: focus stays in the input while arrow
118
+ * keys move `aria-activedescendant` through the listbox, Enter picks, Escape
119
+ * closes. The list renders in a Popover portal anchored to the input, so
120
+ * overflow-hidden containers cannot clip it.
121
+ *
122
+ * @example
123
+ * <Combobox
124
+ * items={communities}
125
+ * value={community}
126
+ * onValueChange={setCommunity}
127
+ * placeholder="Choose a community"
128
+ * aria-label="Community"
129
+ * />
130
+ */
131
+ export function Combobox({
132
+ items,
133
+ value,
134
+ onValueChange,
135
+ onSelect,
136
+ query: queryProp,
137
+ onQueryChange,
138
+ filter = defaultComboboxFilter,
139
+ startContent,
140
+ emptyMessage = (query: string) => `No results for "${query}"`,
141
+ loading = false,
142
+ loadingMessage = "Searching",
143
+ maxPerGroup,
144
+ highlightMatches = true,
145
+ placeholder,
146
+ "aria-label": ariaLabel,
147
+ id,
148
+ name,
149
+ size = "md",
150
+ invalid,
151
+ disabled,
152
+ className,
153
+ }: ComboboxProps) {
154
+ const autoId = React.useId()
155
+ const inputId = id ?? `${autoId}-input`
156
+ const listId = `${autoId}-listbox`
157
+ const anchorRef = React.useRef<HTMLDivElement>(null)
158
+ const inputRef = React.useRef<HTMLInputElement>(null)
159
+
160
+ const isPicker = value !== undefined
161
+ // Remote results may no longer include the picked item, so remember it to keep its label.
162
+ const [lastPicked, setLastPicked] = React.useState<ComboboxItem>()
163
+ const selected = isPicker
164
+ ? items.find((item) => item.value === value) ?? (lastPicked?.value === value ? lastPicked : undefined)
165
+ : undefined
166
+
167
+ const [ownQuery, setOwnQuery] = React.useState("")
168
+ const query = queryProp ?? ownQuery
169
+ const setQuery = (next: string) => {
170
+ if (queryProp === undefined) setOwnQuery(next)
171
+ onQueryChange?.(next)
172
+ }
173
+
174
+ const [open, setOpen] = React.useState(false)
175
+ const [editing, setEditing] = React.useState(false)
176
+ const [active, setActive] = React.useState(-1)
177
+
178
+ const trimmed = query.trim()
179
+ const showStart = !trimmed && startContent !== undefined
180
+ const matches = trimmed ? items.filter((item) => filter(item, trimmed)) : items
181
+ const groups = groupComboboxItems(showStart ? [] : matches, maxPerGroup)
182
+ const visible = groups.flatMap((group) => group.items)
183
+ const activeItem = open && active >= 0 ? visible[active] : undefined
184
+ const optionId = (item: ComboboxItem) => `${autoId}-option-${visible.indexOf(item)}`
185
+
186
+ // In picker mode the input shows the chosen label until the user starts editing.
187
+ const inputValue = isPicker && !editing ? selected?.label ?? "" : query
188
+
189
+ // Typing highlights the first enabled match so Enter picks it straight away. Results
190
+ // that arrive later (remote search) get the same treatment if nothing valid is highlighted.
191
+ const lastQuery = React.useRef(trimmed)
192
+ React.useEffect(() => {
193
+ if (!open || !trimmed) return
194
+ const queryChanged = lastQuery.current !== trimmed
195
+ lastQuery.current = trimmed
196
+ if (queryChanged || active < 0 || active >= visible.length) setActive(nextEnabledIndex(visible, -1, 1))
197
+ }, [open, trimmed, visible.length, active])
198
+
199
+ React.useEffect(() => {
200
+ if (!activeItem) return
201
+ document.getElementById(optionId(activeItem))?.scrollIntoView({ block: "nearest" })
202
+ })
203
+
204
+ function openList() {
205
+ if (disabled) return
206
+ setOpen(true)
207
+ const selectedIndex = selected ? visible.indexOf(selected) : -1
208
+ setActive((current) => (current >= 0 ? current : selectedIndex >= 0 ? selectedIndex : nextEnabledIndex(visible, -1, 1)))
209
+ }
210
+
211
+ function close() {
212
+ setOpen(false)
213
+ setEditing(false)
214
+ setActive(-1)
215
+ }
216
+
217
+ function pick(item: ComboboxItem) {
218
+ if (item.disabled) return
219
+ setLastPicked(item)
220
+ onSelect?.(item)
221
+ if (isPicker) onValueChange?.(item.value)
222
+ setQuery("")
223
+ close()
224
+ }
225
+
226
+ function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
227
+ switch (event.key) {
228
+ case "ArrowDown":
229
+ case "ArrowUp": {
230
+ event.preventDefault()
231
+ if (!open) return openList()
232
+ const direction = event.key === "ArrowDown" ? 1 : -1
233
+ setActive((current) => nextEnabledIndex(visible, current < 0 && direction < 0 ? 0 : current, direction))
234
+ return
235
+ }
236
+ case "Home":
237
+ case "End":
238
+ if (!open || !visible.length) return
239
+ event.preventDefault()
240
+ setActive(event.key === "Home" ? nextEnabledIndex(visible, -1, 1) : nextEnabledIndex(visible, 0, -1))
241
+ return
242
+ case "Enter":
243
+ if (open && activeItem) {
244
+ event.preventDefault()
245
+ pick(activeItem)
246
+ }
247
+ return
248
+ case "Escape":
249
+ if (open) {
250
+ event.preventDefault()
251
+ close()
252
+ }
253
+ return
254
+ case "Tab":
255
+ close()
256
+ }
257
+ }
258
+
259
+ const listContent = loading ? (
260
+ <div className="ax:flex ax:items-center ax:gap-xs ax:px-sm ax:py-sm ax:text-muted-foreground" role="status">
261
+ <Spinner size="sm" decorative className="ax:text-current" />
262
+ <span className={SIZE_TEXT[size]}>{loadingMessage}</span>
263
+ </div>
264
+ ) : showStart ? (
265
+ // Keep focus in the input when start content is clicked; keyboard users can still Tab into it.
266
+ <div onMouseDown={(event) => event.preventDefault()}>{startContent}</div>
267
+ ) : visible.length === 0 ? (
268
+ <p className={cn("ax:px-sm ax:py-sm ax:text-muted-foreground", SIZE_TEXT[size])}>
269
+ {typeof emptyMessage === "function" ? emptyMessage(trimmed) : emptyMessage}
270
+ </p>
271
+ ) : (
272
+ <div role="listbox" id={listId} aria-label={ariaLabel}>
273
+ {groups.map((group, groupIndex) => (
274
+ <div
275
+ key={group.name ?? `group-${groupIndex}`}
276
+ role={group.name ? "group" : undefined}
277
+ aria-labelledby={group.name ? `${autoId}-group-${groupIndex}` : undefined}
278
+ className="ax:py-2xs"
279
+ >
280
+ {group.name && (
281
+ <div id={`${autoId}-group-${groupIndex}`} className="ax:px-sm ax:pt-xs ax:pb-2xs ax:t-overline ax:text-muted-foreground">
282
+ {group.name}
283
+ </div>
284
+ )}
285
+ {group.items.map((item) => {
286
+ const isActive = activeItem === item
287
+ const isSelected = selected?.value === item.value
288
+ return (
289
+ <div
290
+ key={item.value}
291
+ id={optionId(item)}
292
+ role="option"
293
+ aria-selected={isPicker ? isSelected : isActive}
294
+ aria-disabled={item.disabled || undefined}
295
+ data-active={isActive || undefined}
296
+ onMouseDown={(event) => event.preventDefault()}
297
+ onMouseMove={() => !item.disabled && setActive(visible.indexOf(item))}
298
+ onClick={() => pick(item)}
299
+ className={cn(
300
+ "ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:gap-sm ax:rounded-(--input-radius) ax:px-sm ax:py-xs",
301
+ "ax:text-foreground ax:transition-colors",
302
+ SIZE_TEXT[size],
303
+ isActive && "ax:bg-muted",
304
+ isSelected && "ax:font-medium",
305
+ item.disabled && "ax:cursor-not-allowed ax:opacity-50",
306
+ )}
307
+ >
308
+ {item.icon && (
309
+ <span className="ax:flex ax:size-6 ax:shrink-0 ax:items-center ax:justify-center ax:text-muted-foreground ax:[&_svg]:size-4">
310
+ {item.icon}
311
+ </span>
312
+ )}
313
+ <span className="ax:min-w-0 ax:flex-1 ax:truncate">
314
+ {highlightMatches ? <Highlight text={item.label} query={trimmed} /> : item.label}
315
+ </span>
316
+ {item.description && (
317
+ <span className="ax:shrink-0 ax:t-caption ax:text-muted-foreground">{item.description}</span>
318
+ )}
319
+ </div>
320
+ )
321
+ })}
322
+ </div>
323
+ ))}
324
+ </div>
325
+ )
326
+
327
+ return (
328
+ <PopoverPrimitive.Root open={open} onOpenChange={(next) => (next ? openList() : close())} modal={false}>
329
+ <PopoverPrimitive.Anchor asChild>
330
+ <div ref={anchorRef} className={cn("ax:w-full", className)} data-slot="combobox">
331
+ <TextFieldRoot size={size} invalid={invalid}>
332
+ <TextFieldSlot>
333
+ <SearchIcon className="ax:size-4" aria-hidden />
334
+ </TextFieldSlot>
335
+ <Input
336
+ ref={inputRef}
337
+ id={inputId}
338
+ name={name}
339
+ type="text"
340
+ disabled={disabled}
341
+ aria-invalid={invalid || undefined}
342
+ value={inputValue}
343
+ placeholder={placeholder}
344
+ aria-label={ariaLabel}
345
+ role="combobox"
346
+ aria-expanded={open}
347
+ aria-controls={open && !showStart && visible.length > 0 ? listId : undefined}
348
+ aria-autocomplete="list"
349
+ aria-activedescendant={activeItem ? optionId(activeItem) : undefined}
350
+ autoComplete="off"
351
+ onChange={(event) => {
352
+ setEditing(true)
353
+ setQuery(event.target.value)
354
+ setOpen(true)
355
+ }}
356
+ onFocus={openList}
357
+ onClick={openList}
358
+ onKeyDown={onKeyDown}
359
+ />
360
+ {inputValue && !disabled && (
361
+ <TextFieldSlot>
362
+ <button
363
+ type="button"
364
+ aria-label={isPicker ? "Clear selection" : "Clear search"}
365
+ onMouseDown={(event) => event.preventDefault()}
366
+ onClick={() => {
367
+ setQuery("")
368
+ setEditing(true)
369
+ if (isPicker) onValueChange?.(null)
370
+ inputRef.current?.focus()
371
+ }}
372
+ className="ax:rounded-sm ax:text-muted-foreground ax:transition-colors ax:hover:text-foreground ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring"
373
+ >
374
+ <ClearIcon className="ax:size-4" />
375
+ </button>
376
+ </TextFieldSlot>
377
+ )}
378
+ </TextFieldRoot>
379
+ <span className="ax:sr-only" aria-live="polite">
380
+ {open && trimmed && !loading ? `${visible.length} ${visible.length === 1 ? "result" : "results"}` : ""}
381
+ </span>
382
+ </div>
383
+ </PopoverPrimitive.Anchor>
384
+ <PopoverPrimitive.Portal>
385
+ <PopoverPrimitive.Content
386
+ // A listbox popup, not a dialog: focus never leaves the input.
387
+ role="presentation"
388
+ align="start"
389
+ sideOffset={6}
390
+ onOpenAutoFocus={(event) => event.preventDefault()}
391
+ onCloseAutoFocus={(event) => event.preventDefault()}
392
+ onInteractOutside={(event) => {
393
+ if (anchorRef.current?.contains(event.target as Node)) event.preventDefault()
394
+ }}
395
+ className={cn(
396
+ "ax:z-[1100] ax:w-(--radix-popper-anchor-width) ax:max-h-80 ax:overflow-y-auto",
397
+ "ax:rounded-(--input-radius) ax:border ax:border-border ax:bg-popover ax:p-2xs ax:text-popover-foreground ax:shadow-md ax:outline-none",
398
+ "ax:data-[state=open]:animate-in ax:data-[state=closed]:animate-out ax:data-[state=closed]:fade-out-0 ax:data-[state=open]:fade-in-0",
399
+ "ax:[animation-duration:var(--motion-fast)] ax:motion-reduce:animate-none",
400
+ )}
401
+ >
402
+ {listContent}
403
+ </PopoverPrimitive.Content>
404
+ </PopoverPrimitive.Portal>
405
+ </PopoverPrimitive.Root>
406
+ )
407
+ }
@@ -0,0 +1,24 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { CountdownPill } from "./countdown-pill"
4
+
5
+ const meta: Meta<typeof CountdownPill> = {
6
+ title: "Components/CountdownPill",
7
+ component: CountdownPill,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ label: "Starts in",
11
+ value: "2h 15m",
12
+ tone: "info",
13
+ size: "md",
14
+ },
15
+ }
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof CountdownPill>
19
+
20
+ export const Default: Story = {}
21
+
22
+ export const Critical: Story = {
23
+ args: { tone: "critical", value: "5m", label: "Ends in" },
24
+ }
@@ -0,0 +1,129 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Clock } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+
7
+ export type CountdownTone = "neutral" | "info" | "success" | "warning" | "critical"
8
+ export type CountdownSize = "sm" | "md" | "lg"
9
+
10
+ export interface CountdownPillProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ /** The countdown text itself - the bit that gets emphasized.
12
+ * e.g. `"2h 15m"`, `"3 days"`, `"Live now"`. */
13
+ value: string
14
+ /** Optional context label rendered before the value.
15
+ * e.g. `"Starts in"`, `"Bidding ends in"`. */
16
+ label?: string
17
+ /** Visual urgency tier. Drives the value (and icon) color so the consumer
18
+ * can dial attention up or down without changing markup. */
19
+ tone?: CountdownTone
20
+ /** Visual density. */
21
+ size?: CountdownSize
22
+ /** Optional icon override. Defaults to a Clock glyph. Pass `null` to omit. */
23
+ icon?: React.ReactNode | null
24
+ }
25
+
26
+ // Value uses raw `text-* font-bold` (not `t-h*` tokens) on purpose: the
27
+ // typography tokens hard-code `text-foreground-strong`, which would override
28
+ // the semantic tone color and defeat the urgency cue.
29
+
30
+ const VALUE_CLASS: Record<CountdownSize, string> = {
31
+ sm: "ax:text-sm ax:font-bold",
32
+ md: "ax:text-2xl ax:font-bold", // 24px
33
+ lg: "ax:text-4xl ax:font-bold", // 36px
34
+ }
35
+
36
+ const LABEL_CLASS: Record<CountdownSize, string> = {
37
+ sm: "ax:t-caption",
38
+ md: "ax:t-label-sm",
39
+ lg: "ax:t-overline",
40
+ }
41
+
42
+ const ICON_CLASS: Record<CountdownSize, string> = {
43
+ sm: "ax:size-3.5 ax:[&>svg]:size-3.5",
44
+ md: "ax:size-4 ax:[&>svg]:size-4",
45
+ lg: "ax:size-5 ax:[&>svg]:size-5",
46
+ }
47
+
48
+ const GAP_CLASS: Record<CountdownSize, string> = {
49
+ sm: "ax:gap-2xs",
50
+ md: "ax:gap-xs",
51
+ lg: "ax:gap-sm",
52
+ }
53
+
54
+ const TONE_VALUE: Record<CountdownTone, string> = {
55
+ neutral: "ax:text-foreground",
56
+ info: "ax:text-primary",
57
+ success: "ax:text-success-foreground",
58
+ warning: "ax:text-warning-foreground",
59
+ critical: "ax:text-destructive-foreground",
60
+ }
61
+
62
+ // `neutral` keeps the icon muted so it doesn't compete with the label/value;
63
+ // every other tone matches the value color so the icon, label, and value
64
+ // read as a single urgency unit.
65
+ const TONE_ICON: Record<CountdownTone, string> = {
66
+ neutral: "ax:text-muted-foreground",
67
+ info: "ax:text-primary",
68
+ success: "ax:text-success-foreground",
69
+ warning: "ax:text-warning-foreground",
70
+ critical: "ax:text-destructive-foreground",
71
+ }
72
+
73
+ /** Inline countdown display for time-to-event copy (sessions, deadlines,
74
+ * auctions). Designed to *raise* attention: pairs a muted context label
75
+ * with a large, tone-colored value. Three sizes and five urgency tones.
76
+ *
77
+ * The component is presentational only. Compute the `value` string and
78
+ * pick a `tone` in the consumer. Use [[getCountdownLabel]] to format an
79
+ * ISO timestamp into a label like `"Starts in 2h 15m"`. */
80
+ export const CountdownPill = React.forwardRef<HTMLDivElement, CountdownPillProps>(
81
+ ({ value, label, tone = "neutral", size = "md", icon, className, ...props }, ref) => {
82
+ const iconNode = icon === null ? null : icon ?? <Clock />
83
+
84
+ return (
85
+ <div
86
+ ref={ref}
87
+ className={cn("ax:inline-flex ax:items-center", GAP_CLASS[size], className)}
88
+ {...props}
89
+ >
90
+ {iconNode && (
91
+ <span className={cn("ax:shrink-0", ICON_CLASS[size], TONE_ICON[tone])}>
92
+ {iconNode}
93
+ </span>
94
+ )}
95
+ {label && (
96
+ <span className={cn(LABEL_CLASS[size], "ax:text-muted-foreground")}>
97
+ {label}
98
+ </span>
99
+ )}
100
+ <span className={cn(VALUE_CLASS[size], TONE_VALUE[tone])}>
101
+ {value}
102
+ </span>
103
+ </div>
104
+ )
105
+ },
106
+ )
107
+ CountdownPill.displayName = "CountdownPill"
108
+
109
+ /** Format an ISO timestamp into a countdown label.
110
+ *
111
+ * • Past / now → the `ongoingLabel` you pass (e.g. `"Live now"`)
112
+ * • Within 24h → `"2h 15m"`
113
+ * • Further out → `"3 days"`
114
+ *
115
+ * Urgency tone is intentionally NOT inferred. Pick it in the consumer
116
+ * based on your own thresholds and pass it via the `tone` prop. */
117
+ export function getCountdownLabel(dateIso: string, ongoingLabel: string, now = Date.now()): string {
118
+ const diff = new Date(dateIso).getTime() - now
119
+ if (diff <= 0) return ongoingLabel
120
+
121
+ const hours = Math.floor(diff / (1000 * 60 * 60))
122
+ if (hours < 24) {
123
+ const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
124
+ return `${hours}h ${mins}m`
125
+ }
126
+
127
+ const days = Math.floor(hours / 24)
128
+ return `${days} day${days !== 1 ? "s" : ""}`
129
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { CTA } from "./cta"
4
+ import { Button } from "./button"
5
+
6
+ const meta: Meta<typeof CTA> = {
7
+ title: "Components/CTA",
8
+ component: CTA,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ title: "Ready to get started?",
12
+ description: "Join thousands of teams building with Adres.",
13
+ actions: (
14
+ <>
15
+ <Button>Start free trial</Button>
16
+ <Button variant="outline">Talk to sales</Button>
17
+ </>
18
+ ),
19
+ },
20
+ }
21
+
22
+ export default meta
23
+ type Story = StoryObj<typeof CTA>
24
+
25
+ export const Default: Story = {}
26
+
27
+ export const Primary: Story = {
28
+ args: { emphasis: "primary" },
29
+ }
@@ -0,0 +1,52 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../lib/cn"
5
+
6
+ export interface CTAProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
7
+ title: React.ReactNode
8
+ description?: React.ReactNode
9
+ actions: React.ReactNode
10
+ /** Visual emphasis - 'subtle' uses muted surface, 'primary' uses brand surface. */
11
+ emphasis?: "subtle" | "primary"
12
+ }
13
+
14
+ /**
15
+ * CTA - closing call-to-action section for marketing pages.
16
+ * Two emphasis levels so it can adapt to where it sits on the page.
17
+ */
18
+ export const CTA = React.forwardRef<HTMLElement, CTAProps>(
19
+ ({ className, title, description, actions, emphasis = "subtle", ...props }, ref) => (
20
+ <section
21
+ ref={ref}
22
+ className={cn("ax:w-full ax:px-xl ax:md:px-3xl ax:py-5xl", className)}
23
+ {...props}
24
+ >
25
+ <div
26
+ className={cn(
27
+ "ax:mx-auto ax:max-w-5xl ax:rounded-xl ax:px-2xl ax:md:px-4xl ax:py-4xl ax:md:py-5xl",
28
+ "ax:flex ax:flex-col ax:items-center ax:text-center ax:gap-xl",
29
+ emphasis === "primary"
30
+ ? "ax:bg-primary ax:text-primary-foreground"
31
+ : "ax:bg-muted ax:text-foreground"
32
+ )}
33
+ >
34
+ <h2 className="ax:t-display-md ax:md:t-display-lg ax:[text-wrap:balance]">
35
+ {title}
36
+ </h2>
37
+ {description && (
38
+ <p
39
+ className={cn(
40
+ "ax:t-body-md ax:md:t-body-lg ax:[text-wrap:pretty] ax:max-w-2xl",
41
+ emphasis === "primary" ? "ax:text-primary-foreground/80" : "ax:text-muted-foreground"
42
+ )}
43
+ >
44
+ {description}
45
+ </p>
46
+ )}
47
+ <div className="ax:flex ax:flex-wrap ax:gap-sm ax:justify-center ax:pt-xs">{actions}</div>
48
+ </div>
49
+ </section>
50
+ )
51
+ )
52
+ CTA.displayName = "CTA"
@@ -0,0 +1,50 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { DataList, DataListItem, DataListLabel, DataListValue } from "./data-list"
4
+
5
+ const meta: Meta<typeof DataList> = {
6
+ title: "Components/DataList",
7
+ component: DataList,
8
+ tags: ["autodocs"],
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof DataList>
13
+
14
+ export const Default: Story = {
15
+ render: () => (
16
+ <DataList className="ax:max-w-md">
17
+ <DataListItem>
18
+ <DataListLabel>Status</DataListLabel>
19
+ <DataListValue>Active</DataListValue>
20
+ </DataListItem>
21
+ <DataListItem>
22
+ <DataListLabel>Plan</DataListLabel>
23
+ <DataListValue>Professional</DataListValue>
24
+ </DataListItem>
25
+ <DataListItem>
26
+ <DataListLabel>Renewal</DataListLabel>
27
+ <DataListValue>Sep 1, 2026</DataListValue>
28
+ </DataListItem>
29
+ </DataList>
30
+ ),
31
+ }
32
+
33
+ /** Short facts in columns, label above value, without dividers. */
34
+ export const Grid: Story = {
35
+ render: () => (
36
+ <DataList orientation="grid" columns={4} size="sm">
37
+ {[
38
+ ["Trade licence", "2120561384412A"],
39
+ ["Brokerage licence", "8812612491127"],
40
+ ["Licence expiry", "15 May 2028"],
41
+ ["Phone", "00 1098231807"],
42
+ ].map(([label, value]) => (
43
+ <DataListItem key={label}>
44
+ <DataListLabel>{label}</DataListLabel>
45
+ <DataListValue>{value}</DataListValue>
46
+ </DataListItem>
47
+ ))}
48
+ </DataList>
49
+ ),
50
+ }