xertica-ui 2.4.1 → 2.5.0

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 (52) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +28 -2
  3. package/bin/generate-tokens.ts +9 -9
  4. package/components/brand/xertica-provider/XerticaProvider.tsx +4 -1
  5. package/components/layout/sidebar/sidebar.stories.tsx +201 -0
  6. package/components/layout/sidebar/sidebar.tsx +86 -1
  7. package/components/pages/template-page/TemplatePage.stories.tsx +8 -15
  8. package/components/ui/chart/chart.test.tsx +1 -1
  9. package/components/ui/chart/chart.tsx +13 -6
  10. package/contexts/BrandColorsContext.tsx +39 -8
  11. package/contexts/theme-data.ts +51 -0
  12. package/dist/AssistantChart-BZTPJ5dP.cjs +3551 -0
  13. package/dist/AssistantChart-DMJJ_Amf.js +3383 -0
  14. package/dist/BrandColorsContext-BMRJ04Wf.js +718 -0
  15. package/dist/BrandColorsContext-BwY-b6M4.cjs +725 -0
  16. package/dist/VerifyEmailPage-CGIwmWrm.js +3296 -0
  17. package/dist/VerifyEmailPage-CpqqpLpo.cjs +3305 -0
  18. package/dist/XerticaProvider-CeS5G_n5.cjs +45 -0
  19. package/dist/XerticaProvider-ra2NciRq.js +43 -0
  20. package/dist/assistant.cjs.js +1 -1
  21. package/dist/assistant.es.js +1 -1
  22. package/dist/brand.cjs.js +1 -1
  23. package/dist/brand.es.js +1 -1
  24. package/dist/cli.js +45 -9
  25. package/dist/components/brand/xertica-provider/XerticaProvider.d.ts +3 -1
  26. package/dist/components/ui/chart/chart.d.ts +12 -3
  27. package/dist/contexts/theme-data.d.ts +4 -0
  28. package/dist/hooks.cjs.js +1 -1
  29. package/dist/hooks.es.js +1 -1
  30. package/dist/index.cjs.js +6 -6
  31. package/dist/index.es.js +6 -6
  32. package/dist/layout.cjs.js +1 -1
  33. package/dist/layout.es.js +1 -1
  34. package/dist/pages.cjs.js +1 -1
  35. package/dist/pages.es.js +1 -1
  36. package/dist/rich-text-editor-B2CKz7nx.cjs +2903 -0
  37. package/dist/rich-text-editor-DloeW0wc.js +2832 -0
  38. package/dist/sidebar-0ocFLSks.js +878 -0
  39. package/dist/sidebar-CeTMuzOx.cjs +881 -0
  40. package/dist/ui.cjs.js +2 -2
  41. package/dist/ui.es.js +2 -2
  42. package/dist/xertica-assistant-CyikE3N_.js +2173 -0
  43. package/dist/xertica-assistant-QFUnv5I2.cjs +2180 -0
  44. package/dist/xertica-ui.css +1 -1
  45. package/docs/components/sidebar.md +19 -2
  46. package/llms-compact.txt +30 -1
  47. package/llms.txt +1 -1
  48. package/package.json +2 -2
  49. package/styles/xertica/tokens.css +9 -9
  50. package/templates/guidelines/Guidelines.md +643 -355
  51. package/templates/package.json +2 -2
  52. package/templates/src/styles/xertica/tokens.css +9 -9
package/CHANGELOG.md CHANGED
@@ -11,6 +11,55 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
11
11
 
12
12
  ---
13
13
 
