@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.
- package/brand-knowledge.json +451 -0
- package/knowledge/accordion.md +135 -0
- package/knowledge/activity-rings.md +123 -0
- package/knowledge/alert.md +184 -0
- package/knowledge/answer-actions.md +217 -0
- package/knowledge/aspect-ratio.md +82 -0
- package/knowledge/avatar.md +158 -0
- package/knowledge/badge.md +172 -0
- package/knowledge/bar-graph.md +167 -0
- package/knowledge/bento-grid.md +115 -0
- package/knowledge/blockquote.md +153 -0
- package/knowledge/box-list.md +177 -0
- package/knowledge/bullet-list.md +128 -0
- package/knowledge/button-group.md +179 -0
- package/knowledge/button.md +231 -0
- package/knowledge/calendar.md +168 -0
- package/knowledge/card.md +321 -0
- package/knowledge/cards-nav.md +170 -0
- package/knowledge/chain-of-thought.md +164 -0
- package/knowledge/chat-container.md +135 -0
- package/knowledge/chat-message.md +224 -0
- package/knowledge/checkbox-card.md +172 -0
- package/knowledge/checkbox.md +111 -0
- package/knowledge/citations.md +241 -0
- package/knowledge/combobox.md +265 -0
- package/knowledge/countdown-pill.md +153 -0
- package/knowledge/cta.md +118 -0
- package/knowledge/data-list.md +173 -0
- package/knowledge/data-table.md +220 -0
- package/knowledge/dialog.md +249 -0
- package/knowledge/drawer.md +236 -0
- package/knowledge/dropdown-menu.md +238 -0
- package/knowledge/dropzone.md +153 -0
- package/knowledge/email-chips-input.md +161 -0
- package/knowledge/empty-state.md +106 -0
- package/knowledge/entity-header.md +166 -0
- package/knowledge/feature-grid.md +120 -0
- package/knowledge/feedback-bar.md +127 -0
- package/knowledge/footer.md +116 -0
- package/knowledge/heading.md +189 -0
- package/knowledge/hero.md +361 -0
- package/knowledge/input-button.md +187 -0
- package/knowledge/input-group.md +185 -0
- package/knowledge/input.md +256 -0
- package/knowledge/inset.md +177 -0
- package/knowledge/kpi-card.md +206 -0
- package/knowledge/list-nav.md +181 -0
- package/knowledge/logo-cloud.md +102 -0
- package/knowledge/media-gallery.md +158 -0
- package/knowledge/message-actions.md +175 -0
- package/knowledge/nav-sidebar.md +225 -0
- package/knowledge/number-stepper.md +159 -0
- package/knowledge/page-header.md +109 -0
- package/knowledge/pagination.md +225 -0
- package/knowledge/popover.md +159 -0
- package/knowledge/pricing.md +132 -0
- package/knowledge/progress.md +156 -0
- package/knowledge/prompt-input.md +337 -0
- package/knowledge/prompt-suggestions.md +168 -0
- package/knowledge/quota-bar.md +160 -0
- package/knowledge/quote.md +116 -0
- package/knowledge/radio-card.md +172 -0
- package/knowledge/radio-group.md +135 -0
- package/knowledge/reasoning.md +142 -0
- package/knowledge/search.md +131 -0
- package/knowledge/section-nav.md +162 -0
- package/knowledge/segmented-control.md +181 -0
- package/knowledge/select-button.md +205 -0
- package/knowledge/select.md +210 -0
- package/knowledge/separator.md +75 -0
- package/knowledge/shimmer-text.md +140 -0
- package/knowledge/sidebar.md +227 -0
- package/knowledge/skeleton.md +142 -0
- package/knowledge/slider.md +133 -0
- package/knowledge/slideshow.md +159 -0
- package/knowledge/source.md +156 -0
- package/knowledge/spinner.md +120 -0
- package/knowledge/stat-strip.md +128 -0
- package/knowledge/stats.md +117 -0
- package/knowledge/step-indicator.md +157 -0
- package/knowledge/switch.md +88 -0
- package/knowledge/table.md +190 -0
- package/knowledge/tabs.md +169 -0
- package/knowledge/testimonials.md +115 -0
- package/knowledge/text-link.md +192 -0
- package/knowledge/text.md +194 -0
- package/knowledge/thinking-bar.md +115 -0
- package/knowledge/thinking-steps.md +140 -0
- package/knowledge/toaster.md +127 -0
- package/knowledge/toggle.md +113 -0
- package/knowledge/tool-call-card.md +241 -0
- package/knowledge/tooltip.md +120 -0
- package/knowledge/top-nav.md +138 -0
- package/knowledge.json +19854 -0
- package/package.json +94 -4
- package/src/components/accordion.stories.tsx +46 -0
- package/src/components/accordion.tsx +74 -0
- package/src/components/activity-rings.stories.tsx +73 -0
- package/src/components/activity-rings.tsx +121 -0
- package/src/components/alert.stories.tsx +43 -0
- package/src/components/alert.tsx +132 -0
- package/src/components/answer-actions.stories.tsx +54 -0
- package/src/components/answer-actions.test.tsx +37 -0
- package/src/components/answer-actions.tsx +313 -0
- package/src/components/aspect-ratio.stories.tsx +32 -0
- package/src/components/aspect-ratio.tsx +5 -0
- package/src/components/avatar.stories.tsx +40 -0
- package/src/components/avatar.tsx +154 -0
- package/src/components/badge.stories.tsx +27 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/bar-graph.stories.tsx +28 -0
- package/src/components/bar-graph.tsx +204 -0
- package/src/components/bento-grid.stories.tsx +23 -0
- package/src/components/bento-grid.tsx +67 -0
- package/src/components/blockquote.stories.tsx +28 -0
- package/src/components/blockquote.tsx +88 -0
- package/src/components/box-list.stories.tsx +25 -0
- package/src/components/box-list.tsx +108 -0
- package/src/components/bullet-list.stories.tsx +22 -0
- package/src/components/bullet-list.tsx +101 -0
- package/src/components/button-group.stories.tsx +31 -0
- package/src/components/button-group.tsx +156 -0
- package/src/components/button.stories.tsx +66 -0
- package/src/components/button.test.ts +17 -0
- package/src/components/button.tsx +139 -0
- package/src/components/calendar.stories.tsx +25 -0
- package/src/components/calendar.tsx +367 -0
- package/src/components/card.stories.tsx +44 -0
- package/src/components/card.tsx +392 -0
- package/src/components/cards-nav.stories.tsx +35 -0
- package/src/components/cards-nav.tsx +115 -0
- package/src/components/chain-of-thought.stories.tsx +67 -0
- package/src/components/chain-of-thought.test.tsx +50 -0
- package/src/components/chain-of-thought.tsx +170 -0
- package/src/components/chat-container.stories.tsx +47 -0
- package/src/components/chat-container.test.tsx +49 -0
- package/src/components/chat-container.tsx +222 -0
- package/src/components/chat-message.stories.tsx +52 -0
- package/src/components/chat-message.test.tsx +36 -0
- package/src/components/chat-message.tsx +163 -0
- package/src/components/checkbox-card.stories.tsx +27 -0
- package/src/components/checkbox-card.tsx +200 -0
- package/src/components/checkbox.stories.tsx +26 -0
- package/src/components/checkbox.tsx +61 -0
- package/src/components/citations.stories.tsx +64 -0
- package/src/components/citations.test.tsx +61 -0
- package/src/components/citations.tsx +220 -0
- package/src/components/combobox.stories.tsx +184 -0
- package/src/components/combobox.test.tsx +82 -0
- package/src/components/combobox.tsx +407 -0
- package/src/components/countdown-pill.stories.tsx +24 -0
- package/src/components/countdown-pill.tsx +129 -0
- package/src/components/cta.stories.tsx +29 -0
- package/src/components/cta.tsx +52 -0
- package/src/components/data-list.stories.tsx +50 -0
- package/src/components/data-list.test.tsx +35 -0
- package/src/components/data-list.tsx +181 -0
- package/src/components/data-table.stories.tsx +29 -0
- package/src/components/data-table.tsx +200 -0
- package/src/components/dialog.stories.tsx +42 -0
- package/src/components/dialog.tsx +200 -0
- package/src/components/drawer.stories.tsx +48 -0
- package/src/components/drawer.tsx +385 -0
- package/src/components/dropdown-menu.stories.tsx +41 -0
- package/src/components/dropdown-menu.tsx +252 -0
- package/src/components/dropzone.stories.tsx +19 -0
- package/src/components/dropzone.tsx +250 -0
- package/src/components/email-chips-input.stories.tsx +135 -0
- package/src/components/email-chips-input.tsx +210 -0
- package/src/components/empty-state.stories.tsx +22 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/entity-header.stories.tsx +43 -0
- package/src/components/entity-header.test.tsx +35 -0
- package/src/components/entity-header.tsx +92 -0
- package/src/components/feature-grid.stories.tsx +25 -0
- package/src/components/feature-grid.tsx +87 -0
- package/src/components/feedback-bar.stories.tsx +25 -0
- package/src/components/feedback-bar.test.tsx +24 -0
- package/src/components/feedback-bar.tsx +62 -0
- package/src/components/footer.stories.tsx +23 -0
- package/src/components/footer.tsx +78 -0
- package/src/components/heading.stories.tsx +45 -0
- package/src/components/heading.test.tsx +22 -0
- package/src/components/heading.tsx +77 -0
- package/src/components/hero.stories.tsx +38 -0
- package/src/components/hero.tsx +498 -0
- package/src/components/input-button.stories.tsx +19 -0
- package/src/components/input-button.tsx +112 -0
- package/src/components/input-group.stories.tsx +38 -0
- package/src/components/input-group.tsx +157 -0
- package/src/components/input.stories.tsx +31 -0
- package/src/components/input.tsx +284 -0
- package/src/components/inset.stories.tsx +29 -0
- package/src/components/inset.tsx +70 -0
- package/src/components/kpi-card.stories.tsx +29 -0
- package/src/components/kpi-card.tsx +284 -0
- package/src/components/list-nav.stories.tsx +22 -0
- package/src/components/list-nav.tsx +145 -0
- package/src/components/logo-cloud.stories.tsx +23 -0
- package/src/components/logo-cloud.tsx +42 -0
- package/src/components/media-gallery.stories.tsx +25 -0
- package/src/components/media-gallery.tsx +347 -0
- package/src/components/message-actions.stories.tsx +45 -0
- package/src/components/message-actions.test.tsx +32 -0
- package/src/components/message-actions.tsx +119 -0
- package/src/components/nav-sidebar.stories.tsx +58 -0
- package/src/components/nav-sidebar.test.tsx +54 -0
- package/src/components/nav-sidebar.tsx +462 -0
- package/src/components/number-stepper.stories.tsx +19 -0
- package/src/components/number-stepper.tsx +158 -0
- package/src/components/page-header.stories.tsx +25 -0
- package/src/components/page-header.tsx +64 -0
- package/src/components/pagination.stories.tsx +110 -0
- package/src/components/pagination.test.tsx +46 -0
- package/src/components/pagination.tsx +144 -0
- package/src/components/popover.stories.tsx +26 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/pricing.stories.tsx +42 -0
- package/src/components/pricing.tsx +130 -0
- package/src/components/progress.stories.tsx +22 -0
- package/src/components/progress.tsx +189 -0
- package/src/components/prompt-input.stories.tsx +62 -0
- package/src/components/prompt-input.test.tsx +106 -0
- package/src/components/prompt-input.tsx +343 -0
- package/src/components/prompt-suggestions.stories.tsx +53 -0
- package/src/components/prompt-suggestions.test.tsx +37 -0
- package/src/components/prompt-suggestions.tsx +122 -0
- package/src/components/quota-bar.stories.tsx +19 -0
- package/src/components/quota-bar.tsx +191 -0
- package/src/components/quote.stories.tsx +18 -0
- package/src/components/quote.tsx +43 -0
- package/src/components/radio-card.stories.tsx +27 -0
- package/src/components/radio-card.tsx +225 -0
- package/src/components/radio-group.stories.tsx +32 -0
- package/src/components/radio-group.tsx +68 -0
- package/src/components/reasoning.stories.tsx +50 -0
- package/src/components/reasoning.test.tsx +48 -0
- package/src/components/reasoning.tsx +143 -0
- package/src/components/search.stories.tsx +18 -0
- package/src/components/search.tsx +133 -0
- package/src/components/section-nav.stories.tsx +59 -0
- package/src/components/section-nav.test.tsx +36 -0
- package/src/components/section-nav.tsx +146 -0
- package/src/components/segmented-control.stories.tsx +22 -0
- package/src/components/segmented-control.tsx +301 -0
- package/src/components/select-button.stories.tsx +27 -0
- package/src/components/select-button.tsx +174 -0
- package/src/components/select.stories.tsx +27 -0
- package/src/components/select.tsx +198 -0
- package/src/components/separator.stories.tsx +22 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/shimmer-text.stories.tsx +23 -0
- package/src/components/shimmer-text.test.tsx +35 -0
- package/src/components/shimmer-text.tsx +43 -0
- package/src/components/sidebar.stories.tsx +25 -0
- package/src/components/sidebar.tsx +234 -0
- package/src/components/skeleton.stories.tsx +22 -0
- package/src/components/skeleton.tsx +96 -0
- package/src/components/slider.stories.tsx +20 -0
- package/src/components/slider.tsx +176 -0
- package/src/components/slideshow.stories.tsx +30 -0
- package/src/components/slideshow.tsx +291 -0
- package/src/components/source.stories.tsx +32 -0
- package/src/components/source.test.tsx +40 -0
- package/src/components/source.tsx +161 -0
- package/src/components/spinner.stories.tsx +19 -0
- package/src/components/spinner.tsx +76 -0
- package/src/components/stat-strip.stories.tsx +36 -0
- package/src/components/stat-strip.test.tsx +25 -0
- package/src/components/stat-strip.tsx +85 -0
- package/src/components/stats.stories.tsx +24 -0
- package/src/components/stats.tsx +83 -0
- package/src/components/step-indicator.stories.tsx +18 -0
- package/src/components/step-indicator.test.tsx +18 -0
- package/src/components/step-indicator.tsx +286 -0
- package/src/components/switch.stories.tsx +26 -0
- package/src/components/switch.tsx +32 -0
- package/src/components/table.stories.tsx +38 -0
- package/src/components/table.tsx +220 -0
- package/src/components/tabs.stories.tsx +25 -0
- package/src/components/tabs.tsx +276 -0
- package/src/components/testimonials.stories.tsx +22 -0
- package/src/components/testimonials.tsx +109 -0
- package/src/components/text-link.stories.tsx +47 -0
- package/src/components/text-link.test.tsx +18 -0
- package/src/components/text-link.tsx +88 -0
- package/src/components/text.stories.tsx +49 -0
- package/src/components/text.tsx +105 -0
- package/src/components/thinking-bar.stories.tsx +19 -0
- package/src/components/thinking-bar.test.tsx +27 -0
- package/src/components/thinking-bar.tsx +64 -0
- package/src/components/thinking-steps.stories.tsx +44 -0
- package/src/components/thinking-steps.test.tsx +42 -0
- package/src/components/thinking-steps.tsx +92 -0
- package/src/components/toaster.stories.tsx +80 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/toggle.stories.tsx +21 -0
- package/src/components/toggle.tsx +103 -0
- package/src/components/tool-call-card.stories.tsx +97 -0
- package/src/components/tool-call-card.test.tsx +88 -0
- package/src/components/tool-call-card.tsx +231 -0
- package/src/components/tooltip.stories.tsx +25 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/top-nav.stories.tsx +20 -0
- package/src/components/top-nav.tsx +76 -0
- package/src/docs/Colors.stories.tsx +192 -0
- package/src/docs/GettingStarted.mdx +72 -0
- package/src/docs/Mcp.mdx +52 -0
- package/src/docs/Typography.stories.tsx +114 -0
- package/src/hooks/use-in-view.ts +32 -0
- package/src/hooks/use-reduced-motion.ts +26 -0
- package/src/index.ts +343 -0
- package/src/lib/cn.test.ts +67 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/motion.ts +109 -0
- package/src/lib/prefix-utilities.test.ts +56 -0
- package/src/lib/prefix-utilities.ts +50 -0
- package/src/styles/base.css +489 -0
- package/src/styles/motion.css +69 -0
- package/src/styles/overrides.css +44 -0
- package/src/styles/typography.css +173 -0
- package/src/theme/theme.css +391 -0
- package/src/theme/tokens.ts +29 -0
- package/src/theme/types.ts +26 -0
- 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
|
+
}
|