omnira-ui 0.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/cli/omnira-init.mjs +260 -0
  4. package/cli/presets.mjs +386 -0
  5. package/components/ui/AppStoreButton/AppStoreButton.module.css +87 -0
  6. package/components/ui/AppStoreButton/AppStoreButton.tsx +114 -0
  7. package/components/ui/AppStoreButton/index.ts +2 -0
  8. package/components/ui/Avatar/Avatar.module.css +76 -0
  9. package/components/ui/Avatar/Avatar.tsx +102 -0
  10. package/components/ui/Avatar/index.ts +2 -0
  11. package/components/ui/Badge/Badge.module.css +120 -0
  12. package/components/ui/Badge/Badge.tsx +25 -0
  13. package/components/ui/Badge/index.ts +2 -0
  14. package/components/ui/BadgeGroup/BadgeGroup.module.css +214 -0
  15. package/components/ui/BadgeGroup/BadgeGroup.tsx +88 -0
  16. package/components/ui/BadgeGroup/index.ts +2 -0
  17. package/components/ui/Browser/Browser.module.css +105 -0
  18. package/components/ui/Browser/Browser.tsx +54 -0
  19. package/components/ui/Browser/index.ts +2 -0
  20. package/components/ui/Button/Button.module.css +188 -0
  21. package/components/ui/Button/Button.tsx +74 -0
  22. package/components/ui/Button/index.ts +2 -0
  23. package/components/ui/ButtonUtility/ButtonUtility.module.css +154 -0
  24. package/components/ui/ButtonUtility/ButtonUtility.tsx +65 -0
  25. package/components/ui/ButtonUtility/index.ts +2 -0
  26. package/components/ui/Card/Card.module.css +80 -0
  27. package/components/ui/Card/Card.tsx +39 -0
  28. package/components/ui/Card/index.ts +2 -0
  29. package/components/ui/Checkbox/Checkbox.module.css +92 -0
  30. package/components/ui/Checkbox/Checkbox.tsx +95 -0
  31. package/components/ui/Checkbox/index.ts +2 -0
  32. package/components/ui/Collapse/Collapse.module.css +60 -0
  33. package/components/ui/Collapse/Collapse.tsx +100 -0
  34. package/components/ui/Collapse/index.ts +2 -0
  35. package/components/ui/CreditCard/CreditCard.module.css +117 -0
  36. package/components/ui/CreditCard/CreditCard.tsx +86 -0
  37. package/components/ui/CreditCard/index.ts +2 -0
  38. package/components/ui/Dropdown/Dropdown.module.css +269 -0
  39. package/components/ui/Dropdown/Dropdown.tsx +419 -0
  40. package/components/ui/Dropdown/index.ts +31 -0
  41. package/components/ui/GridLines/GridLines.module.css +95 -0
  42. package/components/ui/GridLines/GridLines.tsx +10 -0
  43. package/components/ui/GridLines/index.ts +1 -0
  44. package/components/ui/Illustration/Illustration.module.css +10 -0
  45. package/components/ui/Illustration/Illustration.tsx +96 -0
  46. package/components/ui/Illustration/index.ts +2 -0
  47. package/components/ui/Input/Input.module.css +217 -0
  48. package/components/ui/Input/Input.tsx +93 -0
  49. package/components/ui/Input/index.ts +2 -0
  50. package/components/ui/Phone/Phone.module.css +104 -0
  51. package/components/ui/Phone/Phone.tsx +54 -0
  52. package/components/ui/Phone/index.ts +2 -0
  53. package/components/ui/PinInput/PinInput.module.css +104 -0
  54. package/components/ui/PinInput/PinInput.tsx +210 -0
  55. package/components/ui/PinInput/index.ts +1 -0
  56. package/components/ui/ProgressBar/ProgressBar.module.css +180 -0
  57. package/components/ui/ProgressBar/ProgressBar.tsx +206 -0
  58. package/components/ui/ProgressBar/index.ts +2 -0
  59. package/components/ui/QRCode/QRCode.module.css +13 -0
  60. package/components/ui/QRCode/QRCode.tsx +128 -0
  61. package/components/ui/QRCode/index.ts +2 -0
  62. package/components/ui/RadioButton/RadioButton.module.css +92 -0
  63. package/components/ui/RadioButton/RadioButton.tsx +134 -0
  64. package/components/ui/RadioButton/index.ts +2 -0
  65. package/components/ui/RadioGroup/RadioGroup.module.css +382 -0
  66. package/components/ui/RadioGroup/RadioGroup.tsx +292 -0
  67. package/components/ui/RadioGroup/index.ts +1 -0
  68. package/components/ui/Rating/Rating.module.css +99 -0
  69. package/components/ui/Rating/Rating.tsx +123 -0
  70. package/components/ui/Rating/index.ts +2 -0
  71. package/components/ui/Select/Select.module.css +371 -0
  72. package/components/ui/Select/Select.tsx +507 -0
  73. package/components/ui/Select/index.ts +2 -0
  74. package/components/ui/SidebarNavigation/SidebarDual.tsx +71 -0
  75. package/components/ui/SidebarNavigation/SidebarFeatureCard.module.css +351 -0
  76. package/components/ui/SidebarNavigation/SidebarFeatureCard.tsx +388 -0
  77. package/components/ui/SidebarNavigation/SidebarNavigation.module.css +610 -0
  78. package/components/ui/SidebarNavigation/SidebarParts.tsx +215 -0
  79. package/components/ui/SidebarNavigation/SidebarSectionDividers.tsx +46 -0
  80. package/components/ui/SidebarNavigation/SidebarSectionHeadings.tsx +52 -0
  81. package/components/ui/SidebarNavigation/SidebarSimple.tsx +46 -0
  82. package/components/ui/SidebarNavigation/SidebarSlim.tsx +48 -0
  83. package/components/ui/SidebarNavigation/index.ts +29 -0
  84. package/components/ui/SidebarNavigation/types.ts +27 -0
  85. package/components/ui/Slider/Slider.module.css +93 -0
  86. package/components/ui/Slider/Slider.tsx +118 -0
  87. package/components/ui/Slider/index.ts +2 -0
  88. package/components/ui/SocialButton/SocialButton.module.css +223 -0
  89. package/components/ui/SocialButton/SocialButton.tsx +185 -0
  90. package/components/ui/SocialButton/index.ts +2 -0
  91. package/components/ui/Tag/Tag.module.css +203 -0
  92. package/components/ui/Tag/Tag.tsx +161 -0
  93. package/components/ui/Tag/index.ts +2 -0
  94. package/components/ui/TextEditor/TextEditor.module.css +182 -0
  95. package/components/ui/TextEditor/TextEditor.tsx +323 -0
  96. package/components/ui/TextEditor/index.ts +1 -0
  97. package/components/ui/Textarea/Textarea.module.css +75 -0
  98. package/components/ui/Textarea/Textarea.tsx +59 -0
  99. package/components/ui/Textarea/index.ts +2 -0
  100. package/components/ui/Toggle/Toggle.module.css +118 -0
  101. package/components/ui/Toggle/Toggle.tsx +77 -0
  102. package/components/ui/Toggle/index.ts +2 -0
  103. package/components/ui/Tooltip/Tooltip.module.css +95 -0
  104. package/components/ui/Tooltip/Tooltip.tsx +55 -0
  105. package/components/ui/Tooltip/index.ts +2 -0
  106. package/components/ui/VideoPlayer/VideoPlayer.module.css +120 -0
  107. package/components/ui/VideoPlayer/VideoPlayer.tsx +229 -0
  108. package/components/ui/VideoPlayer/index.ts +2 -0
  109. package/lib/cn.ts +3 -0
  110. package/lib/copy-to-clipboard.ts +21 -0
  111. package/package.json +61 -0
