@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,270 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
RiDeleteBinLine,
|
|
5
|
+
RiEditLine,
|
|
6
|
+
RiMoreLine,
|
|
7
|
+
RiShareLine,
|
|
8
|
+
RiUserAddLine,
|
|
9
|
+
} from "@remixicon/react";
|
|
10
|
+
|
|
11
|
+
import { Button } from "./button";
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuCheckboxItem,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuGroup,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuLabel,
|
|
19
|
+
DropdownMenuRadioGroup,
|
|
20
|
+
DropdownMenuRadioItem,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuShortcut,
|
|
23
|
+
DropdownMenuSub,
|
|
24
|
+
DropdownMenuSubContent,
|
|
25
|
+
DropdownMenuSubTrigger,
|
|
26
|
+
DropdownMenuTrigger,
|
|
27
|
+
} from "./dropdown-menu";
|
|
28
|
+
|
|
29
|
+
const meta: Meta<typeof DropdownMenu> = {
|
|
30
|
+
title: "Overlays/DropdownMenu",
|
|
31
|
+
component: DropdownMenu,
|
|
32
|
+
parameters: { layout: "centered" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
type Story = StoryObj<typeof DropdownMenu>;
|
|
37
|
+
|
|
38
|
+
export const Default: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<DropdownMenu>
|
|
41
|
+
<DropdownMenuTrigger asChild>
|
|
42
|
+
<Button variant="outline" size="icon" aria-label="More actions">
|
|
43
|
+
<RiMoreLine />
|
|
44
|
+
</Button>
|
|
45
|
+
</DropdownMenuTrigger>
|
|
46
|
+
<DropdownMenuContent width="md">
|
|
47
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
48
|
+
<DropdownMenuItem icon={<RiEditLine />}>Edit</DropdownMenuItem>
|
|
49
|
+
<DropdownMenuItem icon={<RiShareLine />}>Share</DropdownMenuItem>
|
|
50
|
+
<DropdownMenuSeparator />
|
|
51
|
+
<DropdownMenuItem variant="destructive" icon={<RiDeleteBinLine />}>
|
|
52
|
+
Delete
|
|
53
|
+
</DropdownMenuItem>
|
|
54
|
+
</DropdownMenuContent>
|
|
55
|
+
</DropdownMenu>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const WithShortcuts: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<DropdownMenu>
|
|
62
|
+
<DropdownMenuTrigger asChild>
|
|
63
|
+
<Button variant="outline">File</Button>
|
|
64
|
+
</DropdownMenuTrigger>
|
|
65
|
+
<DropdownMenuContent width="md">
|
|
66
|
+
<DropdownMenuItem icon={<RiEditLine />} kbd={["⌘", "S"]}>
|
|
67
|
+
Save
|
|
68
|
+
</DropdownMenuItem>
|
|
69
|
+
<DropdownMenuItem icon={<RiShareLine />} kbd={["⌘", "⇧", "S"]}>
|
|
70
|
+
Save as…
|
|
71
|
+
</DropdownMenuItem>
|
|
72
|
+
<DropdownMenuSeparator />
|
|
73
|
+
<DropdownMenuItem icon={<RiUserAddLine />} kbd={["⌘", "I"]}>
|
|
74
|
+
Invite collaborators
|
|
75
|
+
</DropdownMenuItem>
|
|
76
|
+
</DropdownMenuContent>
|
|
77
|
+
</DropdownMenu>
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const WithDescriptions: Story = {
|
|
82
|
+
render: () => (
|
|
83
|
+
<DropdownMenu>
|
|
84
|
+
<DropdownMenuTrigger asChild>
|
|
85
|
+
<Button variant="outline">Account</Button>
|
|
86
|
+
</DropdownMenuTrigger>
|
|
87
|
+
<DropdownMenuContent width="lg">
|
|
88
|
+
<DropdownMenuItem icon={<RiEditLine />} description="Update your profile information.">
|
|
89
|
+
Profile
|
|
90
|
+
</DropdownMenuItem>
|
|
91
|
+
<DropdownMenuItem
|
|
92
|
+
icon={<RiShareLine />}
|
|
93
|
+
description="Manage your active sessions and tokens."
|
|
94
|
+
>
|
|
95
|
+
Security
|
|
96
|
+
</DropdownMenuItem>
|
|
97
|
+
<DropdownMenuItem
|
|
98
|
+
variant="destructive"
|
|
99
|
+
icon={<RiDeleteBinLine />}
|
|
100
|
+
description="Permanently delete your account and data."
|
|
101
|
+
>
|
|
102
|
+
Delete account
|
|
103
|
+
</DropdownMenuItem>
|
|
104
|
+
</DropdownMenuContent>
|
|
105
|
+
</DropdownMenu>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const CheckboxItems: Story = {
|
|
110
|
+
render: () => {
|
|
111
|
+
const Wrapper = () => {
|
|
112
|
+
const [showStatus, setShowStatus] = useState(true);
|
|
113
|
+
const [showActivity, setShowActivity] = useState(false);
|
|
114
|
+
const [showPanel, setShowPanel] = useState(false);
|
|
115
|
+
return (
|
|
116
|
+
<DropdownMenu>
|
|
117
|
+
<DropdownMenuTrigger asChild>
|
|
118
|
+
<Button variant="outline">View</Button>
|
|
119
|
+
</DropdownMenuTrigger>
|
|
120
|
+
<DropdownMenuContent width="md">
|
|
121
|
+
<DropdownMenuLabel>Panels</DropdownMenuLabel>
|
|
122
|
+
<DropdownMenuCheckboxItem checked={showStatus} onCheckedChange={setShowStatus}>
|
|
123
|
+
Status bar
|
|
124
|
+
</DropdownMenuCheckboxItem>
|
|
125
|
+
<DropdownMenuCheckboxItem checked={showActivity} onCheckedChange={setShowActivity}>
|
|
126
|
+
Activity feed
|
|
127
|
+
</DropdownMenuCheckboxItem>
|
|
128
|
+
<DropdownMenuCheckboxItem checked={showPanel} onCheckedChange={setShowPanel}>
|
|
129
|
+
Side panel
|
|
130
|
+
</DropdownMenuCheckboxItem>
|
|
131
|
+
</DropdownMenuContent>
|
|
132
|
+
</DropdownMenu>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
return <Wrapper />;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const RadioItems: Story = {
|
|
140
|
+
render: () => {
|
|
141
|
+
const Wrapper = () => {
|
|
142
|
+
const [position, setPosition] = useState("bottom");
|
|
143
|
+
return (
|
|
144
|
+
<DropdownMenu>
|
|
145
|
+
<DropdownMenuTrigger asChild>
|
|
146
|
+
<Button variant="outline">Panel position: {position}</Button>
|
|
147
|
+
</DropdownMenuTrigger>
|
|
148
|
+
<DropdownMenuContent width="md">
|
|
149
|
+
<DropdownMenuLabel>Position</DropdownMenuLabel>
|
|
150
|
+
<DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
|
|
151
|
+
<DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
|
|
152
|
+
<DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
|
|
153
|
+
<DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
|
|
154
|
+
<DropdownMenuRadioItem value="left">Left</DropdownMenuRadioItem>
|
|
155
|
+
</DropdownMenuRadioGroup>
|
|
156
|
+
</DropdownMenuContent>
|
|
157
|
+
</DropdownMenu>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
return <Wrapper />;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const Submenus: Story = {
|
|
165
|
+
render: () => (
|
|
166
|
+
<DropdownMenu>
|
|
167
|
+
<DropdownMenuTrigger asChild>
|
|
168
|
+
<Button variant="outline">Settings</Button>
|
|
169
|
+
</DropdownMenuTrigger>
|
|
170
|
+
<DropdownMenuContent width="md">
|
|
171
|
+
<DropdownMenuLabel>Account</DropdownMenuLabel>
|
|
172
|
+
<DropdownMenuItem>Profile</DropdownMenuItem>
|
|
173
|
+
<DropdownMenuItem>Billing</DropdownMenuItem>
|
|
174
|
+
<DropdownMenuSeparator />
|
|
175
|
+
<DropdownMenuSub>
|
|
176
|
+
<DropdownMenuSubTrigger>Team</DropdownMenuSubTrigger>
|
|
177
|
+
<DropdownMenuSubContent>
|
|
178
|
+
<DropdownMenuItem>Members</DropdownMenuItem>
|
|
179
|
+
<DropdownMenuItem>Roles</DropdownMenuItem>
|
|
180
|
+
<DropdownMenuItem>Invitations</DropdownMenuItem>
|
|
181
|
+
</DropdownMenuSubContent>
|
|
182
|
+
</DropdownMenuSub>
|
|
183
|
+
<DropdownMenuSub>
|
|
184
|
+
<DropdownMenuSubTrigger>Notifications</DropdownMenuSubTrigger>
|
|
185
|
+
<DropdownMenuSubContent>
|
|
186
|
+
<DropdownMenuItem>Email</DropdownMenuItem>
|
|
187
|
+
<DropdownMenuItem>SMS</DropdownMenuItem>
|
|
188
|
+
<DropdownMenuItem>Push</DropdownMenuItem>
|
|
189
|
+
</DropdownMenuSubContent>
|
|
190
|
+
</DropdownMenuSub>
|
|
191
|
+
</DropdownMenuContent>
|
|
192
|
+
</DropdownMenu>
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const Sizes: Story = {
|
|
197
|
+
render: () => (
|
|
198
|
+
<div className="flex gap-2">
|
|
199
|
+
<DropdownMenu>
|
|
200
|
+
<DropdownMenuTrigger asChild>
|
|
201
|
+
<Button variant="outline" size="sm">
|
|
202
|
+
sm
|
|
203
|
+
</Button>
|
|
204
|
+
</DropdownMenuTrigger>
|
|
205
|
+
<DropdownMenuContent size="sm">
|
|
206
|
+
<DropdownMenuItem icon={<RiEditLine />}>Compact item</DropdownMenuItem>
|
|
207
|
+
<DropdownMenuItem icon={<RiShareLine />}>Share</DropdownMenuItem>
|
|
208
|
+
</DropdownMenuContent>
|
|
209
|
+
</DropdownMenu>
|
|
210
|
+
<DropdownMenu>
|
|
211
|
+
<DropdownMenuTrigger asChild>
|
|
212
|
+
<Button variant="outline" size="default">
|
|
213
|
+
md
|
|
214
|
+
</Button>
|
|
215
|
+
</DropdownMenuTrigger>
|
|
216
|
+
<DropdownMenuContent size="md">
|
|
217
|
+
<DropdownMenuItem icon={<RiEditLine />}>Comfortable item</DropdownMenuItem>
|
|
218
|
+
<DropdownMenuItem icon={<RiShareLine />}>Share</DropdownMenuItem>
|
|
219
|
+
</DropdownMenuContent>
|
|
220
|
+
</DropdownMenu>
|
|
221
|
+
</div>
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const LoadingItem: Story = {
|
|
226
|
+
render: () => (
|
|
227
|
+
<DropdownMenu>
|
|
228
|
+
<DropdownMenuTrigger asChild>
|
|
229
|
+
<Button variant="outline">Actions</Button>
|
|
230
|
+
</DropdownMenuTrigger>
|
|
231
|
+
<DropdownMenuContent width="md">
|
|
232
|
+
<DropdownMenuItem icon={<RiEditLine />}>Edit</DropdownMenuItem>
|
|
233
|
+
<DropdownMenuItem icon={<RiShareLine />} loading>
|
|
234
|
+
Publishing…
|
|
235
|
+
</DropdownMenuItem>
|
|
236
|
+
<DropdownMenuItem variant="destructive" icon={<RiDeleteBinLine />}>
|
|
237
|
+
Delete
|
|
238
|
+
</DropdownMenuItem>
|
|
239
|
+
</DropdownMenuContent>
|
|
240
|
+
</DropdownMenu>
|
|
241
|
+
),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const Grouped: Story = {
|
|
245
|
+
render: () => (
|
|
246
|
+
<DropdownMenu>
|
|
247
|
+
<DropdownMenuTrigger asChild>
|
|
248
|
+
<Button variant="outline">My account</Button>
|
|
249
|
+
</DropdownMenuTrigger>
|
|
250
|
+
<DropdownMenuContent width="md">
|
|
251
|
+
<DropdownMenuLabel>My account</DropdownMenuLabel>
|
|
252
|
+
<DropdownMenuGroup>
|
|
253
|
+
<DropdownMenuItem icon={<RiEditLine />}>
|
|
254
|
+
Profile
|
|
255
|
+
<DropdownMenuShortcut keys={["⌘", "P"]} />
|
|
256
|
+
</DropdownMenuItem>
|
|
257
|
+
<DropdownMenuItem icon={<RiShareLine />}>
|
|
258
|
+
Billing
|
|
259
|
+
<DropdownMenuShortcut keys={["⌘", "B"]} />
|
|
260
|
+
</DropdownMenuItem>
|
|
261
|
+
</DropdownMenuGroup>
|
|
262
|
+
<DropdownMenuSeparator />
|
|
263
|
+
<DropdownMenuLabel>Team</DropdownMenuLabel>
|
|
264
|
+
<DropdownMenuGroup>
|
|
265
|
+
<DropdownMenuItem icon={<RiUserAddLine />}>Invite users</DropdownMenuItem>
|
|
266
|
+
</DropdownMenuGroup>
|
|
267
|
+
</DropdownMenuContent>
|
|
268
|
+
</DropdownMenu>
|
|
269
|
+
),
|
|
270
|
+
};
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
5
|
+
import { RiArrowRightSLine, RiCheckLine, RiCircleFill, RiLoader2Line } from "@remixicon/react";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
// Per docs/emara-ui-phase-3-components.md §5.
|
|
11
|
+
|
|
12
|
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
13
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
14
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
15
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
16
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
17
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
18
|
+
|
|
19
|
+
// ----------------------------------------------------------------------------
|
|
20
|
+
// DropdownMenuContent + size/width variants
|
|
21
|
+
// ----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const dropdownContentVariants = cva(
|
|
24
|
+
[
|
|
25
|
+
"z-popover min-w-32 overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md",
|
|
26
|
+
"data-[state=open]:animate-[scale-in_var(--duration-fast)_var(--ease-out)]",
|
|
27
|
+
"data-[state=closed]:animate-[scale-out_var(--duration-fast)_var(--ease-in)]",
|
|
28
|
+
].join(" "),
|
|
29
|
+
{
|
|
30
|
+
variants: {
|
|
31
|
+
size: {
|
|
32
|
+
sm: "py-1 text-xs [&_[data-slot=item]]:py-1 [&_[data-slot=item]]:ps-7 [&_[data-slot=item]]:pe-2",
|
|
33
|
+
md: "py-1 text-sm [&_[data-slot=item]]:py-1.5 [&_[data-slot=item]]:ps-8 [&_[data-slot=item]]:pe-2",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: { size: "md" },
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
type DropdownWidth = "fit" | "trigger" | "sm" | "md" | "lg" | number;
|
|
41
|
+
|
|
42
|
+
const WIDTH_PX: Record<Exclude<DropdownWidth, "fit" | "trigger" | number>, number> = {
|
|
43
|
+
sm: 192,
|
|
44
|
+
md: 256,
|
|
45
|
+
lg: 320,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function resolveWidthStyle(width: DropdownWidth | undefined): React.CSSProperties | undefined {
|
|
49
|
+
if (width === undefined) return undefined;
|
|
50
|
+
if (width === "fit") return { width: "max-content" };
|
|
51
|
+
if (width === "trigger") return { width: "var(--radix-dropdown-menu-trigger-width)" };
|
|
52
|
+
if (typeof width === "number") return { width: width };
|
|
53
|
+
return { width: WIDTH_PX[width] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type DropdownMenuContentProps = React.ComponentPropsWithoutRef<
|
|
57
|
+
typeof DropdownMenuPrimitive.Content
|
|
58
|
+
> &
|
|
59
|
+
VariantProps<typeof dropdownContentVariants> & {
|
|
60
|
+
width?: DropdownWidth;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const DropdownMenuContent = forwardRef<
|
|
64
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
65
|
+
DropdownMenuContentProps
|
|
66
|
+
>(function DropdownMenuContent({ className, size, width, sideOffset = 4, style, ...props }, ref) {
|
|
67
|
+
return (
|
|
68
|
+
<DropdownMenuPortal>
|
|
69
|
+
<DropdownMenuPrimitive.Content
|
|
70
|
+
ref={ref}
|
|
71
|
+
sideOffset={sideOffset}
|
|
72
|
+
style={{ ...resolveWidthStyle(width), ...style }}
|
|
73
|
+
className={cn(dropdownContentVariants({ size }), className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
</DropdownMenuPortal>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
DropdownMenuContent.displayName = "DropdownMenuContent";
|
|
80
|
+
|
|
81
|
+
// ----------------------------------------------------------------------------
|
|
82
|
+
// Shared item visuals
|
|
83
|
+
// ----------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const itemBase = [
|
|
86
|
+
"relative flex cursor-pointer select-none items-start gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none",
|
|
87
|
+
"transition-colors",
|
|
88
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
89
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
90
|
+
"aria-busy:cursor-progress",
|
|
91
|
+
].join(" ");
|
|
92
|
+
|
|
93
|
+
// ----------------------------------------------------------------------------
|
|
94
|
+
// DropdownMenuItem (Emara additions: variant/icon/description/kbd/loading)
|
|
95
|
+
// ----------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
type DropdownMenuItemProps = React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
98
|
+
variant?: "default" | "destructive";
|
|
99
|
+
icon?: React.ReactNode;
|
|
100
|
+
description?: React.ReactNode;
|
|
101
|
+
kbd?: string[];
|
|
102
|
+
loading?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* `true` keeps the menu open after selection (useful for filter-style menus
|
|
105
|
+
* that toggle multiple options). Default is `false` (menu closes).
|
|
106
|
+
*/
|
|
107
|
+
inset?: boolean;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const DropdownMenuItem = forwardRef<
|
|
111
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
112
|
+
DropdownMenuItemProps
|
|
113
|
+
>(function DropdownMenuItem(
|
|
114
|
+
{
|
|
115
|
+
className,
|
|
116
|
+
variant = "default",
|
|
117
|
+
icon,
|
|
118
|
+
description,
|
|
119
|
+
kbd,
|
|
120
|
+
loading = false,
|
|
121
|
+
inset = false,
|
|
122
|
+
children,
|
|
123
|
+
onSelect,
|
|
124
|
+
...props
|
|
125
|
+
},
|
|
126
|
+
ref,
|
|
127
|
+
) {
|
|
128
|
+
return (
|
|
129
|
+
<DropdownMenuPrimitive.Item
|
|
130
|
+
ref={ref}
|
|
131
|
+
data-slot="item"
|
|
132
|
+
aria-busy={loading || undefined}
|
|
133
|
+
onSelect={(e) => {
|
|
134
|
+
if (loading) {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
onSelect?.(e);
|
|
139
|
+
}}
|
|
140
|
+
className={cn(
|
|
141
|
+
itemBase,
|
|
142
|
+
variant === "destructive" &&
|
|
143
|
+
"text-destructive focus:bg-destructive/10 focus:text-destructive",
|
|
144
|
+
inset && "ps-2",
|
|
145
|
+
className,
|
|
146
|
+
)}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
<span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center text-current">
|
|
150
|
+
{loading ? <RiLoader2Line className="size-3.5 animate-spin" /> : (icon ?? null)}
|
|
151
|
+
</span>
|
|
152
|
+
<span className="flex flex-1 flex-col gap-0.5 leading-none">
|
|
153
|
+
<span>{children}</span>
|
|
154
|
+
{description ? <span className="text-muted-foreground text-xs">{description}</span> : null}
|
|
155
|
+
</span>
|
|
156
|
+
{kbd ? (
|
|
157
|
+
<span className="text-muted-foreground ms-auto inline-flex items-center gap-0.5 text-xs">
|
|
158
|
+
{kbd.map((k, i) => (
|
|
159
|
+
<kbd
|
|
160
|
+
key={`${k}-${i}`}
|
|
161
|
+
className="border-border bg-muted min-w-[1em] rounded border px-1 font-mono text-[10px] leading-tight"
|
|
162
|
+
>
|
|
163
|
+
{k}
|
|
164
|
+
</kbd>
|
|
165
|
+
))}
|
|
166
|
+
</span>
|
|
167
|
+
) : null}
|
|
168
|
+
</DropdownMenuPrimitive.Item>
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
DropdownMenuItem.displayName = "DropdownMenuItem";
|
|
172
|
+
|
|
173
|
+
// ----------------------------------------------------------------------------
|
|
174
|
+
// DropdownMenuCheckboxItem
|
|
175
|
+
// ----------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
const DropdownMenuCheckboxItem = forwardRef<
|
|
178
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
179
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
180
|
+
>(function DropdownMenuCheckboxItem({ className, children, ...props }, ref) {
|
|
181
|
+
return (
|
|
182
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
183
|
+
ref={ref}
|
|
184
|
+
data-slot="item"
|
|
185
|
+
className={cn(itemBase, className)}
|
|
186
|
+
{...props}
|
|
187
|
+
>
|
|
188
|
+
<span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center">
|
|
189
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
190
|
+
<RiCheckLine className="size-4" />
|
|
191
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
192
|
+
</span>
|
|
193
|
+
<span className="flex-1">{children}</span>
|
|
194
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
|
|
198
|
+
|
|
199
|
+
// ----------------------------------------------------------------------------
|
|
200
|
+
// DropdownMenuRadioItem
|
|
201
|
+
// ----------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
const DropdownMenuRadioItem = forwardRef<
|
|
204
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
205
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
206
|
+
>(function DropdownMenuRadioItem({ className, children, ...props }, ref) {
|
|
207
|
+
return (
|
|
208
|
+
<DropdownMenuPrimitive.RadioItem
|
|
209
|
+
ref={ref}
|
|
210
|
+
data-slot="item"
|
|
211
|
+
className={cn(itemBase, className)}
|
|
212
|
+
{...props}
|
|
213
|
+
>
|
|
214
|
+
<span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center">
|
|
215
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
216
|
+
<RiCircleFill className="size-2 fill-current" />
|
|
217
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
218
|
+
</span>
|
|
219
|
+
<span className="flex-1">{children}</span>
|
|
220
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem";
|
|
224
|
+
|
|
225
|
+
// ----------------------------------------------------------------------------
|
|
226
|
+
// DropdownMenuLabel / Separator / Shortcut
|
|
227
|
+
// ----------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
const DropdownMenuLabel = forwardRef<
|
|
230
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
231
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
|
|
232
|
+
>(function DropdownMenuLabel({ className, ...props }, ref) {
|
|
233
|
+
return (
|
|
234
|
+
<DropdownMenuPrimitive.Label
|
|
235
|
+
ref={ref}
|
|
236
|
+
className={cn(
|
|
237
|
+
"text-muted-foreground px-2 py-1.5 text-xs font-semibold tracking-wide uppercase",
|
|
238
|
+
className,
|
|
239
|
+
)}
|
|
240
|
+
{...props}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
DropdownMenuLabel.displayName = "DropdownMenuLabel";
|
|
245
|
+
|
|
246
|
+
const DropdownMenuSeparator = forwardRef<
|
|
247
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
248
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
249
|
+
>(function DropdownMenuSeparator({ className, ...props }, ref) {
|
|
250
|
+
return (
|
|
251
|
+
<DropdownMenuPrimitive.Separator
|
|
252
|
+
ref={ref}
|
|
253
|
+
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
254
|
+
{...props}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* DropdownMenuShortcut — display-only keyboard hint at the end of an item.
|
|
262
|
+
* Renders inline kbd-styled keys, separated by `+`. Stays LTR in RTL.
|
|
263
|
+
*/
|
|
264
|
+
function DropdownMenuShortcut({ keys, className }: { keys: string[]; className?: string }) {
|
|
265
|
+
return (
|
|
266
|
+
<span
|
|
267
|
+
className={cn(
|
|
268
|
+
"text-muted-foreground ms-auto inline-flex items-center gap-0.5 text-xs",
|
|
269
|
+
className,
|
|
270
|
+
)}
|
|
271
|
+
dir="ltr"
|
|
272
|
+
>
|
|
273
|
+
{keys.map((k, i) => (
|
|
274
|
+
<span key={`${k}-${i}`} className="inline-flex items-center gap-0.5">
|
|
275
|
+
{i > 0 ? <span className="opacity-70">+</span> : null}
|
|
276
|
+
<kbd className="border-border bg-muted min-w-[1em] rounded border px-1 font-mono text-[10px] leading-tight">
|
|
277
|
+
{k}
|
|
278
|
+
</kbd>
|
|
279
|
+
</span>
|
|
280
|
+
))}
|
|
281
|
+
</span>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ----------------------------------------------------------------------------
|
|
286
|
+
// DropdownMenuSubTrigger / DropdownMenuSubContent
|
|
287
|
+
// ----------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
const DropdownMenuSubTrigger = forwardRef<
|
|
290
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
291
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
292
|
+
icon?: React.ReactNode;
|
|
293
|
+
}
|
|
294
|
+
>(function DropdownMenuSubTrigger({ className, icon, children, ...props }, ref) {
|
|
295
|
+
return (
|
|
296
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
297
|
+
ref={ref}
|
|
298
|
+
data-slot="item"
|
|
299
|
+
className={cn(
|
|
300
|
+
itemBase,
|
|
301
|
+
"data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
302
|
+
className,
|
|
303
|
+
)}
|
|
304
|
+
{...props}
|
|
305
|
+
>
|
|
306
|
+
<span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center">
|
|
307
|
+
{icon ?? null}
|
|
308
|
+
</span>
|
|
309
|
+
<span className="flex-1">{children}</span>
|
|
310
|
+
<RiArrowRightSLine className="rtl-mirror ms-auto size-4" aria-hidden="true" />
|
|
311
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger";
|
|
315
|
+
|
|
316
|
+
const DropdownMenuSubContent = forwardRef<
|
|
317
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
318
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
319
|
+
>(function DropdownMenuSubContent({ className, ...props }, ref) {
|
|
320
|
+
return (
|
|
321
|
+
<DropdownMenuPrimitive.SubContent
|
|
322
|
+
ref={ref}
|
|
323
|
+
className={cn(
|
|
324
|
+
"border-border bg-popover text-popover-foreground z-popover min-w-32 overflow-hidden rounded-md border py-1 text-sm shadow-md",
|
|
325
|
+
"data-[state=open]:animate-[scale-in_var(--duration-fast)_var(--ease-out)]",
|
|
326
|
+
"data-[state=closed]:animate-[scale-out_var(--duration-fast)_var(--ease-in)]",
|
|
327
|
+
className,
|
|
328
|
+
)}
|
|
329
|
+
{...props}
|
|
330
|
+
/>
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
DropdownMenuSubContent.displayName = "DropdownMenuSubContent";
|
|
334
|
+
|
|
335
|
+
export {
|
|
336
|
+
DropdownMenu,
|
|
337
|
+
DropdownMenuTrigger,
|
|
338
|
+
DropdownMenuContent,
|
|
339
|
+
DropdownMenuItem,
|
|
340
|
+
DropdownMenuCheckboxItem,
|
|
341
|
+
DropdownMenuRadioGroup,
|
|
342
|
+
DropdownMenuRadioItem,
|
|
343
|
+
DropdownMenuLabel,
|
|
344
|
+
DropdownMenuSeparator,
|
|
345
|
+
DropdownMenuShortcut,
|
|
346
|
+
DropdownMenuGroup,
|
|
347
|
+
DropdownMenuPortal,
|
|
348
|
+
DropdownMenuSub,
|
|
349
|
+
DropdownMenuSubTrigger,
|
|
350
|
+
DropdownMenuSubContent,
|
|
351
|
+
dropdownContentVariants,
|
|
352
|
+
};
|
|
353
|
+
export type { DropdownMenuContentProps, DropdownMenuItemProps };
|