@umituz/web-dashboard 3.2.2 → 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.
Files changed (125) hide show
  1. package/README.md +202 -14
  2. package/package.json +21 -29
  3. package/src/domains/analytics/components/AnalyticsCard.tsx +23 -21
  4. package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
  5. package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
  6. package/src/domains/analytics/components/MetricCard.tsx +42 -64
  7. package/src/domains/analytics/hooks/index.ts +7 -1
  8. package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
  9. package/src/domains/analytics/index.ts +11 -0
  10. package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
  11. package/src/domains/analytics/services/PerformanceService.ts +117 -69
  12. package/src/domains/analytics/types/analytics.ts +20 -6
  13. package/src/domains/analytics/types/index.ts +3 -0
  14. package/src/domains/analytics/utils/analytics.test.ts +183 -0
  15. package/src/domains/analytics/utils/analytics.ts +26 -9
  16. package/src/domains/analytics/utils/i18nKeys.ts +40 -0
  17. package/src/domains/analytics/utils/index.ts +2 -0
  18. package/src/domains/auth/components/AuthLayout.tsx +63 -54
  19. package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
  20. package/src/domains/auth/components/LoginForm.tsx +167 -226
  21. package/src/domains/auth/components/RegisterForm.tsx +202 -272
  22. package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
  23. package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
  24. package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
  25. package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
  26. package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
  27. package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
  28. package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
  29. package/src/domains/auth/components/shared/index.ts +20 -0
  30. package/src/domains/auth/hooks/index.ts +1 -0
  31. package/src/domains/auth/hooks/useAuth.ts +151 -232
  32. package/src/domains/auth/index.ts +8 -4
  33. package/src/domains/auth/types/auth.ts +26 -5
  34. package/src/domains/auth/types/index.ts +1 -0
  35. package/src/domains/auth/utils/auth.test.ts +182 -0
  36. package/src/domains/auth/utils/auth.ts +71 -130
  37. package/src/domains/auth/utils/i18nKeys.ts +127 -0
  38. package/src/domains/auth/utils/index.ts +7 -1
  39. package/src/domains/auth/utils/secureToken.ts +33 -0
  40. package/src/domains/auth/utils/validationKeys.ts +23 -0
  41. package/src/domains/calendar/hooks/useCalendar.ts +145 -151
  42. package/src/domains/calendar/index.ts +0 -1
  43. package/src/domains/calendar/services/CalendarService.ts +122 -179
  44. package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
  45. package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
  46. package/src/domains/calendar/utils/index.ts +33 -30
  47. package/src/domains/layouts/components/BrandLogo.tsx +12 -9
  48. package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
  49. package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
  50. package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
  51. package/src/domains/layouts/types/layout.ts +7 -1
  52. package/src/domains/layouts/types/user.ts +3 -1
  53. package/src/domains/modules/build.test.tsx +97 -0
  54. package/src/domains/modules/build.tsx +71 -0
  55. package/src/domains/modules/index.ts +2 -0
  56. package/src/domains/modules/types.ts +29 -0
  57. package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
  58. package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
  59. package/src/domains/onboarding/components/PlanStep.tsx +156 -123
  60. package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
  61. package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
  62. package/src/domains/onboarding/components/StepProgress.tsx +62 -0
  63. package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
  64. package/src/domains/onboarding/components/index.ts +4 -0
  65. package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
  66. package/src/domains/onboarding/index.ts +1 -0
  67. package/src/domains/onboarding/types/onboarding.ts +57 -17
  68. package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
  69. package/src/domains/onboarding/utils/index.ts +2 -0
  70. package/src/domains/permissions/PermissionsContext.tsx +103 -0
  71. package/src/domains/permissions/hasPermission.test.ts +50 -0
  72. package/src/domains/permissions/hasPermission.ts +38 -0
  73. package/src/domains/permissions/index.ts +19 -0
  74. package/src/domains/permissions/types.ts +35 -0
  75. package/src/domains/permissions/utils/i18nKeys.ts +11 -0
  76. package/src/domains/session/DemoLogin.tsx +50 -0
  77. package/src/domains/session/OrgSwitcher.tsx +110 -0
  78. package/src/domains/session/SessionContext.tsx +150 -0
  79. package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
  80. package/src/domains/session/index.ts +18 -0
  81. package/src/domains/session/types.ts +47 -0
  82. package/src/domains/session/utils/i18nKeys.ts +12 -0
  83. package/src/domains/settings/components/AuditLogList.tsx +59 -0
  84. package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
  85. package/src/domains/settings/components/SettingsLayout.tsx +75 -60
  86. package/src/domains/settings/components/SettingsSection.tsx +35 -22
  87. package/src/domains/settings/components/UserManagementTable.tsx +87 -0
  88. package/src/domains/settings/components/index.ts +10 -0
  89. package/src/domains/settings/hooks/useSettings.ts +77 -52
  90. package/src/domains/settings/types/settings.ts +2 -0
  91. package/src/domains/settings/utils/i18nKeys.ts +18 -0
  92. package/src/domains/settings/utils/settings.ts +11 -4
  93. package/src/i18n/I18nContext.tsx +33 -0
  94. package/src/i18n/defaultT.test.ts +43 -0
  95. package/src/i18n/defaultT.ts +34 -0
  96. package/src/i18n/index.ts +7 -0
  97. package/src/i18n/types.ts +13 -0
  98. package/src/index.ts +1 -7
  99. package/src/primitives/Button.tsx +30 -0
  100. package/src/primitives/Checkbox.tsx +40 -0
  101. package/src/primitives/Input.tsx +19 -0
  102. package/src/primitives/Skeleton.tsx +26 -0
  103. package/src/primitives/cn.ts +15 -0
  104. package/src/primitives/index.ts +23 -0
  105. package/src/styles/dashboard.css +3045 -0
  106. package/src/styles/tokens.contract.test.ts +66 -0
  107. package/src/styles/tokens.css +124 -0
  108. package/src/domains/billing/components/BillingLayout.tsx +0 -63
  109. package/src/domains/billing/components/BillingPage.tsx +0 -49
  110. package/src/domains/billing/components/BillingPortal.tsx +0 -198
  111. package/src/domains/billing/components/InvoiceCard.tsx +0 -143
  112. package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
  113. package/src/domains/billing/components/PlanComparison.tsx +0 -199
  114. package/src/domains/billing/components/UsageCard.tsx +0 -103
  115. package/src/domains/billing/components/index.ts +0 -13
  116. package/src/domains/billing/constants/billing.ts +0 -69
  117. package/src/domains/billing/hooks/index.ts +0 -7
  118. package/src/domains/billing/hooks/useBilling.ts +0 -385
  119. package/src/domains/billing/index.ts +0 -73
  120. package/src/domains/billing/types/billing.ts +0 -342
  121. package/src/domains/billing/types/index.ts +0 -30
  122. package/src/domains/billing/utils/billing.ts +0 -360
  123. package/src/domains/billing/utils/index.ts +0 -31
  124. package/src/domains/billing/utils/polar.ts +0 -63
  125. package/src/domains/calendar/components/index.ts +0 -7
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { cn } from "@umituz/web-design-system/utils";
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
- * Supports custom sizing and styling through className.
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("shrink-0", className)}
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="hsl(var(--primary))"
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="hsl(var(--primary))"
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="hsl(var(--primary))"
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="hsl(var(--primary))"
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="hsl(var(--secondary))"
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="hsl(var(--secondary))"
82
+ fill="var(--u-color-secondary)"
80
83
  />
