omnira-ui 0.1.1 → 0.2.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 (42) hide show
  1. package/README.md +43 -15
  2. package/app/globals.css +347 -0
  3. package/cli/omnira-init.mjs +154 -46
  4. package/components/ui/AppStoreButton/AppStoreButton.tsx +1 -1
  5. package/components/ui/Avatar/Avatar.tsx +1 -1
  6. package/components/ui/Badge/Badge.tsx +1 -1
  7. package/components/ui/BadgeGroup/BadgeGroup.tsx +1 -1
  8. package/components/ui/Browser/Browser.tsx +1 -1
  9. package/components/ui/Button/Button.tsx +1 -1
  10. package/components/ui/ButtonUtility/ButtonUtility.tsx +1 -1
  11. package/components/ui/Card/Card.tsx +1 -1
  12. package/components/ui/Checkbox/Checkbox.tsx +1 -1
  13. package/components/ui/Collapse/Collapse.tsx +1 -1
  14. package/components/ui/CreditCard/CreditCard.tsx +1 -1
  15. package/components/ui/Dropdown/Dropdown.tsx +1 -1
  16. package/components/ui/Illustration/Illustration.tsx +1 -1
  17. package/components/ui/Input/Input.tsx +1 -1
  18. package/components/ui/Phone/Phone.tsx +1 -1
  19. package/components/ui/PinInput/PinInput.tsx +1 -1
  20. package/components/ui/ProgressBar/ProgressBar.tsx +1 -1
  21. package/components/ui/QRCode/QRCode.tsx +1 -1
  22. package/components/ui/RadioButton/RadioButton.tsx +1 -1
  23. package/components/ui/RadioGroup/RadioGroup.tsx +1 -1
  24. package/components/ui/Rating/Rating.tsx +1 -1
  25. package/components/ui/Select/Select.tsx +1 -1
  26. package/components/ui/SidebarNavigation/SidebarDual.tsx +1 -1
  27. package/components/ui/SidebarNavigation/SidebarFeatureCard.tsx +4 -4
  28. package/components/ui/SidebarNavigation/SidebarParts.tsx +1 -1
  29. package/components/ui/SidebarNavigation/SidebarSectionDividers.tsx +1 -1
  30. package/components/ui/SidebarNavigation/SidebarSectionHeadings.tsx +1 -1
  31. package/components/ui/SidebarNavigation/SidebarSimple.tsx +1 -1
  32. package/components/ui/SidebarNavigation/SidebarSlim.tsx +1 -1
  33. package/components/ui/Slider/Slider.tsx +1 -1
  34. package/components/ui/SocialButton/SocialButton.tsx +1 -1
  35. package/components/ui/Tag/Tag.tsx +1 -1
  36. package/components/ui/TextEditor/TextEditor.tsx +1 -1
  37. package/components/ui/Textarea/Textarea.tsx +1 -1
  38. package/components/ui/Toggle/Toggle.tsx +1 -1
  39. package/components/ui/Tooltip/Tooltip.tsx +1 -1
  40. package/components/ui/VideoPlayer/VideoPlayer.tsx +1 -1
  41. package/lib/theme-context.tsx +79 -0
  42. package/package.json +3 -1
package/README.md CHANGED
@@ -10,33 +10,55 @@ A premium glassmorphism design system — dark-first, glass-forward, no compromi
10
10
 
11
11
  ## Quick Start
12
12
 
13
- ### 1. Install
13
+ ### 1. Scaffold your project
14
14
 
15
15
  ```bash
16
- npm install omnira-ui
17
- # or
18
- pnpm add omnira-ui
16
+ npx omnira-ui init
19
17
  ```
20
18
 
21
- ### 2. Initialize your project
19
+ The CLI scaffolds the full design system into your project:
20
+
21
+ - **32 base components** → `components/ui/`
22
+ - **Utility helpers** → `lib/cn.ts`, `lib/copy-to-clipboard.ts`
23
+ - **Theme provider** → `lib/theme-context.tsx` (system preference detection + toggle)
24
+ - **Design tokens** → `app/globals.css` (all CSS custom properties)
25
+ - **Accent overrides** → `omnira-overrides.css` (if non-default color)
26
+ - **Config file** → `omnira.config.ts`
27
+
28
+ You choose:
29
+ - **Accent color** — 10 presets: Lime (default), Blue, Cyan, Green, Orange, Pink, Purple, Red, Teal, Yellow
30
+ - **Theme mode** — Dark-first or Light-first (respects system preference)
31
+
32
+ ### 2. Install peer dependencies
22
33
 
23
34
  ```bash
24
- npx omnira-ui init
35
+ npm install iconsax-react
25
36
  ```
26
37
 
27
- The CLI walks you through:
28
- - **Project name**
29
- - **Accent color** — 10 presets: Lime (default), Blue, Cyan, Green, Orange, Pink, Purple, Red, Teal, Yellow
30
- - **Theme mode** — Dark-first or Light-first
38
+ ### 3. Set up your layout
39
+
40
+ ```tsx
41
+ // app/layout.tsx
42
+ import "./globals.css";
43
+ import { Providers } from "./providers";
31
44
 
32
- It generates `omnira.config.ts` and `omnira-overrides.css` ready to import in your root layout.
45
+ export default function RootLayout({ children }) {
46
+ return (
47
+ <html data-theme="dark">
48
+ <body>
49
+ <Providers>{children}</Providers>
50
+ </body>
51
+ </html>
52
+ );
53
+ }
54
+ ```
33
55
 
34
- ### 3. Use components
56
+ ### 4. Use components
35
57
 
36
58
  ```tsx
37
- import { Button } from "omnira-ui/components/ui/Button";
38
- import { Badge } from "omnira-ui/components/ui/Badge";
39
- import { Input } from "omnira-ui/components/ui/Input";
59
+ import { Button } from "@/components/ui/Button";
60
+ import { Badge } from "@/components/ui/Badge";
61
+ import { Input } from "@/components/ui/Input";
40
62
 
41
63
  export default function MyPage() {
42
64
  return (
@@ -49,6 +71,12 @@ export default function MyPage() {
49
71
  }
50
72
  ```
