@djangocfg/layouts 2.1.319 → 2.1.320
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.320",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -74,14 +74,14 @@
|
|
|
74
74
|
"check": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/debuger": "^2.1.
|
|
80
|
-
"@djangocfg/i18n": "^2.1.
|
|
81
|
-
"@djangocfg/monitor": "^2.1.
|
|
82
|
-
"@djangocfg/ui-core": "^2.1.
|
|
83
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
84
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
77
|
+
"@djangocfg/api": "^2.1.320",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.320",
|
|
79
|
+
"@djangocfg/debuger": "^2.1.320",
|
|
80
|
+
"@djangocfg/i18n": "^2.1.320",
|
|
81
|
+
"@djangocfg/monitor": "^2.1.320",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.320",
|
|
83
|
+
"@djangocfg/ui-nextjs": "^2.1.320",
|
|
84
|
+
"@djangocfg/ui-tools": "^2.1.320",
|
|
85
85
|
"@hookform/resolvers": "^5.2.2",
|
|
86
86
|
"consola": "^3.4.2",
|
|
87
87
|
"lucide-react": "^0.545.0",
|
|
@@ -111,15 +111,15 @@
|
|
|
111
111
|
"uuid": "^11.1.0"
|
|
112
112
|
},
|
|
113
113
|
"devDependencies": {
|
|
114
|
-
"@djangocfg/api": "^2.1.
|
|
115
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
116
|
-
"@djangocfg/debuger": "^2.1.
|
|
117
|
-
"@djangocfg/i18n": "^2.1.
|
|
118
|
-
"@djangocfg/monitor": "^2.1.
|
|
119
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
120
|
-
"@djangocfg/ui-core": "^2.1.
|
|
121
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
122
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
114
|
+
"@djangocfg/api": "^2.1.320",
|
|
115
|
+
"@djangocfg/centrifugo": "^2.1.320",
|
|
116
|
+
"@djangocfg/debuger": "^2.1.320",
|
|
117
|
+
"@djangocfg/i18n": "^2.1.320",
|
|
118
|
+
"@djangocfg/monitor": "^2.1.320",
|
|
119
|
+
"@djangocfg/typescript-config": "^2.1.320",
|
|
120
|
+
"@djangocfg/ui-core": "^2.1.320",
|
|
121
|
+
"@djangocfg/ui-nextjs": "^2.1.320",
|
|
122
|
+
"@djangocfg/ui-tools": "^2.1.320",
|
|
123
123
|
"@types/node": "^24.7.2",
|
|
124
124
|
"@types/react": "^19.1.0",
|
|
125
125
|
"@types/react-dom": "^19.1.0",
|
|
@@ -86,6 +86,17 @@ export interface HeaderConfig {
|
|
|
86
86
|
groups?: UserMenuConfig['groups'];
|
|
87
87
|
/** Auth page path (for sign in button) */
|
|
88
88
|
authPath?: string;
|
|
89
|
+
/** Subtitle under the display name in the sidebar footer (e.g. "Max plan"). */
|
|
90
|
+
userPlan?: string;
|
|
91
|
+
/** Optional secondary action button rendered inside the footer trigger (e.g. Get apps download button). */
|
|
92
|
+
footerSecondaryAction?: {
|
|
93
|
+
icon: string | LucideIconType;
|
|
94
|
+
href?: string;
|
|
95
|
+
onClick?: () => void;
|
|
96
|
+
ariaLabel: string;
|
|
97
|
+
/** Show pulsing accent dot on the action (Claude-style "new"). */
|
|
98
|
+
pulse?: boolean;
|
|
99
|
+
};
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
export interface PrivateLayoutProps {
|
|
@@ -47,12 +47,13 @@ function navDensityFromCount(n: number): NavDensity {
|
|
|
47
47
|
* Nav rows use semantic sidebar tokens so light/dark follows ui-core theme vars.
|
|
48
48
|
*/
|
|
49
49
|
const navItemClass = cn(
|
|
50
|
-
'border-0 font-medium shadow-none transition-colors',
|
|
50
|
+
'group/nav border-0 font-medium shadow-none transition-colors',
|
|
51
51
|
'text-sidebar-foreground/80',
|
|
52
52
|
'data-[active=true]:font-semibold data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
|
53
53
|
'hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground',
|
|
54
54
|
'data-[active=true]:hover:bg-sidebar-accent',
|
|
55
55
|
'[&>svg]:shrink-0 [&>svg]:text-sidebar-foreground/70 [&>svg]:opacity-85',
|
|
56
|
+
'[&>svg]:transition-transform [&>svg]:duration-200 group-hover/nav:[&>svg]:scale-110 group-active/nav:[&>svg]:scale-95',
|
|
56
57
|
'data-[active=true]:[&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:opacity-100',
|
|
57
58
|
);
|
|
58
59
|
|
|
@@ -180,6 +181,7 @@ export function PrivateSidebar({ sidebar, header, pathname: pathnameProp, varian
|
|
|
180
181
|
|
|
181
182
|
return sidebar.groups.map((group) => {
|
|
182
183
|
if (group.dynamic && group.items.length === 0) return null;
|
|
184
|
+
const hasLabel = Boolean(group.label && group.label.trim().length > 0);
|
|
183
185
|
const items = group.items.map((item: SidebarItem) => {
|
|
184
186
|
const iconProp = typeof item.icon === 'string' ? item.icon : item.icon;
|
|
185
187
|
const tooltipText = item.tooltip ?? item.label;
|
|
@@ -202,9 +204,12 @@ export function PrivateSidebar({ sidebar, header, pathname: pathnameProp, varian
|
|
|
202
204
|
);
|
|
203
205
|
});
|
|
204
206
|
|
|
207
|
+
const groupKey = group.label || `__flat_${group.items.map((i) => i.href).join('|')}`;
|
|
205
208
|
return (
|
|
206
|
-
<SidebarGroup key={
|
|
207
|
-
|
|
209
|
+
<SidebarGroup key={groupKey} className={sidebarGroupClass}>
|
|
210
|
+
{hasLabel ? (
|
|
211
|
+
<SidebarGroupLabel className={groupLabelClass}>{group.label}</SidebarGroupLabel>
|
|
212
|
+
) : null}
|
|
208
213
|
<SidebarGroupContent>
|
|
209
214
|
<SidebarMenu className={menuNav.menu}>{items}</SidebarMenu>
|
|
210
215
|
</SidebarGroupContent>
|
|
@@ -305,11 +310,16 @@ export function PrivateSidebar({ sidebar, header, pathname: pathnameProp, varian
|
|
|
305
310
|
const railExpandHintClass =
|
|
306
311
|
!isMobile && state === 'collapsed' ? 'cursor-pointer' : undefined;
|
|
307
312
|
|
|
313
|
+
const sidebarRootClass = cn(
|
|
314
|
+
railExpandHintClass,
|
|
315
|
+
'[&>[data-sidebar=sidebar]]:bg-gradient-to-t [&>[data-sidebar=sidebar]]:from-sidebar/85 [&>[data-sidebar=sidebar]]:to-sidebar',
|
|
316
|
+
);
|
|
317
|
+
|
|
308
318
|
return (
|
|
309
319
|
<Sidebar
|
|
310
320
|
collapsible="icon"
|
|
311
321
|
variant={variant}
|
|
312
|
-
className={
|
|
322
|
+
className={sidebarRootClass}
|
|
313
323
|
onClick={expandOnRailClick}
|
|
314
324
|
>
|
|
315
325
|
<SidebarHeader className={sidebarHeaderClass}>{sidebarHeaderContent}</SidebarHeader>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Sidebar account: rich footer trigger (avatar + name + plan + optional secondary
|
|
3
|
+
* action) opens a popover (DropdownMenu) upward with email, account links, locale +
|
|
4
|
+
* theme controls, and sign-out. Replaces the legacy inline collapsible.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
'use client';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { ChevronRight, ChevronsUpDown, Globe, LogOut, Monitor, Moon, Sun } from 'lucide-react';
|
|
9
10
|
import { Link } from '@djangocfg/ui-core/components';
|
|
10
11
|
import React from 'react';
|
|
11
12
|
|
|
@@ -16,65 +17,47 @@ import {
|
|
|
16
17
|
AvatarFallback,
|
|
17
18
|
AvatarImage,
|
|
18
19
|
Button,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
DropdownMenu,
|
|
21
|
+
DropdownMenuContent,
|
|
22
|
+
DropdownMenuItem,
|
|
23
|
+
DropdownMenuLabel,
|
|
24
|
+
DropdownMenuSeparator,
|
|
25
|
+
DropdownMenuTrigger,
|
|
22
26
|
} from '@djangocfg/ui-core/components';
|
|
23
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
27
|
+
import { cn, isDev } from '@djangocfg/ui-core/lib';
|
|
24
28
|
import { useSidebar } from '@djangocfg/ui-nextjs/components';
|
|
25
|
-
import {
|
|
29
|
+
import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
|
|
26
30
|
|
|
27
31
|
import { useLogout } from '../../hooks';
|
|
28
|
-
import {
|
|
32
|
+
import { LocaleSwitcherDialog } from './locale-switcher';
|
|
33
|
+
import { getLocaleMeta } from './locale-switcher/localeMeta';
|
|
29
34
|
import { useLayoutI18nOptional } from '../AppLayout/LayoutI18nProvider';
|
|
35
|
+
import { LucideIcon as LucideIconRender } from '../../components';
|
|
30
36
|
|
|
31
37
|
import type { HeaderConfig } from '../PrivateLayout/PrivateLayout';
|
|
32
38
|
|
|
33
|
-
/** Radix portals (dropdown, select, popover) render outside the account node — ignore those clicks for “outside”. */
|
|
34
|
-
function isPointerFromRadixOverlay(target: EventTarget | null): boolean {
|
|
35
|
-
if (!target || !(target instanceof Element)) return false;
|
|
36
|
-
return Boolean(
|
|
37
|
-
target.closest('[data-radix-popper-content-wrapper]') ||
|
|
38
|
-
target.closest('[data-radix-dropdown-menu-content]') ||
|
|
39
|
-
target.closest('[data-radix-select-content]') ||
|
|
40
|
-
target.closest('[data-radix-popover-content]'),
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
39
|
interface PrivateSidebarAccountProps {
|
|
45
40
|
header?: HeaderConfig;
|
|
46
41
|
}
|
|
47
42
|
|
|
43
|
+
interface AccountView {
|
|
44
|
+
source: 'user' | 'dev-fallback';
|
|
45
|
+
displayName: string;
|
|
46
|
+
email: string | null;
|
|
47
|
+
avatarUrl: string;
|
|
48
|
+
plan: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
48
51
|
export function PrivateSidebarAccount({ header }: PrivateSidebarAccountProps) {
|
|
49
52
|
const { user } = useAuth();
|
|
50
53
|
const handleLogout = useLogout();
|
|
51
54
|
const t = useAppT();
|
|
52
55
|
const layoutI18n = useLayoutI18nOptional();
|
|
53
56
|
const { state, setOpen: setSidebarOpen } = useSidebar();
|
|
54
|
-
const
|
|
55
|
-
const
|
|
57
|
+
const { theme, setTheme } = useThemeContext();
|
|
58
|
+
const [langDialogOpen, setLangDialogOpen] = React.useState(false);
|
|
56
59
|
const narrow = state === 'collapsed';
|
|
57
60
|
|
|
58
|
-
React.useEffect(() => {
|
|
59
|
-
if (state === 'collapsed') setAccountOpen(false);
|
|
60
|
-
}, [state]);
|
|
61
|
-
|
|
62
|
-
React.useEffect(() => {
|
|
63
|
-
if (!accountOpen) return;
|
|
64
|
-
|
|
65
|
-
const handlePointerDown = (event: PointerEvent) => {
|
|
66
|
-
const root = accountRootRef.current;
|
|
67
|
-
const target = event.target;
|
|
68
|
-
if (!(target instanceof Node)) return;
|
|
69
|
-
if (root?.contains(target)) return;
|
|
70
|
-
if (isPointerFromRadixOverlay(target)) return;
|
|
71
|
-
setAccountOpen(false);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
document.addEventListener('pointerdown', handlePointerDown);
|
|
75
|
-
return () => document.removeEventListener('pointerdown', handlePointerDown);
|
|
76
|
-
}, [accountOpen]);
|
|
77
|
-
|
|
78
61
|
const signOutLabel = t('layouts.profile.signOut');
|
|
79
62
|
|
|
80
63
|
const accountLinks = React.useMemo(() => {
|
|
@@ -82,134 +65,280 @@ export function PrivateSidebarAccount({ header }: PrivateSidebarAccountProps) {
|
|
|
82
65
|
return header.groups.flatMap((g) => g.items.filter((i) => i.href));
|
|
83
66
|
}, [header?.groups]);
|
|
84
67
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Single source of truth for what the footer renders. When `useAuth` returns
|
|
70
|
+
* a real user we use it; otherwise (in dev only) we fall back to a placeholder
|
|
71
|
+
* so the menu stays reachable for debugging. Production stays strict and the
|
|
72
|
+
* component returns null below.
|
|
73
|
+
*/
|
|
74
|
+
const account = React.useMemo<AccountView>(() => {
|
|
75
|
+
if (user) {
|
|
76
|
+
return {
|
|
77
|
+
source: 'user',
|
|
78
|
+
displayName: user.display_username || user.email || 'User',
|
|
79
|
+
email: user.email ?? null,
|
|
80
|
+
avatarUrl: user.avatar ?? '',
|
|
81
|
+
plan: header?.userPlan ?? null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
source: 'dev-fallback',
|
|
86
|
+
displayName: 'Guest (dev)',
|
|
87
|
+
email: null,
|
|
88
|
+
avatarUrl: '',
|
|
89
|
+
plan: header?.userPlan ?? 'No session',
|
|
90
|
+
};
|
|
91
|
+
}, [user, header?.userPlan]);
|
|
88
92
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
const onTriggerInteract = React.useCallback(() => {
|
|
94
|
+
if (narrow) setSidebarOpen(true);
|
|
95
|
+
}, [narrow, setSidebarOpen]);
|
|
96
|
+
|
|
97
|
+
const onSecondaryExpand = React.useCallback(() => {
|
|
98
|
+
setSidebarOpen(true);
|
|
99
|
+
}, [setSidebarOpen]);
|
|
100
|
+
|
|
101
|
+
const onLogoutSelect = React.useCallback((e: Event) => {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
handleLogout();
|
|
104
|
+
}, [handleLogout]);
|
|
105
|
+
|
|
106
|
+
const onLanguageSelect = React.useCallback((e: Event) => {
|
|
107
|
+
// Keep the dropdown closed (default behaviour) but defer dialog mount to
|
|
108
|
+
// the next tick so Radix has time to unmount the dropdown overlay first
|
|
109
|
+
// (avoids the "two open overlays steal focus" bug).
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
setTimeout(() => setLangDialogOpen(true), 0);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const onThemeSelect = React.useCallback((e: Event) => {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
// Cycle: light → dark → system → light
|
|
117
|
+
const next = theme === 'light' ? 'dark' : theme === 'dark' ? 'system' : 'light';
|
|
118
|
+
setTheme(next);
|
|
119
|
+
}, [theme, setTheme]);
|
|
120
|
+
|
|
121
|
+
// Hide entirely in production when there's no user (auth still loading or
|
|
122
|
+
// /me failed and the parent guard hasn't redirected yet). In dev keep a
|
|
123
|
+
// placeholder so the footer + Log out are reachable for debugging.
|
|
124
|
+
// NOTE: this early-return must stay AFTER all hooks above to keep hook order stable.
|
|
125
|
+
if (!user && !isDev) return null;
|
|
126
|
+
|
|
127
|
+
const userInitial = (account.displayName.charAt(0) || '?').toUpperCase();
|
|
128
|
+
const secondary = header?.footerSecondaryAction;
|
|
93
129
|
|
|
94
130
|
const triggerClassName = cn(
|
|
95
|
-
'h-auto
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const chevronClassName = cn(
|
|
99
|
-
'h-4 w-4 shrink-0 text-sidebar-foreground/65 transition-transform duration-200',
|
|
100
|
-
accountOpen && 'rotate-180',
|
|
131
|
+
'group h-auto w-full gap-3 rounded-lg px-2 py-2 text-left',
|
|
132
|
+
'hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground',
|
|
133
|
+
narrow ? 'justify-center px-0 py-1.5' : 'min-h-14',
|
|
101
134
|
);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
|
|
136
|
+
const secondaryButton = secondary && !narrow ? (
|
|
137
|
+
<SecondaryAction action={secondary} onParentExpand={onSecondaryExpand} />
|
|
138
|
+
) : null;
|
|
139
|
+
|
|
140
|
+
const dropdownContentClass = cn(
|
|
141
|
+
'p-1.5',
|
|
142
|
+
narrow ? 'min-w-60' : 'w-[var(--radix-dropdown-menu-trigger-width)] min-w-60',
|
|
105
143
|
);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
144
|
+
const dropdownSide: 'top' | 'right' = narrow ? 'right' : 'top';
|
|
145
|
+
const avatarClass = cn(
|
|
146
|
+
'h-9 w-9 shrink-0 border border-transparent transition-colors',
|
|
147
|
+
'group-hover:border-sidebar-border/70',
|
|
109
148
|
);
|
|
110
149
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
150
|
+
const headerLabelText = account.email ?? (account.source === 'dev-fallback' ? 'No active session' : null);
|
|
151
|
+
const headerLabel = headerLabelText ? (
|
|
152
|
+
<DropdownMenuLabel className="truncate px-2 py-1.5 text-xs font-normal text-muted-foreground">
|
|
153
|
+
{headerLabelText}
|
|
154
|
+
</DropdownMenuLabel>
|
|
155
|
+
) : null;
|
|
114
156
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
const accountLinksItems = accountLinks.map((item) => {
|
|
158
|
+
const Icon = item.icon;
|
|
159
|
+
return (
|
|
160
|
+
<DropdownMenuItem key={item.href} asChild>
|
|
161
|
+
<Link
|
|
162
|
+
href={item.href!}
|
|
163
|
+
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm"
|
|
164
|
+
>
|
|
165
|
+
{Icon ? <Icon className="h-4 w-4 shrink-0 text-muted-foreground" /> : null}
|
|
166
|
+
<span className="truncate">{item.label}</span>
|
|
167
|
+
</Link>
|
|
168
|
+
</DropdownMenuItem>
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
const accountLinksBlock = accountLinks.length > 0 ? (
|
|
172
|
+
<>
|
|
173
|
+
{headerLabel ? <DropdownMenuSeparator /> : null}
|
|
174
|
+
{accountLinksItems}
|
|
175
|
+
</>
|
|
176
|
+
) : null;
|
|
132
177
|
|
|
133
|
-
const
|
|
134
|
-
|
|
178
|
+
const currentLocaleLabel = layoutI18n
|
|
179
|
+
? getLocaleMeta(layoutI18n.locale).native
|
|
180
|
+
: null;
|
|
181
|
+
const languageItem = layoutI18n ? (
|
|
182
|
+
<DropdownMenuItem
|
|
183
|
+
onSelect={onLanguageSelect}
|
|
184
|
+
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm"
|
|
185
|
+
>
|
|
186
|
+
<Globe className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
187
|
+
<span className="flex-1 truncate">Language</span>
|
|
188
|
+
<span className="ml-auto flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
|
|
189
|
+
{currentLocaleLabel}
|
|
190
|
+
<ChevronRight className="h-3.5 w-3.5" aria-hidden />
|
|
191
|
+
</span>
|
|
192
|
+
</DropdownMenuItem>
|
|
135
193
|
) : null;
|
|
136
194
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
195
|
+
const themeIcon = theme === 'dark'
|
|
196
|
+
? <Moon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
197
|
+
: theme === 'light'
|
|
198
|
+
? <Sun className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
199
|
+
: <Monitor className="h-4 w-4 shrink-0 text-muted-foreground" />;
|
|
200
|
+
const themeValueLabel = theme === 'dark' ? 'Dark' : theme === 'light' ? 'Light' : 'System';
|
|
201
|
+
const themeItem = (
|
|
202
|
+
<DropdownMenuItem
|
|
203
|
+
onSelect={onThemeSelect}
|
|
204
|
+
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm"
|
|
205
|
+
>
|
|
206
|
+
{themeIcon}
|
|
207
|
+
<span className="flex-1 truncate">Theme</span>
|
|
208
|
+
<span className="ml-auto shrink-0 text-xs text-muted-foreground">{themeValueLabel}</span>
|
|
209
|
+
</DropdownMenuItem>
|
|
210
|
+
);
|
|
143
211
|
|
|
144
|
-
const
|
|
212
|
+
const expandedMeta = narrow ? null : (
|
|
145
213
|
<>
|
|
146
|
-
<span className="min-w-0 flex-1
|
|
147
|
-
|
|
214
|
+
<span className="flex min-w-0 flex-1 flex-col text-left">
|
|
215
|
+
<span className="truncate text-sm font-medium leading-tight text-sidebar-foreground">
|
|
216
|
+
{account.displayName}
|
|
217
|
+
</span>
|
|
218
|
+
{account.plan ? (
|
|
219
|
+
<span className="truncate text-xs leading-snug text-sidebar-foreground/60">
|
|
220
|
+
{account.plan}
|
|
221
|
+
</span>
|
|
222
|
+
) : null}
|
|
223
|
+
</span>
|
|
224
|
+
<span className="flex shrink-0 items-center gap-1.5">
|
|
225
|
+
{secondaryButton}
|
|
226
|
+
<ChevronsUpDown className="h-3.5 w-3.5 text-sidebar-foreground/55" aria-hidden />
|
|
148
227
|
</span>
|
|
149
|
-
<ChevronDown className={chevronClassName} aria-hidden />
|
|
150
228
|
</>
|
|
151
229
|
);
|
|
152
230
|
|
|
153
|
-
const localeThemeGroup = layoutI18n ? (
|
|
154
|
-
<LocaleSwitcher
|
|
155
|
-
variant="dropdown"
|
|
156
|
-
buttonVariant="ghost"
|
|
157
|
-
size="icon"
|
|
158
|
-
showTriggerLabel={false}
|
|
159
|
-
showIcon={false}
|
|
160
|
-
className="h-8 w-8 shrink-0 text-base leading-none"
|
|
161
|
-
/>
|
|
162
|
-
) : null;
|
|
163
|
-
|
|
164
231
|
return (
|
|
165
|
-
<div
|
|
166
|
-
<
|
|
167
|
-
<
|
|
232
|
+
<div className="w-full min-w-0 border-t border-sidebar-border/45 pt-2">
|
|
233
|
+
<DropdownMenu>
|
|
234
|
+
<DropdownMenuTrigger asChild>
|
|
168
235
|
<Button
|
|
169
236
|
type="button"
|
|
170
237
|
variant="ghost"
|
|
171
|
-
aria-
|
|
172
|
-
aria-label={narrow ? 'Account' : undefined}
|
|
238
|
+
aria-label={narrow ? account.displayName : undefined}
|
|
173
239
|
className={triggerClassName}
|
|
174
|
-
onClick={
|
|
240
|
+
onClick={onTriggerInteract}
|
|
175
241
|
>
|
|
176
|
-
<Avatar className=
|
|
177
|
-
<AvatarImage src={
|
|
178
|
-
<AvatarFallback className="text-
|
|
242
|
+
<Avatar className={avatarClass}>
|
|
243
|
+
<AvatarImage src={account.avatarUrl} alt={account.displayName} />
|
|
244
|
+
<AvatarFallback className="text-sm font-semibold">{userInitial}</AvatarFallback>
|
|
179
245
|
</Avatar>
|
|
180
|
-
{
|
|
246
|
+
{expandedMeta}
|
|
181
247
|
</Button>
|
|
182
|
-
</
|
|
183
|
-
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
248
|
+
</DropdownMenuTrigger>
|
|
249
|
+
|
|
250
|
+
<DropdownMenuContent
|
|
251
|
+
side={dropdownSide}
|
|
252
|
+
align="start"
|
|
253
|
+
sideOffset={8}
|
|
254
|
+
className={dropdownContentClass}
|
|
255
|
+
>
|
|
256
|
+
{headerLabel}
|
|
257
|
+
{accountLinksBlock}
|
|
258
|
+
|
|
259
|
+
<DropdownMenuSeparator />
|
|
260
|
+
{languageItem}
|
|
261
|
+
{themeItem}
|
|
262
|
+
|
|
263
|
+
<DropdownMenuSeparator />
|
|
264
|
+
<DropdownMenuItem
|
|
265
|
+
onSelect={onLogoutSelect}
|
|
266
|
+
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-destructive focus:bg-destructive/10 focus:text-destructive"
|
|
267
|
+
>
|
|
268
|
+
<LogOut className="h-4 w-4 shrink-0" />
|
|
269
|
+
<span className="truncate">{signOutLabel}</span>
|
|
270
|
+
</DropdownMenuItem>
|
|
271
|
+
</DropdownMenuContent>
|
|
272
|
+
</DropdownMenu>
|
|
273
|
+
|
|
274
|
+
{layoutI18n ? (
|
|
275
|
+
<LocaleSwitcherDialog
|
|
276
|
+
open={langDialogOpen}
|
|
277
|
+
onOpenChange={setLangDialogOpen}
|
|
278
|
+
locale={layoutI18n.locale}
|
|
279
|
+
locales={layoutI18n.locales}
|
|
280
|
+
onChange={layoutI18n.onLocaleChange}
|
|
281
|
+
brand={layoutI18n.brand}
|
|
282
|
+
i18nLabels={layoutI18n.dialogLabels}
|
|
283
|
+
/>
|
|
284
|
+
) : null}
|
|
213
285
|
</div>
|
|
214
286
|
);
|
|
215
287
|
}
|
|
288
|
+
|
|
289
|
+
interface SecondaryActionProps {
|
|
290
|
+
action: NonNullable<HeaderConfig['footerSecondaryAction']>;
|
|
291
|
+
onParentExpand: () => void;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function SecondaryAction({ action, onParentExpand }: SecondaryActionProps) {
|
|
295
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
296
|
+
// Don't open the parent dropdown when interacting with the secondary action.
|
|
297
|
+
e.stopPropagation();
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
onParentExpand();
|
|
300
|
+
action.onClick?.();
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const inner = (
|
|
304
|
+
<span
|
|
305
|
+
className={cn(
|
|
306
|
+
'relative inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md',
|
|
307
|
+
'border border-sidebar-border/40 bg-transparent text-sidebar-foreground/70',
|
|
308
|
+
'transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground',
|
|
309
|
+
)}
|
|
310
|
+
>
|
|
311
|
+
<LucideIconRender icon={action.icon} className="h-4 w-4" />
|
|
312
|
+
{action.pulse ? (
|
|
313
|
+
<span className="pointer-events-none absolute -right-0.5 -top-0.5 flex h-2 w-2">
|
|
314
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" />
|
|
315
|
+
<span className="relative inline-flex h-2 w-2 rounded-full bg-primary" />
|
|
316
|
+
</span>
|
|
317
|
+
) : null}
|
|
318
|
+
</span>
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (action.href) {
|
|
322
|
+
return (
|
|
323
|
+
<Link
|
|
324
|
+
href={action.href}
|
|
325
|
+
aria-label={action.ariaLabel}
|
|
326
|
+
onClick={handleClick}
|
|
327
|
+
data-no-expand
|
|
328
|
+
>
|
|
329
|
+
{inner}
|
|
330
|
+
</Link>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
aria-label={action.ariaLabel}
|
|
338
|
+
onClick={handleClick}
|
|
339
|
+
data-no-expand
|
|
340
|
+
>
|
|
341
|
+
{inner}
|
|
342
|
+
</button>
|
|
343
|
+
);
|
|
344
|
+
}
|