jamdesk 1.1.167 → 1.1.169
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/dist/__tests__/unit/vendored-sync.test.js +4 -0
- package/dist/__tests__/unit/vendored-sync.test.js.map +1 -1
- package/package.json +1 -1
- package/vendored/components/navigation/Sidebar.tsx +247 -141
- package/vendored/lib/middleware-helpers.ts +265 -2
- package/vendored/lib/project-resolver.ts +10 -0
- package/vendored/lib/screenshot-capture.ts +7 -1
- package/vendored/workspace-package-lock.json +27 -111
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +0 -2
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +0 -1
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js +0 -112
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +0 -1
- package/dist/__tests__/unit/language-filter.test.d.ts +0 -2
- package/dist/__tests__/unit/language-filter.test.d.ts.map +0 -1
- package/dist/__tests__/unit/language-filter.test.js +0 -166
- package/dist/__tests__/unit/language-filter.test.js.map +0 -1
- package/dist/lib/language-filter.d.ts +0 -31
- package/dist/lib/language-filter.d.ts.map +0 -1
- package/dist/lib/language-filter.js +0 -14
- package/dist/lib/language-filter.js.map +0 -1
- package/vendored/lib/sidebar-prefetch-walker.ts +0 -50
|
@@ -36,6 +36,10 @@ const SYNCED_FILES = [
|
|
|
36
36
|
// three must move together with the canonical.
|
|
37
37
|
['build-service/themes/index.ts', 'cli/vendored/themes/index.ts'],
|
|
38
38
|
['build-service/components/navigation/Header.tsx', 'cli/vendored/components/navigation/Header.tsx'],
|
|
39
|
+
[
|
|
40
|
+
"build-service/components/navigation/Sidebar.tsx",
|
|
41
|
+
"cli/vendored/components/navigation/Sidebar.tsx",
|
|
42
|
+
],
|
|
39
43
|
];
|
|
40
44
|
describe('CLI vendored files sync', () => {
|
|
41
45
|
it.each(SYNCED_FILES)('should have matching content: %s', async (buildServicePath, vendoredPath) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vendored-sync.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/vendored-sync.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D;;;;;;;;;;;;GAYG;AACH,MAAM,YAAY,GAAuB;IACvC,+EAA+E;IAC/E,0EAA0E;IAC1E,sEAAsE;IACtE,+EAA+E;IAC/E,+CAA+C;IAC/C,CAAC,+BAA+B,EAAE,8BAA8B,CAAC;IACjE,CAAC,gDAAgD,EAAE,+CAA+C,CAAC;
|
|
1
|
+
{"version":3,"file":"vendored-sync.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/vendored-sync.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.169",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -122,36 +122,90 @@ function findFirstPageInGroups(groups: ResolvedGroup[]): string | null {
|
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
// A group's position within the resolved navigation tree, expressed as the
|
|
126
|
+
// chain of sibling indices from the root (e.g. `[1, 0]` = second top-level
|
|
127
|
+
// group's first nested group). This is the stable identity for accordion
|
|
128
|
+
// state — see `groupStateKey` for why we key on position rather than name.
|
|
129
|
+
type GroupIndexPath = readonly number[];
|
|
130
|
+
|
|
131
|
+
// The navigation context a group is rendered in. Locale + active tab/anchor
|
|
132
|
+
// scope the accordion state so a manual expansion on one tab (or in one
|
|
133
|
+
// language) never leaks into the same tree position on another — labels and
|
|
134
|
+
// index paths repeat across tabs/locales, but the scope does not.
|
|
135
|
+
function navigationScope(nav: {
|
|
136
|
+
currentLanguage?: string;
|
|
137
|
+
activeTab?: string;
|
|
138
|
+
activeAnchor?: string;
|
|
139
|
+
}): string {
|
|
140
|
+
return [
|
|
141
|
+
nav.currentLanguage ?? 'default',
|
|
142
|
+
nav.activeTab ?? nav.activeAnchor ?? 'root',
|
|
143
|
+
].join(':');
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
for (const group of groups) {
|
|
141
|
-
const groupPath = group.name ? [...parentNames, group.name] : parentNames;
|
|
146
|
+
function groupStateKey(scope: string, indexPath: GroupIndexPath): string {
|
|
147
|
+
return `${scope}:${indexPath.join('.')}`;
|
|
148
|
+
}
|
|
142
149
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
function groupPanelId(scope: string, indexPath: GroupIndexPath): string {
|
|
151
|
+
const safeScope = scope.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
152
|
+
return `nav-group-${safeScope}-${indexPath.join('-')}`;
|
|
153
|
+
}
|
|
146
154
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Collect the scoped state keys of every NAMED ancestor of each group matching
|
|
157
|
+
* `predicate` (including the matching group itself when named), so a
|
|
158
|
+
* qualifying descendant opens the whole chain that leads to it. Walks
|
|
159
|
+
* `group.nested` by sibling index — the same named-group index used while
|
|
160
|
+
* rendering ordered `group.items` — so discovery and rendered keys agree.
|
|
161
|
+
*/
|
|
162
|
+
function collectAncestorKeys(
|
|
163
|
+
groups: ResolvedGroup[],
|
|
164
|
+
scope: string,
|
|
165
|
+
predicate: (group: ResolvedGroup) => boolean,
|
|
166
|
+
parentPath: GroupIndexPath = [],
|
|
167
|
+
ancestorKeys: readonly string[] = [],
|
|
168
|
+
): Set<string> {
|
|
169
|
+
const result = new Set<string>();
|
|
170
|
+
groups.forEach((group, index) => {
|
|
171
|
+
const indexPath = [...parentPath, index];
|
|
172
|
+
const key = groupStateKey(scope, indexPath);
|
|
173
|
+
const nextAncestors = group.name ? [...ancestorKeys, key] : ancestorKeys;
|
|
174
|
+
if (predicate(group)) {
|
|
175
|
+
nextAncestors.forEach((ancestorKey) => result.add(ancestorKey));
|
|
150
176
|
}
|
|
151
|
-
|
|
177
|
+
collectAncestorKeys(
|
|
178
|
+
group.nested ?? [], scope, predicate, indexPath, nextAncestors,
|
|
179
|
+
).forEach((nestedKey) => result.add(nestedKey));
|
|
180
|
+
});
|
|
152
181
|
return result;
|
|
153
182
|
}
|
|
154
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Named groups that should start open because a descendant carries the
|
|
186
|
+
* `expanded: true` schema flag.
|
|
187
|
+
*/
|
|
188
|
+
function findInitiallyExpandedGroups(
|
|
189
|
+
groups: ResolvedGroup[],
|
|
190
|
+
scope: string,
|
|
191
|
+
): Set<string> {
|
|
192
|
+
return collectAncestorKeys(groups, scope, (group) => Boolean(group.expanded && group.name));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Named groups on the path from the root to the page at `currentPath`, so the
|
|
197
|
+
* active page's ancestors open by default.
|
|
198
|
+
*/
|
|
199
|
+
function findGroupsContainingPath(
|
|
200
|
+
groups: ResolvedGroup[],
|
|
201
|
+
currentPath: string,
|
|
202
|
+
scope: string,
|
|
203
|
+
): Set<string> {
|
|
204
|
+
return collectAncestorKeys(groups, scope, (group) =>
|
|
205
|
+
group.pages.some((page) => page.path === currentPath),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
155
209
|
export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPositionProp, isOpen = false, onClose }: SidebarProps) {
|
|
156
210
|
const pathname = usePathname() ?? '';
|
|
157
211
|
const router = useRouter();
|
|
@@ -170,6 +224,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
170
224
|
// the active page's parent group collapsed on first paint.
|
|
171
225
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(() => {
|
|
172
226
|
const initialNav = resolveNavigation(config, pathname || '/');
|
|
227
|
+
const initialScope = navigationScope(initialNav);
|
|
173
228
|
const rootPath = linkPrefix || '/';
|
|
174
229
|
// `pathname || rootPath` handles both the null-pathname mask (`?? ''`
|
|
175
230
|
// above) and the bare-root case in one expression — when pathname is the
|
|
@@ -181,8 +236,8 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
181
236
|
if (firstPage) seedPath = `${linkPrefix}/${firstPage}`;
|
|
182
237
|
}
|
|
183
238
|
const initialPath = seedPath.replace(/^\/(?:docs\/)?/, '');
|
|
184
|
-
const seeded = findGroupsContainingPath(initialNav.groups, initialPath);
|
|
185
|
-
findInitiallyExpandedGroups(initialNav.groups).forEach((g) => seeded.add(g));
|
|
239
|
+
const seeded = findGroupsContainingPath(initialNav.groups, initialPath, initialScope);
|
|
240
|
+
findInitiallyExpandedGroups(initialNav.groups, initialScope).forEach((g) => seeded.add(g));
|
|
186
241
|
return seeded;
|
|
187
242
|
});
|
|
188
243
|
const [logoError, setLogoError] = useState(false);
|
|
@@ -246,6 +301,12 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
246
301
|
return resolveNavigation(config, effectivePathname || pathname || '/');
|
|
247
302
|
}, [config, effectivePathname, pathname]);
|
|
248
303
|
|
|
304
|
+
// Locale + active tab/anchor scope for accordion state keys. Derived from the
|
|
305
|
+
// resolved navigation (never from a visible group label) so manually-toggled
|
|
306
|
+
// groups don't leak their open state across tabs or languages that reuse the
|
|
307
|
+
// same labels and tree positions.
|
|
308
|
+
const scope = navigationScope(resolvedNav);
|
|
309
|
+
|
|
249
310
|
// Normalize bare root path to first page path for active state highlighting.
|
|
250
311
|
// When an upstream proxy (e.g., Cloudflare Worker) follows the server redirect
|
|
251
312
|
// from /docs → /docs/introduction internally, the browser URL stays at /docs.
|
|
@@ -263,16 +324,16 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
263
324
|
// Memoize the groups that should be expanded for the current page
|
|
264
325
|
const groupsForCurrentPage = useMemo(() => {
|
|
265
326
|
const currentPath = activePathname?.replace(/^\/(?:docs\/)?/, '') || '';
|
|
266
|
-
return findGroupsContainingPath(resolvedNav.groups, currentPath);
|
|
267
|
-
}, [resolvedNav.groups, activePathname]);
|
|
327
|
+
return findGroupsContainingPath(resolvedNav.groups, currentPath, scope);
|
|
328
|
+
}, [resolvedNav.groups, activePathname, scope]);
|
|
268
329
|
|
|
269
330
|
// Re-evaluated whenever the resolved nav changes (e.g. switching tabs in
|
|
270
331
|
// multi-tab docs), so newly-flagged `expanded: true` groups open on cross-tab
|
|
271
332
|
// navigation. Without this re-eval, the schema flag would only be honored at
|
|
272
333
|
// first mount.
|
|
273
334
|
const initiallyExpandedGroups = useMemo(
|
|
274
|
-
() => findInitiallyExpandedGroups(resolvedNav.groups),
|
|
275
|
-
[resolvedNav.groups],
|
|
335
|
+
() => findInitiallyExpandedGroups(resolvedNav.groups, scope),
|
|
336
|
+
[resolvedNav.groups, scope],
|
|
276
337
|
);
|
|
277
338
|
|
|
278
339
|
// Merge the current page's ancestor groups + `expanded: true` flagged groups
|
|
@@ -372,17 +433,19 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
372
433
|
// Prevent body scroll when sidebar is open on mobile
|
|
373
434
|
useBodyScrollLock(isOpen);
|
|
374
435
|
|
|
375
|
-
// Toggle group expansion; if expanding, navigate to first page.
|
|
436
|
+
// Toggle group expansion; if expanding, navigate to first page. Top-level
|
|
437
|
+
// sections never visually collapse — for them the toggled key only gates
|
|
438
|
+
// prefetch (see renderGroupContents) while the click still navigates.
|
|
376
439
|
// The router.push must run AFTER the setState commits — calling it inside the
|
|
377
440
|
// updater triggers "Cannot update a component (Router) while rendering Sidebar".
|
|
378
|
-
const handleGroupClick = useCallback((group: ResolvedGroup) => {
|
|
379
|
-
const willExpand = !expandedGroups.has(
|
|
441
|
+
const handleGroupClick = useCallback((group: ResolvedGroup, key: string) => {
|
|
442
|
+
const willExpand = !expandedGroups.has(key);
|
|
380
443
|
setExpandedGroups(prev => {
|
|
381
444
|
const newSet = new Set(prev);
|
|
382
|
-
if (prev.has(
|
|
383
|
-
newSet.delete(
|
|
445
|
+
if (prev.has(key)) {
|
|
446
|
+
newSet.delete(key);
|
|
384
447
|
} else {
|
|
385
|
-
newSet.add(
|
|
448
|
+
newSet.add(key);
|
|
386
449
|
}
|
|
387
450
|
return newSet;
|
|
388
451
|
});
|
|
@@ -394,27 +457,124 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
394
457
|
}
|
|
395
458
|
}, [linkPrefix, expandedGroups, handleButtonNav]);
|
|
396
459
|
|
|
397
|
-
// Render a
|
|
398
|
-
|
|
399
|
-
|
|
460
|
+
// Render the pages and nested groups inside a group. Split out from
|
|
461
|
+
// renderGroup so a named group can gate its contents behind an expanded panel
|
|
462
|
+
// (mounting them only while open) while an unnamed group renders them inline.
|
|
463
|
+
function renderGroupContents(group: ResolvedGroup, level: number, indexPath: GroupIndexPath) {
|
|
464
|
+
const isExpanded = expandedGroups.has(groupStateKey(scope, indexPath));
|
|
465
|
+
// Top-level sections stay mounted while collapsed-in-state, so their Links
|
|
466
|
+
// hold prefetch until the section is seeded or clicked expanded (don't fan
|
|
467
|
+
// out RSC prefetches for every section on large sites). Closed NESTED named
|
|
468
|
+
// groups never reach here (their contents are unmounted), so a rendered
|
|
469
|
+
// nested named group is always expanded; unnamed groups always prefetch.
|
|
470
|
+
const shouldPrefetch = !group.name || isExpanded;
|
|
400
471
|
|
|
472
|
+
if (group.items) {
|
|
473
|
+
// Use ordered items array when available (preserves page/group ordering)
|
|
474
|
+
return (
|
|
475
|
+
<div className={level === 1 ? 'ml-3' : ''}>
|
|
476
|
+
{(() => {
|
|
477
|
+
// Group consecutive pages into single ul elements for continuous border
|
|
478
|
+
const elements: React.ReactNode[] = [];
|
|
479
|
+
let currentPages: typeof group.items = [];
|
|
480
|
+
|
|
481
|
+
const flushPages = () => {
|
|
482
|
+
if (currentPages.length > 0) {
|
|
483
|
+
elements.push(
|
|
484
|
+
<ul key={`pages-${elements.length}`}>
|
|
485
|
+
{currentPages.map((p) => p.type === 'page' && (
|
|
486
|
+
<NavPage
|
|
487
|
+
key={p.page.path}
|
|
488
|
+
page={p.page}
|
|
489
|
+
pathname={activePathname}
|
|
490
|
+
layout={layout}
|
|
491
|
+
onNavigate={handleNavigate}
|
|
492
|
+
linkPrefix={linkPrefix}
|
|
493
|
+
showSpinner={spinnerPathname === `${linkPrefix}/${p.page.path}`}
|
|
494
|
+
prefetch={shouldPrefetch}
|
|
495
|
+
/>
|
|
496
|
+
))}
|
|
497
|
+
</ul>
|
|
498
|
+
);
|
|
499
|
+
currentPages = [];
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Track the nested-group sibling index separately from the items
|
|
504
|
+
// index: the i-th nested group is the i-th group-typed item, which
|
|
505
|
+
// is the same index navigation-resolver assigns in `group.nested`.
|
|
506
|
+
// This keeps state keys aligned with the discovery helpers.
|
|
507
|
+
let nestedGroupIndex = 0;
|
|
508
|
+
group.items.forEach((item) => {
|
|
509
|
+
if (item.type === 'page') {
|
|
510
|
+
currentPages.push(item);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
flushPages();
|
|
514
|
+
const childIndex = nestedGroupIndex++;
|
|
515
|
+
elements.push(
|
|
516
|
+
<div key={groupStateKey(scope, [...indexPath, childIndex])} className={level <= 1 ? '' : 'mt-4'}>
|
|
517
|
+
{renderGroup(item.group, level + 1, [...indexPath, childIndex])}
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
flushPages();
|
|
522
|
+
|
|
523
|
+
return elements;
|
|
524
|
+
})()}
|
|
525
|
+
</div>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Fallback to legacy pages + nested (for backwards compatibility)
|
|
530
|
+
return (
|
|
531
|
+
<>
|
|
532
|
+
{group.pages.length > 0 && (
|
|
533
|
+
<ul className={level === 1 ? 'ml-3' : ''}>
|
|
534
|
+
{group.pages.map((page) => (
|
|
535
|
+
<NavPage
|
|
536
|
+
key={page.path}
|
|
537
|
+
page={page}
|
|
538
|
+
pathname={activePathname}
|
|
539
|
+
layout={layout}
|
|
540
|
+
onNavigate={handleNavigate}
|
|
541
|
+
linkPrefix={linkPrefix}
|
|
542
|
+
showSpinner={spinnerPathname === `${linkPrefix}/${page.path}`}
|
|
543
|
+
prefetch={shouldPrefetch}
|
|
544
|
+
/>
|
|
545
|
+
))}
|
|
546
|
+
</ul>
|
|
547
|
+
)}
|
|
548
|
+
{group.nested && group.nested.length > 0 && (
|
|
549
|
+
<div className={level === 0 ? '' : level === 1 ? 'ml-3' : 'mt-4'}>
|
|
550
|
+
{group.nested.map((nested, childIndex) => renderGroup(nested, level + 1, [...indexPath, childIndex]))}
|
|
551
|
+
</div>
|
|
552
|
+
)}
|
|
553
|
+
</>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Render a navigation group (supports nesting). Top-level named groups are
|
|
558
|
+
// permanent sections: the header never collapses, its contents stay mounted,
|
|
559
|
+
// and clicking it navigates to the group's first page. Nested named groups
|
|
560
|
+
// are accordions: a button carrying aria-expanded/aria-controls toggles an
|
|
561
|
+
// addressable panel whose descendants mount only while expanded.
|
|
562
|
+
function renderGroup(group: ResolvedGroup, level: number = 0, indexPath: GroupIndexPath = []) {
|
|
401
563
|
// Skip rendering if group has no name and no content
|
|
402
564
|
if (!group.name && group.pages.length === 0 && !group.nested?.length && !group.items?.length) {
|
|
403
565
|
return null;
|
|
404
566
|
}
|
|
405
567
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
// expanded (see the rendering guard below), so `level !== 0` is safe
|
|
409
|
-
// shorthand there.
|
|
410
|
-
const shouldPrefetch = level !== 0 || isExpanded || !group.name;
|
|
568
|
+
const key = groupStateKey(scope, indexPath);
|
|
569
|
+
const isExpanded = expandedGroups.has(key);
|
|
411
570
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
{
|
|
415
|
-
|
|
571
|
+
if (level === 0) {
|
|
572
|
+
return (
|
|
573
|
+
<div key={key} className="ml-6">
|
|
574
|
+
{group.name && (
|
|
416
575
|
<button
|
|
417
|
-
|
|
576
|
+
type="button"
|
|
577
|
+
onClick={() => handleGroupClick(group, key)}
|
|
418
578
|
className="flex items-center justify-between w-full text-left mb-2 cursor-pointer"
|
|
419
579
|
>
|
|
420
580
|
<span className="text-sm font-semibold text-[var(--color-text-tertiary)] flex items-center gap-2">
|
|
@@ -429,101 +589,47 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
429
589
|
)}
|
|
430
590
|
</span>
|
|
431
591
|
</button>
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
<span className="truncate">{group.name}</span>
|
|
438
|
-
<i
|
|
439
|
-
className={`fa-solid fa-chevron-right text-[8px] text-[var(--color-text-muted)] transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
|
440
|
-
aria-hidden="true"
|
|
441
|
-
/>
|
|
442
|
-
{group.tag && (
|
|
443
|
-
<span className="px-1.5 py-0.5 text-[10px] font-semibold rounded bg-[var(--color-accent)]/15 text-[var(--color-accent)]">
|
|
444
|
-
{group.tag}
|
|
445
|
-
</span>
|
|
446
|
-
)}
|
|
447
|
-
</button>
|
|
448
|
-
)
|
|
449
|
-
)}
|
|
592
|
+
)}
|
|
593
|
+
{renderGroupContents(group, level, indexPath)}
|
|
594
|
+
</div>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
450
597
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
showSpinner={spinnerPathname === `${linkPrefix}/${p.page.path}`}
|
|
474
|
-
prefetch={shouldPrefetch}
|
|
475
|
-
/>
|
|
476
|
-
))}
|
|
477
|
-
</ul>
|
|
478
|
-
);
|
|
479
|
-
currentPages = [];
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
group.items.forEach((item, idx) => {
|
|
484
|
-
if (item.type === 'page') {
|
|
485
|
-
currentPages.push(item);
|
|
486
|
-
} else {
|
|
487
|
-
flushPages();
|
|
488
|
-
elements.push(
|
|
489
|
-
<div key={item.group.name || `nested-${idx}`} className={level === 0 ? '' : level === 1 ? '' : 'mt-4'}>
|
|
490
|
-
{renderGroup(item.group, level + 1)}
|
|
491
|
-
</div>
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
flushPages();
|
|
496
|
-
|
|
497
|
-
return elements;
|
|
498
|
-
})()}
|
|
499
|
-
</div>
|
|
500
|
-
) : (
|
|
501
|
-
// Fallback to legacy pages + nested (for backwards compatibility)
|
|
502
|
-
<>
|
|
503
|
-
{group.pages.length > 0 && (
|
|
504
|
-
<ul className={level === 1 ? 'ml-3' : ''}>
|
|
505
|
-
{group.pages.map((page) => (
|
|
506
|
-
<NavPage
|
|
507
|
-
key={page.path}
|
|
508
|
-
page={page}
|
|
509
|
-
pathname={activePathname}
|
|
510
|
-
layout={layout}
|
|
511
|
-
onNavigate={handleNavigate}
|
|
512
|
-
linkPrefix={linkPrefix}
|
|
513
|
-
showSpinner={spinnerPathname === `${linkPrefix}/${page.path}`}
|
|
514
|
-
prefetch={shouldPrefetch}
|
|
515
|
-
/>
|
|
516
|
-
))}
|
|
517
|
-
</ul>
|
|
518
|
-
)}
|
|
519
|
-
{group.nested && group.nested.length > 0 && (
|
|
520
|
-
<div className={level === 0 ? '' : level === 1 ? 'ml-3' : 'mt-4'}>
|
|
521
|
-
{group.nested.map((nested) => renderGroup(nested, level + 1))}
|
|
522
|
-
</div>
|
|
523
|
-
)}
|
|
524
|
-
</>
|
|
598
|
+
const panelId = groupPanelId(scope, indexPath);
|
|
599
|
+
const outerClassName = level === 1 ? '' : 'ml-3';
|
|
600
|
+
const buttonClassName = `nav-group-l1 flex items-center gap-1.5 ${layout === 'header-logo' ? 'pr-3' : 'px-3'} py-1.5 rounded-lg text-sm transition-colors w-full text-left text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-offset-2`;
|
|
601
|
+
|
|
602
|
+
return (
|
|
603
|
+
<div key={key} className={outerClassName}>
|
|
604
|
+
{group.name && (
|
|
605
|
+
<button
|
|
606
|
+
type="button"
|
|
607
|
+
aria-expanded={isExpanded}
|
|
608
|
+
aria-controls={panelId}
|
|
609
|
+
onClick={() => handleGroupClick(group, key)}
|
|
610
|
+
className={buttonClassName}
|
|
611
|
+
>
|
|
612
|
+
{group.icon && (
|
|
613
|
+
<i className={`${getIconClass(group.icon)} text-[14px]`} aria-hidden="true" />
|
|
614
|
+
)}
|
|
615
|
+
<span className="flex-1 min-w-0 truncate">{group.name}</span>
|
|
616
|
+
{group.tag && (
|
|
617
|
+
<span className="px-1.5 py-0.5 text-[10px] font-semibold rounded bg-[var(--color-accent)]/15 text-[var(--color-accent)]">
|
|
618
|
+
{group.tag}
|
|
619
|
+
</span>
|
|
525
620
|
)}
|
|
526
|
-
|
|
621
|
+
<i
|
|
622
|
+
className={`fa-solid fa-chevron-right text-[8px] text-[var(--color-text-muted)] transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
|
623
|
+
aria-hidden="true"
|
|
624
|
+
/>
|
|
625
|
+
</button>
|
|
626
|
+
)}
|
|
627
|
+
{group.name ? (
|
|
628
|
+
<div id={panelId} hidden={!isExpanded}>
|
|
629
|
+
{isExpanded && renderGroupContents(group, level, indexPath)}
|
|
630
|
+
</div>
|
|
631
|
+
) : (
|
|
632
|
+
renderGroupContents(group, level, indexPath)
|
|
527
633
|
)}
|
|
528
634
|
</div>
|
|
529
635
|
);
|
|
@@ -818,7 +924,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
818
924
|
|
|
819
925
|
{/* Navigation Groups */}
|
|
820
926
|
<div className="sidebar-nav-groups">
|
|
821
|
-
{resolvedNav.groups.map((group) => renderGroup(group))}
|
|
927
|
+
{resolvedNav.groups.map((group, index) => renderGroup(group, 0, [index]))}
|
|
822
928
|
</div>
|
|
823
929
|
</nav>
|
|
824
930
|
|