xertica-ui 2.4.1 → 2.5.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 (53) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +69 -2
  3. package/bin/cli.ts +14 -2
  4. package/bin/generate-tokens.ts +12 -12
  5. package/components/brand/xertica-provider/XerticaProvider.tsx +4 -1
  6. package/components/layout/sidebar/sidebar.stories.tsx +201 -0
  7. package/components/layout/sidebar/sidebar.tsx +86 -1
  8. package/components/pages/template-page/TemplatePage.stories.tsx +8 -15
  9. package/components/ui/chart/chart.test.tsx +1 -1
  10. package/components/ui/chart/chart.tsx +13 -6
  11. package/contexts/BrandColorsContext.tsx +39 -8
  12. package/contexts/theme-data.ts +51 -0
  13. package/dist/AssistantChart-BZTPJ5dP.cjs +3551 -0
  14. package/dist/AssistantChart-DMJJ_Amf.js +3383 -0
  15. package/dist/BrandColorsContext-BMRJ04Wf.js +718 -0
  16. package/dist/BrandColorsContext-BwY-b6M4.cjs +725 -0
  17. package/dist/VerifyEmailPage-CGIwmWrm.js +3296 -0
  18. package/dist/VerifyEmailPage-CpqqpLpo.cjs +3305 -0
  19. package/dist/XerticaProvider-CeS5G_n5.cjs +45 -0
  20. package/dist/XerticaProvider-ra2NciRq.js +43 -0
  21. package/dist/assistant.cjs.js +1 -1
  22. package/dist/assistant.es.js +1 -1
  23. package/dist/brand.cjs.js +1 -1
  24. package/dist/brand.es.js +1 -1
  25. package/dist/cli.js +59 -14
  26. package/dist/components/brand/xertica-provider/XerticaProvider.d.ts +3 -1
  27. package/dist/components/ui/chart/chart.d.ts +12 -3
  28. package/dist/contexts/theme-data.d.ts +4 -0
  29. package/dist/hooks.cjs.js +1 -1
  30. package/dist/hooks.es.js +1 -1
  31. package/dist/index.cjs.js +6 -6
  32. package/dist/index.es.js +6 -6
  33. package/dist/layout.cjs.js +1 -1
  34. package/dist/layout.es.js +1 -1
  35. package/dist/pages.cjs.js +1 -1
  36. package/dist/pages.es.js +1 -1
  37. package/dist/rich-text-editor-B2CKz7nx.cjs +2903 -0
  38. package/dist/rich-text-editor-DloeW0wc.js +2832 -0
  39. package/dist/sidebar-0ocFLSks.js +878 -0
  40. package/dist/sidebar-CeTMuzOx.cjs +881 -0
  41. package/dist/ui.cjs.js +2 -2
  42. package/dist/ui.es.js +2 -2
  43. package/dist/xertica-assistant-CyikE3N_.js +2173 -0
  44. package/dist/xertica-assistant-QFUnv5I2.cjs +2180 -0
  45. package/dist/xertica-ui.css +1 -1
  46. package/docs/components/sidebar.md +19 -2
  47. package/llms-compact.txt +30 -1
  48. package/llms.txt +1 -1
  49. package/package.json +2 -2
  50. package/styles/xertica/tokens.css +9 -9
  51. package/templates/guidelines/Guidelines.md +643 -355
  52. package/templates/package.json +2 -2
  53. package/templates/src/styles/xertica/tokens.css +9 -9
package/CHANGELOG.md CHANGED
@@ -11,6 +11,77 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
11
11
 
12
12
  ---
13
13
 
