@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,202 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+ import {
4
+ RiDeleteBinLine,
5
+ RiEditLine,
6
+ RiFileCopyLine,
7
+ RiShareLine,
8
+ } from "@remixicon/react";
9
+
10
+ import {
11
+ ContextMenu,
12
+ ContextMenuCheckboxItem,
13
+ ContextMenuContent,
14
+ ContextMenuItem,
15
+ ContextMenuLabel,
16
+ ContextMenuRadioGroup,
17
+ ContextMenuRadioItem,
18
+ ContextMenuSeparator,
19
+ ContextMenuShortcut,
20
+ ContextMenuSub,
21
+ ContextMenuSubContent,
22
+ ContextMenuSubTrigger,
23
+ ContextMenuTrigger,
24
+ } from "./context-menu";
25
+
26
+ const meta: Meta<typeof ContextMenu> = {
27
+ title: "Overlays/ContextMenu",
28
+ component: ContextMenu,
29
+ parameters: { layout: "centered" },
30
+ };
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof ContextMenu>;
34
+
35
+ function Surface({ label }: { label: string }) {
36
+ return (
37
+ <div className="flex h-40 w-80 items-center justify-center rounded-md border border-dashed border-border text-sm text-muted-foreground">
38
+ {label}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export const Default: Story = {
44
+ render: () => (
45
+ <ContextMenu>
46
+ <ContextMenuTrigger>
47
+ <Surface label="Right-click anywhere in this area" />
48
+ </ContextMenuTrigger>
49
+ <ContextMenuContent>
50
+ <ContextMenuLabel>Actions</ContextMenuLabel>
51
+ <ContextMenuItem icon={<RiEditLine />}>Edit</ContextMenuItem>
52
+ <ContextMenuItem icon={<RiFileCopyLine />}>Duplicate</ContextMenuItem>
53
+ <ContextMenuItem icon={<RiShareLine />}>Share</ContextMenuItem>
54
+ <ContextMenuSeparator />
55
+ <ContextMenuItem variant="destructive" icon={<RiDeleteBinLine />}>
56
+ Delete
57
+ </ContextMenuItem>
58
+ </ContextMenuContent>
59
+ </ContextMenu>
60
+ ),
61
+ };
62
+
63
+ export const WithShortcuts: Story = {
64
+ render: () => (
65
+ <ContextMenu>
66
+ <ContextMenuTrigger>
67
+ <Surface label="Right-click for shortcuts" />
68
+ </ContextMenuTrigger>
69
+ <ContextMenuContent>
70
+ <ContextMenuItem icon={<RiFileCopyLine />}>
71
+ Copy
72
+ <ContextMenuShortcut keys={["⌘", "C"]} />
73
+ </ContextMenuItem>
74
+ <ContextMenuItem icon={<RiEditLine />}>
75
+ Paste
76
+ <ContextMenuShortcut keys={["⌘", "V"]} />
77
+ </ContextMenuItem>
78
+ <ContextMenuSeparator />
79
+ <ContextMenuItem variant="destructive" icon={<RiDeleteBinLine />}>
80
+ Delete
81
+ <ContextMenuShortcut keys={["⌫"]} />
82
+ </ContextMenuItem>
83
+ </ContextMenuContent>
84
+ </ContextMenu>
85
+ ),
86
+ };
87
+
88
+ export const CheckboxItems: Story = {
89
+ render: () => {
90
+ const Wrapper = () => {
91
+ const [grid, setGrid] = useState(true);
92
+ const [guides, setGuides] = useState(false);
93
+ const [snap, setSnap] = useState(false);
94
+ return (
95
+ <ContextMenu>
96
+ <ContextMenuTrigger>
97
+ <Surface label="Right-click — view toggles" />
98
+ </ContextMenuTrigger>
99
+ <ContextMenuContent>
100
+ <ContextMenuLabel>View</ContextMenuLabel>
101
+ <ContextMenuCheckboxItem checked={grid} onCheckedChange={setGrid}>
102
+ Show grid
103
+ </ContextMenuCheckboxItem>
104
+ <ContextMenuCheckboxItem checked={guides} onCheckedChange={setGuides}>
105
+ Show guides
106
+ </ContextMenuCheckboxItem>
107
+ <ContextMenuCheckboxItem checked={snap} onCheckedChange={setSnap}>
108
+ Snap to grid
109
+ </ContextMenuCheckboxItem>
110
+ </ContextMenuContent>
111
+ </ContextMenu>
112
+ );
113
+ };
114
+ return <Wrapper />;
115
+ },
116
+ };
117
+
118
+ export const RadioItems: Story = {
119
+ render: () => {
120
+ const Wrapper = () => {
121
+ const [zoom, setZoom] = useState("100");
122
+ return (
123
+ <ContextMenu>
124
+ <ContextMenuTrigger>
125
+ <Surface label={`Right-click — zoom ${zoom}%`} />
126
+ </ContextMenuTrigger>
127
+ <ContextMenuContent>
128
+ <ContextMenuLabel>Zoom</ContextMenuLabel>
129
+ <ContextMenuRadioGroup value={zoom} onValueChange={setZoom}>
130
+ <ContextMenuRadioItem value="50">50%</ContextMenuRadioItem>
131
+ <ContextMenuRadioItem value="100">100%</ContextMenuRadioItem>
132
+ <ContextMenuRadioItem value="150">150%</ContextMenuRadioItem>
133
+ <ContextMenuRadioItem value="200">200%</ContextMenuRadioItem>
134
+ </ContextMenuRadioGroup>
135
+ </ContextMenuContent>
136
+ </ContextMenu>
137
+ );
138
+ };
139
+ return <Wrapper />;
140
+ },
141
+ };
142
+
143
+ export const Submenus: Story = {
144
+ render: () => (
145
+ <ContextMenu>
146
+ <ContextMenuTrigger>
147
+ <Surface label="Right-click for submenus" />
148
+ </ContextMenuTrigger>
149
+ <ContextMenuContent>
150
+ <ContextMenuItem>Open</ContextMenuItem>
151
+ <ContextMenuSub>
152
+ <ContextMenuSubTrigger>Open with…</ContextMenuSubTrigger>
153
+ <ContextMenuSubContent>
154
+ <ContextMenuItem>Visual Studio Code</ContextMenuItem>
155
+ <ContextMenuItem>Sublime Text</ContextMenuItem>
156
+ <ContextMenuItem>Vim</ContextMenuItem>
157
+ </ContextMenuSubContent>
158
+ </ContextMenuSub>
159
+ <ContextMenuSeparator />
160
+ <ContextMenuSub>
161
+ <ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>
162
+ <ContextMenuSubContent>
163
+ <ContextMenuItem>Documents</ContextMenuItem>
164
+ <ContextMenuItem>Downloads</ContextMenuItem>
165
+ <ContextMenuItem>Trash</ContextMenuItem>
166
+ </ContextMenuSubContent>
167
+ </ContextMenuSub>
168
+ </ContextMenuContent>
169
+ </ContextMenu>
170
+ ),
171
+ };
172
+
173
+ export const WithDescriptions: Story = {
174
+ render: () => (
175
+ <ContextMenu>
176
+ <ContextMenuTrigger>
177
+ <Surface label="Right-click for descriptions" />
178
+ </ContextMenuTrigger>
179
+ <ContextMenuContent>
180
+ <ContextMenuItem
181
+ icon={<RiEditLine />}
182
+ description="Open the rich text editor."
183
+ >
184
+ Edit content
185
+ </ContextMenuItem>
186
+ <ContextMenuItem
187
+ icon={<RiShareLine />}
188
+ description="Generate a shareable link."
189
+ >
190
+ Share
191
+ </ContextMenuItem>
192
+ <ContextMenuItem
193
+ variant="destructive"
194
+ icon={<RiDeleteBinLine />}
195
+ description="Permanently remove."
196
+ >
197
+ Delete
198
+ </ContextMenuItem>
199
+ </ContextMenuContent>
200
+ </ContextMenu>
201
+ ),
202
+ };
@@ -0,0 +1,309 @@
1
+ "use client";
2
+
3
+ import { forwardRef } from "react";
4
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-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 §6. Same API shape as DropdownMenu;
11
+ // the only difference is `Trigger` wraps any element and listens for the
12
+ // context-menu event (right-click / long-press) instead of being a button.
13
+
14
+ const ContextMenu = ContextMenuPrimitive.Root;
15
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
16
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
17
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
18
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
19
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
20
+
21
+ // ----------------------------------------------------------------------------
22
+ // ContextMenuContent
23
+ // ----------------------------------------------------------------------------
24
+
25
+ const contextContentVariants = cva(
26
+ [
27
+ "z-popover min-w-32 overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md",
28
+ "data-[state=open]:animate-[scale-in_var(--duration-fast)_var(--ease-out)]",
29
+ "data-[state=closed]:animate-[scale-out_var(--duration-fast)_var(--ease-in)]",
30
+ ].join(" "),
31
+ {
32
+ variants: {
33
+ size: {
34
+ sm: "py-1 text-xs",
35
+ md: "py-1 text-sm",
36
+ },
37
+ },
38
+ defaultVariants: { size: "md" },
39
+ },
40
+ );
41
+
42
+ type ContextMenuContentProps = React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> &
43
+ VariantProps<typeof contextContentVariants>;
44
+
45
+ const ContextMenuContent = forwardRef<
46
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
47
+ ContextMenuContentProps
48
+ >(function ContextMenuContent({ className, size, ...props }, ref) {
49
+ return (
50
+ <ContextMenuPortal>
51
+ <ContextMenuPrimitive.Content
52
+ ref={ref}
53
+ className={cn(contextContentVariants({ size }), className)}
54
+ {...props}
55
+ />
56
+ </ContextMenuPortal>
57
+ );
58
+ });
59
+ ContextMenuContent.displayName = "ContextMenuContent";
60
+
61
+ // ----------------------------------------------------------------------------
62
+ // Shared item base
63
+ // ----------------------------------------------------------------------------
64
+
65
+ const itemBase = [
66
+ "relative flex cursor-pointer select-none items-start gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none",
67
+ "transition-colors",
68
+ "focus:bg-accent focus:text-accent-foreground",
69
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
70
+ "aria-busy:cursor-progress",
71
+ ].join(" ");
72
+
73
+ // ----------------------------------------------------------------------------
74
+ // ContextMenuItem (Emara additions: variant/icon/description/kbd/loading)
75
+ // ----------------------------------------------------------------------------
76
+
77
+ type ContextMenuItemProps = React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
78
+ variant?: "default" | "destructive";
79
+ icon?: React.ReactNode;
80
+ description?: React.ReactNode;
81
+ kbd?: string[];
82
+ loading?: boolean;
83
+ inset?: boolean;
84
+ };
85
+
86
+ const ContextMenuItem = forwardRef<
87
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
88
+ ContextMenuItemProps
89
+ >(function ContextMenuItem(
90
+ {
91
+ className,
92
+ variant = "default",
93
+ icon,
94
+ description,
95
+ kbd,
96
+ loading = false,
97
+ inset = false,
98
+ children,
99
+ onSelect,
100
+ ...props
101
+ },
102
+ ref,
103
+ ) {
104
+ return (
105
+ <ContextMenuPrimitive.Item
106
+ ref={ref}
107
+ aria-busy={loading || undefined}
108
+ onSelect={(e) => {
109
+ if (loading) {
110
+ e.preventDefault();
111
+ return;
112
+ }
113
+ onSelect?.(e);
114
+ }}
115
+ className={cn(
116
+ itemBase,
117
+ variant === "destructive" &&
118
+ "text-destructive focus:bg-destructive/10 focus:text-destructive",
119
+ inset && "ps-2",
120
+ className,
121
+ )}
122
+ {...props}
123
+ >
124
+ <span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center text-current">
125
+ {loading ? <RiLoader2Line className="size-3.5 animate-spin" /> : (icon ?? null)}
126
+ </span>
127
+ <span className="flex flex-1 flex-col gap-0.5 leading-none">
128
+ <span>{children}</span>
129
+ {description ? <span className="text-muted-foreground text-xs">{description}</span> : null}
130
+ </span>
131
+ {kbd ? (
132
+ <span className="text-muted-foreground ms-auto inline-flex items-center gap-0.5 text-xs">
133
+ {kbd.map((k, i) => (
134
+ <kbd
135
+ key={`${k}-${i}`}
136
+ className="border-border bg-muted min-w-[1em] rounded border px-1 font-mono text-[10px] leading-tight"
137
+ >
138
+ {k}
139
+ </kbd>
140
+ ))}
141
+ </span>
142
+ ) : null}
143
+ </ContextMenuPrimitive.Item>
144
+ );
145
+ });
146
+ ContextMenuItem.displayName = "ContextMenuItem";
147
+
148
+ // ----------------------------------------------------------------------------
149
+ // CheckboxItem / RadioItem
150
+ // ----------------------------------------------------------------------------
151
+
152
+ const ContextMenuCheckboxItem = forwardRef<
153
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
154
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
155
+ >(function ContextMenuCheckboxItem({ className, children, ...props }, ref) {
156
+ return (
157
+ <ContextMenuPrimitive.CheckboxItem ref={ref} className={cn(itemBase, className)} {...props}>
158
+ <span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center">
159
+ <ContextMenuPrimitive.ItemIndicator>
160
+ <RiCheckLine className="size-4" />
161
+ </ContextMenuPrimitive.ItemIndicator>
162
+ </span>
163
+ <span className="flex-1">{children}</span>
164
+ </ContextMenuPrimitive.CheckboxItem>
165
+ );
166
+ });
167
+ ContextMenuCheckboxItem.displayName = "ContextMenuCheckboxItem";
168
+
169
+ const ContextMenuRadioItem = forwardRef<
170
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
171
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
172
+ >(function ContextMenuRadioItem({ className, children, ...props }, ref) {
173
+ return (
174
+ <ContextMenuPrimitive.RadioItem ref={ref} className={cn(itemBase, className)} {...props}>
175
+ <span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center">
176
+ <ContextMenuPrimitive.ItemIndicator>
177
+ <RiCircleFill className="size-2 fill-current" />
178
+ </ContextMenuPrimitive.ItemIndicator>
179
+ </span>
180
+ <span className="flex-1">{children}</span>
181
+ </ContextMenuPrimitive.RadioItem>
182
+ );
183
+ });
184
+ ContextMenuRadioItem.displayName = "ContextMenuRadioItem";
185
+
186
+ // ----------------------------------------------------------------------------
187
+ // Label / Separator / Shortcut
188
+ // ----------------------------------------------------------------------------
189
+
190
+ const ContextMenuLabel = forwardRef<
191
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
192
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label>
193
+ >(function ContextMenuLabel({ className, ...props }, ref) {
194
+ return (
195
+ <ContextMenuPrimitive.Label
196
+ ref={ref}
197
+ className={cn(
198
+ "text-muted-foreground px-2 py-1.5 text-xs font-semibold tracking-wide uppercase",
199
+ className,
200
+ )}
201
+ {...props}
202
+ />
203
+ );
204
+ });
205
+ ContextMenuLabel.displayName = "ContextMenuLabel";
206
+
207
+ const ContextMenuSeparator = forwardRef<
208
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
209
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
210
+ >(function ContextMenuSeparator({ className, ...props }, ref) {
211
+ return (
212
+ <ContextMenuPrimitive.Separator
213
+ ref={ref}
214
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
215
+ {...props}
216
+ />
217
+ );
218
+ });
219
+ ContextMenuSeparator.displayName = "ContextMenuSeparator";
220
+
221
+ function ContextMenuShortcut({ keys, className }: { keys: string[]; className?: string }) {
222
+ return (
223
+ <span
224
+ className={cn(
225
+ "text-muted-foreground ms-auto inline-flex items-center gap-0.5 text-xs",
226
+ className,
227
+ )}
228
+ dir="ltr"
229
+ >
230
+ {keys.map((k, i) => (
231
+ <span key={`${k}-${i}`} className="inline-flex items-center gap-0.5">
232
+ {i > 0 ? <span className="opacity-70">+</span> : null}
233
+ <kbd className="border-border bg-muted min-w-[1em] rounded border px-1 font-mono text-[10px] leading-tight">
234
+ {k}
235
+ </kbd>
236
+ </span>
237
+ ))}
238
+ </span>
239
+ );
240
+ }
241
+
242
+ // ----------------------------------------------------------------------------
243
+ // SubTrigger / SubContent
244
+ // ----------------------------------------------------------------------------
245
+
246
+ const ContextMenuSubTrigger = forwardRef<
247
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
248
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
249
+ icon?: React.ReactNode;
250
+ }
251
+ >(function ContextMenuSubTrigger({ className, icon, children, ...props }, ref) {
252
+ return (
253
+ <ContextMenuPrimitive.SubTrigger
254
+ ref={ref}
255
+ className={cn(
256
+ itemBase,
257
+ "data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
258
+ className,
259
+ )}
260
+ {...props}
261
+ >
262
+ <span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center">
263
+ {icon ?? null}
264
+ </span>
265
+ <span className="flex-1">{children}</span>
266
+ <RiArrowRightSLine className="rtl-mirror ms-auto size-4" aria-hidden="true" />
267
+ </ContextMenuPrimitive.SubTrigger>
268
+ );
269
+ });
270
+ ContextMenuSubTrigger.displayName = "ContextMenuSubTrigger";
271
+
272
+ const ContextMenuSubContent = forwardRef<
273
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
274
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
275
+ >(function ContextMenuSubContent({ className, ...props }, ref) {
276
+ return (
277
+ <ContextMenuPrimitive.SubContent
278
+ ref={ref}
279
+ className={cn(
280
+ "border-border bg-popover text-popover-foreground z-popover min-w-32 overflow-hidden rounded-md border py-1 text-sm shadow-md",
281
+ "data-[state=open]:animate-[scale-in_var(--duration-fast)_var(--ease-out)]",
282
+ "data-[state=closed]:animate-[scale-out_var(--duration-fast)_var(--ease-in)]",
283
+ className,
284
+ )}
285
+ {...props}
286
+ />
287
+ );
288
+ });
289
+ ContextMenuSubContent.displayName = "ContextMenuSubContent";
290
+
291
+ export {
292
+ ContextMenu,
293
+ ContextMenuTrigger,
294
+ ContextMenuContent,
295
+ ContextMenuItem,
296
+ ContextMenuCheckboxItem,
297
+ ContextMenuRadioGroup,
298
+ ContextMenuRadioItem,
299
+ ContextMenuLabel,
300
+ ContextMenuSeparator,
301
+ ContextMenuShortcut,
302
+ ContextMenuGroup,
303
+ ContextMenuPortal,
304
+ ContextMenuSub,
305
+ ContextMenuSubTrigger,
306
+ ContextMenuSubContent,
307
+ contextContentVariants,
308
+ };
309
+ export type { ContextMenuContentProps, ContextMenuItemProps };