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,507 @@
1
+ "use client";
2
+
3
+ import {
4
+ useState,
5
+ useRef,
6
+ useEffect,
7
+ useCallback,
8
+ } from "react";
9
+ import { ArrowDown2, SearchNormal1, CloseCircle, TickCircle } from "iconsax-react";
10
+ import { cn } from "@/lib/cn";
11
+ import styles from "./Select.module.css";
12
+
13
+ /* ── Types ── */
14
+
15
+ export type SelectSize = "sm" | "md" | "lg";
16
+
17
+ export interface SelectItem {
18
+ id: string;
19
+ label: string;
20
+ supportingText?: string;
21
+ icon?: React.ReactNode;
22
+ avatarSrc?: string;
23
+ dot?: boolean;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ /* ── Select ── */
28
+
29
+ export interface SelectProps {
30
+ items: SelectItem[];
31
+ value?: string;
32
+ defaultValue?: string;
33
+ onValueChange?: (value: string) => void;
34
+ placeholder?: string;
35
+ label?: string;
36
+ hint?: string;
37
+ error?: string;
38
+ size?: SelectSize;
39
+ disabled?: boolean;
40
+ required?: boolean;
41
+ placeholderIcon?: React.ReactNode;
42
+ className?: string;
43
+ }
44
+
45
+ export function Select({
46
+ items,
47
+ value: controlledValue,
48
+ defaultValue = "",
49
+ onValueChange,
50
+ placeholder = "Select an option",
51
+ label,
52
+ hint,
53
+ error,
54
+ size = "md",
55
+ disabled = false,
56
+ required = false,
57
+ placeholderIcon,
58
+ className,
59
+ }: SelectProps) {
60
+ const [internalValue, setInternalValue] = useState(defaultValue);
61
+ const selectedValue = controlledValue !== undefined ? controlledValue : internalValue;
62
+ const [open, setOpen] = useState(false);
63
+ const rootRef = useRef<HTMLDivElement>(null);
64
+
65
+ const selectedItem = items.find((i) => i.id === selectedValue);
66
+
67
+ const handleSelect = useCallback(
68
+ (id: string) => {
69
+ if (controlledValue === undefined) setInternalValue(id);
70
+ onValueChange?.(id);
71
+ setOpen(false);
72
+ },
73
+ [controlledValue, onValueChange],
74
+ );
75
+
76
+ /* Close on outside click */
77
+ useEffect(() => {
78
+ if (!open) return;
79
+ const handler = (e: MouseEvent) => {
80
+ if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
81
+ setOpen(false);
82
+ }
83
+ };
84
+ document.addEventListener("mousedown", handler);
85
+ return () => document.removeEventListener("mousedown", handler);
86
+ }, [open]);
87
+
88
+ /* Close on Escape */
89
+ useEffect(() => {
90
+ if (!open) return;
91
+ const handler = (e: KeyboardEvent) => {
92
+ if (e.key === "Escape") setOpen(false);
93
+ };
94
+ document.addEventListener("keydown", handler);
95
+ return () => document.removeEventListener("keydown", handler);
96
+ }, [open]);
97
+
98
+ return (
99
+ <div className={cn(styles.wrapper, className)} ref={rootRef}>
100
+ {label && (
101
+ <label className={styles.label}>
102
+ {label}
103
+ {required && <span className={styles.required}>*</span>}
104
+ </label>
105
+ )}
106
+ <button
107
+ type="button"
108
+ className={cn(
109
+ styles.trigger,
110
+ styles[size],
111
+ open && styles.triggerOpen,
112
+ error && styles.triggerError,
113
+ disabled && styles.triggerDisabled,
114
+ )}
115
+ onClick={() => !disabled && setOpen(!open)}
116
+ disabled={disabled}
117
+ aria-haspopup="listbox"
118
+ aria-expanded={open}
119
+ >
120
+ <span className={styles.triggerValue}>
121
+ {selectedItem ? (
122
+ <ItemContent item={selectedItem} />
123
+ ) : (
124
+ <span className={styles.placeholder}>
125
+ {placeholderIcon && <span className={styles.placeholderIcon}>{placeholderIcon}</span>}
126
+ {placeholder}
127
+ </span>
128
+ )}
129
+ </span>
130
+ <ArrowDown2
131
+ size={size === "sm" ? 14 : 16}
132
+ color="var(--color-text-tertiary)"
133
+ className={cn(styles.chevron, open && styles.chevronOpen)}
134
+ />
135
+ </button>
136
+
137
+ {open && (
138
+ <div className={styles.listbox} role="listbox">
139
+ {items.map((item) => (
140
+ <button
141
+ key={item.id}
142
+ type="button"
143
+ className={cn(
144
+ styles.option,
145
+ selectedValue === item.id && styles.optionSelected,
146
+ item.disabled && styles.optionDisabled,
147
+ )}
148
+ role="option"
149
+ aria-selected={selectedValue === item.id}
150
+ disabled={item.disabled}
151
+ onClick={() => !item.disabled && handleSelect(item.id)}
152
+ >
153
+ <ItemContent item={item} />
154
+ {selectedValue === item.id && (
155
+ <TickCircle size={16} variant="Bulk" color="var(--color-lime)" />
156
+ )}
157
+ </button>
158
+ ))}
159
+ </div>
160
+ )}
161
+
162
+ {error && <span className={styles.errorText}>{error}</span>}
163
+ {hint && !error && <span className={styles.hint}>{hint}</span>}
164
+ </div>
165
+ );
166
+ }
167
+
168
+ /* ── Search / ComboBox ── */
169
+
170
+ export interface SelectComboBoxProps extends Omit<SelectProps, "placeholderIcon"> {
171
+ searchPlaceholder?: string;
172
+ }
173
+
174
+ export function SelectComboBox({
175
+ items,
176
+ value: controlledValue,
177
+ defaultValue = "",
178
+ onValueChange,
179
+ placeholder = "Select an option",
180
+ searchPlaceholder = "Search...",
181
+ label,
182
+ hint,
183
+ error,
184
+ size = "md",
185
+ disabled = false,
186
+ required = false,
187
+ className,
188
+ }: SelectComboBoxProps) {
189
+ const [internalValue, setInternalValue] = useState(defaultValue);
190
+ const selectedValue = controlledValue !== undefined ? controlledValue : internalValue;
191
+ const [open, setOpen] = useState(false);
192
+ const [search, setSearch] = useState("");
193
+ const rootRef = useRef<HTMLDivElement>(null);
194
+ const searchRef = useRef<HTMLInputElement>(null);
195
+
196
+ const selectedItem = items.find((i) => i.id === selectedValue);
197
+ const filtered = items.filter(
198
+ (i) =>
199
+ i.label.toLowerCase().includes(search.toLowerCase()) ||
200
+ (i.supportingText && i.supportingText.toLowerCase().includes(search.toLowerCase())),
201
+ );
202
+
203
+ const handleSelect = useCallback(
204
+ (id: string) => {
205
+ if (controlledValue === undefined) setInternalValue(id);
206
+ onValueChange?.(id);
207
+ setOpen(false);
208
+ setSearch("");
209
+ },
210
+ [controlledValue, onValueChange],
211
+ );
212
+
213
+ useEffect(() => {
214
+ if (open && searchRef.current) searchRef.current.focus();
215
+ }, [open]);
216
+
217
+ useEffect(() => {
218
+ if (!open) return;
219
+ const handler = (e: MouseEvent) => {
220
+ if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
221
+ setOpen(false);
222
+ setSearch("");
223
+ }
224
+ };
225
+ document.addEventListener("mousedown", handler);
226
+ return () => document.removeEventListener("mousedown", handler);
227
+ }, [open]);
228
+
229
+ useEffect(() => {
230
+ if (!open) return;
231
+ const handler = (e: KeyboardEvent) => {
232
+ if (e.key === "Escape") {
233
+ setOpen(false);
234
+ setSearch("");
235
+ }
236
+ };
237
+ document.addEventListener("keydown", handler);
238
+ return () => document.removeEventListener("keydown", handler);
239
+ }, [open]);
240
+
241
+ return (
242
+ <div className={cn(styles.wrapper, className)} ref={rootRef}>
243
+ {label && (
244
+ <label className={styles.label}>
245
+ {label}
246
+ {required && <span className={styles.required}>*</span>}
247
+ </label>
248
+ )}
249
+ <button
250
+ type="button"
251
+ className={cn(
252
+ styles.trigger,
253
+ styles[size],
254
+ open && styles.triggerOpen,
255
+ error && styles.triggerError,
256
+ disabled && styles.triggerDisabled,
257
+ )}
258
+ onClick={() => !disabled && setOpen(!open)}
259
+ disabled={disabled}
260
+ aria-haspopup="listbox"
261
+ aria-expanded={open}
262
+ >
263
+ <span className={styles.triggerValue}>
264
+ {selectedItem ? (
265
+ <ItemContent item={selectedItem} />
266
+ ) : (
267
+ <span className={styles.placeholder}>{placeholder}</span>
268
+ )}
269
+ </span>
270
+ <ArrowDown2
271
+ size={size === "sm" ? 14 : 16}
272
+ color="var(--color-text-tertiary)"
273
+ className={cn(styles.chevron, open && styles.chevronOpen)}
274
+ />
275
+ </button>
276
+
277
+ {open && (
278
+ <div className={styles.listbox} role="listbox">
279
+ <div className={styles.searchBox}>
280
+ <SearchNormal1 size={14} variant="Bulk" color="var(--color-text-tertiary)" />
281
+ <input
282
+ ref={searchRef}
283
+ className={styles.searchInput}
284
+ type="text"
285
+ placeholder={searchPlaceholder}
286
+ value={search}
287
+ onChange={(e) => setSearch(e.target.value)}
288
+ />
289
+ </div>
290
+ {filtered.length === 0 && (
291
+ <div className={styles.noResults}>No results found</div>
292
+ )}
293
+ {filtered.map((item) => (
294
+ <button
295
+ key={item.id}
296
+ type="button"
297
+ className={cn(
298
+ styles.option,
299
+ selectedValue === item.id && styles.optionSelected,
300
+ item.disabled && styles.optionDisabled,
301
+ )}
302
+ role="option"
303
+ aria-selected={selectedValue === item.id}
304
+ disabled={item.disabled}
305
+ onClick={() => !item.disabled && handleSelect(item.id)}
306
+ >
307
+ <ItemContent item={item} />
308
+ {selectedValue === item.id && (
309
+ <TickCircle size={16} variant="Bulk" color="var(--color-lime)" />
310
+ )}
311
+ </button>
312
+ ))}
313
+ </div>
314
+ )}
315
+
316
+ {error && <span className={styles.errorText}>{error}</span>}
317
+ {hint && !error && <span className={styles.hint}>{hint}</span>}
318
+ </div>
319
+ );
320
+ }
321
+
322
+ /* ── Multi-Select (Tags) ── */
323
+
324
+ export interface SelectTagsProps {
325
+ items: SelectItem[];
326
+ value?: string[];
327
+ defaultValue?: string[];
328
+ onValueChange?: (value: string[]) => void;
329
+ placeholder?: string;
330
+ label?: string;
331
+ hint?: string;
332
+ error?: string;
333
+ size?: SelectSize;
334
+ disabled?: boolean;
335
+ required?: boolean;
336
+ className?: string;
337
+ }
338
+
339
+ export function SelectTags({
340
+ items,
341
+ value: controlledValue,
342
+ defaultValue = [],
343
+ onValueChange,
344
+ placeholder = "Select options",
345
+ label,
346
+ hint,
347
+ error,
348
+ size = "md",
349
+ disabled = false,
350
+ required = false,
351
+ className,
352
+ }: SelectTagsProps) {
353
+ const [internalValue, setInternalValue] = useState<string[]>(defaultValue);
354
+ const selectedValues = controlledValue !== undefined ? controlledValue : internalValue;
355
+ const [open, setOpen] = useState(false);
356
+ const rootRef = useRef<HTMLDivElement>(null);
357
+
358
+ const selectedItems = items.filter((i) => selectedValues.includes(i.id));
359
+
360
+ const toggleItem = useCallback(
361
+ (id: string) => {
362
+ const next = selectedValues.includes(id)
363
+ ? selectedValues.filter((v) => v !== id)
364
+ : [...selectedValues, id];
365
+ if (controlledValue === undefined) setInternalValue(next);
366
+ onValueChange?.(next);
367
+ },
368
+ [controlledValue, selectedValues, onValueChange],
369
+ );
370
+
371
+ const removeItem = useCallback(
372
+ (id: string) => {
373
+ const next = selectedValues.filter((v) => v !== id);
374
+ if (controlledValue === undefined) setInternalValue(next);
375
+ onValueChange?.(next);
376
+ },
377
+ [controlledValue, selectedValues, onValueChange],
378
+ );
379
+
380
+ useEffect(() => {
381
+ if (!open) return;
382
+ const handler = (e: MouseEvent) => {
383
+ if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
384
+ setOpen(false);
385
+ }
386
+ };
387
+ document.addEventListener("mousedown", handler);
388
+ return () => document.removeEventListener("mousedown", handler);
389
+ }, [open]);
390
+
391
+ useEffect(() => {
392
+ if (!open) return;
393
+ const handler = (e: KeyboardEvent) => {
394
+ if (e.key === "Escape") setOpen(false);
395
+ };
396
+ document.addEventListener("keydown", handler);
397
+ return () => document.removeEventListener("keydown", handler);
398
+ }, [open]);
399
+
400
+ return (
401
+ <div className={cn(styles.wrapper, className)} ref={rootRef}>
402
+ {label && (
403
+ <label className={styles.label}>
404
+ {label}
405
+ {required && <span className={styles.required}>*</span>}
406
+ </label>
407
+ )}
408
+ <button
409
+ type="button"
410
+ className={cn(
411
+ styles.trigger,
412
+ styles.triggerTags,
413
+ styles[size],
414
+ open && styles.triggerOpen,
415
+ error && styles.triggerError,
416
+ disabled && styles.triggerDisabled,
417
+ )}
418
+ onClick={() => !disabled && setOpen(!open)}
419
+ disabled={disabled}
420
+ aria-haspopup="listbox"
421
+ aria-expanded={open}
422
+ >
423
+ <span className={styles.tagsContainer}>
424
+ {selectedItems.length > 0 ? (
425
+ selectedItems.map((item) => (
426
+ <span key={item.id} className={styles.tagChip}>
427
+ {item.avatarSrc && (
428
+ <img src={item.avatarSrc} alt="" className={styles.tagChipAvatar} />
429
+ )}
430
+ <span>{item.label}</span>
431
+ <button
432
+ type="button"
433
+ className={styles.tagChipRemove}
434
+ onClick={(e) => {
435
+ e.stopPropagation();
436
+ removeItem(item.id);
437
+ }}
438
+ aria-label={`Remove ${item.label}`}
439
+ >
440
+ <CloseCircle size={12} variant="Bold" color="currentColor" />
441
+ </button>
442
+ </span>
443
+ ))
444
+ ) : (
445
+ <span className={styles.placeholder}>{placeholder}</span>
446
+ )}
447
+ </span>
448
+ <ArrowDown2
449
+ size={size === "sm" ? 14 : 16}
450
+ color="var(--color-text-tertiary)"
451
+ className={cn(styles.chevron, open && styles.chevronOpen)}
452
+ />
453
+ </button>
454
+
455
+ {open && (
456
+ <div className={styles.listbox} role="listbox">
457
+ {items.map((item) => {
458
+ const isSelected = selectedValues.includes(item.id);
459
+ return (
460
+ <button
461
+ key={item.id}
462
+ type="button"
463
+ className={cn(
464
+ styles.option,
465
+ isSelected && styles.optionSelected,
466
+ item.disabled && styles.optionDisabled,
467
+ )}
468
+ role="option"
469
+ aria-selected={isSelected}
470
+ disabled={item.disabled}
471
+ onClick={() => !item.disabled && toggleItem(item.id)}
472
+ >
473
+ <ItemContent item={item} />
474
+ {isSelected && (
475
+ <TickCircle size={16} variant="Bulk" color="var(--color-lime)" />
476
+ )}
477
+ </button>
478
+ );
479
+ })}
480
+ </div>
481
+ )}
482
+
483
+ {error && <span className={styles.errorText}>{error}</span>}
484
+ {hint && !error && <span className={styles.hint}>{hint}</span>}
485
+ </div>
486
+ );
487
+ }
488
+
489
+ /* ── Item Content (shared renderer) ── */
490
+
491
+ function ItemContent({ item }: { item: SelectItem }) {
492
+ return (
493
+ <span className={styles.itemContent}>
494
+ {item.dot && <span className={styles.dot} />}
495
+ {item.icon && <span className={styles.itemIcon}>{item.icon}</span>}
496
+ {item.avatarSrc && (
497
+ <img src={item.avatarSrc} alt="" className={styles.avatar} loading="lazy" />
498
+ )}
499
+ <span className={styles.itemText}>
500
+ <span className={styles.itemLabel}>{item.label}</span>
501
+ {item.supportingText && (
502
+ <span className={styles.itemSupporting}>{item.supportingText}</span>
503
+ )}
504
+ </span>
505
+ </span>
506
+ );
507
+ }
@@ -0,0 +1,2 @@
1
+ export { Select, SelectComboBox, SelectTags } from "./Select";
2
+ export type { SelectProps, SelectComboBoxProps, SelectTagsProps, SelectItem, SelectSize } from "./Select";
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { cn } from "@/lib/cn";
5
+ import type { NavItemType, NavItemDividerType } from "./types";
6
+ import { isDivider, NavRow, SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
7
+ import type { UserCardProps } from "./SidebarParts";
8
+ import styles from "./SidebarNavigation.module.css";
9
+
10
+ export interface DualSection {
11
+ icon: React.ReactNode;
12
+ label: string;
13
+ items: (NavItemType | NavItemDividerType)[];
14
+ }
15
+
16
+ interface SidebarDualProps {
17
+ sections: DualSection[];
18
+ activeUrl?: string;
19
+ brand?: { name: string; logo?: string };
20
+ search?: boolean;
21
+ user?: UserCardProps;
22
+ footer?: React.ReactNode;
23
+ className?: string;
24
+ }
25
+
26
+ export function SidebarDual({ sections, activeUrl, brand, search, user, footer, className }: SidebarDualProps) {
27
+ const [activeSectionIdx, setActiveSectionIdx] = useState(0);
28
+ const activeSection = sections[activeSectionIdx];
29
+
30
+ return (
31
+ <nav className={cn(styles.sidebar, styles.sidebarDual, className)}>
32
+ <div className={styles.dualRail}>
33
+ {brand && (
34
+ <div className={styles.brandLogo} style={{ marginBottom: 8 }}>
35
+ {brand.logo ?? brand.name.charAt(0)}
36
+ </div>
37
+ )}
38
+ {sections.map((section, i) => (
39
+ <button
40
+ key={section.label}
41
+ className={cn(styles.dualRailItem, i === activeSectionIdx && styles.dualRailItemActive)}
42
+ onClick={() => setActiveSectionIdx(i)}
43
+ type="button"
44
+ title={section.label}
45
+ >
46
+ {section.icon}
47
+ </button>
48
+ ))}
49
+ </div>
50
+ <div className={styles.dualContent}>
51
+ {search && <SidebarSearchBar />}
52
+ <div className={styles.sectionHeading}>{activeSection.label}</div>
53
+ {activeSection.items.map((item, i) => {
54
+ if (isDivider(item)) {
55
+ return <hr key={`div-${i}`} className={styles.divider} />;
56
+ }
57
+ return (
58
+ <NavRow
59
+ key={item.href + item.label}
60
+ item={item}
61
+ isActive={activeUrl === item.href}
62
+ activeUrl={activeUrl}
63
+ />
64
+ );
65
+ })}
66
+ {footer && <div className={styles.sidebarFooter}>{footer}</div>}
67
+ {user && <SidebarUserCard {...user} />}
68
+ </div>
69
+ </nav>
70
+ );
71
+ }