xertica-ui 1.4.4 → 1.4.7

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 (47) hide show
  1. package/bin/cli.ts +292 -372
  2. package/components/Header.tsx +58 -11
  3. package/components/TemplateContent.tsx +767 -734
  4. package/components/ThemeToggle.tsx +66 -64
  5. package/components/XerticaProvider.tsx +5 -3
  6. package/components/index.ts +66 -59
  7. package/contexts/BrandColorsContext.tsx +222 -217
  8. package/contexts/ThemeContext.tsx +109 -91
  9. package/dist/cli.js +139 -198
  10. package/dist/components/Header.d.ts +10 -4
  11. package/dist/components/ThemeToggle.d.ts +1 -1
  12. package/dist/components/XerticaProvider.d.ts +3 -1
  13. package/dist/components/index.d.ts +3 -0
  14. package/dist/contexts/BrandColorsContext.d.ts +4 -2
  15. package/dist/contexts/ThemeContext.d.ts +5 -2
  16. package/dist/index.es.js +306 -126
  17. package/dist/index.umd.js +305 -125
  18. package/package.json +126 -136
  19. package/templates/.env.example +2 -0
  20. package/templates/eslint.config.js +29 -0
  21. package/templates/index.css +10 -0
  22. package/templates/index.html +13 -0
  23. package/templates/package.json +40 -0
  24. package/{postcss.config.js → templates/postcss.config.js} +1 -1
  25. package/{App.tsx → templates/src/App.tsx} +19 -48
  26. package/templates/src/content/HomeContent.tsx +88 -0
  27. package/templates/src/content/TemplateContent.tsx +720 -0
  28. package/templates/src/main.tsx +10 -0
  29. package/templates/src/pages/ForgotPasswordPage.tsx +148 -0
  30. package/templates/src/pages/HomePage.tsx +61 -0
  31. package/templates/src/pages/LoginPage.tsx +183 -0
  32. package/templates/src/pages/ResetPasswordPage.tsx +199 -0
  33. package/templates/src/pages/TemplatePage.tsx +40 -0
  34. package/templates/src/pages/VerifyEmailPage.tsx +166 -0
  35. package/{routes.tsx → templates/src/routes.tsx} +1 -1
  36. package/templates/tsconfig.json +21 -0
  37. package/templates/tsconfig.node.json +12 -0
  38. package/templates/vite-env.d.ts +1 -0
  39. package/templates/vite.config.ts +7 -0
  40. package/eslint.config.js +0 -41
  41. package/index.css +0 -4364
  42. package/index.html +0 -14
  43. package/main.tsx +0 -10
  44. package/tsconfig.json +0 -46
  45. package/tsconfig.node.json +0 -10
  46. package/vite-env.d.ts +0 -12
  47. package/vite.config.ts +0 -59
