@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.
Files changed (218) hide show
  1. package/components/ui/.gitkeep +0 -0
  2. package/components/ui/accordion.stories.tsx +231 -0
  3. package/components/ui/accordion.tsx +250 -0
  4. package/components/ui/app-shell.stories.tsx +270 -0
  5. package/components/ui/app-shell.tsx +491 -0
  6. package/components/ui/avatar.stories.tsx +174 -0
  7. package/components/ui/avatar.tsx +257 -0
  8. package/components/ui/badge.stories.tsx +127 -0
  9. package/components/ui/badge.tsx +146 -0
  10. package/components/ui/breadcrumb.stories.tsx +92 -0
  11. package/components/ui/breadcrumb.tsx +302 -0
  12. package/components/ui/button.stories.tsx +186 -0
  13. package/components/ui/button.tsx +128 -0
  14. package/components/ui/card.stories.tsx +279 -0
  15. package/components/ui/card.tsx +250 -0
  16. package/components/ui/checkbox.stories.tsx +93 -0
  17. package/components/ui/checkbox.tsx +131 -0
  18. package/components/ui/combobox.stories.tsx +489 -0
  19. package/components/ui/combobox.tsx +874 -0
  20. package/components/ui/context-menu.stories.tsx +202 -0
  21. package/components/ui/context-menu.tsx +309 -0
  22. package/components/ui/data-table.stories.tsx +227 -0
  23. package/components/ui/data-table.tsx +539 -0
  24. package/components/ui/date-picker.stories.tsx +225 -0
  25. package/components/ui/date-picker.tsx +597 -0
  26. package/components/ui/dialog.stories.tsx +193 -0
  27. package/components/ui/dialog.tsx +262 -0
  28. package/components/ui/divider.stories.tsx +84 -0
  29. package/components/ui/divider.tsx +135 -0
  30. package/components/ui/drawer.stories.tsx +218 -0
  31. package/components/ui/drawer.tsx +329 -0
  32. package/components/ui/dropdown-menu.stories.tsx +270 -0
  33. package/components/ui/dropdown-menu.tsx +353 -0
  34. package/components/ui/empty-state.stories.tsx +121 -0
  35. package/components/ui/empty-state.tsx +289 -0
  36. package/components/ui/field-group.stories.tsx +201 -0
  37. package/components/ui/field-group.tsx +276 -0
  38. package/components/ui/form.stories.tsx +219 -0
  39. package/components/ui/form.tsx +542 -0
  40. package/components/ui/input.stories.tsx +154 -0
  41. package/components/ui/input.tsx +208 -0
  42. package/components/ui/label.stories.tsx +84 -0
  43. package/components/ui/label.tsx +98 -0
  44. package/components/ui/page-header.stories.tsx +136 -0
  45. package/components/ui/page-header.tsx +315 -0
  46. package/components/ui/pagination.stories.tsx +136 -0
  47. package/components/ui/pagination.tsx +427 -0
  48. package/components/ui/popover.stories.tsx +212 -0
  49. package/components/ui/popover.tsx +167 -0
  50. package/components/ui/radio-group.stories.tsx +96 -0
  51. package/components/ui/radio-group.tsx +250 -0
  52. package/components/ui/select.stories.tsx +203 -0
  53. package/components/ui/select.tsx +318 -0
  54. package/components/ui/sidebar.stories.tsx +186 -0
  55. package/components/ui/sidebar.tsx +623 -0
  56. package/components/ui/skeleton.stories.tsx +131 -0
  57. package/components/ui/skeleton.tsx +311 -0
  58. package/components/ui/switch.stories.tsx +74 -0
  59. package/components/ui/switch.tsx +186 -0
  60. package/components/ui/table.stories.tsx +107 -0
  61. package/components/ui/table.tsx +285 -0
  62. package/components/ui/tabs.stories.tsx +222 -0
  63. package/components/ui/tabs.tsx +287 -0
  64. package/components/ui/textarea.stories.tsx +96 -0
  65. package/components/ui/textarea.tsx +182 -0
  66. package/components/ui/toast.stories.tsx +169 -0
  67. package/components/ui/toast.tsx +250 -0
  68. package/components/ui/tooltip.stories.tsx +146 -0
  69. package/components/ui/tooltip.tsx +156 -0
  70. package/components/ui/top-bar.stories.tsx +182 -0
  71. package/components/ui/top-bar.tsx +155 -0
  72. package/dist/components/ui/accordion.d.ts +45 -0
  73. package/dist/components/ui/accordion.d.ts.map +1 -0
  74. package/dist/components/ui/accordion.js +99 -0
  75. package/dist/components/ui/accordion.js.map +1 -0
  76. package/dist/components/ui/app-shell.d.ts +70 -0
  77. package/dist/components/ui/app-shell.d.ts.map +1 -0
  78. package/dist/components/ui/app-shell.js +199 -0
  79. package/dist/components/ui/app-shell.js.map +1 -0
  80. package/dist/components/ui/avatar.d.ts +41 -0
  81. package/dist/components/ui/avatar.d.ts.map +1 -0
  82. package/dist/components/ui/avatar.js +104 -0
  83. package/dist/components/ui/avatar.js.map +1 -0
  84. package/dist/components/ui/badge.d.ts +27 -0
  85. package/dist/components/ui/badge.d.ts.map +1 -0
  86. package/dist/components/ui/badge.js +65 -0
  87. package/dist/components/ui/badge.js.map +1 -0
  88. package/dist/components/ui/breadcrumb.d.ts +35 -0
  89. package/dist/components/ui/breadcrumb.d.ts.map +1 -0
  90. package/dist/components/ui/breadcrumb.js +88 -0
  91. package/dist/components/ui/breadcrumb.js.map +1 -0
  92. package/dist/components/ui/button.d.ts +26 -0
  93. package/dist/components/ui/button.d.ts.map +1 -0
  94. package/dist/components/ui/button.js +73 -0
  95. package/dist/components/ui/button.js.map +1 -0
  96. package/dist/components/ui/card.d.ts +52 -0
  97. package/dist/components/ui/card.d.ts.map +1 -0
  98. package/dist/components/ui/card.js +96 -0
  99. package/dist/components/ui/card.js.map +1 -0
  100. package/dist/components/ui/checkbox.d.ts +18 -0
  101. package/dist/components/ui/checkbox.d.ts.map +1 -0
  102. package/dist/components/ui/checkbox.js +59 -0
  103. package/dist/components/ui/checkbox.js.map +1 -0
  104. package/dist/components/ui/combobox.d.ts +194 -0
  105. package/dist/components/ui/combobox.d.ts.map +1 -0
  106. package/dist/components/ui/combobox.js +361 -0
  107. package/dist/components/ui/combobox.js.map +1 -0
  108. package/dist/components/ui/context-menu.d.ts +46 -0
  109. package/dist/components/ui/context-menu.d.ts.map +1 -0
  110. package/dist/components/ui/context-menu.js +95 -0
  111. package/dist/components/ui/context-menu.js.map +1 -0
  112. package/dist/components/ui/data-table.d.ts +53 -0
  113. package/dist/components/ui/data-table.d.ts.map +1 -0
  114. package/dist/components/ui/data-table.js +163 -0
  115. package/dist/components/ui/data-table.js.map +1 -0
  116. package/dist/components/ui/date-picker.d.ts +103 -0
  117. package/dist/components/ui/date-picker.d.ts.map +1 -0
  118. package/dist/components/ui/date-picker.js +306 -0
  119. package/dist/components/ui/date-picker.js.map +1 -0
  120. package/dist/components/ui/dialog.d.ts +40 -0
  121. package/dist/components/ui/dialog.d.ts.map +1 -0
  122. package/dist/components/ui/dialog.js +110 -0
  123. package/dist/components/ui/dialog.js.map +1 -0
  124. package/dist/components/ui/divider.d.ts +30 -0
  125. package/dist/components/ui/divider.d.ts.map +1 -0
  126. package/dist/components/ui/divider.js +62 -0
  127. package/dist/components/ui/divider.js.map +1 -0
  128. package/dist/components/ui/drawer.d.ts +56 -0
  129. package/dist/components/ui/drawer.d.ts.map +1 -0
  130. package/dist/components/ui/drawer.js +147 -0
  131. package/dist/components/ui/drawer.js.map +1 -0
  132. package/dist/components/ui/dropdown-menu.d.ts +63 -0
  133. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  134. package/dist/components/ui/dropdown-menu.js +116 -0
  135. package/dist/components/ui/dropdown-menu.js.map +1 -0
  136. package/dist/components/ui/empty-state.d.ts +43 -0
  137. package/dist/components/ui/empty-state.d.ts.map +1 -0
  138. package/dist/components/ui/empty-state.js +128 -0
  139. package/dist/components/ui/empty-state.js.map +1 -0
  140. package/dist/components/ui/field-group.d.ts +38 -0
  141. package/dist/components/ui/field-group.d.ts.map +1 -0
  142. package/dist/components/ui/field-group.js +107 -0
  143. package/dist/components/ui/field-group.js.map +1 -0
  144. package/dist/components/ui/form.d.ts +67 -0
  145. package/dist/components/ui/form.d.ts.map +1 -0
  146. package/dist/components/ui/form.js +286 -0
  147. package/dist/components/ui/form.js.map +1 -0
  148. package/dist/components/ui/input.d.ts +36 -0
  149. package/dist/components/ui/input.d.ts.map +1 -0
  150. package/dist/components/ui/input.js +99 -0
  151. package/dist/components/ui/input.js.map +1 -0
  152. package/dist/components/ui/label.d.ts +37 -0
  153. package/dist/components/ui/label.d.ts.map +1 -0
  154. package/dist/components/ui/label.js +34 -0
  155. package/dist/components/ui/label.js.map +1 -0
  156. package/dist/components/ui/page-header.d.ts +65 -0
  157. package/dist/components/ui/page-header.d.ts.map +1 -0
  158. package/dist/components/ui/page-header.js +140 -0
  159. package/dist/components/ui/page-header.js.map +1 -0
  160. package/dist/components/ui/pagination.d.ts +67 -0
  161. package/dist/components/ui/pagination.d.ts.map +1 -0
  162. package/dist/components/ui/pagination.js +109 -0
  163. package/dist/components/ui/pagination.js.map +1 -0
  164. package/dist/components/ui/popover.d.ts +28 -0
  165. package/dist/components/ui/popover.d.ts.map +1 -0
  166. package/dist/components/ui/popover.js +85 -0
  167. package/dist/components/ui/popover.js.map +1 -0
  168. package/dist/components/ui/radio-group.d.ts +35 -0
  169. package/dist/components/ui/radio-group.d.ts.map +1 -0
  170. package/dist/components/ui/radio-group.js +103 -0
  171. package/dist/components/ui/radio-group.js.map +1 -0
  172. package/dist/components/ui/select.d.ts +42 -0
  173. package/dist/components/ui/select.d.ts.map +1 -0
  174. package/dist/components/ui/select.js +86 -0
  175. package/dist/components/ui/select.js.map +1 -0
  176. package/dist/components/ui/sidebar.d.ts +59 -0
  177. package/dist/components/ui/sidebar.d.ts.map +1 -0
  178. package/dist/components/ui/sidebar.js +189 -0
  179. package/dist/components/ui/sidebar.js.map +1 -0
  180. package/dist/components/ui/skeleton.d.ts +77 -0
  181. package/dist/components/ui/skeleton.d.ts.map +1 -0
  182. package/dist/components/ui/skeleton.js +115 -0
  183. package/dist/components/ui/skeleton.js.map +1 -0
  184. package/dist/components/ui/switch.d.ts +26 -0
  185. package/dist/components/ui/switch.d.ts.map +1 -0
  186. package/dist/components/ui/switch.js +84 -0
  187. package/dist/components/ui/switch.js.map +1 -0
  188. package/dist/components/ui/table.d.ts +52 -0
  189. package/dist/components/ui/table.d.ts.map +1 -0
  190. package/dist/components/ui/table.js +109 -0
  191. package/dist/components/ui/table.js.map +1 -0
  192. package/dist/components/ui/tabs.d.ts +42 -0
  193. package/dist/components/ui/tabs.d.ts.map +1 -0
  194. package/dist/components/ui/tabs.js +163 -0
  195. package/dist/components/ui/tabs.js.map +1 -0
  196. package/dist/components/ui/textarea.d.ts +26 -0
  197. package/dist/components/ui/textarea.d.ts.map +1 -0
  198. package/dist/components/ui/textarea.js +96 -0
  199. package/dist/components/ui/textarea.js.map +1 -0
  200. package/dist/components/ui/toast.d.ts +77 -0
  201. package/dist/components/ui/toast.d.ts.map +1 -0
  202. package/dist/components/ui/toast.js +141 -0
  203. package/dist/components/ui/toast.js.map +1 -0
  204. package/dist/components/ui/tooltip.d.ts +31 -0
  205. package/dist/components/ui/tooltip.d.ts.map +1 -0
  206. package/dist/components/ui/tooltip.js +71 -0
  207. package/dist/components/ui/tooltip.js.map +1 -0
  208. package/dist/components/ui/top-bar.d.ts +30 -0
  209. package/dist/components/ui/top-bar.d.ts.map +1 -0
  210. package/dist/components/ui/top-bar.js +64 -0
  211. package/dist/components/ui/top-bar.js.map +1 -0
  212. package/dist/lib/utils.d.ts +3 -0
  213. package/dist/lib/utils.d.ts.map +1 -0
  214. package/dist/lib/utils.js +6 -0
  215. package/dist/lib/utils.js.map +1 -0
  216. package/lib/utils.ts +6 -0
  217. package/package.json +112 -0
  218. 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 };