@djangocfg/layouts 2.1.356 → 2.1.358
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 +21 -19
- package/src/configurator/private/schema.ts +12 -0
- 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 +45 -248
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +113 -430
- package/src/layouts/{_components → PrivateLayout/components}/PrivateSidebarAccount.tsx +82 -105
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +168 -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 +14 -0
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +54 -0
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +110 -0
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +113 -0
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +207 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +21 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +115 -0
- package/src/layouts/PrivateLayout/index.ts +2 -2
- package/src/layouts/PrivateLayout/types.ts +193 -0
- package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +32 -0
- package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
- package/src/layouts/ProfileLayout/ProfileDialog/store.ts +19 -0
- package/src/layouts/ProfileLayout/{context.tsx → ProfileForm/context.tsx} +8 -8
- package/src/layouts/ProfileLayout/ProfileForm/index.tsx +148 -0
- package/src/layouts/ProfileLayout/README.md +118 -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/EditableField.tsx +1 -1
- package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +56 -0
- package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +110 -0
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +35 -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 +5 -2
- package/src/layouts/ProfileLayout/hooks/index.ts +2 -0
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +48 -0
- package/src/layouts/ProfileLayout/index.ts +7 -3
- package/src/layouts/ProfileLayout/types.ts +47 -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 -6
- package/src/layouts/index.ts +9 -4
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +0 -284
- 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,356 +37,143 @@ 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
52
|
|
|
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
|
-
|
|
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, setHoverExpanded } = 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
|
+
setHoverExpanded={setHoverExpanded}
|
|
73
|
+
onMouseEnter={onMouseEnter}
|
|
74
|
+
onMouseLeave={onMouseLeave}
|
|
234
75
|
/>
|
|
235
|
-
|
|
236
|
-
|
|
76
|
+
</PrivateLayoutProvider>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
237
79
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
) : null;
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Inner component — runs inside PrivateLayoutProvider so useShellVisualState
|
|
82
|
+
// can safely consume the context.
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
243
84
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
);
|
|
85
|
+
interface PrivateSidebarInnerProps {
|
|
86
|
+
sidebar: SidebarConfig;
|
|
87
|
+
header?: HeaderConfig;
|
|
88
|
+
variant: 'sidebar' | 'inset';
|
|
89
|
+
collapsedRail: boolean;
|
|
90
|
+
setHoverExpanded: (value: boolean) => void;
|
|
91
|
+
onMouseEnter: () => void;
|
|
92
|
+
onMouseLeave: () => void;
|
|
93
|
+
}
|
|
271
94
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
95
|
+
function PrivateSidebarInner({
|
|
96
|
+
sidebar,
|
|
97
|
+
header,
|
|
98
|
+
variant,
|
|
99
|
+
collapsedRail,
|
|
100
|
+
setHoverExpanded,
|
|
101
|
+
onMouseEnter,
|
|
102
|
+
onMouseLeave,
|
|
103
|
+
}: PrivateSidebarInnerProps) {
|
|
104
|
+
const layoutVariant = variant === 'inset' ? 'boxed' : 'full-bleed';
|
|
105
|
+
const { modifiers } = useShellVisualState(layoutVariant);
|
|
106
|
+
const { setSidebarRef, handleSidebarKeyDown } = useSidebarKeyboard();
|
|
107
|
+
|
|
108
|
+
const railExpandHintClass = collapsedRail ? 'cursor-pointer' : undefined;
|
|
109
|
+
|
|
110
|
+
/** Click on the collapsed rail acts like a hover — temporary expand, not persistent. */
|
|
111
|
+
const expandOnRailClick = React.useCallback(
|
|
112
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
113
|
+
const interactive = (event.target as Element | null)?.closest(
|
|
114
|
+
'a, button, [role="menuitem"], [data-no-expand]',
|
|
115
|
+
);
|
|
116
|
+
if (interactive) return;
|
|
117
|
+
setHoverExpanded(true);
|
|
118
|
+
},
|
|
119
|
+
[setHoverExpanded],
|
|
276
120
|
);
|
|
277
121
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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>
|
|
122
|
+
const sidebarRootClass = React.useMemo(
|
|
123
|
+
() =>
|
|
124
|
+
cn(
|
|
125
|
+
railExpandHintClass,
|
|
126
|
+
'[&>[data-sidebar=sidebar]]:bg-gradient-to-t [&>[data-sidebar=sidebar]]:from-sidebar/85 [&>[data-sidebar=sidebar]]:to-sidebar',
|
|
127
|
+
modifiers.sidebarRoot,
|
|
128
|
+
modifiers.sidebarInner.map((m) => `[&>[data-sidebar=sidebar]]:${m}`),
|
|
129
|
+
),
|
|
130
|
+
[railExpandHintClass, modifiers],
|
|
304
131
|
);
|
|
305
132
|
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
: 'px-2 pt-3.5',
|
|
133
|
+
const sidebarContentClass = React.useMemo(
|
|
134
|
+
() =>
|
|
135
|
+
cn(
|
|
136
|
+
'gap-2',
|
|
137
|
+
modifiers.sidebarContent,
|
|
138
|
+
),
|
|
139
|
+
[modifiers.sidebarContent],
|
|
314
140
|
);
|
|
315
141
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const interactive = (event.target as Element | null)?.closest(
|
|
326
|
-
'a, button, [role="menuitem"], [data-no-expand]',
|
|
142
|
+
const renderedGroups = React.useMemo(
|
|
143
|
+
() =>
|
|
144
|
+
sidebar.groups.map((group) => {
|
|
145
|
+
if (group.dynamic && group.items.length === 0) return null;
|
|
146
|
+
return (
|
|
147
|
+
<SidebarNavGroup
|
|
148
|
+
key={group.label || `__flat_${group.items.map((i) => i.href).join('|')}`}
|
|
149
|
+
group={group}
|
|
150
|
+
/>
|
|
327
151
|
);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
: undefined;
|
|
332
|
-
|
|
333
|
-
const railExpandHintClass =
|
|
334
|
-
!isMobile && state === 'collapsed' ? 'cursor-pointer' : undefined;
|
|
335
|
-
|
|
336
|
-
const sidebarRootClass = cn(
|
|
337
|
-
railExpandHintClass,
|
|
338
|
-
'[&>[data-sidebar=sidebar]]:bg-gradient-to-t [&>[data-sidebar=sidebar]]:from-sidebar/85 [&>[data-sidebar=sidebar]]:to-sidebar',
|
|
152
|
+
}),
|
|
153
|
+
[sidebar.groups],
|
|
339
154
|
);
|
|
340
155
|
|
|
341
156
|
return (
|
|
342
157
|
<Sidebar
|
|
158
|
+
ref={setSidebarRef}
|
|
343
159
|
collapsible="icon"
|
|
344
160
|
variant={variant}
|
|
345
161
|
className={sidebarRootClass}
|
|
346
|
-
onClick={expandOnRailClick}
|
|
162
|
+
onClick={collapsedRail ? expandOnRailClick : undefined}
|
|
163
|
+
onMouseEnter={onMouseEnter}
|
|
164
|
+
onMouseLeave={onMouseLeave}
|
|
165
|
+
onKeyDown={handleSidebarKeyDown}
|
|
347
166
|
>
|
|
348
|
-
<
|
|
167
|
+
<SidebarBrand />
|
|
349
168
|
|
|
350
169
|
<SidebarContent className={sidebarContentClass}>
|
|
351
|
-
|
|
170
|
+
<SidebarSlots />
|
|
352
171
|
{renderedGroups}
|
|
353
|
-
{featuredSlot}
|
|
354
|
-
{menuEndSlot}
|
|
355
172
|
</SidebarContent>
|
|
356
173
|
|
|
357
174
|
<SidebarFooter className="p-0">
|
|
358
|
-
{footerExtra}
|
|
359
175
|
<PrivateSidebarAccount header={header} />
|
|
360
176
|
</SidebarFooter>
|
|
361
177
|
</Sidebar>
|
|
362
178
|
);
|
|
363
179
|
}
|
|
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
|
-
}
|