xertica-ui 2.0.1 → 2.0.2
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.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/bin/cli.ts +472 -285
- package/dist/cli.js +256 -91
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.0.2] — 2026-05-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **CLI rewritten for FSD/FDA structure** — `npx xertica-ui@latest init` agora copia corretamente a estrutura Feature-Sliced Design:
|
|
15
|
+
- Removida cópia de `src/app/routes.tsx` (arquivo não existe mais após refatoração)
|
|
16
|
+
- Adicionada cópia de `src/app/components/AppLayout.tsx`
|
|
17
|
+
- Adicionada cópia completa de `src/shared/` (`auth.ts`, `navigation.ts`, `types/auth.ts`)
|
|
18
|
+
- Adicionada cópia de `src/features/auth|home|template` conforme seleções do usuário
|
|
19
|
+
- Cópia de pages corrigida de `src/app/pages/` → `src/pages/`
|
|
20
|
+
- `AuthGuard.tsx` agora gerado dinamicamente com imports e rotas apenas das páginas selecionadas
|
|
21
|
+
- **`generateDemoResponse` exportado via `xertica-ui/assistant`** — estava ausente do barrel causando `SyntaxError: does not provide an export named 'generateDemoResponse'`
|
|
22
|
+
- **Build corrigido: UMD → CJS** — múltiplos entry points não são suportados com formato UMD no Vite; migrado para CJS (todos os `*.umd.js` → `*.cjs.js` no `package.json` exports)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
10
26
|
## [2.0.0] — 2026-05-11
|
|
11
27
|
|
|
12
28
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Enterprise-grade React design system** built on Tailwind CSS v4, Radix UI, and Lucide Icons — with a robust AI-first documentation layer for precise LLM-driven composition and autonomous agent interaction.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/xertica-ui)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
8
|
---
|
package/bin/cli.ts
CHANGED
|
@@ -1,285 +1,472 @@
|
|
|
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('
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{ title: '
|
|
41
|
-
{ title: '
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
await fs.copy(
|
|
113
|
-
path.join(templatesDir, 'src', 'app', 'App.tsx'),
|
|
114
|
-
path.join(targetDir, 'src', 'app', 'App.tsx')
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
await fs.
|
|
119
|
-
|
|
120
|
-
path.join(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
path.join(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
import {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
console.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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('2.0.2');
|
|
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 templatesDir = path.resolve(__dirname, '../templates');
|
|
30
|
+
|
|
31
|
+
console.log(chalk.blue('🚀 Welcome to Xertica UI CLI!'));
|
|
32
|
+
|
|
33
|
+
const response = await prompts([
|
|
34
|
+
{
|
|
35
|
+
type: 'multiselect',
|
|
36
|
+
name: 'pages',
|
|
37
|
+
message: 'Which pages/templates to include?',
|
|
38
|
+
choices: [
|
|
39
|
+
{ title: 'Login Page (+ Forgot / Verify / Reset Password)', value: 'login', selected: true },
|
|
40
|
+
{ title: 'Home Page', value: 'home', selected: true },
|
|
41
|
+
{ title: 'Template Page (components showcase)', value: 'template', selected: true },
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'select',
|
|
46
|
+
name: 'theme',
|
|
47
|
+
message: 'Select the default color theme for your project:',
|
|
48
|
+
choices: colorThemes.map(t => ({
|
|
49
|
+
title: t.name,
|
|
50
|
+
description: t.description,
|
|
51
|
+
value: t.id
|
|
52
|
+
})),
|
|
53
|
+
initial: 0
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'confirm',
|
|
57
|
+
name: 'install',
|
|
58
|
+
message: 'Install dependencies automatically?',
|
|
59
|
+
initial: true
|
|
60
|
+
}
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
if (!response.pages) return;
|
|
64
|
+
|
|
65
|
+
const spinner = ora('Initializing project...').start();
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await fs.ensureDir(targetDir);
|
|
69
|
+
|
|
70
|
+
const pages = response.pages || [];
|
|
71
|
+
const hasLogin = pages.includes('login');
|
|
72
|
+
const hasHome = pages.includes('home');
|
|
73
|
+
const hasTemplate = pages.includes('template');
|
|
74
|
+
|
|
75
|
+
// 1. Copy root config files
|
|
76
|
+
const rootFilesToCopy = [
|
|
77
|
+
'index.html',
|
|
78
|
+
'vite.config.ts',
|
|
79
|
+
'tsconfig.json',
|
|
80
|
+
'tsconfig.node.json',
|
|
81
|
+
'postcss.config.js',
|
|
82
|
+
'vite-env.d.ts',
|
|
83
|
+
'eslint.config.js',
|
|
84
|
+
'.env.example',
|
|
85
|
+
'guidelines',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const file of rootFilesToCopy) {
|
|
89
|
+
const srcPath = path.join(templatesDir, file);
|
|
90
|
+
if (await fs.pathExists(srcPath)) {
|
|
91
|
+
await fs.copy(srcPath, path.join(targetDir, file));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Copy package.json
|
|
96
|
+
const pkgTemplatePath = path.join(templatesDir, 'package.json');
|
|
97
|
+
if (await fs.pathExists(pkgTemplatePath)) {
|
|
98
|
+
const pkgContent = await fs.readJson(pkgTemplatePath);
|
|
99
|
+
const projectName = path.basename(targetDir) || 'my-xertica-app';
|
|
100
|
+
pkgContent.name = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
101
|
+
await fs.writeJson(path.join(targetDir, 'package.json'), pkgContent, { spaces: 2 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 3. Copy src/main.tsx
|
|
105
|
+
await fs.copy(
|
|
106
|
+
path.join(templatesDir, 'src', 'main.tsx'),
|
|
107
|
+
path.join(targetDir, 'src', 'main.tsx')
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// 4. Copy src/app/App.tsx
|
|
111
|
+
await fs.ensureDir(path.join(targetDir, 'src', 'app'));
|
|
112
|
+
await fs.copy(
|
|
113
|
+
path.join(templatesDir, 'src', 'app', 'App.tsx'),
|
|
114
|
+
path.join(targetDir, 'src', 'app', 'App.tsx')
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// 5. Copy src/app/components/AppLayout.tsx (always needed)
|
|
118
|
+
await fs.ensureDir(path.join(targetDir, 'src', 'app', 'components'));
|
|
119
|
+
await fs.copy(
|
|
120
|
+
path.join(templatesDir, 'src', 'app', 'components', 'AppLayout.tsx'),
|
|
121
|
+
path.join(targetDir, 'src', 'app', 'components', 'AppLayout.tsx')
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 6. Copy src/shared/ (always needed — auth helpers, navigation config, types)
|
|
125
|
+
await fs.copy(
|
|
126
|
+
path.join(templatesDir, 'src', 'shared'),
|
|
127
|
+
path.join(targetDir, 'src', 'shared')
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// 7. Copy features based on selections
|
|
131
|
+
if (hasLogin) {
|
|
132
|
+
await fs.copy(
|
|
133
|
+
path.join(templatesDir, 'src', 'features', 'auth'),
|
|
134
|
+
path.join(targetDir, 'src', 'features', 'auth')
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (hasHome) {
|
|
138
|
+
await fs.copy(
|
|
139
|
+
path.join(templatesDir, 'src', 'features', 'home'),
|
|
140
|
+
path.join(targetDir, 'src', 'features', 'home')
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (hasTemplate) {
|
|
144
|
+
await fs.copy(
|
|
145
|
+
path.join(templatesDir, 'src', 'features', 'template'),
|
|
146
|
+
path.join(targetDir, 'src', 'features', 'template')
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 8. Copy pages based on selections
|
|
151
|
+
await fs.ensureDir(path.join(targetDir, 'src', 'pages'));
|
|
152
|
+
|
|
153
|
+
const pagesToCopy: string[] = [];
|
|
154
|
+
if (hasLogin) pagesToCopy.push('LoginPage.tsx', 'ForgotPasswordPage.tsx', 'VerifyEmailPage.tsx', 'ResetPasswordPage.tsx');
|
|
155
|
+
if (hasHome) pagesToCopy.push('HomePage.tsx');
|
|
156
|
+
if (hasTemplate) pagesToCopy.push('TemplatePage.tsx');
|
|
157
|
+
|
|
158
|
+
for (const pageFile of pagesToCopy) {
|
|
159
|
+
const src = path.join(templatesDir, 'src', 'pages', pageFile);
|
|
160
|
+
if (await fs.pathExists(src)) {
|
|
161
|
+
await fs.copy(src, path.join(targetDir, 'src', 'pages', pageFile));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 9. Generate AuthGuard.tsx based on selected pages
|
|
166
|
+
const firstProtectedPath = hasHome ? '/home' : hasTemplate ? '/template' : '/login';
|
|
167
|
+
|
|
168
|
+
const authGuardImports: string[] = [
|
|
169
|
+
`import React, { useState, useEffect } from 'react';`,
|
|
170
|
+
`import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';`,
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
if (hasLogin) {
|
|
174
|
+
authGuardImports.push(`import { getStoredUser, storeUser, clearStoredUser } from '../../shared/lib/auth';`);
|
|
175
|
+
authGuardImports.push(`import type { User } from '../../shared/types/auth';`);
|
|
176
|
+
authGuardImports.push(`import { LoginPage } from '../../pages/LoginPage';`);
|
|
177
|
+
authGuardImports.push(`import { ForgotPasswordPage } from '../../pages/ForgotPasswordPage';`);
|
|
178
|
+
authGuardImports.push(`import { VerifyEmailPage } from '../../pages/VerifyEmailPage';`);
|
|
179
|
+
authGuardImports.push(`import { ResetPasswordPage } from '../../pages/ResetPasswordPage';`);
|
|
180
|
+
} else {
|
|
181
|
+
authGuardImports.push(`import { getStoredUser, storeUser, clearStoredUser } from '../../shared/lib/auth';`);
|
|
182
|
+
authGuardImports.push(`import type { User } from '../../shared/types/auth';`);
|
|
183
|
+
}
|
|
184
|
+
if (hasHome) authGuardImports.push(`import { HomePage } from '../../pages/HomePage';`);
|
|
185
|
+
if (hasTemplate) authGuardImports.push(`import { TemplatePage } from '../../pages/TemplatePage';`);
|
|
186
|
+
|
|
187
|
+
const protectedRoutes: string[] = [];
|
|
188
|
+
if (hasHome) protectedRoutes.push(` <Route path="/home" element={<ProtectedRoute user={user}><HomePage user={user} onLogout={handleLogout} /></ProtectedRoute>} />`);
|
|
189
|
+
if (hasTemplate) protectedRoutes.push(` <Route path="/template" element={<ProtectedRoute user={user}><TemplatePage user={user} onLogout={handleLogout} /></ProtectedRoute>} />`);
|
|
190
|
+
|
|
191
|
+
const authRoutes: string[] = [];
|
|
192
|
+
if (hasLogin) {
|
|
193
|
+
authRoutes.push(` <Route path="/login" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <LoginPage onLogin={handleLogin} />} />`);
|
|
194
|
+
authRoutes.push(` <Route path="/forgot-password" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <ForgotPasswordPage />} />`);
|
|
195
|
+
authRoutes.push(` <Route path="/verify-email" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <VerifyEmailPage />} />`);
|
|
196
|
+
authRoutes.push(` <Route path="/reset-password" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <ResetPasswordPage />} />`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const handleLoginFn = hasLogin ? `
|
|
200
|
+
const handleLogin = (email: string, password: string): boolean => {
|
|
201
|
+
if (!email.trim() || !password.trim()) return false;
|
|
202
|
+
const userData: User = { email };
|
|
203
|
+
storeUser(userData);
|
|
204
|
+
setUser(userData);
|
|
205
|
+
navigate('${firstProtectedPath}');
|
|
206
|
+
return true;
|
|
207
|
+
};` : '';
|
|
208
|
+
|
|
209
|
+
const authPathsRedirect = hasLogin ? `
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
const authPaths = ['/login', '/forgot-password', '/verify-email', '/reset-password'];
|
|
212
|
+
if (user && authPaths.includes(location.pathname)) {
|
|
213
|
+
navigate('${firstProtectedPath}', { replace: true });
|
|
214
|
+
}
|
|
215
|
+
}, [user, location.pathname, navigate]);` : '';
|
|
216
|
+
|
|
217
|
+
const authGuardContent = `${authGuardImports.join('\n')}
|
|
218
|
+
|
|
219
|
+
function ProtectedRoute({ children, user }: { children: React.ReactNode; user: User | null }) {
|
|
220
|
+
if (!user) return <Navigate to="${hasLogin ? '/login' : firstProtectedPath}" replace />;
|
|
221
|
+
return <>{children}</>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function AuthGuard() {
|
|
225
|
+
const [user, setUser] = useState<User | null>(null);
|
|
226
|
+
const navigate = useNavigate();${hasLogin ? `\n const location = useLocation();` : ''}
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
setUser(getStoredUser());
|
|
230
|
+
}, []);
|
|
231
|
+
${authPathsRedirect}${handleLoginFn}
|
|
232
|
+
|
|
233
|
+
const handleLogout = () => {
|
|
234
|
+
clearStoredUser();
|
|
235
|
+
setUser(null);
|
|
236
|
+
navigate('${hasLogin ? '/login' : firstProtectedPath}');
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="min-h-screen bg-muted overflow-x-hidden max-w-full">
|
|
241
|
+
<Routes>
|
|
242
|
+
${authRoutes.join('\n')}
|
|
243
|
+
${protectedRoutes.join('\n')}
|
|
244
|
+
<Route path="/" element={<Navigate to="${hasLogin ? (firstProtectedPath === '/login' ? '/login' : firstProtectedPath) : firstProtectedPath}" replace />} />
|
|
245
|
+
<Route path="*" element={<Navigate to="${hasLogin ? '/login' : firstProtectedPath}" replace />} />
|
|
246
|
+
</Routes>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
await fs.writeFile(
|
|
253
|
+
path.join(targetDir, 'src', 'app', 'components', 'AuthGuard.tsx'),
|
|
254
|
+
authGuardContent
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// 10. Generate theme tokens
|
|
258
|
+
const selectedTheme = colorThemes.find(t => t.id === response.theme) || colorThemes[0];
|
|
259
|
+
const tokensDir = path.join(targetDir, 'src', 'styles', 'xertica');
|
|
260
|
+
await fs.ensureDir(tokensDir);
|
|
261
|
+
await fs.copy(
|
|
262
|
+
path.join(templatesDir, 'src', 'styles', 'index.css'),
|
|
263
|
+
path.join(targetDir, 'src', 'styles', 'index.css')
|
|
264
|
+
);
|
|
265
|
+
await fs.writeFile(path.join(tokensDir, 'tokens.css'), generateTokensCss(selectedTheme));
|
|
266
|
+
|
|
267
|
+
spinner.succeed('Project initialized successfully!');
|
|
268
|
+
|
|
269
|
+
if (response.install) {
|
|
270
|
+
const installSpinner = ora('Installing dependencies...').start();
|
|
271
|
+
await execa('npm', ['install'], { cwd: targetDir });
|
|
272
|
+
installSpinner.succeed('Dependencies installed!');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(chalk.green('\n✅ Done! Your Xertica UI project is ready.'));
|
|
276
|
+
console.log(chalk.cyan(`\n cd ${directory}`));
|
|
277
|
+
if (!response.install) {
|
|
278
|
+
console.log(chalk.cyan(' npm install'));
|
|
279
|
+
}
|
|
280
|
+
console.log(chalk.cyan(' npm run dev'));
|
|
281
|
+
console.log();
|
|
282
|
+
console.log(chalk.gray(' Components are imported from the xertica-ui package.'));
|
|
283
|
+
console.log(chalk.gray(' Customize the theme in src/styles/xertica/tokens.css'));
|
|
284
|
+
|
|
285
|
+
} catch (error) {
|
|
286
|
+
spinner.fail('Failed to initialize project');
|
|
287
|
+
console.error(error);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
program
|
|
292
|
+
.command('update')
|
|
293
|
+
.alias('update-theme')
|
|
294
|
+
.description('Update theme or project files to the latest version')
|
|
295
|
+
.action(async () => {
|
|
296
|
+
const targetDir = process.cwd();
|
|
297
|
+
|
|
298
|
+
const { updateType } = await prompts({
|
|
299
|
+
type: 'select',
|
|
300
|
+
name: 'updateType',
|
|
301
|
+
message: 'What do you want to update?',
|
|
302
|
+
choices: [
|
|
303
|
+
{ title: 'Theme only', description: 'Change the color tokens (tokens.css)', value: 'theme' },
|
|
304
|
+
{ title: 'Project files', description: 'Update app shell, shared, features and pages to a specific version', value: 'project' },
|
|
305
|
+
],
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!updateType) return;
|
|
309
|
+
|
|
310
|
+
// ── Theme update ─────────────────────────────────────────────────────────
|
|
311
|
+
if (updateType === 'theme') {
|
|
312
|
+
const { theme } = await prompts({
|
|
313
|
+
type: 'select',
|
|
314
|
+
name: 'theme',
|
|
315
|
+
message: 'Select the new color theme:',
|
|
316
|
+
choices: colorThemes.map(t => ({
|
|
317
|
+
title: t.name,
|
|
318
|
+
description: t.description,
|
|
319
|
+
value: t.id
|
|
320
|
+
})),
|
|
321
|
+
initial: 0
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (!theme) return;
|
|
325
|
+
|
|
326
|
+
const spinner = ora('Updating theme...').start();
|
|
327
|
+
try {
|
|
328
|
+
const tokensPath = path.join(targetDir, 'src', 'styles', 'xertica', 'tokens.css');
|
|
329
|
+
const selectedTheme = colorThemes.find(t => t.id === theme);
|
|
330
|
+
if (selectedTheme) {
|
|
331
|
+
await fs.ensureDir(path.dirname(tokensPath));
|
|
332
|
+
await fs.writeFile(tokensPath, generateTokensCss(selectedTheme));
|
|
333
|
+
spinner.succeed(`Theme updated to "${selectedTheme.name}" successfully!`);
|
|
334
|
+
} else {
|
|
335
|
+
spinner.fail('Theme not found.');
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
spinner.fail('Failed to update theme');
|
|
339
|
+
console.error(error);
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Project files update ──────────────────────────────────────────────────
|
|
345
|
+
const { versionType } = await prompts({
|
|
346
|
+
type: 'select',
|
|
347
|
+
name: 'versionType',
|
|
348
|
+
message: 'Which version do you want to update to?',
|
|
349
|
+
choices: [
|
|
350
|
+
{ title: 'Latest', description: 'Install the latest published version', value: 'latest' },
|
|
351
|
+
{ title: 'Specific version', description: 'Enter a version number (e.g. 2.0.2)', value: 'specific' },
|
|
352
|
+
],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (!versionType) return;
|
|
356
|
+
|
|
357
|
+
let targetVersion = 'latest';
|
|
358
|
+
if (versionType === 'specific') {
|
|
359
|
+
const { version } = await prompts({
|
|
360
|
+
type: 'text',
|
|
361
|
+
name: 'version',
|
|
362
|
+
message: 'Enter the version (e.g. 2.0.2):',
|
|
363
|
+
validate: v => /^\d+\.\d+\.\d+/.test(v.trim()) ? true : 'Enter a valid semver (e.g. 2.0.2)',
|
|
364
|
+
});
|
|
365
|
+
if (!version) return;
|
|
366
|
+
targetVersion = version.trim();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { filesToUpdate } = await prompts({
|
|
370
|
+
type: 'multiselect',
|
|
371
|
+
name: 'filesToUpdate',
|
|
372
|
+
message: 'Select which parts of the project to update:',
|
|
373
|
+
choices: [
|
|
374
|
+
{ title: 'App shell (src/app/)', description: 'App.tsx, AppLayout.tsx', value: 'app', selected: true },
|
|
375
|
+
{ title: 'Shared utilities (src/shared/)', description: 'auth.ts, navigation.ts, types', value: 'shared', selected: true },
|
|
376
|
+
{ title: 'Features (src/features/)', description: 'auth, home, template UI components', value: 'features', selected: true },
|
|
377
|
+
{ title: 'Pages (src/pages/)', description: 'Thin page wrapper components', value: 'pages', selected: true },
|
|
378
|
+
{ title: 'Root config files', description: 'vite.config.ts, tsconfig.json, etc.', value: 'config', selected: false },
|
|
379
|
+
],
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (!filesToUpdate || filesToUpdate.length === 0) return;
|
|
383
|
+
|
|
384
|
+
const { confirmed } = await prompts({
|
|
385
|
+
type: 'confirm',
|
|
386
|
+
name: 'confirmed',
|
|
387
|
+
message: chalk.yellow(`⚠️ This will overwrite the selected files. Local changes will be lost. Continue?`),
|
|
388
|
+
initial: false,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (!confirmed) {
|
|
392
|
+
console.log(chalk.gray('Update cancelled.'));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const spinner = ora(`Installing xertica-ui@${targetVersion}...`).start();
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
// Install the target version in the consumer project
|
|
400
|
+
await execa('npm', ['install', `xertica-ui@${targetVersion}`], { cwd: targetDir });
|
|
401
|
+
spinner.text = 'Copying updated files...';
|
|
402
|
+
|
|
403
|
+
// Templates now come from the freshly installed version
|
|
404
|
+
const updatedTemplatesDir = path.join(targetDir, 'node_modules', 'xertica-ui', 'templates');
|
|
405
|
+
|
|
406
|
+
if (filesToUpdate.includes('app')) {
|
|
407
|
+
await fs.copy(
|
|
408
|
+
path.join(updatedTemplatesDir, 'src', 'app', 'App.tsx'),
|
|
409
|
+
path.join(targetDir, 'src', 'app', 'App.tsx'),
|
|
410
|
+
{ overwrite: true }
|
|
411
|
+
);
|
|
412
|
+
await fs.copy(
|
|
413
|
+
path.join(updatedTemplatesDir, 'src', 'app', 'components', 'AppLayout.tsx'),
|
|
414
|
+
path.join(targetDir, 'src', 'app', 'components', 'AppLayout.tsx'),
|
|
415
|
+
{ overwrite: true }
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (filesToUpdate.includes('shared')) {
|
|
420
|
+
await fs.copy(
|
|
421
|
+
path.join(updatedTemplatesDir, 'src', 'shared'),
|
|
422
|
+
path.join(targetDir, 'src', 'shared'),
|
|
423
|
+
{ overwrite: true }
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (filesToUpdate.includes('features')) {
|
|
428
|
+
// Only update feature directories that already exist in the project
|
|
429
|
+
for (const feature of ['auth', 'home', 'template']) {
|
|
430
|
+
const destFeature = path.join(targetDir, 'src', 'features', feature);
|
|
431
|
+
const srcFeature = path.join(updatedTemplatesDir, 'src', 'features', feature);
|
|
432
|
+
if (await fs.pathExists(destFeature) && await fs.pathExists(srcFeature)) {
|
|
433
|
+
await fs.copy(srcFeature, destFeature, { overwrite: true });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (filesToUpdate.includes('pages')) {
|
|
439
|
+
const pagesDir = path.join(targetDir, 'src', 'pages');
|
|
440
|
+
const srcPagesDir = path.join(updatedTemplatesDir, 'src', 'pages');
|
|
441
|
+
if (await fs.pathExists(pagesDir) && await fs.pathExists(srcPagesDir)) {
|
|
442
|
+
// Only overwrite pages that already exist in the project
|
|
443
|
+
const existingPages = await fs.readdir(pagesDir);
|
|
444
|
+
for (const pageFile of existingPages) {
|
|
445
|
+
const src = path.join(srcPagesDir, pageFile);
|
|
446
|
+
if (await fs.pathExists(src)) {
|
|
447
|
+
await fs.copy(src, path.join(pagesDir, pageFile), { overwrite: true });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (filesToUpdate.includes('config')) {
|
|
454
|
+
const configFiles = ['vite.config.ts', 'tsconfig.json', 'tsconfig.node.json', 'postcss.config.js'];
|
|
455
|
+
for (const file of configFiles) {
|
|
456
|
+
const src = path.join(updatedTemplatesDir, '..', file);
|
|
457
|
+
if (await fs.pathExists(src)) {
|
|
458
|
+
await fs.copy(src, path.join(targetDir, file), { overwrite: true });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
spinner.succeed(`Project updated to xertica-ui@${targetVersion} successfully!`);
|
|
464
|
+
console.log(chalk.gray('\n Run npm run dev to start the development server.'));
|
|
465
|
+
|
|
466
|
+
} catch (error) {
|
|
467
|
+
spinner.fail('Failed to update project');
|
|
468
|
+
console.error(error);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
program.parse();
|
package/dist/cli.js
CHANGED
|
@@ -528,7 +528,7 @@ var generateTokensCss = (theme) => {
|
|
|
528
528
|
var __filename = fileURLToPath(import.meta.url);
|
|
529
529
|
var __dirname = path.dirname(__filename);
|
|
530
530
|
var program = new Command();
|
|
531
|
-
program.name("xertica-ui").description("CLI to initialize Xertica UI projects").version("
|
|
531
|
+
program.name("xertica-ui").description("CLI to initialize Xertica UI projects").version("2.0.2");
|
|
532
532
|
program.command("init").description("Initialize a new Xertica UI project").argument("[directory]", "Directory to initialize in", ".").action(async (directory) => {
|
|
533
533
|
const targetDir = path.resolve(process.cwd(), directory);
|
|
534
534
|
const templatesDir = path.resolve(__dirname, "../templates");
|
|
@@ -583,9 +583,8 @@ program.command("init").description("Initialize a new Xertica UI project").argum
|
|
|
583
583
|
];
|
|
584
584
|
for (const file of rootFilesToCopy) {
|
|
585
585
|
const srcPath = path.join(templatesDir, file);
|
|
586
|
-
const destPath = path.join(targetDir, file);
|
|
587
586
|
if (await fs.pathExists(srcPath)) {
|
|
588
|
-
await fs.copy(srcPath,
|
|
587
|
+
await fs.copy(srcPath, path.join(targetDir, file));
|
|
589
588
|
}
|
|
590
589
|
}
|
|
591
590
|
const pkgTemplatePath = path.join(templatesDir, "package.json");
|
|
@@ -595,86 +594,136 @@ program.command("init").description("Initialize a new Xertica UI project").argum
|
|
|
595
594
|
pkgContent.name = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
596
595
|
await fs.writeJson(path.join(targetDir, "package.json"), pkgContent, { spaces: 2 });
|
|
597
596
|
}
|
|
597
|
+
await fs.copy(
|
|
598
|
+
path.join(templatesDir, "src", "main.tsx"),
|
|
599
|
+
path.join(targetDir, "src", "main.tsx")
|
|
600
|
+
);
|
|
598
601
|
await fs.ensureDir(path.join(targetDir, "src", "app"));
|
|
599
602
|
await fs.copy(
|
|
600
603
|
path.join(templatesDir, "src", "app", "App.tsx"),
|
|
601
604
|
path.join(targetDir, "src", "app", "App.tsx")
|
|
602
605
|
);
|
|
606
|
+
await fs.ensureDir(path.join(targetDir, "src", "app", "components"));
|
|
603
607
|
await fs.copy(
|
|
604
|
-
path.join(templatesDir, "src", "
|
|
605
|
-
path.join(targetDir, "src", "
|
|
608
|
+
path.join(templatesDir, "src", "app", "components", "AppLayout.tsx"),
|
|
609
|
+
path.join(targetDir, "src", "app", "components", "AppLayout.tsx")
|
|
606
610
|
);
|
|
607
611
|
await fs.copy(
|
|
608
|
-
path.join(templatesDir, "src", "
|
|
609
|
-
path.join(targetDir, "src", "
|
|
612
|
+
path.join(templatesDir, "src", "shared"),
|
|
613
|
+
path.join(targetDir, "src", "shared")
|
|
610
614
|
);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
reset: { file: "ResetPasswordPage.tsx", dir: "ResetPassword", shouldCopy: hasLogin },
|
|
617
|
-
home: { file: "HomePage.tsx", dir: "Home", shouldCopy: hasHome },
|
|
618
|
-
template: { file: "TemplatePage.tsx", dir: "Template", shouldCopy: hasTemplate }
|
|
619
|
-
};
|
|
620
|
-
const srcPagesDir = path.join(templatesDir, "src", "app", "pages");
|
|
621
|
-
for (const key in optionalPages) {
|
|
622
|
-
const { file, dir, shouldCopy } = optionalPages[key];
|
|
623
|
-
if (shouldCopy) {
|
|
624
|
-
const srcFilePath = path.join(srcPagesDir, file);
|
|
625
|
-
if (await fs.pathExists(srcFilePath)) {
|
|
626
|
-
await fs.copy(srcFilePath, path.join(targetDir, "src", "app", "pages", file));
|
|
627
|
-
}
|
|
628
|
-
if (dir) {
|
|
629
|
-
const srcDirPath = path.join(srcPagesDir, dir);
|
|
630
|
-
if (await fs.pathExists(srcDirPath)) {
|
|
631
|
-
await fs.copy(srcDirPath, path.join(targetDir, "src", "app", "pages", dir));
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
615
|
+
if (hasLogin) {
|
|
616
|
+
await fs.copy(
|
|
617
|
+
path.join(templatesDir, "src", "features", "auth"),
|
|
618
|
+
path.join(targetDir, "src", "features", "auth")
|
|
619
|
+
);
|
|
635
620
|
}
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
621
|
+
if (hasHome) {
|
|
622
|
+
await fs.copy(
|
|
623
|
+
path.join(templatesDir, "src", "features", "home"),
|
|
624
|
+
path.join(targetDir, "src", "features", "home")
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (hasTemplate) {
|
|
628
|
+
await fs.copy(
|
|
629
|
+
path.join(templatesDir, "src", "features", "template"),
|
|
630
|
+
path.join(targetDir, "src", "features", "template")
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
await fs.ensureDir(path.join(targetDir, "src", "pages"));
|
|
634
|
+
const pagesToCopy = [];
|
|
635
|
+
if (hasLogin) pagesToCopy.push("LoginPage.tsx", "ForgotPasswordPage.tsx", "VerifyEmailPage.tsx", "ResetPasswordPage.tsx");
|
|
636
|
+
if (hasHome) pagesToCopy.push("HomePage.tsx");
|
|
637
|
+
if (hasTemplate) pagesToCopy.push("TemplatePage.tsx");
|
|
638
|
+
for (const pageFile of pagesToCopy) {
|
|
639
|
+
const src = path.join(templatesDir, "src", "pages", pageFile);
|
|
640
|
+
if (await fs.pathExists(src)) {
|
|
641
|
+
await fs.copy(src, path.join(targetDir, "src", "pages", pageFile));
|
|
656
642
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
643
|
+
}
|
|
644
|
+
const firstProtectedPath = hasHome ? "/home" : hasTemplate ? "/template" : "/login";
|
|
645
|
+
const authGuardImports = [
|
|
646
|
+
`import React, { useState, useEffect } from 'react';`,
|
|
647
|
+
`import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';`
|
|
648
|
+
];
|
|
649
|
+
if (hasLogin) {
|
|
650
|
+
authGuardImports.push(`import { getStoredUser, storeUser, clearStoredUser } from '../../shared/lib/auth';`);
|
|
651
|
+
authGuardImports.push(`import type { User } from '../../shared/types/auth';`);
|
|
652
|
+
authGuardImports.push(`import { LoginPage } from '../../pages/LoginPage';`);
|
|
653
|
+
authGuardImports.push(`import { ForgotPasswordPage } from '../../pages/ForgotPasswordPage';`);
|
|
654
|
+
authGuardImports.push(`import { VerifyEmailPage } from '../../pages/VerifyEmailPage';`);
|
|
655
|
+
authGuardImports.push(`import { ResetPasswordPage } from '../../pages/ResetPasswordPage';`);
|
|
656
|
+
} else {
|
|
657
|
+
authGuardImports.push(`import { getStoredUser, storeUser, clearStoredUser } from '../../shared/lib/auth';`);
|
|
658
|
+
authGuardImports.push(`import type { User } from '../../shared/types/auth';`);
|
|
659
|
+
}
|
|
660
|
+
if (hasHome) authGuardImports.push(`import { HomePage } from '../../pages/HomePage';`);
|
|
661
|
+
if (hasTemplate) authGuardImports.push(`import { TemplatePage } from '../../pages/TemplatePage';`);
|
|
662
|
+
const protectedRoutes = [];
|
|
663
|
+
if (hasHome) protectedRoutes.push(` <Route path="/home" element={<ProtectedRoute user={user}><HomePage user={user} onLogout={handleLogout} /></ProtectedRoute>} />`);
|
|
664
|
+
if (hasTemplate) protectedRoutes.push(` <Route path="/template" element={<ProtectedRoute user={user}><TemplatePage user={user} onLogout={handleLogout} /></ProtectedRoute>} />`);
|
|
665
|
+
const authRoutes = [];
|
|
666
|
+
if (hasLogin) {
|
|
667
|
+
authRoutes.push(` <Route path="/login" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <LoginPage onLogin={handleLogin} />} />`);
|
|
668
|
+
authRoutes.push(` <Route path="/forgot-password" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <ForgotPasswordPage />} />`);
|
|
669
|
+
authRoutes.push(` <Route path="/verify-email" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <VerifyEmailPage />} />`);
|
|
670
|
+
authRoutes.push(` <Route path="/reset-password" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <ResetPasswordPage />} />`);
|
|
671
|
+
}
|
|
672
|
+
const handleLoginFn = hasLogin ? `
|
|
673
|
+
const handleLogin = (email: string, password: string): boolean => {
|
|
674
|
+
if (!email.trim() || !password.trim()) return false;
|
|
675
|
+
const userData: User = { email };
|
|
676
|
+
storeUser(userData);
|
|
677
|
+
setUser(userData);
|
|
678
|
+
navigate('${firstProtectedPath}');
|
|
679
|
+
return true;
|
|
680
|
+
};` : "";
|
|
681
|
+
const authPathsRedirect = hasLogin ? `
|
|
682
|
+
useEffect(() => {
|
|
683
|
+
const authPaths = ['/login', '/forgot-password', '/verify-email', '/reset-password'];
|
|
684
|
+
if (user && authPaths.includes(location.pathname)) {
|
|
685
|
+
navigate('${firstProtectedPath}', { replace: true });
|
|
686
|
+
}
|
|
687
|
+
}, [user, location.pathname, navigate]);` : "";
|
|
688
|
+
const authGuardContent = `${authGuardImports.join("\n")}
|
|
689
|
+
|
|
690
|
+
function ProtectedRoute({ children, user }: { children: React.ReactNode; user: User | null }) {
|
|
691
|
+
if (!user) return <Navigate to="${hasLogin ? "/login" : firstProtectedPath}" replace />;
|
|
692
|
+
return <>{children}</>;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function AuthGuard() {
|
|
696
|
+
const [user, setUser] = useState<User | null>(null);
|
|
697
|
+
const navigate = useNavigate();${hasLogin ? `
|
|
698
|
+
const location = useLocation();` : ""}
|
|
699
|
+
|
|
700
|
+
useEffect(() => {
|
|
701
|
+
setUser(getStoredUser());
|
|
702
|
+
}, []);
|
|
703
|
+
${authPathsRedirect}${handleLoginFn}
|
|
704
|
+
|
|
705
|
+
const handleLogout = () => {
|
|
706
|
+
clearStoredUser();
|
|
707
|
+
setUser(null);
|
|
708
|
+
navigate('${hasLogin ? "/login" : firstProtectedPath}');
|
|
709
|
+
};
|
|
661
710
|
|
|
662
|
-
export default function App() {
|
|
663
711
|
return (
|
|
664
|
-
<
|
|
665
|
-
<
|
|
666
|
-
|
|
667
|
-
${
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
</XerticaProvider>
|
|
712
|
+
<div className="min-h-screen bg-muted overflow-x-hidden max-w-full">
|
|
713
|
+
<Routes>
|
|
714
|
+
${authRoutes.join("\n")}
|
|
715
|
+
${protectedRoutes.join("\n")}
|
|
716
|
+
<Route path="/" element={<Navigate to="${hasLogin ? firstProtectedPath === "/login" ? "/login" : firstProtectedPath : firstProtectedPath}" replace />} />
|
|
717
|
+
<Route path="*" element={<Navigate to="${hasLogin ? "/login" : firstProtectedPath}" replace />} />
|
|
718
|
+
</Routes>
|
|
719
|
+
</div>
|
|
673
720
|
);
|
|
674
721
|
}
|
|
675
722
|
`;
|
|
676
|
-
|
|
677
|
-
|
|
723
|
+
await fs.writeFile(
|
|
724
|
+
path.join(targetDir, "src", "app", "components", "AuthGuard.tsx"),
|
|
725
|
+
authGuardContent
|
|
726
|
+
);
|
|
678
727
|
const selectedTheme = colorThemes.find((t) => t.id === response.theme) || colorThemes[0];
|
|
679
728
|
const tokensDir = path.join(targetDir, "src", "styles", "xertica");
|
|
680
729
|
await fs.ensureDir(tokensDir);
|
|
@@ -682,8 +731,7 @@ ${routes.join("\n")}
|
|
|
682
731
|
path.join(templatesDir, "src", "styles", "index.css"),
|
|
683
732
|
path.join(targetDir, "src", "styles", "index.css")
|
|
684
733
|
);
|
|
685
|
-
|
|
686
|
-
await fs.writeFile(path.join(tokensDir, "tokens.css"), newTokensCss);
|
|
734
|
+
await fs.writeFile(path.join(tokensDir, "tokens.css"), generateTokensCss(selectedTheme));
|
|
687
735
|
spinner.succeed("Project initialized successfully!");
|
|
688
736
|
if (response.install) {
|
|
689
737
|
const installSpinner = ora("Installing dependencies...").start();
|
|
@@ -705,34 +753,151 @@ ${routes.join("\n")}
|
|
|
705
753
|
console.error(error);
|
|
706
754
|
}
|
|
707
755
|
});
|
|
708
|
-
program.command("update").alias("update-theme").description("Update theme
|
|
756
|
+
program.command("update").alias("update-theme").description("Update theme or project files to the latest version").action(async () => {
|
|
709
757
|
const targetDir = process.cwd();
|
|
710
|
-
const
|
|
758
|
+
const { updateType } = await prompts({
|
|
759
|
+
type: "select",
|
|
760
|
+
name: "updateType",
|
|
761
|
+
message: "What do you want to update?",
|
|
762
|
+
choices: [
|
|
763
|
+
{ title: "Theme only", description: "Change the color tokens (tokens.css)", value: "theme" },
|
|
764
|
+
{ title: "Project files", description: "Update app shell, shared, features and pages to a specific version", value: "project" }
|
|
765
|
+
]
|
|
766
|
+
});
|
|
767
|
+
if (!updateType) return;
|
|
768
|
+
if (updateType === "theme") {
|
|
769
|
+
const { theme } = await prompts({
|
|
770
|
+
type: "select",
|
|
771
|
+
name: "theme",
|
|
772
|
+
message: "Select the new color theme:",
|
|
773
|
+
choices: colorThemes.map((t) => ({
|
|
774
|
+
title: t.name,
|
|
775
|
+
description: t.description,
|
|
776
|
+
value: t.id
|
|
777
|
+
})),
|
|
778
|
+
initial: 0
|
|
779
|
+
});
|
|
780
|
+
if (!theme) return;
|
|
781
|
+
const spinner2 = ora("Updating theme...").start();
|
|
782
|
+
try {
|
|
783
|
+
const tokensPath = path.join(targetDir, "src", "styles", "xertica", "tokens.css");
|
|
784
|
+
const selectedTheme = colorThemes.find((t) => t.id === theme);
|
|
785
|
+
if (selectedTheme) {
|
|
786
|
+
await fs.ensureDir(path.dirname(tokensPath));
|
|
787
|
+
await fs.writeFile(tokensPath, generateTokensCss(selectedTheme));
|
|
788
|
+
spinner2.succeed(`Theme updated to "${selectedTheme.name}" successfully!`);
|
|
789
|
+
} else {
|
|
790
|
+
spinner2.fail("Theme not found.");
|
|
791
|
+
}
|
|
792
|
+
} catch (error) {
|
|
793
|
+
spinner2.fail("Failed to update theme");
|
|
794
|
+
console.error(error);
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const { versionType } = await prompts({
|
|
711
799
|
type: "select",
|
|
712
|
-
name: "
|
|
713
|
-
message: "
|
|
714
|
-
choices:
|
|
715
|
-
title:
|
|
716
|
-
description:
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
800
|
+
name: "versionType",
|
|
801
|
+
message: "Which version do you want to update to?",
|
|
802
|
+
choices: [
|
|
803
|
+
{ title: "Latest", description: "Install the latest published version", value: "latest" },
|
|
804
|
+
{ title: "Specific version", description: "Enter a version number (e.g. 2.0.2)", value: "specific" }
|
|
805
|
+
]
|
|
806
|
+
});
|
|
807
|
+
if (!versionType) return;
|
|
808
|
+
let targetVersion = "latest";
|
|
809
|
+
if (versionType === "specific") {
|
|
810
|
+
const { version } = await prompts({
|
|
811
|
+
type: "text",
|
|
812
|
+
name: "version",
|
|
813
|
+
message: "Enter the version (e.g. 2.0.2):",
|
|
814
|
+
validate: (v) => /^\d+\.\d+\.\d+/.test(v.trim()) ? true : "Enter a valid semver (e.g. 2.0.2)"
|
|
815
|
+
});
|
|
816
|
+
if (!version) return;
|
|
817
|
+
targetVersion = version.trim();
|
|
818
|
+
}
|
|
819
|
+
const { filesToUpdate } = await prompts({
|
|
820
|
+
type: "multiselect",
|
|
821
|
+
name: "filesToUpdate",
|
|
822
|
+
message: "Select which parts of the project to update:",
|
|
823
|
+
choices: [
|
|
824
|
+
{ title: "App shell (src/app/)", description: "App.tsx, AppLayout.tsx", value: "app", selected: true },
|
|
825
|
+
{ title: "Shared utilities (src/shared/)", description: "auth.ts, navigation.ts, types", value: "shared", selected: true },
|
|
826
|
+
{ title: "Features (src/features/)", description: "auth, home, template UI components", value: "features", selected: true },
|
|
827
|
+
{ title: "Pages (src/pages/)", description: "Thin page wrapper components", value: "pages", selected: true },
|
|
828
|
+
{ title: "Root config files", description: "vite.config.ts, tsconfig.json, etc.", value: "config", selected: false }
|
|
829
|
+
]
|
|
720
830
|
});
|
|
721
|
-
if (!
|
|
722
|
-
const
|
|
831
|
+
if (!filesToUpdate || filesToUpdate.length === 0) return;
|
|
832
|
+
const { confirmed } = await prompts({
|
|
833
|
+
type: "confirm",
|
|
834
|
+
name: "confirmed",
|
|
835
|
+
message: chalk.yellow(`\u26A0\uFE0F This will overwrite the selected files. Local changes will be lost. Continue?`),
|
|
836
|
+
initial: false
|
|
837
|
+
});
|
|
838
|
+
if (!confirmed) {
|
|
839
|
+
console.log(chalk.gray("Update cancelled."));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const spinner = ora(`Installing xertica-ui@${targetVersion}...`).start();
|
|
723
843
|
try {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
844
|
+
await execa("npm", ["install", `xertica-ui@${targetVersion}`], { cwd: targetDir });
|
|
845
|
+
spinner.text = "Copying updated files...";
|
|
846
|
+
const updatedTemplatesDir = path.join(targetDir, "node_modules", "xertica-ui", "templates");
|
|
847
|
+
if (filesToUpdate.includes("app")) {
|
|
848
|
+
await fs.copy(
|
|
849
|
+
path.join(updatedTemplatesDir, "src", "app", "App.tsx"),
|
|
850
|
+
path.join(targetDir, "src", "app", "App.tsx"),
|
|
851
|
+
{ overwrite: true }
|
|
852
|
+
);
|
|
853
|
+
await fs.copy(
|
|
854
|
+
path.join(updatedTemplatesDir, "src", "app", "components", "AppLayout.tsx"),
|
|
855
|
+
path.join(targetDir, "src", "app", "components", "AppLayout.tsx"),
|
|
856
|
+
{ overwrite: true }
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
if (filesToUpdate.includes("shared")) {
|
|
860
|
+
await fs.copy(
|
|
861
|
+
path.join(updatedTemplatesDir, "src", "shared"),
|
|
862
|
+
path.join(targetDir, "src", "shared"),
|
|
863
|
+
{ overwrite: true }
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
if (filesToUpdate.includes("features")) {
|
|
867
|
+
for (const feature of ["auth", "home", "template"]) {
|
|
868
|
+
const destFeature = path.join(targetDir, "src", "features", feature);
|
|
869
|
+
const srcFeature = path.join(updatedTemplatesDir, "src", "features", feature);
|
|
870
|
+
if (await fs.pathExists(destFeature) && await fs.pathExists(srcFeature)) {
|
|
871
|
+
await fs.copy(srcFeature, destFeature, { overwrite: true });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (filesToUpdate.includes("pages")) {
|
|
876
|
+
const pagesDir = path.join(targetDir, "src", "pages");
|
|
877
|
+
const srcPagesDir = path.join(updatedTemplatesDir, "src", "pages");
|
|
878
|
+
if (await fs.pathExists(pagesDir) && await fs.pathExists(srcPagesDir)) {
|
|
879
|
+
const existingPages = await fs.readdir(pagesDir);
|
|
880
|
+
for (const pageFile of existingPages) {
|
|
881
|
+
const src = path.join(srcPagesDir, pageFile);
|
|
882
|
+
if (await fs.pathExists(src)) {
|
|
883
|
+
await fs.copy(src, path.join(pagesDir, pageFile), { overwrite: true });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (filesToUpdate.includes("config")) {
|
|
889
|
+
const configFiles = ["vite.config.ts", "tsconfig.json", "tsconfig.node.json", "postcss.config.js"];
|
|
890
|
+
for (const file of configFiles) {
|
|
891
|
+
const src = path.join(updatedTemplatesDir, "..", file);
|
|
892
|
+
if (await fs.pathExists(src)) {
|
|
893
|
+
await fs.copy(src, path.join(targetDir, file), { overwrite: true });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
733
896
|
}
|
|
897
|
+
spinner.succeed(`Project updated to xertica-ui@${targetVersion} successfully!`);
|
|
898
|
+
console.log(chalk.gray("\n Run npm run dev to start the development server."));
|
|
734
899
|
} catch (error) {
|
|
735
|
-
spinner.fail("Failed to update
|
|
900
|
+
spinner.fail("Failed to update project");
|
|
736
901
|
console.error(error);
|
|
737
902
|
}
|
|
738
903
|
});
|
package/package.json
CHANGED