@umituz/web-dashboard 3.2.4 → 4.0.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/README.md +202 -14
- package/package.json +21 -29
- package/src/domains/analytics/components/AnalyticsCard.tsx +23 -21
- package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
- package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
- package/src/domains/analytics/components/MetricCard.tsx +42 -64
- package/src/domains/analytics/hooks/index.ts +7 -1
- package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
- package/src/domains/analytics/index.ts +11 -0
- package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
- package/src/domains/analytics/services/PerformanceService.ts +117 -69
- package/src/domains/analytics/types/analytics.ts +20 -6
- package/src/domains/analytics/types/index.ts +3 -0
- package/src/domains/analytics/utils/analytics.test.ts +183 -0
- package/src/domains/analytics/utils/analytics.ts +26 -9
- package/src/domains/analytics/utils/i18nKeys.ts +40 -0
- package/src/domains/analytics/utils/index.ts +2 -0
- package/src/domains/auth/components/AuthLayout.tsx +63 -54
- package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
- package/src/domains/auth/components/LoginForm.tsx +167 -226
- package/src/domains/auth/components/RegisterForm.tsx +202 -272
- package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
- package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
- package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
- package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
- package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
- package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
- package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
- package/src/domains/auth/components/shared/index.ts +20 -0
- package/src/domains/auth/hooks/index.ts +1 -0
- package/src/domains/auth/hooks/useAuth.ts +151 -232
- package/src/domains/auth/index.ts +8 -4
- package/src/domains/auth/types/auth.ts +26 -5
- package/src/domains/auth/types/index.ts +1 -0
- package/src/domains/auth/utils/auth.test.ts +182 -0
- package/src/domains/auth/utils/auth.ts +71 -130
- package/src/domains/auth/utils/i18nKeys.ts +127 -0
- package/src/domains/auth/utils/index.ts +7 -1
- package/src/domains/auth/utils/secureToken.ts +33 -0
- package/src/domains/auth/utils/validationKeys.ts +23 -0
- package/src/domains/calendar/hooks/useCalendar.ts +145 -151
- package/src/domains/calendar/index.ts +0 -1
- package/src/domains/calendar/services/CalendarService.ts +122 -179
- package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
- package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
- package/src/domains/calendar/utils/index.ts +33 -30
- package/src/domains/layouts/components/BrandLogo.tsx +12 -9
- package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
- package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
- package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
- package/src/domains/layouts/types/layout.ts +7 -1
- package/src/domains/layouts/types/user.ts +3 -1
- package/src/domains/modules/build.test.tsx +97 -0
- package/src/domains/modules/build.tsx +71 -0
- package/src/domains/modules/index.ts +2 -0
- package/src/domains/modules/types.ts +29 -0
- package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
- package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
- package/src/domains/onboarding/components/PlanStep.tsx +156 -123
- package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
- package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
- package/src/domains/onboarding/components/StepProgress.tsx +62 -0
- package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
- package/src/domains/onboarding/components/index.ts +4 -0
- package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
- package/src/domains/onboarding/index.ts +1 -0
- package/src/domains/onboarding/types/onboarding.ts +57 -17
- package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
- package/src/domains/onboarding/utils/index.ts +2 -0
- package/src/domains/permissions/PermissionsContext.tsx +103 -0
- package/src/domains/permissions/hasPermission.test.ts +50 -0
- package/src/domains/permissions/hasPermission.ts +38 -0
- package/src/domains/permissions/index.ts +19 -0
- package/src/domains/permissions/types.ts +35 -0
- package/src/domains/permissions/utils/i18nKeys.ts +11 -0
- package/src/domains/session/DemoLogin.tsx +50 -0
- package/src/domains/session/OrgSwitcher.tsx +110 -0
- package/src/domains/session/SessionContext.tsx +150 -0
- package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
- package/src/domains/session/index.ts +18 -0
- package/src/domains/session/types.ts +47 -0
- package/src/domains/session/utils/i18nKeys.ts +12 -0
- package/src/domains/settings/components/AuditLogList.tsx +59 -0
- package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
- package/src/domains/settings/components/SettingsLayout.tsx +75 -60
- package/src/domains/settings/components/SettingsSection.tsx +35 -22
- package/src/domains/settings/components/UserManagementTable.tsx +87 -0
- package/src/domains/settings/components/index.ts +10 -0
- package/src/domains/settings/hooks/useSettings.ts +77 -52
- package/src/domains/settings/types/settings.ts +2 -0
- package/src/domains/settings/utils/i18nKeys.ts +18 -0
- package/src/domains/settings/utils/settings.ts +11 -4
- package/src/i18n/I18nContext.tsx +33 -0
- package/src/i18n/defaultT.test.ts +43 -0
- package/src/i18n/defaultT.ts +34 -0
- package/src/i18n/index.ts +7 -0
- package/src/i18n/types.ts +13 -0
- package/src/index.ts +1 -7
- package/src/primitives/Button.tsx +30 -0
- package/src/primitives/Checkbox.tsx +40 -0
- package/src/primitives/Input.tsx +19 -0
- package/src/primitives/Skeleton.tsx +26 -0
- package/src/primitives/cn.ts +15 -0
- package/src/primitives/index.ts +23 -0
- package/src/styles/dashboard.css +3045 -0
- package/src/styles/tokens.contract.test.ts +66 -0
- package/src/styles/tokens.css +124 -0
- package/src/domains/billing/components/BillingLayout.tsx +0 -63
- package/src/domains/billing/components/BillingPage.tsx +0 -49
- package/src/domains/billing/components/BillingPortal.tsx +0 -198
- package/src/domains/billing/components/InvoiceCard.tsx +0 -143
- package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
- package/src/domains/billing/components/PlanComparison.tsx +0 -199
- package/src/domains/billing/components/UsageCard.tsx +0 -103
- package/src/domains/billing/components/index.ts +0 -13
- package/src/domains/billing/constants/billing.ts +0 -69
- package/src/domains/billing/hooks/index.ts +0 -7
- package/src/domains/billing/hooks/useBilling.ts +0 -385
- package/src/domains/billing/index.ts +0 -73
- package/src/domains/billing/types/billing.ts +0 -342
- package/src/domains/billing/types/index.ts +0 -30
- package/src/domains/billing/utils/billing.ts +0 -360
- package/src/domains/billing/utils/index.ts +0 -31
- package/src/domains/billing/utils/polar.ts +0 -63
- package/src/domains/calendar/components/index.ts +0 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { cn } from "
|
|
2
|
+
import { cn } from "../../../primitives";
|
|
3
3
|
|
|
4
4
|
interface BrandLogoProps {
|
|
5
5
|
className?: string;
|
|
@@ -10,7 +10,9 @@ interface BrandLogoProps {
|
|
|
10
10
|
* BrandLogo Component
|
|
11
11
|
*
|
|
12
12
|
* Displays the application brand logo as an SVG.
|
|
13
|
-
*
|
|
13
|
+
* Colors come from the package tokens (`--u-color-primary`,
|
|
14
|
+
* `--u-color-secondary`) so hosts restyle it by overriding CSS
|
|
15
|
+
* custom properties — no host theme variables required.
|
|
14
16
|
*
|
|
15
17
|
* @param className - Optional CSS classes for styling
|
|
16
18
|
* @param size - Width and height in pixels (default: 32)
|
|
@@ -23,7 +25,8 @@ export const BrandLogo = ({ className, size = 32 }: BrandLogoProps) => {
|
|
|
23
25
|
viewBox="0 0 100 100"
|
|
24
26
|
fill="none"
|
|
25
27
|
xmlns="http://www.w3.org/2000/svg"
|
|
26
|
-
className={cn("
|
|
28
|
+
className={cn("udb-brand-logo", className)}
|
|
29
|
+
aria-hidden="true"
|
|
27
30
|
>
|
|
28
31
|
{/* Solid Foundation / Platform */}
|
|
29
32
|
<rect
|
|
@@ -32,7 +35,7 @@ export const BrandLogo = ({ className, size = 32 }: BrandLogoProps) => {
|
|
|
32
35
|
width="70"
|
|
33
36
|
height="15"
|
|
34
37
|
rx="4"
|
|
35
|
-
fill="
|
|
38
|
+
fill="var(--u-color-primary)"
|
|
36
39
|
/>
|
|
37
40
|
|
|
38
41
|
{/* Assembly Paths / Structure */}
|
|
@@ -42,7 +45,7 @@ export const BrandLogo = ({ className, size = 32 }: BrandLogoProps) => {
|
|
|
42
45
|
width="12"
|
|
43
46
|
height="40"
|
|
44
47
|
rx="2"
|
|
45
|
-
fill="
|
|
48
|
+
fill="var(--u-color-primary)"
|
|
46
49
|
/>
|
|
47
50
|
<rect
|
|
48
51
|
x="44"
|
|
@@ -50,7 +53,7 @@ export const BrandLogo = ({ className, size = 32 }: BrandLogoProps) => {
|
|
|
50
53
|
width="12"
|
|
51
54
|
height="30"
|
|
52
55
|
rx="2"
|
|
53
|
-
fill="
|
|
56
|
+
fill="var(--u-color-primary)"
|
|
54
57
|
/>
|
|
55
58
|
<rect
|
|
56
59
|
x="63"
|
|
@@ -58,7 +61,7 @@ export const BrandLogo = ({ className, size = 32 }: BrandLogoProps) => {
|
|
|
58
61
|
width="12"
|
|
59
62
|
height="45"
|
|
60
63
|
rx="2"
|
|
61
|
-
fill="
|
|
64
|
+
fill="var(--u-color-primary)"
|
|
62
65
|
/>
|
|
63
66
|
|
|
64
67
|
{/* Modern Accent - The 'Assembly' Bridge */}
|
|
@@ -68,7 +71,7 @@ export const BrandLogo = ({ className, size = 32 }: BrandLogoProps) => {
|
|
|
68
71
|
width="60"
|
|
69
72
|
height="10"
|
|
70
73
|
rx="2"
|
|
71
|
-
fill="
|
|
74
|
+
fill="var(--u-color-secondary)"
|
|
72
75
|
/>
|
|
73
76
|
|
|
74
77
|
{/* Connection Point / Beacon */}
|
|
@@ -76,7 +79,7 @@ export const BrandLogo = ({ className, size = 32 }: BrandLogoProps) => {
|
|
|
76
79
|
cx="50"
|
|
77
80
|
cy="20"
|
|
78
81
|
r="5"
|
|
79
|
-
fill="
|
|
82
|
+
fill="var(--u-color-secondary)"
|
|
80
83
|
/>
|
|
81
84
|
</svg>
|
|
82
85
|
);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Bell, X, Sun, Moon, Menu, User, Settings, LogOut,
|
|
4
4
|
ChevronDown, CreditCard
|
|
5
5
|
} from "lucide-react";
|
|
6
|
-
import { Button } from "
|
|
6
|
+
import { Button } from "../../../primitives";
|
|
7
7
|
import { useNavigate } from "react-router-dom";
|
|
8
|
-
import {
|
|
8
|
+
import { useT } from "../../../i18n";
|
|
9
9
|
import type { DashboardHeaderProps } from "../types/layout";
|
|
10
10
|
import type { DashboardNotification } from "../types/notification";
|
|
11
11
|
import type { DashboardUser } from "../types/user";
|
|
@@ -34,7 +34,8 @@ interface DashboardHeaderPropsExtended extends DashboardHeaderProps {
|
|
|
34
34
|
* Dashboard Header Component
|
|
35
35
|
*
|
|
36
36
|
* Displays top navigation bar with theme toggle, notifications,
|
|
37
|
-
* user menu, and organisation selector.
|
|
37
|
+
* user menu, and organisation selector. Styling is entirely
|
|
38
|
+
* `udb-header*` / `udb-menu*` classes from the package stylesheet.
|
|
38
39
|
*
|
|
39
40
|
* @param props - Dashboard header props
|
|
40
41
|
*/
|
|
@@ -43,6 +44,8 @@ export const DashboardHeader = ({
|
|
|
43
44
|
setCollapsed,
|
|
44
45
|
setMobileOpen,
|
|
45
46
|
title,
|
|
47
|
+
theme = "light",
|
|
48
|
+
onToggleTheme,
|
|
46
49
|
user,
|
|
47
50
|
notifications = [],
|
|
48
51
|
onLogout,
|
|
@@ -53,7 +56,7 @@ export const DashboardHeader = ({
|
|
|
53
56
|
billingRoute = "/dashboard/billing",
|
|
54
57
|
}: DashboardHeaderPropsExtended) => {
|
|
55
58
|
const navigate = useNavigate();
|
|
56
|
-
const
|
|
59
|
+
const t = useT();
|
|
57
60
|
const [notifOpen, setNotifOpen] = useState(false);
|
|
58
61
|
const [profileOpen, setProfileOpen] = useState(false);
|
|
59
62
|
|
|
@@ -72,98 +75,88 @@ export const DashboardHeader = ({
|
|
|
72
75
|
}
|
|
73
76
|
};
|
|
74
77
|
|
|
75
|
-
// Placeholder components - these should be provided by the consuming app
|
|
76
|
-
const ThemeToggle = () => {
|
|
77
|
-
const [resolvedTheme, setResolvedTheme] = React.useState<"light" | "dark">("light");
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<Button
|
|
81
|
-
variant="ghost"
|
|
82
|
-
size="icon"
|
|
83
|
-
onClick={() => setResolvedTheme(resolvedTheme === "light" ? "dark" : "light")}
|
|
84
|
-
className="text-muted-foreground h-9 w-9"
|
|
85
|
-
title={resolvedTheme === "dark" ? t('common.tooltips.switchLight') : t('common.tooltips.switchDark')}
|
|
86
|
-
>
|
|
87
|
-
{resolvedTheme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
|
88
|
-
</Button>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
78
|
return (
|
|
93
|
-
<header className="
|
|
94
|
-
<div className="
|
|
95
|
-
<Button variant="ghost" size="icon" className="
|
|
96
|
-
<Menu className="
|
|
79
|
+
<header className="udb-header">
|
|
80
|
+
<div className="udb-header__lead">
|
|
81
|
+
<Button variant="ghost" size="icon" className="udb-header__mobile-toggle" onClick={() => setMobileOpen(true)}>
|
|
82
|
+
<Menu className="udb-header__toggle-icon" />
|
|
97
83
|
</Button>
|
|
98
84
|
{collapsed && (
|
|
99
|
-
<Button variant="ghost" size="icon" className="
|
|
100
|
-
<Menu className="
|
|
85
|
+
<Button variant="ghost" size="icon" className="udb-header__desktop-toggle" onClick={() => setCollapsed(false)}>
|
|
86
|
+
<Menu className="udb-header__toggle-icon" />
|
|
101
87
|
</Button>
|
|
102
88
|
)}
|
|
103
|
-
<h2 className="
|
|
89
|
+
<h2 className="udb-header__title">
|
|
104
90
|
{title}
|
|
105
91
|
</h2>
|
|
106
92
|
</div>
|
|
107
93
|
|
|
108
|
-
<div className="
|
|
109
|
-
{/* Theme Toggle */}
|
|
110
|
-
|
|
94
|
+
<div className="udb-header__actions">
|
|
95
|
+
{/* Theme Toggle — rendered only when the host app wires it up */}
|
|
96
|
+
{onToggleTheme && (
|
|
97
|
+
<Button
|
|
98
|
+
variant="ghost"
|
|
99
|
+
size="icon"
|
|
100
|
+
onClick={onToggleTheme}
|
|
101
|
+
className="udb-header__icon-btn"
|
|
102
|
+
title={theme === "dark" ? t('common.tooltips.switchLight') : t('common.tooltips.switchDark')}
|
|
103
|
+
aria-label={theme === "dark" ? t('common.tooltips.switchLight') : t('common.tooltips.switchDark')}
|
|
104
|
+
>
|
|
105
|
+
{theme === "dark" ? <Sun className="udb-header__icon" aria-hidden="true" /> : <Moon className="udb-header__icon" aria-hidden="true" />}
|
|
106
|
+
</Button>
|
|
107
|
+
)}
|
|
111
108
|
|
|
112
109
|
{/* Notifications */}
|
|
113
|
-
<div className="
|
|
110
|
+
<div className="udb-header__anchor">
|
|
114
111
|
<Button
|
|
115
112
|
variant="ghost"
|
|
116
113
|
size="icon"
|
|
117
|
-
className="
|
|
114
|
+
className="udb-header__icon-btn"
|
|
118
115
|
onClick={() => {
|
|
119
116
|
setNotifOpen(!notifOpen);
|
|
120
117
|
setProfileOpen(false);
|
|
121
118
|
}}
|
|
122
119
|
>
|
|
123
|
-
<Bell className="
|
|
124
|
-
{unreadCount > 0 &&
|
|
125
|
-
<span className="absolute top-2 right-2 flex h-2 w-2">
|
|
126
|
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-destructive opacity-75"></span>
|
|
127
|
-
<span className="relative inline-flex rounded-full h-2 w-2 bg-destructive"></span>
|
|
128
|
-
</span>
|
|
129
|
-
)}
|
|
120
|
+
<Bell className="udb-header__icon" />
|
|
121
|
+
{unreadCount > 0 && <span className="udb-header__unread"><span className="udb-header__unread-core" /></span>}
|
|
130
122
|
</Button>
|
|
131
123
|
|
|
132
124
|
{notifOpen && (
|
|
133
125
|
<>
|
|
134
|
-
<div className="
|
|
135
|
-
<div className="
|
|
136
|
-
<div className="
|
|
137
|
-
<h3 className="
|
|
126
|
+
<div className="udb-overlay" onClick={() => setNotifOpen(false)} />
|
|
127
|
+
<div className="udb-menu udb-notif">
|
|
128
|
+
<div className="udb-menu__header">
|
|
129
|
+
<h3 className="udb-menu__heading">{t('dashboard.notifications.title')}</h3>
|
|
138
130
|
{unreadCount > 0 && (
|
|
139
|
-
<button onClick={markAllRead} className="
|
|
131
|
+
<button type="button" onClick={markAllRead} className="udb-menu__action">{t('dashboard.notifications.markAllRead')}</button>
|
|
140
132
|
)}
|
|
141
133
|
</div>
|
|
142
134
|
|
|
143
|
-
<div className="
|
|
135
|
+
<div className="udb-notif__list">
|
|
144
136
|
{notifications.map((n) => (
|
|
145
|
-
<div key={n.id} className={`
|
|
146
|
-
<div className="
|
|
147
|
-
<p className="
|
|
148
|
-
<p className="
|
|
149
|
-
<span className="
|
|
137
|
+
<div key={n.id} className={`udb-notif__item${!n.read ? " udb-notif__item--unread" : ""}`}>
|
|
138
|
+
<div className="udb-notif__body">
|
|
139
|
+
<p className="udb-notif__text">{n.text}</p>
|
|
140
|
+
<p className="udb-notif__meta">
|
|
141
|
+
<span className="udb-notif__dot" />
|
|
150
142
|
{formatNotificationTime(n.createdAt, t)}
|
|
151
143
|
</p>
|
|
152
144
|
</div>
|
|
153
145
|
<button
|
|
146
|
+
type="button"
|
|
154
147
|
onClick={() => onDismissNotification?.(n.id)}
|
|
155
|
-
className="
|
|
148
|
+
className="udb-notif__dismiss"
|
|
156
149
|
>
|
|
157
|
-
<X className="
|
|
150
|
+
<X className="udb-notif__dismiss-icon" />
|
|
158
151
|
</button>
|
|
159
152
|
</div>
|
|
160
153
|
))}
|
|
161
154
|
{notifications.length === 0 && (
|
|
162
|
-
<div className="
|
|
163
|
-
<div className="
|
|
164
|
-
<Bell className="
|
|
155
|
+
<div className="udb-notif__empty">
|
|
156
|
+
<div className="udb-notif__empty-icon">
|
|
157
|
+
<Bell className="udb-notif__empty-bell" />
|
|
165
158
|
</div>
|
|
166
|
-
<p className="
|
|
159
|
+
<p className="udb-notif__empty-text">{t('dashboard.notifications.none')}</p>
|
|
167
160
|
</div>
|
|
168
161
|
)}
|
|
169
162
|
</div>
|
|
@@ -172,62 +165,67 @@ export const DashboardHeader = ({
|
|
|
172
165
|
)}
|
|
173
166
|
</div>
|
|
174
167
|
|
|
175
|
-
<div className="
|
|
168
|
+
<div className="udb-header__divider" />
|
|
176
169
|
|
|
177
|
-
<div className="
|
|
170
|
+
<div className="udb-header__anchor">
|
|
178
171
|
<button
|
|
172
|
+
type="button"
|
|
179
173
|
onClick={() => {
|
|
180
174
|
setProfileOpen(!profileOpen);
|
|
181
175
|
setNotifOpen(false);
|
|
182
176
|
}}
|
|
183
|
-
className="
|
|
177
|
+
className="udb-profile-btn"
|
|
184
178
|
>
|
|
185
|
-
<
|
|
186
|
-
{user?.avatar && <img src={user.avatar} alt="
|
|
187
|
-
</
|
|
188
|
-
<ChevronDown className={`
|
|
179
|
+
<span className="udb-avatar">
|
|
180
|
+
{user?.avatar && <img src={user.avatar} alt="" className="udb-avatar__img" />}
|
|
181
|
+
</span>
|
|
182
|
+
<ChevronDown className={`udb-profile-btn__chevron${profileOpen ? " udb-profile-btn__chevron--open" : ""}`} />
|
|
189
183
|
</button>
|
|
190
184
|
|
|
191
185
|
{profileOpen && (
|
|
192
186
|
<>
|
|
193
|
-
<div className="
|
|
194
|
-
<div className="
|
|
195
|
-
<div className="
|
|
196
|
-
<p className="
|
|
197
|
-
<p className="
|
|
187
|
+
<div className="udb-overlay" onClick={() => setProfileOpen(false)} />
|
|
188
|
+
<div className="udb-menu udb-profile-menu">
|
|
189
|
+
<div className="udb-profile-menu__id">
|
|
190
|
+
<p className="udb-profile-menu__name">{user?.name || t("common.roles.user")}</p>
|
|
191
|
+
<p className="udb-profile-menu__email">{user?.email}</p>
|
|
198
192
|
</div>
|
|
199
193
|
|
|
200
|
-
<div className="
|
|
194
|
+
<div className="udb-profile-menu__items">
|
|
201
195
|
<button
|
|
196
|
+
type="button"
|
|
202
197
|
onClick={() => { navigate(profileRoute); setProfileOpen(false); }}
|
|
203
|
-
className="
|
|
198
|
+
className="udb-menu__item"
|
|
204
199
|
>
|
|
205
|
-
<User className="
|
|
200
|
+
<User className="udb-menu__item-icon" />
|
|
206
201
|
{t('common.profile')}
|
|
207
202
|
</button>
|
|
208
203
|
<button
|
|
204
|
+
type="button"
|
|
209
205
|
onClick={() => { navigate(billingRoute); setProfileOpen(false); }}
|
|
210
|
-
className="
|
|
206
|
+
className="udb-menu__item"
|
|
211
207
|
>
|
|
212
|
-
<CreditCard className="
|
|
208
|
+
<CreditCard className="udb-menu__item-icon" />
|
|
213
209
|
{t('common.billing')}
|
|
214
210
|
</button>
|
|
215
211
|
<button
|
|
212
|
+
type="button"
|
|
216
213
|
onClick={() => { navigate(settingsRoute); setProfileOpen(false); }}
|
|
217
|
-
className="
|
|
214
|
+
className="udb-menu__item"
|
|
218
215
|
>
|
|
219
|
-
<Settings className="
|
|
216
|
+
<Settings className="udb-menu__item-icon" />
|
|
220
217
|
{t('common.settings')}
|
|
221
218
|
</button>
|
|
222
219
|
</div>
|
|
223
220
|
|
|
224
|
-
<div className="
|
|
221
|
+
<div className="udb-profile-menu__separator" />
|
|
225
222
|
|
|
226
223
|
<button
|
|
224
|
+
type="button"
|
|
227
225
|
onClick={handleLogout}
|
|
228
|
-
className="
|
|
226
|
+
className="udb-menu__item udb-menu__item--danger"
|
|
229
227
|
>
|
|
230
|
-
<LogOut className="
|
|
228
|
+
<LogOut className="udb-menu__item-icon" />
|
|
231
229
|
{t('common.logout')}
|
|
232
230
|
</button>
|
|
233
231
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
2
|
import { useLocation, Outlet, Navigate } from "react-router-dom";
|
|
3
|
-
import {
|
|
3
|
+
import { useT } from "../../../i18n";
|
|
4
|
+
import { Skeleton } from "../../../primitives";
|
|
4
5
|
import { DashboardSidebar } from "./DashboardSidebar";
|
|
5
6
|
import { DashboardHeader } from "./DashboardHeader";
|
|
6
7
|
import type { DashboardLayoutConfig } from "../types/layout";
|
|
@@ -26,6 +27,16 @@ interface DashboardLayoutProps {
|
|
|
26
27
|
onDismissNotification?: (id: string) => void;
|
|
27
28
|
/** Login route for redirect */
|
|
28
29
|
loginRoute?: string;
|
|
30
|
+
/** Current theme, forwarded to the header toggle (default: "light") */
|
|
31
|
+
theme?: "light" | "dark";
|
|
32
|
+
/** Theme toggle handler — header toggle is hidden when omitted */
|
|
33
|
+
onToggleTheme?: () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Artificial route-transition skeleton duration in ms (default: 0 = off).
|
|
36
|
+
* Content renders immediately by default; set > 0 only if you want
|
|
37
|
+
* a skeleton to mask route swaps.
|
|
38
|
+
*/
|
|
39
|
+
routeTransitionMs?: number;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
/**
|
|
@@ -38,9 +49,12 @@ interface DashboardLayoutProps {
|
|
|
38
49
|
* - Collapsible sidebar
|
|
39
50
|
* - Mobile menu overlay
|
|
40
51
|
* - Breadcrumb page titles
|
|
41
|
-
* - Loading skeletons
|
|
52
|
+
* - Loading skeletons (opt-in via routeTransitionMs)
|
|
42
53
|
* - Auth protection
|
|
43
54
|
*
|
|
55
|
+
* All styling comes from `udb-*` classes in the package stylesheet
|
|
56
|
+
* (`@umituz/web-dashboard/styles.css`) — no utility framework required.
|
|
57
|
+
*
|
|
44
58
|
* @param props - Dashboard layout props
|
|
45
59
|
*/
|
|
46
60
|
export const DashboardLayout = ({
|
|
@@ -53,17 +67,25 @@ export const DashboardLayout = ({
|
|
|
53
67
|
onMarkAllRead,
|
|
54
68
|
onDismissNotification,
|
|
55
69
|
loginRoute = "/login",
|
|
70
|
+
theme,
|
|
71
|
+
onToggleTheme,
|
|
72
|
+
routeTransitionMs = 0,
|
|
56
73
|
}: DashboardLayoutProps) => {
|
|
57
74
|
const location = useLocation();
|
|
75
|
+
const t = useT();
|
|
58
76
|
const [collapsed, setCollapsed] = useState(false);
|
|
59
77
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
60
|
-
const [loading, setLoading] = useState(
|
|
78
|
+
const [loading, setLoading] = useState(routeTransitionMs > 0);
|
|
61
79
|
|
|
62
80
|
useEffect(() => {
|
|
81
|
+
if (routeTransitionMs <= 0) {
|
|
82
|
+
setLoading(false);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
63
85
|
setLoading(true);
|
|
64
|
-
const timer = setTimeout(() => setLoading(false),
|
|
86
|
+
const timer = setTimeout(() => setLoading(false), routeTransitionMs);
|
|
65
87
|
return () => clearTimeout(timer);
|
|
66
|
-
}, [location.pathname]);
|
|
88
|
+
}, [location.pathname, routeTransitionMs]);
|
|
67
89
|
|
|
68
90
|
useEffect(() => {
|
|
69
91
|
setMobileOpen(false);
|
|
@@ -77,19 +99,19 @@ export const DashboardLayout = ({
|
|
|
77
99
|
.find((i) => i.path === location.pathname);
|
|
78
100
|
|
|
79
101
|
const getTitle = () => {
|
|
80
|
-
if (
|
|
81
|
-
|
|
102
|
+
if (activeItem) return t(activeItem.label);
|
|
103
|
+
const extraTitle = config.extraTitleMap?.[location.pathname];
|
|
104
|
+
if (extraTitle) return t(extraTitle);
|
|
105
|
+
return config.defaultTitle ?? "Dashboard";
|
|
82
106
|
};
|
|
83
107
|
|
|
84
108
|
const currentTitle = getTitle();
|
|
85
109
|
|
|
86
110
|
return (
|
|
87
|
-
<div className="
|
|
111
|
+
<div className="udb-root udb-layout">
|
|
88
112
|
{/* Desktop Sidebar */}
|
|
89
113
|
<aside
|
|
90
|
-
className={`
|
|
91
|
-
collapsed ? "w-16" : "w-60"
|
|
92
|
-
}`}
|
|
114
|
+
className={`udb-layout__sidebar ${collapsed ? "udb-layout__sidebar--collapsed" : ""}`}
|
|
93
115
|
>
|
|
94
116
|
<DashboardSidebar
|
|
95
117
|
collapsed={collapsed}
|
|
@@ -103,9 +125,9 @@ export const DashboardLayout = ({
|
|
|
103
125
|
|
|
104
126
|
{/* Mobile Menu Overlay */}
|
|
105
127
|
{mobileOpen && (
|
|
106
|
-
<div className="
|
|
107
|
-
<div className="
|
|
108
|
-
<aside className="
|
|
128
|
+
<div className="udb-layout__mobile">
|
|
129
|
+
<div className="udb-layout__backdrop" onClick={() => setMobileOpen(false)} />
|
|
130
|
+
<aside className="udb-layout__mobile-sidebar">
|
|
109
131
|
<DashboardSidebar
|
|
110
132
|
collapsed={false}
|
|
111
133
|
setCollapsed={() => setMobileOpen(false)}
|
|
@@ -119,12 +141,14 @@ export const DashboardLayout = ({
|
|
|
119
141
|
)}
|
|
120
142
|
|
|
121
143
|
{/* Main Content Area */}
|
|
122
|
-
<div className="
|
|
144
|
+
<div className="udb-layout__main">
|
|
123
145
|
<DashboardHeader
|
|
124
146
|
collapsed={collapsed}
|
|
125
147
|
setCollapsed={setCollapsed}
|
|
126
148
|
setMobileOpen={setMobileOpen}
|
|
127
149
|
title={currentTitle}
|
|
150
|
+
theme={theme}
|
|
151
|
+
onToggleTheme={onToggleTheme}
|
|
128
152
|
user={user}
|
|
129
153
|
notifications={notifications}
|
|
130
154
|
onLogout={onLogout}
|
|
@@ -132,16 +156,16 @@ export const DashboardLayout = ({
|
|
|
132
156
|
onDismissNotification={onDismissNotification}
|
|
133
157
|
/>
|
|
134
158
|
|
|
135
|
-
<main className="
|
|
159
|
+
<main className="udb-layout__content">
|
|
136
160
|
{loading ? (
|
|
137
|
-
<div className="
|
|
138
|
-
<Skeleton className="
|
|
139
|
-
<div className="
|
|
161
|
+
<div className="udb-page-skeleton">
|
|
162
|
+
<Skeleton className="udb-skeleton--title" />
|
|
163
|
+
<div className="udb-skeleton-grid">
|
|
140
164
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
141
|
-
<Skeleton key={i} className="
|
|
165
|
+
<Skeleton key={i} className="udb-skeleton--card" />
|
|
142
166
|
))}
|
|
143
167
|
</div>
|
|
144
|
-
<Skeleton className="
|
|
168
|
+
<Skeleton className="udb-skeleton--section" />
|
|
145
169
|
</div>
|
|
146
170
|
) : (
|
|
147
171
|
<Outlet />
|