@djangocfg/layouts 2.1.591 → 2.1.592
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/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 +32 -62
- 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/SettingsLayout/README.md +23 -10
- package/src/layouts/SettingsLayout/SettingsDialog.tsx +14 -8
- package/src/layouts/SettingsLayout/SettingsForm.tsx +13 -9
- 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 +52 -105
- package/src/layouts/SettingsLayout/components/index.ts +0 -1
- package/src/layouts/SettingsLayout/context/SettingsContext.tsx +5 -2
- 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 +0 -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 +12 -4
- package/src/layouts/SettingsLayout/sections/builtins.tsx +34 -29
- package/src/layouts/SettingsLayout/sections/index.ts +2 -2
- package/src/layouts/SettingsLayout/types.ts +12 -4
- package/src/layouts/types/layout.types.ts +5 -6
- 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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar's content: brand header, scrollable nav, account footer.
|
|
3
|
+
* Rendered by the docked sidebar, the peek panel and the mobile drawer alike.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
import { usePrivateLayoutContext } from '../context';
|
|
11
|
+
import { PrivateSidebarAccount } from './PrivateSidebarAccount';
|
|
12
|
+
import { SidebarBrand } from './SidebarBrand';
|
|
13
|
+
import { SidebarNavGroup } from './SidebarNavGroup';
|
|
14
|
+
import { SidebarSlots } from './SidebarSlots';
|
|
15
|
+
|
|
16
|
+
export function SidebarBody() {
|
|
17
|
+
const { sidebar, header } = usePrivateLayoutContext();
|
|
18
|
+
|
|
19
|
+
const groups = useMemo(
|
|
20
|
+
() =>
|
|
21
|
+
(sidebar?.groups ?? []).map((group) =>
|
|
22
|
+
group.dynamic && group.items.length === 0 ? null : (
|
|
23
|
+
<SidebarNavGroup
|
|
24
|
+
key={group.label || `__flat_${group.items.map((i) => i.href).join('|')}`}
|
|
25
|
+
group={group}
|
|
26
|
+
/>
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
[sidebar?.groups],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<SidebarBrand />
|
|
35
|
+
<div
|
|
36
|
+
data-sidebar="content"
|
|
37
|
+
className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden py-2"
|
|
38
|
+
>
|
|
39
|
+
<SidebarSlots />
|
|
40
|
+
{groups}
|
|
41
|
+
</div>
|
|
42
|
+
<PrivateSidebarAccount header={header} />
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -1,179 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sidebar
|
|
3
|
-
*
|
|
4
|
-
* Renders the sidebar header with brand mark + title.
|
|
5
|
-
* Three modes: expanded (desktop), collapsed rail (desktop), mobile drawer.
|
|
2
|
+
* Sidebar header: brand (or workspace switcher) on the left, one control on the
|
|
3
|
+
* right — collapse on desktop, close on the mobile drawer.
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
'use client';
|
|
9
7
|
|
|
10
|
-
import React, { memo
|
|
8
|
+
import React, { memo } from 'react';
|
|
9
|
+
import { X } from 'lucide-react';
|
|
11
10
|
|
|
12
|
-
import {
|
|
11
|
+
import { Button, Link, SidebarTrigger, useSidebar } from '@djangocfg/ui-core/components';
|
|
13
12
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
13
|
|
|
15
14
|
import { LucideIcon } from '../../../components';
|
|
16
15
|
import { usePrivateLayoutContext } from '../context';
|
|
17
|
-
import { useShellVisualState } from '../hooks';
|
|
18
16
|
import { SidebarBrandSwitcher } from './SidebarBrandSwitcher';
|
|
19
17
|
|
|
20
|
-
const
|
|
18
|
+
const BRAND_LINK =
|
|
19
|
+
'flex min-w-0 items-center gap-2.5 rounded-lg py-1 outline-none ring-sidebar-ring focus-visible:ring-2';
|
|
21
20
|
|
|
22
21
|
function SidebarBrandRaw() {
|
|
23
|
-
const { header, homeHref, brandTitle, brandMonogram, isMobile } =
|
|
24
|
-
|
|
25
|
-
const { content } = useShellVisualState();
|
|
26
|
-
|
|
27
|
-
const brandMark = useMemo(
|
|
28
|
-
() =>
|
|
29
|
-
header?.brandIcon ? (
|
|
30
|
-
<LucideIcon
|
|
31
|
-
icon={header.brandIcon}
|
|
32
|
-
className="h-4 w-4 text-sidebar-primary-foreground"
|
|
33
|
-
/>
|
|
34
|
-
) : (
|
|
35
|
-
<span className="text-[11px] font-bold leading-none tracking-tight text-sidebar-primary-foreground">
|
|
36
|
-
{brandMonogram}
|
|
37
|
-
</span>
|
|
38
|
-
),
|
|
39
|
-
[header?.brandIcon, brandMonogram],
|
|
40
|
-
);
|
|
22
|
+
const { header, homeHref, brandTitle, brandMonogram, isMobile } = usePrivateLayoutContext();
|
|
23
|
+
const { setOpenMobile } = useSidebar();
|
|
41
24
|
|
|
42
25
|
const customBrand = header?.brand;
|
|
43
26
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
</Link>
|
|
60
|
-
) : (
|
|
61
|
-
customBrand
|
|
62
|
-
)
|
|
63
|
-
) : (
|
|
64
|
-
<Link
|
|
65
|
-
href={homeHref}
|
|
66
|
-
className="flex min-w-0 items-center gap-2 rounded-md py-0.5 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
67
|
-
>
|
|
68
|
-
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">
|
|
69
|
-
{brandMark}
|
|
70
|
-
</div>
|
|
71
|
-
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">
|
|
72
|
-
{brandTitle}
|
|
73
|
-
</span>
|
|
74
|
-
</Link>
|
|
27
|
+
const brand =
|
|
28
|
+
customBrand != null && customBrand !== false ? (
|
|
29
|
+
typeof customBrand === 'string' ? (
|
|
30
|
+
<Link href={homeHref} className={BRAND_LINK}>
|
|
31
|
+
<span className="truncate text-base font-medium tracking-tight">{customBrand}</span>
|
|
32
|
+
</Link>
|
|
33
|
+
) : (
|
|
34
|
+
customBrand
|
|
35
|
+
)
|
|
36
|
+
) : (
|
|
37
|
+
<Link href={homeHref} className={BRAND_LINK}>
|
|
38
|
+
<span
|
|
39
|
+
className={cn(
|
|
40
|
+
'flex shrink-0 items-center justify-center rounded-lg bg-sidebar-primary',
|
|
41
|
+
isMobile ? 'size-9' : 'size-7',
|
|
75
42
|
)}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
),
|
|
80
|
-
[headerRowClass, customBrand, homeHref, brandMark, brandTitle, isMobile],
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const collapsedHeader = useMemo(
|
|
84
|
-
() => (
|
|
85
|
-
<div className="group/collapsed-brand flex justify-center py-1">
|
|
86
|
-
<div className="relative h-7 w-7">
|
|
87
|
-
<Link
|
|
88
|
-
href={homeHref}
|
|
89
|
-
className="absolute inset-0 flex items-center justify-center rounded-md bg-sidebar-primary outline-none ring-sidebar-ring focus-visible:ring-2 transition-opacity group-hover/collapsed-brand:opacity-0"
|
|
90
|
-
aria-label={brandTitle}
|
|
91
|
-
>
|
|
92
|
-
{brandMark}
|
|
93
|
-
</Link>
|
|
94
|
-
<div className="absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-hover/collapsed-brand:opacity-100">
|
|
95
|
-
<SidebarTrigger aria-label="Expand sidebar" />
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
),
|
|
100
|
-
[homeHref, brandTitle, brandMark],
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
/** Mobile drawer: menu open/close only from the main column trigger — no duplicate toggle in the sheet. */
|
|
104
|
-
const mobileHeader = useMemo(
|
|
105
|
-
() => (
|
|
106
|
-
<div className="flex items-center gap-3">
|
|
107
|
-
<div className="min-w-0 flex-1">
|
|
108
|
-
{customBrand != null && customBrand !== false ? (
|
|
109
|
-
typeof customBrand === 'string' ? (
|
|
110
|
-
<Link
|
|
111
|
-
href={homeHref}
|
|
112
|
-
className="flex min-w-0 items-center gap-3 rounded-md py-1 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
113
|
-
>
|
|
114
|
-
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">
|
|
115
|
-
{customBrand}
|
|
116
|
-
</span>
|
|
117
|
-
</Link>
|
|
118
|
-
) : (
|
|
119
|
-
customBrand
|
|
120
|
-
)
|
|
43
|
+
>
|
|
44
|
+
{header?.brandIcon ? (
|
|
45
|
+
<LucideIcon icon={header.brandIcon} className="size-4 text-sidebar-primary-foreground" />
|
|
121
46
|
) : (
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
>
|
|
126
|
-
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">
|
|
127
|
-
{brandMark}
|
|
128
|
-
</div>
|
|
129
|
-
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">
|
|
130
|
-
{brandTitle}
|
|
131
|
-
</span>
|
|
132
|
-
</Link>
|
|
47
|
+
<span className="text-[11px] font-bold leading-none text-sidebar-primary-foreground">
|
|
48
|
+
{brandMonogram}
|
|
49
|
+
</span>
|
|
133
50
|
)}
|
|
134
|
-
</
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
51
|
+
</span>
|
|
52
|
+
<span className={cn('truncate font-medium tracking-tight', isMobile ? 'text-lg' : 'text-base')}>
|
|
53
|
+
{brandTitle}
|
|
54
|
+
</span>
|
|
55
|
+
</Link>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const trailing = isMobile ? (
|
|
59
|
+
<Button
|
|
60
|
+
type="button"
|
|
61
|
+
variant="ghost"
|
|
62
|
+
size="icon"
|
|
63
|
+
className="size-11 shrink-0 text-sidebar-foreground/70"
|
|
64
|
+
aria-label="Close menu"
|
|
65
|
+
onClick={() => setOpenMobile(false)}
|
|
66
|
+
>
|
|
67
|
+
<X className="size-5" />
|
|
68
|
+
</Button>
|
|
69
|
+
) : header?.switcher ? null : (
|
|
70
|
+
<SidebarTrigger className="shrink-0 text-sidebar-foreground/60 hover:text-sidebar-foreground" />
|
|
138
71
|
);
|
|
139
72
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
config={header.switcher}
|
|
146
|
-
showCollapseTrigger={content.showLabels && !isMobile}
|
|
147
|
-
/>
|
|
148
|
-
</div>
|
|
149
|
-
)
|
|
150
|
-
: null;
|
|
151
|
-
|
|
152
|
-
const sidebarHeaderContent = switcherContent
|
|
153
|
-
?? (isMobile
|
|
154
|
-
? mobileHeader
|
|
155
|
-
: content.showLabels
|
|
156
|
-
? expandedHeader
|
|
157
|
-
: collapsedHeader);
|
|
158
|
-
|
|
159
|
-
const sidebarHeaderClass = useMemo(
|
|
160
|
-
() =>
|
|
161
|
-
cn(
|
|
162
|
-
'pb-2',
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-sidebar="header"
|
|
76
|
+
className={cn(
|
|
77
|
+
'flex shrink-0 items-center gap-2',
|
|
163
78
|
isMobile
|
|
164
|
-
? 'pb-
|
|
165
|
-
: 'pt-3
|
|
166
|
-
)
|
|
167
|
-
|
|
79
|
+
? 'px-4 pb-2 pt-[max(0.5rem,env(safe-area-inset-top,0px))]'
|
|
80
|
+
: 'px-3 pb-1 pt-3',
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<div className="min-w-0 flex-1">
|
|
84
|
+
{header?.switcher ? <SidebarBrandSwitcher config={header.switcher} showCollapseTrigger={!isMobile} /> : brand}
|
|
85
|
+
</div>
|
|
86
|
+
{trailing}
|
|
87
|
+
</div>
|
|
168
88
|
);
|
|
169
|
-
|
|
170
|
-
return <SidebarHeader className={sidebarHeaderClass}>{sidebarHeaderContent}</SidebarHeader>;
|
|
171
89
|
}
|
|
172
90
|
|
|
173
|
-
/**
|
|
174
|
-
* Memoised brand header. Re-renders only when context values change
|
|
175
|
-
* (header config, brand title, mobile state, showLabels). The three
|
|
176
|
-
* visual modes (expanded / collapsed rail / mobile) are pre-built with
|
|
177
|
-
* useMemo so the JSX tree is stable across renders.
|
|
178
|
-
*/
|
|
179
91
|
export const SidebarBrand = memo(SidebarBrandRaw);
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* Dropdown for switching workspaces / accounts / projects.
|
|
5
5
|
* Renders in the sidebar header area, replacing the static brand block.
|
|
6
6
|
*
|
|
7
|
-
* Collapsed rail: shows only the active item's avatar/monogram (no dropdown trigger).
|
|
8
|
-
* Hover-expanded / mobile: shows full trigger + dropdown.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
'use client';
|
|
@@ -27,7 +25,7 @@ import {
|
|
|
27
25
|
import { Link } from '@djangocfg/ui-core/components';
|
|
28
26
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
29
27
|
|
|
30
|
-
import {
|
|
28
|
+
import { usePeekPin } from '../hooks';
|
|
31
29
|
import type { SidebarBrandSwitcherConfig, SidebarBrandSwitcherItem } from '../types';
|
|
32
30
|
|
|
33
31
|
interface SidebarBrandSwitcherProps {
|
|
@@ -37,8 +35,8 @@ interface SidebarBrandSwitcherProps {
|
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
function SidebarBrandSwitcherRaw({ config, showCollapseTrigger }: SidebarBrandSwitcherProps) {
|
|
40
|
-
const { content } = useShellVisualState();
|
|
41
38
|
const [open, setOpen] = React.useState(false);
|
|
39
|
+
usePeekPin(open);
|
|
42
40
|
|
|
43
41
|
const activeItem = useMemo(
|
|
44
42
|
() => config.items.find((i) => i.active) ?? config.items[0] ?? null,
|
|
@@ -66,32 +64,6 @@ function SidebarBrandSwitcherRaw({ config, showCollapseTrigger }: SidebarBrandSw
|
|
|
66
64
|
</Avatar>
|
|
67
65
|
);
|
|
68
66
|
|
|
69
|
-
// Collapsed rail — avatar with trigger on hover
|
|
70
|
-
if (!content.showLabels) {
|
|
71
|
-
return (
|
|
72
|
-
<div className="group/collapsed-switcher flex justify-center py-1">
|
|
73
|
-
<div className="relative h-7 w-7">
|
|
74
|
-
{activeItem.href ? (
|
|
75
|
-
<Link
|
|
76
|
-
href={activeItem.href}
|
|
77
|
-
className="absolute inset-0 flex items-center justify-center rounded-md outline-none ring-sidebar-ring focus-visible:ring-2 transition-opacity group-hover/collapsed-switcher:opacity-0"
|
|
78
|
-
aria-label={activeItem.label}
|
|
79
|
-
>
|
|
80
|
-
{activeAvatar}
|
|
81
|
-
</Link>
|
|
82
|
-
) : (
|
|
83
|
-
<div className="absolute inset-0 flex items-center justify-center transition-opacity group-hover/collapsed-switcher:opacity-0">
|
|
84
|
-
{activeAvatar}
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
87
|
-
<div className="absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-hover/collapsed-switcher:opacity-100">
|
|
88
|
-
<SidebarTrigger aria-label="Expand sidebar" />
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
67
|
return (
|
|
96
68
|
<div className="flex items-center gap-1">
|
|
97
69
|
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
|
@@ -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
|
+
);
|