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.
- package/CHANGELOG.md +71 -0
- package/README.md +69 -2
- package/bin/cli.ts +14 -2
- package/bin/generate-tokens.ts +12 -12
- 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 +59 -14
- 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,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
|
-
[](https://www.npmjs.com/package/xertica-ui)
|
|
6
6
|
[](./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:
|
|
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
|
}
|
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);
|
|
@@ -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)};
|
|
236
|
-
--sidebar-accent: rgba(
|
|
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(
|
|
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
|
-
<
|
|
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(
|