@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,48 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Button } from "./button"
4
+ import {
5
+ Drawer,
6
+ DrawerBody,
7
+ DrawerClose,
8
+ DrawerContent,
9
+ DrawerDescription,
10
+ DrawerFooter,
11
+ DrawerHeader,
12
+ DrawerTitle,
13
+ DrawerTrigger,
14
+ } from "./drawer"
15
+
16
+ const meta: Meta<typeof Drawer> = {
17
+ title: "Components/Drawer",
18
+ component: Drawer,
19
+ tags: ["autodocs"],
20
+ }
21
+
22
+ export default meta
23
+ type Story = StoryObj<typeof Drawer>
24
+
25
+ export const Default: Story = {
26
+ render: () => (
27
+ <Drawer>
28
+ <DrawerTrigger asChild>
29
+ <Button>Open drawer</Button>
30
+ </DrawerTrigger>
31
+ <DrawerContent side="right">
32
+ <DrawerHeader>
33
+ <DrawerTitle>Filters</DrawerTitle>
34
+ <DrawerDescription>Refine your search results.</DrawerDescription>
35
+ </DrawerHeader>
36
+ <DrawerBody>
37
+ <p className="ax:t-body-sm ax:text-muted-foreground">Drawer content goes here.</p>
38
+ </DrawerBody>
39
+ <DrawerFooter>
40
+ <DrawerClose asChild>
41
+ <Button variant="outline">Close</Button>
42
+ </DrawerClose>
43
+ <Button>Apply</Button>
44
+ </DrawerFooter>
45
+ </DrawerContent>
46
+ </Drawer>
47
+ ),
48
+ }
@@ -0,0 +1,385 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { motion, AnimatePresence, type PanInfo } from "motion/react"
6
+ import { X } from "lucide-react"
7
+ import { cn } from "../lib/cn"
8
+ import { useReducedMotion } from "../hooks/use-reduced-motion"
9
+ import { transitions } from "../lib/motion"
10
+
11
+ // ─── Types ────────────────────────────────────────────────────────────────────
12
+
13
+ export type DrawerSide = "right" | "bottom" | "left" | "top"
14
+ export type DrawerSize = "sm" | "md" | "lg"
15
+
16
+ const DrawerSizeContext = React.createContext<DrawerSize>("md")
17
+
18
+ // Tracks open state and an imperative setter so DrawerContent can drive
19
+ // AnimatePresence independently of Radix's internal presence system, AND
20
+ // can dismiss itself in response to drag-past-threshold gestures.
21
+ interface DrawerOpenContextValue {
22
+ open: boolean
23
+ setOpen: (open: boolean) => void
24
+ }
25
+ const DrawerOpenContext = React.createContext<DrawerOpenContextValue>({
26
+ open: false,
27
+ setOpen: () => {},
28
+ })
29
+
30
+ // ─── Size tokens ──────────────────────────────────────────────────────────────
31
+
32
+ // Left/right share the same width scale; top/bottom share heights.
33
+ const SIDE_WIDTH: Record<DrawerSize, string> = {
34
+ sm: "ax:w-80", // 320px - filters, quick actions
35
+ md: "ax:w-[480px]", // 480px - detail panels
36
+ lg: "ax:w-1/2", // 50vw - wide layouts
37
+ }
38
+
39
+ const END_HEIGHT: Record<DrawerSize, string> = {
40
+ sm: "ax:max-h-[40vh]",
41
+ md: "ax:max-h-[60vh]",
42
+ lg: "ax:max-h-[80vh]",
43
+ }
44
+
45
+ const TITLE_SIZE: Record<DrawerSize, string> = {
46
+ sm: "ax:t-h5",
47
+ md: "ax:t-h4",
48
+ lg: "ax:t-h3",
49
+ }
50
+
51
+ const DESCRIPTION_SIZE: Record<DrawerSize, string> = {
52
+ sm: "ax:t-caption",
53
+ md: "ax:t-body-sm",
54
+ lg: "ax:t-body-sm",
55
+ }
56
+
57
+ // ─── Animation variants per side ─────────────────────────────────────────────
58
+ //
59
+ // All timings come from the design-system motion tokens (lib/motion.ts):
60
+ // transitions.slow - enter (420ms, ease-out): large surface arriving
61
+ // transitions.exitSlow - exit (280ms, ease-in): large surface leaving
62
+ // transitions.smooth - backdrop enter
63
+ // transitions.exit - backdrop exit
64
+ //
65
+ // Backdrop fades faster than the panel slides: it recedes before the
66
+ // panel finishes leaving.
67
+
68
+ const SLIDE = {
69
+ right: {
70
+ initial: { x: "100%" },
71
+ animate: { x: 0, transition: transitions.slow },
72
+ exit: { x: "100%", transition: transitions.exitSlow },
73
+ },
74
+ left: {
75
+ initial: { x: "-100%" },
76
+ animate: { x: 0, transition: transitions.slow },
77
+ exit: { x: "-100%", transition: transitions.exitSlow },
78
+ },
79
+ bottom: {
80
+ initial: { y: "100%" },
81
+ animate: { y: 0, transition: transitions.slow },
82
+ exit: { y: "100%", transition: transitions.exitSlow },
83
+ },
84
+ top: {
85
+ initial: { y: "-100%" },
86
+ animate: { y: 0, transition: transitions.slow },
87
+ exit: { y: "-100%", transition: transitions.exitSlow },
88
+ },
89
+ } as const
90
+
91
+ // ─── Drag-to-close config per side ────────────────────────────────────────────
92
+ //
93
+ // All four sides support drag-to-close so touch users can swipe the panel
94
+ // off-screen in its open direction. Past a threshold (offset OR velocity),
95
+ // the drawer dismisses; below it, Framer's spring physics return the panel
96
+ // to its open position automatically.
97
+
98
+ const DRAG_OFFSET_THRESHOLD = 100 // px past which release commits a dismiss
99
+ const DRAG_VELOCITY_THRESHOLD = 500 // px/sec flick velocity that always commits
100
+
101
+ interface DragConfig {
102
+ drag: "x" | "y"
103
+ dragConstraints: { top: number; bottom: number; left: number; right: number }
104
+ /** Per-edge elasticity: 0 means "can't drag past this edge"; non-zero
105
+ * allows resistive over-drag in that direction (the close direction). */
106
+ dragElastic: { top: number; bottom: number; left: number; right: number }
107
+ /** Returns true when offset/velocity warrant dismissing. */
108
+ shouldClose: (info: PanInfo) => boolean
109
+ }
110
+
111
+ const DRAG_BY_SIDE: Record<DrawerSide, DragConfig> = {
112
+ right: {
113
+ drag: "x",
114
+ dragConstraints: { top: 0, bottom: 0, left: 0, right: 0 },
115
+ dragElastic: { top: 0, bottom: 0, left: 0, right: 0.4 },
116
+ shouldClose: ({ offset, velocity }) =>
117
+ offset.x > DRAG_OFFSET_THRESHOLD || velocity.x > DRAG_VELOCITY_THRESHOLD,
118
+ },
119
+ left: {
120
+ drag: "x",
121
+ dragConstraints: { top: 0, bottom: 0, left: 0, right: 0 },
122
+ dragElastic: { top: 0, bottom: 0, left: 0.4, right: 0 },
123
+ shouldClose: ({ offset, velocity }) =>
124
+ offset.x < -DRAG_OFFSET_THRESHOLD || velocity.x < -DRAG_VELOCITY_THRESHOLD,
125
+ },
126
+ bottom: {
127
+ drag: "y",
128
+ dragConstraints: { top: 0, bottom: 0, left: 0, right: 0 },
129
+ dragElastic: { top: 0, bottom: 0.4, left: 0, right: 0 },
130
+ shouldClose: ({ offset, velocity }) =>
131
+ offset.y > DRAG_OFFSET_THRESHOLD || velocity.y > DRAG_VELOCITY_THRESHOLD,
132
+ },
133
+ top: {
134
+ drag: "y",
135
+ dragConstraints: { top: 0, bottom: 0, left: 0, right: 0 },
136
+ dragElastic: { top: 0.4, bottom: 0, left: 0, right: 0 },
137
+ shouldClose: ({ offset, velocity }) =>
138
+ offset.y < -DRAG_OFFSET_THRESHOLD || velocity.y < -DRAG_VELOCITY_THRESHOLD,
139
+ },
140
+ }
141
+
142
+ // ─── Root ─────────────────────────────────────────────────────────────────────
143
+
144
+ // Wraps Radix Dialog.Root and mirrors the open state into DrawerOpenContext so
145
+ // DrawerContent can use AnimatePresence without depending on Radix's internal
146
+ // presence system (which conflicts with motion/react exit animations).
147
+ export interface DrawerProps
148
+ extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {}
149
+
150
+ export function Drawer({
151
+ open: controlledOpen,
152
+ onOpenChange,
153
+ defaultOpen,
154
+ children,
155
+ ...props
156
+ }: DrawerProps) {
157
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false)
158
+ const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen
159
+
160
+ React.useEffect(() => {
161
+ if (!isOpen) return
162
+ const prev = document.body.style.overflow
163
+ document.body.style.overflow = "hidden"
164
+ return () => { document.body.style.overflow = prev }
165
+ }, [isOpen])
166
+
167
+ const setOpen = React.useCallback(
168
+ (v: boolean) => { setInternalOpen(v); onOpenChange?.(v) },
169
+ [onOpenChange]
170
+ )
171
+
172
+ const ctx = React.useMemo<DrawerOpenContextValue>(
173
+ () => ({ open: isOpen, setOpen }),
174
+ [isOpen, setOpen]
175
+ )
176
+
177
+ return (
178
+ <DrawerOpenContext.Provider value={ctx}>
179
+ <DialogPrimitive.Root open={isOpen} onOpenChange={setOpen} {...props}>
180
+ {children}
181
+ </DialogPrimitive.Root>
182
+ </DrawerOpenContext.Provider>
183
+ )
184
+ }
185
+
186
+ // ─── Trigger / Close (pass-through) ──────────────────────────────────────────
187
+
188
+ export const DrawerTrigger = DialogPrimitive.Trigger
189
+ export const DrawerClose = DialogPrimitive.Close
190
+
191
+ // ─── Content ─────────────────────────────────────────────────────────────────
192
+
193
+ export interface DrawerContentProps
194
+ extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
195
+ side?: DrawerSide
196
+ size?: DrawerSize
197
+ /** Show the translucent backdrop. Default: true. */
198
+ backdrop?: boolean
199
+ }
200
+
201
+ export const DrawerContent = React.forwardRef<
202
+ React.ElementRef<typeof DialogPrimitive.Content>,
203
+ DrawerContentProps
204
+ >(({ className, children, side = "right", size = "md", backdrop = true, ...props }, ref) => {
205
+ const { open: isOpen, setOpen } = React.useContext(DrawerOpenContext)
206
+ const reduce = useReducedMotion()
207
+ const slide = SLIDE[side]
208
+ const drag = DRAG_BY_SIDE[side]
209
+
210
+ // Under reduced-motion collapse all transitions to instant and disable drag.
211
+ const panelVariants = reduce ? { initial: {}, animate: {}, exit: {} } : slide
212
+ const backdropEnter = reduce ? { duration: 0 } : transitions.smooth
213
+ const backdropExit = reduce ? { duration: 0 } : transitions.exit
214
+ const dragProps = reduce
215
+ ? {}
216
+ : {
217
+ drag: drag.drag,
218
+ dragConstraints: drag.dragConstraints,
219
+ dragElastic: drag.dragElastic,
220
+ onDragEnd: (_: PointerEvent, info: PanInfo) => {
221
+ if (drag.shouldClose(info)) setOpen(false)
222
+ },
223
+ }
224
+
225
+ // Drag handle is only shown for bottom/top: the recognised mobile-sheet
226
+ // affordance. Left/right still support drag-to-close functionally, but the
227
+ // expected gesture on side panels is the edge-swipe rather than a handle.
228
+ const showDragHandle = !reduce && (side === "bottom" || side === "top")
229
+
230
+ return (
231
+ <DrawerSizeContext.Provider value={size}>
232
+ {/*
233
+ * forceMount on Portal bypasses Radix's own presence/animation system so
234
+ * motion/react AnimatePresence is the sole controller of mount/unmount.
235
+ */}
236
+ <DialogPrimitive.Portal forceMount>
237
+
238
+ {/* Backdrop - independent AnimatePresence so it fades separately from the panel */}
239
+ <AnimatePresence>
240
+ {isOpen && backdrop && (
241
+ <motion.div
242
+ key="drawer-backdrop"
243
+ // Black scrim in dark mode (`--foreground` flips light there); same opacity + blur.
244
+ className="ax:fixed ax:inset-0 ax:z-[1100] ax:bg-foreground/40 ax:dark:bg-black/40 ax:backdrop-blur-sm"
245
+ initial={{ opacity: 0 }}
246
+ animate={{ opacity: 1, transition: backdropEnter }}
247
+ exit={{ opacity: 0, transition: backdropExit }}
248
+ aria-hidden="true"
249
+ />
250
+ )}
251
+ </AnimatePresence>
252
+
253
+ {/* Panel - separate AnimatePresence so enter/exit curves are fully independent */}
254
+ <AnimatePresence>
255
+ {isOpen && (
256
+ <DialogPrimitive.Content ref={ref} forceMount asChild {...props}>
257
+ <motion.div
258
+ className={cn(
259
+ "ax:fixed ax:z-[1101] ax:flex ax:flex-col ax:bg-card ax:shadow-xl",
260
+ side === "right" && ["ax:right-0 ax:top-0 ax:h-full ax:border-l ax:border-border", SIDE_WIDTH[size]],
261
+ side === "left" && ["ax:left-0 ax:top-0 ax:h-full ax:border-r ax:border-border", SIDE_WIDTH[size]],
262
+ side === "bottom" && ["ax:bottom-0 ax:left-0 ax:right-0 ax:rounded-t-xl ax:border-t ax:border-border", END_HEIGHT[size]],
263
+ side === "top" && ["ax:top-0 ax:left-0 ax:right-0 ax:rounded-b-xl ax:border-b ax:border-border", END_HEIGHT[size]],
264
+ className
265
+ )}
266
+ initial={panelVariants.initial}
267
+ animate={panelVariants.animate}
268
+ exit={panelVariants.exit}
269
+ {...dragProps}
270
+ >
271
+ {/* Drag handle affordance - bottom/top only */}
272
+ {showDragHandle && (
273
+ <div
274
+ aria-hidden="true"
275
+ className={cn(
276
+ "ax:absolute ax:left-1/2 ax:-translate-x-1/2 ax:h-1 ax:w-10 ax:rounded-full ax:bg-foreground/20",
277
+ side === "bottom" && "ax:top-2",
278
+ side === "top" && "ax:bottom-2",
279
+ )}
280
+ />
281
+ )}
282
+ <DialogPrimitive.Close
283
+ className={cn(
284
+ "ax:absolute ax:right-md ax:top-md ax:z-10 ax:rounded-sm ax:opacity-70 ax:transition-opacity",
285
+ "ax:hover:opacity-100 ax:focus:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-2",
286
+ "ax:disabled:pointer-events-none"
287
+ )}
288
+ >
289
+ <X className="ax:size-4" aria-hidden="true" />
290
+ <span className="ax:sr-only">Close</span>
291
+ </DialogPrimitive.Close>
292
+ {children}
293
+ </motion.div>
294
+ </DialogPrimitive.Content>
295
+ )}
296
+ </AnimatePresence>
297
+
298
+ </DialogPrimitive.Portal>
299
+ </DrawerSizeContext.Provider>
300
+ )
301
+ })
302
+ DrawerContent.displayName = "DrawerContent"
303
+
304
+ // ─── Header ───────────────────────────────────────────────────────────────────
305
+
306
+ export const DrawerHeader = React.forwardRef<
307
+ HTMLDivElement,
308
+ React.HTMLAttributes<HTMLDivElement>
309
+ >(({ className, ...props }, ref) => (
310
+ <div
311
+ ref={ref}
312
+ className={cn(
313
+ // pr-4xl (48px) reserves space for the absolute-positioned close button.
314
+ "ax:flex ax:flex-col ax:gap-2xs ax:px-xl ax:pt-xl ax:pb-md ax:pr-4xl ax:shrink-0 ax:border-b ax:border-border",
315
+ className
316
+ )}
317
+ {...props}
318
+ />
319
+ ))
320
+ DrawerHeader.displayName = "DrawerHeader"
321
+
322
+ // ─── Body (scrollable content area) ──────────────────────────────────────────
323
+
324
+ export const DrawerBody = React.forwardRef<
325
+ HTMLDivElement,
326
+ React.HTMLAttributes<HTMLDivElement>
327
+ >(({ className, ...props }, ref) => (
328
+ <div
329
+ ref={ref}
330
+ className={cn("ax:flex-1 ax:overflow-y-auto ax:px-xl ax:py-lg", className)}
331
+ {...props}
332
+ />
333
+ ))
334
+ DrawerBody.displayName = "DrawerBody"
335
+
336
+ // ─── Footer ───────────────────────────────────────────────────────────────────
337
+
338
+ export const DrawerFooter = React.forwardRef<
339
+ HTMLDivElement,
340
+ React.HTMLAttributes<HTMLDivElement>
341
+ >(({ className, ...props }, ref) => (
342
+ <div
343
+ ref={ref}
344
+ className={cn(
345
+ "ax:flex ax:items-center ax:justify-end ax:gap-xs ax:px-xl ax:py-md ax:shrink-0 ax:border-t ax:border-border",
346
+ className
347
+ )}
348
+ {...props}
349
+ />
350
+ ))
351
+ DrawerFooter.displayName = "DrawerFooter"
352
+
353
+ // ─── Title ────────────────────────────────────────────────────────────────────
354
+
355
+ export const DrawerTitle = React.forwardRef<
356
+ React.ElementRef<typeof DialogPrimitive.Title>,
357
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
358
+ >(({ className, ...props }, ref) => {
359
+ const size = React.useContext(DrawerSizeContext)
360
+ return (
361
+ <DialogPrimitive.Title
362
+ ref={ref}
363
+ className={cn(TITLE_SIZE[size], className)}
364
+ {...props}
365
+ />
366
+ )
367
+ })
368
+ DrawerTitle.displayName = "DrawerTitle"
369
+
370
+ // ─── Description ─────────────────────────────────────────────────────────────
371
+
372
+ export const DrawerDescription = React.forwardRef<
373
+ React.ElementRef<typeof DialogPrimitive.Description>,
374
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
375
+ >(({ className, ...props }, ref) => {
376
+ const size = React.useContext(DrawerSizeContext)
377
+ return (
378
+ <DialogPrimitive.Description
379
+ ref={ref}
380
+ className={cn("ax:text-muted-foreground", DESCRIPTION_SIZE[size], className)}
381
+ {...props}
382
+ />
383
+ )
384
+ })
385
+ DrawerDescription.displayName = "DrawerDescription"
@@ -0,0 +1,41 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { User } from "lucide-react"
4
+ import { Button } from "./button"
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ } from "./dropdown-menu"
13
+
14
+ const meta: Meta<typeof DropdownMenu> = {
15
+ title: "Components/DropdownMenu",
16
+ component: DropdownMenu,
17
+ tags: ["autodocs"],
18
+ }
19
+
20
+ export default meta
21
+ type Story = StoryObj<typeof DropdownMenu>
22
+
23
+ export const Default: Story = {
24
+ render: () => (
25
+ <DropdownMenu>
26
+ <DropdownMenuTrigger asChild>
27
+ <Button variant="outline">Account</Button>
28
+ </DropdownMenuTrigger>
29
+ <DropdownMenuContent align="start">
30
+ <DropdownMenuLabel>My account</DropdownMenuLabel>
31
+ <DropdownMenuSeparator />
32
+ <DropdownMenuItem>
33
+ <User className="ax:size-4" />
34
+ Profile
35
+ </DropdownMenuItem>
36
+ <DropdownMenuItem>Billing</DropdownMenuItem>
37
+ <DropdownMenuItem variant="destructive">Sign out</DropdownMenuItem>
38
+ </DropdownMenuContent>
39
+ </DropdownMenu>
40
+ ),
41
+ }