@emara/ui 1.1.0
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/components/ui/.gitkeep +0 -0
- package/components/ui/accordion.stories.tsx +231 -0
- package/components/ui/accordion.tsx +250 -0
- package/components/ui/app-shell.stories.tsx +270 -0
- package/components/ui/app-shell.tsx +491 -0
- package/components/ui/avatar.stories.tsx +174 -0
- package/components/ui/avatar.tsx +257 -0
- package/components/ui/badge.stories.tsx +127 -0
- package/components/ui/badge.tsx +146 -0
- package/components/ui/breadcrumb.stories.tsx +92 -0
- package/components/ui/breadcrumb.tsx +302 -0
- package/components/ui/button.stories.tsx +186 -0
- package/components/ui/button.tsx +128 -0
- package/components/ui/card.stories.tsx +279 -0
- package/components/ui/card.tsx +250 -0
- package/components/ui/checkbox.stories.tsx +93 -0
- package/components/ui/checkbox.tsx +131 -0
- package/components/ui/combobox.stories.tsx +489 -0
- package/components/ui/combobox.tsx +874 -0
- package/components/ui/context-menu.stories.tsx +202 -0
- package/components/ui/context-menu.tsx +309 -0
- package/components/ui/data-table.stories.tsx +227 -0
- package/components/ui/data-table.tsx +539 -0
- package/components/ui/date-picker.stories.tsx +225 -0
- package/components/ui/date-picker.tsx +597 -0
- package/components/ui/dialog.stories.tsx +193 -0
- package/components/ui/dialog.tsx +262 -0
- package/components/ui/divider.stories.tsx +84 -0
- package/components/ui/divider.tsx +135 -0
- package/components/ui/drawer.stories.tsx +218 -0
- package/components/ui/drawer.tsx +329 -0
- package/components/ui/dropdown-menu.stories.tsx +270 -0
- package/components/ui/dropdown-menu.tsx +353 -0
- package/components/ui/empty-state.stories.tsx +121 -0
- package/components/ui/empty-state.tsx +289 -0
- package/components/ui/field-group.stories.tsx +201 -0
- package/components/ui/field-group.tsx +276 -0
- package/components/ui/form.stories.tsx +219 -0
- package/components/ui/form.tsx +542 -0
- package/components/ui/input.stories.tsx +154 -0
- package/components/ui/input.tsx +208 -0
- package/components/ui/label.stories.tsx +84 -0
- package/components/ui/label.tsx +98 -0
- package/components/ui/page-header.stories.tsx +136 -0
- package/components/ui/page-header.tsx +315 -0
- package/components/ui/pagination.stories.tsx +136 -0
- package/components/ui/pagination.tsx +427 -0
- package/components/ui/popover.stories.tsx +212 -0
- package/components/ui/popover.tsx +167 -0
- package/components/ui/radio-group.stories.tsx +96 -0
- package/components/ui/radio-group.tsx +250 -0
- package/components/ui/select.stories.tsx +203 -0
- package/components/ui/select.tsx +318 -0
- package/components/ui/sidebar.stories.tsx +186 -0
- package/components/ui/sidebar.tsx +623 -0
- package/components/ui/skeleton.stories.tsx +131 -0
- package/components/ui/skeleton.tsx +311 -0
- package/components/ui/switch.stories.tsx +74 -0
- package/components/ui/switch.tsx +186 -0
- package/components/ui/table.stories.tsx +107 -0
- package/components/ui/table.tsx +285 -0
- package/components/ui/tabs.stories.tsx +222 -0
- package/components/ui/tabs.tsx +287 -0
- package/components/ui/textarea.stories.tsx +96 -0
- package/components/ui/textarea.tsx +182 -0
- package/components/ui/toast.stories.tsx +169 -0
- package/components/ui/toast.tsx +250 -0
- package/components/ui/tooltip.stories.tsx +146 -0
- package/components/ui/tooltip.tsx +156 -0
- package/components/ui/top-bar.stories.tsx +182 -0
- package/components/ui/top-bar.tsx +155 -0
- package/dist/components/ui/accordion.d.ts +45 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +99 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/app-shell.d.ts +70 -0
- package/dist/components/ui/app-shell.d.ts.map +1 -0
- package/dist/components/ui/app-shell.js +199 -0
- package/dist/components/ui/app-shell.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +41 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +104 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +27 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +65 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +35 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +88 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +26 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +73 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +52 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +96 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +18 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +59 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +194 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +361 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +46 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +95 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +53 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +163 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/date-picker.d.ts +103 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +306 -0
- package/dist/components/ui/date-picker.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +40 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +110 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/divider.d.ts +30 -0
- package/dist/components/ui/divider.d.ts.map +1 -0
- package/dist/components/ui/divider.js +62 -0
- package/dist/components/ui/divider.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +56 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +147 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +63 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +116 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty-state.d.ts +43 -0
- package/dist/components/ui/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty-state.js +128 -0
- package/dist/components/ui/empty-state.js.map +1 -0
- package/dist/components/ui/field-group.d.ts +38 -0
- package/dist/components/ui/field-group.d.ts.map +1 -0
- package/dist/components/ui/field-group.js +107 -0
- package/dist/components/ui/field-group.js.map +1 -0
- package/dist/components/ui/form.d.ts +67 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +286 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/input.d.ts +36 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +99 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +37 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +34 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +65 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +140 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +67 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +109 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +28 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +85 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +35 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +103 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +42 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +86 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +59 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +189 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +77 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +115 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/switch.d.ts +26 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +84 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +52 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +109 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +42 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +163 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +26 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +96 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +77 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +141 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +31 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +71 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/top-bar.d.ts +30 -0
- package/dist/components/ui/top-bar.d.ts.map +1 -0
- package/dist/components/ui/top-bar.js +64 -0
- package/dist/components/ui/top-bar.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/lib/utils.ts +6 -0
- package/package.json +112 -0
- package/styles/globals.css +685 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
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: "Overlays/Drawer",
|
|
18
|
+
component: Drawer,
|
|
19
|
+
parameters: { layout: "centered" },
|
|
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>
|
|
32
|
+
<DrawerHeader>
|
|
33
|
+
<DrawerTitle>Filters</DrawerTitle>
|
|
34
|
+
<DrawerDescription>Refine results without leaving the page.</DrawerDescription>
|
|
35
|
+
</DrawerHeader>
|
|
36
|
+
<DrawerBody>
|
|
37
|
+
<p className="text-sm">Filter controls go here.</p>
|
|
38
|
+
</DrawerBody>
|
|
39
|
+
<DrawerFooter>
|
|
40
|
+
<DrawerClose asChild>
|
|
41
|
+
<Button variant="ghost">Cancel</Button>
|
|
42
|
+
</DrawerClose>
|
|
43
|
+
<Button>Apply</Button>
|
|
44
|
+
</DrawerFooter>
|
|
45
|
+
</DrawerContent>
|
|
46
|
+
</Drawer>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Positions: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<div className="flex flex-wrap gap-2">
|
|
53
|
+
{(["start", "end", "top", "bottom"] as const).map((p) => (
|
|
54
|
+
<Drawer key={p}>
|
|
55
|
+
<DrawerTrigger asChild>
|
|
56
|
+
<Button variant="outline" size="sm">
|
|
57
|
+
{p}
|
|
58
|
+
</Button>
|
|
59
|
+
</DrawerTrigger>
|
|
60
|
+
<DrawerContent position={p}>
|
|
61
|
+
<DrawerHeader>
|
|
62
|
+
<DrawerTitle>position="{p}"</DrawerTitle>
|
|
63
|
+
<DrawerDescription>
|
|
64
|
+
Slides in from the {p === "start" ? "leading" : p === "end" ? "trailing" : p} edge.
|
|
65
|
+
{p === "start" || p === "end" ? " Flips in RTL." : ""}
|
|
66
|
+
</DrawerDescription>
|
|
67
|
+
</DrawerHeader>
|
|
68
|
+
<DrawerBody>
|
|
69
|
+
<p className="text-sm">Content for the {p} drawer.</p>
|
|
70
|
+
</DrawerBody>
|
|
71
|
+
</DrawerContent>
|
|
72
|
+
</Drawer>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Sizes: Story = {
|
|
79
|
+
render: () => (
|
|
80
|
+
<div className="flex flex-wrap gap-2">
|
|
81
|
+
{(["xs", "sm", "md", "lg", "xl", "full"] as const).map((s) => (
|
|
82
|
+
<Drawer key={s}>
|
|
83
|
+
<DrawerTrigger asChild>
|
|
84
|
+
<Button variant="outline" size="sm">
|
|
85
|
+
{s}
|
|
86
|
+
</Button>
|
|
87
|
+
</DrawerTrigger>
|
|
88
|
+
<DrawerContent size={s}>
|
|
89
|
+
<DrawerHeader>
|
|
90
|
+
<DrawerTitle>size="{s}"</DrawerTitle>
|
|
91
|
+
<DrawerDescription>End-positioned drawer.</DrawerDescription>
|
|
92
|
+
</DrawerHeader>
|
|
93
|
+
<DrawerBody>
|
|
94
|
+
<p className="text-sm">
|
|
95
|
+
Horizontal widths: xs 280, sm 360, md 440 (default), lg 560, xl 720, full
|
|
96
|
+
full-screen.
|
|
97
|
+
</p>
|
|
98
|
+
</DrawerBody>
|
|
99
|
+
</DrawerContent>
|
|
100
|
+
</Drawer>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const Scrollable: Story = {
|
|
107
|
+
render: () => (
|
|
108
|
+
<Drawer>
|
|
109
|
+
<DrawerTrigger asChild>
|
|
110
|
+
<Button>Long content</Button>
|
|
111
|
+
</DrawerTrigger>
|
|
112
|
+
<DrawerContent scrollable>
|
|
113
|
+
<DrawerHeader>
|
|
114
|
+
<DrawerTitle>Filters</DrawerTitle>
|
|
115
|
+
<DrawerDescription>Scroll the body when needed.</DrawerDescription>
|
|
116
|
+
</DrawerHeader>
|
|
117
|
+
<DrawerBody>
|
|
118
|
+
{Array.from({ length: 40 }, (_, i) => (
|
|
119
|
+
<p key={i} className="mb-2 text-sm">
|
|
120
|
+
Filter option {i + 1}
|
|
121
|
+
</p>
|
|
122
|
+
))}
|
|
123
|
+
</DrawerBody>
|
|
124
|
+
<DrawerFooter>
|
|
125
|
+
<DrawerClose asChild>
|
|
126
|
+
<Button variant="ghost">Reset</Button>
|
|
127
|
+
</DrawerClose>
|
|
128
|
+
<Button>Apply</Button>
|
|
129
|
+
</DrawerFooter>
|
|
130
|
+
</DrawerContent>
|
|
131
|
+
</Drawer>
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Opt out of body scroll. The whole drawer becomes a single scroll surface
|
|
137
|
+
* — header + body + footer flow together. Useful for short forms or when
|
|
138
|
+
* you want the entire content to scroll as one (e.g. a fixed-footer CTA
|
|
139
|
+
* that should scroll away with the body).
|
|
140
|
+
*/
|
|
141
|
+
export const NonScrollableBody: Story = {
|
|
142
|
+
render: () => (
|
|
143
|
+
<Drawer>
|
|
144
|
+
<DrawerTrigger asChild>
|
|
145
|
+
<Button>Non-scrollable body</Button>
|
|
146
|
+
</DrawerTrigger>
|
|
147
|
+
<DrawerContent scrollable={false}>
|
|
148
|
+
<DrawerHeader>
|
|
149
|
+
<DrawerTitle>Short form</DrawerTitle>
|
|
150
|
+
<DrawerDescription>Body sizes to content; no inner scroll.</DrawerDescription>
|
|
151
|
+
</DrawerHeader>
|
|
152
|
+
<DrawerBody>
|
|
153
|
+
<p className="text-sm">Just two lines.</p>
|
|
154
|
+
<p className="mt-2 text-sm">No overflow region.</p>
|
|
155
|
+
</DrawerBody>
|
|
156
|
+
<DrawerFooter>
|
|
157
|
+
<Button>OK</Button>
|
|
158
|
+
</DrawerFooter>
|
|
159
|
+
</DrawerContent>
|
|
160
|
+
</Drawer>
|
|
161
|
+
),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const Dismissible: Story = {
|
|
165
|
+
render: () => (
|
|
166
|
+
<Drawer>
|
|
167
|
+
<DrawerTrigger asChild>
|
|
168
|
+
<Button>Non-dismissible drawer</Button>
|
|
169
|
+
</DrawerTrigger>
|
|
170
|
+
<DrawerContent dismissible={false}>
|
|
171
|
+
<DrawerHeader>
|
|
172
|
+
<DrawerTitle>Action required</DrawerTitle>
|
|
173
|
+
<DrawerDescription>
|
|
174
|
+
No X, no Esc, no overlay click — use the explicit action below to close.
|
|
175
|
+
</DrawerDescription>
|
|
176
|
+
</DrawerHeader>
|
|
177
|
+
<DrawerBody />
|
|
178
|
+
<DrawerFooter>
|
|
179
|
+
<DrawerClose asChild>
|
|
180
|
+
<Button>Acknowledge</Button>
|
|
181
|
+
</DrawerClose>
|
|
182
|
+
</DrawerFooter>
|
|
183
|
+
</DrawerContent>
|
|
184
|
+
</Drawer>
|
|
185
|
+
),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const ConfirmBeforeClose: Story = {
|
|
189
|
+
render: () => (
|
|
190
|
+
<Drawer>
|
|
191
|
+
<DrawerTrigger asChild>
|
|
192
|
+
<Button>Open (confirm on close)</Button>
|
|
193
|
+
</DrawerTrigger>
|
|
194
|
+
<DrawerContent confirmBeforeClose>
|
|
195
|
+
<DrawerHeader>
|
|
196
|
+
<DrawerTitle>Edit profile</DrawerTitle>
|
|
197
|
+
<DrawerDescription>
|
|
198
|
+
Outside-click prompts confirmation — useful for dirty forms.
|
|
199
|
+
</DrawerDescription>
|
|
200
|
+
</DrawerHeader>
|
|
201
|
+
<DrawerBody>
|
|
202
|
+
<p className="text-sm">Try clicking on the overlay.</p>
|
|
203
|
+
</DrawerBody>
|
|
204
|
+
</DrawerContent>
|
|
205
|
+
</Drawer>
|
|
206
|
+
),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const Loading: Story = {
|
|
210
|
+
render: () => (
|
|
211
|
+
<Drawer>
|
|
212
|
+
<DrawerTrigger asChild>
|
|
213
|
+
<Button>Open (loading)</Button>
|
|
214
|
+
</DrawerTrigger>
|
|
215
|
+
<DrawerContent loading />
|
|
216
|
+
</Drawer>
|
|
217
|
+
),
|
|
218
|
+
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, forwardRef, useContext, useMemo } from "react";
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
|
+
import { RiCloseLine } from "@remixicon/react";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import { Skeleton } from "./skeleton";
|
|
10
|
+
|
|
11
|
+
// Context lets DrawerBody pick up the `scrollable` flag set on DrawerContent
|
|
12
|
+
// without consumers having to thread it through.
|
|
13
|
+
type DrawerContextValue = { scrollable: boolean };
|
|
14
|
+
const DrawerContext = createContext<DrawerContextValue>({ scrollable: true });
|
|
15
|
+
const useDrawerContext = () => useContext(DrawerContext);
|
|
16
|
+
|
|
17
|
+
// Per docs/emara-ui-phase-3-components.md §2. Built on Radix Dialog (not Vaul)
|
|
18
|
+
// so the slide animation, focus trap, and Esc handling stay consistent with
|
|
19
|
+
// the rest of the system.
|
|
20
|
+
|
|
21
|
+
const Drawer = DialogPrimitive.Root;
|
|
22
|
+
const DrawerTrigger = DialogPrimitive.Trigger;
|
|
23
|
+
const DrawerPortal = DialogPrimitive.Portal;
|
|
24
|
+
const DrawerClose = DialogPrimitive.Close;
|
|
25
|
+
|
|
26
|
+
// ----------------------------------------------------------------------------
|
|
27
|
+
// DrawerOverlay — same as Dialog's overlay.
|
|
28
|
+
// ----------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const DrawerOverlay = forwardRef<
|
|
31
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
32
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
33
|
+
>(function DrawerOverlay({ className, ...props }, ref) {
|
|
34
|
+
return (
|
|
35
|
+
<DialogPrimitive.Overlay
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(
|
|
38
|
+
"z-modal bg-foreground/50 fixed inset-0",
|
|
39
|
+
"data-[state=open]:animate-[fade-in_var(--duration-normal)_var(--ease-out)]",
|
|
40
|
+
"data-[state=closed]:animate-[fade-out_var(--duration-fast)_var(--ease-in)]",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
DrawerOverlay.displayName = "DrawerOverlay";
|
|
48
|
+
|
|
49
|
+
// ----------------------------------------------------------------------------
|
|
50
|
+
// DrawerContent
|
|
51
|
+
// ----------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
type DrawerPosition = "start" | "end" | "top" | "bottom";
|
|
54
|
+
|
|
55
|
+
const drawerContentVariants = cva(
|
|
56
|
+
["fixed z-modal flex flex-col bg-card text-card-foreground shadow-xl", "border-border"].join(" "),
|
|
57
|
+
{
|
|
58
|
+
variants: {
|
|
59
|
+
position: {
|
|
60
|
+
// Logical sides — `start-0` is left in LTR, right in RTL.
|
|
61
|
+
start:
|
|
62
|
+
"inset-y-0 start-0 h-full border-e " +
|
|
63
|
+
"data-[state=open]:animate-[slide-in-from-start_var(--duration-normal)_var(--ease-out)] " +
|
|
64
|
+
"data-[state=closed]:animate-[slide-in-from-start_var(--duration-fast)_var(--ease-in)_reverse]",
|
|
65
|
+
end:
|
|
66
|
+
"inset-y-0 end-0 h-full border-s " +
|
|
67
|
+
"data-[state=open]:animate-[slide-in-from-end_var(--duration-normal)_var(--ease-out)] " +
|
|
68
|
+
"data-[state=closed]:animate-[slide-in-from-end_var(--duration-fast)_var(--ease-in)_reverse]",
|
|
69
|
+
top:
|
|
70
|
+
"inset-x-0 top-0 w-full border-b " +
|
|
71
|
+
"data-[state=open]:animate-[slide-in-from-top_var(--duration-normal)_var(--ease-out)] " +
|
|
72
|
+
"data-[state=closed]:animate-[slide-in-from-top_var(--duration-fast)_var(--ease-in)_reverse]",
|
|
73
|
+
bottom:
|
|
74
|
+
"inset-x-0 bottom-0 w-full border-t " +
|
|
75
|
+
"data-[state=open]:animate-[slide-in-from-bottom_var(--duration-normal)_var(--ease-out)] " +
|
|
76
|
+
"data-[state=closed]:animate-[slide-in-from-bottom_var(--duration-fast)_var(--ease-in)_reverse]",
|
|
77
|
+
},
|
|
78
|
+
size: {
|
|
79
|
+
xs: "",
|
|
80
|
+
sm: "",
|
|
81
|
+
md: "",
|
|
82
|
+
lg: "",
|
|
83
|
+
xl: "",
|
|
84
|
+
full: "",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
compoundVariants: [
|
|
88
|
+
// Horizontal (start/end) — width scale (tokens in design-tokens §11b)
|
|
89
|
+
{ position: "start", size: "xs", class: "w-drawer-xs" },
|
|
90
|
+
{ position: "start", size: "sm", class: "w-drawer-sm" },
|
|
91
|
+
{ position: "start", size: "md", class: "w-drawer-md" },
|
|
92
|
+
{ position: "start", size: "lg", class: "w-drawer-lg" },
|
|
93
|
+
{ position: "start", size: "xl", class: "w-drawer-xl" },
|
|
94
|
+
{ position: "start", size: "full", class: "w-screen" },
|
|
95
|
+
{ position: "end", size: "xs", class: "w-drawer-xs" },
|
|
96
|
+
{ position: "end", size: "sm", class: "w-drawer-sm" },
|
|
97
|
+
{ position: "end", size: "md", class: "w-drawer-md" },
|
|
98
|
+
{ position: "end", size: "lg", class: "w-drawer-lg" },
|
|
99
|
+
{ position: "end", size: "xl", class: "w-drawer-xl" },
|
|
100
|
+
{ position: "end", size: "full", class: "w-screen" },
|
|
101
|
+
// Vertical (top/bottom) — height scale
|
|
102
|
+
{ position: "top", size: "xs", class: "h-drawer-xs" },
|
|
103
|
+
{ position: "top", size: "sm", class: "h-drawer-sm" },
|
|
104
|
+
{ position: "top", size: "md", class: "h-drawer-md" },
|
|
105
|
+
{ position: "top", size: "lg", class: "h-drawer-lg" },
|
|
106
|
+
{ position: "top", size: "xl", class: "h-drawer-xl" },
|
|
107
|
+
{ position: "top", size: "full", class: "h-screen" },
|
|
108
|
+
{ position: "bottom", size: "xs", class: "h-drawer-xs" },
|
|
109
|
+
{ position: "bottom", size: "sm", class: "h-drawer-sm" },
|
|
110
|
+
{ position: "bottom", size: "md", class: "h-drawer-md" },
|
|
111
|
+
{ position: "bottom", size: "lg", class: "h-drawer-lg" },
|
|
112
|
+
{ position: "bottom", size: "xl", class: "h-drawer-xl" },
|
|
113
|
+
{ position: "bottom", size: "full", class: "h-screen" },
|
|
114
|
+
],
|
|
115
|
+
defaultVariants: {
|
|
116
|
+
position: "end",
|
|
117
|
+
size: "md",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
type DrawerContentVariants = VariantProps<typeof drawerContentVariants>;
|
|
123
|
+
|
|
124
|
+
type DrawerContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
|
|
125
|
+
DrawerContentVariants & {
|
|
126
|
+
closeOnOverlayClick?: boolean;
|
|
127
|
+
closeOnEscape?: boolean;
|
|
128
|
+
confirmBeforeClose?: boolean | (() => boolean | Promise<boolean>);
|
|
129
|
+
loading?: boolean;
|
|
130
|
+
scrollable?: boolean;
|
|
131
|
+
hideCloseButton?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* When `false`, the drawer can only be closed programmatically:
|
|
134
|
+
* - hides the close X
|
|
135
|
+
* - blocks overlay click
|
|
136
|
+
* - blocks Escape
|
|
137
|
+
*/
|
|
138
|
+
dismissible?: boolean;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const DrawerContent = forwardRef<
|
|
142
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
143
|
+
DrawerContentProps
|
|
144
|
+
>(function DrawerContent(
|
|
145
|
+
{
|
|
146
|
+
className,
|
|
147
|
+
position,
|
|
148
|
+
size,
|
|
149
|
+
closeOnOverlayClick = true,
|
|
150
|
+
closeOnEscape = true,
|
|
151
|
+
confirmBeforeClose,
|
|
152
|
+
loading = false,
|
|
153
|
+
scrollable = true,
|
|
154
|
+
hideCloseButton = false,
|
|
155
|
+
dismissible = true,
|
|
156
|
+
onPointerDownOutside,
|
|
157
|
+
onEscapeKeyDown,
|
|
158
|
+
onInteractOutside,
|
|
159
|
+
children,
|
|
160
|
+
...props
|
|
161
|
+
},
|
|
162
|
+
ref,
|
|
163
|
+
) {
|
|
164
|
+
const guardedClose = async (e: Event): Promise<void> => {
|
|
165
|
+
if (!confirmBeforeClose) return;
|
|
166
|
+
if (typeof confirmBeforeClose === "function") {
|
|
167
|
+
const ok = await confirmBeforeClose();
|
|
168
|
+
if (!ok) e.preventDefault();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const ok = window.confirm("Discard unsaved changes?");
|
|
172
|
+
if (!ok) e.preventDefault();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const allowOverlayClose = dismissible && closeOnOverlayClick;
|
|
176
|
+
const allowEscape = dismissible && closeOnEscape;
|
|
177
|
+
|
|
178
|
+
const drawerCtx = useMemo<DrawerContextValue>(() => ({ scrollable }), [scrollable]);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<DrawerContext.Provider value={drawerCtx}>
|
|
182
|
+
<DrawerPortal>
|
|
183
|
+
<DrawerOverlay />
|
|
184
|
+
<DialogPrimitive.Content
|
|
185
|
+
ref={ref}
|
|
186
|
+
onPointerDownOutside={(e) => {
|
|
187
|
+
if (!allowOverlayClose) e.preventDefault();
|
|
188
|
+
onPointerDownOutside?.(e);
|
|
189
|
+
}}
|
|
190
|
+
onEscapeKeyDown={(e) => {
|
|
191
|
+
if (!allowEscape) e.preventDefault();
|
|
192
|
+
onEscapeKeyDown?.(e);
|
|
193
|
+
}}
|
|
194
|
+
onInteractOutside={async (e) => {
|
|
195
|
+
if (confirmBeforeClose && allowOverlayClose) {
|
|
196
|
+
await guardedClose(e as unknown as Event);
|
|
197
|
+
}
|
|
198
|
+
onInteractOutside?.(e);
|
|
199
|
+
}}
|
|
200
|
+
className={cn(drawerContentVariants({ position, size }), className)}
|
|
201
|
+
{...props}
|
|
202
|
+
>
|
|
203
|
+
{loading ? (
|
|
204
|
+
<div className="space-y-3 p-6" aria-busy="true" aria-live="polite">
|
|
205
|
+
<Skeleton className="h-6 w-2/5" />
|
|
206
|
+
<Skeleton className="h-4 w-3/5" />
|
|
207
|
+
<Skeleton className="h-4 w-full" />
|
|
208
|
+
<Skeleton className="h-4 w-10/12" />
|
|
209
|
+
<Skeleton className="h-4 w-4/5" />
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
children
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{dismissible && !hideCloseButton && !loading ? (
|
|
216
|
+
<DialogPrimitive.Close
|
|
217
|
+
aria-label="Close"
|
|
218
|
+
className={cn(
|
|
219
|
+
"absolute end-3 top-3 inline-flex size-7 items-center justify-center rounded-md",
|
|
220
|
+
"text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
221
|
+
"focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
222
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
<RiCloseLine />
|
|
226
|
+
</DialogPrimitive.Close>
|
|
227
|
+
) : null}
|
|
228
|
+
|
|
229
|
+
{/* Overflow + scroll containment is owned by DrawerBody — the
|
|
230
|
+
`scrollable` flag on DrawerContent gates how DrawerBody styles
|
|
231
|
+
itself via DrawerContext. */}
|
|
232
|
+
</DialogPrimitive.Content>
|
|
233
|
+
</DrawerPortal>
|
|
234
|
+
</DrawerContext.Provider>
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
DrawerContent.displayName = "DrawerContent";
|
|
238
|
+
|
|
239
|
+
// ----------------------------------------------------------------------------
|
|
240
|
+
// DrawerHeader / Title / Description / Body / Footer
|
|
241
|
+
// ----------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
const DrawerHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
244
|
+
function DrawerHeader({ className, ...props }, ref) {
|
|
245
|
+
return (
|
|
246
|
+
<div ref={ref} className={cn("space-y-1.5 p-6 pe-12 text-start", className)} {...props} />
|
|
247
|
+
);
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
DrawerHeader.displayName = "DrawerHeader";
|
|
251
|
+
|
|
252
|
+
const DrawerTitle = forwardRef<
|
|
253
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
254
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
255
|
+
>(function DrawerTitle({ className, ...props }, ref) {
|
|
256
|
+
return (
|
|
257
|
+
<DialogPrimitive.Title
|
|
258
|
+
ref={ref}
|
|
259
|
+
className={cn("text-lg leading-snug font-semibold tracking-tight", className)}
|
|
260
|
+
{...props}
|
|
261
|
+
/>
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
DrawerTitle.displayName = "DrawerTitle";
|
|
265
|
+
|
|
266
|
+
const DrawerDescription = forwardRef<
|
|
267
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
268
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
269
|
+
>(function DrawerDescription({ className, ...props }, ref) {
|
|
270
|
+
return (
|
|
271
|
+
<DialogPrimitive.Description
|
|
272
|
+
ref={ref}
|
|
273
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
274
|
+
{...props}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
DrawerDescription.displayName = "DrawerDescription";
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Scroll region between header and footer. Always flex-1; overflow behavior
|
|
282
|
+
* follows DrawerContent's `scrollable` flag (default true) — when false, the
|
|
283
|
+
* body sizes to its content and DrawerContent's own height controls overflow.
|
|
284
|
+
*/
|
|
285
|
+
const DrawerBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
286
|
+
function DrawerBody({ className, ...props }, ref) {
|
|
287
|
+
const { scrollable } = useDrawerContext();
|
|
288
|
+
return (
|
|
289
|
+
<div
|
|
290
|
+
ref={ref}
|
|
291
|
+
className={cn("flex-1 px-6 pb-6", scrollable && "overflow-y-auto", className)}
|
|
292
|
+
{...props}
|
|
293
|
+
/>
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
DrawerBody.displayName = "DrawerBody";
|
|
298
|
+
|
|
299
|
+
const DrawerFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
300
|
+
function DrawerFooter({ className, ...props }, ref) {
|
|
301
|
+
return (
|
|
302
|
+
<div
|
|
303
|
+
ref={ref}
|
|
304
|
+
className={cn(
|
|
305
|
+
"border-border flex flex-col-reverse items-stretch gap-2 border-t p-4 sm:flex-row sm:items-center sm:justify-end",
|
|
306
|
+
className,
|
|
307
|
+
)}
|
|
308
|
+
{...props}
|
|
309
|
+
/>
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
DrawerFooter.displayName = "DrawerFooter";
|
|
314
|
+
|
|
315
|
+
export {
|
|
316
|
+
Drawer,
|
|
317
|
+
DrawerTrigger,
|
|
318
|
+
DrawerPortal,
|
|
319
|
+
DrawerOverlay,
|
|
320
|
+
DrawerContent,
|
|
321
|
+
DrawerHeader,
|
|
322
|
+
DrawerTitle,
|
|
323
|
+
DrawerDescription,
|
|
324
|
+
DrawerBody,
|
|
325
|
+
DrawerFooter,
|
|
326
|
+
DrawerClose,
|
|
327
|
+
drawerContentVariants,
|
|
328
|
+
};
|
|
329
|
+
export type { DrawerContentProps, DrawerPosition };
|