xertica-ui 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -2
- package/README.md +33 -22
- package/bin/cli.ts +136 -47
- package/bin/language-config.ts +5 -8
- package/components/assistant/modern-chat-input/ModernChatInput.tsx +17 -7
- package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +1 -3
- package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +13 -3
- package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +10 -6
- package/components/assistant/xertica-assistant/xertica-assistant.tsx +1 -3
- package/components/blocks/card-patterns/FeatureCardSkeleton.tsx +1 -6
- package/components/blocks/card-patterns/ProfileCard.tsx +1 -3
- package/components/blocks/card-patterns/ProjectCardSkeleton.tsx +1 -6
- package/components/brand/language-selector/language-selector.stories.tsx +1 -4
- package/components/brand/theme-toggle/ThemeToggle.tsx +5 -1
- package/components/brand/xertica-provider/XerticaProvider.tsx +1 -4
- package/components/index.ts +1 -5
- package/components/layout/sidebar/sidebar.tsx +9 -3
- package/components/media/audio-player/AudioPlayer.tsx +4 -2
- package/components/pages/forgot-password-page/ForgotPasswordPage.tsx +188 -188
- package/components/pages/home-content/HomeContent.tsx +55 -55
- package/components/pages/home-page/HomePage.tsx +5 -1
- package/components/pages/login-page/LoginPage.tsx +4 -2
- package/components/pages/reset-password-page/ResetPasswordPage.tsx +7 -3
- package/components/pages/template-content/TemplateContent.tsx +268 -149
- package/components/pages/verify-email-page/VerifyEmailPage.tsx +9 -9
- package/components/shared/error-boundary.stories.tsx +114 -132
- package/components/shared/error-boundary.tsx +150 -154
- package/components/shared/error-fallbacks.tsx +222 -226
- package/components/ui/stats-card/stats-card-skeleton.tsx +1 -3
- package/components/ui/stats-card/stats-card.stories.tsx +18 -0
- package/components/ui/stats-card/stats-card.tsx +18 -2
- package/components.json +512 -892
- package/contexts/AuthContext.tsx +121 -118
- package/contexts/LanguageContext.tsx +1 -2
- package/dist/AssistantChart-BKVtGUKF.js +3383 -0
- package/dist/AssistantChart-WeycT5Pd.cjs +3551 -0
- package/dist/VerifyEmailPage-Bp1XXl3H.cjs +3305 -0
- package/dist/VerifyEmailPage-DGhuIqkb.js +3296 -0
- package/dist/XerticaProvider-BErr83Bg.js +42 -0
- package/dist/XerticaProvider-CwOkHxiT.cjs +44 -0
- package/dist/XerticaXLogo-BX3ueACh.js +255 -0
- package/dist/XerticaXLogo-qBPhwK3g.cjs +260 -0
- package/dist/assistant.cjs.js +1 -1
- package/dist/assistant.es.js +1 -1
- package/dist/brand.cjs.js +2 -2
- package/dist/brand.es.js +2 -2
- package/dist/cli.js +90 -37
- package/dist/components/brand/theme-toggle/ThemeToggle.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/ui/stats-card/stats-card.d.ts +10 -0
- 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/sidebar-B4ZWaMrE.js +792 -0
- package/dist/sidebar-BS1p2V7t.cjs +795 -0
- package/dist/ui.cjs.js +1 -1
- package/dist/ui.es.js +1 -1
- package/dist/xertica-assistant-B1NaSFFj.js +2173 -0
- package/dist/xertica-assistant-CIaUlbIt.cjs +2180 -0
- package/dist/xertica-ui.css +1 -1
- package/docs/architecture-improvements.md +5 -5
- package/docs/architecture.md +16 -10
- package/docs/components/card-patterns.md +19 -17
- package/docs/components/error-boundary.md +201 -191
- package/docs/components/hooks.md +15 -13
- package/docs/components/language-selector.md +20 -16
- package/docs/components/pages.md +323 -309
- package/docs/components/stats-card.md +20 -2
- package/docs/doc-audit.md +12 -11
- package/docs/getting-started.md +41 -28
- package/docs/guidelines.md +14 -12
- package/docs/i18n.md +61 -57
- package/docs/installation.md +268 -267
- package/docs/llms.md +17 -17
- package/docs/state-management.md +17 -17
- package/guidelines/Guidelines.md +17 -14
- package/llms-compact.txt +1 -1
- package/llms-full.txt +11553 -7133
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/styles/xertica/base.css +90 -84
- package/templates/CLAUDE.md +16 -1
- package/templates/guidelines/Guidelines.md +42 -18
- package/templates/package.json +3 -3
- package/templates/src/app/components/AuthGuard.tsx +131 -82
- package/templates/src/features/auth/ui/AuthPageShell.tsx +32 -32
- package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +1 -3
- package/templates/src/features/auth/ui/ResetPasswordContent.tsx +6 -2
- package/templates/src/features/auth/ui/VerifyEmailContent.tsx +2 -6
- package/templates/src/features/home/data/mock.ts +41 -35
- package/templates/src/features/home/ui/HomeContent.tsx +62 -64
- package/templates/src/features/template/ui/CrudTemplate.tsx +1 -4
- package/templates/src/features/template/ui/LoginTemplate.tsx +1 -1
- package/templates/src/features/template/ui/TemplateContent.tsx +28 -20
- package/templates/src/locales/en/pages/templates.json +17 -17
- package/templates/src/locales/es/pages/templates.json +17 -17
- package/templates/src/locales/pt-BR/pages/templates.json +17 -17
- package/templates/src/pages/AssistantPage.tsx +26 -20
- package/templates/src/pages/HomePage.tsx +5 -1
- package/templates/src/shared/error-boundary.tsx +150 -154
- package/templates/src/shared/error-fallbacks.tsx +222 -226
- package/templates/vite.config.ts +12 -9
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Xertica UI
|
|
2
2
|
|
|
3
|
-
> Enterprise-grade React design system — 97 components built on Tailwind CSS v4, Radix UI, and Lucide Icons. Designed to be consumed directly by AI coding agents (LLMs, Cursor, Claude Code, Copilot). v2.
|
|
3
|
+
> Enterprise-grade React design system — 97 components built on Tailwind CSS v4, Radix UI, and Lucide Icons. Designed to be consumed directly by AI coding agents (LLMs, Cursor, Claude Code, Copilot). v2.4.1
|
|
4
4
|
|
|
5
5
|
Xertica UI ships with full source, typed declarations, Storybook stories, and structured documentation inside `node_modules/xertica-ui/`. AI agents should read this file first, then follow links for component-specific detail.
|
|
6
6
|
|
package/package.json
CHANGED
package/styles/xertica/base.css
CHANGED
|
@@ -1,84 +1,90 @@
|
|
|
1
|
-
/* ============================================
|
|
2
|
-
🧱 BASE STYLES
|
|
3
|
-
============================================ */
|
|
4
|
-
|
|
5
|
-
@import 'tailwindcss';
|
|
6
|
-
@plugin "tailwindcss-animate";
|
|
7
|
-
|
|
8
|
-
@custom-variant dark (&:is(.dark *));
|
|
9
|
-
@custom-variant mobile (@media (max-width: 767px));
|
|
10
|
-
|
|
11
|
-
@layer base {
|
|
12
|
-
* {
|
|
13
|
-
@apply border-border outline-ring/50;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
font-size: var(--text-
|
|
33
|
-
font-weight: var(--font-weight-
|
|
34
|
-
line-height: 1.2;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
font-size: var(--text-
|
|
39
|
-
font-weight: var(--font-weight-semibold);
|
|
40
|
-
line-height: 1.2;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
font-size: var(--text-
|
|
45
|
-
font-weight: var(--font-weight-semibold);
|
|
46
|
-
line-height: 1.2;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
font-size: var(--text-
|
|
51
|
-
font-weight: var(--font-weight-
|
|
52
|
-
line-height: 1.
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
font-size: var(--text-
|
|
57
|
-
font-weight: var(--font-weight-
|
|
58
|
-
line-height: 1.
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
font-size: var(--text-
|
|
63
|
-
font-weight: var(--font-weight-
|
|
64
|
-
line-height: 1.
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
font-size: var(--text-small);
|
|
69
|
-
font-weight: var(--font-weight-
|
|
70
|
-
line-height: 1.4;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
/* ============================================
|
|
2
|
+
🧱 BASE STYLES
|
|
3
|
+
============================================ */
|
|
4
|
+
|
|
5
|
+
@import 'tailwindcss';
|
|
6
|
+
@plugin "tailwindcss-animate";
|
|
7
|
+
|
|
8
|
+
@custom-variant dark (&:is(.dark *));
|
|
9
|
+
@custom-variant mobile (@media (max-width: 767px));
|
|
10
|
+
|
|
11
|
+
@layer base {
|
|
12
|
+
* {
|
|
13
|
+
@apply border-border outline-ring/50;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
html,
|
|
17
|
+
body {
|
|
18
|
+
height: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
@apply bg-background text-foreground;
|
|
24
|
+
font-family: 'Roboto', sans-serif;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
html {
|
|
28
|
+
font-size: var(--font-size);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
h1 {
|
|
32
|
+
font-size: var(--text-h1);
|
|
33
|
+
font-weight: var(--font-weight-extrabold);
|
|
34
|
+
line-height: 1.2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
h2 {
|
|
38
|
+
font-size: var(--text-h2);
|
|
39
|
+
font-weight: var(--font-weight-semibold);
|
|
40
|
+
line-height: 1.2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
h3 {
|
|
44
|
+
font-size: var(--text-h3);
|
|
45
|
+
font-weight: var(--font-weight-semibold);
|
|
46
|
+
line-height: 1.2;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
h4 {
|
|
50
|
+
font-size: var(--text-h4);
|
|
51
|
+
font-weight: var(--font-weight-semibold);
|
|
52
|
+
line-height: 1.2;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
p {
|
|
56
|
+
font-size: var(--text-p);
|
|
57
|
+
font-weight: var(--font-weight-regular);
|
|
58
|
+
line-height: 1.5;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
label {
|
|
62
|
+
font-size: var(--text-label);
|
|
63
|
+
font-weight: var(--font-weight-semibold);
|
|
64
|
+
line-height: 1.3;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
button {
|
|
68
|
+
font-size: var(--text-small);
|
|
69
|
+
font-weight: var(--font-weight-medium);
|
|
70
|
+
line-height: 1.4;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
input {
|
|
74
|
+
font-size: var(--text-small);
|
|
75
|
+
font-weight: var(--font-weight-regular);
|
|
76
|
+
line-height: 1.4;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@layer utilities {
|
|
81
|
+
/* Global mobile content padding utility class.
|
|
82
|
+
Add class="content-area" to any wrapper to receive the mobile padding.
|
|
83
|
+
Change --mobile-content-padding in tokens.css to adjust globally. */
|
|
84
|
+
@media (max-width: 767px) {
|
|
85
|
+
.content-area {
|
|
86
|
+
padding-left: var(--mobile-content-padding);
|
|
87
|
+
padding-right: var(--mobile-content-padding);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -23,6 +23,7 @@ This project follows **FSD (Feature-Sliced Design)** layered architecture combin
|
|
|
23
23
|
- **`src/pages/`**: Thin route wrappers composing `AppLayout` + feature content. No logic or API calls here.
|
|
24
24
|
|
|
25
25
|
### Adding a New Route
|
|
26
|
+
|
|
26
27
|
1. **Create the feature content**: `src/features/<name>/ui/<NameContent>.tsx`
|
|
27
28
|
2. **Export from the barrel**: `src/features/<name>/index.ts`
|
|
28
29
|
3. **Create the page**: `src/pages/<NamePage>.tsx` (thin AppLayout wrapper)
|
|
@@ -34,6 +35,7 @@ This project follows **FSD (Feature-Sliced Design)** layered architecture combin
|
|
|
34
35
|
## Import Rules
|
|
35
36
|
|
|
36
37
|
Always use the correct `xertica-ui` subpath:
|
|
38
|
+
|
|
37
39
|
- **UI Primitives**: `xertica-ui/ui` (e.g. `Button`, `Card`, `Input`, `Badge`, `Table`, `Dialog`, `Select`)
|
|
38
40
|
- **Providers & Brand**: `xertica-ui/brand` (e.g. `XerticaProvider`, `XerticaLogo`, `ThemeToggle`, `LanguageSelector`)
|
|
39
41
|
- **Navigation Shell**: `xertica-ui/layout` (e.g. `Sidebar`, `Header`)
|
|
@@ -49,7 +51,9 @@ Icons always come from `lucide-react` — never from `xertica-ui`.
|
|
|
49
51
|
## Non-Negotiable Coding Rules
|
|
50
52
|
|
|
51
53
|
### HTML & Radix Elements
|
|
54
|
+
|
|
52
55
|
Never use native HTML interactive elements where design system components exist:
|
|
56
|
+
|
|
53
57
|
- `<Button>` instead of `<button>`
|
|
54
58
|
- `<Input>` instead of `<input>`
|
|
55
59
|
- `<Select>` instead of `<select>`
|
|
@@ -58,19 +62,24 @@ Never use native HTML interactive elements where design system components exist:
|
|
|
58
62
|
- `<ScrollArea>` instead of custom scrollbars
|
|
59
63
|
|
|
60
64
|
### Color & Border Radius Styling
|
|
65
|
+
|
|
61
66
|
- **Never use raw color values** (`#hex`, `rgb()`, `hsl()`) or inline styles for theming.
|
|
62
67
|
- **Never use standard Tailwind color classes for semantic/status contexts** (e.g., do not use `bg-red-500` or `text-green-500` for status/feedback/errors). Use semantic tokens: `bg-destructive`/`text-destructive` for errors, `bg-success` for success, `bg-warning` for warnings, `bg-info` for info.
|
|
63
68
|
- **Tailwind colors** (`bg-blue-500`, `text-gray-700`) are allowed **only** for layout, spacing, and general non-semantic UI where no semantic token applies.
|
|
64
69
|
- **Borders**: Always use `rounded-[var(--radius)]` — never use fixed radius classes like `rounded-lg` or `rounded-xl`.
|
|
65
70
|
|
|
66
71
|
### Layout State
|
|
72
|
+
|
|
67
73
|
Never hardcode sidebar or layout widths (e.g. `256px`). Read them dynamically:
|
|
74
|
+
|
|
68
75
|
- `const { sidebarExpanded, sidebarWidth } = useLayout()`
|
|
69
76
|
- For components that might render outside the layout provider:
|
|
70
77
|
`const layout = useOptionalLayout(); const fallbackSidebarWidth = layout?.sidebarWidth ?? 80;`
|
|
71
78
|
|
|
72
79
|
### Confirm Destructive Actions
|
|
80
|
+
|
|
73
81
|
Always wrap destructive actions in `<AlertDialog>` for confirmation:
|
|
82
|
+
|
|
74
83
|
```tsx
|
|
75
84
|
<AlertDialog>
|
|
76
85
|
<AlertDialogTrigger asChild>
|
|
@@ -90,11 +99,14 @@ Always wrap destructive actions in `<AlertDialog>` for confirmation:
|
|
|
90
99
|
```
|
|
91
100
|
|
|
92
101
|
### Toast Notifications
|
|
102
|
+
|
|
93
103
|
Always use `toast` from `sonner` and translate messages via `t()`:
|
|
104
|
+
|
|
94
105
|
```tsx
|
|
95
106
|
toast.success(t('common.saveSuccess'));
|
|
96
107
|
toast.error(t('errors.somethingWentWrong'));
|
|
97
108
|
```
|
|
109
|
+
|
|
98
110
|
Never render `<Toaster>` manually — it is auto-injected by `<XerticaProvider>`.
|
|
99
111
|
|
|
100
112
|
---
|
|
@@ -102,6 +114,7 @@ Never render `<Toaster>` manually — it is auto-injected by `<XerticaProvider>`
|
|
|
102
114
|
## Internationalization (i18n)
|
|
103
115
|
|
|
104
116
|
Every user-facing string must go through `useTranslation()`:
|
|
117
|
+
|
|
105
118
|
- Translate all labels, placeholders, titles, aria-labels, tooltips, toasts, etc.
|
|
106
119
|
- Add translation keys to split JSON files in `src/locales/<lang>/` for all languages.
|
|
107
120
|
- **Monolingual Mode**: `<LanguageSelector>` auto-hides when a single language is configured. Use `<LanguageSelector showWhenMonolingual />` to override.
|
|
@@ -124,7 +137,7 @@ Every user-facing string must go through `useTranslation()`:
|
|
|
124
137
|
return useQuery({
|
|
125
138
|
queryKey: ['home', 'feature-cards', language],
|
|
126
139
|
queryFn: fetchFeatureCards,
|
|
127
|
-
staleTime: 10 * 60 * 1000
|
|
140
|
+
staleTime: 10 * 60 * 1000,
|
|
128
141
|
});
|
|
129
142
|
```
|
|
130
143
|
|
|
@@ -133,6 +146,7 @@ Every user-facing string must go through `useTranslation()`:
|
|
|
133
146
|
## Loading States (Skeletons)
|
|
134
147
|
|
|
135
148
|
Always render matching skeletons instead of spinners for data loading:
|
|
149
|
+
|
|
136
150
|
- Wrap cards/lists with their matching FSD skeleton companions (`ActivityCardSkeleton`, `ProfileCardSkeleton`, `ProjectCardSkeleton`, `NotificationCardSkeleton`, `QuickActionCardSkeleton`, `FeatureCardSkeleton`, `StatsCardSkeleton`).
|
|
137
151
|
- Pass `rows` to skeletons (e.g. `<ActivityCardSkeleton rows={5} />`) to match the loaded state height.
|
|
138
152
|
- For tables, build loading rows using `<TableRow>` + `<TableCell>` + `<Skeleton className="h-3.5 w-28" />`.
|
|
@@ -149,6 +163,7 @@ Always render matching skeletons instead of spinners for data loading:
|
|
|
149
163
|
## Authentication Pattern
|
|
150
164
|
|
|
151
165
|
The auth flow is managed in `src/app/context/AuthContext.tsx` via the `useAuth()` hook:
|
|
166
|
+
|
|
152
167
|
- `getStoredUser()` / `storeUser()` / `clearStoredUser()` — from `src/shared/lib/auth.ts`
|
|
153
168
|
- Auth pages (`/login`, `/forgot-password`, etc.) redirect to `/home` when user is already logged in
|
|
154
169
|
- Protected routes use `<ProtectedRoute>` wrapper that redirects to `/login` if user is null
|
|
@@ -302,11 +302,11 @@ Every user-facing string must come from `useTranslation()`. Add new keys to the
|
|
|
302
302
|
|
|
303
303
|
```tsx
|
|
304
304
|
// ❌ Wrong
|
|
305
|
-
<Button aria-label="Save">Save</Button
|
|
305
|
+
<Button aria-label="Save">Save</Button>;
|
|
306
306
|
|
|
307
307
|
// ✅ Correct
|
|
308
308
|
const { t } = useTranslation();
|
|
309
|
-
<Button aria-label={t('common.save')}>{t('common.save')}</Button
|
|
309
|
+
<Button aria-label={t('common.save')}>{t('common.save')}</Button>;
|
|
310
310
|
```
|
|
311
311
|
|
|
312
312
|
This applies to `aria-label`, `placeholder`, `title`, toast messages, error text, dropdown items — everything the user can read.
|
|
@@ -340,6 +340,7 @@ npx xertica-ui update
|
|
|
340
340
|
```
|
|
341
341
|
|
|
342
342
|
The CLI shows your current selection, prompts for the new set, displays the diff (`+ es`, `- en`), and on confirm:
|
|
343
|
+
|
|
343
344
|
- copies any newly-added locale JSONs from `node_modules/xertica-ui/templates/src/locales/`
|
|
344
345
|
- removes JSONs of unselected languages
|
|
345
346
|
- regenerates `src/i18n.ts` and `src/app/App.tsx`
|
|
@@ -347,6 +348,24 @@ The CLI shows your current selection, prompts for the new set, displays the diff
|
|
|
347
348
|
|
|
348
349
|
> The `update` → **Project files** flow also reads `.languages.json` and preserves your selection. Updating App.tsx and i18n.ts won't reset your languages.
|
|
349
350
|
|
|
351
|
+
### Enabling or disabling Dark Mode support later
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
npx xertica-ui update
|
|
355
|
+
# → select "Dark Mode"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The CLI reads your current setting from `.xertica.json` and prompts you to enable or disable dark mode. On confirmation, it updates `.xertica.json` and regenerates `App.tsx` to set `disableDarkMode={true}` or `false`.
|
|
359
|
+
|
|
360
|
+
### Adding or removing the AI Assistant later
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
npx xertica-ui update
|
|
364
|
+
# → select "Assistant"
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
The CLI detects whether the assistant is currently active (via `.xertica.json` or page presence) and prompts to add or remove the feature. It automatically copies/deletes the feature files and updates `AuthGuard.tsx`, `HomePage.tsx`, and `TemplatePage.tsx` to reflect the change.
|
|
368
|
+
|
|
350
369
|
### Using translations in components
|
|
351
370
|
|
|
352
371
|
```tsx
|
|
@@ -385,10 +404,13 @@ export function getMockOptions() {
|
|
|
385
404
|
Inside components, build label maps for enums with `useMemo([..., t])`:
|
|
386
405
|
|
|
387
406
|
```tsx
|
|
388
|
-
const statusLabels = useMemo(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
407
|
+
const statusLabels = useMemo(
|
|
408
|
+
() => ({
|
|
409
|
+
active: t('status.active'),
|
|
410
|
+
inactive: t('status.inactive'),
|
|
411
|
+
}),
|
|
412
|
+
[t]
|
|
413
|
+
);
|
|
392
414
|
```
|
|
393
415
|
|
|
394
416
|
---
|
|
@@ -447,9 +469,9 @@ Every card pattern ships with a matching `*Skeleton`:
|
|
|
447
469
|
```tsx
|
|
448
470
|
import { ActivityCard, ActivityCardSkeleton } from 'xertica-ui';
|
|
449
471
|
|
|
450
|
-
{
|
|
451
|
-
? <ActivityCardSkeleton rows={5} />
|
|
452
|
-
|
|
472
|
+
{
|
|
473
|
+
isLoading ? <ActivityCardSkeleton rows={5} /> : <ActivityCard items={items} />;
|
|
474
|
+
}
|
|
453
475
|
```
|
|
454
476
|
|
|
455
477
|
Available skeleton companions: `ActivityCardSkeleton`, `ProfileCardSkeleton`, `ProjectCardSkeleton`, `NotificationCardSkeleton`, `QuickActionCardSkeleton`, `FeatureCardSkeleton`, `StatsCardSkeleton`.
|
|
@@ -457,15 +479,17 @@ Available skeleton companions: `ActivityCardSkeleton`, `ProfileCardSkeleton`, `P
|
|
|
457
479
|
For grids, render one skeleton per expected card:
|
|
458
480
|
|
|
459
481
|
```tsx
|
|
460
|
-
{
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
)
|
|
482
|
+
{
|
|
483
|
+
isLoading ? (
|
|
484
|
+
<>
|
|
485
|
+
<FeatureCardSkeleton showAction />
|
|
486
|
+
<FeatureCardSkeleton showAction />
|
|
487
|
+
<FeatureCardSkeleton showAction />
|
|
488
|
+
</>
|
|
489
|
+
) : (
|
|
490
|
+
data.map(item => <FeatureCard key={item.id} {...item} />)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
469
493
|
```
|
|
470
494
|
|
|
471
495
|
For tables, render skeleton rows with the `<Skeleton>` primitive (from `xertica-ui/ui`):
|
package/templates/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xertica-ui-template",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"react-i18next": "^17.0.8",
|
|
25
25
|
"react-router-dom": "^7.1.3",
|
|
26
26
|
"sonner": "^1.7.3",
|
|
27
|
-
"xertica-ui": "^2.
|
|
27
|
+
"xertica-ui": "^2.4.1",
|
|
28
28
|
"zustand": "^5.0.13"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
@@ -66,4 +66,4 @@
|
|
|
66
66
|
"vite": "6.3.5"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
}
|
|
69
|
+
}
|
|
@@ -1,82 +1,131 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
3
|
-
import { useAuth } from '../context/AuthContext';
|
|
4
|
-
|
|
5
|
-
// ─── Lazy page imports ────────────────────────────────────────────────────────
|
|
6
|
-
// Each route bundle is loaded only when the route is first visited.
|
|
7
|
-
|
|
8
|
-
const LoginPage = React.lazy(() =>
|
|
9
|
-
import('../../pages/LoginPage').then(m => ({ default: m.LoginPage }))
|
|
10
|
-
);
|
|
11
|
-
const ForgotPasswordPage = React.lazy(() =>
|
|
12
|
-
import('../../pages/ForgotPasswordPage').then(m => ({ default: m.ForgotPasswordPage }))
|
|
13
|
-
);
|
|
14
|
-
const VerifyEmailPage = React.lazy(() =>
|
|
15
|
-
import('../../pages/VerifyEmailPage').then(m => ({ default: m.VerifyEmailPage }))
|
|
16
|
-
);
|
|
17
|
-
const ResetPasswordPage = React.lazy(() =>
|
|
18
|
-
import('../../pages/ResetPasswordPage').then(m => ({ default: m.ResetPasswordPage }))
|
|
19
|
-
);
|
|
20
|
-
const HomePage = React.lazy(() =>
|
|
21
|
-
import('../../pages/HomePage').then(m => ({ default: m.HomePage }))
|
|
22
|
-
);
|
|
23
|
-
const TemplatePage = React.lazy(() =>
|
|
24
|
-
import('../../pages/TemplatePage').then(m => ({ default: m.TemplatePage }))
|
|
25
|
-
);
|
|
26
|
-
const AssistantPage = React.lazy(() =>
|
|
27
|
-
import('../../pages/AssistantPage').then(m => ({ default: m.AssistantPage }))
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// ─── Route guards ─────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
/** Redirects unauthenticated visitors to /login. */
|
|
33
|
-
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
34
|
-
const { user, isLoading } = useAuth();
|
|
35
|
-
if (isLoading) return null;
|
|
36
|
-
if (!user) return <Navigate to="/login" replace />;
|
|
37
|
-
return <>{children}</>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Redirects authenticated users away from auth-only pages. */
|
|
41
|
-
function GuestRoute({ children }: { children: React.ReactNode }) {
|
|
42
|
-
const { user, isLoading } = useAuth();
|
|
43
|
-
if (isLoading) return null;
|
|
44
|
-
if (user) return <Navigate to="/home" replace />;
|
|
45
|
-
return <>{children}</>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ─── Bridge for LoginPage ─────────────────────────────────────────────────────
|
|
49
|
-
// LoginPage expects an onLogin prop — this bridge wires AuthContext.login to it,
|
|
50
|
-
// keeping the LoginPage component decoupled from the AuthContext.
|
|
51
|
-
|
|
52
|
-
function LoginPageWithAuth() {
|
|
53
|
-
const { login } = useAuth();
|
|
54
|
-
return <LoginPage onLogin={login} />;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ─── Route tree ───────────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
export function AuthGuard() {
|
|
60
|
-
const { user } = useAuth();
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div className="
|
|
64
|
-
<Routes>
|
|
65
|
-
{/* Auth pages — redirect to /home when already signed in */}
|
|
66
|
-
<Route
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<Route
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../context/AuthContext';
|
|
4
|
+
|
|
5
|
+
// ─── Lazy page imports ────────────────────────────────────────────────────────
|
|
6
|
+
// Each route bundle is loaded only when the route is first visited.
|
|
7
|
+
|
|
8
|
+
const LoginPage = React.lazy(() =>
|
|
9
|
+
import('../../pages/LoginPage').then(m => ({ default: m.LoginPage }))
|
|
10
|
+
);
|
|
11
|
+
const ForgotPasswordPage = React.lazy(() =>
|
|
12
|
+
import('../../pages/ForgotPasswordPage').then(m => ({ default: m.ForgotPasswordPage }))
|
|
13
|
+
);
|
|
14
|
+
const VerifyEmailPage = React.lazy(() =>
|
|
15
|
+
import('../../pages/VerifyEmailPage').then(m => ({ default: m.VerifyEmailPage }))
|
|
16
|
+
);
|
|
17
|
+
const ResetPasswordPage = React.lazy(() =>
|
|
18
|
+
import('../../pages/ResetPasswordPage').then(m => ({ default: m.ResetPasswordPage }))
|
|
19
|
+
);
|
|
20
|
+
const HomePage = React.lazy(() =>
|
|
21
|
+
import('../../pages/HomePage').then(m => ({ default: m.HomePage }))
|
|
22
|
+
);
|
|
23
|
+
const TemplatePage = React.lazy(() =>
|
|
24
|
+
import('../../pages/TemplatePage').then(m => ({ default: m.TemplatePage }))
|
|
25
|
+
);
|
|
26
|
+
const AssistantPage = React.lazy(() =>
|
|
27
|
+
import('../../pages/AssistantPage').then(m => ({ default: m.AssistantPage }))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// ─── Route guards ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** Redirects unauthenticated visitors to /login. */
|
|
33
|
+
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
34
|
+
const { user, isLoading } = useAuth();
|
|
35
|
+
if (isLoading) return null;
|
|
36
|
+
if (!user) return <Navigate to="/login" replace />;
|
|
37
|
+
return <>{children}</>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Redirects authenticated users away from auth-only pages. */
|
|
41
|
+
function GuestRoute({ children }: { children: React.ReactNode }) {
|
|
42
|
+
const { user, isLoading } = useAuth();
|
|
43
|
+
if (isLoading) return null;
|
|
44
|
+
if (user) return <Navigate to="/home" replace />;
|
|
45
|
+
return <>{children}</>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Bridge for LoginPage ─────────────────────────────────────────────────────
|
|
49
|
+
// LoginPage expects an onLogin prop — this bridge wires AuthContext.login to it,
|
|
50
|
+
// keeping the LoginPage component decoupled from the AuthContext.
|
|
51
|
+
|
|
52
|
+
function LoginPageWithAuth() {
|
|
53
|
+
const { login } = useAuth();
|
|
54
|
+
return <LoginPage onLogin={login} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Route tree ───────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function AuthGuard() {
|
|
60
|
+
const { user } = useAuth();
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="h-screen bg-muted overflow-hidden max-w-full">
|
|
64
|
+
<Routes>
|
|
65
|
+
{/* Auth pages — redirect to /home when already signed in */}
|
|
66
|
+
<Route
|
|
67
|
+
path="/login"
|
|
68
|
+
element={
|
|
69
|
+
<GuestRoute>
|
|
70
|
+
<LoginPageWithAuth />
|
|
71
|
+
</GuestRoute>
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
<Route
|
|
75
|
+
path="/forgot-password"
|
|
76
|
+
element={
|
|
77
|
+
<GuestRoute>
|
|
78
|
+
<ForgotPasswordPage />
|
|
79
|
+
</GuestRoute>
|
|
80
|
+
}
|
|
81
|
+
/>
|
|
82
|
+
<Route
|
|
83
|
+
path="/verify-email"
|
|
84
|
+
element={
|
|
85
|
+
<GuestRoute>
|
|
86
|
+
<VerifyEmailPage />
|
|
87
|
+
</GuestRoute>
|
|
88
|
+
}
|
|
89
|
+
/>
|
|
90
|
+
<Route
|
|
91
|
+
path="/reset-password"
|
|
92
|
+
element={
|
|
93
|
+
<GuestRoute>
|
|
94
|
+
<ResetPasswordPage />
|
|
95
|
+
</GuestRoute>
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{/* Protected pages */}
|
|
100
|
+
<Route
|
|
101
|
+
path="/home"
|
|
102
|
+
element={
|
|
103
|
+
<ProtectedRoute>
|
|
104
|
+
<HomePage />
|
|
105
|
+
</ProtectedRoute>
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
<Route
|
|
109
|
+
path="/template"
|
|
110
|
+
element={
|
|
111
|
+
<ProtectedRoute>
|
|
112
|
+
<TemplatePage />
|
|
113
|
+
</ProtectedRoute>
|
|
114
|
+
}
|
|
115
|
+
/>
|
|
116
|
+
<Route
|
|
117
|
+
path="/assistente"
|
|
118
|
+
element={
|
|
119
|
+
<ProtectedRoute>
|
|
120
|
+
<AssistantPage />
|
|
121
|
+
</ProtectedRoute>
|
|
122
|
+
}
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
{/* Fallback */}
|
|
126
|
+
<Route path="/" element={<Navigate to={user ? '/home' : '/login'} replace />} />
|
|
127
|
+
<Route path="*" element={<Navigate to={user ? '/home' : '/login'} replace />} />
|
|
128
|
+
</Routes>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|