@djangocfg/layouts 2.1.591 → 2.1.593
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 +12 -12
- package/src/components/errors/ErrorLayout.tsx +1 -164
- package/src/components/errors/ErrorsTracker/providers/ErrorTrackingProvider.tsx +1 -100
- package/src/components/errors/ErrorsTracker/providers/errorDedupe.ts +101 -0
- package/src/components/errors/errorGlyphs.tsx +167 -0
- package/src/layouts/AppLayout/BaseApp.tsx +1 -1
- package/src/layouts/AppLayout/index.ts +2 -2
- package/src/layouts/AuthLayout/AuthLayout.tsx +3 -23
- package/src/layouts/AuthLayout/components/shared/AuthTopBar.tsx +1 -1
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +1 -1
- package/src/layouts/AuthLayout/layoutContext.ts +25 -0
- package/src/layouts/AuthLayout/styles/split-shell.css +0 -1
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +37 -26
- package/src/layouts/PrivateLayout/README.md +20 -12
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +29 -42
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +23 -92
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +41 -112
- package/src/layouts/PrivateLayout/components/SidebarBody.tsx +45 -0
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +64 -152
- package/src/layouts/PrivateLayout/components/SidebarBrandSwitcher.tsx +2 -30
- package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +55 -115
- package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +37 -100
- package/src/layouts/PrivateLayout/components/SidebarPeek.tsx +34 -0
- package/src/layouts/PrivateLayout/components/SidebarResizeHandle.tsx +76 -0
- package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +14 -53
- package/src/layouts/PrivateLayout/components/index.ts +0 -4
- package/src/layouts/PrivateLayout/context.tsx +10 -115
- package/src/layouts/PrivateLayout/geometry.ts +17 -0
- package/src/layouts/PrivateLayout/hooks/index.ts +2 -1
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +4 -4
- package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +33 -93
- package/src/layouts/PrivateLayout/hooks/useSidebarPeek.tsx +151 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarWidth.ts +83 -0
- package/src/layouts/PrivateLayout/types.ts +13 -20
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +3 -89
- package/src/layouts/PublicLayout/footers/DefaultFooter/ThemeModeControl.tsx +90 -0
- package/src/layouts/PublicLayout/primitives/DesktopDropdownPortal.tsx +47 -0
- package/src/layouts/PublicLayout/primitives/NavControls.tsx +1 -1
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +2 -83
- package/src/layouts/PublicLayout/primitives/navDesktopStyles.ts +42 -0
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +4 -171
- package/src/layouts/PublicLayout/shared/index.ts +1 -1
- package/src/layouts/PublicLayout/shared/navbarShell.types.ts +109 -0
- package/src/layouts/PublicLayout/shared/navbarShellStyles.ts +69 -0
- package/src/layouts/SettingsLayout/README.md +23 -10
- package/src/layouts/SettingsLayout/SettingsDialog.tsx +27 -11
- package/src/layouts/SettingsLayout/SettingsForm.tsx +13 -9
- package/src/layouts/SettingsLayout/components/AccountMenuButton.tsx +83 -0
- package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +9 -7
- package/src/layouts/SettingsLayout/components/ApiKeySection/context.tsx +6 -41
- package/src/layouts/SettingsLayout/components/AvatarSection.tsx +11 -3
- package/src/layouts/SettingsLayout/components/SettingsNav.tsx +5 -9
- package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +5 -7
- package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +5 -10
- package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +5 -3
- package/src/layouts/SettingsLayout/components/SettingsShell.tsx +14 -21
- package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSection.tsx +63 -131
- package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSetupDialog.tsx +35 -0
- package/src/layouts/SettingsLayout/components/index.ts +0 -1
- package/src/layouts/SettingsLayout/context/SettingsContext.tsx +5 -2
- package/src/layouts/SettingsLayout/hooks/index.ts +3 -0
- package/src/layouts/SettingsLayout/hooks/useAccountMenuItems.ts +69 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +6 -3
- package/src/layouts/SettingsLayout/i18n/index.ts +39 -0
- package/src/layouts/SettingsLayout/i18n/locales/ar.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/da.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/de.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/en.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/es.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/fr.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/it.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ja.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ko.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/nl.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/no.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/pl.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ptBR.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ru.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/sv.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/tr.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/zh.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/types.ts +65 -0
- package/src/layouts/SettingsLayout/index.ts +3 -1
- package/src/layouts/SettingsLayout/sections/AccountSection.tsx +9 -4
- package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +4 -1
- package/src/layouts/SettingsLayout/sections/GeneralSection.tsx +25 -0
- package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +13 -5
- package/src/layouts/SettingsLayout/sections/builtins.tsx +34 -29
- package/src/layouts/SettingsLayout/sections/index.ts +2 -2
- package/src/layouts/SettingsLayout/store.ts +12 -0
- package/src/layouts/SettingsLayout/types.ts +12 -4
- package/src/layouts/_components/locale-switcher/LocaleSwitcher.tsx +1 -1
- package/src/layouts/types/layout.types.ts +5 -6
- package/src/{layouts/AppLayout → shared}/LayoutI18nProvider.tsx +1 -1
- package/src/styles/dashboard.css +10 -0
- package/src/layouts/PrivateLayout/density.ts +0 -48
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +0 -110
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +0 -130
- package/src/layouts/SettingsLayout/components/SettingsRailFooter.tsx +0 -74
- package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +0 -15
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sidebar Nav Group
|
|
3
|
-
*
|
|
4
|
-
* Renders a group of nav items as either a flat list or collapsible accordion.
|
|
2
|
+
* Sidebar Nav Group — a labelled list, or an accordion when `collapsible`.
|
|
3
|
+
* Groups are separated by a hairline, not by louder labels.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
'use client';
|
|
8
7
|
|
|
9
|
-
import React, {
|
|
8
|
+
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
|
+
import { ChevronDown } from 'lucide-react';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
Collapsible,
|
|
@@ -17,26 +17,24 @@ import {
|
|
|
17
17
|
SidebarGroupLabel,
|
|
18
18
|
SidebarMenu,
|
|
19
19
|
} from '@djangocfg/ui-core/components';
|
|
20
|
-
import { ChevronDown } from 'lucide-react';
|
|
21
20
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
22
21
|
|
|
23
22
|
import { LucideIcon } from '../../../components';
|
|
23
|
+
import { shallowEqualBy } from '../../../utils/shallowEqualBy';
|
|
24
24
|
import { usePrivateLayoutContext } from '../context';
|
|
25
|
-
import { useShellVisualState } from '../hooks';
|
|
26
25
|
import type { SidebarGroupConfig } from '../types';
|
|
27
26
|
import { SidebarNavItem } from './SidebarNavItem';
|
|
28
|
-
import { shallowEqualBy } from '../../../utils/shallowEqualBy';
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
const GROUP_CLASS = cn(
|
|
29
|
+
'gap-0 px-2 py-1',
|
|
30
|
+
'[&:not(:first-child)]:mt-1 [&:not(:first-child)]:border-t [&:not(:first-child)]:border-sidebar-border/60 [&:not(:first-child)]:pt-2',
|
|
31
|
+
);
|
|
33
32
|
|
|
34
|
-
function SidebarNavGroupRaw({ group }:
|
|
35
|
-
const { isActive,
|
|
36
|
-
const { content } = useShellVisualState();
|
|
33
|
+
function SidebarNavGroupRaw({ group }: { group: SidebarGroupConfig }) {
|
|
34
|
+
const { isActive, geometry } = usePrivateLayoutContext();
|
|
37
35
|
|
|
38
|
-
const hasLabel =
|
|
39
|
-
const isCollapsible = Boolean(group.collapsible) && hasLabel
|
|
36
|
+
const hasLabel = group.label.trim().length > 0;
|
|
37
|
+
const isCollapsible = Boolean(group.collapsible) && hasLabel;
|
|
40
38
|
const hideItemIcons = group.hideItemIcons ?? isCollapsible;
|
|
41
39
|
|
|
42
40
|
const hasActiveChild = useMemo(
|
|
@@ -44,94 +42,47 @@ function SidebarNavGroupRaw({ group }: SidebarNavGroupProps) {
|
|
|
44
42
|
[group.items, isActive],
|
|
45
43
|
);
|
|
46
44
|
|
|
47
|
-
const [open, setOpen] = useState
|
|
48
|
-
isCollapsible ? Boolean(group.defaultOpen) || hasActiveChild : true,
|
|
49
|
-
);
|
|
45
|
+
const [open, setOpen] = useState(isCollapsible ? Boolean(group.defaultOpen) || hasActiveChild : true);
|
|
50
46
|
|
|
51
47
|
useEffect(() => {
|
|
52
48
|
if (isCollapsible && hasActiveChild) setOpen(true);
|
|
53
49
|
}, [isCollapsible, hasActiveChild]);
|
|
54
50
|
|
|
55
|
-
const
|
|
56
|
-
() =>
|
|
57
|
-
group.items.map((item) => (
|
|
58
|
-
<SidebarNavItem key={item.href} item={item} hideItemIcons={hideItemIcons} />
|
|
59
|
-
)),
|
|
60
|
-
[group.items, hideItemIcons],
|
|
61
|
-
);
|
|
51
|
+
const handleOpenChange = useCallback((value: boolean) => setOpen(value), []);
|
|
62
52
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
const groupLabelPlainClass = useMemo(
|
|
68
|
-
() =>
|
|
69
|
-
cn(
|
|
70
|
-
'px-2 text-sm font-semibold text-sidebar-foreground',
|
|
71
|
-
'h-7 leading-none',
|
|
72
|
-
),
|
|
73
|
-
[],
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const labelClass = useMemo(
|
|
77
|
-
() =>
|
|
78
|
-
cn(
|
|
79
|
-
isCollapsible || groupLabelStyle === 'plain'
|
|
80
|
-
? groupLabelPlainClass
|
|
81
|
-
: groupLabelUppercaseClass,
|
|
82
|
-
content.showGroupLabels && '!mt-0 !opacity-100 !pointer-events-auto !flex !h-auto',
|
|
83
|
-
),
|
|
84
|
-
[isCollapsible, groupLabelStyle, groupLabelPlainClass, groupLabelUppercaseClass, content.showGroupLabels],
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const sidebarGroupClass = useMemo(
|
|
88
|
-
() => cn('gap-0', menuNav.groupPad),
|
|
89
|
-
[menuNav.groupPad],
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const handleOpenChange = useCallback((value: boolean) => {
|
|
93
|
-
setOpen(value);
|
|
94
|
-
}, []);
|
|
53
|
+
const items = group.items.map((item) => (
|
|
54
|
+
<SidebarNavItem key={item.href} item={item} hideItemIcons={hideItemIcons} />
|
|
55
|
+
));
|
|
95
56
|
|
|
96
57
|
if (isCollapsible) {
|
|
97
|
-
const triggerIcon = group.icon ? (
|
|
98
|
-
<LucideIcon
|
|
99
|
-
icon={group.icon}
|
|
100
|
-
className={cn(menuNav.iconClass, 'shrink-0 text-sidebar-foreground/70')}
|
|
101
|
-
/>
|
|
102
|
-
) : null;
|
|
103
|
-
|
|
104
58
|
return (
|
|
105
|
-
<SidebarGroup className={
|
|
59
|
+
<SidebarGroup className={GROUP_CLASS}>
|
|
106
60
|
<Collapsible open={open} onOpenChange={handleOpenChange} className="w-full">
|
|
107
61
|
<CollapsibleTrigger asChild>
|
|
108
62
|
<button
|
|
109
63
|
type="button"
|
|
64
|
+
aria-expanded={open}
|
|
110
65
|
className={cn(
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
|
|
66
|
+
'flex w-full items-center gap-2.5 rounded-lg font-normal text-sidebar-foreground',
|
|
67
|
+
'transition-colors duration-150 hover:bg-sidebar-accent/60',
|
|
68
|
+
'outline-none ring-sidebar-ring focus-visible:ring-2',
|
|
69
|
+
geometry.item,
|
|
115
70
|
)}
|
|
116
|
-
aria-expanded={open}
|
|
117
|
-
data-no-expand
|
|
118
71
|
>
|
|
119
|
-
{
|
|
72
|
+
{group.icon ? <LucideIcon icon={group.icon} className="shrink-0 text-sidebar-foreground/60" /> : null}
|
|
120
73
|
<span className="flex-1 truncate text-left">{group.label}</span>
|
|
121
74
|
<ChevronDown
|
|
75
|
+
aria-hidden
|
|
122
76
|
className={cn(
|
|
123
|
-
'
|
|
77
|
+
'size-4 shrink-0 text-sidebar-foreground/50 transition-transform duration-200',
|
|
124
78
|
open && 'rotate-180',
|
|
125
79
|
)}
|
|
126
|
-
aria-hidden
|
|
127
80
|
/>
|
|
128
81
|
</button>
|
|
129
82
|
</CollapsibleTrigger>
|
|
130
83
|
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down">
|
|
131
84
|
<SidebarGroupContent>
|
|
132
|
-
<SidebarMenu className=
|
|
133
|
-
{items}
|
|
134
|
-
</SidebarMenu>
|
|
85
|
+
<SidebarMenu className="mt-0.5 gap-0.5">{items}</SidebarMenu>
|
|
135
86
|
</SidebarGroupContent>
|
|
136
87
|
</CollapsibleContent>
|
|
137
88
|
</Collapsible>
|
|
@@ -140,50 +91,39 @@ function SidebarNavGroupRaw({ group }: SidebarNavGroupProps) {
|
|
|
140
91
|
}
|
|
141
92
|
|
|
142
93
|
return (
|
|
143
|
-
<SidebarGroup className={
|
|
94
|
+
<SidebarGroup className={GROUP_CLASS}>
|
|
144
95
|
{hasLabel ? (
|
|
145
|
-
<SidebarGroupLabel
|
|
96
|
+
<SidebarGroupLabel
|
|
97
|
+
className={cn('font-medium normal-case tracking-normal text-sidebar-foreground/50', geometry.label)}
|
|
98
|
+
>
|
|
99
|
+
{group.label}
|
|
100
|
+
</SidebarGroupLabel>
|
|
146
101
|
) : null}
|
|
147
|
-
<SidebarGroupContent
|
|
148
|
-
<SidebarMenu className=
|
|
102
|
+
<SidebarGroupContent>
|
|
103
|
+
<SidebarMenu className="gap-0.5">{items}</SidebarMenu>
|
|
149
104
|
</SidebarGroupContent>
|
|
150
105
|
</SidebarGroup>
|
|
151
106
|
);
|
|
152
107
|
}
|
|
153
108
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
a.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
b.items,
|
|
171
|
-
(ai, bi) =>
|
|
172
|
-
ai.href === bi.href &&
|
|
173
|
-
ai.label === bi.label &&
|
|
174
|
-
ai.icon === bi.icon &&
|
|
175
|
-
ai.badge === bi.badge &&
|
|
176
|
-
ai.badgeVariant === bi.badgeVariant &&
|
|
177
|
-
ai.tooltip === bi.tooltip,
|
|
178
|
-
)
|
|
109
|
+
const groupEqual = (a: SidebarGroupConfig, b: SidebarGroupConfig) =>
|
|
110
|
+
a.label === b.label &&
|
|
111
|
+
a.collapsible === b.collapsible &&
|
|
112
|
+
a.defaultOpen === b.defaultOpen &&
|
|
113
|
+
a.icon === b.icon &&
|
|
114
|
+
a.hideItemIcons === b.hideItemIcons &&
|
|
115
|
+
a.dynamic === b.dynamic &&
|
|
116
|
+
shallowEqualBy(
|
|
117
|
+
a.items,
|
|
118
|
+
b.items,
|
|
119
|
+
(x, y) =>
|
|
120
|
+
x.href === y.href &&
|
|
121
|
+
x.label === y.label &&
|
|
122
|
+
x.icon === y.icon &&
|
|
123
|
+
x.badge === y.badge &&
|
|
124
|
+
x.badgeVariant === y.badgeVariant,
|
|
179
125
|
);
|
|
180
|
-
}
|
|
181
126
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
* ignored thanks to `groupShallowEqual`.
|
|
186
|
-
*/
|
|
187
|
-
export const SidebarNavGroup = memo(SidebarNavGroupRaw, (prev, next) => {
|
|
188
|
-
return groupShallowEqual(prev.group, next.group);
|
|
189
|
-
});
|
|
127
|
+
export const SidebarNavGroup = memo(SidebarNavGroupRaw, (prev, next) =>
|
|
128
|
+
groupEqual(prev.group, next.group),
|
|
129
|
+
);
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sidebar Nav Item
|
|
3
|
-
*
|
|
4
|
-
* Individual navigation item with icon, label, badge, and active state.
|
|
5
|
-
* Memoized with shallow item comparison so badge number changes still re-render.
|
|
2
|
+
* Sidebar Nav Item — icon, label, optional badge. Active state is a surface
|
|
3
|
+
* plus `aria-current`; weight never changes, so labels do not jump.
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
'use client';
|
|
9
7
|
|
|
10
|
-
import React, { memo
|
|
8
|
+
import React, { memo } from 'react';
|
|
11
9
|
|
|
12
10
|
import {
|
|
13
11
|
Link,
|
|
@@ -19,7 +17,6 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
19
17
|
|
|
20
18
|
import { LucideIcon } from '../../../components';
|
|
21
19
|
import { usePrivateLayoutContext } from '../context';
|
|
22
|
-
import { useShellVisualState } from '../hooks';
|
|
23
20
|
import type { SidebarItem } from '../types';
|
|
24
21
|
|
|
25
22
|
interface SidebarNavItemProps {
|
|
@@ -27,111 +24,51 @@ interface SidebarNavItemProps {
|
|
|
27
24
|
hideItemIcons?: boolean;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
() =>
|
|
38
|
-
!hideItemIcons && item.icon ? (
|
|
39
|
-
<LucideIcon icon={item.icon} className={menuNav.iconClass} />
|
|
40
|
-
) : null,
|
|
41
|
-
[hideItemIcons, item.icon, menuNav.iconClass],
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const hasBadge = Boolean(item.badge);
|
|
27
|
+
const ITEM_CLASS = cn(
|
|
28
|
+
'rounded-lg border-0 font-normal shadow-none transition-colors duration-150',
|
|
29
|
+
'text-sidebar-foreground/90 hover:bg-sidebar-accent/70 hover:text-sidebar-foreground',
|
|
30
|
+
'data-[active=true]:bg-sidebar-accent data-[active=true]:font-normal data-[active=true]:text-sidebar-foreground',
|
|
31
|
+
'[&>svg]:text-sidebar-foreground/80 [&>svg]:opacity-100 hover:[&>svg]:text-sidebar-foreground',
|
|
32
|
+
'data-[active=true]:[&>svg]:text-sidebar-foreground',
|
|
33
|
+
);
|
|
45
34
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<span className="absolute right-1 top-1 flex h-2 w-2">
|
|
50
|
-
<span className="inline-flex h-full w-full rounded-full bg-primary" />
|
|
51
|
-
</span>
|
|
52
|
-
) : null,
|
|
53
|
-
[content.showBadgeText, hasBadge],
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const expandedBadgeNode = useMemo(
|
|
57
|
-
() =>
|
|
58
|
-
content.showBadgeText && hasBadge ? (
|
|
59
|
-
<SidebarMenuBadge
|
|
60
|
-
className={cn(
|
|
61
|
-
item.badgeVariant === 'pill'
|
|
62
|
-
? 'bg-primary/15 text-primary px-1.5 rounded-md font-medium'
|
|
63
|
-
: undefined,
|
|
64
|
-
)}
|
|
65
|
-
>
|
|
66
|
-
{item.badge}
|
|
67
|
-
</SidebarMenuBadge>
|
|
68
|
-
) : null,
|
|
69
|
-
[content.showBadgeText, hasBadge, item.badgeVariant, item.badge],
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const isItemActive = isActive(item.href);
|
|
73
|
-
|
|
74
|
-
const buttonClass = useMemo(
|
|
75
|
-
() =>
|
|
76
|
-
cn(
|
|
77
|
-
'group/nav relative border-0 font-medium shadow-none transition-colors',
|
|
78
|
-
'text-sidebar-foreground/80',
|
|
79
|
-
'data-[active=true]:font-semibold data-[active=true]:text-sidebar-accent-foreground',
|
|
80
|
-
'hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground',
|
|
81
|
-
'[&>svg]:shrink-0 [&>svg]:text-sidebar-foreground/70 [&>svg]:opacity-85',
|
|
82
|
-
'[&>svg]:transition-transform [&>svg]:duration-200 group-hover/nav:[&>svg]:scale-110 group-active/nav:[&>svg]:scale-95',
|
|
83
|
-
'data-[active=true]:[&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:opacity-100',
|
|
84
|
-
hideItemIcons && 'pl-8',
|
|
85
|
-
content.showLabels && '[&>span]:!inline !p-2 !justify-start',
|
|
86
|
-
),
|
|
87
|
-
[hideItemIcons, content.showLabels],
|
|
88
|
-
);
|
|
35
|
+
function SidebarNavItemRaw({ item, hideItemIcons }: SidebarNavItemProps) {
|
|
36
|
+
const { isActive, geometry } = usePrivateLayoutContext();
|
|
37
|
+
const active = isActive(item.href);
|
|
89
38
|
|
|
90
39
|
return (
|
|
91
40
|
<SidebarMenuItem>
|
|
92
41
|
<SidebarMenuButton
|
|
93
42
|
asChild
|
|
94
|
-
isActive={
|
|
95
|
-
|
|
96
|
-
tooltip={content.showTooltips ? tooltipText : undefined}
|
|
97
|
-
className={buttonClass}
|
|
43
|
+
isActive={active}
|
|
44
|
+
className={cn(ITEM_CLASS, geometry.item, hideItemIcons && 'pl-8')}
|
|
98
45
|
>
|
|
99
|
-
<Link href={item.href}>
|
|
100
|
-
{
|
|
46
|
+
<Link href={item.href} aria-current={active ? 'page' : undefined}>
|
|
47
|
+
{!hideItemIcons && item.icon ? <LucideIcon icon={item.icon} /> : null}
|
|
101
48
|
<span>{item.label}</span>
|
|
102
|
-
{
|
|
49
|
+
{item.badge ? (
|
|
50
|
+
<SidebarMenuBadge
|
|
51
|
+
className={cn(
|
|
52
|
+
item.badgeVariant === 'pill' && 'rounded-md bg-primary/15 px-1.5 font-medium text-primary',
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{item.badge}
|
|
56
|
+
</SidebarMenuBadge>
|
|
57
|
+
) : null}
|
|
103
58
|
</Link>
|
|
104
59
|
</SidebarMenuButton>
|
|
105
|
-
{collapsedBadgeDot}
|
|
106
60
|
</SidebarMenuItem>
|
|
107
61
|
);
|
|
108
62
|
}
|
|
109
63
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
a.badgeVariant === b.badgeVariant &&
|
|
122
|
-
a.tooltip === b.tooltip
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Memoised leaf component. Re-renders only when:
|
|
128
|
-
* - `hideItemIcons` changes, or
|
|
129
|
-
* - any field inside `item` changes (badge count, label, href, etc.).
|
|
130
|
-
* Parent re-renders with the same item data are skipped.
|
|
131
|
-
*/
|
|
132
|
-
export const SidebarNavItem = memo(SidebarNavItemRaw, (prev, next) => {
|
|
133
|
-
return (
|
|
134
|
-
prev.hideItemIcons === next.hideItemIcons &&
|
|
135
|
-
itemShallowEqual(prev.item, next.item)
|
|
136
|
-
);
|
|
137
|
-
});
|
|
64
|
+
const itemEqual = (a: SidebarItem, b: SidebarItem) =>
|
|
65
|
+
a.href === b.href &&
|
|
66
|
+
a.label === b.label &&
|
|
67
|
+
a.icon === b.icon &&
|
|
68
|
+
a.badge === b.badge &&
|
|
69
|
+
a.badgeVariant === b.badgeVariant;
|
|
70
|
+
|
|
71
|
+
export const SidebarNavItem = memo(
|
|
72
|
+
SidebarNavItemRaw,
|
|
73
|
+
(prev, next) => prev.hideItemIcons === next.hideItemIcons && itemEqual(prev.item, next.item),
|
|
74
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peek panel: the full sidebar floated over the page while it is collapsed.
|
|
3
|
+
* Opened and held by `SidebarPeekProvider`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
import { useSidebarPeek } from '../hooks';
|
|
11
|
+
import { SidebarBody } from './SidebarBody';
|
|
12
|
+
import { SidebarResizeHandle } from './SidebarResizeHandle';
|
|
13
|
+
|
|
14
|
+
export function SidebarPeek() {
|
|
15
|
+
const peek = useSidebarPeek();
|
|
16
|
+
if (!peek?.open) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
data-sidebar="peek"
|
|
21
|
+
role="complementary"
|
|
22
|
+
aria-label="Navigation"
|
|
23
|
+
{...peek.panelProps}
|
|
24
|
+
className={[
|
|
25
|
+
'fixed bottom-2 left-2 top-2 z-50 flex w-(--sidebar-width) flex-col overflow-hidden',
|
|
26
|
+
'rounded-xl border border-sidebar-border bg-sidebar text-sidebar-foreground shadow-2xl',
|
|
27
|
+
'animate-in fade-in-0 slide-in-from-left-4 duration-150',
|
|
28
|
+
].join(' ')}
|
|
29
|
+
>
|
|
30
|
+
<SidebarBody />
|
|
31
|
+
<SidebarResizeHandle />
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag handle on the sidebar's right edge. Pointer drag, arrow keys and
|
|
3
|
+
* double-click-to-reset; the width itself lives in `useSidebarWidth`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useRef } from 'react';
|
|
9
|
+
|
|
10
|
+
import { useSidebarWidthContext } from '../hooks';
|
|
11
|
+
|
|
12
|
+
const KEY_STEP = 16;
|
|
13
|
+
|
|
14
|
+
export function SidebarResizeHandle() {
|
|
15
|
+
const sidebarWidth = useSidebarWidthContext();
|
|
16
|
+
const origin = useRef<{ x: number; width: number } | null>(null);
|
|
17
|
+
|
|
18
|
+
if (!sidebarWidth?.enabled) return null;
|
|
19
|
+
const { width, min, max, dragging, setWidth, setDragging, reset } = sidebarWidth;
|
|
20
|
+
|
|
21
|
+
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
22
|
+
if (e.button !== 0) return;
|
|
23
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
24
|
+
origin.current = { x: e.clientX, width };
|
|
25
|
+
setDragging(true);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
29
|
+
if (!origin.current) return;
|
|
30
|
+
setWidth(origin.current.width + e.clientX - origin.current.x);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const endDrag = () => {
|
|
34
|
+
origin.current = null;
|
|
35
|
+
setDragging(false);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
39
|
+
const next =
|
|
40
|
+
e.key === 'ArrowLeft' ? width - KEY_STEP
|
|
41
|
+
: e.key === 'ArrowRight' ? width + KEY_STEP
|
|
42
|
+
: e.key === 'Home' ? min
|
|
43
|
+
: e.key === 'End' ? max
|
|
44
|
+
: null;
|
|
45
|
+
if (next === null) return;
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
setWidth(next);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
role="separator"
|
|
54
|
+
aria-orientation="vertical"
|
|
55
|
+
aria-label="Resize sidebar"
|
|
56
|
+
aria-valuemin={min}
|
|
57
|
+
aria-valuemax={max}
|
|
58
|
+
aria-valuenow={width}
|
|
59
|
+
tabIndex={0}
|
|
60
|
+
data-dragging={dragging || undefined}
|
|
61
|
+
onPointerDown={onPointerDown}
|
|
62
|
+
onPointerMove={onPointerMove}
|
|
63
|
+
onPointerUp={endDrag}
|
|
64
|
+
onPointerCancel={endDrag}
|
|
65
|
+
onDoubleClick={reset}
|
|
66
|
+
onKeyDown={onKeyDown}
|
|
67
|
+
className={[
|
|
68
|
+
'absolute inset-y-0 right-0 z-20 hidden w-2 cursor-col-resize touch-none outline-none md:block',
|
|
69
|
+
// 2px primary line on the border; fades in after a short hover delay.
|
|
70
|
+
'after:absolute after:inset-y-0 after:right-0 after:w-0.5 after:bg-transparent',
|
|
71
|
+
'after:transition-colors after:duration-150',
|
|
72
|
+
'hover:after:bg-primary/70 hover:after:delay-100 focus-visible:after:bg-primary data-[dragging]:after:bg-primary',
|
|
73
|
+
].join(' ')}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -1,71 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Renders optional content slots: menuStart, menuEnd, featured CTA, footer.
|
|
2
|
+
* Optional slots around the nav groups: menuStart, featured, menuEnd, footer.
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
'use client';
|
|
8
6
|
|
|
9
|
-
import React, { memo
|
|
7
|
+
import React, { memo } from 'react';
|
|
10
8
|
|
|
11
|
-
import { SidebarFeatured } from './SidebarFeatured';
|
|
12
9
|
import { usePrivateLayoutContext } from '../context';
|
|
13
|
-
import {
|
|
10
|
+
import { SidebarFeatured } from './SidebarFeatured';
|
|
11
|
+
|
|
12
|
+
const SLOT = 'w-full min-w-0 shrink-0 px-2';
|
|
14
13
|
|
|
15
14
|
function SidebarSlotsRaw() {
|
|
16
15
|
const { sidebar } = usePrivateLayoutContext();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const menuStartSlot = useMemo(
|
|
20
|
-
() =>
|
|
21
|
-
sidebar?.menuStart ? (
|
|
22
|
-
<div className="w-full min-w-0 shrink-0 px-2">{sidebar.menuStart}</div>
|
|
23
|
-
) : null,
|
|
24
|
-
[sidebar?.menuStart],
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
const menuEndSlot = useMemo(
|
|
28
|
-
() =>
|
|
29
|
-
sidebar?.menuEnd ? (
|
|
30
|
-
<div className="w-full min-w-0 shrink-0 px-2">{sidebar.menuEnd}</div>
|
|
31
|
-
) : null,
|
|
32
|
-
[sidebar?.menuEnd],
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const featuredSlot = useMemo(
|
|
36
|
-
() =>
|
|
37
|
-
sidebar?.featured ? (
|
|
38
|
-
<div className="w-full min-w-0 shrink-0 px-2">
|
|
39
|
-
<SidebarFeatured config={sidebar.featured} />
|
|
40
|
-
</div>
|
|
41
|
-
) : null,
|
|
42
|
-
[sidebar?.featured],
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const footerExtra = useMemo(
|
|
46
|
-
() =>
|
|
47
|
-
sidebar?.footer ? (
|
|
48
|
-
<div className="mb-2">{sidebar.footer}</div>
|
|
49
|
-
) : null,
|
|
50
|
-
[sidebar?.footer],
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
if (content.hideSlots) return null;
|
|
16
|
+
if (!sidebar) return null;
|
|
54
17
|
|
|
55
18
|
return (
|
|
56
19
|
<>
|
|
57
|
-
{
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
20
|
+
{sidebar.menuStart ? <div className={SLOT}>{sidebar.menuStart}</div> : null}
|
|
21
|
+
{sidebar.featured ? (
|
|
22
|
+
<div className={SLOT}>
|
|
23
|
+
<SidebarFeatured config={sidebar.featured} />
|
|
24
|
+
</div>
|
|
25
|
+
) : null}
|
|
26
|
+
{sidebar.menuEnd ? <div className={SLOT}>{sidebar.menuEnd}</div> : null}
|
|
27
|
+
{sidebar.footer ? <div className={SLOT}>{sidebar.footer}</div> : null}
|
|
61
28
|
</>
|
|
62
29
|
);
|
|
63
30
|
}
|
|
64
31
|
|
|
65
|
-
/**
|
|
66
|
-
* Memoised slots wrapper. Skips re-renders when the sidebar config object
|
|
67
|
-
* reference stays the same. The slot ReactNodes themselves (menuStart,
|
|
68
|
-
* menuEnd, footer) are *not* deeply compared — they remain reactive because
|
|
69
|
-
* the consumer is expected to pass a new ReactNode when slot content changes.
|
|
70
|
-
*/
|
|
71
32
|
export const SidebarSlots = memo(SidebarSlotsRaw);
|
|
@@ -6,8 +6,4 @@ export { PrivateSidebar } from './PrivateSidebar';
|
|
|
6
6
|
export { PrivateContent } from './PrivateContent';
|
|
7
7
|
export { ForbiddenState } from './ForbiddenState';
|
|
8
8
|
export type { ForbiddenStateProps } from './ForbiddenState';
|
|
9
|
-
export { SidebarBrand } from './SidebarBrand';
|
|
10
9
|
export { SidebarBrandSwitcher } from './SidebarBrandSwitcher';
|
|
11
|
-
export { SidebarNavGroup } from './SidebarNavGroup';
|
|
12
|
-
export { SidebarNavItem } from './SidebarNavItem';
|
|
13
|
-
export { SidebarSlots } from './SidebarSlots';
|