51
73
 
74
+ ### 5. Need more components?
75
+
76
+ Browse the full library and copy advanced components (Sidebar, Feature Cards, etc.) directly from the docs:
77
+
78
+ **[ui.omnira.space →](https://ui.omnira.space)**
79
+
52
80
  ---
53
81
 
54
82
  ## Components
@@ -0,0 +1,347 @@
1
+ /* ============================================
2
+ Omnira UI — Design System CSS Variables
3
+ ============================================ */
4
+
5
+ /* --- Font Faces --- */
6
+ @font-face {
7
+ font-family: "Host Grotesk";
8
+ src: url("/fonts/HostGrotesk-Bold.woff2") format("woff2");
9
+ font-weight: 700;
10
+ font-style: normal;
11
+ font-display: swap;
12
+ }
13
+
14
+ /* --- CSS Custom Properties: Dark Mode (Default) --- */
15
+ [data-theme="dark"] {
16
+ --color-lime: #D2FE17;
17
+ --color-lime-hover: #c0e616;
18
+ --color-lime-gradient: #ABC928;
19
+ --color-lime-text: #121212;
20
+
21
+ --color-bg-primary: #202020;
22
+ --color-bg-secondary: #1a1a1a;
23
+ --color-bg-card: rgba(248, 248, 248, 0.03);
24
+ --color-bg-elevated: rgba(248, 248, 248, 0.06);
25
+ --color-bg-input: rgba(248, 248, 248, 0.04);
26
+ --color-bg-hover: rgba(248, 248, 248, 0.05);
27
+ --color-bg-overlay: rgba(10, 10, 10, 0.97);
28
+ --color-bg-sidebar: #1a1a1a;
29
+
30
+ --color-text-primary: rgba(248, 248, 248, 0.95);
31
+ --color-text-secondary: rgba(248, 248, 248, 0.70);
32
+ --color-text-tertiary: rgba(248, 248, 248, 0.50);
33
+
34
+ --color-border-subtle: rgba(255, 255, 255, 0.05);
35
+ --color-border-standard: rgba(255, 255, 255, 0.06);
36
+ --color-border-medium: rgba(255, 255, 255, 0.08);
37
+ --color-border-strong: rgba(255, 255, 255, 0.15);
38
+
39
+ --color-border-lime-subtle: rgba(210, 254, 23, 0.1);
40
+ --color-border-lime-medium: rgba(210, 254, 23, 0.15);
41
+ --color-border-lime-strong: rgba(210, 254, 23, 0.3);
42
+
43
+ --color-bg-lime-subtle: rgba(210, 254, 23, 0.06);
44
+ --color-bg-lime-medium: rgba(210, 254, 23, 0.08);
45
+ --color-bg-lime-strong: rgba(210, 254, 23, 0.12);
46
+
47
+ --color-error: #ef4444;
48
+ --color-warning: #ffbd2e;
49
+ --color-success: #28c840;
50
+ --color-info: #3b82f6;
51
+
52
+ --color-error-bg: rgba(239, 68, 68, 0.08);
53
+ --color-error-border: rgba(239, 68, 68, 0.15);
54
+ --color-warning-bg: rgba(255, 189, 46, 0.08);
55
+ --color-warning-border: rgba(255, 189, 46, 0.15);
56
+ --color-success-bg: rgba(40, 200, 64, 0.08);
57
+ --color-success-border: rgba(40, 200, 64, 0.15);
58
+ --color-info-bg: rgba(59, 130, 246, 0.08);
59
+ --color-info-border: rgba(59, 130, 246, 0.15);
60
+
61
+ --shadow-card: inset 2px 4px 16px rgba(248, 248, 248, 0.06), 0px 8px 32px rgba(0, 0, 0, 0.2);
62
+ --shadow-card-light: inset 1px 2px 12px rgba(248, 248, 248, 0.03), 0px 8px 28px rgba(0, 0, 0, 0.12);
63
+ --shadow-card-accent: inset 2px 4px 16px rgba(210, 254, 23, 0.04), 0px 16px 48px rgba(0, 0, 0, 0.2);
64
+ --shadow-card-hover: inset 2px 4px 16px rgba(248, 248, 248, 0.08), 0px 12px 40px rgba(0, 0, 0, 0.28);
65
+ --shadow-btn-primary: 0 8px 24px rgba(210, 254, 23, 0.3);
66
+ --shadow-glow-lime: 0 0 8px rgba(210, 254, 23, 0.6), 0 0 16px rgba(210, 254, 23, 0.3);
67
+
68
+ --gradient-text: linear-gradient(93deg, rgba(248, 248, 248, 0.9), rgba(248, 248, 248, 0.5));
69
+ --gradient-framework: linear-gradient(124deg, rgba(248, 248, 248, 0.04) 0%, rgba(248, 248, 248, 0.01) 46.5%), linear-gradient(180deg, rgba(248, 248, 248, 0.03) 0%, rgba(248, 248, 248, 0.00) 100%);
70
+ --gradient-progress: linear-gradient(90deg, #D2FE17, #e5ff54);
71
+
72
+ --blur-standard: blur(50px);
73
+ --radius-sm: 8px;
74
+ --radius-md: 14px;
75
+ --radius-lg: 20px;
76
+ --radius-xl: 28px;
77
+ --radius-2xl: 32px;
78
+ --radius-3xl: 40px;
79
+ --radius-full: 100px;
80
+
81
+ --toggle-track-off: rgba(248, 248, 248, 0.1);
82
+ --color-grid-line: rgba(248, 248, 248, 0.05);
83
+ }
84
+
85
+ /* --- CSS Custom Properties: Light Mode --- */
86
+ [data-theme="light"] {
87
+ --color-lime: #8AB400;
88
+ --color-lime-hover: #7DA310;
89
+ --color-lime-gradient: #6B9A00;
90
+ --color-lime-text: #ffffff;
91
+
92
+ --color-bg-primary: #f5f5f7;
93
+ --color-bg-secondary: #ebebed;
94
+ --color-bg-card: rgba(255, 255, 255, 0.7);
95
+ --color-bg-elevated: rgba(255, 255, 255, 0.9);
96
+ --color-bg-input: rgba(0, 0, 0, 0.03);
97
+ --color-bg-hover: rgba(0, 0, 0, 0.04);
98
+ --color-bg-overlay: rgba(255, 255, 255, 0.97);
99
+ --color-bg-sidebar: #ffffff;
100
+
101
+ --color-text-primary: rgba(18, 18, 18, 0.95);
102
+ --color-text-secondary: rgba(18, 18, 18, 0.65);
103
+ --color-text-tertiary: rgba(18, 18, 18, 0.45);
104
+
105
+ --color-border-subtle: rgba(0, 0, 0, 0.06);
106
+ --color-border-standard: rgba(0, 0, 0, 0.08);
107
+ --color-border-medium: rgba(0, 0, 0, 0.10);
108
+ --color-border-strong: rgba(0, 0, 0, 0.18);
109
+
110
+ --color-border-lime-subtle: rgba(138, 180, 0, 0.15);
111
+ --color-border-lime-medium: rgba(138, 180, 0, 0.25);
112
+ --color-border-lime-strong: rgba(138, 180, 0, 0.4);
113
+
114
+ --color-bg-lime-subtle: rgba(138, 180, 0, 0.06);
115
+ --color-bg-lime-medium: rgba(138, 180, 0, 0.10);
116
+ --color-bg-lime-strong: rgba(138, 180, 0, 0.15);
117
+
118
+ --color-error: #dc2626;
119
+ --color-warning: #d97706;
120
+ --color-success: #16a34a;
121
+ --color-info: #2563eb;
122
+
123
+ --color-error-bg: rgba(220, 38, 38, 0.06);
124
+ --color-error-border: rgba(220, 38, 38, 0.15);
125
+ --color-warning-bg: rgba(217, 119, 6, 0.06);
126
+ --color-warning-border: rgba(217, 119, 6, 0.15);
127
+ --color-success-bg: rgba(22, 163, 74, 0.06);
128
+ --color-success-border: rgba(22, 163, 74, 0.15);
129
+ --color-info-bg: rgba(37, 99, 235, 0.06);
130
+ --color-info-border: rgba(37, 99, 235, 0.15);
131
+
132
+ --shadow-card: 0px 1px 3px rgba(0, 0, 0, 0.04), 0px 6px 24px rgba(0, 0, 0, 0.06);
133
+ --shadow-card-light: 0px 1px 2px rgba(0, 0, 0, 0.03), 0px 4px 16px rgba(0, 0, 0, 0.04);
134
+ --shadow-card-accent: 0px 2px 8px rgba(138, 180, 0, 0.08), 0px 12px 36px rgba(0, 0, 0, 0.06);
135
+ --shadow-card-hover: 0px 2px 6px rgba(0, 0, 0, 0.06), 0px 12px 36px rgba(0, 0, 0, 0.1);
136
+ --shadow-btn-primary: 0 6px 20px rgba(138, 180, 0, 0.25);
137
+ --shadow-glow-lime: 0 0 6px rgba(138, 180, 0, 0.4), 0 0 12px rgba(138, 180, 0, 0.2);
138
+
139
+ --gradient-text: linear-gradient(93deg, rgba(18, 18, 18, 0.95), rgba(18, 18, 18, 0.55));
140
+ --gradient-framework: linear-gradient(124deg, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.4) 46.5%), linear-gradient(180deg, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0.2) 100%);
141
+ --gradient-progress: linear-gradient(90deg, #8AB400, #a5d600);
142
+
143
+ --blur-standard: blur(50px);
144
+ --radius-sm: 8px;
145
+ --radius-md: 14px;
146
+ --radius-lg: 20px;
147
+ --radius-xl: 28px;
148
+ --radius-2xl: 32px;
149
+ --radius-3xl: 40px;
150
+ --radius-full: 100px;
151
+
152
+ --toggle-track-off: rgba(0, 0, 0, 0.12);
153
+ --color-grid-line: rgba(0, 0, 0, 0.06);
154
+ }
155
+
156
+ /* --- Font Variables --- */
157
+ :root {
158
+ --font-display: "Host Grotesk", sans-serif;
159
+ --font-body: "Rubik", sans-serif;
160
+ }
161
+
162
+ /* --- Global Reset & Base Styles --- */
163
+ *,
164
+ *::before,
165
+ *::after {
166
+ box-sizing: border-box;
167
+ padding: 0;
168
+ margin: 0;
169
+ }
170
+
171
+ html,
172
+ body {
173
+ max-width: 100vw;
174
+ overflow-x: hidden;
175
+ }
176
+
177
+ html {
178
+ scroll-behavior: smooth;
179
+ }
180
+
181
+ body {
182
+ font-family: var(--font-body);
183
+ font-size: 16px;
184
+ font-weight: 400;
185
+ line-height: 1.6;
186
+ color: var(--color-text-primary);
187
+ background: var(--color-bg-primary);
188
+ -webkit-font-smoothing: antialiased;
189
+ -moz-osx-font-smoothing: grayscale;
190
+ transition: background 0.4s ease, color 0.4s ease;
191
+ }
192
+
193
+ a {
194
+ color: inherit;
195
+ text-decoration: none;
196
+ }
197
+
198
+ button {
199
+ font-family: inherit;
200
+ cursor: pointer;
201
+ }
202
+
203
+ input,
204
+ textarea,
205
+ select {
206
+ font-family: inherit;
207
+ }
208
+
209
+ img {
210
+ max-width: 100%;
211
+ display: block;
212
+ }
213
+
214
+ /* --- Typography Utility Classes --- */
215
+ .gradient-text {
216
+ background-image: var(--gradient-text);
217
+ -webkit-background-clip: text;
218
+ -webkit-text-fill-color: transparent;
219
+ background-clip: text;
220
+ }
221
+
222
+ .lime-text {
223
+ color: var(--color-lime);
224
+ }
225
+
226
+ .font-display {
227
+ font-family: var(--font-display);
228
+ }
229
+
230
+ .font-body {
231
+ font-family: var(--font-body);
232
+ }
233
+
234
+ /* --- Glass Card Utility --- */
235
+ .glass-card {
236
+ border-radius: var(--radius-2xl);
237
+ background: var(--color-bg-card);
238
+ border: 1.5px solid var(--color-border-standard);
239
+ box-shadow: var(--shadow-card);
240
+ backdrop-filter: var(--blur-standard);
241
+ transition: all 0.35s ease;
242
+ }
243
+
244
+ .glass-card:hover {
245
+ transform: translateY(-4px);
246
+ box-shadow: var(--shadow-card-hover);
247
+ }
248
+
249
+ /* --- Icon Box Utility --- */
250
+ .icon-box {
251
+ display: flex;
252
+ justify-content: center;
253
+ align-items: center;
254
+ width: 48px;
255
+ height: 48px;
256
+ border-radius: var(--radius-md);
257
+ background: var(--color-bg-lime-subtle);
258
+ border: 1px solid var(--color-border-lime-subtle);
259
+ transition: all 0.3s;
260
+ }
261
+
262
+ /* --- Status Dots --- */
263
+ .dot-active {
264
+ background: var(--color-lime);
265
+ box-shadow: var(--shadow-glow-lime);
266
+ }
267
+
268
+ .dot-warning {
269
+ background: var(--color-warning);
270
+ box-shadow: 0 0 8px rgba(255, 189, 46, 0.5);
271
+ }
272
+
273
+ .dot-error {
274
+ background: var(--color-error);
275
+ box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
276
+ }
277
+
278
+ /* --- Animations --- */
279
+ @keyframes fadeInUp {
280
+ from {
281
+ opacity: 0;
282
+ transform: translateY(24px);
283
+ }
284
+ to {
285
+ opacity: 1;
286
+ transform: translateY(0);
287
+ }
288
+ }
289
+
290
+ @keyframes scaleIn {
291
+ from {
292
+ opacity: 0;
293
+ transform: scale(0.5);
294
+ }
295
+ to {
296
+ opacity: 1;
297
+ transform: scale(1);
298
+ }
299
+ }
300
+
301
+ @keyframes livePulse {
302
+ 0%, 100% {
303
+ opacity: 1;
304
+ box-shadow: 0 0 0 0 rgba(210, 254, 23, 0.4);
305
+ }
306
+ 50% {
307
+ opacity: 0.7;
308
+ box-shadow: 0 0 0 6px rgba(210, 254, 23, 0);
309
+ }
310
+ }
311
+
312
+ @keyframes pulse {
313
+ 0%, 100% {
314
+ transform: scale(1);
315
+ opacity: 1;
316
+ }
317
+ 50% {
318
+ transform: scale(1.06);
319
+ opacity: 0.85;
320
+ }
321
+ }
322
+
323
+ @keyframes button-circle {
324
+ 0% { transform: translateX(0) rotate(0); }
325
+ 25% { transform: translateX(-30px) rotate(90deg); }
326
+ 50% { transform: translateX(0) rotate(180deg); }
327
+ 75% { transform: translateX(30px) rotate(270deg); }
328
+ 100% { transform: translateX(0) rotate(360deg); }
329
+ }
330
+
331
+ /* --- Scrollbar Styling --- */
332
+ ::-webkit-scrollbar {
333
+ width: 6px;
334
+ }
335
+
336
+ ::-webkit-scrollbar-track {
337
+ background: transparent;
338
+ }
339
+
340
+ ::-webkit-scrollbar-thumb {
341
+ background: var(--color-border-medium);
342
+ border-radius: 3px;
343
+ }
344
+
345
+ ::-webkit-scrollbar-thumb:hover {
346
+ background: var(--color-border-strong);
347
+ }
@@ -1,22 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Omnira UI — Interactive Setup CLI
4
+ * Omnira UI — Project Scaffolding CLI
5
5
  *
6
6
  * Usage: npx omnira-ui init
7
- * node cli/omnira-init.mjs
8
7
  *
9
- * Walks the user through project naming, accent color selection,
10
- * and theme mode then generates omnira.config.ts and a CSS
11
- * overrides file ready to import.
8
+ * Scaffolds the full Omnira UI design system into your project:
9
+ * - All base components components/ui/
10
+ * - Utility helpers → lib/
11
+ * - Theme provider → lib/theme-context.tsx
12
+ * - Design tokens CSS → app/globals.css
13
+ * - Accent overrides → omnira-overrides.css (if non-default)
14
+ * - Config file → omnira.config.ts
15
+ *
16
+ * Advanced components (Sidebar, Feature Cards, etc.) can be copied
17
+ * from the documentation site: https://ui.omnira.space
12
18
  */
13
19
 
14
20
  import * as readline from "node:readline";
15
21
  import * as fs from "node:fs";
16
22
  import * as path from "node:path";
17
- import { COLOR_PRESETS, DEFAULT_COLOR, DEFAULT_THEME, THEME_MODES } from "./presets.mjs";
23
+ import { fileURLToPath } from "node:url";
24
+ import { COLOR_PRESETS, DEFAULT_COLOR, DEFAULT_THEME } from "./presets.mjs";
25
+
26
+ // ── Resolve package root (where this CLI lives inside node_modules) ──
18
27
 
19
- // ── Helpers ──────────────────────────────────────────────────────────
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+ const PKG_ROOT = path.resolve(__dirname, "..");
31
+
32
+ // ── ANSI helpers ─────────────────────────────────────────────────────
20
33
 
21
34
  const RESET = "\x1b[0m";
22
35
  const BOLD = "\x1b[1m";
@@ -26,6 +39,7 @@ const CYAN = "\x1b[36m";
26
39
  const YELLOW = "\x1b[33m";
27
40
  const MAGENTA = "\x1b[35m";
28
41
  const WHITE = "\x1b[97m";
42
+ const RED = "\x1b[31m";
29
43
 
30
44
  function colorize(hex) {
31
45
  const r = parseInt(hex.slice(1, 3), 16);
@@ -63,7 +77,7 @@ function createPrompt() {
63
77
  return { ask, close };
64
78
  }
65
79
 
66
- // ── Color picker (interactive list) ─────────────────────────────────
80
+ // ── Color picker ─────────────────────────────────────────────────────
67
81
 
68
82
  async function pickColor(promptFn) {
69
83
  const keys = Object.keys(COLOR_PRESETS);
@@ -86,17 +100,12 @@ async function pickColor(promptFn) {
86
100
 
87
101
  if (!answer) return DEFAULT_COLOR;
88
102
 
89
- // Match by number
90
103
  const num = parseInt(answer, 10);
91
- if (!isNaN(num) && num >= 1 && num <= keys.length) {
92
- return keys[num - 1];
93
- }
104
+ if (!isNaN(num) && num >= 1 && num <= keys.length) return keys[num - 1];
94
105
 
95
- // Match by name (case-insensitive)
96
106
  const lower = answer.toLowerCase();
97
107
  if (COLOR_PRESETS[lower]) return lower;
98
108
 
99
- // Fuzzy match
100
109
  const match = keys.find((k) => k.startsWith(lower) || COLOR_PRESETS[k].label.toLowerCase().startsWith(lower));
101
110
  if (match) return match;
102
111
 
@@ -104,13 +113,13 @@ async function pickColor(promptFn) {
104
113
  return DEFAULT_COLOR;
105
114
  }
106
115
 
107
- // ── Theme mode picker ───────────────────────────────────────────────
116
+ // ── Theme mode picker ────────────────────────────────────────────────
108
117
 
109
118
  async function pickTheme(promptFn) {
110
119
  blank();
111
- log(`${BOLD}${WHITE} Theme modes:${RESET}`);
120
+ log(`${BOLD}${WHITE} Default theme mode:${RESET}`);
112
121
  blank();
113
- log(` ${DIM} 1.${RESET} Dark-first ${DIM}(default)${RESET}`);
122
+ log(` ${DIM} 1.${RESET} Dark-first ${DIM}(default — follows system preference)${RESET}`);
114
123
  log(` ${DIM} 2.${RESET} Light-first`);
115
124
  blank();
116
125
 
@@ -123,15 +132,43 @@ async function pickTheme(promptFn) {
123
132
  return DEFAULT_THEME;
124
133
  }
125
134
 
126
- // ── File generators ─────────────────────────────────────────────────
135
+ // ── File copy utilities ──────────────────────────────────────────────
136
+
137
+ function copyDirRecursive(src, dest) {
138
+ let count = 0;
139
+ if (!fs.existsSync(src)) return count;
140
+ fs.mkdirSync(dest, { recursive: true });
141
+
142
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
143
+ const srcPath = path.join(src, entry.name);
144
+ const destPath = path.join(dest, entry.name);
145
+
146
+ if (entry.isDirectory()) {
147
+ count += copyDirRecursive(srcPath, destPath);
148
+ } else {
149
+ fs.copyFileSync(srcPath, destPath);
150
+ count++;
151
+ }
152
+ }
153
+ return count;
154
+ }
155
+
156
+ function copyFile(src, dest) {
157
+ if (!fs.existsSync(src)) return false;
158
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
159
+ fs.copyFileSync(src, dest);
160
+ return true;
161
+ }
162
+
163
+ // ── File generators ──────────────────────────────────────────────────
127
164
 
128
165
  function generateConfig(projectName, colorKey, themeMode) {
129
166
  return `/**
130
167
  * Omnira UI Configuration
131
168
  * Generated by: npx omnira-ui init
132
169
  *
133
- * This file defines your project's Omnira UI settings.
134
- * Import the generated CSS overrides in your root layout.
170
+ * Accent: ${colorKey}
171
+ * Theme: ${themeMode}
135
172
  */
136
173
 
137
174
  const omniraConfig = {
@@ -148,16 +185,10 @@ function generateCSS(colorKey, themeMode) {
148
185
  const preset = COLOR_PRESETS[colorKey];
149
186
  if (!preset) return "";
150
187
 
151
- // If lime (default), no overrides needed
152
188
  if (colorKey === "lime") {
153
189
  return `/**
154
190
  * Omnira UI — Theme Overrides
155
- * Generated by: npx omnira-ui init
156
- *
157
- * Accent: ${preset.label} (${preset.hex})
158
- * Theme: ${themeMode}
159
- *
160
- * Using default Lime preset — no overrides needed.
191
+ * Accent: ${preset.label} (${preset.hex}) — default, no overrides needed.
161
192
  * Import this file after globals.css in your root layout.
162
193
  */
163
194
  `;
@@ -165,8 +196,6 @@ function generateCSS(colorKey, themeMode) {
165
196
 
166
197
  let css = `/**
167
198
  * Omnira UI — Theme Overrides
168
- * Generated by: npx omnira-ui init
169
- *
170
199
  * Accent: ${preset.label} (${preset.hex})
171
200
  * Theme: ${themeMode}
172
201
  *
@@ -176,14 +205,12 @@ function generateCSS(colorKey, themeMode) {
176
205
 
177
206
  `;
178
207
 
179
- // Dark overrides
180
208
  css += `[data-theme="dark"] {\n`;
181
209
  for (const [prop, value] of Object.entries(preset.dark)) {
182
210
  css += ` ${prop}: ${value};\n`;
183
211
  }
184
212
  css += `}\n\n`;
185
213
 
186
- // Light overrides
187
214
  css += `[data-theme="light"] {\n`;
188
215
  for (const [prop, value] of Object.entries(preset.light)) {
189
216
  css += ` ${prop}: ${value};\n`;
@@ -193,13 +220,25 @@ function generateCSS(colorKey, themeMode) {
193
220
  return css;
194
221
  }
195
222
 
223
+ function generateProviders(themeMode) {
224
+ const defaultTheme = themeMode === "light-first" ? "light" : "dark";
225
+ return `"use client";
226
+
227
+ import { ThemeProvider } from "@/lib/theme-context";
228
+
229
+ export function Providers({ children }: { children: React.ReactNode }) {
230
+ return <ThemeProvider defaultTheme="${defaultTheme}">{children}</ThemeProvider>;
231
+ }
232
+ `;
233
+ }
234
+
196
235
  // ── Main ─────────────────────────────────────────────────────────────
197
236
 
198
237
  async function main() {
199
238
  const { ask, close } = createPrompt();
200
239
 
201
240
  blank();
202
- log(` ${BOLD}${GREEN}✦${RESET} ${BOLD}${WHITE}Omnira UI Setup${RESET}`);
241
+ log(` ${BOLD}${GREEN}✦${RESET} ${BOLD}${WHITE}Omnira UI — Project Scaffolding${RESET}`);
203
242
  log(` ${DIM}The premium glassmorphism design system${RESET}`);
204
243
  blank();
205
244
 
@@ -226,31 +265,100 @@ async function main() {
226
265
  log(` ${DIM}Theme:${RESET} ${WHITE}${themeMode === "dark-first" ? "Dark-first" : "Light-first"}${RESET}`);
227
266
  blank();
228
267
 
229
- // ── Write files ──
268
+ log(` ${DIM}Scaffolding...${RESET}`);
269
+ blank();
270
+
230
271
  const cwd = process.cwd();
231
- const configPath = path.join(cwd, "omnira.config.ts");
232
- const cssPath = path.join(cwd, "omnira-overrides.css");
233
272
 
234
- // Config file
273
+ // ── 1. Copy all base components ──
274
+ const componentsSrc = path.join(PKG_ROOT, "components", "ui");
275
+ const componentsDest = path.join(cwd, "components", "ui");
276
+
277
+ if (fs.existsSync(componentsSrc)) {
278
+ const count = copyDirRecursive(componentsSrc, componentsDest);
279
+ log(` ${GREEN}✓${RESET} Copied ${BOLD}${count} files${RESET} → ${DIM}components/ui/${RESET}`);
280
+ } else {
281
+ log(` ${RED}✗${RESET} Components source not found at ${componentsSrc}`);
282
+ log(` ${DIM}This may happen when running locally. Components are bundled in the npm package.${RESET}`);
283
+ }
284
+
285
+ // ── 2. Copy lib utilities ──
286
+ const libFiles = ["cn.ts", "copy-to-clipboard.ts", "theme-context.tsx"];
287
+ const libDest = path.join(cwd, "lib");
288
+ fs.mkdirSync(libDest, { recursive: true });
289
+
290
+ for (const file of libFiles) {
291
+ const src = path.join(PKG_ROOT, "lib", file);
292
+ const dest = path.join(libDest, file);
293
+ if (copyFile(src, dest)) {
294
+ log(` ${GREEN}✓${RESET} Copied ${BOLD}lib/${file}${RESET}`);
295
+ }
296
+ }
297
+
298
+ // ── 3. Copy globals.css (design system tokens) ──
299
+ const globalsSrc = path.join(PKG_ROOT, "app", "globals.css");
300
+ const appDir = path.join(cwd, "app");
301
+ fs.mkdirSync(appDir, { recursive: true });
302
+
303
+ if (copyFile(globalsSrc, path.join(appDir, "globals.css"))) {
304
+ log(` ${GREEN}✓${RESET} Copied ${BOLD}app/globals.css${RESET} ${DIM}(design system tokens)${RESET}`);
305
+ }
306
+
307
+ // ── 4. Generate omnira.config.ts ──
308
+ const configPath = path.join(cwd, "omnira.config.ts");
235
309
  fs.writeFileSync(configPath, generateConfig(projectName, colorKey, themeMode), "utf-8");
236
310
  log(` ${GREEN}✓${RESET} Created ${BOLD}omnira.config.ts${RESET}`);
237
311
 
238
- // CSS overrides
239
- const cssContent = generateCSS(colorKey, themeMode);
240
- fs.writeFileSync(cssPath, cssContent, "utf-8");
312
+ // ── 5. Generate accent overrides CSS ──
313
+ const cssPath = path.join(cwd, "omnira-overrides.css");
314
+ fs.writeFileSync(cssPath, generateCSS(colorKey, themeMode), "utf-8");
241
315
  log(` ${GREEN}✓${RESET} Created ${BOLD}omnira-overrides.css${RESET}`);
242
316
 
317
+ // ── 6. Generate providers.tsx ──
318
+ const providersPath = path.join(appDir, "providers.tsx");
319
+ if (!fs.existsSync(providersPath)) {
320
+ fs.writeFileSync(providersPath, generateProviders(themeMode), "utf-8");
321
+ log(` ${GREEN}✓${RESET} Created ${BOLD}app/providers.tsx${RESET} ${DIM}(ThemeProvider wrapper)${RESET}`);
322
+ } else {
323
+ log(` ${YELLOW}~${RESET} Skipped ${BOLD}app/providers.tsx${RESET} ${DIM}(already exists)${RESET}`);
324
+ }
325
+
326
+ // ── Done ──
327
+ blank();
328
+ log(` ${DIM}─────────────────────────────────────${RESET}`);
329
+ blank();
330
+ log(` ${GREEN}✓${RESET} ${BOLD}${WHITE}Omnira UI scaffolded successfully!${RESET}`);
243
331
  blank();
244
332
 
333
+ log(` ${BOLD}${WHITE}Next steps:${RESET}`);
334
+ blank();
335
+ log(` ${DIM}1.${RESET} Install peer dependencies:`);
336
+ blank();
337
+ log(` ${CYAN}npm install iconsax-react clsx${RESET}`);
338
+ blank();
339
+ log(` ${DIM}2.${RESET} Wrap your root layout with the ThemeProvider:`);
340
+ blank();
341
+ log(` ${DIM}// app/layout.tsx${RESET}`);
342
+ log(` ${MAGENTA}import${RESET} ${WHITE}"./globals.css"${RESET};`);
245
343
  if (colorKey !== "lime") {
246
- log(` ${DIM}Import the overrides in your root layout:${RESET}`);
247
- blank();
248
- log(` ${MAGENTA}import${RESET} ${WHITE}"./globals.css"${RESET};`);
249
- log(` ${MAGENTA}import${RESET} ${WHITE}"./omnira-overrides.css"${RESET};`);
250
- blank();
344
+ log(` ${MAGENTA}import${RESET} ${WHITE}"../omnira-overrides.css"${RESET};`);
251
345
  }
252
-
253
- log(` ${GREEN}✓${RESET} ${BOLD}${WHITE}Ready to go!${RESET} Run ${CYAN}npx omnira-ui${RESET} to reconfigure anytime.`);
346
+ log(` ${MAGENTA}import${RESET} { Providers } ${MAGENTA}from${RESET} ${WHITE}"./providers"${RESET};`);
347
+ blank();
348
+ log(` ${DIM}export default function RootLayout({ children }) {${RESET}`);
349
+ log(` ${DIM} return (${RESET}`);
350
+ log(` ${DIM} <html data-theme="${themeMode === "light-first" ? "light" : "dark"}">${RESET}`);
351
+ log(` ${DIM} <body><Providers>{children}</Providers></body>${RESET}`);
352
+ log(` ${DIM} </html>${RESET}`);
353
+ log(` ${DIM} );${RESET}`);
354
+ log(` ${DIM}}${RESET}`);
355
+ blank();
356
+ log(` ${DIM}3.${RESET} Use components:`);
357
+ blank();
358
+ log(` ${MAGENTA}import${RESET} { Button } ${MAGENTA}from${RESET} ${WHITE}"@/components/ui/Button"${RESET};`);
359
+ blank();
360
+ log(` ${DIM}4.${RESET} Browse all components & copy advanced ones:`);
361
+ log(` ${CYAN}https://ui.omnira.space${RESET}`);
254
362
  blank();
255
363
  }
256
364
 
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./AppStoreButton.module.css";
6
6
 
7
7
  export type AppStoreSize = "sm" | "md" | "lg";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Avatar.module.css";
6
6
 
7
7
  export interface AvatarProps {
@@ -1,4 +1,4 @@
1
- import { cn } from "../../../lib/cn";
1
+ import { cn } from "@/lib/cn";
2
2
  import styles from "./Badge.module.css";
3
3
 
4
4
  export interface BadgeProps {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import { ArrowRight2 } from "iconsax-react";
6
6
  import styles from "./BadgeGroup.module.css";
7
7
 
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Browser.module.css";
6
6
 
7
7
  export interface BrowserProps {
@@ -1,5 +1,5 @@
1
1
  import { forwardRef } from "react";
2
- import { cn } from "../../../lib/cn";
2
+ import { cn } from "@/lib/cn";
3
3
  import styles from "./Button.module.css";
4
4
 
5
5
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef, useState, useRef, useEffect } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./ButtonUtility.module.css";
6
6
 
7
7
  export type UtilitySize = "xs" | "sm" | "md" | "lg";
@@ -1,5 +1,5 @@
1
1
  import { forwardRef } from "react";
2
- import { cn } from "../../../lib/cn";
2
+ import { cn } from "@/lib/cn";
3
3
  import styles from "./Card.module.css";
4
4
 
5
5
  export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef, useState, useCallback, useId } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Checkbox.module.css";
6
6
 
7
7
  export interface CheckboxProps {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useCallback, createContext, useContext, forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Collapse.module.css";
6
6
 
7
7
  /* ══════════════════════════════════════
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./CreditCard.module.css";
6
6
 
7
7
  export interface CreditCardProps {
@@ -10,7 +10,7 @@ import {
10
10
  forwardRef,
11
11
  } from "react";
12
12
  import { TickSquare, MinusSquare, Record as RadioIcon, ArrowRight2 } from "iconsax-react";
13
- import { cn } from "../../../lib/cn";
13
+ import { cn } from "@/lib/cn";
14
14
  import styles from "./Dropdown.module.css";
15
15
 
16
16
  /* ── Context ── */
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Illustration.module.css";
6
6
 
7
7
  export interface IllustrationProps {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Input.module.css";
6
6
 
7
7
  export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Phone.module.css";
6
6
 
7
7
  export interface PhoneProps {
@@ -8,7 +8,7 @@ import {
8
8
  useCallback,
9
9
  forwardRef,
10
10
  } from "react";
11
- import { cn } from "../../../lib/cn";
11
+ import { cn } from "@/lib/cn";
12
12
  import styles from "./PinInput.module.css";
13
13
 
14
14
  /* ── Context ── */
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { cn } from "../../../lib/cn";
3
+ import { cn } from "@/lib/cn";
4
4
  import styles from "./ProgressBar.module.css";
5
5
 
6
6
  /* ── Linear Progress Bar ── */
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useMemo, forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./QRCode.module.css";
6
6
 
7
7
  export interface QRCodeProps {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { createContext, useContext, useState, useCallback, forwardRef, useId } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./RadioButton.module.css";
6
6
 
7
7
  /* ── Group Context ── */
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useCallback } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./RadioGroup.module.css";
6
6
 
7
7
  /* ── Shared types ── */
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useCallback, forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Rating.module.css";
6
6
 
7
7
  /* ══════════════════════════════════════
@@ -7,7 +7,7 @@ import {
7
7
  useCallback,
8
8
  } from "react";
9
9
  import { ArrowDown2, SearchNormal1, CloseCircle, TickCircle } from "iconsax-react";
10
- import { cn } from "../../../lib/cn";
10
+ import { cn } from "@/lib/cn";
11
11
  import styles from "./Select.module.css";
12
12
 
13
13
  /* ── Types ── */
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import type { NavItemType, NavItemDividerType } from "./types";
6
6
  import { isDivider, NavRow, SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
7
7
  import type { UserCardProps } from "./SidebarParts";
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { useState, useCallback } from "react";
4
4
  import { CloseCircle, Copy, TickCircle, Calendar, Star1, Flash, MessageText1, Scan, Gift, Headphones, Crown1, Clock } from "iconsax-react";
5
- import { cn } from "../../../lib/cn";
6
- import { Button } from "../Button";
7
- import { Badge } from "../Badge";
8
- import { copyToClipboard } from "../../../lib/copy-to-clipboard";
5
+ import { cn } from "@/lib/cn";
6
+ import { Button } from "@/components/ui/Button";
7
+ import { Badge } from "@/components/ui/Badge";
8
+ import { copyToClipboard } from "@/lib/copy-to-clipboard";
9
9
  import styles from "./SidebarFeatureCard.module.css";
10
10
 
11
11
  /* ── 1. Progress ── */
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useRef, useEffect, useCallback } from "react";
4
4
  import { ArrowDown2, SearchNormal1, ExportSquare, ArrowUp2, User, Setting2, Book1, LogoutCurve, AddCircle } from "iconsax-react";
5
- import { cn } from "../../../lib/cn";
5
+ import { cn } from "@/lib/cn";
6
6
  import type { NavItemType, NavItemDividerType, NavSubItem } from "./types";
7
7
  import styles from "./SidebarNavigation.module.css";
8
8
 
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { cn } from "../../../lib/cn";
3
+ import { cn } from "@/lib/cn";
4
4
  import type { NavItemType, NavItemDividerType } from "./types";
5
5
  import { isDivider, NavRow, SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
6
6
  import type { UserCardProps } from "./SidebarParts";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { cn } from "../../../lib/cn";
3
+ import { cn } from "@/lib/cn";
4
4
  import type { NavSectionType } from "./types";
5
5
  import { isDivider, NavRow, SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
6
6
  import type { UserCardProps } from "./SidebarParts";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { cn } from "../../../lib/cn";
3
+ import { cn } from "@/lib/cn";
4
4
  import type { NavItemType, NavItemDividerType } from "./types";
5
5
  import { isDivider, NavRow, SidebarSearchBar, SidebarUserCard } from "./SidebarParts";
6
6
  import type { UserCardProps } from "./SidebarParts";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { cn } from "../../../lib/cn";
3
+ import { cn } from "@/lib/cn";
4
4
  import type { NavItemType, NavItemDividerType } from "./types";
5
5
  import styles from "./SidebarNavigation.module.css";
6
6
 
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useCallback, useRef, useEffect, forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Slider.module.css";
6
6
 
7
7
  export interface SliderProps {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./SocialButton.module.css";
6
6
 
7
7
  export type SocialProvider =
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState } from "react";
4
4
  import { CloseCircle, TickSquare, MinusSquare } from "iconsax-react";
5
- import { cn } from "../../../lib/cn";
5
+ import { cn } from "@/lib/cn";
6
6
  import styles from "./Tag.module.css";
7
7
 
8
8
  export type TagSize = "sm" | "md" | "lg";
@@ -8,7 +8,7 @@ import {
8
8
  useCallback,
9
9
  useEffect,
10
10
  } from "react";
11
- import { cn } from "../../../lib/cn";
11
+ import { cn } from "@/lib/cn";
12
12
  import styles from "./TextEditor.module.css";
13
13
 
14
14
  /* ── Context ── */
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Textarea.module.css";
6
6
 
7
7
  export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef, useState, useCallback } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Toggle.module.css";
6
6
 
7
7
  export interface ToggleProps {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useRef, useCallback, useEffect } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./Tooltip.module.css";
6
6
 
7
7
  export interface TooltipProps {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useRef, useCallback, useEffect, forwardRef } from "react";
4
- import { cn } from "../../../lib/cn";
4
+ import { cn } from "@/lib/cn";
5
5
  import styles from "./VideoPlayer.module.css";
6
6
 
7
7
  export interface VideoPlayerProps {
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useEffect, useState, useCallback } from "react";
4
+
5
+ type Theme = "dark" | "light";
6
+
7
+ interface ThemeContextType {
8
+ theme: Theme;
9
+ toggleTheme: () => void;
10
+ setTheme: (theme: Theme) => void;
11
+ }
12
+
13
+ interface ThemeProviderProps {
14
+ children: React.ReactNode;
15
+ defaultTheme?: Theme;
16
+ }
17
+
18
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
19
+
20
+ const STORAGE_KEY = "omnira-ui-theme";
21
+
22
+ function getSystemTheme(): Theme {
23
+ if (typeof window !== "undefined" && window.matchMedia) {
24
+ return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
25
+ }
26
+ return "dark";
27
+ }
28
+
29
+ export function ThemeProvider({ children, defaultTheme }: ThemeProviderProps) {
30
+ const [theme, setThemeState] = useState<Theme>(defaultTheme ?? "dark");
31
+ const [mounted, setMounted] = useState(false);
32
+
33
+ useEffect(() => {
34
+ const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
35
+ if (stored === "dark" || stored === "light") {
36
+ setThemeState(stored);
37
+ } else {
38
+ setThemeState(defaultTheme ?? getSystemTheme());
39
+ }
40
+ setMounted(true);
41
+ }, [defaultTheme]);
42
+
43
+ useEffect(() => {
44
+ if (mounted) {
45
+ document.documentElement.setAttribute("data-theme", theme);
46
+ localStorage.setItem(STORAGE_KEY, theme);
47
+ }
48
+ }, [theme, mounted]);
49
+
50
+ const toggleTheme = useCallback(() => {
51
+ setThemeState((prev) => (prev === "dark" ? "light" : "dark"));
52
+ }, []);
53
+
54
+ const setTheme = useCallback((newTheme: Theme) => {
55
+ setThemeState(newTheme);
56
+ }, []);
57
+
58
+ if (!mounted) {
59
+ return <>{children}</>;
60
+ }
61
+
62
+ return (
63
+ <ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
64
+ {children}
65
+ </ThemeContext.Provider>
66
+ );
67
+ }
68
+
69
+ export function useTheme(): ThemeContextType {
70
+ const context = useContext(ThemeContext);
71
+ if (!context) {
72
+ return {
73
+ theme: "dark",
74
+ toggleTheme: () => {},
75
+ setTheme: () => {},
76
+ };
77
+ }
78
+ return context;
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnira-ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A premium glassmorphism design system — dark-first, glass-forward. 30+ base components, sidebar navigation, feature cards, and a CLI to scaffold your project.",
5
5
  "keywords": [
6
6
  "design-system",
@@ -29,6 +29,8 @@
29
29
  "components/ui/",
30
30
  "lib/cn.ts",
31
31
  "lib/copy-to-clipboard.ts",
32
+ "lib/theme-context.tsx",
33
+ "app/globals.css",
32
34
  "README.md",
33
35
  "LICENSE"
34
36
  ],