@shellui/core 0.2.0 → 0.3.0-beta.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.
Files changed (111) hide show
  1. package/package.json +9 -4
  2. package/src/app.tsx +12 -9
  3. package/src/components/ui/badge.tsx +35 -0
  4. package/src/components/ui/dropdown-menu.tsx +94 -0
  5. package/src/components/ui/sidebar.tsx +1 -1
  6. package/src/constants/urls.ts +8 -0
  7. package/src/features/admin/AdminView.tsx +154 -0
  8. package/src/features/admin/components/AdminForbiddenAccess.tsx +10 -0
  9. package/src/features/auth/AuthProvider.tsx +464 -0
  10. package/src/features/auth/backends/index.ts +41 -0
  11. package/src/features/auth/backends/shellui.ts +278 -0
  12. package/src/features/auth/backends/supabase.ts +300 -0
  13. package/src/features/auth/backends/types.ts +30 -0
  14. package/src/features/auth/components/LoginButton.tsx +360 -0
  15. package/src/features/auth/components/LoginButtonIcons.tsx +48 -0
  16. package/src/features/auth/components/LoginView.tsx +721 -0
  17. package/src/features/auth/components/OAuthCallbackView.tsx +119 -0
  18. package/src/features/auth/hooks/useAuth.tsx +37 -0
  19. package/src/features/auth/types.ts +51 -0
  20. package/src/features/auth/utils/buildSessionFromParams.spec.ts +61 -0
  21. package/src/features/auth/utils/buildSessionFromParams.ts +79 -0
  22. package/src/features/auth/utils/clearStoredAuthSession.spec.ts +23 -0
  23. package/src/features/auth/utils/clearStoredAuthSession.ts +10 -0
  24. package/src/features/auth/utils/clientLoginContext.spec.ts +83 -0
  25. package/src/features/auth/utils/clientLoginContext.ts +89 -0
  26. package/src/features/auth/utils/decodeJwtPayload.spec.ts +17 -0
  27. package/src/features/auth/utils/decodeJwtPayload.ts +24 -0
  28. package/src/features/auth/utils/formatProviderLabel.spec.ts +19 -0
  29. package/src/features/auth/utils/formatProviderLabel.ts +11 -0
  30. package/src/features/auth/utils/getOAuthProviderCandidates.spec.ts +16 -0
  31. package/src/features/auth/utils/getOAuthProviderCandidates.ts +15 -0
  32. package/src/features/auth/utils/getPreferredBackendProvider.spec.ts +15 -0
  33. package/src/features/auth/utils/getPreferredBackendProvider.ts +10 -0
  34. package/src/features/auth/utils/getProviderVisual.spec.ts +30 -0
  35. package/src/features/auth/utils/getProviderVisual.ts +83 -0
  36. package/src/features/auth/utils/getUserFromSdkSettings.spec.ts +32 -0
  37. package/src/features/auth/utils/getUserFromSdkSettings.ts +13 -0
  38. package/src/features/auth/utils/index.ts +21 -0
  39. package/src/features/auth/utils/isLoginMethod.spec.ts +18 -0
  40. package/src/features/auth/utils/isLoginMethod.ts +5 -0
  41. package/src/features/auth/utils/isSessionExpired.spec.ts +23 -0
  42. package/src/features/auth/utils/isSessionExpired.ts +5 -0
  43. package/src/features/auth/utils/normalizeAuthSettings.spec.ts +60 -0
  44. package/src/features/auth/utils/normalizeAuthSettings.ts +71 -0
  45. package/src/features/auth/utils/normalizeNextPath.spec.ts +21 -0
  46. package/src/features/auth/utils/normalizeNextPath.ts +12 -0
  47. package/src/features/auth/utils/normalizeRedirectPath.spec.ts +12 -0
  48. package/src/features/auth/utils/normalizeRedirectPath.ts +3 -0
  49. package/src/features/auth/utils/persistAuthSession.spec.ts +35 -0
  50. package/src/features/auth/utils/persistAuthSession.ts +12 -0
  51. package/src/features/auth/utils/readStoredAuthSession.spec.ts +34 -0
  52. package/src/features/auth/utils/readStoredAuthSession.ts +14 -0
  53. package/src/features/auth/utils/toAuthSessionFromSettingsUser.spec.ts +76 -0
  54. package/src/features/auth/utils/toAuthSessionFromSettingsUser.ts +36 -0
  55. package/src/features/config/types.ts +55 -0
  56. package/src/features/layouts/AppLayout.tsx +8 -6
  57. package/src/features/layouts/appbar/AppBarLayout.tsx +42 -23
  58. package/src/features/layouts/fullscreen/FullscreenLayout.tsx +3 -2
  59. package/src/features/layouts/sidebar/SidebarInner.tsx +7 -5
  60. package/src/features/layouts/sidebar/SidebarLayout.tsx +16 -3
  61. package/src/features/layouts/utils.ts +54 -0
  62. package/src/features/layouts/windows/WindowsLayout.tsx +22 -4
  63. package/src/features/legal/LegalDocumentContent.tsx +102 -0
  64. package/src/features/legal/LegalDocumentView.tsx +42 -0
  65. package/src/features/legal/LegalDocumentsIndexView.tsx +51 -0
  66. package/src/features/legal/LegalDocumentsLinks.tsx +29 -0
  67. package/src/features/legal/legalDocuments.ts +62 -0
  68. package/src/features/settings/SettingsIcons.tsx +20 -0
  69. package/src/features/settings/SettingsProvider.tsx +347 -245
  70. package/src/features/settings/SettingsRoutes.tsx +8 -0
  71. package/src/features/settings/SettingsView.tsx +43 -8
  72. package/src/features/settings/components/Develop.tsx +2 -2
  73. package/src/features/settings/components/LegalDocumentsPanel.tsx +46 -0
  74. package/src/features/settings/components/UserIcon.tsx +20 -0
  75. package/src/features/settings/components/UserSettingsPanel.tsx +438 -0
  76. package/src/features/settings/components/createUserSettingsRoute.tsx +43 -0
  77. package/src/features/settings/utils/buildSettingsForPropagation.spec.ts +167 -0
  78. package/src/features/settings/utils/buildSettingsForPropagation.ts +61 -0
  79. package/src/features/settings/utils/flattenNavigationItems.spec.ts +17 -0
  80. package/src/features/settings/utils/flattenNavigationItems.ts +12 -0
  81. package/src/features/settings/utils/getAvailableThemesForSettings.spec.ts +15 -0
  82. package/src/features/settings/utils/getAvailableThemesForSettings.ts +16 -0
  83. package/src/features/settings/utils/getBrowserTimezone.spec.ts +11 -0
  84. package/src/features/settings/utils/getBrowserTimezone.ts +7 -0
  85. package/src/features/settings/utils/getPreferenceSnapshot.spec.ts +35 -0
  86. package/src/features/settings/utils/getPreferenceSnapshot.ts +10 -0
  87. package/src/features/settings/utils/getResolvedAppearanceForSettings.spec.ts +97 -0
  88. package/src/features/settings/utils/getResolvedAppearanceForSettings.ts +48 -0
  89. package/src/features/settings/utils/index.ts +12 -0
  90. package/src/features/settings/utils/isSameUser.spec.ts +35 -0
  91. package/src/features/settings/utils/isSameUser.ts +17 -0
  92. package/src/features/settings/utils/mergePreferencesIntoSettings.spec.ts +108 -0
  93. package/src/features/settings/utils/mergePreferencesIntoSettings.ts +47 -0
  94. package/src/features/settings/utils/resolveColorMode.spec.ts +29 -0
  95. package/src/features/settings/utils/resolveColorMode.ts +6 -0
  96. package/src/features/settings/utils/resolveLabel.spec.ts +17 -0
  97. package/src/features/settings/utils/resolveLabel.ts +7 -0
  98. package/src/features/settings/utils/toAbsoluteFontUrls.spec.ts +26 -0
  99. package/src/features/settings/utils/toAbsoluteFontUrls.ts +15 -0
  100. package/src/features/settings/utils/toSettingsUser.spec.ts +49 -0
  101. package/src/features/settings/utils/toSettingsUser.ts +15 -0
  102. package/src/i18n/translations/en/common.json +14 -0
  103. package/src/i18n/translations/en/settings.json +45 -0
  104. package/src/i18n/translations/fr/common.json +14 -0
  105. package/src/i18n/translations/fr/settings.json +45 -0
  106. package/src/index.css +37 -0
  107. package/src/index.ts +6 -0
  108. package/src/routes/components/NavigationItemRoute.tsx +32 -1
  109. package/src/routes/components/NotFoundView.tsx +13 -3
  110. package/src/routes/hooks/useNavigationItems.ts +19 -4
  111. package/src/routes/routes.tsx +87 -0
