@djangocfg/layouts 2.1.355 → 2.1.357
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 +17 -17
- package/src/layouts/AdminLayout/AdminLayout.tsx +2 -1
- package/src/layouts/AppLayout/AppLayout.tsx +35 -15
- package/src/layouts/AppLayout/BaseApp.tsx +2 -2
- package/src/layouts/AuthLayout/AuthLayout.tsx +26 -19
- package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +10 -4
- package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +10 -10
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -5
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +10 -10
- package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +28 -20
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +11 -5
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -4
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +9 -4
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +12 -5
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +9 -4
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +11 -5
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +15 -5
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +9 -4
- package/src/layouts/AuthLayout/context.tsx +35 -13
- package/src/layouts/AuthLayout/shells/AuthShell.tsx +11 -4
- package/src/layouts/AuthLayout/shells/CenteredShell.tsx +10 -4
- package/src/layouts/AuthLayout/shells/SplitShell.tsx +10 -4
- package/src/layouts/AuthLayout/shells/context.tsx +16 -5
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +32 -247
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +115 -426
- package/src/layouts/{_components → PrivateLayout/components}/PrivateSidebarAccount.tsx +40 -19
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +165 -0
- package/src/layouts/{_components → PrivateLayout/components}/SidebarFeatured.tsx +2 -2
- package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +189 -0
- package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +137 -0
- package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +71 -0
- package/src/layouts/PrivateLayout/components/index.ts +4 -0
- package/src/layouts/PrivateLayout/context.tsx +211 -0
- package/src/layouts/PrivateLayout/density.ts +48 -0
- package/src/layouts/PrivateLayout/hooks/index.ts +13 -0
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +54 -0
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +103 -0
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +113 -0
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +207 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +115 -0
- package/src/layouts/PrivateLayout/index.ts +2 -2
- package/src/layouts/PrivateLayout/types.ts +187 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +44 -183
- package/src/layouts/ProfileLayout/README.md +58 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +197 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/context.tsx +159 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/index.ts +3 -0
- package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +110 -0
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +29 -0
- package/src/layouts/ProfileLayout/components/{TwoFactorSection.tsx → TwoFactorSection/TwoFactorSection.tsx} +1 -1
- package/src/layouts/ProfileLayout/components/TwoFactorSection/index.ts +1 -0
- package/src/layouts/ProfileLayout/components/index.ts +4 -2
- package/src/layouts/ProfileLayout/context.tsx +4 -6
- package/src/layouts/ProfileLayout/hooks/index.ts +2 -0
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +43 -0
- package/src/layouts/ProfileLayout/index.ts +6 -3
- package/src/layouts/ProfileLayout/types.ts +37 -0
- package/src/layouts/{_components → PublicLayout/components}/UserMenu.tsx +3 -3
- package/src/layouts/PublicLayout/components/index.ts +4 -0
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +12 -2
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -1
- package/src/layouts/PublicLayout/primitives/NavActions.tsx +44 -3
- package/src/layouts/PublicLayout/primitives/NavBrand.tsx +4 -2
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +42 -2
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +1 -1
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +60 -1
- package/src/layouts/_components/index.ts +2 -7
- package/src/layouts/index.ts +9 -4
- package/src/layouts/ProfileLayout/__tests__/TwoFactorSection.test.tsx +0 -234
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +0 -198
- /package/src/layouts/{_components → PublicLayout/components}/UserAvatar.tsx +0 -0
|
@@ -1,133 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Private
|
|
3
|
-
*
|
|
2
|
+
* Private Sidebar
|
|
3
|
+
*
|
|
4
|
+
* Composed from smaller components: SidebarBrand, SidebarNavGroup, SidebarSlots.
|
|
5
|
+
* Uses PrivateLayoutContext for all UI state.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
'use client';
|
|
7
9
|
|
|
8
|
-
import {
|
|
9
|
-
Collapsible,
|
|
10
|
-
CollapsibleContent,
|
|
11
|
-
CollapsibleTrigger,
|
|
12
|
-
Link,
|
|
13
|
-
} from '@djangocfg/ui-core/components';
|
|
14
|
-
import { ChevronDown } from 'lucide-react';
|
|
15
|
-
import { usePathname as useNextPathname } from 'next/navigation';
|
|
16
10
|
import React from 'react';
|
|
17
11
|
|
|
18
12
|
import {
|
|
19
13
|
Sidebar,
|
|
20
14
|
SidebarContent,
|
|
21
15
|
SidebarFooter,
|
|
22
|
-
SidebarGroup,
|
|
23
|
-
SidebarGroupContent,
|
|
24
|
-
SidebarGroupLabel,
|
|
25
|
-
SidebarHeader,
|
|
26
|
-
SidebarMenu,
|
|
27
|
-
SidebarMenuBadge,
|
|
28
|
-
SidebarMenuButton,
|
|
29
|
-
SidebarMenuItem,
|
|
30
|
-
SidebarTrigger,
|
|
31
16
|
useSidebar,
|
|
32
17
|
} from '@djangocfg/ui-core/components';
|
|
33
18
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
34
19
|
|
|
35
|
-
import { PrivateSidebarAccount } from '
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
import type {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
SidebarGroupConfig,
|
|
43
|
-
SidebarGroupLabelStyle,
|
|
44
|
-
SidebarItem,
|
|
45
|
-
} from '../PrivateLayout';
|
|
46
|
-
import { SidebarFeatured } from '../../_components/SidebarFeatured';
|
|
47
|
-
|
|
48
|
-
/** Few items → roomier rows; many items → tighter. Same breakpoints for demo, CarAPIS, etc. */
|
|
49
|
-
const DENSITY_COMFORTABLE_MAX = 5;
|
|
50
|
-
const DENSITY_DEFAULT_MAX = 9;
|
|
51
|
-
|
|
52
|
-
type NavDensity = 'comfortable' | 'default' | 'compact';
|
|
53
|
-
|
|
54
|
-
function navDensityFromCount(n: number): NavDensity {
|
|
55
|
-
if (n <= DENSITY_COMFORTABLE_MAX) return 'comfortable';
|
|
56
|
-
if (n <= DENSITY_DEFAULT_MAX) return 'default';
|
|
57
|
-
return 'compact';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Nav rows use semantic sidebar tokens so light/dark follows ui-core theme vars.
|
|
62
|
-
*/
|
|
63
|
-
const navItemBaseClass = cn(
|
|
64
|
-
'group/nav relative border-0 font-medium shadow-none transition-colors',
|
|
65
|
-
'text-sidebar-foreground/80',
|
|
66
|
-
'data-[active=true]:font-semibold data-[active=true]:text-sidebar-accent-foreground',
|
|
67
|
-
'hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground',
|
|
68
|
-
'[&>svg]:shrink-0 [&>svg]:text-sidebar-foreground/70 [&>svg]:opacity-85',
|
|
69
|
-
'[&>svg]:transition-transform [&>svg]:duration-200 group-hover/nav:[&>svg]:scale-110 group-active/nav:[&>svg]:scale-95',
|
|
70
|
-
'data-[active=true]:[&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:opacity-100',
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const ACTIVE_INDICATOR_CLASS: Record<SidebarActiveIndicator, string> = {
|
|
74
|
-
background: cn(
|
|
75
|
-
'data-[active=true]:bg-sidebar-accent',
|
|
76
|
-
'data-[active=true]:hover:bg-sidebar-accent',
|
|
77
|
-
),
|
|
78
|
-
rail: cn(
|
|
79
|
-
'data-[active=true]:after:absolute data-[active=true]:after:right-0',
|
|
80
|
-
'data-[active=true]:after:top-1.5 data-[active=true]:after:bottom-1.5',
|
|
81
|
-
'data-[active=true]:after:w-[2px] data-[active=true]:after:rounded-l-full',
|
|
82
|
-
'data-[active=true]:after:bg-primary',
|
|
83
|
-
),
|
|
84
|
-
both: cn(
|
|
85
|
-
'data-[active=true]:bg-sidebar-accent data-[active=true]:hover:bg-sidebar-accent',
|
|
86
|
-
'data-[active=true]:after:absolute data-[active=true]:after:right-0',
|
|
87
|
-
'data-[active=true]:after:top-1.5 data-[active=true]:after:bottom-1.5',
|
|
88
|
-
'data-[active=true]:after:w-[2px] data-[active=true]:after:rounded-l-full',
|
|
89
|
-
'data-[active=true]:after:bg-primary',
|
|
90
|
-
),
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const DENSITY = {
|
|
94
|
-
comfortable: {
|
|
95
|
-
menu: 'gap-1.5',
|
|
96
|
-
group: 'gap-2',
|
|
97
|
-
groupPad: 'px-2 py-1',
|
|
98
|
-
label:
|
|
99
|
-
'h-7 uppercase text-[10px] font-light leading-none tracking-[0.14em] text-sidebar-foreground/40',
|
|
100
|
-
buttonSize: 'lg' as const,
|
|
101
|
-
iconClass: 'h-5 w-5',
|
|
102
|
-
extraButton: 'rounded-lg !px-3',
|
|
103
|
-
headerRowInset: 'pl-2',
|
|
104
|
-
},
|
|
105
|
-
default: {
|
|
106
|
-
menu: 'gap-1',
|
|
107
|
-
group: 'gap-1.5',
|
|
108
|
-
groupPad: 'px-2 py-1',
|
|
109
|
-
label:
|
|
110
|
-
'uppercase text-[9px] font-light leading-none tracking-[0.12em] text-sidebar-foreground/50',
|
|
111
|
-
buttonSize: 'default' as const,
|
|
112
|
-
iconClass: 'h-4 w-4',
|
|
113
|
-
extraButton: 'rounded-lg',
|
|
114
|
-
headerRowInset: 'pl-1.5',
|
|
115
|
-
},
|
|
116
|
-
compact: {
|
|
117
|
-
menu: 'gap-0.5',
|
|
118
|
-
group: 'gap-0.5',
|
|
119
|
-
groupPad: 'px-2 py-0.5',
|
|
120
|
-
label:
|
|
121
|
-
'h-6 uppercase text-[8px] font-light leading-none tracking-[0.1em] text-sidebar-foreground/40',
|
|
122
|
-
buttonSize: 'sm' as const,
|
|
123
|
-
iconClass: 'h-3.5 w-3.5',
|
|
124
|
-
extraButton: 'rounded-md !px-2',
|
|
125
|
-
headerRowInset: 'pl-1.5',
|
|
126
|
-
},
|
|
127
|
-
} as const;
|
|
128
|
-
|
|
129
|
-
/** Icon rail: always the same geometry — ignore comfortable/compact (larger/smaller rows only when expanded). */
|
|
130
|
-
const RAIL_NAV = DENSITY.default;
|
|
20
|
+
import { PrivateSidebarAccount } from './PrivateSidebarAccount';
|
|
21
|
+
import { PrivateLayoutProvider } from '../context';
|
|
22
|
+
import { useHoverExpand, useShellVisualState, useSidebarKeyboard } from '../hooks';
|
|
23
|
+
import type { HeaderConfig, SidebarConfig } from '../types';
|
|
24
|
+
import { SidebarBrand } from './SidebarBrand';
|
|
25
|
+
import { SidebarNavGroup } from './SidebarNavGroup';
|
|
26
|
+
import { SidebarSlots } from './SidebarSlots';
|
|
131
27
|
|
|
132
28
|
interface PrivateSidebarProps {
|
|
133
29
|
sidebar: SidebarConfig;
|
|
@@ -141,177 +37,73 @@ interface PrivateSidebarProps {
|
|
|
141
37
|
variant?: 'sidebar' | 'inset';
|
|
142
38
|
}
|
|
143
39
|
|
|
144
|
-
export function PrivateSidebar({
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
40
|
+
export function PrivateSidebar({
|
|
41
|
+
sidebar,
|
|
42
|
+
header,
|
|
43
|
+
pathname: pathnameProp,
|
|
44
|
+
variant = 'sidebar',
|
|
45
|
+
}: PrivateSidebarProps) {
|
|
46
|
+
const { state, isMobile, setOpenMobile, setOpen } = useSidebar();
|
|
47
|
+
const pathname = pathnameProp ?? '';
|
|
149
48
|
|
|
150
49
|
React.useEffect(() => {
|
|
151
50
|
if (isMobile) setOpenMobile(false);
|
|
152
51
|
}, [pathname, isMobile, setOpenMobile]);
|
|
153
|
-
const brandTitle = header?.title?.trim() || 'Dashboard';
|
|
154
|
-
const brandMonogram = (header?.brandLetter?.trim().charAt(0) || brandTitle.charAt(0) || 'D').toUpperCase();
|
|
155
|
-
const customBrand = header?.brand;
|
|
156
|
-
|
|
157
|
-
const allItems = React.useMemo(
|
|
158
|
-
() => sidebar.groups.flatMap((g) => g.items),
|
|
159
|
-
[sidebar.groups],
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const density = React.useMemo(() => navDensityFromCount(allItems.length), [allItems.length]);
|
|
163
|
-
const tierNav = DENSITY[density];
|
|
164
|
-
/** Expanded: follow item-count tier; collapsed rail: fixed `default` sizing so icons stay uniform. */
|
|
165
|
-
const menuNav = state === 'collapsed' ? RAIL_NAV : tierNav;
|
|
166
|
-
|
|
167
|
-
const isActive = React.useCallback(
|
|
168
|
-
(href: string) => {
|
|
169
|
-
const matches = pathname === href || pathname.startsWith(`${href}/`);
|
|
170
|
-
if (!matches) return false;
|
|
171
|
-
return !allItems.some(
|
|
172
|
-
(other) =>
|
|
173
|
-
other.href !== href &&
|
|
174
|
-
other.href.startsWith(`${href}/`) &&
|
|
175
|
-
(pathname === other.href || pathname.startsWith(`${other.href}/`)),
|
|
176
|
-
);
|
|
177
|
-
},
|
|
178
|
-
[pathname, allItems],
|
|
179
|
-
);
|
|
180
52
|
|
|
181
|
-
const expanded = state === 'expanded';
|
|
182
|
-
|
|
183
|
-
const headerRowClass = cn('flex items-center gap-2', tierNav.headerRowInset);
|
|
184
|
-
const brandMark = header?.brandIcon ? (
|
|
185
|
-
<LucideIcon icon={header.brandIcon} className="h-4 w-4 text-sidebar-primary-foreground" />
|
|
186
|
-
) : (
|
|
187
|
-
<span className="text-[11px] font-bold leading-none tracking-tight text-sidebar-primary-foreground">
|
|
188
|
-
{brandMonogram}
|
|
189
|
-
</span>
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const hasMenuStart = sidebar.menuStart != null && sidebar.menuStart !== false;
|
|
193
|
-
const hasMenuEnd = sidebar.menuEnd != null && sidebar.menuEnd !== false;
|
|
194
|
-
// Hide slots on the desktop icon rail unless the consumer opted in. Mobile
|
|
195
|
-
// drawer always shows them — there's no rail in the drawer to begin with.
|
|
196
53
|
const collapsedRail = !isMobile && state === 'collapsed';
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
hasMenuEnd && (!collapsedRail || sidebar.menuEndShowOnCollapsed === true);
|
|
201
|
-
const menuStartSlot = showMenuStart ? (
|
|
202
|
-
<div className="w-full min-w-0 shrink-0 px-2">{sidebar.menuStart}</div>
|
|
203
|
-
) : null;
|
|
204
|
-
const menuEndSlot = showMenuEnd ? (
|
|
205
|
-
<div className="w-full min-w-0 shrink-0 px-2">{sidebar.menuEnd}</div>
|
|
206
|
-
) : null;
|
|
207
|
-
|
|
208
|
-
const sidebarContentClass = cn('gap-2', menuNav.group);
|
|
209
|
-
|
|
210
|
-
const activeIndicator: SidebarActiveIndicator = sidebar.activeIndicator ?? 'background';
|
|
211
|
-
const groupLabelStyle: SidebarGroupLabelStyle = sidebar.groupLabelStyle ?? 'uppercase';
|
|
212
|
-
const navButtonClass = cn(navItemBaseClass, ACTIVE_INDICATOR_CLASS[activeIndicator], menuNav.extraButton);
|
|
213
|
-
const groupLabelUppercaseClass = cn('px-2', menuNav.label);
|
|
214
|
-
const groupLabelPlainClass = cn(
|
|
215
|
-
'px-2 text-sm font-semibold text-sidebar-foreground',
|
|
216
|
-
'h-7 leading-none',
|
|
217
|
-
);
|
|
218
|
-
const sidebarGroupClass = cn('gap-0', menuNav.groupPad);
|
|
54
|
+
const { isHoverExpanded, onMouseEnter, onMouseLeave } = useHoverExpand({
|
|
55
|
+
enabled: collapsedRail,
|
|
56
|
+
});
|
|
219
57
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
58
|
+
return (
|
|
59
|
+
<PrivateLayoutProvider
|
|
60
|
+
sidebar={sidebar}
|
|
61
|
+
header={header}
|
|
62
|
+
pathname={pathname}
|
|
63
|
+
isMobile={isMobile}
|
|
64
|
+
state={state}
|
|
65
|
+
isHoverExpanded={isHoverExpanded}
|
|
66
|
+
>
|
|
67
|
+
<PrivateSidebarInner
|
|
68
|
+
sidebar={sidebar}
|
|
69
|
+
header={header}
|
|
70
|
+
variant={variant}
|
|
233
71
|
collapsedRail={collapsedRail}
|
|
72
|
+
setOpen={setOpen}
|
|
73
|
+
onMouseEnter={onMouseEnter}
|
|
74
|
+
onMouseLeave={onMouseLeave}
|
|
234
75
|
/>
|
|
235
|
-
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const featuredSlot = sidebar.featured && !collapsedRail ? (
|
|
239
|
-
<div className="w-full min-w-0 shrink-0 px-2">
|
|
240
|
-
<SidebarFeatured config={sidebar.featured} />
|
|
241
|
-
</div>
|
|
242
|
-
) : null;
|
|
243
|
-
|
|
244
|
-
const expandedHeader = (
|
|
245
|
-
<div className={headerRowClass}>
|
|
246
|
-
<div className="min-w-0 flex-1">
|
|
247
|
-
{customBrand != null && customBrand !== false
|
|
248
|
-
? typeof customBrand === 'string'
|
|
249
|
-
? (
|
|
250
|
-
<Link
|
|
251
|
-
href={homeHref}
|
|
252
|
-
className="flex min-w-0 items-center gap-2 rounded-md py-0.5 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
253
|
-
>
|
|
254
|
-
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{customBrand}</span>
|
|
255
|
-
</Link>
|
|
256
|
-
)
|
|
257
|
-
: customBrand
|
|
258
|
-
: (
|
|
259
|
-
<Link
|
|
260
|
-
href={homeHref}
|
|
261
|
-
className="flex min-w-0 items-center gap-2 rounded-md py-0.5 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
262
|
-
>
|
|
263
|
-
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">{brandMark}</div>
|
|
264
|
-
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{brandTitle}</span>
|
|
265
|
-
</Link>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
{!isMobile && <SidebarTrigger className="shrink-0" aria-label="Collapse sidebar" />}
|
|
269
|
-
</div>
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
const collapsedHeader = (
|
|
273
|
-
<div className="flex justify-center py-1">
|
|
274
|
-
<SidebarTrigger aria-label="Expand sidebar" />
|
|
275
|
-
</div>
|
|
76
|
+
</PrivateLayoutProvider>
|
|
276
77
|
);
|
|
78
|
+
}
|
|
277
79
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
{customBrand != null && customBrand !== false
|
|
283
|
-
? typeof customBrand === 'string'
|
|
284
|
-
? (
|
|
285
|
-
<Link
|
|
286
|
-
href={homeHref}
|
|
287
|
-
className="flex min-w-0 items-center gap-3 rounded-md py-1 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
288
|
-
>
|
|
289
|
-
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{customBrand}</span>
|
|
290
|
-
</Link>
|
|
291
|
-
)
|
|
292
|
-
: customBrand
|
|
293
|
-
: (
|
|
294
|
-
<Link
|
|
295
|
-
href={homeHref}
|
|
296
|
-
className="flex min-w-0 items-center gap-3 rounded-md py-1 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
297
|
-
>
|
|
298
|
-
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">{brandMark}</div>
|
|
299
|
-
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{brandTitle}</span>
|
|
300
|
-
</Link>
|
|
301
|
-
)}
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
304
|
-
);
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Inner component — runs inside PrivateLayoutProvider so useShellVisualState
|
|
82
|
+
// can safely consume the context.
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
305
84
|
|
|
306
|
-
|
|
307
|
-
|
|
85
|
+
interface PrivateSidebarInnerProps {
|
|
86
|
+
sidebar: SidebarConfig;
|
|
87
|
+
header?: HeaderConfig;
|
|
88
|
+
variant: 'sidebar' | 'inset';
|
|
89
|
+
collapsedRail: boolean;
|
|
90
|
+
setOpen: (open: boolean) => void;
|
|
91
|
+
onMouseEnter: () => void;
|
|
92
|
+
onMouseLeave: () => void;
|
|
93
|
+
}
|
|
308
94
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
95
|
+
function PrivateSidebarInner({
|
|
96
|
+
sidebar,
|
|
97
|
+
header,
|
|
98
|
+
variant,
|
|
99
|
+
collapsedRail,
|
|
100
|
+
setOpen,
|
|
101
|
+
onMouseEnter,
|
|
102
|
+
onMouseLeave,
|
|
103
|
+
}: PrivateSidebarInnerProps) {
|
|
104
|
+
const layoutVariant = variant === 'inset' ? 'boxed' : 'full-bleed';
|
|
105
|
+
const { modifiers } = useShellVisualState(layoutVariant);
|
|
106
|
+
const { setSidebarRef, handleSidebarKeyDown } = useSidebarKeyboard();
|
|
315
107
|
|
|
316
108
|
/**
|
|
317
109
|
* Click on the collapsed icon-rail expands the sidebar — but only on empty
|
|
@@ -320,177 +112,74 @@ export function PrivateSidebar({ sidebar, header, pathname: pathnameProp, varian
|
|
|
320
112
|
* target sits inside a `button`, `a`, or anything explicitly marked
|
|
321
113
|
* non-expandable via `data-no-expand`.
|
|
322
114
|
*/
|
|
323
|
-
const expandOnRailClick =
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
115
|
+
const expandOnRailClick = React.useCallback(
|
|
116
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
117
|
+
const interactive = (event.target as Element | null)?.closest(
|
|
118
|
+
'a, button, [role="menuitem"], [data-no-expand]',
|
|
119
|
+
);
|
|
120
|
+
if (interactive) return;
|
|
121
|
+
setOpen(true);
|
|
122
|
+
},
|
|
123
|
+
[setOpen],
|
|
124
|
+
);
|
|
332
125
|
|
|
333
|
-
const railExpandHintClass =
|
|
334
|
-
|
|
126
|
+
const railExpandHintClass = collapsedRail ? 'cursor-pointer' : undefined;
|
|
127
|
+
|
|
128
|
+
const sidebarRootClass = React.useMemo(
|
|
129
|
+
() =>
|
|
130
|
+
cn(
|
|
131
|
+
railExpandHintClass,
|
|
132
|
+
'[&>[data-sidebar=sidebar]]:bg-gradient-to-t [&>[data-sidebar=sidebar]]:from-sidebar/85 [&>[data-sidebar=sidebar]]:to-sidebar',
|
|
133
|
+
modifiers.sidebarRoot,
|
|
134
|
+
modifiers.sidebarInner.map((m) => `[&>[data-sidebar=sidebar]]:${m}`),
|
|
135
|
+
),
|
|
136
|
+
[railExpandHintClass, modifiers],
|
|
137
|
+
);
|
|
335
138
|
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
139
|
+
const sidebarContentClass = React.useMemo(
|
|
140
|
+
() =>
|
|
141
|
+
cn(
|
|
142
|
+
'gap-2',
|
|
143
|
+
modifiers.sidebarContent,
|
|
144
|
+
),
|
|
145
|
+
[modifiers.sidebarContent],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const renderedGroups = React.useMemo(
|
|
149
|
+
() =>
|
|
150
|
+
sidebar.groups.map((group) => {
|
|
151
|
+
if (group.dynamic && group.items.length === 0) return null;
|
|
152
|
+
return (
|
|
153
|
+
<SidebarNavGroup
|
|
154
|
+
key={group.label || `__flat_${group.items.map((i) => i.href).join('|')}`}
|
|
155
|
+
group={group}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}),
|
|
159
|
+
[sidebar.groups],
|
|
339
160
|
);
|
|
340
161
|
|
|
341
162
|
return (
|
|
342
163
|
<Sidebar
|
|
164
|
+
ref={setSidebarRef}
|
|
343
165
|
collapsible="icon"
|
|
344
166
|
variant={variant}
|
|
345
167
|
className={sidebarRootClass}
|
|
346
|
-
onClick={expandOnRailClick}
|
|
168
|
+
onClick={collapsedRail ? expandOnRailClick : undefined}
|
|
169
|
+
onMouseEnter={onMouseEnter}
|
|
170
|
+
onMouseLeave={onMouseLeave}
|
|
171
|
+
onKeyDown={handleSidebarKeyDown}
|
|
347
172
|
>
|
|
348
|
-
<
|
|
173
|
+
<SidebarBrand />
|
|
349
174
|
|
|
350
175
|
<SidebarContent className={sidebarContentClass}>
|
|
351
|
-
|
|
176
|
+
<SidebarSlots />
|
|
352
177
|
{renderedGroups}
|
|
353
|
-
{featuredSlot}
|
|
354
|
-
{menuEndSlot}
|
|
355
178
|
</SidebarContent>
|
|
356
179
|
|
|
357
180
|
<SidebarFooter className="p-0">
|
|
358
|
-
{footerExtra}
|
|
359
181
|
<PrivateSidebarAccount header={header} />
|
|
360
182
|
</SidebarFooter>
|
|
361
183
|
</Sidebar>
|
|
362
184
|
);
|
|
363
185
|
}
|
|
364
|
-
|
|
365
|
-
interface SidebarGroupRendererProps {
|
|
366
|
-
group: SidebarGroupConfig;
|
|
367
|
-
isActive: (href: string) => boolean;
|
|
368
|
-
navButtonClass: string;
|
|
369
|
-
sidebarGroupClass: string;
|
|
370
|
-
groupLabelUppercaseClass: string;
|
|
371
|
-
groupLabelPlainClass: string;
|
|
372
|
-
groupLabelStyle: SidebarGroupLabelStyle;
|
|
373
|
-
menuNav: typeof DENSITY[keyof typeof DENSITY];
|
|
374
|
-
collapsedRail: boolean;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function SidebarGroupRenderer({
|
|
378
|
-
group,
|
|
379
|
-
isActive,
|
|
380
|
-
navButtonClass,
|
|
381
|
-
sidebarGroupClass,
|
|
382
|
-
groupLabelUppercaseClass,
|
|
383
|
-
groupLabelPlainClass,
|
|
384
|
-
groupLabelStyle,
|
|
385
|
-
menuNav,
|
|
386
|
-
collapsedRail,
|
|
387
|
-
}: SidebarGroupRendererProps) {
|
|
388
|
-
const hasLabel = Boolean(group.label && group.label.trim().length > 0);
|
|
389
|
-
const isCollapsible = Boolean(group.collapsible) && hasLabel && !collapsedRail;
|
|
390
|
-
const hideItemIcons = group.hideItemIcons ?? isCollapsible;
|
|
391
|
-
|
|
392
|
-
const hasActiveChild = React.useMemo(
|
|
393
|
-
() => group.items.some((item) => isActive(item.href)),
|
|
394
|
-
[group.items, isActive],
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
const [open, setOpen] = React.useState<boolean>(
|
|
398
|
-
isCollapsible ? Boolean(group.defaultOpen) || hasActiveChild : true,
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
React.useEffect(() => {
|
|
402
|
-
if (isCollapsible && hasActiveChild) setOpen(true);
|
|
403
|
-
}, [isCollapsible, hasActiveChild]);
|
|
404
|
-
|
|
405
|
-
const items = group.items.map((item: SidebarItem) => {
|
|
406
|
-
const tooltipText = item.tooltip ?? item.label;
|
|
407
|
-
const itemIcon = !hideItemIcons && item.icon ? (
|
|
408
|
-
<LucideIcon icon={item.icon} className={menuNav.iconClass} />
|
|
409
|
-
) : null;
|
|
410
|
-
const itemClassName = hideItemIcons
|
|
411
|
-
? cn(navButtonClass, 'pl-8')
|
|
412
|
-
: navButtonClass;
|
|
413
|
-
const badgeNode = item.badge ? (
|
|
414
|
-
<SidebarMenuBadge
|
|
415
|
-
className={item.badgeVariant === 'pill'
|
|
416
|
-
? 'bg-primary/15 text-primary px-1.5 rounded-md font-medium'
|
|
417
|
-
: undefined}
|
|
418
|
-
>
|
|
419
|
-
{item.badge}
|
|
420
|
-
</SidebarMenuBadge>
|
|
421
|
-
) : null;
|
|
422
|
-
|
|
423
|
-
return (
|
|
424
|
-
<SidebarMenuItem key={item.href}>
|
|
425
|
-
<SidebarMenuButton
|
|
426
|
-
asChild
|
|
427
|
-
isActive={isActive(item.href)}
|
|
428
|
-
size={menuNav.buttonSize}
|
|
429
|
-
tooltip={tooltipText}
|
|
430
|
-
className={itemClassName}
|
|
431
|
-
>
|
|
432
|
-
<Link href={item.href}>
|
|
433
|
-
{itemIcon}
|
|
434
|
-
<span>{item.label}</span>
|
|
435
|
-
{badgeNode}
|
|
436
|
-
</Link>
|
|
437
|
-
</SidebarMenuButton>
|
|
438
|
-
</SidebarMenuItem>
|
|
439
|
-
);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
const labelClass = isCollapsible || groupLabelStyle === 'plain'
|
|
443
|
-
? groupLabelPlainClass
|
|
444
|
-
: groupLabelUppercaseClass;
|
|
445
|
-
|
|
446
|
-
if (isCollapsible) {
|
|
447
|
-
const triggerIcon = group.icon ? (
|
|
448
|
-
<LucideIcon icon={group.icon} className={cn(menuNav.iconClass, 'shrink-0 text-sidebar-foreground/70')} />
|
|
449
|
-
) : null;
|
|
450
|
-
return (
|
|
451
|
-
<SidebarGroup className={sidebarGroupClass}>
|
|
452
|
-
<Collapsible open={open} onOpenChange={setOpen} className="w-full">
|
|
453
|
-
<CollapsibleTrigger asChild>
|
|
454
|
-
<button
|
|
455
|
-
type="button"
|
|
456
|
-
className={cn(
|
|
457
|
-
'group/trig flex w-full items-center gap-2 rounded-md px-2 py-1.5',
|
|
458
|
-
'text-sm font-semibold text-sidebar-foreground',
|
|
459
|
-
'transition-colors hover:bg-sidebar-accent/40',
|
|
460
|
-
'data-[no-expand]', // marker so rail-expand click handler ignores it (pattern in PrivateSidebar)
|
|
461
|
-
)}
|
|
462
|
-
aria-expanded={open}
|
|
463
|
-
data-no-expand
|
|
464
|
-
>
|
|
465
|
-
{triggerIcon}
|
|
466
|
-
<span className="flex-1 truncate text-left">{group.label}</span>
|
|
467
|
-
<ChevronDown
|
|
468
|
-
className={cn(
|
|
469
|
-
'h-4 w-4 shrink-0 text-sidebar-foreground/55 transition-transform duration-200',
|
|
470
|
-
open && 'rotate-180',
|
|
471
|
-
)}
|
|
472
|
-
aria-hidden
|
|
473
|
-
/>
|
|
474
|
-
</button>
|
|
475
|
-
</CollapsibleTrigger>
|
|
476
|
-
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down">
|
|
477
|
-
<SidebarGroupContent>
|
|
478
|
-
<SidebarMenu className={cn(menuNav.menu, 'mt-1')}>{items}</SidebarMenu>
|
|
479
|
-
</SidebarGroupContent>
|
|
480
|
-
</CollapsibleContent>
|
|
481
|
-
</Collapsible>
|
|
482
|
-
</SidebarGroup>
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
return (
|
|
487
|
-
<SidebarGroup className={sidebarGroupClass}>
|
|
488
|
-
{hasLabel ? (
|
|
489
|
-
<SidebarGroupLabel className={labelClass}>{group.label}</SidebarGroupLabel>
|
|
490
|
-
) : null}
|
|
491
|
-
<SidebarGroupContent>
|
|
492
|
-
<SidebarMenu className={menuNav.menu}>{items}</SidebarMenu>
|
|
493
|
-
</SidebarGroupContent>
|
|
494
|
-
</SidebarGroup>
|
|
495
|
-
);
|
|
496
|
-
}
|