14
+ ## [2.5.1] — 2026-06-22
15
+
16
+ ### Fixed
17
+
18
+ - **CLI `update → Theme` — tema atual não era pré-selecionado** — o prompt de seleção de tema sempre iniciava no índice 0 (`xertica-original`), independente do tema instalado. Corrigido lendo `themeId` do `.xertica.json` e calculando o índice inicial correspondente. O nome do tema atual é exibido antes da seleção.
19
+ - **CLI `update → Theme` — `themeId` não era persistido** — após trocar o tema, o `.xertica.json` não era atualizado com o novo `themeId`, fazendo com que execuções futuras continuassem sem memória da escolha. Corrigido com `writeXerticaConfig(targetDir, { themeId: selectedTheme.id })` após a escrita do `tokens.css`.
20
+ - **CLI `init` — `themeId` não era salvo em `.xertica.json`** — o tema escolhido durante `init` não era persistido no arquivo de configuração do projeto. Corrigido passando `themeId: response.theme` ao `writeXerticaConfig`.
21
+ - **`generate-tokens.ts` — `--sidebar-border` no dark mode estava hardcoded** — o valor `rgba(65, 61, 107, 1)` (indigo fixo) era usado para todos os temas. Corrigido usando `colors.darkBorder` para que cada tema utilize sua própria cor de borda tingida.
22
+ - **`XerticaConfig` — campo `themeId` faltando na interface** — adicionado campo opcional `themeId?: string` à interface `XerticaConfig` em `bin/cli.ts`.
23
+ - **Erros de TypeScript do `recharts` 3.x** — atualização da biblioteca quebrou os tipos de `ChartTooltipContent` e `ChartLegendContent`. Corrigido:
24
+ - `ChartTooltipContent`: migrado de `React.ComponentProps<typeof Tooltip>` (que omite `payload`/`label` no recharts 3) para tipos explícitos usando `RechartsPrimitive.TooltipPayload` e `DefaultTooltipContentProps['formatter']`
25
+ - `ChartLegendContent`: substituído `Pick<LegendProps, 'payload' | 'verticalAlign'>` por `{ payload?: LegendPayload[]; verticalAlign?: ... }` (recharts 3 removeu `payload` de `LegendProps`)
26
+ - `DonutBreakdownChart`: removida prop `activeIndex` do `<Pie>` (não existe mais no recharts 3)
27
+ - `PieMetricChart`: adicionado guard `if (midAngle == null || percent == null) return null` no callback de label
28
+ - `chart.test.tsx`: adicionado `graphicalItemId` obrigatório ao mock de payload
29
+
30
+ ### Changed
31
+
32
+ - **CLI `update → Theme`** — exibe o tema atual antes do prompt de seleção com `chalk.gray('Current theme: ...')`.
33
+
34
+ ---
35
+
36
+ ## [2.5.0] — 2026-06-22
37
+
38
+ ### Added
39
+
40
+ - **Sidebar — Subitens inline no mobile** — em viewports mobile (< 768 px) os itens com `children` agora expandem/colapsam de forma inline (accordion) abaixo do item pai, em vez de abrir um `DropdownMenu` lateral que ficava invisível fora dos limites do container. O botão `ChevronRight` rotaciona 90° ao abrir e os filhos surgem com animação via Framer Motion. No desktop o comportamento de dropdown lateral permanece inalterado.
41
+
42
+ - **Storybook — Stories de sidebar com subitens (desktop e mobile)** — adicionadas duas novas stories na documentação da `Sidebar`:
43
+ - `WithSubitemsDesktop` — sidebar iniciada expandida mostrando o botão `ChevronRight` nos itens com filhos e o dropdown lateral ao clicar.
44
+ - `WithSubitemsMobile` — simula um viewport de 375 px com header de app mobile (botão hambúrguer). Ao abrir, a sidebar cobre a tela inteira e os subitens expandem inline, permitindo inspeção e ajuste do visual mobile.
45
+
46
+ - **Tema de cores — Dark mode tingido por hue** — todos os tokens de superfície do dark mode (`--background`, `--card`, `--popover`, `--muted`, `--secondary`, `--accent`, `--border`, `--input`) agora são tingidos com a hue da cor primária do tema ativo, em vez de cinza-zinc neutro para todos os temas:
47
+ - **primary / xertica-original**: fundo `#05050d` (azul-índigo muito sutil)
48
+ - **blue**: fundo `#03050f` (azul marinho profundo)
49
+ - **violet**: fundo `#07040f` (violeta escuro)
50
+ - **rose**: fundo `#0f0305` (rosa/vermelho escuro)
51
+ - **emerald**: fundo `#030f08` (verde escuro)
52
+ - **amber**: fundo `#0f0a03` (âmbar escuro)
53
+ - **orange**: fundo `#0f0703` (laranja escuro)
54
+ - **zinc / slate**: mantêm o azul-índigo sutil (alinhado com o tema default)
55
+
56
+ - **`BrandColorsContext` — novos campos `darkBackground`, `darkCard`, `darkMuted`, `darkBorder`** — a interface `BrandColors` em `contexts/theme-data.ts` ganhou 4 novos campos que definem as cores de superfície específicas de cada tema no dark mode. Todos os 9 temas foram atualizados com valores correspondentes.
57
+
58
+ - **`XerticaProvider` — nova prop `defaultColorTheme`** — aceita o ID de um `ColorTheme` (ex: `'blue'`, `'rose'`, `'emerald'`) para selecionar o tema de cor completo via props, sem a limitação de passar apenas um hex isolado via `primaryColor`.
59
+
60
+ - **Storybook — troca de temas de cor funcional** — o toolbar `Brand Color` agora troca o tema completo (primary, sidebar, charts **e** superfícies dark) com total fidelidade. Os valores do toolbar foram migrados de hex brutos para IDs de tema (`xertica-original`, `blue`, `violet`, etc.).
61
+
62
+ ### Fixed
63
+
64
+ - **`BrandColorsContext` — tokens de superfície dark não refletiam no Storybook** — o `applyColors()` injetava o style tag com `document.head.prepend()`, colocando-o antes de todos os outros estilos. Como os tokens de cor do `tokens.css` apareciam depois (e com seletores de maior especificidade como `:root[data-mode='dark']`), sobrescreviam os valores injetados. Corrigido com três ajustes:
65
+ 1. `prepend` → `appendChild` para que o style injetado sempre apareça por último no `<head>` e vença na cascata de mesma especificidade.
66
+ 2. Os tokens de primary/sidebar/charts/semantic passaram a ser aplicados via `root.style.setProperty()` (inline style no `<html>`), que tem especificidade máxima e sempre vence qualquer regra de stylesheet.
67
+ 3. O bloco dark de superfícies usa o seletor `:root[data-mode='dark'], .dark` para igualar a especificidade do `tokens.css` e ser decidido pela ordem de aparição.
68
+
69
+ - **`TemplatePage.stories.tsx` — erro ao renderizar no Storybook** — o componente `TemplatePage` usa `useAuth()` internamente, mas o decorator das stories não incluía `AuthProvider`. Ao carregar a página de documentação, o hook lançava `"useAuth must be used within <AuthProvider>"`. Corrigido adicionando `AuthProvider` dentro do `MemoryRouter` no decorator.
70
+
71
+ - **Sidebar — `isMobileViewport` não consumido em `SidebarNav`** — o valor já existia no `SidebarContext` mas não era lido pelo `SidebarNav`, impedindo a bifurcação entre accordion (mobile) e dropdown (desktop).
72
+
73
+ ### Changed
74
+
75
+ - **`BrandColorsContext.applyColors` — estratégia de injeção de CSS** — tokens de primary aplicados via inline style (máxima prioridade); tokens de superfície dark injetados via `<style>` tag no final do `<head>`.
76
+
77
+ - **Storybook `preview.tsx` — toolbar `brandColor`** — os itens do toolbar passaram de hex brutos para IDs de tema; o decorator usa `defaultColorTheme` em vez de `primaryColor` no `XerticaProvider`.
78
+
79
+ - **`bin/generate-tokens.ts`** — o bloco dark mode agora usa `colors.darkBackground`, `colors.darkCard`, `colors.darkMuted` e `colors.darkBorder` em vez de valores zinc hardcoded, gerando CSS correto para todos os temas.
80
+
81
+ - **Três arquivos `tokens.css`** (`styles/xertica/`, `src/styles/xertica/`, `templates/src/styles/xertica/`) — atualizados com os valores dark tingidos para o tema xertica-original (azul-índigo).
82
+
83
+ ---
84
+
14
85
  ## [2.4.1] — 2026-06-17
