xertica-ui 2.3.0 → 2.4.1
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 +22 -2
- package/README.md +33 -22
- package/bin/cli.ts +136 -47
- package/bin/language-config.ts +5 -8
- package/components/assistant/modern-chat-input/ModernChatInput.tsx +17 -7
- package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +1 -3
- package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +13 -3
- package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +10 -6
- package/components/assistant/xertica-assistant/xertica-assistant.tsx +1 -3
- package/components/blocks/card-patterns/FeatureCardSkeleton.tsx +1 -6
- package/components/blocks/card-patterns/ProfileCard.tsx +1 -3
- package/components/blocks/card-patterns/ProjectCardSkeleton.tsx +1 -6
- package/components/brand/language-selector/language-selector.stories.tsx +1 -4
- package/components/brand/theme-toggle/ThemeToggle.tsx +5 -1
- package/components/brand/xertica-provider/XerticaProvider.tsx +1 -4
- package/components/index.ts +1 -5
- package/components/layout/sidebar/sidebar.tsx +9 -3
- package/components/media/audio-player/AudioPlayer.tsx +4 -2
- package/components/pages/forgot-password-page/ForgotPasswordPage.tsx +188 -188
- package/components/pages/home-content/HomeContent.tsx +55 -55
- package/components/pages/home-page/HomePage.tsx +5 -1
- package/components/pages/login-page/LoginPage.tsx +4 -2
- package/components/pages/reset-password-page/ResetPasswordPage.tsx +7 -3
- package/components/pages/template-content/TemplateContent.tsx +268 -149
- package/components/pages/verify-email-page/VerifyEmailPage.tsx +9 -9
- package/components/shared/error-boundary.stories.tsx +114 -132
- package/components/shared/error-boundary.tsx +150 -154
- package/components/shared/error-fallbacks.tsx +222 -226
- package/components/ui/stats-card/stats-card-skeleton.tsx +1 -3
- package/components/ui/stats-card/stats-card.stories.tsx +18 -0
- package/components/ui/stats-card/stats-card.tsx +18 -2
- package/components.json +512 -892
- package/contexts/AuthContext.tsx +121 -118
- package/contexts/LanguageContext.tsx +1 -2
- package/dist/AssistantChart-BKVtGUKF.js +3383 -0
- package/dist/AssistantChart-WeycT5Pd.cjs +3551 -0
- package/dist/VerifyEmailPage-Bp1XXl3H.cjs +3305 -0
- package/dist/VerifyEmailPage-DGhuIqkb.js +3296 -0
- package/dist/XerticaProvider-BErr83Bg.js +42 -0
- package/dist/XerticaProvider-CwOkHxiT.cjs +44 -0
- package/dist/XerticaXLogo-BX3ueACh.js +255 -0
- package/dist/XerticaXLogo-qBPhwK3g.cjs +260 -0
- package/dist/assistant.cjs.js +1 -1
- package/dist/assistant.es.js +1 -1
- package/dist/brand.cjs.js +2 -2
- package/dist/brand.es.js +2 -2
- package/dist/cli.js +90 -37
- package/dist/components/brand/theme-toggle/ThemeToggle.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/ui/stats-card/stats-card.d.ts +10 -0
- package/dist/index.cjs.js +6 -6
- package/dist/index.es.js +6 -6
- package/dist/layout.cjs.js +1 -1
- package/dist/layout.es.js +1 -1
- package/dist/pages.cjs.js +1 -1
- package/dist/pages.es.js +1 -1
- package/dist/sidebar-B4ZWaMrE.js +792 -0
- package/dist/sidebar-BS1p2V7t.cjs +795 -0
- package/dist/ui.cjs.js +1 -1
- package/dist/ui.es.js +1 -1
- package/dist/xertica-assistant-B1NaSFFj.js +2173 -0
- package/dist/xertica-assistant-CIaUlbIt.cjs +2180 -0
- package/dist/xertica-ui.css +1 -1
- package/docs/architecture-improvements.md +5 -5
- package/docs/architecture.md +16 -10
- package/docs/components/card-patterns.md +19 -17
- package/docs/components/error-boundary.md +201 -191
- package/docs/components/hooks.md +15 -13
- package/docs/components/language-selector.md +20 -16
- package/docs/components/pages.md +323 -309
- package/docs/components/stats-card.md +20 -2
- package/docs/doc-audit.md +12 -11
- package/docs/getting-started.md +41 -28
- package/docs/guidelines.md +14 -12
- package/docs/i18n.md +61 -57
- package/docs/installation.md +268 -267
- package/docs/llms.md +17 -17
- package/docs/state-management.md +17 -17
- package/guidelines/Guidelines.md +17 -14
- package/llms-compact.txt +1 -1
- package/llms-full.txt +11553 -7133
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/styles/xertica/base.css +90 -84
- package/templates/CLAUDE.md +16 -1
- package/templates/guidelines/Guidelines.md +42 -18
- package/templates/package.json +3 -3
- package/templates/src/app/components/AuthGuard.tsx +131 -82
- package/templates/src/features/auth/ui/AuthPageShell.tsx +32 -32
- package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +1 -3
- package/templates/src/features/auth/ui/ResetPasswordContent.tsx +6 -2
- package/templates/src/features/auth/ui/VerifyEmailContent.tsx +2 -6
- package/templates/src/features/home/data/mock.ts +41 -35
- package/templates/src/features/home/ui/HomeContent.tsx +62 -64
- package/templates/src/features/template/ui/CrudTemplate.tsx +1 -4
- package/templates/src/features/template/ui/LoginTemplate.tsx +1 -1
- package/templates/src/features/template/ui/TemplateContent.tsx +28 -20
- package/templates/src/locales/en/pages/templates.json +17 -17
- package/templates/src/locales/es/pages/templates.json +17 -17
- package/templates/src/locales/pt-BR/pages/templates.json +17 -17
- package/templates/src/pages/AssistantPage.tsx +26 -20
- package/templates/src/pages/HomePage.tsx +5 -1
- package/templates/src/shared/error-boundary.tsx +150 -154
- package/templates/src/shared/error-fallbacks.tsx +222 -226
- package/templates/vite.config.ts +12 -9
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,26 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [2.4.1] — 2026-06-17
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **StatsCard — Cores de ícone customizáveis** — Adicionadas as propriedades `iconColor` e `iconBg` ao `StatsCard`, permitindo customizar a cor do ícone e o fundo do ícone (que anteriormente eram fixos em `text-muted-foreground` e `bg-muted`). Documentação e histórias do Storybook foram atualizadas correspondendo a essas propriedades.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [2.4.0] — 2026-06-16
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **CLI — Ativação/Desativação de Dark Mode** — nova funcionalidade no CLI para ativar ou desativar o dark mode ao criar (`init`) ou atualizar (`update`) o projeto. A escolha do usuário é armazenada em `.xertica.json` na raiz e gera `disableDarkMode={true}` dentro do `<XerticaProvider>` em `App.tsx`. O botão `ThemeToggle` e a aba de switch de temas nas configurações ocultam-se automaticamente caso o dark mode esteja desativado.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Layout — Correção do scroll vertical** — ajustado o comportamento do scroll vertical para travar o viewport com `h-screen overflow-hidden` em `App.tsx` e `AuthGuard.tsx`, bem como `height: 100%; overflow: hidden` no `html, body` em `index.css` e `base.css`. Páginas de autenticação (como login, recuperação de senha, etc.) agora possuem rolagem interna (`h-full overflow-y-auto`), impedindo que o scroll global suba/desloque o cabeçalho (`Header`) e o assistente virtual.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
14
34
|
## [2.3.0] — 2026-06-15
|
|
15
35
|
|
|
16
36
|
### Added
|
|
@@ -81,11 +101,11 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
81
101
|
- **CLI Initialization** — Correções essenciais no comando `init`:
|
|
82
102
|
- `i18n.ts` e o diretório `locales/` agora são copiados corretamente, restaurando as traduções na aplicação gerada.
|
|
83
103
|
- `AuthContext.tsx` e dependências agora são incluídas, habilitando o hook `useAuth`.
|
|
84
|
-
- A geração do `AuthGuard.tsx` foi reconstruída de forma dinâmica para consumir corretamente o `useAuth()` e garantir integridade das rotas protegidas e abertas, suportando
|
|
104
|
+
- A geração do `AuthGuard.tsx` foi reconstruída de forma dinâmica para consumir corretamente o `useAuth()` e garantir integridade das rotas protegidas e abertas, suportando _lazy loading_ dinâmico das rotas de acordo com a seleção do usuário.
|
|
85
105
|
- O diretório `features/assistant/` agora é sempre copiado visto que o `AppLayout` depende nativamente da `AssistantPage`.
|
|
86
106
|
- **Template Lints** — Resolução de diversos `unused-vars` (variáveis e imports órfãos) espalhados pelo `HomeContent.tsx`, `TemplateContent.tsx`, `AssistantPage.tsx` e `AuthContext.tsx`, garantindo que o `npm run check` (`tsc` + `eslint`) execute com 100% de sucesso imediatamente após a geração do projeto.
|
|
87
107
|
- **`LanguageSelector` e `LanguageContext`** — Correção de bugs na seleção de idiomas e fallback:
|
|
88
|
-
- `LanguageContext.tsx` agora valida de forma robusta e intercepta valores legados como `'PT'` no `localStorage`, realizando um
|
|
108
|
+
- `LanguageContext.tsx` agora valida de forma robusta e intercepta valores legados como `'PT'` no `localStorage`, realizando um _fallback_ seguro para `'pt-BR'`.
|
|
89
109
|
- O componente `LanguageSelector.tsx` foi ajustado na sua composição com Radix UI: a dependência restritiva do `<SelectValue>` foi removida no trigger, evitando a sobrescrita do conteúdo. Agora o componente exibe a variante `minimal` (como `PT`, `EN`, `ES`) perfeitamente, sincronizada com o estado global de tradução.
|
|
90
110
|
|
|
91
111
|
---
|
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
|
---
|
|
@@ -32,21 +32,22 @@ npm run dev
|
|
|
32
32
|
|
|
33
33
|
During `init`, the CLI walks you through:
|
|
34
34
|
|
|
35
|
-
| Prompt
|
|
36
|
-
|
|
37
|
-
| Pages to include
|
|
38
|
-
| **Languages to support**
|
|
39
|
-
| Default color theme
|
|
40
|
-
| **
|
|
41
|
-
|
|
|
35
|
+
| Prompt | Choices |
|
|
36
|
+
| ---------------------------------- | -------------------------------------------------------- |
|
|
37
|
+
| Pages to include | Login, Home, Template (multi-select) |
|
|
38
|
+
| **Languages to support** | Português (BR), English, Español — select **1, 2, or 3** |
|
|
39
|
+
| Default color theme | Xertica, Slate, Blue, Violet, Rose, Emerald, … |
|
|
40
|
+
| **Enable dark mode support?** | yes (default) / no |
|
|
41
|
+
| **Include AI Assistant** | yes (default) / no |
|
|
42
|
+
| Install dependencies automatically | yes / no |
|
|
42
43
|
|
|
43
44
|
The CLI generates a tailored project with:
|
|
44
45
|
|
|
45
46
|
- Only the locale JSON files for the languages you picked (no orphan files)
|
|
46
47
|
- `src/i18n.ts` with imports + `resources` for exactly those languages
|
|
47
|
-
- `src/app/App.tsx` with the `availableLanguages` prop on `<XerticaProvider>` (omitted when all 3 defaults are selected)
|
|
48
|
+
- `src/app/App.tsx` with the `availableLanguages` prop on `<XerticaProvider>` (omitted when all 3 defaults are selected) and `disableDarkMode={true}` if dark mode support was disabled
|
|
48
49
|
- A persisted selection in `src/locales/.languages.json` so the `update` command can preserve it
|
|
49
|
-
- Feature flags in `.xertica.json` (e.g. `hasAssistant`) so the `update` command can read the current state
|
|
50
|
+
- Feature flags in `.xertica.json` (e.g. `hasAssistant`, `disableDarkMode`) so the `update` command can read the current state
|
|
50
51
|
|
|
51
52
|
### Monolingual mode (transparent)
|
|
52
53
|
|
|
@@ -59,6 +60,7 @@ npx xertica-ui update # then choose "Languages"
|
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
The flow shows your current selection, lets you toggle languages, and on confirmation:
|
|
63
|
+
|
|
62
64
|
- copies any newly-added locale JSON from `node_modules/xertica-ui/templates/src/locales/`
|
|
63
65
|
- removes JSONs of unselected languages
|
|
64
66
|
- regenerates `src/i18n.ts` and `src/app/App.tsx`
|
|
@@ -66,6 +68,14 @@ The flow shows your current selection, lets you toggle languages, and on confirm
|
|
|
66
68
|
|
|
67
69
|
The `update` → **Project files** flow also reads `.languages.json` and preserves your selection — overwrites of `App.tsx` and `i18n.ts` won't reset your languages to defaults.
|
|
68
70
|
|
|
71
|
+
### Enabling or disabling Dark Mode support later
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx xertica-ui update # then choose "Dark Mode"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The flow detects the current state (via `.xertica.json`) and prompts you to enable or disable dark mode. On confirmation, it updates `.xertica.json` and regenerates `App.tsx` with the updated `disableDarkMode` flag (which hides the `ThemeToggle` and template settings tab switch when disabled).
|
|
78
|
+
|
|
69
79
|
### Adding or removing the AI Assistant later
|
|
70
80
|
|
|
71
81
|
```bash
|
|
@@ -73,6 +83,7 @@ npx xertica-ui update # then choose "Assistant"
|
|
|
73
83
|
```
|
|
74
84
|
|
|
75
85
|
The flow detects the current state (via `.xertica.json` or file presence) and shows the appropriate action:
|
|
86
|
+
|
|
76
87
|
- **Add**: copies `src/features/assistant/` and `src/pages/AssistantPage.tsx`, adds the `/assistente` route to `AuthGuard.tsx`, and updates `HomePage.tsx` / `TemplatePage.tsx` to include the assistant panel.
|
|
77
88
|
- **Remove**: deletes those files, removes the route, and regenerates the page files without assistant imports.
|
|
78
89
|
|
|
@@ -278,22 +289,22 @@ Each feature only imports from `shared/` or its own domain. Pages only compose f
|
|
|
278
289
|
|
|
279
290
|
### Composed Blocks (with matching skeleton variants)
|
|
280
291
|
|
|
281
|
-
| Card
|
|
282
|
-
|
|
283
|
-
| `FeatureCard`
|
|
284
|
-
| `ActivityCard`
|
|
285
|
-
| `ProfileCard`
|
|
286
|
-
| `ProjectCard`
|
|
287
|
-
| `QuickActionCard`
|
|
288
|
-
| `NotificationCard`
|
|
289
|
-
| `StatsCard` (in `xertica-ui/ui`) | `StatsCardSkeleton`
|
|
292
|
+
| Card | Skeleton |
|
|
293
|
+
| -------------------------------- | -------------------------- |
|
|
294
|
+
| `FeatureCard` | `FeatureCardSkeleton` |
|
|
295
|
+
| `ActivityCard` | `ActivityCardSkeleton` |
|
|
296
|
+
| `ProfileCard` | `ProfileCardSkeleton` |
|
|
297
|
+
| `ProjectCard` | `ProjectCardSkeleton` |
|
|
298
|
+
| `QuickActionCard` | `QuickActionCardSkeleton` |
|
|
299
|
+
| `NotificationCard` | `NotificationCardSkeleton` |
|
|
300
|
+
| `StatsCard` (in `xertica-ui/ui`) | `StatsCardSkeleton` |
|
|
290
301
|
|
|
291
302
|
Each skeleton mirrors its card's visual layout with pulsing placeholders for loading states:
|
|
292
303
|
|
|
293
304
|
```tsx
|
|
294
|
-
{
|
|
295
|
-
? <ActivityCardSkeleton rows={5} />
|
|
296
|
-
|
|
305
|
+
{
|
|
306
|
+
isLoading ? <ActivityCardSkeleton rows={5} /> : <ActivityCard items={items} />;
|
|
307
|
+
}
|
|
297
308
|
```
|
|
298
309
|
|
|
299
310
|
---
|
package/bin/cli.ts
CHANGED
|
@@ -29,21 +29,28 @@ const XERTICA_CONFIG_FILE = '.xertica.json';
|
|
|
29
29
|
interface XerticaConfig {
|
|
30
30
|
version: 1;
|
|
31
31
|
hasAssistant: boolean;
|
|
32
|
+
disableDarkMode?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
async function readXerticaConfig(targetDir: string): Promise<XerticaConfig | null> {
|
|
35
36
|
const configPath = path.join(targetDir, XERTICA_CONFIG_FILE);
|
|
36
37
|
if (!(await fs.pathExists(configPath))) return null;
|
|
37
38
|
try {
|
|
38
|
-
return await fs.readJson(configPath) as XerticaConfig;
|
|
39
|
+
return (await fs.readJson(configPath)) as XerticaConfig;
|
|
39
40
|
} catch {
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
async function writeXerticaConfig(
|
|
45
|
+
async function writeXerticaConfig(
|
|
46
|
+
targetDir: string,
|
|
47
|
+
config: Partial<XerticaConfig>
|
|
48
|
+
): Promise<void> {
|
|
45
49
|
const configPath = path.join(targetDir, XERTICA_CONFIG_FILE);
|
|
46
|
-
const existing = (await readXerticaConfig(targetDir)) ?? {
|
|
50
|
+
const existing = (await readXerticaConfig(targetDir)) ?? {
|
|
51
|
+
version: 1 as const,
|
|
52
|
+
hasAssistant: false,
|
|
53
|
+
};
|
|
47
54
|
await fs.writeJson(configPath, { ...existing, ...config, version: 1 }, { spaces: 2 });
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -80,10 +87,14 @@ import { useAuth } from '../context/AuthContext';
|
|
|
80
87
|
|
|
81
88
|
// ─── Lazy page imports ────────────────────────────────────────────────────────
|
|
82
89
|
|
|
83
|
-
${
|
|
90
|
+
${
|
|
91
|
+
hasLogin
|
|
92
|
+
? `const LoginPage = React.lazy(() => import('../../pages/LoginPage').then(m => ({ default: m.LoginPage })));
|
|
84
93
|
const ForgotPasswordPage = React.lazy(() => import('../../pages/ForgotPasswordPage').then(m => ({ default: m.ForgotPasswordPage })));
|
|
85
94
|
const VerifyEmailPage = React.lazy(() => import('../../pages/VerifyEmailPage').then(m => ({ default: m.VerifyEmailPage })));
|
|
86
|
-
const ResetPasswordPage = React.lazy(() => import('../../pages/ResetPasswordPage').then(m => ({ default: m.ResetPasswordPage })));`
|
|
95
|
+
const ResetPasswordPage = React.lazy(() => import('../../pages/ResetPasswordPage').then(m => ({ default: m.ResetPasswordPage })));`
|
|
96
|
+
: ''
|
|
97
|
+
}
|
|
87
98
|
|
|
88
99
|
${hasHome ? `const HomePage = React.lazy(() => import('../../pages/HomePage').then(m => ({ default: m.HomePage })));` : ''}
|
|
89
100
|
${hasTemplate ? `const TemplatePage = React.lazy(() => import('../../pages/TemplatePage').then(m => ({ default: m.TemplatePage })));` : ''}
|
|
@@ -98,7 +109,9 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
|
98
109
|
return <>{children}</>;
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
${
|
|
112
|
+
${
|
|
113
|
+
hasLogin
|
|
114
|
+
? `function GuestRoute({ children }: { children: React.ReactNode }) {
|
|
102
115
|
const { user, isLoading } = useAuth();
|
|
103
116
|
if (isLoading) return null;
|
|
104
117
|
if (user) return <Navigate to="${firstProtectedPath}" replace />;
|
|
@@ -108,7 +121,9 @@ ${hasLogin ? `function GuestRoute({ children }: { children: React.ReactNode }) {
|
|
|
108
121
|
function LoginPageWithAuth() {
|
|
109
122
|
const { login } = useAuth();
|
|
110
123
|
return <LoginPage onLogin={login} />;
|
|
111
|
-
}`
|
|
124
|
+
}`
|
|
125
|
+
: ''
|
|
126
|
+
}
|
|
112
127
|
|
|
113
128
|
// ─── Route tree ───────────────────────────────────────────────────────────────
|
|
114
129
|
|
|
@@ -118,10 +133,14 @@ export function AuthGuard() {
|
|
|
118
133
|
return (
|
|
119
134
|
<div className="min-h-screen bg-muted overflow-x-hidden max-w-full">
|
|
120
135
|
<Routes>
|
|
121
|
-
${
|
|
136
|
+
${
|
|
137
|
+
hasLogin
|
|
138
|
+
? ` <Route path="/login" element={<GuestRoute><LoginPageWithAuth /></GuestRoute>} />
|
|
122
139
|
<Route path="/forgot-password" element={<GuestRoute><ForgotPasswordPage /></GuestRoute>} />
|
|
123
140
|
<Route path="/verify-email" element={<GuestRoute><VerifyEmailPage /></GuestRoute>} />
|
|
124
|
-
<Route path="/reset-password" element={<GuestRoute><ResetPasswordPage /></GuestRoute>} />`
|
|
141
|
+
<Route path="/reset-password" element={<GuestRoute><ResetPasswordPage /></GuestRoute>} />`
|
|
142
|
+
: ''
|
|
143
|
+
}
|
|
125
144
|
|
|
126
145
|
${hasHome ? ` <Route path="/home" element={<ProtectedRoute><HomePage /></ProtectedRoute>} />` : ''}
|
|
127
146
|
${hasTemplate ? ` <Route path="/template" element={<ProtectedRoute><TemplatePage /></ProtectedRoute>} />` : ''}
|
|
@@ -326,6 +345,12 @@ program
|
|
|
326
345
|
message: 'Include AI Assistant? (XerticaAssistant chat page + sidebar variant)',
|
|
327
346
|
initial: true,
|
|
328
347
|
},
|
|
348
|
+
{
|
|
349
|
+
type: 'confirm',
|
|
350
|
+
name: 'enableDarkMode',
|
|
351
|
+
message: 'Enable dark mode support?',
|
|
352
|
+
initial: true,
|
|
353
|
+
},
|
|
329
354
|
{
|
|
330
355
|
type: 'confirm',
|
|
331
356
|
name: 'install',
|
|
@@ -392,12 +417,14 @@ program
|
|
|
392
417
|
path.join(targetDir, 'src', 'main.tsx')
|
|
393
418
|
);
|
|
394
419
|
|
|
420
|
+
const disableDarkMode = response.enableDarkMode === false;
|
|
421
|
+
|
|
395
422
|
// 4. Generate src/app/App.tsx with the user's language selection
|
|
396
423
|
// (instead of copying the static template, we inject `availableLanguages`)
|
|
397
424
|
await fs.ensureDir(path.join(targetDir, 'src', 'app'));
|
|
398
425
|
await fs.writeFile(
|
|
399
426
|
path.join(targetDir, 'src', 'app', 'App.tsx'),
|
|
400
|
-
generateAppTsx(selectedLanguages)
|
|
427
|
+
generateAppTsx(selectedLanguages, disableDarkMode)
|
|
401
428
|
);
|
|
402
429
|
|
|
403
430
|
// 5. Copy src/app/components/AppLayout.tsx (always needed)
|
|
@@ -426,7 +453,7 @@ program
|
|
|
426
453
|
await writeLanguagesConfig(targetDir, selectedLanguages);
|
|
427
454
|
|
|
428
455
|
// 6.5 Persist project feature flags (.xertica.json)
|
|
429
|
-
await writeXerticaConfig(targetDir, { hasAssistant });
|
|
456
|
+
await writeXerticaConfig(targetDir, { hasAssistant, disableDarkMode });
|
|
430
457
|
|
|
431
458
|
// 6.4 Copy context
|
|
432
459
|
await fs.ensureDir(path.join(targetDir, 'src', 'app', 'context'));
|
|
@@ -501,7 +528,13 @@ program
|
|
|
501
528
|
|
|
502
529
|
// 9. Generate AuthGuard.tsx based on selected pages
|
|
503
530
|
const firstProtectedPath = hasHome ? '/home' : hasTemplate ? '/template' : '/login';
|
|
504
|
-
const authGuardContent = generateAuthGuard({
|
|
531
|
+
const authGuardContent = generateAuthGuard({
|
|
532
|
+
hasLogin,
|
|
533
|
+
hasHome,
|
|
534
|
+
hasTemplate,
|
|
535
|
+
hasAssistant,
|
|
536
|
+
firstProtectedPath,
|
|
537
|
+
});
|
|
505
538
|
|
|
506
539
|
await fs.writeFile(
|
|
507
540
|
path.join(targetDir, 'src', 'app', 'components', 'AuthGuard.tsx'),
|
|
@@ -543,9 +576,7 @@ program
|
|
|
543
576
|
` Languages: ${langLabels}${selectedLanguages.length === 1 ? ' (monolingual — LanguageSelector hidden)' : ''}`
|
|
544
577
|
)
|
|
545
578
|
);
|
|
546
|
-
console.log(
|
|
547
|
-
chalk.gray(' To add/remove languages later: npx xertica-ui update → Languages')
|
|
548
|
-
);
|
|
579
|
+
console.log(chalk.gray(' To add/remove languages later: npx xertica-ui update → Languages'));
|
|
549
580
|
console.log(
|
|
550
581
|
chalk.gray(` AI Assistant: ${hasAssistant ? 'included (/assistente)' : 'not included'}`)
|
|
551
582
|
);
|
|
@@ -591,6 +622,13 @@ program
|
|
|
591
622
|
: 'Add the AI Assistant to your project',
|
|
592
623
|
value: 'assistant',
|
|
593
624
|
},
|
|
625
|
+
{
|
|
626
|
+
title: 'Dark Mode',
|
|
627
|
+
description: currentConfig?.disableDarkMode
|
|
628
|
+
? 'Enable dark mode support in your project'
|
|
629
|
+
: 'Disable dark mode support in your project',
|
|
630
|
+
value: 'darkmode',
|
|
631
|
+
},
|
|
594
632
|
{
|
|
595
633
|
title: 'Project files',
|
|
596
634
|
description: 'Update app shell, shared, features and pages to a specific version',
|
|
@@ -657,9 +695,11 @@ program
|
|
|
657
695
|
|
|
658
696
|
console.log(
|
|
659
697
|
chalk.cyan(
|
|
660
|
-
`\nCurrent languages: ${
|
|
661
|
-
.
|
|
662
|
-
|
|
698
|
+
`\nCurrent languages: ${
|
|
699
|
+
SUPPORTED_LANGUAGES.filter(l => currentCodes.includes(l.code))
|
|
700
|
+
.map(l => l.label)
|
|
701
|
+
.join(', ') || '(none)'
|
|
702
|
+
}\n`
|
|
663
703
|
)
|
|
664
704
|
);
|
|
665
705
|
|
|
@@ -692,10 +732,8 @@ program
|
|
|
692
732
|
}
|
|
693
733
|
|
|
694
734
|
const summary: string[] = [];
|
|
695
|
-
if (toAdd.length > 0)
|
|
696
|
-
|
|
697
|
-
if (toRemove.length > 0)
|
|
698
|
-
summary.push(chalk.red(` - ${toRemove.join(', ')}`));
|
|
735
|
+
if (toAdd.length > 0) summary.push(chalk.green(` + ${toAdd.join(', ')}`));
|
|
736
|
+
if (toRemove.length > 0) summary.push(chalk.red(` - ${toRemove.join(', ')}`));
|
|
699
737
|
console.log(`\n${summary.join('\n')}\n`);
|
|
700
738
|
|
|
701
739
|
const { confirmed } = await prompts({
|
|
@@ -728,23 +766,17 @@ program
|
|
|
728
766
|
: path.resolve(__dirname, '../templates');
|
|
729
767
|
|
|
730
768
|
// 1) Sync locale JSON files: copy newly-added, prune removed
|
|
731
|
-
const { copied, removed } = await syncLocaleFiles(
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
newCodes,
|
|
735
|
-
{ pruneOthers: true }
|
|
736
|
-
);
|
|
769
|
+
const { copied, removed } = await syncLocaleFiles(templatesSourceDir, targetDir, newCodes, {
|
|
770
|
+
pruneOthers: true,
|
|
771
|
+
});
|
|
737
772
|
|
|
738
773
|
// 2) Regenerate i18n.ts so imports/resources reflect the new set
|
|
739
|
-
await fs.writeFile(
|
|
740
|
-
path.join(targetDir, 'src', 'i18n.ts'),
|
|
741
|
-
generateI18nFile(newCodes)
|
|
742
|
-
);
|
|
774
|
+
await fs.writeFile(path.join(targetDir, 'src', 'i18n.ts'), generateI18nFile(newCodes));
|
|
743
775
|
|
|
744
776
|
// 3) Regenerate App.tsx so the `availableLanguages` prop matches
|
|
745
777
|
await fs.writeFile(
|
|
746
778
|
path.join(targetDir, 'src', 'app', 'App.tsx'),
|
|
747
|
-
generateAppTsx(newCodes)
|
|
779
|
+
generateAppTsx(newCodes, currentConfig?.disableDarkMode ?? false)
|
|
748
780
|
);
|
|
749
781
|
|
|
750
782
|
// 4) Persist the new selection
|
|
@@ -756,9 +788,7 @@ program
|
|
|
756
788
|
if (removed.length > 0) console.log(chalk.red(` Removed: ${removed.join(', ')}`));
|
|
757
789
|
if (newCodes.length === 1) {
|
|
758
790
|
console.log(
|
|
759
|
-
chalk.gray(
|
|
760
|
-
` Project is now monolingual — the LanguageSelector will auto-hide.`
|
|
761
|
-
)
|
|
791
|
+
chalk.gray(` Project is now monolingual — the LanguageSelector will auto-hide.`)
|
|
762
792
|
);
|
|
763
793
|
}
|
|
764
794
|
} catch (error) {
|
|
@@ -798,11 +828,19 @@ program
|
|
|
798
828
|
: 'Add the AI Assistant to your project?',
|
|
799
829
|
choices: currentlyHas
|
|
800
830
|
? [
|
|
801
|
-
{
|
|
831
|
+
{
|
|
832
|
+
title: 'Remove assistant',
|
|
833
|
+
description: 'Deletes AssistantPage and assistant feature files',
|
|
834
|
+
value: 'remove',
|
|
835
|
+
},
|
|
802
836
|
{ title: 'Cancel', value: 'cancel' },
|
|
803
837
|
]
|
|
804
838
|
: [
|
|
805
|
-
{
|
|
839
|
+
{
|
|
840
|
+
title: 'Add assistant',
|
|
841
|
+
description: 'Copies AssistantPage and assistant feature files',
|
|
842
|
+
value: 'add',
|
|
843
|
+
},
|
|
806
844
|
{ title: 'Cancel', value: 'cancel' },
|
|
807
845
|
],
|
|
808
846
|
});
|
|
@@ -828,10 +866,17 @@ program
|
|
|
828
866
|
return;
|
|
829
867
|
}
|
|
830
868
|
|
|
831
|
-
const spinner = ora(
|
|
869
|
+
const spinner = ora(
|
|
870
|
+
action === 'add' ? 'Adding assistant...' : 'Removing assistant...'
|
|
871
|
+
).start();
|
|
832
872
|
|
|
833
873
|
try {
|
|
834
|
-
const installedTemplatesDir = path.join(
|
|
874
|
+
const installedTemplatesDir = path.join(
|
|
875
|
+
targetDir,
|
|
876
|
+
'node_modules',
|
|
877
|
+
'xertica-ui',
|
|
878
|
+
'templates'
|
|
879
|
+
);
|
|
835
880
|
const templatesSourceDir = (await fs.pathExists(installedTemplatesDir))
|
|
836
881
|
? installedTemplatesDir
|
|
837
882
|
: path.resolve(__dirname, '../templates');
|
|
@@ -917,6 +962,51 @@ program
|
|
|
917
962
|
return;
|
|
918
963
|
}
|
|
919
964
|
|
|
965
|
+
// ── Dark Mode update (enable / disable) ──────────────────────────────────
|
|
966
|
+
if (updateType === 'darkmode') {
|
|
967
|
+
const currentlyDisabled = currentConfig?.disableDarkMode ?? false;
|
|
968
|
+
const { enableDarkMode } = await prompts({
|
|
969
|
+
type: 'confirm',
|
|
970
|
+
name: 'enableDarkMode',
|
|
971
|
+
message: currentlyDisabled
|
|
972
|
+
? 'Enable dark mode support in your project?'
|
|
973
|
+
: 'Disable dark mode support in your project? (This will hide the toggle and force light mode)',
|
|
974
|
+
initial: !currentlyDisabled,
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
if (enableDarkMode === undefined) return;
|
|
978
|
+
|
|
979
|
+
const newDisableDarkMode = !enableDarkMode;
|
|
980
|
+
|
|
981
|
+
const spinner = ora(
|
|
982
|
+
newDisableDarkMode ? 'Disabling dark mode...' : 'Enabling dark mode...'
|
|
983
|
+
).start();
|
|
984
|
+
try {
|
|
985
|
+
// Persist the selection
|
|
986
|
+
await writeXerticaConfig(targetDir, { disableDarkMode: newDisableDarkMode });
|
|
987
|
+
|
|
988
|
+
// Regenerate App.tsx with the new dark mode flag
|
|
989
|
+
const persistedCodes = await readLanguagesConfig(targetDir);
|
|
990
|
+
const selectedCodes =
|
|
991
|
+
persistedCodes && persistedCodes.length > 0 ? persistedCodes : DEFAULT_SELECTION;
|
|
992
|
+
|
|
993
|
+
await fs.writeFile(
|
|
994
|
+
path.join(targetDir, 'src', 'app', 'App.tsx'),
|
|
995
|
+
generateAppTsx(selectedCodes, newDisableDarkMode)
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
spinner.succeed(
|
|
999
|
+
newDisableDarkMode
|
|
1000
|
+
? 'Dark mode disabled successfully! (Locked to Light Mode)'
|
|
1001
|
+
: 'Dark mode enabled successfully!'
|
|
1002
|
+
);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
spinner.fail('Failed to update dark mode configuration');
|
|
1005
|
+
console.error(error);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
920
1010
|
// ── Project files update ──────────────────────────────────────────────────
|
|
921
1011
|
const { versionType } = await prompts({
|
|
922
1012
|
type: 'select',
|
|
@@ -1038,15 +1128,14 @@ program
|
|
|
1038
1128
|
await writeLanguagesConfig(targetDir, selectedCodes);
|
|
1039
1129
|
}
|
|
1040
1130
|
|
|
1131
|
+
const projectConfig = await readXerticaConfig(targetDir);
|
|
1132
|
+
|
|
1041
1133
|
// Regenerate App.tsx and i18n.ts honoring the persisted language set
|
|
1042
1134
|
await fs.writeFile(
|
|
1043
1135
|
path.join(targetDir, 'src', 'app', 'App.tsx'),
|
|
1044
|
-
generateAppTsx(selectedCodes)
|
|
1045
|
-
);
|
|
1046
|
-
await fs.writeFile(
|
|
1047
|
-
path.join(targetDir, 'src', 'i18n.ts'),
|
|
1048
|
-
generateI18nFile(selectedCodes)
|
|
1136
|
+
generateAppTsx(selectedCodes, projectConfig?.disableDarkMode ?? false)
|
|
1049
1137
|
);
|
|
1138
|
+
await fs.writeFile(path.join(targetDir, 'src', 'i18n.ts'), generateI18nFile(selectedCodes));
|
|
1050
1139
|
|
|
1051
1140
|
// Refresh locale JSON files for the selected languages (keys grow over
|
|
1052
1141
|
// library updates) — but prune any orphans from prior selections.
|
|
@@ -1055,13 +1144,13 @@ program
|
|
|
1055
1144
|
});
|
|
1056
1145
|
|
|
1057
1146
|
// Regenerate AuthGuard preserving the current page set and assistant flag
|
|
1058
|
-
const projectConfig = await readXerticaConfig(targetDir);
|
|
1059
1147
|
const pagesDir = path.join(targetDir, 'src', 'pages');
|
|
1060
1148
|
const existingPages = (await fs.pathExists(pagesDir)) ? await fs.readdir(pagesDir) : [];
|
|
1061
1149
|
const hasLoginP = existingPages.includes('LoginPage.tsx');
|
|
1062
1150
|
const hasHomeP = existingPages.includes('HomePage.tsx');
|
|
1063
1151
|
const hasTemplateP = existingPages.includes('TemplatePage.tsx');
|
|
1064
|
-
const hasAssistantP =
|
|
1152
|
+
const hasAssistantP =
|
|
1153
|
+
projectConfig?.hasAssistant ?? existingPages.includes('AssistantPage.tsx');
|
|
1065
1154
|
const firstProtectedP = hasHomeP ? '/home' : hasTemplateP ? '/template' : '/login';
|
|
1066
1155
|
await fs.writeFile(
|
|
1067
1156
|
path.join(targetDir, 'src', 'app', 'components', 'AuthGuard.tsx'),
|
package/bin/language-config.ts
CHANGED
|
@@ -71,10 +71,7 @@ export async function readLanguagesConfig(targetDir: string): Promise<string[] |
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** Write the language selection to `src/locales/.languages.json`. */
|
|
74
|
-
export async function writeLanguagesConfig(
|
|
75
|
-
targetDir: string,
|
|
76
|
-
codes: string[]
|
|
77
|
-
): Promise<void> {
|
|
74
|
+
export async function writeLanguagesConfig(targetDir: string, codes: string[]): Promise<void> {
|
|
78
75
|
const configPath = path.join(targetDir, 'src', 'locales', LANGUAGES_CONFIG_FILENAME);
|
|
79
76
|
await fs.ensureDir(path.dirname(configPath));
|
|
80
77
|
const payload: LanguagesConfigFile = { version: 1, codes };
|
|
@@ -273,9 +270,10 @@ export default i18n;
|
|
|
273
270
|
* we omit the prop entirely (the library default already matches). Otherwise
|
|
274
271
|
* we emit an explicit array.
|
|
275
272
|
*/
|
|
276
|
-
export function generateAppTsx(selectedCodes: string[]): string {
|
|
273
|
+
export function generateAppTsx(selectedCodes: string[], disableDarkMode: boolean = false): string {
|
|
277
274
|
const selectedLangs = SUPPORTED_LANGUAGES.filter(l => selectedCodes.includes(l.code));
|
|
278
275
|
const isMonolingual = selectedLangs.length === 1;
|
|
276
|
+
const disableDarkModeProp = disableDarkMode ? `\n disableDarkMode={true}` : '';
|
|
279
277
|
const isAllDefaults =
|
|
280
278
|
selectedLangs.length === SUPPORTED_LANGUAGES.length &&
|
|
281
279
|
selectedLangs.every(l => DEFAULT_SELECTION.includes(l.code));
|
|
@@ -285,8 +283,7 @@ export function generateAppTsx(selectedCodes: string[]): string {
|
|
|
285
283
|
// prop (the library default matches). Otherwise we emit the explicit set.
|
|
286
284
|
const languagesArrayLiteral = selectedLangs
|
|
287
285
|
.map(
|
|
288
|
-
l =>
|
|
289
|
-
` { code: '${l.code}', label: '${l.label}', shortLabel: '${l.shortLabel}' },`
|
|
286
|
+
l => ` { code: '${l.code}', label: '${l.label}', shortLabel: '${l.shortLabel}' },`
|
|
290
287
|
)
|
|
291
288
|
.join('\n');
|
|
292
289
|
|
|
@@ -333,7 +330,7 @@ export default function App() {
|
|
|
333
330
|
<QueryClientProvider client={queryClient}>
|
|
334
331
|
<XerticaProvider
|
|
335
332
|
apiKey={geminiApiKey}
|
|
336
|
-
googleMapsApiKey={googleMapsApiKey}${availableLanguagesProp}
|
|
333
|
+
googleMapsApiKey={googleMapsApiKey}${availableLanguagesProp}${disableDarkModeProp}
|
|
337
334
|
>
|
|
338
335
|
<Router>
|
|
339
336
|
{/* AuthProvider must be inside Router (needs useNavigate) */}
|
|
@@ -219,9 +219,17 @@ export function ModernChatInput({
|
|
|
219
219
|
const getActionInfo = (action: ActionType) => {
|
|
220
220
|
switch (action) {
|
|
221
221
|
case 'document':
|
|
222
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
label: t('assistant.actions.createDocument'),
|
|
224
|
+
icon: FileText,
|
|
225
|
+
color: 'bg-[var(--chart-4)]',
|
|
226
|
+
};
|
|
223
227
|
case 'podcast':
|
|
224
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
label: t('assistant.actions.generatePodcast'),
|
|
230
|
+
icon: Radio,
|
|
231
|
+
color: 'bg-[var(--chart-1)]',
|
|
232
|
+
};
|
|
225
233
|
case 'search':
|
|
226
234
|
return { label: t('assistant.actions.search'), icon: Search, color: 'bg-[var(--chart-2)]' };
|
|
227
235
|
default:
|
|
@@ -280,7 +288,9 @@ export function ModernChatInput({
|
|
|
280
288
|
))}
|
|
281
289
|
</div>
|
|
282
290
|
|
|
283
|
-
<span className="text-sm font-medium text-destructive">
|
|
291
|
+
<span className="text-sm font-medium text-destructive">
|
|
292
|
+
{t('assistant.recordingAudio')}
|
|
293
|
+
</span>
|
|
284
294
|
</div>
|
|
285
295
|
|
|
286
296
|
<div className="flex items-center gap-3">
|
|
@@ -478,7 +488,9 @@ export function ModernChatInput({
|
|
|
478
488
|
? 'bg-destructive hover:bg-destructive/90 text-white animate-pulse'
|
|
479
489
|
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
480
490
|
}`}
|
|
481
|
-
aria-label={
|
|
491
|
+
aria-label={
|
|
492
|
+
isRecording ? t('assistant.stopRecording') : t('assistant.suggestWithVoice')
|
|
493
|
+
}
|
|
482
494
|
>
|
|
483
495
|
<Mic className="w-4 h-4" />
|
|
484
496
|
</Button>
|
|
@@ -545,9 +557,7 @@ export function ModernChatInput({
|
|
|
545
557
|
animate={{ opacity: 1, y: 0 }}
|
|
546
558
|
transition={{ delay: 0.1 }}
|
|
547
559
|
>
|
|
548
|
-
<p className="text-xs text-muted-foreground">
|
|
549
|
-
{t('assistant.disclaimer')}
|
|
550
|
-
</p>
|
|
560
|
+
<p className="text-xs text-muted-foreground">{t('assistant.disclaimer')}</p>
|
|
551
561
|
</motion.div>
|
|
552
562
|
</div>
|
|
553
563
|
);
|
|
@@ -50,9 +50,7 @@ export function AssistantConversationList({
|
|
|
50
50
|
<div className="text-center py-8">
|
|
51
51
|
<Heart className="w-12 h-12 mx-auto text-muted-foreground/50 mb-2" />
|
|
52
52
|
<p className="text-muted-foreground">
|
|
53
|
-
{activeTab === 'favoritos'
|
|
54
|
-
? t('assistant.noFavorites')
|
|
55
|
-
: t('assistant.noHistory')}
|
|
53
|
+
{activeTab === 'favoritos' ? t('assistant.noFavorites') : t('assistant.noHistory')}
|
|
56
54
|
</p>
|
|
57
55
|
</div>
|
|
58
56
|
) : (
|
|
@@ -56,8 +56,16 @@ export function AssistantFeedbackDialog({
|
|
|
56
56
|
<div className="grid gap-4 py-4 px-6">
|
|
57
57
|
<Textarea
|
|
58
58
|
className="min-h-[100px]"
|
|
59
|
-
placeholder={
|
|
60
|
-
|
|
59
|
+
placeholder={
|
|
60
|
+
state.category
|
|
61
|
+
? t('assistant.feedbackDialog.placeholderOptional')
|
|
62
|
+
: t('assistant.feedbackDialog.placeholder')
|
|
63
|
+
}
|
|
64
|
+
aria-label={
|
|
65
|
+
state.category
|
|
66
|
+
? t('assistant.feedbackDialog.placeholderOptional')
|
|
67
|
+
: t('assistant.feedbackDialog.placeholder')
|
|
68
|
+
}
|
|
61
69
|
value={state.reason}
|
|
62
70
|
onChange={e => onReasonChange(e.target.value)}
|
|
63
71
|
rows={4}
|
|
@@ -69,7 +77,9 @@ export function AssistantFeedbackDialog({
|
|
|
69
77
|
{t('common.cancel')}
|
|
70
78
|
</Button>
|
|
71
79
|
<Button onClick={onSubmit} disabled={!state.category && !state.reason.trim()}>
|
|
72
|
-
{state.category
|
|
80
|
+
{state.category
|
|
81
|
+
? t('assistant.feedbackDialog.confirmAndSend')
|
|
82
|
+
: t('assistant.feedbackDialog.send')}
|
|
73
83
|
</Button>
|
|
74
84
|
</DialogFooter>
|
|
75
85
|
</DialogContent>
|