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.
- package/CHANGELOG.md +49 -0
- package/README.md +28 -2
- package/bin/generate-tokens.ts +9 -9
- package/components/brand/xertica-provider/XerticaProvider.tsx +4 -1
- package/components/layout/sidebar/sidebar.stories.tsx +201 -0
- package/components/layout/sidebar/sidebar.tsx +86 -1
- package/components/pages/template-page/TemplatePage.stories.tsx +8 -15
- package/components/ui/chart/chart.test.tsx +1 -1
- package/components/ui/chart/chart.tsx +13 -6
- package/contexts/BrandColorsContext.tsx +39 -8
- package/contexts/theme-data.ts +51 -0
- package/dist/AssistantChart-BZTPJ5dP.cjs +3551 -0
- package/dist/AssistantChart-DMJJ_Amf.js +3383 -0
- package/dist/BrandColorsContext-BMRJ04Wf.js +718 -0
- package/dist/BrandColorsContext-BwY-b6M4.cjs +725 -0
- package/dist/VerifyEmailPage-CGIwmWrm.js +3296 -0
- package/dist/VerifyEmailPage-CpqqpLpo.cjs +3305 -0
- package/dist/XerticaProvider-CeS5G_n5.cjs +45 -0
- package/dist/XerticaProvider-ra2NciRq.js +43 -0
- package/dist/assistant.cjs.js +1 -1
- package/dist/assistant.es.js +1 -1
- package/dist/brand.cjs.js +1 -1
- package/dist/brand.es.js +1 -1
- package/dist/cli.js +45 -9
- package/dist/components/brand/xertica-provider/XerticaProvider.d.ts +3 -1
- package/dist/components/ui/chart/chart.d.ts +12 -3
- package/dist/contexts/theme-data.d.ts +4 -0
- package/dist/hooks.cjs.js +1 -1
- package/dist/hooks.es.js +1 -1
- package/dist/index.cjs.js +6 -6
- package/dist/index.es.js +6 -6
- package/dist/layout.cjs.js +1 -1
- package/dist/layout.es.js +1 -1
- package/dist/pages.cjs.js +1 -1
- package/dist/pages.es.js +1 -1
- package/dist/rich-text-editor-B2CKz7nx.cjs +2903 -0
- package/dist/rich-text-editor-DloeW0wc.js +2832 -0
- package/dist/sidebar-0ocFLSks.js +878 -0
- package/dist/sidebar-CeTMuzOx.cjs +881 -0
- package/dist/ui.cjs.js +2 -2
- package/dist/ui.es.js +2 -2
- package/dist/xertica-assistant-CyikE3N_.js +2173 -0
- package/dist/xertica-assistant-QFUnv5I2.cjs +2180 -0
- package/dist/xertica-ui.css +1 -1
- package/docs/components/sidebar.md +19 -2
- package/llms-compact.txt +30 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/styles/xertica/tokens.css +9 -9
- package/templates/guidelines/Guidelines.md +643 -355
- package/templates/package.json +2 -2
- 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
|
-
[](https://www.npmjs.com/package/xertica-ui)
|
|
6
6
|
[](./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`:
|
package/bin/generate-tokens.ts
CHANGED
|
@@ -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(
|
|
181
|
+
--background: ${rgba(colors.darkBackground)};
|
|
182
182
|
--foreground: rgba(250, 250, 250, 1);
|
|
183
183
|
|
|
184
|
-
--card: rgba(
|
|
184
|
+
--card: ${rgba(colors.darkCard)};
|
|
185
185
|
--card-foreground: rgba(250, 250, 250, 1);
|
|
186
186
|
|
|
187
|
-
--popover: rgba(
|
|
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(
|
|
194
|
+
--secondary: ${rgba(colors.darkMuted)};
|
|
195
195
|
--secondary-foreground: rgba(250, 250, 250, 1);
|
|
196
196
|
|
|
197
|
-
--muted: rgba(
|
|
197
|
+
--muted: ${rgba(colors.darkMuted)};
|
|
198
198
|
--muted-foreground: rgba(161, 161, 170, 1);
|
|
199
199
|
|
|
200
|
-
--accent: rgba(
|
|
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(
|
|
215
|
-
--input: rgba(
|
|
216
|
-
--input-background: rgba(
|
|
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
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
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<
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
214
|
+
document.head.appendChild(styleEl);
|
|
215
|
+
} else {
|
|
216
|
+
document.head.appendChild(styleEl);
|
|
191
217
|
}
|
|
192
|
-
|
|
193
|
-
|
|
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
|