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,215 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import { ArrowDown2, SearchNormal1, ExportSquare, ArrowUp2, User, Setting2, Book1, LogoutCurve, AddCircle } from "iconsax-react";
5
+ import { cn } from "@/lib/cn";
6
+ import type { NavItemType, NavItemDividerType, NavSubItem } from "./types";
7
+ import styles from "./SidebarNavigation.module.css";
8
+
9
+ /* ── Type guard ── */
10
+ export function isDivider(item: NavItemType | NavItemDividerType): item is NavItemDividerType {
11
+ return "divider" in item && item.divider === true;
12
+ }
13
+
14
+ /* ── Search Bar ── */
15
+ export function SidebarSearchBar() {
16
+ return (
17
+ <div className={styles.searchBar}>
18
+ <span className={styles.searchIcon}>
19
+ <SearchNormal1 size={16} variant="Bulk" color="currentColor" />
20
+ </span>
21
+ <span className={styles.searchText}>Search</span>
22
+ <span className={styles.searchShortcut}>⌘K</span>
23
+ </div>
24
+ );
25
+ }
26
+
27
+ /* ── User Card ── */
28
+ export interface UserCardProps {
29
+ name: string;
30
+ email: string;
31
+ initials?: string;
32
+ online?: boolean;
33
+ }
34
+
35
+ export interface SwitchAccount {
36
+ name: string;
37
+ email: string;
38
+ initials?: string;
39
+ online?: boolean;
40
+ }
41
+
42
+ export function SidebarUserCard({ name, email, initials, online = true }: UserCardProps) {
43
+ const [open, setOpen] = useState(false);
44
+ const [activeAccount, setActiveAccount] = useState(0);
45
+ const wrapRef = useRef<HTMLDivElement>(null);
46
+
47
+ const accounts: SwitchAccount[] = [
48
+ { name, email, initials, online },
49
+ { name: "Alex Rivera", email: "alex@omnira.space", initials: "AR", online: true },
50
+ ];
51
+
52
+ const close = useCallback(() => setOpen(false), []);
53
+
54
+ useEffect(() => {
55
+ if (!open) return;
56
+ const handleClick = (e: MouseEvent) => {
57
+ if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) close();
58
+ };
59
+ const handleKey = (e: KeyboardEvent) => {
60
+ if (e.key === "Escape") close();
61
+ };
62
+ document.addEventListener("mousedown", handleClick);
63
+ document.addEventListener("keydown", handleKey);
64
+ return () => {
65
+ document.removeEventListener("mousedown", handleClick);
66
+ document.removeEventListener("keydown", handleKey);
67
+ };
68
+ }, [open, close]);
69
+
70
+ return (
71
+ <div className={styles.userCardWrap} ref={wrapRef}>
72
+ {open && (
73
+ <div className={styles.userPopup}>
74
+ <a href="/profile" className={styles.popupItem} onClick={close}>
75
+ <span className={styles.popupItemIcon}><User size={16} variant="Bulk" color="currentColor" /></span>
76
+ View profile
77
+ <span className={styles.popupItemShortcut}>⌘K→P</span>
78
+ </a>
79
+ <a href="/settings/account" className={styles.popupItem} onClick={close}>
80
+ <span className={styles.popupItemIcon}><Setting2 size={16} variant="Bulk" color="currentColor" /></span>
81
+ Account settings
82
+ <span className={styles.popupItemShortcut}>⌘S</span>
83
+ </a>
84
+ <a href="/docs" className={styles.popupItem} onClick={close}>
85
+ <span className={styles.popupItemIcon}><Book1 size={16} variant="Bulk" color="currentColor" /></span>
86
+ Documentation
87
+ </a>
88
+ <hr className={styles.popupDivider} />
89
+ <div className={styles.popupSectionLabel}>Switch account</div>
90
+ {accounts.map((acc, i) => (
91
+ <button
92
+ key={acc.email}
93
+ className={styles.popupAccountRow}
94
+ onClick={() => setActiveAccount(i)}
95
+ type="button"
96
+ >
97
+ <div className={styles.popupAccountAvatar}>{acc.initials ?? acc.name.charAt(0)}</div>
98
+ <div className={styles.popupAccountInfo}>
99
+ <span className={styles.popupAccountName}>{acc.name}</span>
100
+ <span className={styles.popupAccountEmail}>
101
+ {acc.online && <span className={styles.userOnlineDot} />}
102
+ {acc.email}
103
+ </span>
104
+ </div>
105
+ <span className={cn(styles.popupRadio, i === activeAccount && styles.popupRadioActive)} />
106
+ </button>
107
+ ))}
108
+ <button className={styles.popupItem} type="button">
109
+ <span className={styles.popupItemIcon}><AddCircle size={16} variant="Bulk" color="currentColor" /></span>
110
+ Add account
111
+ </button>
112
+ <hr className={styles.popupDivider} />
113
+ <button className={cn(styles.popupItem, styles.popupItemDanger)} type="button" onClick={close}>
114
+ <span className={styles.popupItemIcon}><LogoutCurve size={16} color="currentColor" /></span>
115
+ Sign out
116
+ <span className={styles.popupItemShortcut}>⌥⇧Q</span>
117
+ </button>
118
+ </div>
119
+ )}
120
+ <div className={styles.userCard} onClick={() => setOpen((v) => !v)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") setOpen((v) => !v); }}>
121
+ <div className={styles.userAvatar}>{initials ?? name.charAt(0)}</div>
122
+ <div className={styles.userInfo}>
123
+ <span className={styles.userName}>{name}</span>
124
+ <span className={styles.userEmail}>
125
+ {online && <span className={styles.userOnlineDot} />}
126
+ {email}
127
+ </span>
128
+ </div>
129
+ <span className={styles.userChevron}>
130
+ <ArrowUp2 size={14} color="currentColor" />
131
+ </span>
132
+ </div>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ /* ── Sub Item Row ── */
138
+ export function SubItemRow({ item, isActive }: { item: NavSubItem; isActive: boolean }) {
139
+ return (
140
+ <a href={item.href} className={cn(styles.subItem, isActive && styles.subItemActive)}>
141
+ <span>{item.label}</span>
142
+ {item.badge != null && (
143
+ <span className={styles.subItemBadge}>
144
+ {typeof item.badge === "number" ? item.badge : item.badge}
145
+ </span>
146
+ )}
147
+ </a>
148
+ );
149
+ }
150
+
151
+ /* ── Nav Row ── */
152
+ export function NavRow({
153
+ item,
154
+ isActive,
155
+ activeUrl,
156
+ }: {
157
+ item: NavItemType;
158
+ isActive: boolean;
159
+ activeUrl?: string;
160
+ }) {
161
+ const [open, setOpen] = useState(false);
162
+ const hasChildren = item.items && item.items.length > 0;
163
+
164
+ const handleClick = () => {
165
+ if (hasChildren) setOpen((prev) => !prev);
166
+ };
167
+
168
+ const content = (
169
+ <>
170
+ {item.icon && <span className={styles.navItemIcon}>{item.icon}</span>}
171
+ <span className={styles.navItemLabel}>{item.label}</span>
172
+ {item.badge && <span className={styles.navItemBadge}>{item.badge}</span>}
173
+ {item.external && (
174
+ <span className={styles.externalIcon}>
175
+ <ExportSquare size={14} color="currentColor" />
176
+ </span>
177
+ )}
178
+ {hasChildren && (
179
+ <span className={cn(styles.navItemChevron, open && styles.navItemChevronOpen)}>
180
+ <ArrowDown2 size={14} color="currentColor" />
181
+ </span>
182
+ )}
183
+ </>
184
+ );
185
+
186
+ return (
187
+ <div>
188
+ {hasChildren ? (
189
+ <button
190
+ className={cn(styles.navItem, isActive && styles.navItemActive)}
191
+ onClick={handleClick}
192
+ type="button"
193
+ >
194
+ {content}
195
+ </button>
196
+ ) : (
197
+ <a
198
+ href={item.href}
199
+ className={cn(styles.navItem, isActive && styles.navItemActive)}
200
+ target={item.external ? "_blank" : undefined}
201
+ rel={item.external ? "noopener noreferrer" : undefined}
202
+ >
203
+ {content}
204
+ </a>
205
+ )}
206
+ {hasChildren && open && (
207
+ <div className={styles.subItems}>
208
+ {item.items!.map((sub) => (
209
+ <SubItemRow key={sub.href} item={sub} isActive={activeUrl === sub.href} />
210
+ ))}
211
+ </div>
212
+ )}
213
+ </div>
214
+ );
215
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/cn";
4
+ import type { NavItemType, NavItemDividerType } from "./types";
5
+ import { isDivider, NavRow, SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
6
+ import type { UserCardProps } from "./SidebarParts";
7
+ import styles from "./SidebarNavigation.module.css";
8
+
9
+ interface SidebarSectionDividersProps {
10
+ items: (NavItemType | NavItemDividerType)[];
11
+ activeUrl?: string;
12
+ brand?: { name: string; logo?: string };
13
+ search?: boolean;
14
+ user?: UserCardProps;
15
+ footer?: React.ReactNode;
16
+ className?: string;
17
+ }
18
+
19
+ export function SidebarSectionDividers({ items, activeUrl, brand, search, user, footer, className }: SidebarSectionDividersProps) {
20
+ return (
21
+ <nav className={cn(styles.sidebar, className)}>
22
+ {brand && (
23
+ <div className={styles.brand}>
24
+ <div className={styles.brandLogo}>{brand.logo ?? brand.name.charAt(0)}</div>
25
+ <span className={styles.brandName}>{brand.name}</span>
26
+ </div>
27
+ )}
28
+ {search && <SidebarSearchBar />}
29
+ {items.map((item, i) => {
30
+ if (isDivider(item)) {
31
+ return <hr key={`div-${i}`} className={styles.divider} />;
32
+ }
33
+ return (
34
+ <NavRow
35
+ key={item.href + item.label}
36
+ item={item}
37
+ isActive={activeUrl === item.href}
38
+ activeUrl={activeUrl}
39
+ />
40
+ );
41
+ })}
42
+ {footer && <div className={styles.sidebarFooter}>{footer}</div>}
43
+ {user && <SidebarUserCard {...user} />}
44
+ </nav>
45
+ );
46
+ }
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/cn";
4
+ import type { NavSectionType } from "./types";
5
+ import { isDivider, NavRow, SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
6
+ import type { UserCardProps } from "./SidebarParts";
7
+ import styles from "./SidebarNavigation.module.css";
8
+
9
+ interface SidebarSectionHeadingsProps {
10
+ sections: NavSectionType[];
11
+ activeUrl?: string;
12
+ brand?: { name: string; logo?: string };
13
+ search?: boolean;
14
+ user?: UserCardProps;
15
+ footer?: React.ReactNode;
16
+ className?: string;
17
+ }
18
+
19
+ export function SidebarSectionHeadings({ sections, activeUrl, brand, search, user, footer, className }: SidebarSectionHeadingsProps) {
20
+ return (
21
+ <nav className={cn(styles.sidebar, className)}>
22
+ {brand && (
23
+ <div className={styles.brand}>
24
+ <div className={styles.brandLogo}>{brand.logo ?? brand.name.charAt(0)}</div>
25
+ <span className={styles.brandName}>{brand.name}</span>
26
+ </div>
27
+ )}
28
+ {search && <SidebarSearchBar />}
29
+ {sections.map((section, si) => (
30
+ <div key={section.heading}>
31
+ {si > 0 && <hr className={styles.divider} />}
32
+ <div className={styles.sectionHeading}>{section.heading}</div>
33
+ {section.items.map((item, i) => {
34
+ if (isDivider(item)) {
35
+ return <hr key={`div-${si}-${i}`} className={styles.divider} />;
36
+ }
37
+ return (
38
+ <NavRow
39
+ key={item.href + item.label}
40
+ item={item}
41
+ isActive={activeUrl === item.href}
42
+ activeUrl={activeUrl}
43
+ />
44
+ );
45
+ })}
46
+ </div>
47
+ ))}
48
+ {footer && <div className={styles.sidebarFooter}>{footer}</div>}
49
+ {user && <SidebarUserCard {...user} />}
50
+ </nav>
51
+ );
52
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/cn";
4
+ import type { NavItemType, NavItemDividerType } from "./types";
5
+ import { isDivider, NavRow, SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
6
+ import type { UserCardProps } from "./SidebarParts";
7
+ import styles from "./SidebarNavigation.module.css";
8
+
9
+ interface SidebarSimpleProps {
10
+ items: (NavItemType | NavItemDividerType)[];
11
+ activeUrl?: string;
12
+ brand?: { name: string; logo?: string };
13
+ search?: boolean;
14
+ user?: UserCardProps;
15
+ footer?: React.ReactNode;
16
+ className?: string;
17
+ }
18
+
19
+ export function SidebarSimple({ items, activeUrl, brand, search, user, footer, className }: SidebarSimpleProps) {
20
+ return (
21
+ <nav className={cn(styles.sidebar, className)}>
22
+ {brand && (
23
+ <div className={styles.brand}>
24
+ <div className={styles.brandLogo}>{brand.logo ?? brand.name.charAt(0)}</div>
25
+ <span className={styles.brandName}>{brand.name}</span>
26
+ </div>
27
+ )}
28
+ {search && <SidebarSearchBar />}
29
+ {items.map((item, i) => {
30
+ if (isDivider(item)) {
31
+ return <hr key={`div-${i}`} className={styles.divider} />;
32
+ }
33
+ return (
34
+ <NavRow
35
+ key={item.href + item.label}
36
+ item={item}
37
+ isActive={activeUrl === item.href}
38
+ activeUrl={activeUrl}
39
+ />
40
+ );
41
+ })}
42
+ {footer && <div className={styles.sidebarFooter}>{footer}</div>}
43
+ {user && <SidebarUserCard {...user} />}
44
+ </nav>
45
+ );
46
+ }
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/cn";
4
+ import type { NavItemType, NavItemDividerType } from "./types";
5
+ import styles from "./SidebarNavigation.module.css";
6
+
7
+ interface SidebarSlimProps {
8
+ items: (NavItemType | NavItemDividerType)[];
9
+ activeUrl?: string;
10
+ brandLogo?: string;
11
+ footer?: React.ReactNode;
12
+ className?: string;
13
+ }
14
+
15
+ function isDivider(item: NavItemType | NavItemDividerType): item is NavItemDividerType {
16
+ return "divider" in item && item.divider === true;
17
+ }
18
+
19
+ export function SidebarSlim({ items, activeUrl, brandLogo, footer, className }: SidebarSlimProps) {
20
+ return (
21
+ <nav className={cn(styles.sidebar, styles.sidebarSlim, className)}>
22
+ {brandLogo && (
23
+ <div className={styles.brandLogo} style={{ marginBottom: 8 }}>
24
+ {brandLogo}
25
+ </div>
26
+ )}
27
+ {items.map((item, i) => {
28
+ if (isDivider(item)) {
29
+ return <hr key={`div-${i}`} className={styles.divider} style={{ width: "100%" }} />;
30
+ }
31
+ const isActive = activeUrl === item.href;
32
+ return (
33
+ <a
34
+ key={item.href + item.label}
35
+ href={item.href}
36
+ className={cn(styles.navItemSlim, isActive && styles.navItemSlimActive)}
37
+ target={item.external ? "_blank" : undefined}
38
+ rel={item.external ? "noopener noreferrer" : undefined}
39
+ >
40
+ {item.icon}
41
+ <span className={styles.slimTooltip}>{item.label}</span>
42
+ </a>
43
+ );
44
+ })}
45
+ {footer && <div className={styles.sidebarFooter}>{footer}</div>}
46
+ </nav>
47
+ );
48
+ }
@@ -0,0 +1,29 @@
1
+ export { SidebarSimple } from "./SidebarSimple";
2
+ export { SidebarSectionDividers } from "./SidebarSectionDividers";
3
+ export { SidebarSectionHeadings } from "./SidebarSectionHeadings";
4
+ export { SidebarSlim } from "./SidebarSlim";
5
+ export { SidebarDual } from "./SidebarDual";
6
+ export { SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
7
+ export type { UserCardProps } from "./SidebarParts";
8
+ export {
9
+ FeatureCardProgress,
10
+ FeatureCardProgressCode,
11
+ FeatureCardImage,
12
+ FeatureCardCookie,
13
+ FeatureCardReferral,
14
+ FeatureCardOnboarding,
15
+ FeatureCardUpgrade,
16
+ FeatureCardSupport,
17
+ FeatureCardEvent,
18
+ FeatureCardMessage,
19
+ FeatureCardCurrentProject,
20
+ FeatureCardFreeTrial,
21
+ FeatureCardQRCode,
22
+ } from "./SidebarFeatureCard";
23
+ export type {
24
+ NavItemType,
25
+ NavItemDividerType,
26
+ NavSectionType,
27
+ NavSubItem,
28
+ SidebarNavItem,
29
+ } from "./types";
@@ -0,0 +1,27 @@
1
+ import { ReactNode } from "react";
2
+
3
+ export interface NavSubItem {
4
+ label: string;
5
+ href: string;
6
+ badge?: ReactNode;
7
+ }
8
+
9
+ export interface NavItemType {
10
+ label: string;
11
+ href: string;
12
+ icon?: ReactNode;
13
+ badge?: ReactNode;
14
+ items?: NavSubItem[];
15
+ external?: boolean;
16
+ }
17
+
18
+ export interface NavItemDividerType {
19
+ divider: true;
20
+ }
21
+
22
+ export interface NavSectionType {
23
+ heading: string;
24
+ items: (NavItemType | NavItemDividerType)[];
25
+ }
26
+
27
+ export type SidebarNavItem = NavItemType | NavItemDividerType;
@@ -0,0 +1,93 @@
1
+ .wrapper {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 8px;
5
+ width: 100%;
6
+ }
7
+
8
+ .wrapperDisabled {
9
+ opacity: 0.5;
10
+ pointer-events: none;
11
+ }
12
+
13
+ .header {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ gap: 8px;
18
+ }
19
+
20
+ .label {
21
+ font-size: 13px;
22
+ font-weight: 500;
23
+ color: var(--color-text-secondary);
24
+ }
25
+
26
+ .hint {
27
+ font-size: 12px;
28
+ color: var(--color-text-tertiary);
29
+ margin-top: -4px;
30
+ }
31
+
32
+ .valueText {
33
+ font-size: 13px;
34
+ font-weight: 600;
35
+ color: var(--color-text-primary);
36
+ margin-left: auto;
37
+ }
38
+
39
+ /* ── Track ── */
40
+ .track {
41
+ position: relative;
42
+ width: 100%;
43
+ border-radius: 999px;
44
+ background: var(--color-bg-elevated);
45
+ cursor: pointer;
46
+ touch-action: none;
47
+ user-select: none;
48
+ }
49
+
50
+ .track_sm { height: 4px; }
51
+ .track_md { height: 6px; }
52
+ .track_lg { height: 8px; }
53
+
54
+ /* ── Fill ── */
55
+ .fill {
56
+ height: 100%;
57
+ border-radius: 999px;
58
+ background: var(--color-lime);
59
+ pointer-events: none;
60
+ transition: width 0.05s linear;
61
+ }
62
+
63
+ /* ── Thumb ── */
64
+ .thumb {
65
+ position: absolute;
66
+ top: 50%;
67
+ transform: translate(-50%, -50%);
68
+ border-radius: 50%;
69
+ background: var(--color-lime);
70
+ border: 2px solid var(--color-bg-primary);
71
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
72
+ pointer-events: none;
73
+ transition: width 0.05s linear, height 0.05s linear;
74
+ }
75
+
76
+ .thumb_sm {
77
+ width: 14px;
78
+ height: 14px;
79
+ }
80
+
81
+ .thumb_md {
82
+ width: 18px;
83
+ height: 18px;
84
+ }
85
+
86
+ .thumb_lg {
87
+ width: 22px;
88
+ height: 22px;
89
+ }
90
+
91
+ .track:hover .thumb {
92
+ box-shadow: 0 0 0 4px rgba(163, 230, 53, 0.2), 0 1px 4px rgba(0, 0, 0, 0.25);
93
+ }
@@ -0,0 +1,118 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useRef, useEffect, forwardRef } from "react";
4
+ import { cn } from "@/lib/cn";
5
+ import styles from "./Slider.module.css";
6
+
7
+ export interface SliderProps {
8
+ min?: number;
9
+ max?: number;
10
+ step?: number;
11
+ value?: number;
12
+ defaultValue?: number;
13
+ onChange?: (value: number) => void;
14
+ label?: string;
15
+ hint?: string;
16
+ showValue?: boolean;
17
+ size?: "sm" | "md" | "lg";
18
+ disabled?: boolean;
19
+ className?: string;
20
+ }
21
+
22
+ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
23
+ (
24
+ {
25
+ min = 0,
26
+ max = 100,
27
+ step = 1,
28
+ value: controlledValue,
29
+ defaultValue = 0,
30
+ onChange,
31
+ label,
32
+ hint,
33
+ showValue = true,
34
+ size = "md",
35
+ disabled = false,
36
+ className,
37
+ },
38
+ ref,
39
+ ) => {
40
+ const isControlled = controlledValue !== undefined;
41
+ const [internalValue, setInternalValue] = useState(defaultValue);
42
+ const value = isControlled ? controlledValue : internalValue;
43
+ const pct = ((value - min) / (max - min)) * 100;
44
+
45
+ const trackRef = useRef<HTMLDivElement>(null);
46
+ const dragging = useRef(false);
47
+
48
+ const updateValue = useCallback(
49
+ (clientX: number) => {
50
+ const track = trackRef.current;
51
+ if (!track || disabled) return;
52
+ const rect = track.getBoundingClientRect();
53
+ let ratio = (clientX - rect.left) / rect.width;
54
+ ratio = Math.max(0, Math.min(1, ratio));
55
+ let newVal = min + ratio * (max - min);
56
+ newVal = Math.round(newVal / step) * step;
57
+ newVal = Math.max(min, Math.min(max, newVal));
58
+ if (!isControlled) setInternalValue(newVal);
59
+ onChange?.(newVal);
60
+ },
61
+ [min, max, step, disabled, isControlled, onChange],
62
+ );
63
+
64
+ const handlePointerDown = useCallback(
65
+ (e: React.PointerEvent) => {
66
+ if (disabled) return;
67
+ dragging.current = true;
68
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
69
+ updateValue(e.clientX);
70
+ },
71
+ [disabled, updateValue],
72
+ );
73
+
74
+ const handlePointerMove = useCallback(
75
+ (e: React.PointerEvent) => {
76
+ if (!dragging.current) return;
77
+ updateValue(e.clientX);
78
+ },
79
+ [updateValue],
80
+ );
81
+
82
+ const handlePointerUp = useCallback(() => {
83
+ dragging.current = false;
84
+ }, []);
85
+
86
+ return (
87
+ <div ref={ref} className={cn(styles.wrapper, disabled && styles.wrapperDisabled, className)}>
88
+ {(label || (showValue && label)) && (
89
+ <div className={styles.header}>
90
+ {label && <span className={styles.label}>{label}</span>}
91
+ {showValue && <span className={styles.valueText}>{value}</span>}
92
+ </div>
93
+ )}
94
+ {hint && <span className={styles.hint}>{hint}</span>}
95
+ <div
96
+ ref={trackRef}
97
+ className={cn(styles.track, styles[`track_${size}`])}
98
+ onPointerDown={handlePointerDown}
99
+ onPointerMove={handlePointerMove}
100
+ onPointerUp={handlePointerUp}
101
+ >
102
+ <div className={styles.fill} style={{ width: `${pct}%` }} />
103
+ <div
104
+ className={cn(styles.thumb, styles[`thumb_${size}`])}
105
+ style={{ left: `${pct}%` }}
106
+ />
107
+ </div>
108
+ {!label && showValue && (
109
+ <div className={styles.header}>
110
+ <span className={styles.valueText}>{value}</span>
111
+ </div>
112
+ )}
113
+ </div>
114
+ );
115
+ },
116
+ );
117
+
118
+ Slider.displayName = "Slider";
@@ -0,0 +1,2 @@
1
+ export { Slider } from "./Slider";
2
+ export type { SliderProps } from "./Slider";