@djangocfg/layouts 2.1.254 → 2.1.256
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.256",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -74,14 +74,14 @@
|
|
|
74
74
|
"check": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/i18n": "^2.1.
|
|
80
|
-
"@djangocfg/monitor": "^2.1.
|
|
81
|
-
"@djangocfg/debuger": "^2.1.
|
|
82
|
-
"@djangocfg/ui-core": "^2.1.
|
|
83
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
84
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
77
|
+
"@djangocfg/api": "^2.1.256",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.256",
|
|
79
|
+
"@djangocfg/i18n": "^2.1.256",
|
|
80
|
+
"@djangocfg/monitor": "^2.1.256",
|
|
81
|
+
"@djangocfg/debuger": "^2.1.256",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.256",
|
|
83
|
+
"@djangocfg/ui-nextjs": "^2.1.256",
|
|
84
|
+
"@djangocfg/ui-tools": "^2.1.256",
|
|
85
85
|
"@hookform/resolvers": "^5.2.2",
|
|
86
86
|
"consola": "^3.4.2",
|
|
87
87
|
"lucide-react": "^0.545.0",
|
|
@@ -109,15 +109,15 @@
|
|
|
109
109
|
"uuid": "^11.1.0"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
|
-
"@djangocfg/api": "^2.1.
|
|
113
|
-
"@djangocfg/i18n": "^2.1.
|
|
114
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
115
|
-
"@djangocfg/monitor": "^2.1.
|
|
116
|
-
"@djangocfg/debuger": "^2.1.
|
|
117
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
118
|
-
"@djangocfg/ui-core": "^2.1.
|
|
119
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
120
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
112
|
+
"@djangocfg/api": "^2.1.256",
|
|
113
|
+
"@djangocfg/i18n": "^2.1.256",
|
|
114
|
+
"@djangocfg/centrifugo": "^2.1.256",
|
|
115
|
+
"@djangocfg/monitor": "^2.1.256",
|
|
116
|
+
"@djangocfg/debuger": "^2.1.256",
|
|
117
|
+
"@djangocfg/typescript-config": "^2.1.256",
|
|
118
|
+
"@djangocfg/ui-core": "^2.1.256",
|
|
119
|
+
"@djangocfg/ui-nextjs": "^2.1.256",
|
|
120
|
+
"@djangocfg/ui-tools": "^2.1.256",
|
|
121
121
|
"@types/node": "^24.7.2",
|
|
122
122
|
"@types/react": "^19.1.0",
|
|
123
123
|
"@types/react-dom": "^19.1.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Private layout main column —
|
|
3
|
-
* On viewports below `md`, the desktop sidebar is off-canvas; the trigger opens the `
|
|
2
|
+
* Private layout main column — on narrow viewports a fixed menu FAB (`SidebarTrigger`) + scrollable area.
|
|
3
|
+
* On viewports below `md`, the desktop sidebar is off-canvas; the trigger opens the `Drawer` from ui-nextjs sidebar.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
@@ -22,24 +22,34 @@ export function PrivateContent({
|
|
|
22
22
|
padding = 'default',
|
|
23
23
|
hasSidebar = true,
|
|
24
24
|
}: PrivateContentProps) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/** Space for fixed FAB + safe area so content does not sit under the button. */
|
|
26
|
+
const mobileFabClearance =
|
|
27
|
+
hasSidebar &&
|
|
28
|
+
'max-md:pt-[max(4.75rem,calc(3.25rem+env(safe-area-inset-top,0px)))]';
|
|
29
|
+
|
|
29
30
|
const scrollAreaClass = cn(
|
|
30
31
|
'min-h-0 flex-1 overflow-y-auto',
|
|
31
32
|
padding === 'default' && 'p-4 sm:p-6 lg:p-8',
|
|
33
|
+
mobileFabClearance,
|
|
32
34
|
);
|
|
33
35
|
|
|
34
|
-
const
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const mobileMenuFab = hasSidebar ? (
|
|
37
|
+
<SidebarTrigger
|
|
38
|
+
variant="secondary"
|
|
39
|
+
className={cn(
|
|
40
|
+
'fixed z-40 md:hidden',
|
|
41
|
+
'left-3 top-[max(0.75rem,env(safe-area-inset-top,0px))]',
|
|
42
|
+
'h-12 w-12 rounded-xl',
|
|
43
|
+
'border border-border shadow-md',
|
|
44
|
+
'[&_svg]:!h-6 [&_svg]:!w-6',
|
|
45
|
+
'touch-manipulation',
|
|
46
|
+
)}
|
|
47
|
+
/>
|
|
38
48
|
) : null;
|
|
39
49
|
|
|
40
50
|
return (
|
|
41
51
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
42
|
-
{
|
|
52
|
+
{mobileMenuFab}
|
|
43
53
|
<div className={scrollAreaClass}>{children}</div>
|
|
44
54
|
</div>
|
|
45
55
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Private sidebar: header (brand only when expanded; icon mode shows expand trigger only),
|
|
3
|
-
* nav groups, account footer.
|
|
3
|
+
* nav groups, account footer. Item-count density when **expanded**; collapsed rail always uses `default` metrics so icons match.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
@@ -32,11 +32,15 @@ import { LucideIcon } from '../../../components';
|
|
|
32
32
|
import type { I18nLayoutConfig } from '../../AppLayout/AppLayout';
|
|
33
33
|
import type { HeaderConfig, SidebarItem, SidebarConfig } from '../PrivateLayout';
|
|
34
34
|
|
|
35
|
+
/** Few items → roomier rows; many items → tighter. Same breakpoints for demo, CarAPIS, etc. */
|
|
36
|
+
const DENSITY_COMFORTABLE_MAX = 6;
|
|
37
|
+
const DENSITY_DEFAULT_MAX = 14;
|
|
38
|
+
|
|
35
39
|
type NavDensity = 'comfortable' | 'default' | 'compact';
|
|
36
40
|
|
|
37
41
|
function navDensityFromCount(n: number): NavDensity {
|
|
38
|
-
if (n <=
|
|
39
|
-
if (n <=
|
|
42
|
+
if (n <= DENSITY_COMFORTABLE_MAX) return 'comfortable';
|
|
43
|
+
if (n <= DENSITY_DEFAULT_MAX) return 'default';
|
|
40
44
|
return 'compact';
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -57,15 +61,13 @@ const DENSITY = {
|
|
|
57
61
|
comfortable: {
|
|
58
62
|
menu: 'gap-1.5',
|
|
59
63
|
group: 'gap-2',
|
|
60
|
-
/** Tighter than default SidebarGroup `p-2` (doubles Y between groups). */
|
|
61
64
|
groupPad: 'px-2 py-1',
|
|
62
65
|
label:
|
|
63
66
|
'h-7 uppercase text-[10px] font-light leading-none tracking-[0.14em] text-sidebar-foreground/40',
|
|
64
67
|
buttonSize: 'lg' as const,
|
|
65
68
|
iconClass: 'h-5 w-5',
|
|
66
69
|
extraButton: 'rounded-lg !px-3',
|
|
67
|
-
|
|
68
|
-
headerRowInset: 'pl-3',
|
|
70
|
+
headerRowInset: 'pl-2',
|
|
69
71
|
},
|
|
70
72
|
default: {
|
|
71
73
|
menu: 'gap-1',
|
|
@@ -76,7 +78,7 @@ const DENSITY = {
|
|
|
76
78
|
buttonSize: 'default' as const,
|
|
77
79
|
iconClass: 'h-4 w-4',
|
|
78
80
|
extraButton: 'rounded-lg',
|
|
79
|
-
headerRowInset: 'pl-
|
|
81
|
+
headerRowInset: 'pl-1.5',
|
|
80
82
|
},
|
|
81
83
|
compact: {
|
|
82
84
|
menu: 'gap-0.5',
|
|
@@ -87,10 +89,13 @@ const DENSITY = {
|
|
|
87
89
|
buttonSize: 'sm' as const,
|
|
88
90
|
iconClass: 'h-3.5 w-3.5',
|
|
89
91
|
extraButton: 'rounded-md !px-2',
|
|
90
|
-
headerRowInset: 'pl-
|
|
92
|
+
headerRowInset: 'pl-1.5',
|
|
91
93
|
},
|
|
92
94
|
} as const;
|
|
93
95
|
|
|
96
|
+
/** Icon rail: always the same geometry — ignore comfortable/compact (larger/smaller rows only when expanded). */
|
|
97
|
+
const RAIL_NAV = DENSITY.default;
|
|
98
|
+
|
|
94
99
|
interface PrivateSidebarProps {
|
|
95
100
|
sidebar: SidebarConfig;
|
|
96
101
|
header?: HeaderConfig;
|
|
@@ -116,7 +121,9 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
116
121
|
);
|
|
117
122
|
|
|
118
123
|
const density = React.useMemo(() => navDensityFromCount(allItems.length), [allItems.length]);
|
|
119
|
-
const
|
|
124
|
+
const tierNav = DENSITY[density];
|
|
125
|
+
/** Expanded: follow item-count tier; collapsed rail: fixed `default` sizing so icons stay uniform. */
|
|
126
|
+
const menuNav = state === 'collapsed' ? RAIL_NAV : tierNav;
|
|
120
127
|
|
|
121
128
|
const isActive = React.useCallback(
|
|
122
129
|
(href: string) => {
|
|
@@ -134,7 +141,7 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
134
141
|
|
|
135
142
|
const expanded = state === 'expanded';
|
|
136
143
|
|
|
137
|
-
const headerRowClass = cn('flex items-center gap-2',
|
|
144
|
+
const headerRowClass = cn('flex items-center gap-2', tierNav.headerRowInset);
|
|
138
145
|
const brandMark = header?.brandIcon ? (
|
|
139
146
|
<LucideIcon icon={header.brandIcon} className="h-4 w-4 text-sidebar-primary-foreground" />
|
|
140
147
|
) : (
|
|
@@ -152,12 +159,12 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
152
159
|
<div className="w-full min-w-0 shrink-0 px-2">{sidebar.menuEnd}</div>
|
|
153
160
|
) : null;
|
|
154
161
|
|
|
155
|
-
const sidebarContentClass = cn('gap-2',
|
|
162
|
+
const sidebarContentClass = cn('gap-2', menuNav.group);
|
|
156
163
|
|
|
157
164
|
const renderedGroups = React.useMemo(() => {
|
|
158
|
-
const navButtonClass = cn(navItemClass,
|
|
159
|
-
const groupLabelClass = cn('px-2',
|
|
160
|
-
const sidebarGroupClass = cn('gap-0',
|
|
165
|
+
const navButtonClass = cn(navItemClass, menuNav.extraButton);
|
|
166
|
+
const groupLabelClass = cn('px-2', menuNav.label);
|
|
167
|
+
const sidebarGroupClass = cn('gap-0', menuNav.groupPad);
|
|
161
168
|
|
|
162
169
|
return sidebar.groups.map((group) => {
|
|
163
170
|
if (group.dynamic && group.items.length === 0) return null;
|
|
@@ -169,12 +176,12 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
169
176
|
<SidebarMenuButton
|
|
170
177
|
asChild
|
|
171
178
|
isActive={isActive(item.href)}
|
|
172
|
-
size={
|
|
179
|
+
size={menuNav.buttonSize}
|
|
173
180
|
tooltip={tooltipText}
|
|
174
181
|
className={navButtonClass}
|
|
175
182
|
>
|
|
176
183
|
<Link href={item.href}>
|
|
177
|
-
{item.icon ? <LucideIcon icon={iconProp} className={
|
|
184
|
+
{item.icon ? <LucideIcon icon={iconProp} className={menuNav.iconClass} /> : null}
|
|
178
185
|
<span>{item.label}</span>
|
|
179
186
|
{item.badge ? <SidebarMenuBadge>{item.badge}</SidebarMenuBadge> : null}
|
|
180
187
|
</Link>
|
|
@@ -187,12 +194,12 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
187
194
|
<SidebarGroup key={group.label} className={sidebarGroupClass}>
|
|
188
195
|
<SidebarGroupLabel className={groupLabelClass}>{group.label}</SidebarGroupLabel>
|
|
189
196
|
<SidebarGroupContent>
|
|
190
|
-
<SidebarMenu className={
|
|
197
|
+
<SidebarMenu className={menuNav.menu}>{items}</SidebarMenu>
|
|
191
198
|
</SidebarGroupContent>
|
|
192
199
|
</SidebarGroup>
|
|
193
200
|
);
|
|
194
201
|
});
|
|
195
|
-
}, [sidebar.groups, isActive,
|
|
202
|
+
}, [sidebar.groups, isActive, menuNav]);
|
|
196
203
|
|
|
197
204
|
const expandedHeader = (
|
|
198
205
|
<div className={headerRowClass}>
|
|
@@ -203,7 +210,7 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
203
210
|
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">{brandMark}</div>
|
|
204
211
|
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{brandTitle}</span>
|
|
205
212
|
</Link>
|
|
206
|
-
<SidebarTrigger className="shrink-0" aria-label="Collapse sidebar" />
|
|
213
|
+
{!isMobile && <SidebarTrigger className="shrink-0" aria-label="Collapse sidebar" />}
|
|
207
214
|
</div>
|
|
208
215
|
);
|
|
209
216
|
|
|
@@ -213,12 +220,32 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
213
220
|
</div>
|
|
214
221
|
);
|
|
215
222
|
|
|
216
|
-
|
|
223
|
+
/** Mobile drawer: menu open/close only from the main column trigger — no duplicate toggle in the sheet. */
|
|
224
|
+
const mobileHeader = (
|
|
225
|
+
<div className="flex items-center gap-3">
|
|
226
|
+
<Link
|
|
227
|
+
href={homeHref}
|
|
228
|
+
className="flex min-w-0 flex-1 items-center gap-3 rounded-md py-1 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
229
|
+
>
|
|
230
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">{brandMark}</div>
|
|
231
|
+
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{brandTitle}</span>
|
|
232
|
+
</Link>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const sidebarHeaderContent = isMobile ? mobileHeader : expanded ? expandedHeader : collapsedHeader;
|
|
217
237
|
const footerExtra = sidebar.footer ? <div className="mb-2">{sidebar.footer}</div> : null;
|
|
218
238
|
|
|
239
|
+
const sidebarHeaderClass = cn(
|
|
240
|
+
'pb-2',
|
|
241
|
+
isMobile
|
|
242
|
+
? 'px-4 pb-3 pt-[max(1.25rem,env(safe-area-inset-top,0px))]'
|
|
243
|
+
: 'px-2 pt-3.5',
|
|
244
|
+
);
|
|
245
|
+
|
|
219
246
|
return (
|
|
220
247
|
<Sidebar collapsible="icon">
|
|
221
|
-
<SidebarHeader className=
|
|
248
|
+
<SidebarHeader className={sidebarHeaderClass}>{sidebarHeaderContent}</SidebarHeader>
|
|
222
249
|
|
|
223
250
|
<SidebarContent className={sidebarContentClass}>
|
|
224
251
|
{menuStartSlot}
|