package/bin/cli.ts CHANGED
@@ -1,372 +1,292 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import prompts from 'prompts';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import fs from 'fs-extra';
7
- import path from 'path';
8
- import { fileURLToPath } from 'url';
9
- import { execa } from 'execa';
10
- import { colorThemes } from '../contexts/theme-data';
11
- import { generateTokensCss } from './generate-tokens';
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(__filename);
15
-
16
- const program = new Command();
17
-
18
- program
19
- .name('xertica-ui')
20
- .description('CLI to initialize Xertica UI projects')
21
- .version('1.0.0');
22
-
23
- program
24
- .command('init')
25
- .description('Initialize a new Xertica UI project')
26
- .argument('[directory]', 'Directory to initialize in', '.')
27
- .action(async (directory) => {
28
- const targetDir = path.resolve(process.cwd(), directory);
29
- const sourceRoot = path.resolve(__dirname, '../');
30
-
31
- // Scan for components
32
- const uiComponentsDir = path.join(sourceRoot, 'components/ui');
33
- let componentChoices = [{ title: 'All Components', value: 'all', selected: true }];
34
-
35
- if (await fs.pathExists(uiComponentsDir)) {
36
- const files = await fs.readdir(uiComponentsDir);
37
- const components = files
38
- .filter(f => f.endsWith('.tsx') && !f.startsWith('index'))
39
- .map(f => ({ title: f.replace('.tsx', ''), value: f, selected: true }));
40
-
41
- componentChoices = [
42
- { title: 'All Components', value: 'all', selected: true },
43
- ...components
44
- ];
45
- }
46
-
47
-
48
-
49
- console.log(chalk.blue('🚀 Welcome to Xertica UI CLI!'));
50
-
51
- const response = await prompts([
52
- {
53
- type: 'multiselect',
54
- name: 'components',
55
- message: 'Which components would you like to include?',
56
- choices: componentChoices.length > 1 ? componentChoices : [
57
- { title: 'All Components', value: 'all', selected: true },
58
- // Fetch components logic was above, I'll rely on existing code for that part
59
- // and just insert my code around existing blocks in separate steps if needed.
60
- // But replace_file_content requires me to match content.
61
- ],
62
- hint: '- Space to select. Return to submit',
63
- min: 1
64
- },
65
- {
66
- type: 'multiselect',
67
- name: 'pages',
68
- message: 'Which pages/templates to include?',
69
- choices: [
70
- { title: 'Login Page', value: 'login', selected: true },
71
- { title: 'Home Page', value: 'home', selected: true },
72
- { title: 'Template Page', value: 'template', selected: true },
73
- ]
74
- },
75
- {
76
- type: 'select',
77
- name: 'theme',
78
- message: 'Select the default color theme for your project:',
79
- choices: colorThemes.map(t => ({
80
- title: t.name,
81
- description: t.description,
82
- value: t.id
83
- })),
84
- initial: 0
85
- },
86
- {
87
- type: 'confirm',
88
- name: 'install',
89
- message: 'Install dependencies automatically?',
90
- initial: true
91
- }
92
- ]);
93
-
94
- if (!response.components) return;
95
-
96
- const spinner = ora('Initializing project...').start();
97
-
98
- try {
99
- // 1. Create Target Directory
100
- await fs.ensureDir(targetDir);
101
-
102
- // 2. Copy Base Files
103
- const foldersToCopy = ['components', 'contexts', 'styles', 'hooks', 'assets', 'imports', 'utils', 'guidelines'];
104
- const filesToCopy = [
105
- 'package.json', 'tsconfig.json', 'tsconfig.node.json', 'vite.config.ts',
106
- 'index.html', 'routes.tsx', 'main.tsx', 'postcss.config.js',
107
- 'vite-env.d.ts', 'index.css', 'eslint.config.js'
108
- ];
109
- // Note: App.tsx is handled separately
110
-
111
- for (const file of filesToCopy) {
112
- const srcPath = path.join(sourceRoot, file);
113
- const destPath = path.join(targetDir, file);
114
- if (await fs.pathExists(srcPath)) {
115
- await fs.copy(srcPath, destPath);
116
- }
117
- }
118
-
119
- // Create .env.example
120
- const envExampleContent = `VITE_GOOGLE_MAPS_API_KEY=
121
- VITE_GEMINI_API_KEY=`;
122
- await fs.writeFile(path.join(targetDir, '.env.example'), envExampleContent);
123
-
124
- // Handle App.tsx specifically
125
- const pages = response.pages || [];
126
- const hasLogin = pages.includes('login');
127
- const hasHome = pages.includes('home');
128
- const hasTemplate = pages.includes('template');
129
- const hasAllPages = hasLogin && hasHome && hasTemplate;
130
-
131
- if (hasAllPages) {
132
- if (await fs.pathExists(path.join(sourceRoot, 'App.tsx'))) {
133
- await fs.copy(path.join(sourceRoot, 'App.tsx'), path.join(targetDir, 'App.tsx'));
134
- }
135
- } else {
136
- // Minimal App.tsx
137
- const minimalApp = `import React from 'react';
138
- import { Toaster } from './components/ui/sonner';
139
-
140
- export default function App() {
141
- return (
142
- <div className="min-h-screen bg-white p-8">
143
- <h1 className="text-3xl font-bold mb-4">Welcome to Xertica UI</h1>
144
- <p>Start building your app by editing App.tsx</p>
145
- <Toaster />
146
- </div>
147
- );
148
- }
149
- `;
150
- await fs.writeFile(path.join(targetDir, 'App.tsx'), minimalApp);
151
- }
152
-
153
- // Copy folders with filter
154
- const selectedComponents = response.components;
155
- const copyAllComponents = selectedComponents.includes('all');
156
-
157
- for (const folder of foldersToCopy) {
158
- const srcPath = path.join(sourceRoot, folder);
159
- if (await fs.pathExists(srcPath)) {
160
- await fs.copy(srcPath, path.join(targetDir, folder), {
161
- filter: (src) => {
162
- const basename = path.basename(src);
163
- if (basename === 'node_modules') return false;
164
-
165
- // Component filtering
166
- const isUI = src.includes(path.join('components', 'ui'));
167
-
168
- if (isUI && folder === 'components') {
169
- if (basename === 'index.ts') return true;
170
- if (copyAllComponents) return true;
171
-
172
- // If it's a file
173
- if (path.extname(basename) === '.tsx') {
174
- if (!selectedComponents.includes(basename)) return false;
175
- }
176
- }
177
-
178
- // Page filtering (in components root)
179
- const isComponentsRoot = src.endsWith('components') || path.dirname(src).endsWith('components');
180
- if (isComponentsRoot && folder === 'components') {
181
- if (!hasLogin && basename === 'LoginPage.tsx') return false;
182
- if (!hasLogin && basename === 'ForgotPasswordPage.tsx') return false;
183
- if (!hasLogin && basename === 'VerifyEmailPage.tsx') return false;
184
- if (!hasLogin && basename === 'ResetPasswordPage.tsx') return false;
185
-
186
- if (!hasHome && basename === 'HomePage.tsx') return false;
187
- if (!hasTemplate && basename === 'TemplatePage.tsx') return false;
188
- }
189
-
190
- return true;
191
- }
192
- });
193
- }
194
- }
195
-
196
- // --- Apply Selected Theme ---
197
- if (response.theme) {
198
- // 1. Update React Context
199
- const contextPath = path.join(targetDir, 'contexts', 'BrandColorsContext.tsx');
200
- if (await fs.pathExists(contextPath)) {
201
- let content = await fs.readFile(contextPath, 'utf8');
202
- content = content.replace(
203
- /return saved \|\| 'xertica-original';/,
204
- `return saved || '${response.theme}';`
205
- );
206
- await fs.writeFile(contextPath, content);
207
- }
208
-
209
- // 2. Update styles/xertica/tokens.css
210
- const tokensPath = path.join(targetDir, 'styles', 'xertica', 'tokens.css');
211
- const selectedTheme = colorThemes.find(t => t.id === response.theme);
212
-
213
- if (selectedTheme) {
214
- // Ensure the directory exists (it should, from copy)
215
- await fs.ensureDir(path.dirname(tokensPath));
216
-
217
- const newTokensCss = generateTokensCss(selectedTheme);
218
- await fs.writeFile(tokensPath, newTokensCss);
219
- }
220
- }
221
-
222
- spinner.succeed('Project initialized successfully!');
223
-
224
- if (response.install) {
225
- const installSpinner = ora('Installing dependencies...').start();
226
- await execa('npm', ['install'], { cwd: targetDir });
227
- installSpinner.succeed('Dependencies installed!');
228
- }
229
-
230
- console.log(chalk.green('\nDone! Now run:'));
231
- console.log(chalk.cyan(` cd ${directory}`));
232
- console.log(chalk.cyan(' npm run dev'));
233
-
234
- } catch (error) {
235
- spinner.fail('Failed to initialize project');
236
- console.error(error);
237
- }
238
- });
239
-
240
- program
241
- .command('update')
242
- .description('Update components in your project')
243
- .argument('[components...]', 'Components to update')
244
- .option('-a, --all', 'Update all installed components')
245
- .action(async (components, options) => {
246
- const targetDir = process.cwd();
247
- const sourceRoot = path.resolve(__dirname, '../');
248
- const uiComponentsDir = path.join(targetDir, 'components/ui');
249
- const sourceUiDir = path.join(sourceRoot, 'components/ui');
250
-
251
- if (!await fs.pathExists(uiComponentsDir)) {
252
- console.error(chalk.red('Components directory not found. Is this a Xertica UI project?'));
253
- return;
254
- }
255
-
256
- if (!await fs.pathExists(sourceUiDir)) {
257
- console.error(chalk.red('Source components not found. Try reinstalling the CLI package.'));
258
- return;
259
- }
260
-
261
- let componentsToUpdate: string[] = [];
262
-
263
- if (options.all) {
264
- const files = await fs.readdir(uiComponentsDir);
265
- componentsToUpdate = files.filter(f => f.endsWith('.tsx'));
266
- } else if (components && components.length > 0) {
267
- componentsToUpdate = components.map((c: string) => c.endsWith('.tsx') ? c : `${c}.tsx`);
268
- } else {
269
- // Interactive mode
270
- const files = await fs.readdir(uiComponentsDir);
271
- const installedComponents = files.filter(f => f.endsWith('.tsx'));
272
-
273
- if (installedComponents.length === 0) {
274
- console.log(chalk.yellow('No components found to update.'));
275
- return;
276
- }
277
-
278
- const response = await prompts({
279
- type: 'multiselect',
280
- name: 'selected',
281
- message: 'Select components to update',
282
- choices: installedComponents.map(f => ({ title: f, value: f })),
283
- min: 1
284
- });
285
-
286
- if (!response.selected) return;
287
- componentsToUpdate = response.selected;
288
- }
289
-
290
-
291
-
292
- // --- Theme Update Logic ---
293
- // Ask if user wants to update theme
294
- const themeResponse = await prompts({
295
- type: 'confirm',
296
- name: 'updateTheme',
297
- message: 'Do you want to update/change the project theme?',
298
- initial: false
299
- });
300
-
301
- if (themeResponse.updateTheme) {
302
- const themeSelection = await prompts({
303
- type: 'select',
304
- name: 'theme',
305
- message: 'Select the new color theme:',
306
- choices: colorThemes.map(t => ({
307
- title: t.name,
308
- description: t.description,
309
- value: t.id
310
- })),
311
- initial: 0
312
- });
313
-
314
- if (themeSelection.theme) {
315
- const spinnerTheme = ora('Updating theme...').start();
316
- // 1. Update React Context
317
- const contextPath = path.join(targetDir, 'contexts', 'BrandColorsContext.tsx');
318
- if (await fs.pathExists(contextPath)) {
319
- let content = await fs.readFile(contextPath, 'utf8');
320
- // Replace existing default. We can try to find any string like "return saved || '...';"
321
- // The regex needs to be robust to catch whatever is there currently.
322
- // It might be 'xertica-original' or previously set 'rose', etc.
323
- // Regex: /return saved \|\| '([a-zA-Z0-9-]+)';/
324
- content = content.replace(
325
- /return saved \|\| '([a-zA-Z0-9-]+)';/,
326
- `return saved || '${themeSelection.theme}';`
327
- );
328
- await fs.writeFile(contextPath, content);
329
- } else {
330
- spinnerTheme.warn('BrandColorsContext.tsx not found. Skipping context update.');
331
- }
332
-
333
- // 2. Update styles/xertica/tokens.css
334
- const tokensPath = path.join(targetDir, 'styles', 'xertica', 'tokens.css');
335
- const selectedTheme = colorThemes.find(t => t.id === themeSelection.theme);
336
-
337
- if (selectedTheme) {
338
- await fs.ensureDir(path.dirname(tokensPath));
339
- const newTokensCss = generateTokensCss(selectedTheme);
340
- await fs.writeFile(tokensPath, newTokensCss);
341
- }
342
- spinnerTheme.succeed('Theme updated successfully!');
343
- }
344
- }
345
-
346
- const spinner = ora('Updating components...').start();
347
-
348
- try {
349
- let updatedCount = 0;
350
- for (const component of componentsToUpdate) {
351
- const srcPath = path.join(sourceUiDir, component);
352
- const destPath = path.join(uiComponentsDir, component);
353
-
354
- if (await fs.pathExists(srcPath)) {
355
- await fs.copy(srcPath, destPath);
356
- updatedCount++;
357
- } else {
358
- spinner.warn(`Component ${component} not found in source.`);
359
- }
360
- }
361
- if (updatedCount > 0) {
362
- spinner.succeed(`Successfully updated ${updatedCount} components!`);
363
- } else {
364
- spinner.info('No components updated.');
365
- }
366
- } catch (error) {
367
- spinner.fail('Failed to update components');
368
- console.error(error);
369
- }
370
- });
371
-
372
- program.parse();
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import prompts from 'prompts';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { execa } from 'execa';
10
+ import { colorThemes } from '../contexts/theme-data';
11
+ import { generateTokensCss } from './generate-tokens';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('xertica-ui')
20
+ .description('CLI to initialize Xertica UI projects')
21
+ .version('1.0.0');
22
+
23
+ program
24
+ .command('init')
25
+ .description('Initialize a new Xertica UI project')
26
+ .argument('[directory]', 'Directory to initialize in', '.')
27
+ .action(async (directory) => {
28
+ const targetDir = path.resolve(process.cwd(), directory);
29
+ // The templates directory is bundled alongside the CLI
30
+ const templatesDir = path.resolve(__dirname, '../templates');
31
+
32
+ console.log(chalk.blue('🚀 Welcome to Xertica UI CLI!'));
33
+
34
+ const response = await prompts([
35
+ {
36
+ type: 'multiselect',
37
+ name: 'pages',
38
+ message: 'Which pages/templates to include?',
39
+ choices: [
40
+ { title: 'Login Page (+ Forgot / Verify / Reset Password)', value: 'login', selected: true },
41
+ { title: 'Home Page', value: 'home', selected: true },
42
+ { title: 'Template Page (components showcase)', value: 'template', selected: true },
43
+ ]
44
+ },
45
+ {
46
+ type: 'select',
47
+ name: 'theme',
48
+ message: 'Select the default color theme for your project:',
49
+ choices: colorThemes.map(t => ({
50
+ title: t.name,
51
+ description: t.description,
52
+ value: t.id
53
+ })),
54
+ initial: 0
55
+ },
56
+ {
57
+ type: 'confirm',
58
+ name: 'install',
59
+ message: 'Install dependencies automatically?',
60
+ initial: true
61
+ }
62
+ ]);
63
+
64
+ if (!response.pages) return;
65
+
66
+ const spinner = ora('Initializing project...').start();
67
+
68
+ try {
69
+ // 1. Create Target Directory
70
+ await fs.ensureDir(targetDir);
71
+
72
+ const pages = response.pages || [];
73
+ const hasLogin = pages.includes('login');
74
+ const hasHome = pages.includes('home');
75
+ const hasTemplate = pages.includes('template');
76
+
77
+ // 2. Copy root config files from templates/
78
+ const rootFilesToCopy = [
79
+ 'index.html',
80
+ 'vite.config.ts',
81
+ 'tsconfig.json',
82
+ 'tsconfig.node.json',
83
+ 'postcss.config.js',
84
+ 'vite-env.d.ts',
85
+ 'eslint.config.js',
86
+ 'index.css',
87
+ '.env.example',
88
+ ];
89
+
90
+ for (const file of rootFilesToCopy) {
91
+ const srcPath = path.join(templatesDir, file);
92
+ const destPath = path.join(targetDir, file);
93
+ if (await fs.pathExists(srcPath)) {
94
+ await fs.copy(srcPath, destPath);
95
+ }
96
+ }
97
+
98
+ // 3. Copy package.json template and customize it
99
+ const pkgTemplatePath = path.join(templatesDir, 'package.json');
100
+ if (await fs.pathExists(pkgTemplatePath)) {
101
+ const pkgContent = await fs.readJson(pkgTemplatePath);
102
+ // Use directory name as project name
103
+ const projectName = path.basename(targetDir) || 'my-xertica-app';
104
+ pkgContent.name = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
105
+ await fs.writeJson(path.join(targetDir, 'package.json'), pkgContent, { spaces: 2 });
106
+ }
107
+
108
+ // 4. Ensure src directory exists
109
+ await fs.ensureDir(path.join(targetDir, 'src'));
110
+
111
+ // 5. Copy src/App.tsx
112
+ await fs.copy(
113
+ path.join(templatesDir, 'src', 'App.tsx'),
114
+ path.join(targetDir, 'src', 'App.tsx')
115
+ );
116
+
117
+ // 6. Copy src/main.tsx
118
+ await fs.copy(
119
+ path.join(templatesDir, 'src', 'main.tsx'),
120
+ path.join(targetDir, 'src', 'main.tsx')
121
+ );
122
+
123
+ // 7. Copy src/routes.tsx
124
+ await fs.copy(
125
+ path.join(templatesDir, 'src', 'routes.tsx'),
126
+ path.join(targetDir, 'src', 'routes.tsx')
127
+ );
128
+
129
+ // 8. Copy pages
130
+ await fs.ensureDir(path.join(targetDir, 'src', 'pages'));
131
+
132
+ // Always needed pages
133
+ const alwaysCopyPages = ['LoginPage.tsx', 'ForgotPasswordPage.tsx', 'VerifyEmailPage.tsx', 'ResetPasswordPage.tsx'];
134
+ const optionalPages: Record<string, boolean> = {
135
+ 'LoginPage.tsx': hasLogin,
136
+ 'ForgotPasswordPage.tsx': hasLogin,
137
+ 'VerifyEmailPage.tsx': hasLogin,
138
+ 'ResetPasswordPage.tsx': hasLogin,
139
+ 'HomePage.tsx': hasHome,
140
+ 'TemplatePage.tsx': hasTemplate,
141
+ };
142
+
143
+ const srcPagesDir = path.join(templatesDir, 'src', 'pages');
144
+ for (const [file, shouldCopy] of Object.entries(optionalPages)) {
145
+ if (shouldCopy) {
146
+ const srcPath = path.join(srcPagesDir, file);
147
+ if (await fs.pathExists(srcPath)) {
148
+ await fs.copy(srcPath, path.join(targetDir, 'src', 'pages', file));
149
+ }
150
+ }
151
+ }
152
+
153
+ // 9. Copy content components
154
+ await fs.ensureDir(path.join(targetDir, 'src', 'content'));
155
+ const srcContentDir = path.join(templatesDir, 'src', 'content');
156
+
157
+ if (hasHome && await fs.pathExists(path.join(srcContentDir, 'HomeContent.tsx'))) {
158
+ await fs.copy(
159
+ path.join(srcContentDir, 'HomeContent.tsx'),
160
+ path.join(targetDir, 'src', 'content', 'HomeContent.tsx')
161
+ );
162
+ }
163
+
164
+ if (hasTemplate && await fs.pathExists(path.join(srcContentDir, 'TemplateContent.tsx'))) {
165
+ await fs.copy(
166
+ path.join(srcContentDir, 'TemplateContent.tsx'),
167
+ path.join(targetDir, 'src', 'content', 'TemplateContent.tsx')
168
+ );
169
+ }
170
+
171
+ // 10. If NOT all pages selected, create a minimal App.tsx
172
+ if (!hasLogin || !hasHome || !hasTemplate) {
173
+ const imports: string[] = [];
174
+ const routes: string[] = [];
175
+
176
+ if (hasLogin) {
177
+ imports.push(`import { LoginPage } from './pages/LoginPage';`);
178
+ imports.push(`import { ForgotPasswordPage } from './pages/ForgotPasswordPage';`);
179
+ imports.push(`import { VerifyEmailPage } from './pages/VerifyEmailPage';`);
180
+ imports.push(`import { ResetPasswordPage } from './pages/ResetPasswordPage';`);
181
+ routes.push(` <Route path="/login" element={<LoginPage onLogin={handleLogin} />} />`);
182
+ routes.push(` <Route path="/forgot-password" element={<ForgotPasswordPage />} />`);
183
+ routes.push(` <Route path="/verify-email" element={<VerifyEmailPage />} />`);
184
+ routes.push(` <Route path="/reset-password" element={<ResetPasswordPage />} />`);
185
+ }
186
+ if (hasHome) {
187
+ imports.push(`import { HomePage } from './pages/HomePage';`);
188
+ routes.push(` <Route path="/home" element={<HomePage user={user} onLogout={handleLogout} />} />`);
189
+ }
190
+ if (hasTemplate) {
191
+ imports.push(`import { TemplatePage } from './pages/TemplatePage';`);
192
+ routes.push(` <Route path="/template" element={<TemplatePage user={user} onLogout={handleLogout} />} />`);
193
+ }
194
+
195
+ const minimalApp = `import React, { useState, useEffect, useLayoutEffect } from 'react';
196
+ import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
197
+ import { XerticaProvider, Toaster } from 'xertica-ui';
198
+ import 'xertica-ui/style.css';
199
+ ${imports.join('\n')}
200
+
201
+ export default function App() {
202
+ return (
203
+ <XerticaProvider>
204
+ <Router>
205
+ <Routes>
206
+ ${routes.join('\n')}
207
+ <Route path="/" element={<Navigate to="${hasLogin ? '/login' : hasHome ? '/home' : '/template'}" replace />} />
208
+ <Route path="*" element={<Navigate to="${hasLogin ? '/login' : hasHome ? '/home' : '/template'}" replace />} />
209
+ </Routes>
210
+ <Toaster position="top-right" richColors />
211
+ </Router>
212
+ </XerticaProvider>
213
+ );
214
+ }
215
+ `;
216
+ await fs.writeFile(path.join(targetDir, 'src', 'App.tsx'), minimalApp);
217
+ }
218
+
219
+ // 11. Generate theme tokens
220
+ const selectedTheme = colorThemes.find(t => t.id === response.theme) || colorThemes[0];
221
+
222
+ // Create styles/xertica directory
223
+ const tokensDir = path.join(targetDir, 'styles', 'xertica');
224
+ await fs.ensureDir(tokensDir);
225
+ const newTokensCss = generateTokensCss(selectedTheme);
226
+ await fs.writeFile(path.join(tokensDir, 'tokens.css'), newTokensCss);
227
+
228
+ spinner.succeed('Project initialized successfully!');
229
+
230
+ if (response.install) {
231
+ const installSpinner = ora('Installing dependencies...').start();
232
+ await execa('npm', ['install'], { cwd: targetDir });
233
+ installSpinner.succeed('Dependencies installed!');
234
+ }
235
+
236
+ console.log(chalk.green('\n✅ Done! Your Xertica UI project is ready.'));
237
+ console.log(chalk.cyan(`\n cd ${directory}`));
238
+ if (!response.install) {
239
+ console.log(chalk.cyan(' npm install'));
240
+ }
241
+ console.log(chalk.cyan(' npm run dev'));
242
+ console.log();
243
+ console.log(chalk.gray(' Components are imported from the xertica-ui package.'));
244
+ console.log(chalk.gray(' Customize the theme in styles/xertica/tokens.css'));
245
+
246
+ } catch (error) {
247
+ spinner.fail('Failed to initialize project');
248
+ console.error(error);
249
+ }
250
+ });
251
+
252
+ program
253
+ .command('update')
254
+ .description('Update theme tokens in your project')
255
+ .action(async () => {
256
+ const targetDir = process.cwd();
257
+
258
+ const themeResponse = await prompts({
259
+ type: 'select',
260
+ name: 'theme',
261
+ message: 'Select the new color theme:',
262
+ choices: colorThemes.map(t => ({
263
+ title: t.name,
264
+ description: t.description,
265
+ value: t.id
266
+ })),
267
+ initial: 0
268
+ });
269
+
270
+ if (!themeResponse.theme) return;
271
+
272
+ const spinner = ora('Updating theme...').start();
273
+
274
+ try {
275
+ const tokensPath = path.join(targetDir, 'styles', 'xertica', 'tokens.css');
276
+ const selectedTheme = colorThemes.find(t => t.id === themeResponse.theme);
277
+
278
+ if (selectedTheme) {
279
+ await fs.ensureDir(path.dirname(tokensPath));
280
+ const newTokensCss = generateTokensCss(selectedTheme);
281
+ await fs.writeFile(tokensPath, newTokensCss);
282
+ spinner.succeed(`Theme updated to "${selectedTheme.name}" successfully!`);
283
+ } else {
284
+ spinner.fail('Theme not found.');
285
+ }
286
+ } catch (error) {
287
+ spinner.fail('Failed to update theme');
288
+ console.error(error);
289
+ }
290
+ });
291
+
292
+ program.parse();