@djangocfg/layouts 2.1.542 → 2.1.544
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/components/SidebarNavGroup.tsx +7 -7
- package/src/layouts/PublicLayout/hooks/useResponsiveOverflow.ts +13 -10
- package/src/layouts/PublicLayout/primitives/NavActions.tsx +7 -9
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +1 -22
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +1 -22
- package/src/utils/navItemsShallowEqual.ts +34 -0
- package/src/utils/shallowEqualBy.ts +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.544",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -90,11 +90,11 @@
|
|
|
90
90
|
"check": "tsc --noEmit"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
|
-
"@djangocfg/analytics": "^2.1.
|
|
94
|
-
"@djangocfg/api": "^2.1.
|
|
95
|
-
"@djangocfg/devtools": "^2.1.
|
|
96
|
-
"@djangocfg/i18n": "^2.1.
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.
|
|
93
|
+
"@djangocfg/analytics": "^2.1.544",
|
|
94
|
+
"@djangocfg/api": "^2.1.544",
|
|
95
|
+
"@djangocfg/devtools": "^2.1.544",
|
|
96
|
+
"@djangocfg/i18n": "^2.1.544",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.544",
|
|
98
98
|
"@hookform/resolvers": "^5.2.2",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -124,12 +124,12 @@
|
|
|
124
124
|
"uuid": "^11.1.1"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
|
-
"@djangocfg/analytics": "^2.1.
|
|
128
|
-
"@djangocfg/api": "^2.1.
|
|
129
|
-
"@djangocfg/devtools": "^2.1.
|
|
130
|
-
"@djangocfg/i18n": "^2.1.
|
|
131
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
132
|
-
"@djangocfg/ui-core": "^2.1.
|
|
127
|
+
"@djangocfg/analytics": "^2.1.544",
|
|
128
|
+
"@djangocfg/api": "^2.1.544",
|
|
129
|
+
"@djangocfg/devtools": "^2.1.544",
|
|
130
|
+
"@djangocfg/i18n": "^2.1.544",
|
|
131
|
+
"@djangocfg/typescript-config": "^2.1.544",
|
|
132
|
+
"@djangocfg/ui-core": "^2.1.544",
|
|
133
133
|
"@storybook/react-vite": "^10.5.0",
|
|
134
134
|
"@types/node": "^25.9.5",
|
|
135
135
|
"@types/react": "19.2.15",
|
|
@@ -25,6 +25,7 @@ import { usePrivateLayoutContext } from '../context';
|
|
|
25
25
|
import { useShellVisualState } from '../hooks';
|
|
26
26
|
import type { SidebarGroupConfig } from '../types';
|
|
27
27
|
import { SidebarNavItem } from './SidebarNavItem';
|
|
28
|
+
import { shallowEqualBy } from '../../../utils/shallowEqualBy';
|
|
28
29
|
|
|
29
30
|
interface SidebarNavGroupProps {
|
|
30
31
|
group: SidebarGroupConfig;
|
|
@@ -164,18 +165,17 @@ function groupShallowEqual(a: SidebarGroupConfig, b: SidebarGroupConfig): boolea
|
|
|
164
165
|
a.icon === b.icon &&
|
|
165
166
|
a.hideItemIcons === b.hideItemIcons &&
|
|
166
167
|
a.dynamic === b.dynamic &&
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
shallowEqualBy(
|
|
169
|
+
a.items,
|
|
170
|
+
b.items,
|
|
171
|
+
(ai, bi) =>
|
|
171
172
|
ai.href === bi.href &&
|
|
172
173
|
ai.label === bi.label &&
|
|
173
174
|
ai.icon === bi.icon &&
|
|
174
175
|
ai.badge === bi.badge &&
|
|
175
176
|
ai.badgeVariant === bi.badgeVariant &&
|
|
176
|
-
ai.tooltip === bi.tooltip
|
|
177
|
-
|
|
178
|
-
})
|
|
177
|
+
ai.tooltip === bi.tooltip,
|
|
178
|
+
)
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -58,17 +58,18 @@ export function useResponsiveOverflow({
|
|
|
58
58
|
const available = container.clientWidth;
|
|
59
59
|
if (available <= 0) return;
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Built with `from` rather than an index loop so `widths` and `total` cannot
|
|
62
|
+
// disagree on length — the two reads below then need no bounds check.
|
|
63
|
+
const widths = Array.from({ length: total }, (_, i) => {
|
|
63
64
|
const el = itemElsRef.current[i];
|
|
64
|
-
|
|
65
|
-
}
|
|
65
|
+
return el ? Math.ceil(el.getBoundingClientRect().width) : 0;
|
|
66
|
+
});
|
|
66
67
|
|
|
67
68
|
// Try to fit all first.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const usedAll = widths.reduce(
|
|
70
|
+
(sum, width, i) => sum + width + (i > 0 ? gap : 0),
|
|
71
|
+
0,
|
|
72
|
+
);
|
|
72
73
|
if (usedAll <= available) {
|
|
73
74
|
setVisibleCount(total);
|
|
74
75
|
setMeasured(true);
|
|
@@ -79,8 +80,10 @@ export function useResponsiveOverflow({
|
|
|
79
80
|
const budget = available - moreWidth - gap;
|
|
80
81
|
let used = 0;
|
|
81
82
|
let fit = 0;
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
// Iterating values, not indices: this loop breaks early, so `reduce` does
|
|
84
|
+
// not fit, and `entries()` still keeps each width tied to its position.
|
|
85
|
+
for (const [i, width] of widths.entries()) {
|
|
86
|
+
const next = used + width + (i > 0 ? gap : 0);
|
|
84
87
|
if (next > budget) break;
|
|
85
88
|
used = next;
|
|
86
89
|
fit = i + 1;
|
|
@@ -10,6 +10,7 @@ import { UserMenu } from '../components/UserMenu';
|
|
|
10
10
|
import type { UserMenuConfig } from '../../types';
|
|
11
11
|
|
|
12
12
|
import { NavActionItem, type NavAction } from './NavActionItem';
|
|
13
|
+
import { shallowEqualBy } from '../../../utils/shallowEqualBy';
|
|
13
14
|
|
|
14
15
|
interface NavActionsProps {
|
|
15
16
|
userMenu?: UserMenuConfig;
|
|
@@ -133,18 +134,15 @@ function NavActionsRaw({
|
|
|
133
134
|
* skip on reference churn from parent re-renders.
|
|
134
135
|
*/
|
|
135
136
|
function actionsEqual(a?: NavAction[], b?: NavAction[]): boolean {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const bi = b[i];
|
|
141
|
-
return (
|
|
137
|
+
return shallowEqualBy(
|
|
138
|
+
a,
|
|
139
|
+
b,
|
|
140
|
+
(ai, bi) =>
|
|
142
141
|
ai.label === bi.label &&
|
|
143
142
|
ai.href === bi.href &&
|
|
144
143
|
ai.variant === bi.variant &&
|
|
145
|
-
ai.external === bi.external
|
|
146
|
-
|
|
147
|
-
});
|
|
144
|
+
ai.external === bi.external,
|
|
145
|
+
);
|
|
148
146
|
}
|
|
149
147
|
|
|
150
148
|
/**
|
|
@@ -8,6 +8,7 @@ import { Button, Link } from '@djangocfg/ui-core/components';
|
|
|
8
8
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
9
9
|
|
|
10
10
|
import type { NavigationItem } from '../../types';
|
|
11
|
+
import { navItemsShallowEqual } from '../../../utils/navItemsShallowEqual';
|
|
11
12
|
import type { UseDropdownMenuReturn } from '../hooks/useDropdownMenu';
|
|
12
13
|
import { useResponsiveOverflow } from '../hooks/useResponsiveOverflow';
|
|
13
14
|
import type { PublicDesktopDropdownRenderer } from '../navbarTypes';
|
|
@@ -317,28 +318,6 @@ function NavDesktopItemsRaw({
|
|
|
317
318
|
);
|
|
318
319
|
}
|
|
319
320
|
|
|
320
|
-
/**
|
|
321
|
-
* Shallow equality for NavigationItem arrays. Compares href, label, external
|
|
322
|
-
* and one-level-deep sub-items so that nav label/badge changes re-render,
|
|
323
|
-
* but object-reference churn from parent is ignored.
|
|
324
|
-
*/
|
|
325
|
-
function navItemsShallowEqual(a: NavigationItem[], b: NavigationItem[]): boolean {
|
|
326
|
-
if (a === b) return true;
|
|
327
|
-
if (a.length !== b.length) return false;
|
|
328
|
-
return a.every((ai, i) => {
|
|
329
|
-
const bi = b[i];
|
|
330
|
-
if (ai.href !== bi.href || ai.label !== bi.label || ai.external !== bi.external) return false;
|
|
331
|
-
if ((ai.items?.length ?? 0) !== (bi.items?.length ?? 0)) return false;
|
|
332
|
-
if (ai.items) {
|
|
333
|
-
return ai.items.every((sub, j) => {
|
|
334
|
-
const bsub = bi.items![j];
|
|
335
|
-
return sub.href === bsub.href && sub.label === bsub.label && sub.external === bsub.external;
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
return true;
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
321
|
/**
|
|
343
322
|
* Memoised desktop navigation row. Re-renders only when:
|
|
344
323
|
* - the items array content changes (via navItemsShallowEqual),
|
|
@@ -29,6 +29,7 @@ import { useIsTabletOrBelow } from '@djangocfg/ui-core/hooks';
|
|
|
29
29
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
30
30
|
|
|
31
31
|
import { usePathnameWithoutLocale } from '../../../hooks';
|
|
32
|
+
import { navItemsShallowEqual } from '../../../utils/navItemsShallowEqual';
|
|
32
33
|
import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
|
|
33
34
|
import { useLayoutI18nOptional } from '../../AppLayout/LayoutI18nProvider';
|
|
34
35
|
import { usePublicLayoutOptional } from '../context';
|
|
@@ -420,28 +421,6 @@ function NavbarShellRaw(props: NavbarShellProps) {
|
|
|
420
421
|
);
|
|
421
422
|
}
|
|
422
423
|
|
|
423
|
-
/**
|
|
424
|
-
* Shallow equality for NavigationItem arrays. Compares href, label, external
|
|
425
|
-
* and one-level-deep sub-items so that nav label/badge changes re-render,
|
|
426
|
-
* but object-reference churn from parent is ignored.
|
|
427
|
-
*/
|
|
428
|
-
function navItemsShallowEqual(a: NavigationItem[], b: NavigationItem[]): boolean {
|
|
429
|
-
if (a === b) return true;
|
|
430
|
-
if (a.length !== b.length) return false;
|
|
431
|
-
return a.every((ai, i) => {
|
|
432
|
-
const bi = b[i];
|
|
433
|
-
if (ai.href !== bi.href || ai.label !== bi.label || ai.external !== bi.external) return false;
|
|
434
|
-
if ((ai.items?.length ?? 0) !== (bi.items?.length ?? 0)) return false;
|
|
435
|
-
if (ai.items) {
|
|
436
|
-
return ai.items.every((sub, j) => {
|
|
437
|
-
const bsub = bi.items![j];
|
|
438
|
-
return sub.href === bsub.href && sub.label === bsub.label && sub.external === bsub.external;
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
return true;
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
424
|
/**
|
|
446
425
|
* Memoised navbar orchestrator. Re-renders only when any config prop changes
|
|
447
426
|
* (variant, position, nav items, actions, slots, etc.). The heavy inner JSX
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { NavigationItem } from '../layouts/types/ui.types';
|
|
2
|
+
import { shallowEqualBy } from './shallowEqualBy';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shallow equality for NavigationItem arrays. Compares href, label, external
|
|
6
|
+
* and one-level-deep sub-items so that nav label/badge changes re-render,
|
|
7
|
+
* but object-reference churn from parent is ignored.
|
|
8
|
+
*
|
|
9
|
+
* Lived as two byte-identical copies in NavbarShell and NavDesktopItems.
|
|
10
|
+
* The sub-item pass used `bi.items![j]`, an assertion resting on the length
|
|
11
|
+
* comparison two lines above it; `shallowEqualBy` makes that structural
|
|
12
|
+
* instead, so the non-null assertion is gone rather than relocated.
|
|
13
|
+
*/
|
|
14
|
+
export function navItemsShallowEqual(
|
|
15
|
+
a: NavigationItem[],
|
|
16
|
+
b: NavigationItem[],
|
|
17
|
+
): boolean {
|
|
18
|
+
return shallowEqualBy(
|
|
19
|
+
a,
|
|
20
|
+
b,
|
|
21
|
+
(ai, bi) =>
|
|
22
|
+
ai.href === bi.href &&
|
|
23
|
+
ai.label === bi.label &&
|
|
24
|
+
ai.external === bi.external &&
|
|
25
|
+
shallowEqualBy(
|
|
26
|
+
ai.items,
|
|
27
|
+
bi.items,
|
|
28
|
+
(sub, bsub) =>
|
|
29
|
+
sub.href === bsub.href &&
|
|
30
|
+
sub.label === bsub.label &&
|
|
31
|
+
sub.external === bsub.external,
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairwise array comparison for `React.memo` props.
|
|
3
|
+
*
|
|
4
|
+
* Four hand-rolled copies of this existed across the nav layouts — two of them
|
|
5
|
+
* (`navItemsShallowEqual` in NavbarShell and NavDesktopItems) byte-identical.
|
|
6
|
+
* Each wrote `const bi = b[i]` inside `a.every`, which is safe only because the
|
|
7
|
+
* length was compared on the line above; under `strict` that produced 26 of the
|
|
8
|
+
* package's 28 errors, all asking the same question about the same missing
|
|
9
|
+
* connection between the guard and the index.
|
|
10
|
+
*
|
|
11
|
+
* `entries()` carries the pair, so an element and its counterpart are produced
|
|
12
|
+
* together and neither can be `undefined`. The guard is not restated — it is
|
|
13
|
+
* expressed once, in the shape of the loop.
|
|
14
|
+
*/
|
|
15
|
+
export function shallowEqualBy<T>(
|
|
16
|
+
a: readonly T[] | undefined,
|
|
17
|
+
b: readonly T[] | undefined,
|
|
18
|
+
equal: (x: T, y: T) => boolean,
|
|
19
|
+
): boolean {
|
|
20
|
+
if (a === b) return true;
|
|
21
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
22
|
+
for (const [index, left] of a.entries()) {
|
|
23
|
+
const right = b[index];
|
|
24
|
+
// `index` comes from `a.entries()` and the lengths matched, so `right`
|
|
25
|
+
// exists. The check is what proves it to the compiler; it is not reachable.
|
|
26
|
+
if (right === undefined || !equal(left, right)) return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|