14
+ ## [2.5.0] — 2026-06-22
15
+
16
+ ### Added
17
+
18
+ - **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.
19
+
20
+ - **Storybook — Stories de sidebar com subitens (desktop e mobile)** — adicionadas duas novas stories na documentação da `Sidebar`:
21
+ - `WithSubitemsDesktop` — sidebar iniciada expandida mostrando o botão `ChevronRight` nos itens com filhos e o dropdown lateral ao clicar.
22
+ - `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.
23
+
24
+ - **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:
25
+ - **primary / xertica-original**: fundo `#05050d` (azul-índigo muito sutil)
26
+ - **blue**: fundo `#03050f` (azul marinho profundo)
27
+ - **violet**: fundo `#07040f` (violeta escuro)
28
+ - **rose**: fundo `#0f0305` (rosa/vermelho escuro)
29
+ - **emerald**: fundo `#030f08` (verde escuro)
30
+ - **amber**: fundo `#0f0a03` (âmbar escuro)
31
+ - **orange**: fundo `#0f0703` (laranja escuro)
32
+ - **zinc / slate**: mantêm o azul-índigo sutil (alinhado com o tema default)
33
+
34
+ - **`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.
35
+
36
+ - **`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`.
37
+
38
+ - **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.).
39
+
40
+ ### Fixed
41
+
42
+ - **`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:
43
+ 1. `prepend` → `appendChild` para que o style injetado sempre apareça por último no `<head>` e vença na cascata de mesma especificidade.
44
+ 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.
45
+ 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.
46
+
47
+ - **`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.
48
+
49
+ - **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).
50
+
51
+ ### Changed
52
+
53
+ - **`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>`.
54
+
55
+ - **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`.
56
+
57
+ - **`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.
58
+
59
+ - **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).
60
+
61
+ ---
62
+
14
63
  ## [2.4.1] — 2026-06-17
15
64
 
16
65
  ### 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.0-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,36 @@ 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 color themes, each with a hue-tinted dark mode:
357
+
358
+ | ID | Name | Dark background |
359
+ | ------------------- | --------------- | --------------- |
360
+ | `xertica-original` | Xertica | `#05050d` (indigo) |
361
+ | `zinc` | Zinc | `#05050d` (indigo) |
362
+ | `slate` | Slate | `#05050d` (indigo) |
363
+ | `blue` | Blue | `#03050f` (blue) |
364
+ | `violet` | Violet | `#07040f` (violet) |
365
+ | `rose` | Rose | `#0f0305` (rose) |
366
+ | `emerald` | Emerald | `#030f08` (green) |
367
+ | `amber` | Amber | `#0f0a03` (amber) |
368
+ | `orange` | Orange | `#0f0703` (orange) |
369
+
370
+ Select a theme via `XerticaProvider`:
371
+
372
+ ```tsx
373
+ // Full theme preset — correct primary, sidebar, charts AND dark surfaces
374
+ <XerticaProvider defaultColorTheme="blue">
375
+
376
+ // Custom hex primary only (dark surfaces stay neutral)
377
+ <XerticaProvider primaryColor="#7C3AED">
378
+ ```
379
+
354
380
  ### Mobile Content Padding
355
381
 