15
86
 
16
87
  ### Changed
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.4.1-blue)](https://www.npmjs.com/package/xertica-ui)
5
+ [![npm version](https://img.shields.io/badge/npm-2.5.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
  ---
@@ -347,10 +347,77 @@ Map stories use a wider responsive preview frame, making `Map`, `RouteMap`, and
347
347
 
348
348
  ---
349
349
 
350
- ## 🎨 Design Tokens
350
+ ## 🎨 Design Tokens & Theming
351
351
 
352
352
  Xertica UI uses semantic CSS tokens. **Never use raw colors or generic Tailwind color classes**.
353
353
 
354
+ ### Color Themes
355
+
356
+ Nine built-in themes — each sets the primary color, sidebar, chart palette, and dark-mode surface hue. Select at `init` time or change later with `npx xertica-ui update → Theme only`.
357
+
358
+ ```
359
+ ┌────────────────────────────────────────────────────────────────────────────┐
360
+ │ Theme ID Primary Sidebar Dark bg │
361
+ ├────────────────────────────────────────────────────────────────────────────┤
362
+ │ 🟣 Xertica xertica-original #2C275B #2C275B #05050d │
363
+ │ ⚫ Zinc zinc #18181B #18181B #05050d │
364
+ │ 🌑 Slate slate #0F172A #0F172A #05050d │
365
+ │ 🔵 Blue blue #2563EB #1E3A8A #03050f │
366
+ │ 🟣 Violet violet #7C3AED #4C1D95 #07040f │
367
+ │ 🌹 Rose rose #BE123C #881337 #0f0305 │
368
+ │ 🟢 Emerald emerald #047857 #064E3B #030f08 │
369
+ │ 🟡 Amber amber #B45309 #78350F #0f0a03 │
370
+ │ 🟠 Orange orange #C2410C #7C2D12 #0f0703 │
371
+ └────────────────────────────────────────────────────────────────────────────┘
372
+ ```
373
+
374
+ **CLI — Select at project creation:**
375
+
376
+ ```bash
377
+ npx xertica-ui@latest init my-app
378
+ # → "Select the default color theme" prompt appears during init
379
+ ```
380
+
381
+ **CLI — Change theme in an existing project:**
382
+
383
+ ```bash
384
+ npx xertica-ui update
385
+ # → choose "Theme only"
386
+ # → select any of the 9 themes
387
+ # → src/styles/xertica/tokens.css is fully regenerated
388
+ # → .xertica.json is updated with the new themeId for future updates
389
+ ```
390
+
391
+ **Code — Select theme in `XerticaProvider`:**
392
+
393
+ ```tsx
394
+ // Full preset — sets primary, sidebar, charts AND hue-tinted dark surfaces
395
+ <XerticaProvider defaultColorTheme="blue">
396
+ <XerticaProvider defaultColorTheme="rose">
397
+ <XerticaProvider defaultColorTheme="emerald">
398
+ <XerticaProvider defaultColorTheme="violet">
399
+ <XerticaProvider defaultColorTheme="amber">
400
+ <XerticaProvider defaultColorTheme="orange">
401
+ <XerticaProvider defaultColorTheme="xertica-original"> {/* default */}
402
+
403
+ // Custom hex — sets primary only; dark surfaces use the neutral default
404
+ <XerticaProvider primaryColor="#7C3AED">
405
+ ```
406
+
407
+ **What each theme changes:**
408
+
409
+ | Token category | Controlled by theme |
410
+ | -------------------- | ------------------- |
411
+ | `--primary` | ✅ Light + dark variant |
412
+ | `--sidebar` | ✅ Light + dark variant |
413
+ | `--chart-1..5` | ✅ Brand-matched palette |
414
+ | `--gradient-diagonal`| ✅ Brand-matched gradient |
415
+ | `--background` | ✅ Dark mode only (hue-tinted) |
416
+ | `--card`, `--popover`| ✅ Dark mode only (hue-tinted) |
417
+ | `--muted`, `--accent`| ✅ Dark mode only (hue-tinted) |
418
+ | `--border`, `--input`| ✅ Dark mode only (hue-tinted) |
419
+ | Light mode surfaces | ❌ Always white/zinc (unchanged) |
420
+
354
421
  ### Mobile Content Padding
355
422
 
356
423
  The CSS token `--mobile-content-padding` (default `1.25rem`) controls the horizontal padding of content areas on small screens (`< 768px`). To adjust it globally, override it in your project's `src/styles/xertica/tokens.css`:
package/bin/cli.ts CHANGED
@@ -30,6 +30,7 @@ interface XerticaConfig {
30
30
  version: 1;
31
31
  hasAssistant: boolean;
32
32
  disableDarkMode?: boolean;
33
+ themeId?: string;
33
34
  }
34
35
 
35
36
  async function readXerticaConfig(targetDir: string): Promise<XerticaConfig | null> {
@@ -453,7 +454,7 @@ program
453
454
  await writeLanguagesConfig(targetDir, selectedLanguages);
454
455
 
455
456
  // 6.5 Persist project feature flags (.xertica.json)
456
- await writeXerticaConfig(targetDir, { hasAssistant, disableDarkMode });
457
+ await writeXerticaConfig(targetDir, { hasAssistant, disableDarkMode, themeId: response.theme });
457
458
 
458
459
  // 6.4 Copy context
459
460
  await fs.ensureDir(path.join(targetDir, 'src', 'app', 'context'));
@@ -641,6 +642,15 @@ program
641
642
 
642
643
  // ── Theme update ─────────────────────────────────────────────────────────
643
644
  if (updateType === 'theme') {
645
+ const currentThemeId = currentConfig?.themeId ?? 'xertica-original';
646
+ const currentThemeIndex = Math.max(
647
+ 0,
648
+ colorThemes.findIndex(t => t.id === currentThemeId)
649
+ );
650
+
651
+ const currentThemeName = colorThemes[currentThemeIndex]?.name ?? 'Xertica';
652
+ console.log(chalk.gray(` Current theme: ${currentThemeName}`));
653
+
644
654
  const { theme } = await prompts({
645
655
  type: 'select',
646
656
  name: 'theme',
@@ -650,7 +660,7 @@ program
650
660
  description: t.description,
651
661
  value: t.id,
652
662
  })),
653
- initial: 0,
663
+ initial: currentThemeIndex,
654
664
  });
655
665
 
656
666
  if (!theme) return;
@@ -662,7 +672,9 @@ program
662
672
  if (selectedTheme) {
663
673
  await fs.ensureDir(path.dirname(tokensPath));
664
674
  await fs.writeFile(tokensPath, generateTokensCss(selectedTheme));
675
+ await writeXerticaConfig(targetDir, { themeId: selectedTheme.id });
665
676
  spinner.succeed(`Theme updated to "${selectedTheme.name}" successfully!`);
677
+ console.log(chalk.gray(' File updated: src/styles/xertica/tokens.css'));
666
678
  } else {
667
679
  spinner.fail('Theme not found.');
668
680
  }
@@ -178,26 +178,26 @@ export const generateTokensCss = (theme: ColorTheme): string => {
178
178
  --primary: var(--xertica-primary);
179
179
 
180
180
  /* Semantic Colors */
181
- --background: rgba(5, 5, 5, 1);
181
+ --background: ${rgba(colors.darkBackground)};
182
182
  --foreground: rgba(250, 250, 250, 1);
183
183
 
184
- --card: rgba(20, 20, 22, 1);
184
+ --card: ${rgba(colors.darkCard)};
185
185
  --card-foreground: rgba(250, 250, 250, 1);
186
186
 
187
- --popover: rgba(20, 20, 22, 1);
187
+ --popover: ${rgba(colors.darkCard)};
188
188
  --popover-foreground: rgba(250, 250, 250, 1);
189
189
 
190
190
  --primary-foreground: ${rgba(colors.primaryForegroundDark)};
191
191
  --primary-light: ${rgba(colors.primaryDarkMode, 0.15)};
192
192
  --primary-light-foreground: ${rgba(colors.primaryDarkMode)};
193
193
 
194
- --secondary: rgba(39, 39, 42, 1);
194
+ --secondary: ${rgba(colors.darkMuted)};
195
195
  --secondary-foreground: rgba(250, 250, 250, 1);
196
196
 
197
- --muted: rgba(39, 39, 42, 1);
197
+ --muted: ${rgba(colors.darkMuted)};
198
198
  --muted-foreground: rgba(161, 161, 170, 1);
199
199
 
200
- --accent: rgba(39, 39, 42, 1);
200
+ --accent: ${rgba(colors.darkMuted)};
201
201
  --accent-foreground: rgba(250, 250, 250, 1);
202
202
 
203
203
  --destructive: rgba(239, 68, 68, 1);
@@ -211,9 +211,9 @@ export const generateTokensCss = (theme: ColorTheme): string => {
211
211
  --warning: rgba(251, 191, 36, 1);
212
212
  --warning-foreground: rgba(5, 5, 5, 1);
213
213
 
214
- --border: rgba(63, 63, 70, 1);
215
- --input: rgba(39, 39, 42, 0.5);
216
- --input-background: rgba(39, 39, 42, 0.5);
214
+ --border: ${rgba(colors.darkBorder)};
215
+ --input: ${rgba(colors.darkMuted, 0.5)};
216
+ --input-background: ${rgba(colors.darkMuted, 0.5)};
217
217
  --ring: ${rgba(colors.primaryDarkMode, 0.5)};
218
218
 
219
219
  --elevation-sm: 0px 0px 48px 0px rgba(0, 0, 0, 0.3);
@@ -232,10 +232,10 @@ export const generateTokensCss = (theme: ColorTheme): string => {
232
232
  --sidebar: ${rgba(colors.sidebarDark)};
233
233
  --sidebar-foreground: rgba(250, 250, 250, 1);
234
234
  --sidebar-primary: rgba(255, 255, 255, 1);
235
- --sidebar-primary-foreground: ${rgba(colors.primary)}; /* Often primary brand color on dark background */
236
- --sidebar-accent: rgba(63, 63, 70, 1);
235
+ --sidebar-primary-foreground: ${rgba(colors.primary)};
236
+ --sidebar-accent: rgba(255, 255, 255, 0.08);
237
237
  --sidebar-accent-foreground: rgba(250, 250, 250, 1);
238
- --sidebar-border: rgba(65, 61, 107, 1); /* Keeping subtle border */
238
+ --sidebar-border: ${rgba(colors.darkBorder)};
239
239
  --sidebar-ring: ${rgba(colors.primaryDarkMode, 0.5)};
240
240
 
241
241
  /* Gradients */
@@ -25,6 +25,8 @@ interface XerticaProviderProps {
25
25
  googleMapsApiKey?: string;
26
26
  /** Default theme name for branding ('default', 'dark', etc.) */
27
27
  defaultBrandTheme?: string;
28
+ /** Color theme ID (e.g. 'xertica-original', 'blue', 'rose') — selects a full ColorTheme preset */
29
+ defaultColorTheme?: string;
28
30
  /** Primary brand color hex value */
29
31
  primaryColor?: string;
30
32
  /** Whether to use custom theme tokens from CSS */
@@ -72,6 +74,7 @@ export function XerticaProvider({
72
74
  apiKey,
73
75
  googleMapsApiKey,
74
76
  defaultBrandTheme,
77
+ defaultColorTheme,
75
78
  primaryColor,
76
79
  useCustomTokens,
77
80
  disableDarkMode,
@@ -81,7 +84,7 @@ export function XerticaProvider({
81
84
  return (
82
85
  <ThemeProvider disableDarkMode={disableDarkMode}>
83
86
  <BrandColorsProvider
84
- defaultTheme={defaultBrandTheme}
87
+ defaultTheme={defaultColorTheme || defaultBrandTheme}
85
88
  primaryColor={primaryColor}
86
89
  useCustomTokens={useCustomTokens}
87
90
  >
@@ -424,6 +424,207 @@ export const Autonomous: Story = {
424
424
  },
425
425
  };
426
426
 
427
+ /**
428
+ * Desktop view with sub-items expanded.
429
+ * Items with `children` show a ChevronRight button; clicking it opens a
430
+ * dropdown listing the child routes positioned to the right of the sidebar.
431
+ */
432
+ export const WithSubitemsDesktop: Story = {
433
+ name: 'With Sub-items (Desktop)',
434
+ args: {
435
+ variant: 'default',
436
+ expanded: true,
437
+ user: { name: 'Admin', email: 'admin@example.com' },
438
+ onLogout: () => console.log('Logged out'),
439
+ location: { pathname: '/dashboard' },
440
+ navigationGroups: [
441
+ {
442
+ id: 'main',
443
+ label: 'Principal',
444
+ items: [
445
+ { path: '/home', label: 'Início', icon: Home },
446
+ {
447
+ path: '/dashboard',
448
+ label: 'Dashboard',
449
+ icon: BarChart,
450
+ children: [
451
+ { path: '/dashboard/overview', label: 'Visão Geral', icon: BarChart },
452
+ { path: '/dashboard/reports', label: 'Relatórios', icon: FileEdit },
453
+ { path: '/dashboard/analytics', label: 'Analytics', icon: Map },
454
+ ],
455
+ },
456
+ ],
457
+ },
458
+ {
459
+ id: 'admin',
460
+ label: 'Administração',
461
+ items: [
462
+ {
463
+ path: '/users',
464
+ label: 'Usuários',
465
+ icon: Users,
466
+ children: [
467
+ { path: '/users/list', label: 'Lista de Usuários', icon: Users },
468
+ { path: '/users/roles', label: 'Perfis de Acesso', icon: Settings },
469
+ ],
470
+ },
471
+ { path: '/settings', label: 'Configurações', icon: Settings },
472
+ ],
473
+ },
474
+ ],
475
+ footer: {
476
+ showUser: true,
477
+ showSettings: false,
478
+ showLogout: true,
479
+ },
480
+ },
481
+ render: args => {
482
+ const [isExpanded, setIsExpanded] = useState(true);
483
+ const currentWidth = isExpanded ? 280 : 80;
484
+
485
+ return (
486
+ <div
487
+ className="relative h-screen w-full border rounded-[var(--radius-lg)] bg-muted/20 overflow-hidden"
488
+ style={{ transform: 'translateZ(0)' }}
489
+ >
490
+ <Sidebar
491
+ {...args}
492
+ expanded={isExpanded}
493
+ onToggle={() => setIsExpanded(!isExpanded)}
494
+ width={currentWidth}
495
+ navigate={() => {}}
496
+ />
497
+ <div
498
+ className="absolute inset-y-0 right-0 p-8 flex items-center justify-center transition-all duration-300"
499
+ style={{ left: `${currentWidth}px` }}
500
+ >
501
+ <div className="text-center max-w-sm space-y-2">
502
+ <p className="text-muted-foreground font-medium">Desktop — Subitens</p>
503
+ <p className="text-xs text-muted-foreground">
504
+ Itens com subitens exibem um botão{' '}
505
+ <code className="bg-muted px-1 rounded text-xs">ChevronRight</code> ao final da
506
+ linha. Clique nele para abrir o dropdown de rotas filhas.
507
+ </p>
508
+ <p className="text-xs text-muted-foreground mt-2">
509
+ Use o botão de toggle para alternar entre expandido e recolhido.
510
+ </p>
511
+ </div>
512
+ </div>
513
+ </div>
514
+ );
515
+ },
516
+ };
517
+
518
+ /**
519
+ * Mobile view with sub-items.
520
+ * Simulates a narrow viewport (375 px) so the sidebar renders in its
521
+ * full-screen overlay mode. When expanded the sidebar covers the entire
522
+ * container; clicking a nav item (or the back-arrow toggle) closes it.
523
+ *
524
+ * Sub-item dropdowns behave identically to desktop — the ChevronRight
525
+ * button opens a `DropdownMenuContent` positioned to the right.
526
+ * This story exists so the mobile layout can be inspected and adjusted.
527
+ */
528
+ export const WithSubitemsMobile: Story = {
529
+ name: 'With Sub-items (Mobile)',
530
+ parameters: {
531
+ viewport: { defaultViewport: 'mobile1' },
532
+ },
533
+ args: {
534
+ variant: 'default',
535
+ expanded: false,
536
+ user: { name: 'Admin', email: 'admin@example.com' },
537
+ onLogout: () => console.log('Logged out'),
538
+ location: { pathname: '/dashboard' },
539
+ navigationGroups: [
540
+ {
541
+ id: 'main',
542
+ label: 'Principal',
543
+ items: [
544
+ { path: '/home', label: 'Início', icon: Home },
545
+ {
546
+ path: '/dashboard',
547
+ label: 'Dashboard',
548
+ icon: BarChart,
549
+ children: [
550
+ { path: '/dashboard/overview', label: 'Visão Geral', icon: BarChart },
551
+ { path: '/dashboard/reports', label: 'Relatórios', icon: FileEdit },
552
+ { path: '/dashboard/analytics', label: 'Analytics', icon: Map },
553
+ ],
554
+ },
555
+ ],
556
+ },
557
+ {
558
+ id: 'admin',
559
+ label: 'Administração',
560
+ items: [
561
+ {
562
+ path: '/users',
563
+ label: 'Usuários',
564
+ icon: Users,
565
+ children: [
566
+ { path: '/users/list', label: 'Lista de Usuários', icon: Users },
567
+ { path: '/users/roles', label: 'Perfis de Acesso', icon: Settings },
568
+ ],
569
+ },
570
+ { path: '/settings', label: 'Configurações', icon: Settings },
571
+ ],
572
+ },
573
+ ],
574
+ footer: {
575
+ showUser: true,
576
+ showSettings: false,
577
+ showLogout: true,
578
+ },
579
+ },
580
+ render: args => {
581
+ const [isExpanded, setIsExpanded] = useState(false);
582
+
583
+ return (
584
+ /* Constrain to ~375 px to reproduce a mobile viewport inside Storybook */
585
+ <div
586
+ className="relative bg-muted/20 overflow-hidden border rounded-[var(--radius-lg)]"
587
+ style={{ width: '375px', height: '100vh', margin: '0 auto' }}
588
+ >
589
+ {/* Page content visible when sidebar is closed */}
590
+ {!isExpanded && (
591
+ <div className="h-full flex flex-col">
592
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border bg-background">
593
+ <button
594
+ onClick={() => setIsExpanded(true)}
595
+ className="p-2 rounded-md hover:bg-accent transition-colors"
596
+ aria-label="Abrir menu"
597
+ >
598
+ <Menu className="w-5 h-5" />
599
+ </button>
600
+ <span className="font-semibold text-sm">Minha Aplicação</span>
601
+ </div>
602
+ <div className="flex-1 flex items-center justify-center p-8">
603
+ <div className="text-center space-y-2">
604
+ <p className="text-muted-foreground font-medium">Mobile — Subitens</p>
605
+ <p className="text-xs text-muted-foreground">
606
+ Clique no ícone de menu (☰) no topo para abrir a sidebar em modo
607
+ tela-cheia. Itens com subitens mostram um{' '}
608
+ <code className="bg-muted px-1 rounded text-xs">ChevronRight</code> ao final
609
+ da linha.
610
+ </p>
611
+ </div>
612
+ </div>
613
+ </div>
614
+ )}
615
+
616
+ <Sidebar
617
+ {...args}
618
+ expanded={isExpanded}
619
+ onToggle={() => setIsExpanded(prev => !prev)}
620
+ width={375}
621
+ navigate={() => setIsExpanded(false)}
622
+ />
623
+ </div>
624
+ );
625
+ },
626
+ };
627
+
427
628
  /**
428
629
  * Compound Component API — demonstrates using Sidebar.Root + sub-components
429
630
  * for full layout control. This is the recommended pattern for advanced
@@ -334,13 +334,26 @@ function SidebarNav({
334
334
  routes?: RouteConfig[];
335
335
  variant?: 'default' | 'assistant';
336
336
  }) {
337
- const { expanded, navigate, location, onToggle } = useSidebarContext();
337
+ const { expanded, isMobileViewport, navigate, location, onToggle } = useSidebarContext();
338
338
  const { t } = useTranslation();
339
339
  const navRef = useRef<HTMLDivElement>(null);
340
340
  const [localActiveItem, setLocalActiveItem] = useState<string | null>(null);
341
341
  const [hasOverflow, setHasOverflow] = useState(false);
342
342
  const [visibleItems, setVisibleItems] = useState<NavigationItem[]>([]);
343
343
  const [overflowItems, setOverflowItems] = useState<NavigationItem[]>([]);
344
+ const [openSubmenus, setOpenSubmenus] = useState<Set<string>>(new Set());
345
+
346
+ const toggleSubmenu = (path: string) => {
347
+ setOpenSubmenus(prev => {
348
+ const next = new Set(prev);
349
+ if (next.has(path)) {
350
+ next.delete(path);
351
+ } else {
352
+ next.add(path);
353
+ }
354
+ return next;
355
+ });
356
+ };
344
357
 
345
358
  const labelTranslations = useMemo<Record<string, string>>(
346
359
  () => ({
@@ -492,6 +505,78 @@ function SidebarNav({
492
505
  );
493
506
  }
494
507
 
508
+ // Mobile: accordion inline — subitens abrem abaixo do item pai
509
+ if (isMobileViewport && hasChildren) {
510
+ const isOpen = openSubmenus.has(item.path);
511
+ return (
512
+ <div key={item.path}>
513
+ <div
514
+ className={cn(
515
+ 'group/item flex items-center w-full h-10 rounded-[var(--radius-button)] transition-all duration-200',
516
+ activeClass
517
+ )}
518
+ >
519
+ <button
520
+ onClick={() => handleNavigate(item.path)}
521
+ className="flex items-center gap-3 px-3 flex-1 h-full min-w-0 text-left"
522
+ >
523
+ {Icon && <Icon className="w-5 h-5 flex-shrink-0" />}
524
+ <span className="truncate flex-1">{item.label}</span>
525
+ </button>
526
+ <button
527
+ onClick={e => {
528
+ e.stopPropagation();
529
+ toggleSubmenu(item.path);
530
+ }}
531
+ className="h-full px-2 pr-2.5 flex items-center justify-center text-sidebar-foreground/40 hover:text-sidebar-foreground transition-colors"
532
+ aria-label={t('sidebar.submenu', { label: item.label })}
533
+ aria-expanded={isOpen}
534
+ >
535
+ <ChevronRight
536
+ className={cn('w-4 h-4 transition-transform duration-200', isOpen && 'rotate-90')}
537
+ />
538
+ </button>
539
+ </div>
540
+ <AnimatePresence initial={false}>
541
+ {isOpen && (
542
+ <motion.div
543
+ initial={{ height: 0, opacity: 0 }}
544
+ animate={{ height: 'auto', opacity: 1 }}
545
+ exit={{ height: 0, opacity: 0 }}
546
+ transition={{ duration: 0.2 }}
547
+ className="overflow-hidden"
548
+ >
549
+ <div className="ml-4 mt-0.5 mb-0.5 space-y-0.5">
550
+ {item.children!.map(child => {
551
+ const ChildIcon = child.icon;
552
+ const isChildActive =
553
+ location.pathname === child.path ||
554
+ location.pathname.startsWith(child.path + '/');
555
+ return (
556
+ <button
557
+ key={child.path}
558
+ onClick={() => handleNavigate(child.path)}
559
+ className={cn(
560
+ 'w-full h-9 flex items-center gap-2.5 px-3 rounded-[var(--radius-button)] transition-all duration-200 text-left',
561
+ isChildActive
562
+ ? 'bg-sidebar-foreground/15 text-sidebar-foreground'
563
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground'
564
+ )}
565
+ >
566
+ {ChildIcon && <ChildIcon className="h-4 w-4 flex-shrink-0" />}
567
+ <span className="truncate text-sm">{child.label}</span>
568
+ </button>
569
+ );
570
+ })}
571
+ </div>
572
+ </motion.div>
573
+ )}
574
+ </AnimatePresence>
575
+ </div>
576
+ );
577
+ }
578
+
579
+ // Desktop: dropdown lateral (comportamento original)
495
580
  return (
496
581
  <div
497
582
  key={item.path}
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { TemplatePage } from './TemplatePage';
3
3
  import { MemoryRouter } from 'react-router-dom';
4
4
  import { LayoutProvider } from '../../../contexts/LayoutContext';
5
+ import { AuthProvider } from '../../../contexts/AuthContext';
5
6
  import React from 'react';
6
7
 
7
8
  const meta: Meta<typeof TemplatePage> = {
@@ -10,9 +11,11 @@ const meta: Meta<typeof TemplatePage> = {
10
11
  decorators: [
11
12
  Story => (
12
13
  <MemoryRouter initialEntries={['/template']}>
13
- <LayoutProvider>
14
- <Story />
15
- </LayoutProvider>
14
+ <AuthProvider>
15
+ <LayoutProvider>
16
+ <Story />
17
+ </LayoutProvider>
18
+ </AuthProvider>
16
19
  </MemoryRouter>
17
20
  ),
18
21
  ],
@@ -24,16 +27,6 @@ const meta: Meta<typeof TemplatePage> = {
24
27
  export default meta;
25
28
  type Story = StoryObj<typeof TemplatePage>;
26
29
 
27
- export const Default: Story = {
28
- args: {
29
- user: { email: 'ariel@xertica.com' },
30
- onLogout: () => {},
31
- },
32
- };
30
+ export const Default: Story = {};
33
31
 
34
- export const NoUser: Story = {
35
- args: {
36
- user: null,
37
- onLogout: () => {},
38
- },
39
- };
32
+ export const NoUser: Story = {};
@@ -56,7 +56,7 @@ describe('Chart Components', () => {
56
56
  });
57
57
 
58
58
  it('renders ChartTooltipContent correctly', () => {
59
- const payload = [{ name: 'item1', value: 10, fill: '#000', dataKey: 'label1' }];
59
+ const payload = [{ name: 'item1', value: 10, fill: '#000', dataKey: 'label1', graphicalItemId: 'item1' }];
60
60
 
61
61
  // Test the content component directly since Tooltip is tricky
62
62
  render(