@@ -0,0 +1,269 @@
1
+ /* ── Root ── */
2
+ .root {
3
+ position: relative;
4
+ display: inline-flex;
5
+ }
6
+
7
+ /* ── Trigger ── */
8
+ .trigger {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ gap: 6px;
12
+ background: none;
13
+ border: none;
14
+ padding: 0;
15
+ cursor: pointer;
16
+ font: inherit;
17
+ color: inherit;
18
+ }
19
+
20
+ /* ── Content ── */
21
+ .content {
22
+ position: absolute;
23
+ top: 100%;
24
+ z-index: 100;
25
+ padding: 6px;
26
+ border-radius: var(--radius-xl);
27
+ background: var(--color-bg-card);
28
+ border: 1.5px solid var(--color-border-standard);
29
+ backdrop-filter: var(--blur-standard);
30
+ box-shadow: var(--shadow-card);
31
+ animation: dropdownIn 0.15s ease-out;
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 2px;
35
+ }
36
+
37
+ .alignstart {
38
+ left: 0;
39
+ }
40
+
41
+ .aligncenter {
42
+ left: 50%;
43
+ transform: translateX(-50%);
44
+ }
45
+
46
+ .alignend {
47
+ right: 0;
48
+ }
49
+
50
+ @keyframes dropdownIn {
51
+ from {
52
+ opacity: 0;
53
+ transform: translateY(-4px);
54
+ }
55
+ to {
56
+ opacity: 1;
57
+ transform: translateY(0);
58
+ }
59
+ }
60
+
61
+ .aligncenter {
62
+ animation: dropdownInCenter 0.15s ease-out;
63
+ }
64
+
65
+ @keyframes dropdownInCenter {
66
+ from {
67
+ opacity: 0;
68
+ transform: translateX(-50%) translateY(-4px);
69
+ }
70
+ to {
71
+ opacity: 1;
72
+ transform: translateX(-50%) translateY(0);
73
+ }
74
+ }
75
+
76
+ /* ── Group ── */
77
+ .group {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 2px;
81
+ }
82
+
83
+ /* ── Label ── */
84
+ .label {
85
+ padding: 8px 12px 4px;
86
+ font-size: 11px;
87
+ font-weight: 700;
88
+ text-transform: uppercase;
89
+ letter-spacing: 0.06em;
90
+ color: var(--color-text-tertiary);
91
+ user-select: none;
92
+ }
93
+
94
+ /* ── Separator ── */
95
+ .separator {
96
+ height: 1px;
97
+ margin: 4px 0;
98
+ background: var(--color-border-subtle);
99
+ }
100
+
101
+ /* ── Item ── */
102
+ .item {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 10px;
106
+ width: 100%;
107
+ padding: 8px 12px;
108
+ border: none;
109
+ border-radius: var(--radius-md);
110
+ background: transparent;
111
+ color: var(--color-text-primary);
112
+ font-size: 13px;
113
+ font-weight: 500;
114
+ font-family: inherit;
115
+ cursor: pointer;
116
+ transition: all 0.15s;
117
+ text-align: left;
118
+ white-space: nowrap;
119
+ line-height: 1.3;
120
+ }
121
+
122
+ .item:hover {
123
+ background: var(--color-bg-hover);
124
+ }
125
+
126
+ .item:active {
127
+ background: var(--color-bg-elevated);
128
+ }
129
+
130
+ .itemDisabled {
131
+ opacity: 0.4;
132
+ cursor: not-allowed;
133
+ pointer-events: none;
134
+ }
135
+
136
+ /* ── Destructive ── */
137
+ .destructive {
138
+ color: var(--color-error);
139
+ }
140
+
141
+ .destructive:hover {
142
+ background: var(--color-error-bg);
143
+ }
144
+
145
+ /* ── Item Icon ── */
146
+ .itemIcon {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ flex-shrink: 0;
151
+ width: 18px;
152
+ height: 18px;
153
+ color: var(--color-text-tertiary);
154
+ }
155
+
156
+ .item:hover .itemIcon {
157
+ color: var(--color-text-secondary);
158
+ }
159
+
160
+ .destructive .itemIcon {
161
+ color: var(--color-error);
162
+ }
163
+
164
+ /* ── Item Label ── */
165
+ .itemLabel {
166
+ flex: 1;
167
+ }
168
+
169
+ /* ── Shortcut ── */
170
+ .shortcut {
171
+ margin-left: auto;
172
+ padding-left: 24px;
173
+ font-size: 11px;
174
+ font-weight: 500;
175
+ color: var(--color-text-tertiary);
176
+ letter-spacing: 0.02em;
177
+ font-family: "SF Mono", "Fira Code", Menlo, Consolas, monospace;
178
+ }
179
+
180
+ /* ── Checkbox Item ── */
181
+ .checkItem {
182
+ gap: 8px;
183
+ }
184
+
185
+ .checkIndicator {
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ flex-shrink: 0;
190
+ width: 16px;
191
+ height: 16px;
192
+ }
193
+
194
+ .checkItemChecked {
195
+ background: var(--color-bg-lime-subtle);
196
+ }
197
+
198
+ .checkItemChecked:hover {
199
+ background: var(--color-bg-lime-medium);
200
+ }
201
+
202
+ /* ── Radio Item ── */
203
+ .radioItem {
204
+ gap: 8px;
205
+ }
206
+
207
+ .radioIndicator {
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ flex-shrink: 0;
212
+ width: 16px;
213
+ height: 16px;
214
+ }
215
+
216
+ .radioItemSelected {
217
+ background: var(--color-bg-lime-subtle);
218
+ }
219
+
220
+ .radioItemSelected:hover {
221
+ background: var(--color-bg-lime-medium);
222
+ }
223
+
224
+ /* ── Sub Menu ── */
225
+ .sub {
226
+ position: relative;
227
+ }
228
+
229
+ .subTrigger {
230
+ justify-content: flex-start;
231
+ }
232
+
233
+ .subTriggerOpen {
234
+ background: var(--color-bg-hover);
235
+ }
236
+
237
+ .subArrow {
238
+ margin-left: auto;
239
+ display: flex;
240
+ align-items: center;
241
+ color: var(--color-text-tertiary);
242
+ }
243
+
244
+ .subContent {
245
+ position: absolute;
246
+ left: 100%;
247
+ top: -6px;
248
+ margin-left: 4px;
249
+ min-width: 180px;
250
+ padding: 6px;
251
+ border-radius: var(--radius-xl);
252
+ background: var(--color-bg-card);
253
+ border: 1.5px solid var(--color-border-standard);
254
+ backdrop-filter: var(--blur-standard);
255
+ box-shadow: var(--shadow-card);
256
+ z-index: 101;
257
+ animation: subIn 0.12s ease-out;
258
+ }
259
+
260
+ @keyframes subIn {
261
+ from {
262
+ opacity: 0;
263
+ transform: translateX(-4px);
264
+ }
265
+ to {
266
+ opacity: 1;
267
+ transform: translateX(0);
268
+ }
269
+ }
@@ -0,0 +1,419 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useRef,
8
+ useEffect,
9
+ useCallback,
10
+ forwardRef,
11
+ } from "react";
12
+ import { TickSquare, MinusSquare, Record as RadioIcon, ArrowRight2 } from "iconsax-react";
13
+ import { cn } from "@/lib/cn";
14
+ import styles from "./Dropdown.module.css";
15
+
16
+ /* ── Context ── */
17
+
18
+ interface DropdownCtx {
19
+ open: boolean;
20
+ setOpen: (v: boolean) => void;
21
+ triggerRef: React.RefObject<HTMLButtonElement | null>;
22
+ }
23
+
24
+ const Ctx = createContext<DropdownCtx>({
25
+ open: false,
26
+ setOpen: () => {},
27
+ triggerRef: { current: null },
28
+ });
29
+
30
+ /* ── Root ── */
31
+
32
+ export interface DropdownMenuProps {
33
+ children: React.ReactNode;
34
+ open?: boolean;
35
+ onOpenChange?: (open: boolean) => void;
36
+ }
37
+
38
+ export function DropdownMenu({ children, open: controlledOpen, onOpenChange }: DropdownMenuProps) {
39
+ const [internalOpen, setInternalOpen] = useState(false);
40
+ const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
41
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
42
+
43
+ const setOpen = useCallback(
44
+ (v: boolean) => {
45
+ if (controlledOpen === undefined) setInternalOpen(v);
46
+ onOpenChange?.(v);
47
+ },
48
+ [controlledOpen, onOpenChange],
49
+ );
50
+
51
+ return (
52
+ <Ctx.Provider value={{ open: isOpen, setOpen, triggerRef }}>
53
+ <div className={styles.root}>{children}</div>
54
+ </Ctx.Provider>
55
+ );
56
+ }
57
+
58
+ /* ── Trigger ── */
59
+
60
+ export interface DropdownMenuTriggerProps {
61
+ children: React.ReactNode;
62
+ className?: string;
63
+ }
64
+
65
+ export const DropdownMenuTrigger = forwardRef<HTMLButtonElement, DropdownMenuTriggerProps>(
66
+ ({ children, className }, ref) => {
67
+ const { open, setOpen, triggerRef } = useContext(Ctx);
68
+
69
+ return (
70
+ <button
71
+ ref={(node) => {
72
+ triggerRef.current = node;
73
+ if (typeof ref === "function") ref(node);
74
+ else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;
75
+ }}
76
+ className={cn(styles.trigger, className)}
77
+ onClick={() => setOpen(!open)}
78
+ aria-haspopup="menu"
79
+ aria-expanded={open}
80
+ type="button"
81
+ >
82
+ {children}
83
+ </button>
84
+ );
85
+ },
86
+ );
87
+ DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
88
+
89
+ /* ── Content ── */
90
+
91
+ export type DropdownAlign = "start" | "center" | "end";
92
+
93
+ export interface DropdownMenuContentProps {
94
+ children: React.ReactNode;
95
+ align?: DropdownAlign;
96
+ sideOffset?: number;
97
+ className?: string;
98
+ minWidth?: number;
99
+ }
100
+
101
+ export function DropdownMenuContent({
102
+ children,
103
+ align = "start",
104
+ sideOffset = 6,
105
+ className,
106
+ minWidth = 200,
107
+ }: DropdownMenuContentProps) {
108
+ const { open, setOpen, triggerRef } = useContext(Ctx);
109
+ const contentRef = useRef<HTMLDivElement>(null);
110
+
111
+ /* Close on outside click */
112
+ useEffect(() => {
113
+ if (!open) return;
114
+ const handler = (e: MouseEvent) => {
115
+ const target = e.target as Node;
116
+ if (
117
+ contentRef.current &&
118
+ !contentRef.current.contains(target) &&
119
+ triggerRef.current &&
120
+ !triggerRef.current.contains(target)
121
+ ) {
122
+ setOpen(false);
123
+ }
124
+ };
125
+ document.addEventListener("mousedown", handler);
126
+ return () => document.removeEventListener("mousedown", handler);
127
+ }, [open, setOpen, triggerRef]);
128
+
129
+ /* Close on Escape */
130
+ useEffect(() => {
131
+ if (!open) return;
132
+ const handler = (e: KeyboardEvent) => {
133
+ if (e.key === "Escape") setOpen(false);
134
+ };
135
+ document.addEventListener("keydown", handler);
136
+ return () => document.removeEventListener("keydown", handler);
137
+ }, [open, setOpen]);
138
+
139
+ if (!open) return null;
140
+
141
+ return (
142
+ <div
143
+ ref={contentRef}
144
+ className={cn(styles.content, styles[`align${align}`], className)}
145
+ style={{ marginTop: sideOffset, minWidth }}
146
+ role="menu"
147
+ >
148
+ {children}
149
+ </div>
150
+ );
151
+ }
152
+
153
+ /* ── Group ── */
154
+
155
+ export interface DropdownMenuGroupProps {
156
+ children: React.ReactNode;
157
+ className?: string;
158
+ }
159
+
160
+ export function DropdownMenuGroup({ children, className }: DropdownMenuGroupProps) {
161
+ return <div className={cn(styles.group, className)} role="group">{children}</div>;
162
+ }
163
+
164
+ /* ── Label ── */
165
+
166
+ export interface DropdownMenuLabelProps {
167
+ children: React.ReactNode;
168
+ className?: string;
169
+ }
170
+
171
+ export function DropdownMenuLabel({ children, className }: DropdownMenuLabelProps) {
172
+ return <div className={cn(styles.label, className)}>{children}</div>;
173
+ }
174
+
175
+ /* ── Separator ── */
176
+
177
+ export function DropdownMenuSeparator({ className }: { className?: string }) {
178
+ return <div className={cn(styles.separator, className)} role="separator" />;
179
+ }
180
+
181
+ /* ── Item ── */
182
+
183
+ export interface DropdownMenuItemProps {
184
+ children: React.ReactNode;
185
+ icon?: React.ReactNode;
186
+ shortcut?: string;
187
+ disabled?: boolean;
188
+ destructive?: boolean;
189
+ onSelect?: () => void;
190
+ className?: string;
191
+ }
192
+
193
+ export function DropdownMenuItem({
194
+ children,
195
+ icon,
196
+ shortcut,
197
+ disabled = false,
198
+ destructive = false,
199
+ onSelect,
200
+ className,
201
+ }: DropdownMenuItemProps) {
202
+ const { setOpen } = useContext(Ctx);
203
+
204
+ const handleClick = () => {
205
+ if (disabled) return;
206
+ onSelect?.();
207
+ setOpen(false);
208
+ };
209
+
210
+ return (
211
+ <button
212
+ className={cn(
213
+ styles.item,
214
+ destructive && styles.destructive,
215
+ disabled && styles.itemDisabled,
216
+ className,
217
+ )}
218
+ role="menuitem"
219
+ disabled={disabled}
220
+ onClick={handleClick}
221
+ type="button"
222
+ >
223
+ {icon && <span className={styles.itemIcon}>{icon}</span>}
224
+ <span className={styles.itemLabel}>{children}</span>
225
+ {shortcut && <span className={styles.shortcut}>{shortcut}</span>}
226
+ </button>
227
+ );
228
+ }
229
+
230
+ /* ── Checkbox Item ── */
231
+
232
+ export interface DropdownMenuCheckboxItemProps {
233
+ children: React.ReactNode;
234
+ checked: boolean;
235
+ onCheckedChange: (checked: boolean) => void;
236
+ icon?: React.ReactNode;
237
+ disabled?: boolean;
238
+ className?: string;
239
+ }
240
+
241
+ export function DropdownMenuCheckboxItem({
242
+ children,
243
+ checked,
244
+ onCheckedChange,
245
+ icon,
246
+ disabled = false,
247
+ className,
248
+ }: DropdownMenuCheckboxItemProps) {
249
+ return (
250
+ <button
251
+ className={cn(styles.item, styles.checkItem, checked && styles.checkItemChecked, disabled && styles.itemDisabled, className)}
252
+ role="menuitemcheckbox"
253
+ aria-checked={checked}
254
+ disabled={disabled}
255
+ onClick={() => !disabled && onCheckedChange(!checked)}
256
+ type="button"
257
+ >
258
+ <span className={styles.checkIndicator}>
259
+ {checked ? (
260
+ <TickSquare size={16} variant="Bulk" color="var(--color-lime)" />
261
+ ) : (
262
+ <MinusSquare size={16} variant="Linear" color="var(--color-text-tertiary)" />
263
+ )}
264
+ </span>
265
+ {icon && <span className={styles.itemIcon}>{icon}</span>}
266
+ <span className={styles.itemLabel}>{children}</span>
267
+ </button>
268
+ );
269
+ }
270
+
271
+ /* ── Radio Group ── */
272
+
273
+ interface RadioCtx {
274
+ value: string;
275
+ onChange: (value: string) => void;
276
+ }
277
+
278
+ const RadioGroupCtx = createContext<RadioCtx>({ value: "", onChange: () => {} });
279
+
280
+ export interface DropdownMenuRadioGroupProps {
281
+ children: React.ReactNode;
282
+ value: string;
283
+ onValueChange: (value: string) => void;
284
+ }
285
+
286
+ export function DropdownMenuRadioGroup({ children, value, onValueChange }: DropdownMenuRadioGroupProps) {
287
+ return (
288
+ <RadioGroupCtx.Provider value={{ value, onChange: onValueChange }}>
289
+ <div role="group">{children}</div>
290
+ </RadioGroupCtx.Provider>
291
+ );
292
+ }
293
+
294
+ /* ── Radio Item ── */
295
+
296
+ export interface DropdownMenuRadioItemProps {
297
+ children: React.ReactNode;
298
+ value: string;
299
+ icon?: React.ReactNode;
300
+ disabled?: boolean;
301
+ className?: string;
302
+ }
303
+
304
+ export function DropdownMenuRadioItem({
305
+ children,
306
+ value,
307
+ icon,
308
+ disabled = false,
309
+ className,
310
+ }: DropdownMenuRadioItemProps) {
311
+ const { value: selectedValue, onChange } = useContext(RadioGroupCtx);
312
+ const isSelected = selectedValue === value;
313
+
314
+ return (
315
+ <button
316
+ className={cn(styles.item, styles.radioItem, isSelected && styles.radioItemSelected, disabled && styles.itemDisabled, className)}
317
+ role="menuitemradio"
318
+ aria-checked={isSelected}
319
+ disabled={disabled}
320
+ onClick={() => !disabled && onChange(value)}
321
+ type="button"
322
+ >
323
+ <span className={styles.radioIndicator}>
324
+ <RadioIcon
325
+ size={16}
326
+ variant={isSelected ? "Bulk" : "Linear"}
327
+ color={isSelected ? "var(--color-lime)" : "var(--color-text-tertiary)"}
328
+ />
329
+ </span>
330
+ {icon && <span className={styles.itemIcon}>{icon}</span>}
331
+ <span className={styles.itemLabel}>{children}</span>
332
+ </button>
333
+ );
334
+ }
335
+
336
+ /* ── Sub Menu ── */
337
+
338
+ const SubCtx = createContext<{ subOpen: boolean; setSubOpen: (v: boolean) => void }>({ subOpen: false, setSubOpen: () => {} });
339
+
340
+ export interface DropdownMenuSubProps {
341
+ children: React.ReactNode;
342
+ }
343
+
344
+ export function DropdownMenuSub({ children }: DropdownMenuSubProps) {
345
+ const [subOpen, setSubOpen] = useState(false);
346
+ const timeoutRef = useRef<number | null>(null);
347
+
348
+ const handleEnter = () => {
349
+ if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current);
350
+ setSubOpen(true);
351
+ };
352
+
353
+ const handleLeave = () => {
354
+ timeoutRef.current = window.setTimeout(() => setSubOpen(false), 150);
355
+ };
356
+
357
+ return (
358
+ <SubCtx.Provider value={{ subOpen, setSubOpen }}>
359
+ <div
360
+ className={styles.sub}
361
+ onMouseEnter={handleEnter}
362
+ onMouseLeave={handleLeave}
363
+ >
364
+ {children}
365
+ </div>
366
+ </SubCtx.Provider>
367
+ );
368
+ }
369
+
370
+ /* ── Sub Trigger ── */
371
+
372
+ export interface DropdownMenuSubTriggerProps {
373
+ children: React.ReactNode;
374
+ icon?: React.ReactNode;
375
+ className?: string;
376
+ }
377
+
378
+ export function DropdownMenuSubTrigger({
379
+ children,
380
+ icon,
381
+ className,
382
+ }: DropdownMenuSubTriggerProps) {
383
+ const { subOpen, setSubOpen } = useContext(SubCtx);
384
+
385
+ return (
386
+ <button
387
+ className={cn(styles.item, styles.subTrigger, subOpen && styles.subTriggerOpen, className)}
388
+ onClick={() => setSubOpen(!subOpen)}
389
+ type="button"
390
+ aria-haspopup="menu"
391
+ aria-expanded={subOpen}
392
+ >
393
+ {icon && <span className={styles.itemIcon}>{icon}</span>}
394
+ <span className={styles.itemLabel}>{children}</span>
395
+ <span className={styles.subArrow}>
396
+ <ArrowRight2 size={12} color="currentColor" />
397
+ </span>
398
+ </button>
399
+ );
400
+ }
401
+
402
+ /* ── Sub Content ── */
403
+
404
+ export interface DropdownMenuSubContentProps {
405
+ children: React.ReactNode;
406
+ className?: string;
407
+ }
408
+
409
+ export function DropdownMenuSubContent({ children, className }: DropdownMenuSubContentProps) {
410
+ const { subOpen } = useContext(SubCtx);
411
+
412
+ if (!subOpen) return null;
413
+
414
+ return (
415
+ <div className={cn(styles.subContent, className)} role="menu">
416
+ {children}
417
+ </div>
418
+ );
419
+ }
@@ -0,0 +1,31 @@
1
+ export {
2
+ DropdownMenu,
3
+ DropdownMenuTrigger,
4
+ DropdownMenuContent,
5
+ DropdownMenuGroup,
6
+ DropdownMenuLabel,
7
+ DropdownMenuSeparator,
8
+ DropdownMenuItem,
9
+ DropdownMenuCheckboxItem,
10
+ DropdownMenuRadioGroup,
11
+ DropdownMenuRadioItem,
12
+ DropdownMenuSub,
13
+ DropdownMenuSubTrigger,
14
+ DropdownMenuSubContent,
15
+ } from "./Dropdown";
16
+
17
+ export type {
18
+ DropdownMenuProps,
19
+ DropdownMenuTriggerProps,
20
+ DropdownMenuContentProps,
21
+ DropdownAlign,
22
+ DropdownMenuGroupProps,
23
+ DropdownMenuLabelProps,
24
+ DropdownMenuItemProps,
25
+ DropdownMenuCheckboxItemProps,
26
+ DropdownMenuRadioGroupProps,
27
+ DropdownMenuRadioItemProps,
28
+ DropdownMenuSubProps,
29
+ DropdownMenuSubTriggerProps,
30
+ DropdownMenuSubContentProps,
31
+ } from "./Dropdown";