356
382
  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`:
@@ -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);
@@ -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(
@@ -212,13 +212,19 @@ function ChartTooltipContent({
212
212
  color,
213
213
  nameKey,
214
214
  labelKey,
215
- }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
216
- React.ComponentProps<'div'> & {
215
+ }: React.ComponentProps<'div'> & {
216
+ active?: boolean;
217
+ payload?: RechartsPrimitive.TooltipPayload;
218
+ label?: React.ReactNode;
219
+ labelFormatter?: (label: React.ReactNode, payload: RechartsPrimitive.TooltipPayload) => React.ReactNode;
220
+ labelClassName?: string;
217
221
  hideLabel?: boolean;
218
222
  hideIndicator?: boolean;
219
223
  indicator?: 'line' | 'dot' | 'dashed';
220
224
  nameKey?: string;
221
225
  labelKey?: string;
226
+ color?: string;
227
+ formatter?: RechartsPrimitive.DefaultTooltipContentProps['formatter'];
222
228
  }) {
223
229
  const { config } = useChart();
224
230
 
@@ -270,7 +276,7 @@ function ChartTooltipContent({
270
276
 
271
277
  return (
272
278
  <div
273
- key={item.dataKey}
279
+ key={`${item.dataKey ?? index}`}
274
280
  className={cn(
275
281
  '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
276
282
  indicator === 'dot' && 'items-center'
@@ -340,8 +346,9 @@ function ChartLegendContent({
340
346
  payload,
341
347
  verticalAlign = 'bottom',
342
348
  nameKey,
343
- }: React.ComponentProps<'div'> &
344
- Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
349
+ }: React.ComponentProps<'div'> & {
350
+ payload?: RechartsPrimitive.LegendPayload[];
351
+ verticalAlign?: 'top' | 'middle' | 'bottom';
345
352
  hideIcon?: boolean;
346
353
  nameKey?: string;
347
354
  }) {
@@ -1460,7 +1467,6 @@ function DonutBreakdownChart({
1460
1467
  innerRadius={innerRadius}
1461
1468
  outerRadius={outerRadius}
1462
1469
  paddingAngle={3}
1463
- activeIndex={activeIndex}
1464
1470
  onMouseEnter={(_, index) => setActiveIndex(index)}
1465
1471
  isAnimationActive
1466
1472
  animationDuration={600}
@@ -1822,6 +1828,7 @@ function PieMetricChart({
1822
1828
  label={
1823
1829
  showLabels
1824
1830
  ? ({ cx, cy, midAngle, innerRadius: ir, outerRadius: or, percent }) => {
1831
+ if (midAngle == null || percent == null) return null;
1825
1832
  const RADIAN = Math.PI / 180;
1826
1833
  const radius = Number(ir) + (Number(or) - Number(ir)) * 1.35;
1827
1834
  const x = Number(cx) + radius * Math.cos(-midAngle * RADIAN);
@@ -178,20 +178,51 @@ export const BrandColorsProvider: React.FC<BrandColorsProviderProps> = ({
178
178
  `--gradient-diagonal: linear-gradient(135deg, ${gradientStart} 0%, ${gradientEnd} 100%)`
179
179
  );
180
180
 
181
- // Inject into head as a style tag rather than inline on HTML element
182
- // Usamos prepend em vez de appendChild para garantir que a injeção seja
183
- // lida ANTES de qualquer arquivo CSS estático (como tokens.css), garantindo que
184
- // as modificações feitas pelo desenvolvedor em :root tenham prioridade natural de cascata.
181
+ // 7. Surface colors for dark mode (tinted toward the theme hue)
182
+ // Only applied when dark surface tokens exist on the theme.
183
+ const darkSurfaceVars: string[] = [];
184
+ if (colors.darkBackground) {
185
+ darkSurfaceVars.push(`--background: ${colors.darkBackground}`);
186
+ darkSurfaceVars.push(`--card: ${colors.darkCard}`);
187
+ darkSurfaceVars.push(`--popover: ${colors.darkCard}`);
188
+ darkSurfaceVars.push(`--secondary: ${colors.darkMuted}`);
189
+ darkSurfaceVars.push(`--muted: ${colors.darkMuted}`);
190
+ darkSurfaceVars.push(`--accent: ${colors.darkMuted}`);
191
+ darkSurfaceVars.push(`--border: ${colors.darkBorder}`);
192
+ darkSurfaceVars.push(`--input: ${colors.darkMuted}`);
193
+ darkSurfaceVars.push(`--input-background: ${colors.darkMuted}`);
194
+ }
195
+
196
+ // Apply primary-related tokens directly as inline styles on <html>.
197
+ // Inline styles have the highest possible specificity and always win over
198
+ // any stylesheet rule, regardless of order or selector specificity.
199
+ cssVars.forEach(declaration => {
200
+ const eqIdx = declaration.indexOf(':');
201
+ if (eqIdx === -1) return;
202
+ const prop = declaration.substring(0, eqIdx).trim();
203
+ const val = declaration.substring(eqIdx + 1).trim();
204
+ root.style.setProperty(prop, val); // `root` is document.documentElement, declared at line 79
205
+ });
206
+
207
+ // Surface tokens (background, card, muted, etc.) are injected via a style
208
+ // tag appended last in <head> so they win the cascade for their selectors.
185
209
  const styleId = 'xertica-brand-colors-injection';
186
210
  let styleEl = document.getElementById(styleId);
187
211
  if (!styleEl) {
188
212
  styleEl = document.createElement('style');
189
213
  styleEl.id = styleId;
190
- document.head.prepend(styleEl);
214
+ document.head.appendChild(styleEl);
215
+ } else {
216
+ document.head.appendChild(styleEl);
191
217
  }
192
- styleEl.innerHTML = `:root {
193
- ${cssVars.join(';\n ')};
194
- }`;
218
+
219
+ const darkSurfaceBlock =
220
+ darkSurfaceVars.length > 0
221
+ ? `\n:root[data-mode='dark'], .dark {\n ${darkSurfaceVars.join(';\n ')};\n}`
222
+ : '';
223
+
224
+ // The style tag handles only the surface tokens that vary per theme in dark mode.
225
+ styleEl.innerHTML = darkSurfaceBlock.trim();
195
226
  }, [colors, currentTheme, radius, useCustomTokens]);
196
227
 
197
228
  // Aplicar cores quando elas mudarem