81
84
  </svg>
82
85
  );
@@ -1,11 +1,11 @@
1
- import React, { useState } from "react";
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 "@umituz/web-design-system/atoms";
6
+ import { Button } from "../../../primitives";
7
7
  import { useNavigate } from "react-router-dom";
8
- import { useTranslation } from "react-i18next";
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 { t } = useTranslation();
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="flex h-14 items-center justify-between border-b border-border bg-card/50 backdrop-blur-md px-4 shrink-0 z-30">
94
- <div className="flex items-center gap-3">
95
- <Button variant="ghost" size="icon" className="md:hidden" onClick={() => setMobileOpen(true)}>
96
- <Menu className="h-5 w-5" />
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="hidden md:inline-flex" onClick={() => setCollapsed(false)}>
100
- <Menu className="h-5 w-5" />
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="text-sm font-semibold text-foreground">
89
+ <h2 className="udb-header__title">
104
90
  {title}
105
91
  </h2>
106
92
  </div>
107
93
 
108
- <div className="flex items-center gap-2">
109
- {/* Theme Toggle */}
110
- <ThemeToggle />
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="relative">
110
+ <div className="udb-header__anchor">
114
111
  <Button
115
112
  variant="ghost"
116
113
  size="icon"
117
- className="text-muted-foreground relative h-9 w-9"
114
+ className="udb-header__icon-btn"
118
115
  onClick={() => {
119
116
  setNotifOpen(!notifOpen);
120
117
  setProfileOpen(false);
121
118
  }}
122
119
  >
123
- <Bell className="h-4 w-4" />
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="fixed inset-0 z-40" onClick={() => setNotifOpen(false)} />
135
- <div className="absolute top-12 right-0 w-80 bg-popover border border-border rounded-xl shadow-xl z-50 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
136
- <div className="flex items-center justify-between px-4 py-3 border-b border-border bg-muted/50">
137
- <h3 className="text-xs font-bold uppercase tracking-wider text-foreground">{t('dashboard.notifications.title')}</h3>
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="text-[10px] font-bold text-primary hover:underline uppercase">{t('dashboard.notifications.markAllRead')}</button>
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="max-h-[400px] overflow-y-auto">
135
+ <div className="udb-notif__list">
144
136
  {notifications.map((n) => (
145
- <div key={n.id} className={`px-4 py-3 border-b border-border last:border-0 flex items-start gap-3 transition-colors hover:bg-muted/30 ${!n.read ? "bg-primary/5" : ""}`}>
146
- <div className="flex-1 min-w-0">
147
- <p className="text-sm text-foreground leading-snug">{n.text}</p>
148
- <p className="text-[10px] text-muted-foreground mt-1 flex items-center gap-1">
149
- <span className="inline-block w-1 h-1 rounded-full bg-muted-foreground/30" />
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="text-muted-foreground/50 hover:text-foreground shrink-0 transition-colors"
148
+ className="udb-notif__dismiss"
156
149
  >
157
- <X className="h-3 w-3" />
150
+ <X className="udb-notif__dismiss-icon" />
158
151
  </button>
159
152
  </div>
160
153
  ))}
