@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,210 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { X } from "lucide-react"
5
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
6
+ import { cn } from "../lib/cn"
7
+
8
+ function isValidEmail(email: string): boolean {
9
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
10
+ }
11
+
12
+ // Splits a pasted blob into candidate addresses. Handles the shapes users
13
+ // realistically paste: comma-, semicolon-, newline-, tab-, or whitespace-
14
+ // separated lists (incl. mixed). Empty fragments are filtered out by callers.
15
+ const SPLIT_RE = /[\s,;]+/
16
+
17
+ export type EmailChipsInputVariant = "default" | "primary" | "destructive"
18
+
19
+ export interface EmailChip {
20
+ value: string
21
+ valid: boolean
22
+ }
23
+
24
+ export interface EmailChipsInputProps
25
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
26
+ chips: EmailChip[]
27
+ onChange: (chips: EmailChip[]) => void
28
+ placeholder?: string
29
+ /** Fired when the user tries to add an address that's already in the
30
+ * list. Useful for surfacing a non-blocking warning toast. */
31
+ onDuplicate?: () => void
32
+ /** Tone applied to valid chips. `default` is neutral, `primary` tints
33
+ * brand, `destructive` tints destructive. Invalid chips always render in
34
+ * destructive regardless of variant - the error signal stays distinct. */
35
+ variant?: EmailChipsInputVariant
36
+ /** Disables the input and chip-removal buttons. */
37
+ disabled?: boolean
38
+ /** Tints the wrapper border destructive and sets `aria-invalid` on the
39
+ * inner input. Consumer-controlled - independent of per-chip validity. */
40
+ invalid?: boolean
41
+ /** id for the inner <input>, so a sibling <Label htmlFor> binds correctly. */
42
+ id?: string
43
+ }
44
+
45
+ // Valid-chip tint per variant. Invalid chips bypass this map and always
46
+ // render in the destructive treatment so the error reads unambiguously.
47
+ const VARIANT_VALID_CLASS: Record<EmailChipsInputVariant, string> = {
48
+ default: "ax:bg-muted ax:dark:bg-neutral ax:text-foreground ax:dark:text-neutral-muted",
49
+ primary: "ax:bg-primary-weakest ax:dark:bg-primary-strong ax:text-primary-foreground ax:dark:text-primary-muted",
50
+ destructive: "ax:bg-destructive-weakest ax:dark:bg-destructive-strong ax:text-destructive-foreground ax:dark:text-destructive-muted",
51
+ }
52
+
53
+ /** Multi-email input that turns each accepted entry into a chip. Adds on
54
+ * `Enter` or `,`. Backspace on an empty input removes the last chip.
55
+ * Pasting a `, ; \n \t`-separated list adds each address as a chip.
56
+ *
57
+ * Validation is local (regex-based) and non-blocking: invalid chips
58
+ * render in a destructive tint with a tooltip explaining why. Consumers
59
+ * filter `chip.valid` before submitting. */
60
+ export const EmailChipsInput = React.forwardRef<HTMLDivElement, EmailChipsInputProps>(
61
+ (
62
+ {
63
+ chips,
64
+ onChange,
65
+ placeholder = "Enter email and press Enter…",
66
+ onDuplicate,
67
+ variant = "default",
68
+ disabled = false,
69
+ invalid = false,
70
+ id,
71
+ className,
72
+ ...props
73
+ },
74
+ ref,
75
+ ) => {
76
+ const [input, setInput] = React.useState("")
77
+ const inputRef = React.useRef<HTMLInputElement>(null)
78
+
79
+ function addChip(raw: string) {
80
+ const value = raw.trim().toLowerCase()
81
+ if (!value) return
82
+ if (chips.some((c) => c.value === value)) {
83
+ onDuplicate?.()
84
+ setInput("")
85
+ return
86
+ }
87
+ onChange([...chips, { value, valid: isValidEmail(value) }])
88
+ setInput("")
89
+ }
90
+
91
+ // Multi-add for paste: split, de-dupe against current chips AND against
92
+ // earlier entries in the same paste, then emit a single onChange update.
93
+ function addMany(raws: string[]) {
94
+ const next: EmailChip[] = [...chips]
95
+ const seen = new Set(chips.map((c) => c.value))
96
+ let duped = false
97
+
98
+ for (const raw of raws) {
99
+ const value = raw.trim().toLowerCase()
100
+ if (!value) continue
101
+ if (seen.has(value)) { duped = true; continue }
102
+ seen.add(value)
103
+ next.push({ value, valid: isValidEmail(value) })
104
+ }
105
+
106
+ if (next.length !== chips.length) onChange(next)
107
+ if (duped) onDuplicate?.()
108
+ setInput("")
109
+ }
110
+
111
+ function removeChip(index: number) {
112
+ if (disabled) return
113
+ onChange(chips.filter((_, i) => i !== index))
114
+ }
115
+
116
+ function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
117
+ if (e.key === "Enter" || e.key === ",") {
118
+ e.preventDefault()
119
+ addChip(input)
120
+ } else if (e.key === "Backspace" && !input && chips.length > 0) {
121
+ removeChip(chips.length - 1)
122
+ }
123
+ }
124
+
125
+ function handlePaste(e: React.ClipboardEvent<HTMLInputElement>) {
126
+ const pasted = e.clipboardData.getData("text")
127
+ if (!pasted) return
128
+ // Only intercept if the paste looks list-shaped: single-address
129
+ // pastes still go through the native flow + Enter/blur.
130
+ if (SPLIT_RE.test(pasted)) {
131
+ e.preventDefault()
132
+ // Include any partial input the user already typed before pasting.
133
+ const combined = input ? [input, pasted].join(" ") : pasted
134
+ addMany(combined.split(SPLIT_RE))
135
+ }
136
+ }
137
+
138
+ return (
139
+ <TooltipProvider>
140
+ <div
141
+ ref={ref}
142
+ className={cn(
143
+ "ax:min-h-3xl ax:flex ax:flex-wrap ax:gap-xs ax:p-xs",
144
+ "ax:rounded-(--input-radius) ax:border ax:border-input ax:bg-background",
145
+ "ax:focus-within:ring-2 ax:focus-within:ring-ring ax:focus-within:ring-offset-0",
146
+ "ax:cursor-text",
147
+ invalid && "ax:border-destructive ax:focus-within:ring-destructive",
148
+ disabled && "ax:opacity-60 ax:cursor-not-allowed ax:bg-muted/30",
149
+ className,
150
+ )}
151
+ onClick={() => !disabled && inputRef.current?.focus()}
152
+ {...props}
153
+ >
154
+ {chips.map((chip, i) => (
155
+ <Tooltip key={`${chip.value}-${i}`}>
156
+ <TooltipTrigger asChild>
157
+ <span
158
+ className={cn(
159
+ "ax:inline-flex ax:items-center ax:gap-1 ax:px-xs ax:py-2xs ax:rounded-badge",
160
+ "ax:t-label-sm ax:max-w-50",
161
+ chip.valid
162
+ ? VARIANT_VALID_CLASS[variant]
163
+ : "ax:bg-destructive-weakest ax:dark:bg-destructive-strong ax:text-destructive-foreground ax:dark:text-destructive-muted",
164
+ )}
165
+ >
166
+ <span className="ax:truncate">{chip.value}</span>
167
+ <button
168
+ type="button"
169
+ disabled={disabled}
170
+ onClick={(e) => { e.stopPropagation(); removeChip(i) }}
171
+ className={cn(
172
+ "ax:shrink-0 ax:rounded-full ax:hover:bg-foreground/10",
173
+ "ax:focus-visible:outline-none ax:focus-visible:ring-1 ax:focus-visible:ring-ring",
174
+ "ax:disabled:pointer-events-none",
175
+ )}
176
+ aria-label={`Remove ${chip.value}`}
177
+ >
178
+ <X className="ax:size-3" />
179
+ </button>
180
+ </span>
181
+ </TooltipTrigger>
182
+ {!chip.valid && (
183
+ <TooltipContent side="top">Invalid email address</TooltipContent>
184
+ )}
185
+ </Tooltip>
186
+ ))}
187
+
188
+ <input
189
+ ref={inputRef}
190
+ id={id}
191
+ value={input}
192
+ onChange={(e) => setInput(e.target.value)}
193
+ onKeyDown={handleKeyDown}
194
+ onPaste={handlePaste}
195
+ onBlur={() => { if (input.trim()) addChip(input) }}
196
+ disabled={disabled}
197
+ aria-invalid={invalid || undefined}
198
+ placeholder={chips.length === 0 ? placeholder : ""}
199
+ className={cn(
200
+ "ax:flex-1 ax:min-w-40 ax:bg-transparent ax:outline-none",
201
+ "ax:t-body-sm ax:text-foreground ax:placeholder:text-muted-foreground",
202
+ "ax:disabled:cursor-not-allowed",
203
+ )}
204
+ />
205
+ </div>
206
+ </TooltipProvider>
207
+ )
208
+ },
209
+ )
210
+ EmailChipsInput.displayName = "EmailChipsInput"
@@ -0,0 +1,22 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { FolderOpen } from "lucide-react"
4
+ import { Button } from "./button"
5
+ import { EmptyState } from "./empty-state"
6
+
7
+ const meta: Meta<typeof EmptyState> = {
8
+ title: "Components/EmptyState",
9
+ component: EmptyState,
10
+ tags: ["autodocs"],
11
+ args: {
12
+ icon: <FolderOpen className="ax:size-6" />,
13
+ title: "No projects yet",
14
+ description: "Create your first project to get started.",
15
+ action: <Button>Create project</Button>,
16
+ },
17
+ }
18
+
19
+ export default meta
20
+ type Story = StoryObj<typeof EmptyState>
21
+
22
+ export const Default: Story = {}
@@ -0,0 +1,35 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/cn"
3
+
4
+ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ icon?: React.ReactNode
6
+ title: string
7
+ description?: string
8
+ action?: React.ReactNode
9
+ }
10
+
11
+ export function EmptyState({ className, icon, title, description, action, ...props }: EmptyStateProps) {
12
+ return (
13
+ <div
14
+ className={cn(
15
+ "ax:flex ax:flex-col ax:items-center ax:justify-center ax:text-center",
16
+ "ax:py-5xl ax:px-xl ax:gap-md",
17
+ className
18
+ )}
19
+ {...props}
20
+ >
21
+ {icon && (
22
+ <div className="ax:size-4xl ax:rounded-full ax:bg-muted ax:flex ax:items-center ax:justify-center ax:text-muted-foreground">
23
+ {icon}
24
+ </div>
25
+ )}
26
+ <div className="ax:space-y-1.5 ax:max-w-md">
27
+ <h3 className="ax:t-h5 ax:text-foreground">{title}</h3>
28
+ {description && (
29
+ <p className="ax:t-body-sm ax:text-muted-foreground">{description}</p>
30
+ )}
31
+ </div>
32
+ {action && <div>{action}</div>}
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,43 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { EntityHeader } from "./entity-header"
4
+ import { DataList, DataListItem, DataListLabel, DataListValue } from "./data-list"
5
+ import { Button } from "./button"
6
+
7
+ const DETAILS = [
8
+ { label: "Trade licence", value: "2120561384412A" },
9
+ { label: "Brokerage licence", value: "8812612491127" },
10
+ { label: "Licence expiry", value: "15 May 2028" },
11
+ { label: "Phone", value: "00 1098231807" },
12
+ ]
13
+
14
+ const details = (
15
+ <DataList orientation="grid" columns={4} size="sm">
16
+ {DETAILS.map((detail) => (
17
+ <DataListItem key={detail.label}>
18
+ <DataListLabel>{detail.label}</DataListLabel>
19
+ <DataListValue>{detail.value}</DataListValue>
20
+ </DataListItem>
21
+ ))}
22
+ </DataList>
23
+ )
24
+
25
+ const meta: Meta<typeof EntityHeader> = {
26
+ title: "Components/EntityHeader",
27
+ component: EntityHeader,
28
+ tags: ["autodocs"],
29
+ args: { name: "Remax RealEstate", subtitle: "Remax Real Estate Brokerage L.L.C." },
30
+ }
31
+
32
+ export default meta
33
+ type Story = StoryObj<typeof EntityHeader>
34
+
35
+ export const Default: Story = {}
36
+
37
+ export const WithDetails: Story = { args: { details } }
38
+
39
+ export const WithActions: Story = {
40
+ args: { details, actions: <><Button variant="outline" size="sm">Edit details</Button><Button size="sm">Contact</Button></> },
41
+ }
42
+
43
+ export const Medium: Story = { args: { size: "md", name: "Omar Khalifa Real Estate", subtitle: "CN-1907642" } }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { EntityHeader, entityInitials } from "./entity-header"
5
+
6
+ describe("entityInitials", () => {
7
+ it("takes the first letter of the first two words", () => {
8
+ expect(entityInitials("Remax RealEstate")).toBe("RR")
9
+ expect(entityInitials(" omar khalifa real estate ")).toBe("OK")
10
+ expect(entityInitials("Aldar")).toBe("A")
11
+ })
12
+ })
13
+
14
+ describe("EntityHeader", () => {
15
+ it("renders the name as a heading at the chosen level", () => {
16
+ expect(renderToStaticMarkup(<EntityHeader name="Remax RealEstate" />)).toMatch(/<h2[^>]*>Remax RealEstate<\/h2>/)
17
+ expect(renderToStaticMarkup(<EntityHeader name="Remax RealEstate" as="h1" />)).toMatch(/<h1[^>]*>Remax RealEstate<\/h1>/)
18
+ })
19
+
20
+ it("shows decorative initials without a logo, and the logo with its alt text when given", () => {
21
+ const initials = renderToStaticMarkup(<EntityHeader name="Remax RealEstate" />)
22
+ expect(initials).toMatch(/aria-hidden="true"[^>]*>RR</)
23
+ const logo = renderToStaticMarkup(<EntityHeader name="Remax RealEstate" logo={{ src: "/remax.webp", alt: "Remax logo" }} />)
24
+ expect(logo).toContain('alt="Remax logo"')
25
+ expect(logo).not.toContain(">RR<")
26
+ })
27
+
28
+ it("renders the details and actions slots only when provided", () => {
29
+ const aside = "ax:flex ax:min-w-0 ax:flex-col ax:gap-md ax:lg:items-end"
30
+ expect(renderToStaticMarkup(<EntityHeader name="A" />)).not.toContain(aside)
31
+ const withDetails = renderToStaticMarkup(<EntityHeader name="A" details={<span>Licence</span>} />)
32
+ expect(withDetails).toContain(aside)
33
+ expect(withDetails).toContain("Licence")
34
+ })
35
+ })
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+ import { tv, type VariantProps } from "tailwind-variants"
3
+ import { Heading, type HeadingAs, type HeadingSize } from "./heading"
4
+ import { cn } from "../lib/cn"
5
+
6
+ export type EntityHeaderSize = "md" | "lg"
7
+
8
+ /** Up to two initials from the name's first words, e.g. "Remax RealEstate" -> "RR". */
9
+ export function entityInitials(name: string) {
10
+ return name
11
+ .trim()
12
+ .split(/\s+/)
13
+ .slice(0, 2)
14
+ .map((word) => word.charAt(0).toUpperCase())
15
+ .join("")
16
+ }
17
+
18
+ export const entityHeaderVariants = tv({
19
+ slots: {
20
+ root: "ax:flex ax:flex-col ax:gap-lg ax:lg:flex-row ax:lg:items-end ax:lg:justify-between",
21
+ identity: "ax:flex ax:min-w-0 ax:items-center ax:gap-md",
22
+ mark: "ax:flex ax:shrink-0 ax:items-center ax:justify-center ax:overflow-hidden ax:rounded-md ax:bg-primary-weaker ax:font-bold ax:text-primary-foreground ax:dark:bg-primary-strong ax:dark:text-primary-weaker",
23
+ text: "ax:flex ax:min-w-0 ax:flex-col",
24
+ subtitle: "ax:t-caption ax:text-muted-foreground",
25
+ aside: "ax:flex ax:min-w-0 ax:flex-col ax:gap-md ax:lg:items-end",
26
+ },
27
+ variants: {
28
+ size: {
29
+ md: { mark: "ax:size-12 ax:t-body-lg" },
30
+ lg: { mark: "ax:size-14 ax:t-body-xl" },
31
+ },
32
+ },
33
+ defaultVariants: { size: "lg" },
34
+ }, { twMerge: false })
35
+
36
+ const NAME_SIZE: Record<EntityHeaderSize, HeadingSize> = { md: "h3", lg: "h2" }
37
+
38
+ export interface EntityHeaderProps
39
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
40
+ VariantProps<typeof entityHeaderVariants> {
41
+ /** Organisation, company or property name. */
42
+ name: string
43
+ /** Secondary line, e.g. the legal name or a registration number. */
44
+ subtitle?: React.ReactNode
45
+ /** Logo image. Without it the mark shows initials from `name`. */
46
+ logo?: { src: string; alt: string }
47
+ /** Heading level for the name. Defaults to `h2`. */
48
+ as?: HeadingAs
49
+ /** Key facts beside the identity, typically `<DataList orientation="grid">`. */
50
+ details?: React.ReactNode
51
+ /** Actions such as Edit or Contact, aligned with the details. */
52
+ actions?: React.ReactNode
53
+ }
54
+
55
+ /**
56
+ * EntityHeader - the identity block for an organisation, brokerage or
57
+ * property: a logo (or initials) mark, the name as a heading, a secondary
58
+ * line, and slots for key details and actions. Stacks on small screens and
59
+ * sits in a row, bottom-aligned, from `lg` up.
60
+ *
61
+ * @example
62
+ * <EntityHeader
63
+ * name="Remax RealEstate"
64
+ * subtitle="Remax Real Estate Brokerage L.L.C."
65
+ * details={<DataList orientation="grid" columns={4} size="sm">…</DataList>}
66
+ * />
67
+ */
68
+ export const EntityHeader = React.forwardRef<HTMLDivElement, EntityHeaderProps>(
69
+ ({ className, name, subtitle, logo, as = "h2", size, details, actions, ...props }, ref) => {
70
+ const styles = entityHeaderVariants({ size })
71
+ return (
72
+ <div ref={ref} data-slot="entity-header" className={cn(styles.root(), className)} {...props}>
73
+ <div className={styles.identity()}>
74
+ <span className={styles.mark()} aria-hidden={logo ? undefined : true}>
75
+ {logo ? <img src={logo.src} alt={logo.alt} className="ax:size-full ax:object-contain" /> : entityInitials(name)}
76
+ </span>
77
+ <div className={styles.text()}>
78
+ <Heading as={as} size={NAME_SIZE[size ?? "lg"]} truncate>{name}</Heading>
79
+ {subtitle && <p className={styles.subtitle()}>{subtitle}</p>}
80
+ </div>
81
+ </div>
82
+ {(details || actions) && (
83
+ <div className={styles.aside()}>
84
+ {details}
85
+ {actions && <div className="ax:flex ax:flex-wrap ax:gap-xs">{actions}</div>}
86
+ </div>
87
+ )}
88
+ </div>
89
+ )
90
+ },
91
+ )
92
+ EntityHeader.displayName = "EntityHeader"
@@ -0,0 +1,25 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Sparkles, Shield, Zap } from "lucide-react"
4
+ import { FeatureGrid } from "./feature-grid"
5
+
6
+ const meta: Meta<typeof FeatureGrid> = {
7
+ title: "Components/FeatureGrid",
8
+ component: FeatureGrid,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ eyebrow: "Features",
12
+ title: "Everything you need to ship faster",
13
+ description: "Composable primitives with brand-aware styling.",
14
+ features: [
15
+ { icon: <Zap className="ax:size-5" />, title: "Fast", description: "Optimized for performance." },
16
+ { icon: <Shield className="ax:size-5" />, title: "Accessible", description: "Built on Radix primitives." },
17
+ { icon: <Sparkles className="ax:size-5" />, title: "Beautiful", description: "Polished defaults out of the box." },
18
+ ],
19
+ },
20
+ }
21
+
22
+ export default meta
23
+ type Story = StoryObj<typeof FeatureGrid>
24
+
25
+ export const Default: Story = {}
@@ -0,0 +1,87 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { motion } from "motion/react"
5
+ import { cn } from "../lib/cn"
6
+ import { useReducedMotion } from "../hooks/use-reduced-motion"
7
+ import { stagger, variants, viewport as defaultViewport } from "../lib/motion"
8
+
9
+ export interface Feature {
10
+ icon?: React.ReactNode
11
+ title: React.ReactNode
12
+ description: React.ReactNode
13
+ }
14
+
15
+ export interface FeatureGridProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
16
+ eyebrow?: React.ReactNode
17
+ title?: React.ReactNode
18
+ description?: React.ReactNode
19
+ features: Feature[]
20
+ /** Number of columns at the largest breakpoint. Default 3. */
21
+ columns?: 2 | 3 | 4
22
+ }
23
+
24
+ /**
25
+ * FeatureGrid - marketing feature showcase.
26
+ * Mobile-first: 1 column. Tablet: 2. Desktop: 3 (or 2/4 via prop).
27
+ */
28
+ export const FeatureGrid = React.forwardRef<HTMLElement, FeatureGridProps>(
29
+ ({ className, eyebrow, title, description, features, columns = 3, ...props }, ref) => {
30
+ const reduce = useReducedMotion()
31
+
32
+ const colClass =
33
+ columns === 2 ? "ax:md:grid-cols-2" : columns === 4 ? "ax:md:grid-cols-2 ax:lg:grid-cols-4" : "ax:md:grid-cols-2 ax:lg:grid-cols-3"
34
+
35
+ return (
36
+ <section ref={ref} className={cn("ax:w-full ax:px-xl ax:md:px-3xl ax:py-5xl ax:md:py-6xl", className)} {...props}>
37
+ <div className="ax:mx-auto ax:max-w-420">
38
+ {(eyebrow || title || description) && (
39
+ <div className="ax:max-w-3xl ax:mb-4xl ax:md:mb-5xl">
40
+ {eyebrow && (
41
+ <span className="ax:inline-block ax:t-overline ax:text-primary ax:mb-sm">
42
+ {eyebrow}
43
+ </span>
44
+ )}
45
+ {title && (
46
+ <h2 className="ax:t-display-md ax:md:t-display-lg ax:text-foreground ax:[text-wrap:balance]">
47
+ {title}
48
+ </h2>
49
+ )}
50
+ {description && (
51
+ <p className="ax:mt-md ax:t-body-md ax:md:t-body-lg ax:text-muted-foreground ax:[text-wrap:pretty]">
52
+ {description}
53
+ </p>
54
+ )}
55
+ </div>
56
+ )}
57
+ <motion.ul
58
+ className={cn("ax:grid ax:grid-cols-1 ax:gap-x-2xl ax:gap-y-3xl", colClass)}
59
+ {...(reduce ? {} : {
60
+ variants: stagger(),
61
+ initial: "hidden",
62
+ whileInView: "visible",
63
+ viewport: defaultViewport,
64
+ })}
65
+ >
66
+ {features.map((feature, i) => (
67
+ <motion.li
68
+ key={i}
69
+ {...(reduce ? {} : { variants: variants.fadeUp })}
70
+ className="ax:flex ax:flex-col ax:gap-sm"
71
+ >
72
+ {feature.icon && (
73
+ <div className="ax:size-3xl ax:rounded-md ax:bg-primary/10 ax:text-primary ax:flex ax:items-center ax:justify-center ax:[&>svg]:size-5">
74
+ {feature.icon}
75
+ </div>
76
+ )}
77
+ <h3 className="ax:t-h3 ax:text-foreground">{feature.title}</h3>
78
+ <p className="ax:t-body-sm ax:text-muted-foreground">{feature.description}</p>
79
+ </motion.li>
80
+ ))}
81
+ </motion.ul>
82
+ </div>
83
+ </section>
84
+ )
85
+ }
86
+ )
87
+ FeatureGrid.displayName = "FeatureGrid"
@@ -0,0 +1,25 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Sparkles } from "lucide-react"
4
+ import { FeedbackBar } from "./feedback-bar"
5
+
6
+ const meta: Meta<typeof FeedbackBar> = {
7
+ title: "Components/FeedbackBar",
8
+ component: FeedbackBar,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ title: "Was this helpful?",
12
+ onHelpful: () => undefined,
13
+ onNotHelpful: () => undefined,
14
+ onClose: () => undefined,
15
+ },
16
+ }
17
+
18
+ export default meta
19
+ type Story = StoryObj<typeof FeedbackBar>
20
+
21
+ export const Default: Story = {}
22
+
23
+ export const WithIcon: Story = { args: { icon: <Sparkles /> } }
24
+
25
+ export const WithoutClose: Story = { args: { onClose: undefined } }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { FeedbackBar } from "./feedback-bar"
5
+
6
+ describe("FeedbackBar", () => {
7
+ it("names the group by its title and labels both ratings", () => {
8
+ const html = renderToStaticMarkup(<FeedbackBar title="Was this helpful?" />)
9
+ expect(html).toContain('role="group"')
10
+ expect(html).toContain('aria-label="Was this helpful?"')
11
+ expect(html).toContain('aria-label="Helpful"')
12
+ expect(html).toContain('aria-label="Not helpful"')
13
+ })
14
+
15
+ it("shows the close button only when onClose is passed", () => {
16
+ expect(renderToStaticMarkup(<FeedbackBar title="Was this helpful?" />)).not.toContain('aria-label="Close"')
17
+ expect(renderToStaticMarkup(<FeedbackBar title="Was this helpful?" onClose={() => undefined} />)).toContain('aria-label="Close"')
18
+ })
19
+
20
+ it("hides the decorative icon from assistive tech", () => {
21
+ const html = renderToStaticMarkup(<FeedbackBar title="Was this helpful?" icon={<svg data-testid="logo" />} />)
22
+ expect(html).toMatch(/<span[^>]*aria-hidden="true"[^>]*><svg data-testid="logo"/)
23
+ })
24
+ })
@@ -0,0 +1,62 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ThumbsDown, ThumbsUp, X } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+ import { Button } from "./button"
7
+
8
+ export interface FeedbackBarProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ /** The question, e.g. "Was this helpful?". Also names the group. */
10
+ title: string
11
+ /** Optional mark before the title, such as the assistant's logo. */
12
+ icon?: React.ReactNode
13
+ onHelpful?: () => void
14
+ onNotHelpful?: () => void
15
+ /** Shows the close button. Hide the bar yourself when it fires. */
16
+ onClose?: () => void
17
+ }
18
+
19
+ /**
20
+ * FeedbackBar - a compact "Was this helpful?" prompt: a title, thumbs up and
21
+ * down, and an optional close. Use it once at the end of a conversation or
22
+ * page; for the actions under each answer use AnswerActions.
23
+ *
24
+ * @example
25
+ * <FeedbackBar title="Was this helpful?" onHelpful={up} onNotHelpful={down} onClose={hide} />
26
+ */
27
+ export const FeedbackBar = React.forwardRef<HTMLDivElement, FeedbackBarProps>(
28
+ ({ title, icon, onHelpful, onNotHelpful, onClose, className, ...props }, ref) => (
29
+ <div
30
+ ref={ref}
31
+ role="group"
32
+ aria-label={title}
33
+ data-slot="feedback-bar"
34
+ className={cn(
35
+ "ax:inline-flex ax:items-stretch ax:rounded-lg ax:border ax:border-border ax:bg-background ax:t-body-sm",
36
+ className,
37
+ )}
38
+ {...props}
39
+ >
40
+ <div className="ax:flex ax:flex-1 ax:items-center ax:gap-sm ax:py-sm ax:ps-md">
41
+ {icon && <span className="ax:inline-flex ax:shrink-0 ax:[&_svg]:size-4" aria-hidden>{icon}</span>}
42
+ <span className="ax:font-medium ax:text-foreground">{title}</span>
43
+ </div>
44
+ <div className="ax:flex ax:items-center ax:gap-2xs ax:px-sm">
45
+ <Button variant="ghost" size="icon-sm" className="ax:text-muted-foreground ax:hover:text-foreground" aria-label="Helpful" onClick={onHelpful}>
46
+ <ThumbsUp />
47
+ </Button>
48
+ <Button variant="ghost" size="icon-sm" className="ax:text-muted-foreground ax:hover:text-foreground" aria-label="Not helpful" onClick={onNotHelpful}>
49
+ <ThumbsDown />
50
+ </Button>
51
+ </div>
52
+ {onClose && (
53
+ <div className="ax:flex ax:items-center ax:border-s ax:border-border ax:px-2xs">
54
+ <Button variant="ghost" size="icon-sm" className="ax:text-muted-foreground ax:hover:text-foreground" aria-label="Close" onClick={onClose}>
55
+ <X />
56
+ </Button>
57
+ </div>
58
+ )}
59
+ </div>
60
+ ),
61
+ )
62
+ FeedbackBar.displayName = "FeedbackBar"