@@ -0,0 +1,360 @@
1
+ import { shellui } from '@shellui/sdk';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Link, useLocation, useNavigate } from 'react-router';
5
+ import urls from '../../../constants/urls';
6
+ import { cn } from '../../../lib/utils';
7
+ import { useAuth } from '../hooks/useAuth';
8
+ import { UserIcon } from '../../settings/components/UserIcon';
9
+ import { useConfig } from '../../config/useConfig';
10
+ import { flattenNavigationItems, getNavPathPrefix } from '../../layouts/utils';
11
+ import { LogoutIcon, SidebarCaretIcon } from './LoginButtonIcons';
12
+ import {
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuItem,
16
+ DropdownMenuLabel,
17
+ DropdownMenuSeparator,
18
+ DropdownMenuTrigger,
19
+ } from '../../../components/ui/dropdown-menu';
20
+
21
+ type LoginButtonVariant = 'sidebar' | 'appbar' | 'windows';
22
+
23
+ const AdministrationIcon = ({ className }: { className?: string }) => (
24
+ <svg
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ width="24"
27
+ height="24"
28
+ viewBox="0 0 24 24"
29
+ fill="none"
30
+ stroke="currentColor"
31
+ strokeWidth="2"
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ className={className}
35
+ aria-hidden
36
+ >
37
+ <path d="M12 3l8 4v5c0 5-3.5 8.6-8 9-4.5-.4-8-4-8-9V7l8-4z" />
38
+ <path d="M9.5 12.5 11 14l3.5-3.5" />
39
+ </svg>
40
+ );
41
+
42
+ const SettingsMenuIcon = ({ className }: { className?: string }) => (
43
+ <svg
44
+ xmlns="http://www.w3.org/2000/svg"
45
+ width="24"
46
+ height="24"
47
+ viewBox="0 0 24 24"
48
+ fill="none"
49
+ stroke="currentColor"
50
+ strokeWidth="2"
51
+ strokeLinecap="round"
52
+ strokeLinejoin="round"
53
+ className={className}
54
+ aria-hidden
55
+ >
56
+ <circle
57
+ cx="12"
58
+ cy="12"
59
+ r="3"
60
+ />
61
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
62
+ </svg>
63
+ );
64
+
65
+ const variantConfig: Record<
66
+ LoginButtonVariant,
67
+ {
68
+ button: { authenticated: string; loggedOut: string };
69
+ avatar: string;
70
+ menu: { width: string; side: 'top' | 'right' | 'bottom'; align: 'start' | 'end' };
71
+ showDisplayName: boolean;
72
+ showCaret: boolean;
73
+ }
74
+ > = {
75
+ sidebar: {
76
+ button: {
77
+ authenticated:
78
+ 'w-full h-8 rounded-md px-2 text-sm text-left text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
79
+ loggedOut:
80
+ 'w-full h-8 rounded-md px-2 text-sm text-left text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
81
+ },
82
+ avatar: 'h-5 w-5',
83
+ menu: { width: 'w-64', side: 'right', align: 'start' },
84
+ showDisplayName: true,
85
+ showCaret: true,
86
+ },
87
+ appbar: {
88
+ button: {
89
+ authenticated:
90
+ 'h-8 w-8 rounded-md p-0 justify-center text-sm text-muted-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
91
+ loggedOut:
92
+ 'h-8 max-w-[220px] rounded-md px-2 text-sm text-muted-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
93
+ },
94
+ avatar: 'h-5 w-5',
95
+ menu: { width: 'w-64', side: 'bottom', align: 'end' },
96
+ showDisplayName: false,
97
+ showCaret: false,
98
+ },
99
+ windows: {
100
+ button: {
101
+ authenticated:
102
+ 'h-8 w-8 rounded-md p-0 justify-center text-xs text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
103
+ loggedOut:
104
+ 'h-8 max-w-[180px] rounded-md px-2 text-xs text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
105
+ },
106
+ avatar: 'h-4 w-4',
107
+ menu: { width: 'w-60', side: 'top', align: 'end' },
108
+ showDisplayName: false,
109
+ showCaret: false,
110
+ },
111
+ };
112
+
113
+ export const LoginButton = ({
114
+ variant,
115
+ hideWhenLoggedOut = false,
116
+ logoutOnly = false,
117
+ }: {
118
+ variant: LoginButtonVariant;
119
+ hideWhenLoggedOut?: boolean;
120
+ logoutOnly?: boolean;
121
+ }) => {
122
+ const currentVariantConfig = variantConfig[variant];
123
+ const { t } = useTranslation('common');
124
+ const { config } = useConfig();
125
+ const { isAuthenticated, user, logout } = useAuth();
126
+ const location = useLocation();
127
+ const navigate = useNavigate();
128
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
129
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
130
+ const contentRef = useRef<HTMLDivElement | null>(null);
131
+
132
+ const displayName = useMemo(() => {
133
+ const name = user?.name?.trim();
134
+ const email = user?.email?.trim();
135
+ return name || email || t('authMenu.userFallback');
136
+ }, [t, user?.email, user?.name]);
137
+
138
+ const fallbackInitial = displayName.charAt(0).toUpperCase();
139
+ const { showDisplayName, showCaret } = currentVariantConfig;
140
+
141
+ const baseButtonClasses = cn(
142
+ 'inline-flex items-center gap-2 min-w-0 shrink-0 transition-colors cursor-pointer',
143
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
144
+ 'disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed',
145
+ isAuthenticated
146
+ ? currentVariantConfig.button.authenticated
147
+ : currentVariantConfig.button.loggedOut,
148
+ );
149
+ const isOnRequiredAuthRoute = useMemo(() => {
150
+ const nav = config.navigation;
151
+ if (!nav || nav.length === 0) return false;
152
+ const requiredItems = flattenNavigationItems(nav).filter((item) => item.requiresAuth);
153
+ if (requiredItems.length === 0) return false;
154
+ return requiredItems.some((item) => {
155
+ const pathPrefix = getNavPathPrefix(item);
156
+ return (
157
+ location.pathname === pathPrefix ||
158
+ location.pathname.startsWith(`${pathPrefix === '/' ? '' : pathPrefix}/`)
159
+ );
160
+ });
161
+ }, [config.navigation, location.pathname]);
162
+
163
+ const openProfileSettingsModal = useCallback(() => {
164
+ setIsMenuOpen(false);
165
+ shellui.openModal(`${urls.settings}/user`);
166
+ }, []);
167
+
168
+ const canAccessAdmin = useMemo(
169
+ () => Boolean(user?.isStaff || user?.isCompanyOwner),
170
+ [user?.isCompanyOwner, user?.isStaff],
171
+ );
172
+ const configuredAdminPathname = useMemo(
173
+ () => config.backend?.adminPathname?.trim() ?? null,
174
+ [config.backend?.adminPathname],
175
+ );
176
+ const adminPath =
177
+ configuredAdminPathname && configuredAdminPathname.startsWith('/')
178
+ ? configuredAdminPathname
179
+ : urls.admin;
180
+ const isOnAdminRoute = useMemo(
181
+ () => location.pathname === adminPath || location.pathname.startsWith(`${adminPath}/`),
182
+ [adminPath, location.pathname],
183
+ );
184
+
185
+ const openAdminPanel = useCallback(() => {
186
+ if (!canAccessAdmin) {
187
+ return;
188
+ }
189
+ setIsMenuOpen(false);
190
+ navigate(isOnAdminRoute ? '/' : adminPath);
191
+ }, [adminPath, canAccessAdmin, isOnAdminRoute, navigate]);
192
+
193
+ const handleLogout = useCallback(async () => {
194
+ setIsMenuOpen(false);
195
+ if (isOnRequiredAuthRoute) {
196
+ navigate('/', { replace: true });
197
+ }
198
+ shellui.sendMessageToParent({
199
+ type: 'SHELLUI_LOGOUT',
200
+ payload: {},
201
+ });
202
+ await logout();
203
+ }, [isOnRequiredAuthRoute, logout, navigate]);
204
+
205
+ useEffect(() => {
206
+ if (!isMenuOpen) {
207
+ return;
208
+ }
209
+
210
+ const handlePointerDown = (event: PointerEvent) => {
211
+ const target = event.target as Node | null;
212
+ if (!target) return;
213
+ if (triggerRef.current?.contains(target)) return;
214
+ if (contentRef.current?.contains(target)) return;
215
+ setIsMenuOpen(false);
216
+ };
217
+
218
+ const handleWindowBlur = () => {
219
+ setIsMenuOpen(false);
220
+ };
221
+
222
+ document.addEventListener('pointerdown', handlePointerDown, true);
223
+ window.addEventListener('blur', handleWindowBlur);
224
+
225
+ return () => {
226
+ document.removeEventListener('pointerdown', handlePointerDown, true);
227
+ window.removeEventListener('blur', handleWindowBlur);
228
+ };
229
+ }, [isMenuOpen]);
230
+
231
+ if (!config.backend) {
232
+ return null;
233
+ }
234
+
235
+ if (!isAuthenticated && hideWhenLoggedOut) {
236
+ return null;
237
+ }
238
+
239
+ if (!isAuthenticated) {
240
+ return (
241
+ <Link
242
+ to={urls.login}
243
+ className={baseButtonClasses}
244
+ aria-label={t('authMenu.goToLoginAriaLabel')}
245
+ title={t('authMenu.login')}
246
+ >
247
+ <UserIcon />
248
+ <span className="truncate">{t('authMenu.login')}</span>
249
+ </Link>
250
+ );
251
+ }
252
+
253
+ if (!user) {
254
+ return null;
255
+ }
256
+
257
+ return (
258
+ <DropdownMenu
259
+ open={isMenuOpen}
260
+ onOpenChange={setIsMenuOpen}
261
+ modal={false}
262
+ >
263
+ <DropdownMenuTrigger asChild>
264
+ <button
265
+ ref={triggerRef}
266
+ type="button"
267
+ className={baseButtonClasses}
268
+ title={displayName}
269
+ aria-label={t('authMenu.openAccountMenuAriaLabel', { name: displayName })}
270
+ >
271
+ {user.profilePicture ? (
272
+ <img
273
+ src={user.profilePicture}
274
+ alt={displayName}
275
+ className={cn(
276
+ 'shrink-0 rounded-full border border-sidebar-border object-cover',
277
+ currentVariantConfig.avatar,
278
+ )}
279
+ referrerPolicy="no-referrer"
280
+ />
281
+ ) : (
282
+ <span
283
+ className={cn(
284
+ 'shrink-0 rounded-full border border-sidebar-border bg-muted flex items-center justify-center text-[10px] font-semibold',
285
+ currentVariantConfig.avatar,
286
+ )}
287
+ aria-hidden
288
+ >
289
+ {fallbackInitial}
290
+ </span>
291
+ )}
292
+ {showDisplayName && <span className="truncate">{displayName}</span>}
293
+ {showCaret && (
294
+ <span
295
+ aria-hidden
296
+ className={cn(
297
+ 'ml-auto shrink-0 text-[10px]',
298
+ isMenuOpen ? 'text-foreground' : 'text-muted-foreground',
299
+ )}
300
+ >
301
+ <SidebarCaretIcon
302
+ isOpen={isMenuOpen}
303
+ className="h-3 w-3"
304
+ />
305
+ </span>
306
+ )}
307
+ </button>
308
+ </DropdownMenuTrigger>
309
+ <DropdownMenuContent
310
+ ref={contentRef}
311
+ forceMount
312
+ data-auth-menu-content
313
+ side={currentVariantConfig.menu.side}
314
+ align={currentVariantConfig.menu.align}
315
+ collisionPadding={8}
316
+ className={cn('p-1.5', currentVariantConfig.menu.width)}
317
+ onPointerDownOutside={() => setIsMenuOpen(false)}
318
+ onFocusOutside={() => setIsMenuOpen(false)}
319
+ onEscapeKeyDown={() => setIsMenuOpen(false)}
320
+ >
321
+ <DropdownMenuLabel className="space-y-0.5">
322
+ <p className="truncate text-sm font-semibold text-popover-foreground">{displayName}</p>
323
+ <p className="truncate text-xs font-normal text-muted-foreground">
324
+ {user.email || t('authMenu.noEmail')}
325
+ </p>
326
+ </DropdownMenuLabel>
327
+ <DropdownMenuSeparator />
328
+ {!logoutOnly && !isOnAdminRoute && (
329
+ <DropdownMenuItem onSelect={openProfileSettingsModal}>
330
+ <UserIcon />
331
+ <span>{t('authMenu.profile')}</span>
332
+ </DropdownMenuItem>
333
+ )}
334
+ {logoutOnly && (
335
+ <DropdownMenuItem onSelect={openProfileSettingsModal}>
336
+ <SettingsMenuIcon className="h-4 w-4" />
337
+ <span>{t('authMenu.settings')}</span>
338
+ </DropdownMenuItem>
339
+ )}
340
+ {!logoutOnly && canAccessAdmin && (
341
+ <DropdownMenuItem onSelect={openAdminPanel}>
342
+ <AdministrationIcon className="h-4 w-4" />
343
+ <span>
344
+ {isOnAdminRoute
345
+ ? t('authMenu.backToHome', { defaultValue: 'Back to home' })
346
+ : t('authMenu.administration')}
347
+ </span>
348
+ </DropdownMenuItem>
349
+ )}
350
+ <DropdownMenuItem
351
+ onSelect={() => void handleLogout()}
352
+ className="text-destructive focus:text-destructive"
353
+ >
354
+ <LogoutIcon className="h-4 w-4" />
355
+ <span>{t('authMenu.logout')}</span>
356
+ </DropdownMenuItem>
357
+ </DropdownMenuContent>
358
+ </DropdownMenu>
359
+ );
360
+ };
@@ -0,0 +1,48 @@
1
+ export const SidebarCaretIcon = ({
2
+ className,
3
+ isOpen,
4
+ }: {
5
+ className?: string;
6
+ isOpen: boolean;
7
+ }) => (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ strokeWidth="2"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ className={className}
19
+ aria-hidden
20
+ >
21
+ {isOpen ? <path d="m15 6-6 6 6 6" /> : <path d="m9 6 6 6-6 6" />}
22
+ </svg>
23
+ );
24
+
25
+ export const LogoutIcon = ({ className }: { className?: string }) => (
26
+ <svg
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ width="24"
29
+ height="24"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ strokeWidth="2"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ className={className}
37
+ aria-hidden
38
+ >
39
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
40
+ <polyline points="16 17 21 12 16 7" />
41
+ <line
42
+ x1="21"
43
+ y1="12"
44
+ x2="9"
45
+ y2="12"
46
+ />
47
+ </svg>
48
+ );