161
154
  {notifications.length === 0 && (
162
- <div className="px-4 py-10 text-center">
163
- <div className="mx-auto w-10 h-10 rounded-full bg-muted flex items-center justify-center mb-3">
164
- <Bell className="h-5 w-5 text-muted-foreground/50" />
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="text-sm text-muted-foreground">{t('dashboard.notifications.none')}</p>
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="h-6 w-px bg-border mx-1" />
168
+ <div className="udb-header__divider" />
176
169
 
177
- <div className="relative">
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="flex items-center gap-2 p-1 pl-1 rounded-full hover:bg-muted transition-colors group"
177
+ className="udb-profile-btn"
184
178
  >
185
- <div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-[10px] font-bold text-primary overflow-hidden border border-primary/20 ring-primary/20 group-hover:ring-4 transition-all">
186
- {user?.avatar && <img src={user.avatar} alt="User" className="w-full h-full object-cover" />}
187
- </div>
188
- <ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform duration-200 ${profileOpen && "rotate-180"}`} />
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="fixed inset-0 z-40" onClick={() => setProfileOpen(false)} />
194
- <div className="absolute top-12 right-0 w-56 bg-popover border border-border rounded-xl shadow-xl z-50 overflow-hidden animate-in fade-in zoom-in-95 duration-200 p-1.5">
195
- <div className="px-3 py-2 border-b border-border/50 mb-1">
196
- <p className="text-sm font-bold text-foreground">{user?.name || t("common.roles.user")}</p>
197
- <p className="text-xs text-muted-foreground truncate">{user?.email}</p>
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="space-y-0.5">
194
+ <div className="udb-profile-menu__items">
201
195
  <button
196
+ type="button"
202
197
  onClick={() => { navigate(profileRoute); setProfileOpen(false); }}
203
- className="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-foreground hover:bg-muted rounded-lg transition-colors"
198
+ className="udb-menu__item"
204
199
  >
205
- <User className="h-4 w-4 text-muted-foreground" />
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="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-foreground hover:bg-muted rounded-lg transition-colors"
206
+ className="udb-menu__item"
211
207
  >
212
- <CreditCard className="h-4 w-4 text-muted-foreground" />
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="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-foreground hover:bg-muted rounded-lg transition-colors"
214
+ className="udb-menu__item"
218
215
  >
219
- <Settings className="h-4 w-4 text-muted-foreground" />
216
+ <Settings className="udb-menu__item-icon" />
220
217
  {t('common.settings')}
221
218
  </button>
222
219
  </div>
223
220
 
224
- <div className="h-px bg-border my-1.5" />
221
+ <div className="udb-profile-menu__separator" />
225
222
 
226
223
  <button
224
+ type="button"
227
225
  onClick={handleLogout}
228
- className="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors font-medium"
226
+ className="udb-menu__item udb-menu__item--danger"
229
227
  >
230
- <LogOut className="h-4 w-4" />
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 { Skeleton } from "@umituz/web-design-system/atoms";
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(true);
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), 300);
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 (!activeItem) return config.extraTitleMap?.[location.pathname] || "Dashboard";
81
- return activeItem.label; // Note: In real app, this would be translated
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="flex h-screen w-full bg-background font-sans">
111
+ <div className="udb-root udb-layout">
88
112
  {/* Desktop Sidebar */}
89
113
  <aside
90
- className={`hidden md:flex flex-col shrink-0 border-r border-sidebar-border bg-sidebar transition-all duration-300 ${
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="fixed inset-0 z-50 md:hidden">
107
- <div className="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={() => setMobileOpen(false)} />
108
- <aside className="absolute left-0 top-0 h-full w-60 border-r border-sidebar-border bg-sidebar shadow-xl">
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="flex flex-1 flex-col overflow-hidden min-w-0">
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="flex-1 overflow-y-auto p-4 md:p-8">
159
+ <main className="udb-layout__content">
136
160
  {loading ? (
137
- <div className="mx-auto w-full max-w-7xl space-y-6">
138
- <Skeleton className="h-8 w-1/3 rounded-xl" />
139
- <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
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="h-28 rounded-2xl" />
165
+ <Skeleton key={i} className="udb-skeleton--card" />
142
166
  ))}
143
167
  </div>
144
- <Skeleton className="h-64 rounded-[32px]" />
168
+ <Skeleton className="udb-skeleton--section" />
145
169
  </div>
146
170
  ) : (
147
171
  <Outlet />