@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,252 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+ import { cn } from "../lib/cn"
7
+
8
+ // ─── Primitives (pass-through) ────────────────────────────────────────────────
9
+
10
+ export const DropdownMenu = DropdownMenuPrimitive.Root
11
+ export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12
+ export const DropdownMenuGroup = DropdownMenuPrimitive.Group
13
+ export const DropdownMenuPortal = DropdownMenuPrimitive.Portal
14
+ export const DropdownMenuSub = DropdownMenuPrimitive.Sub
15
+ export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
16
+
17
+ // ─── Shared row-state classes ─────────────────────────────────────────────────
18
+ //
19
+ // Radix exposes BOTH `:focus` (DOM focus) AND `data-highlighted` (keyboard nav
20
+ // highlight) for the active item. Listening for both ensures the highlight
21
+ // renders correctly regardless of whether the user is mousing or tabbing.
22
+
23
+ const ROW_BASE_DEFAULT =
24
+ "ax:text-foreground " +
25
+ "ax:focus:bg-muted ax:focus:text-foreground " +
26
+ "ax:data-[highlighted]:bg-muted ax:data-[highlighted]:text-foreground"
27
+
28
+ const ROW_BASE_DESTRUCTIVE =
29
+ "ax:text-destructive-strong " +
30
+ "ax:focus:bg-destructive/10 ax:focus:text-destructive-strong " +
31
+ "ax:data-[highlighted]:bg-destructive/10 ax:data-[highlighted]:text-destructive-strong " +
32
+ "ax:[&_svg]:text-destructive-strong"
33
+
34
+ // ─── Content ──────────────────────────────────────────────────────────────────
35
+
36
+ export interface DropdownMenuContentProps
37
+ extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> {}
38
+
39
+ export const DropdownMenuContent = React.forwardRef<
40
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
41
+ DropdownMenuContentProps
42
+ >(({ className, sideOffset = 6, ...props }, ref) => (
43
+ <DropdownMenuPrimitive.Portal>
44
+ <DropdownMenuPrimitive.Content
45
+ ref={ref}
46
+ sideOffset={sideOffset}
47
+ className={cn(
48
+ // Layered above Dialog/Drawer (z-[1100]/z-[1101]) so menus opened from
49
+ // within a drawer's footer (e.g. a mobile nav account card) stay visible.
50
+ "ax:z-1200 ax:min-w-[10rem] ax:overflow-hidden ax:rounded-(--input-radius) ax:border ax:border-border ax:bg-popover ax:p-2xs",
51
+ "ax:text-popover-foreground ax:shadow-md",
52
+ "ax:data-[state=open]:animate-in ax:data-[state=closed]:animate-out",
53
+ "ax:data-[state=closed]:fade-out-0 ax:data-[state=open]:fade-in-0",
54
+ "ax:data-[state=closed]:zoom-out-[0.96] ax:data-[state=open]:zoom-in-[0.96]",
55
+ "ax:data-[side=bottom]:slide-in-from-top-1",
56
+ "ax:data-[side=left]:slide-in-from-right-1",
57
+ "ax:data-[side=right]:slide-in-from-left-1",
58
+ "ax:data-[side=top]:slide-in-from-bottom-1",
59
+ "ax:[animation-duration:var(--motion-fast)] ax:[animation-timing-function:var(--motion-ease-out)]",
60
+ "ax:motion-reduce:animate-none",
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ </DropdownMenuPrimitive.Portal>
66
+ ))
67
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
68
+
69
+ // ─── Item ─────────────────────────────────────────────────────────────────────
70
+
71
+ export interface DropdownMenuItemProps
72
+ extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> {
73
+ /** Indents the item to align with siblings that have a checkbox/radio indicator column. */
74
+ inset?: boolean
75
+ /** Colour treatment. `destructive` tints text + icon + hover/highlight background. */
76
+ variant?: "default" | "destructive"
77
+ }
78
+
79
+ export const DropdownMenuItem = React.forwardRef<
80
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
81
+ DropdownMenuItemProps
82
+ >(({ className, inset, variant = "default", ...props }, ref) => (
83
+ <DropdownMenuPrimitive.Item
84
+ ref={ref}
85
+ className={cn(
86
+ "ax:relative ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:gap-xs ax:rounded-(--input-radius) ax:px-xs ax:py-1.5",
87
+ "ax:t-body-sm ax:outline-none ax:transition-colors",
88
+ "ax:data-[disabled]:pointer-events-none ax:data-[disabled]:opacity-50",
89
+ "ax:[&_svg]:size-4 ax:[&_svg]:shrink-0 ax:[&_svg]:text-muted-foreground",
90
+ variant === "destructive" ? ROW_BASE_DESTRUCTIVE : ROW_BASE_DEFAULT,
91
+ inset && "ax:pl-8",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
98
+
99
+ // ─── CheckboxItem ─────────────────────────────────────────────────────────────
100
+
101
+ export const DropdownMenuCheckboxItem = React.forwardRef<
102
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
103
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
104
+ >(({ className, children, checked, ...props }, ref) => (
105
+ <DropdownMenuPrimitive.CheckboxItem
106
+ ref={ref}
107
+ className={cn(
108
+ "ax:relative ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:rounded-(--input-radius) ax:py-1.5 ax:pl-8 ax:pr-xs",
109
+ "ax:t-body-sm ax:outline-none ax:transition-colors",
110
+ "ax:data-[disabled]:pointer-events-none ax:data-[disabled]:opacity-50",
111
+ ROW_BASE_DEFAULT,
112
+ className
113
+ )}
114
+ checked={checked}
115
+ {...props}
116
+ >
117
+ <span className="ax:absolute ax:left-2 ax:flex ax:size-3.5 ax:items-center ax:justify-center">
118
+ <DropdownMenuPrimitive.ItemIndicator>
119
+ <Check className="ax:size-4" />
120
+ </DropdownMenuPrimitive.ItemIndicator>
121
+ </span>
122
+ {children}
123
+ </DropdownMenuPrimitive.CheckboxItem>
124
+ ))
125
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
126
+
127
+ // ─── RadioItem ────────────────────────────────────────────────────────────────
128
+
129
+ export const DropdownMenuRadioItem = React.forwardRef<
130
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
131
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
132
+ >(({ className, children, ...props }, ref) => (
133
+ <DropdownMenuPrimitive.RadioItem
134
+ ref={ref}
135
+ className={cn(
136
+ "ax:relative ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:rounded-(--input-radius) ax:py-1.5 ax:pl-8 ax:pr-xs",
137
+ "ax:t-body-sm ax:outline-none ax:transition-colors",
138
+ "ax:data-[disabled]:pointer-events-none ax:data-[disabled]:opacity-50",
139
+ ROW_BASE_DEFAULT,
140
+ className
141
+ )}
142
+ {...props}
143
+ >
144
+ <span className="ax:absolute ax:left-2 ax:flex ax:size-3.5 ax:items-center ax:justify-center">
145
+ <DropdownMenuPrimitive.ItemIndicator>
146
+ <Circle className="ax:size-2 ax:fill-current" />
147
+ </DropdownMenuPrimitive.ItemIndicator>
148
+ </span>
149
+ {children}
150
+ </DropdownMenuPrimitive.RadioItem>
151
+ ))
152
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
153
+
154
+ // ─── Label ────────────────────────────────────────────────────────────────────
155
+
156
+ export const DropdownMenuLabel = React.forwardRef<
157
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
158
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }
159
+ >(({ className, inset, ...props }, ref) => (
160
+ <DropdownMenuPrimitive.Label
161
+ ref={ref}
162
+ className={cn(
163
+ "ax:px-xs ax:py-1.5 ax:t-overline ax:text-muted-foreground",
164
+ inset && "ax:pl-8",
165
+ className
166
+ )}
167
+ {...props}
168
+ />
169
+ ))
170
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
171
+
172
+ // ─── Separator ────────────────────────────────────────────────────────────────
173
+
174
+ export const DropdownMenuSeparator = React.forwardRef<
175
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
176
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
177
+ >(({ className, ...props }, ref) => (
178
+ <DropdownMenuPrimitive.Separator
179
+ ref={ref}
180
+ className={cn("ax:-mx-2xs ax:my-2xs ax:h-px ax:bg-border", className)}
181
+ {...props}
182
+ />
183
+ ))
184
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
185
+
186
+ // ─── SubTrigger / SubContent ─────────────────────────────────────────────────
187
+
188
+ export const DropdownMenuSubTrigger = React.forwardRef<
189
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
190
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }
191
+ >(({ className, inset, children, ...props }, ref) => (
192
+ <DropdownMenuPrimitive.SubTrigger
193
+ ref={ref}
194
+ className={cn(
195
+ "ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:gap-xs ax:rounded-(--input-radius) ax:px-xs ax:py-1.5",
196
+ "ax:t-body-sm ax:text-foreground ax:outline-none ax:transition-colors",
197
+ "ax:focus:bg-muted ax:data-[highlighted]:bg-muted ax:data-[state=open]:bg-muted",
198
+ "ax:[&_svg]:size-4 ax:[&_svg]:shrink-0 ax:[&_svg]:text-muted-foreground",
199
+ inset && "ax:pl-8",
200
+ className
201
+ )}
202
+ {...props}
203
+ >
204
+ {children}
205
+ <ChevronRight className="ax:ml-auto ax:size-4" />
206
+ </DropdownMenuPrimitive.SubTrigger>
207
+ ))
208
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
209
+
210
+ export const DropdownMenuSubContent = React.forwardRef<
211
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
212
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
213
+ >(({ className, ...props }, ref) => (
214
+ <DropdownMenuPrimitive.SubContent
215
+ ref={ref}
216
+ className={cn(
217
+ // See DropdownMenuContent, layered above Dialog/Drawer.
218
+ "ax:z-1200 ax:min-w-[10rem] ax:overflow-hidden ax:rounded-(--input-radius) ax:border ax:border-border ax:bg-popover ax:p-2xs",
219
+ "ax:text-popover-foreground ax:shadow-lg",
220
+ "ax:data-[state=open]:animate-in ax:data-[state=closed]:animate-out",
221
+ "ax:data-[state=closed]:fade-out-0 ax:data-[state=open]:fade-in-0",
222
+ "ax:data-[state=closed]:zoom-out-[0.96] ax:data-[state=open]:zoom-in-[0.96]",
223
+ // Side-aware slide-ins so sub-menus opening left vs right feel directional
224
+ // (mirrors the Content animation set above).
225
+ "ax:data-[side=bottom]:slide-in-from-top-1",
226
+ "ax:data-[side=left]:slide-in-from-right-1",
227
+ "ax:data-[side=right]:slide-in-from-left-1",
228
+ "ax:data-[side=top]:slide-in-from-bottom-1",
229
+ "ax:[animation-duration:var(--motion-fast)] ax:[animation-timing-function:var(--motion-ease-out)]",
230
+ "ax:motion-reduce:animate-none",
231
+ className
232
+ )}
233
+ {...props}
234
+ />
235
+ ))
236
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
237
+
238
+ // ─── Shortcut ─────────────────────────────────────────────────────────────────
239
+ //
240
+ // Right-aligned keyboard-shortcut hint for use inside DropdownMenuItem etc.
241
+ // Visually-only. Wire the actual shortcut keys via a global handler elsewhere.
242
+
243
+ export const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
244
+ <span
245
+ className={cn(
246
+ "ax:ml-auto ax:pl-md ax:t-caption ax:text-muted-foreground ax:tracking-wider",
247
+ className
248
+ )}
249
+ {...props}
250
+ />
251
+ )
252
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
@@ -0,0 +1,19 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Dropzone } from "./dropzone"
4
+
5
+ const meta: Meta<typeof Dropzone> = {
6
+ title: "Components/Dropzone",
7
+ component: Dropzone,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ onFile: () => {},
11
+ accept: ".csv",
12
+ prompt: "Drop a CSV file here, or browse",
13
+ },
14
+ }
15
+
16
+ export default meta
17
+ type Story = StoryObj<typeof Dropzone>
18
+
19
+ export const Default: Story = {}
@@ -0,0 +1,250 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { FileText, UploadCloud } from "lucide-react"
5
+ import { Button } from "./button"
6
+ import { Spinner } from "./spinner"
7
+ import { cn } from "../lib/cn"
8
+
9
+ export type DropzoneErrorReason = "invalid-type" | "too-large"
10
+
11
+ export interface DropzoneError {
12
+ reason: DropzoneErrorReason
13
+ file: File
14
+ message: string
15
+ }
16
+
17
+ // `onError` is our domain callback (file-rejection event); the HTMLAttributes
18
+ // `onError` (image-style ReactEventHandler) is omitted to keep the type clean.
19
+ export interface DropzoneProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onError"> {
20
+ /** Called when a file is dropped or selected via the file picker AND
21
+ * passes type / size validation. */
22
+ onFile: (file: File) => void
23
+ /** Called when a dropped or picked file fails validation (wrong type,
24
+ * exceeds `maxSize`). The file is NOT forwarded to `onFile`. */
25
+ onError?: (error: DropzoneError) => void
26
+ /** Currently-selected file. When set, the dropzone collapses to a
27
+ * selected-file row with a "Replace" button. */
28
+ file?: File | null
29
+ /** While true, the dropzone shows a spinner + "Processing…" instead of
30
+ * the upload affordances. Drive this from the parent's upload state. */
31
+ processing?: boolean
32
+ /** Disables the drop target and Browse button. */
33
+ disabled?: boolean
34
+ /** Accept attribute passed through to the underlying `<input>` (e.g.
35
+ * `".csv"`, `"image/*"`). Also enforced on drag-and-drop - browsers
36
+ * don't natively validate `accept` for drops. */
37
+ accept?: string
38
+ /** Maximum file size in bytes. Files exceeding this trigger `onError`
39
+ * rather than `onFile`. */
40
+ maxSize?: number
41
+ /** Localizable prompt text. Defaults to a CSV-flavored prompt. */
42
+ prompt?: string
43
+ /** Optional helper link rendered under the Browse button. */
44
+ templateHref?: string
45
+ templateLabel?: string
46
+ }
47
+
48
+ /** File dropzone with three states: empty (drag-or-browse prompt), selected
49
+ * (file row + replace), and processing (spinner). The component is fully
50
+ * controlled: the parent owns the file + processing state and decides
51
+ * what to do when `onFile` fires. */
52
+
53
+ // Matches a file against an accept attribute string. Returns true if the
54
+ // file's extension or MIME type satisfies any of the comma-separated entries.
55
+ function fileMatchesAccept(file: File, accept: string): boolean {
56
+ if (!accept) return true
57
+ const lowerName = file.name.toLowerCase()
58
+ const lowerType = file.type.toLowerCase()
59
+ return accept
60
+ .split(",")
61
+ .map((s) => s.trim().toLowerCase())
62
+ .filter(Boolean)
63
+ .some((entry) => {
64
+ if (entry.startsWith(".")) return lowerName.endsWith(entry)
65
+ if (entry.endsWith("/*")) return lowerType.startsWith(entry.slice(0, -1))
66
+ return lowerType === entry
67
+ })
68
+ }
69
+
70
+ function formatBytes(n: number): string {
71
+ if (n < 1024) return `${n} B`
72
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
73
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`
74
+ }
75
+
76
+ export const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
77
+ (
78
+ {
79
+ onFile,
80
+ onError,
81
+ file,
82
+ processing,
83
+ disabled = false,
84
+ accept = ".csv",
85
+ maxSize,
86
+ prompt = "Drag & drop your CSV file here",
87
+ templateHref,
88
+ templateLabel = "Download Template",
89
+ className,
90
+ ...props
91
+ },
92
+ ref,
93
+ ) => {
94
+ const inputRef = React.useRef<HTMLInputElement>(null)
95
+ // Counter (not boolean) so flicker over child elements (icon, text)
96
+ // doesn't toggle the highlight on/off as drag enters/leaves descendants.
97
+ const dragCount = React.useRef(0)
98
+ const [dragging, setDragging] = React.useState(false)
99
+
100
+ function validateAndForward(candidate: File) {
101
+ if (!fileMatchesAccept(candidate, accept)) {
102
+ onError?.({
103
+ reason: "invalid-type",
104
+ file: candidate,
105
+ message: `File type not allowed (expected ${accept}).`,
106
+ })
107
+ return
108
+ }
109
+ if (maxSize !== undefined && candidate.size > maxSize) {
110
+ onError?.({
111
+ reason: "too-large",
112
+ file: candidate,
113
+ message: `File exceeds the ${formatBytes(maxSize)} limit.`,
114
+ })
115
+ return
116
+ }
117
+ onFile(candidate)
118
+ }
119
+
120
+ function handleDragEnter(e: React.DragEvent<HTMLDivElement>) {
121
+ if (disabled) return
122
+ e.preventDefault()
123
+ dragCount.current++
124
+ setDragging(true)
125
+ }
126
+
127
+ function handleDragLeave(e: React.DragEvent<HTMLDivElement>) {
128
+ if (disabled) return
129
+ e.preventDefault()
130
+ dragCount.current = Math.max(0, dragCount.current - 1)
131
+ if (dragCount.current === 0) setDragging(false)
132
+ }
133
+
134
+ function handleDrop(e: React.DragEvent<HTMLDivElement>) {
135
+ if (disabled) return
136
+ e.preventDefault()
137
+ dragCount.current = 0
138
+ setDragging(false)
139
+ const dropped = e.dataTransfer.files[0]
140
+ if (dropped) validateAndForward(dropped)
141
+ }
142
+
143
+ function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
144
+ const selected = e.target.files?.[0]
145
+ if (selected) validateAndForward(selected)
146
+ // Reset so picking the same file twice still fires onChange.
147
+ e.target.value = ""
148
+ }
149
+
150
+ if (processing) {
151
+ return (
152
+ <div
153
+ ref={ref}
154
+ className={cn("ax:flex ax:flex-col ax:items-center ax:gap-md ax:py-2xl", className)}
155
+ {...props}
156
+ >
157
+ <Spinner />
158
+ <p className="ax:t-body-sm ax:text-muted-foreground">Processing…</p>
159
+ </div>
160
+ )
161
+ }
162
+
163
+ if (file) {
164
+ return (
165
+ <div
166
+ ref={ref}
167
+ className={cn(
168
+ "ax:flex ax:items-center ax:gap-sm ax:p-md ax:rounded-md ax:border ax:border-border ax:bg-muted/30",
169
+ disabled && "ax:opacity-60 ax:cursor-not-allowed",
170
+ className,
171
+ )}
172
+ {...props}
173
+ >
174
+ <FileText className="ax:size-5 ax:text-primary ax:shrink-0" />
175
+ <div className="ax:flex-1 ax:min-w-0">
176
+ <p className="ax:t-label-sm ax:text-foreground ax:truncate">{file.name}</p>
177
+ <p className="ax:t-caption ax:text-muted-foreground">{formatBytes(file.size)}</p>
178
+ </div>
179
+ <Button
180
+ variant="ghost"
181
+ size="sm"
182
+ disabled={disabled}
183
+ onClick={() => inputRef.current?.click()}
184
+ >
185
+ Replace
186
+ </Button>
187
+ <input
188
+ ref={inputRef}
189
+ type="file"
190
+ accept={accept}
191
+ disabled={disabled}
192
+ className="ax:hidden"
193
+ onChange={handleChange}
194
+ />
195
+ </div>
196
+ )
197
+ }
198
+
199
+ return (
200
+ <div
201
+ ref={ref}
202
+ className={cn(
203
+ "ax:flex ax:flex-col ax:items-center ax:gap-md ax:py-2xl ax:px-xl",
204
+ "ax:border-2 ax:border-dashed ax:rounded-md ax:border-border",
205
+ "ax:transition-colors",
206
+ dragging && !disabled && "ax:border-primary ax:bg-primary/5",
207
+ disabled && "ax:opacity-60 ax:cursor-not-allowed ax:bg-muted/30",
208
+ className,
209
+ )}
210
+ onDragOver={(e) => { if (!disabled) e.preventDefault() }}
211
+ onDragEnter={handleDragEnter}
212
+ onDragLeave={handleDragLeave}
213
+ onDrop={handleDrop}
214
+ {...props}
215
+ >
216
+ <UploadCloud className="ax:size-3xl ax:text-muted-foreground" />
217
+ <div className="ax:text-center">
218
+ <p className="ax:t-body-sm ax:text-foreground ax:font-medium">{prompt}</p>
219
+ <p className="ax:t-caption ax:text-muted-foreground ax:mt-1">or</p>
220
+ </div>
221
+
222
+ <div className="ax:flex ax:flex-col ax:items-center ax:gap-xs">
223
+ <Button
224
+ variant="outline"
225
+ size="sm"
226
+ disabled={disabled}
227
+ onClick={() => inputRef.current?.click()}
228
+ >
229
+ Browse File
230
+ </Button>
231
+ {templateHref && (
232
+ <a href={templateHref} className="ax:t-caption ax:text-primary ax:hover:underline ax:focus-visible:underline">
233
+ {templateLabel}
234
+ </a>
235
+ )}
236
+ </div>
237
+
238
+ <input
239
+ ref={inputRef}
240
+ type="file"
241
+ accept={accept}
242
+ disabled={disabled}
243
+ className="ax:hidden"
244
+ onChange={handleChange}
245
+ />
246
+ </div>
247
+ )
248
+ },
249
+ )
250
+ Dropzone.displayName = "Dropzone"
@@ -0,0 +1,135 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import {
4
+ EmailChipsInput,
5
+ type EmailChip,
6
+ type EmailChipsInputProps,
7
+ } from "./email-chips-input"
8
+
9
+ const meta: Meta<typeof EmailChipsInput> = {
10
+ title: "Components/EmailChipsInput",
11
+ component: EmailChipsInput,
12
+ tags: ["autodocs"],
13
+ parameters: {
14
+ layout: "centered",
15
+ docs: {
16
+ description: {
17
+ component:
18
+ "Multi-email input that turns each accepted entry into a chip. Adds on Enter or `,`. Backspace on an empty input removes the last chip. Pasting a `, ; \\n \\t`-separated list adds each address as a chip.\n\nValidation is local (regex-based) and non-blocking: invalid chips render in a destructive tint with a tooltip explaining why. Consumers filter `chip.valid` before submitting.",
19
+ },
20
+ },
21
+ },
22
+ argTypes: {
23
+ chips: { control: false },
24
+ onChange: { control: false },
25
+ onDuplicate: { control: false },
26
+ },
27
+ args: {
28
+ placeholder: "Add email addresses…",
29
+ variant: "default",
30
+ disabled: false,
31
+ invalid: false,
32
+ },
33
+ }
34
+
35
+ export default meta
36
+ type Story = StoryObj<typeof EmailChipsInput>
37
+
38
+ function EmailChipsInputDemo({
39
+ initialChips = [],
40
+ ...props
41
+ }: Omit<EmailChipsInputProps, "chips" | "onChange"> & {
42
+ initialChips?: EmailChip[]
43
+ }) {
44
+ const [chips, setChips] = React.useState<EmailChip[]>(initialChips)
45
+ const validCount = chips.filter((c) => c.valid).length
46
+
47
+ return (
48
+ <div className="ax:w-full ax:min-w-[20rem] ax:max-w-md ax:space-y-2">
49
+ <EmailChipsInput chips={chips} onChange={setChips} {...props} />
50
+ <p className="ax:t-caption ax:text-muted-foreground">
51
+ {validCount} valid · {chips.length - validCount} invalid · press Enter or comma to add
52
+ </p>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ export const Default: Story = {
58
+ render: (args) => <EmailChipsInputDemo {...args} />,
59
+ }
60
+
61
+ export const WithValues: Story = {
62
+ render: (args) => (
63
+ <EmailChipsInputDemo
64
+ {...args}
65
+ initialChips={[
66
+ { value: "alex@example.com", valid: true },
67
+ { value: "sam@example.com", valid: true },
68
+ ]}
69
+ />
70
+ ),
71
+ }
72
+
73
+ export const WithInvalid: Story = {
74
+ render: (args) => (
75
+ <EmailChipsInputDemo
76
+ {...args}
77
+ initialChips={[
78
+ { value: "yara@adres.ae", valid: true },
79
+ { value: "not-an-email", valid: false },
80
+ ]}
81
+ />
82
+ ),
83
+ }
84
+
85
+ export const Primary: Story = {
86
+ render: (args) => (
87
+ <EmailChipsInputDemo
88
+ {...args}
89
+ variant="primary"
90
+ initialChips={[
91
+ { value: "yara@adres.ae", valid: true },
92
+ { value: "ali@adres.ae", valid: true },
93
+ ]}
94
+ />
95
+ ),
96
+ }
97
+
98
+ export const Destructive: Story = {
99
+ render: (args) => (
100
+ <EmailChipsInputDemo
101
+ {...args}
102
+ variant="destructive"
103
+ initialChips={[
104
+ { value: "revoke@example.com", valid: true },
105
+ { value: "bad-address", valid: false },
106
+ ]}
107
+ />
108
+ ),
109
+ }
110
+
111
+ export const Invalid: Story = {
112
+ render: (args) => (
113
+ <EmailChipsInputDemo
114
+ {...args}
115
+ invalid
116
+ initialChips={[
117
+ { value: "yara@adres.ae", valid: true },
118
+ { value: "not-an-email", valid: false },
119
+ ]}
120
+ />
121
+ ),
122
+ }
123
+
124
+ export const Disabled: Story = {
125
+ render: (args) => (
126
+ <EmailChipsInputDemo
127
+ {...args}
128
+ disabled
129
+ initialChips={[
130
+ { value: "yara@adres.ae", valid: true },
131
+ { value: "ali@adres.ae", valid: true },
132
+ ]}
133
+ />
134
+ ),
135
+ }