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.
Files changed (105) hide show
  1. package/CHANGELOG.md +22 -2
  2. package/README.md +33 -22
  3. package/bin/cli.ts +136 -47
  4. package/bin/language-config.ts +5 -8
  5. package/components/assistant/modern-chat-input/ModernChatInput.tsx +17 -7
  6. package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +1 -3
  7. package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +13 -3
  8. package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +10 -6
  9. package/components/assistant/xertica-assistant/xertica-assistant.tsx +1 -3
  10. package/components/blocks/card-patterns/FeatureCardSkeleton.tsx +1 -6
  11. package/components/blocks/card-patterns/ProfileCard.tsx +1 -3
  12. package/components/blocks/card-patterns/ProjectCardSkeleton.tsx +1 -6
  13. package/components/brand/language-selector/language-selector.stories.tsx +1 -4
  14. package/components/brand/theme-toggle/ThemeToggle.tsx +5 -1
  15. package/components/brand/xertica-provider/XerticaProvider.tsx +1 -4
  16. package/components/index.ts +1 -5
  17. package/components/layout/sidebar/sidebar.tsx +9 -3
  18. package/components/media/audio-player/AudioPlayer.tsx +4 -2
  19. package/components/pages/forgot-password-page/ForgotPasswordPage.tsx +188 -188
  20. package/components/pages/home-content/HomeContent.tsx +55 -55
  21. package/components/pages/home-page/HomePage.tsx +5 -1
  22. package/components/pages/login-page/LoginPage.tsx +4 -2
  23. package/components/pages/reset-password-page/ResetPasswordPage.tsx +7 -3
  24. package/components/pages/template-content/TemplateContent.tsx +268 -149
  25. package/components/pages/verify-email-page/VerifyEmailPage.tsx +9 -9
  26. package/components/shared/error-boundary.stories.tsx +114 -132
  27. package/components/shared/error-boundary.tsx +150 -154
  28. package/components/shared/error-fallbacks.tsx +222 -226
  29. package/components/ui/stats-card/stats-card-skeleton.tsx +1 -3
  30. package/components/ui/stats-card/stats-card.stories.tsx +18 -0
  31. package/components/ui/stats-card/stats-card.tsx +18 -2
  32. package/components.json +512 -892
  33. package/contexts/AuthContext.tsx +121 -118
  34. package/contexts/LanguageContext.tsx +1 -2
  35. package/dist/AssistantChart-BKVtGUKF.js +3383 -0
  36. package/dist/AssistantChart-WeycT5Pd.cjs +3551 -0
  37. package/dist/VerifyEmailPage-Bp1XXl3H.cjs +3305 -0
  38. package/dist/VerifyEmailPage-DGhuIqkb.js +3296 -0
  39. package/dist/XerticaProvider-BErr83Bg.js +42 -0
  40. package/dist/XerticaProvider-CwOkHxiT.cjs +44 -0
  41. package/dist/XerticaXLogo-BX3ueACh.js +255 -0
  42. package/dist/XerticaXLogo-qBPhwK3g.cjs +260 -0
  43. package/dist/assistant.cjs.js +1 -1
  44. package/dist/assistant.es.js +1 -1
  45. package/dist/brand.cjs.js +2 -2
  46. package/dist/brand.es.js +2 -2
  47. package/dist/cli.js +90 -37
  48. package/dist/components/brand/theme-toggle/ThemeToggle.d.ts +1 -1
  49. package/dist/components/index.d.ts +1 -1
  50. package/dist/components/ui/stats-card/stats-card.d.ts +10 -0
  51. package/dist/index.cjs.js +6 -6
  52. package/dist/index.es.js +6 -6
  53. package/dist/layout.cjs.js +1 -1
  54. package/dist/layout.es.js +1 -1
  55. package/dist/pages.cjs.js +1 -1
  56. package/dist/pages.es.js +1 -1
  57. package/dist/sidebar-B4ZWaMrE.js +792 -0
  58. package/dist/sidebar-BS1p2V7t.cjs +795 -0
  59. package/dist/ui.cjs.js +1 -1
  60. package/dist/ui.es.js +1 -1
  61. package/dist/xertica-assistant-B1NaSFFj.js +2173 -0
  62. package/dist/xertica-assistant-CIaUlbIt.cjs +2180 -0
  63. package/dist/xertica-ui.css +1 -1
  64. package/docs/architecture-improvements.md +5 -5
  65. package/docs/architecture.md +16 -10
  66. package/docs/components/card-patterns.md +19 -17
  67. package/docs/components/error-boundary.md +201 -191
  68. package/docs/components/hooks.md +15 -13
  69. package/docs/components/language-selector.md +20 -16
  70. package/docs/components/pages.md +323 -309
  71. package/docs/components/stats-card.md +20 -2
  72. package/docs/doc-audit.md +12 -11
  73. package/docs/getting-started.md +41 -28
  74. package/docs/guidelines.md +14 -12
  75. package/docs/i18n.md +61 -57
  76. package/docs/installation.md +268 -267
  77. package/docs/llms.md +17 -17
  78. package/docs/state-management.md +17 -17
  79. package/guidelines/Guidelines.md +17 -14
  80. package/llms-compact.txt +1 -1
  81. package/llms-full.txt +11553 -7133
  82. package/llms.txt +1 -1
  83. package/package.json +1 -1
  84. package/styles/xertica/base.css +90 -84
  85. package/templates/CLAUDE.md +16 -1
  86. package/templates/guidelines/Guidelines.md +42 -18
  87. package/templates/package.json +3 -3
  88. package/templates/src/app/components/AuthGuard.tsx +131 -82
  89. package/templates/src/features/auth/ui/AuthPageShell.tsx +32 -32
  90. package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +1 -3
  91. package/templates/src/features/auth/ui/ResetPasswordContent.tsx +6 -2
  92. package/templates/src/features/auth/ui/VerifyEmailContent.tsx +2 -6
  93. package/templates/src/features/home/data/mock.ts +41 -35
  94. package/templates/src/features/home/ui/HomeContent.tsx +62 -64
  95. package/templates/src/features/template/ui/CrudTemplate.tsx +1 -4
  96. package/templates/src/features/template/ui/LoginTemplate.tsx +1 -1
  97. package/templates/src/features/template/ui/TemplateContent.tsx +28 -20
  98. package/templates/src/locales/en/pages/templates.json +17 -17
  99. package/templates/src/locales/es/pages/templates.json +17 -17
  100. package/templates/src/locales/pt-BR/pages/templates.json +17 -17
  101. package/templates/src/pages/AssistantPage.tsx +26 -20
  102. package/templates/src/pages/HomePage.tsx +5 -1
  103. package/templates/src/shared/error-boundary.tsx +150 -154
  104. package/templates/src/shared/error-fallbacks.tsx +222 -226
  105. 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 *lazy loading* dinâmico das rotas de acordo com a seleção do usuário.
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 *fallback* seguro para `'pt-BR'`.
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
- [![npm version](https://img.shields.io/badge/npm-2.3.0-blue)](https://www.npmjs.com/package/xertica-ui)
5
+ [![npm version](https://img.shields.io/badge/npm-2.4.1-blue)](https://www.npmjs.com/package/xertica-ui)
6
6
  [![license](https://img.shields.io/badge/license-proprietary-red)](./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 | 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
- | **Include AI Assistant** | yes (default) / no |
41
- | Install dependencies automatically | yes / no |
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 | Skeleton |
282
- |---|---|
283
- | `FeatureCard` | `FeatureCardSkeleton` |
284
- | `ActivityCard` | `ActivityCardSkeleton` |
285
- | `ProfileCard` | `ProfileCardSkeleton` |
286
- | `ProjectCard` | `ProjectCardSkeleton` |
287
- | `QuickActionCard` | `QuickActionCardSkeleton` |
288
- | `NotificationCard` | `NotificationCardSkeleton` |
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
- {isLoading
295
- ? <ActivityCardSkeleton rows={5} />
296
- : <ActivityCard items={items} />}
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(targetDir: string, config: Partial<XerticaConfig>): Promise<void> {
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)) ?? { version: 1 as const, hasAssistant: false };
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
- ${hasLogin ? `const LoginPage = React.lazy(() => import('../../pages/LoginPage').then(m => ({ default: m.LoginPage })));
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
- ${hasLogin ? `function GuestRoute({ children }: { children: React.ReactNode }) {
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
- ${hasLogin ? ` <Route path="/login" element={<GuestRoute><LoginPageWithAuth /></GuestRoute>} />
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({ hasLogin, hasHome, hasTemplate, hasAssistant, firstProtectedPath });
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: ${SUPPORTED_LANGUAGES.filter(l => currentCodes.includes(l.code))
661
- .map(l => l.label)
662
- .join(', ') || '(none)'}\n`
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
- summary.push(chalk.green(` + ${toAdd.join(', ')}`));
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
- templatesSourceDir,
733
- targetDir,
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
- { title: 'Remove assistant', description: 'Deletes AssistantPage and assistant feature files', value: 'remove' },
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
- { title: 'Add assistant', description: 'Copies AssistantPage and assistant feature files', value: 'add' },
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(action === 'add' ? 'Adding assistant...' : 'Removing assistant...').start();
869
+ const spinner = ora(
870
+ action === 'add' ? 'Adding assistant...' : 'Removing assistant...'
871
+ ).start();
832
872
 
833
873
  try {
834
- const installedTemplatesDir = path.join(targetDir, 'node_modules', 'xertica-ui', 'templates');
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 = projectConfig?.hasAssistant ?? existingPages.includes('AssistantPage.tsx');
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'),
@@ -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 { label: t('assistant.actions.createDocument'), icon: FileText, color: 'bg-[var(--chart-4)]' };
222
+ return {
223
+ label: t('assistant.actions.createDocument'),
224
+ icon: FileText,
225
+ color: 'bg-[var(--chart-4)]',
226
+ };
223
227
  case 'podcast':
224
- return { label: t('assistant.actions.generatePodcast'), icon: Radio, color: 'bg-[var(--chart-1)]' };
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">{t('assistant.recordingAudio')}</span>
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={isRecording ? t('assistant.stopRecording') : t('assistant.suggestWithVoice')}
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={state.category ? t('assistant.feedbackDialog.placeholderOptional') : t('assistant.feedbackDialog.placeholder')}
60
- aria-label={state.category ? t('assistant.feedbackDialog.placeholderOptional') : t('assistant.feedbackDialog.placeholder')}
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 ? t('assistant.feedbackDialog.confirmAndSend') : t('assistant.feedbackDialog.send')}
80
+ {state.category
81
+ ? t('assistant.feedbackDialog.confirmAndSend')
82
+ : t('assistant.feedbackDialog.send')}
73
83
  </Button>
74
84
  </DialogFooter>
75
85
  </DialogContent>