Skip to content

Commit e022a2e

Browse files
committed
feat: Sidebar enhancements
1 parent e45645a commit e022a2e

8 files changed

Lines changed: 280 additions & 20 deletions

File tree

src/generators/web/constants.mjs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,125 @@ export const SPECULATION_RULES = JSON.stringify({
8181
{ where: { selector_matches: '[rel~=prefetch]' }, eagerness: 'moderate' },
8282
],
8383
});
84+
85+
/**
86+
* @deprecated This is being exported temporarily during the transition period.
87+
* For a more general solution, category information should be added to pages in
88+
* YAML format, and this array should be removed.
89+
*
90+
* Defines the sidebar navigation groups and their associated page URLs.
91+
* @type {Array<{ groupName: string, items: Array<string> }>}
92+
*/
93+
export const SIDEBAR_GROUPS = [
94+
{
95+
groupName: 'Getting Started',
96+
items: [
97+
'documentation.html',
98+
'synopsis.html',
99+
'cli.html',
100+
'environment_variables.html',
101+
'globals.html',
102+
],
103+
},
104+
{
105+
groupName: 'Module System',
106+
items: [
107+
'modules.html',
108+
'esm.html',
109+
'module.html',
110+
'packages.html',
111+
'typescript.html',
112+
],
113+
},
114+
{
115+
groupName: 'Networking & Protocols',
116+
items: [
117+
'http.html',
118+
'http2.html',
119+
'https.html',
120+
'net.html',
121+
'dns.html',
122+
'dgram.html',
123+
'quic.html',
124+
],
125+
},
126+
{
127+
groupName: 'File System & I/O',
128+
items: [
129+
'fs.html',
130+
'path.html',
131+
'buffer.html',
132+
'stream.html',
133+
'string_decoder.html',
134+
'zlib.html',
135+
'readline.html',
136+
'tty.html',
137+
],
138+
},
139+
{
140+
groupName: 'Asynchronous Programming',
141+
items: [
142+
'async_context.html',
143+
'async_hooks.html',
144+
'events.html',
145+
'timers.html',
146+
'webstreams.html',
147+
],
148+
},
149+
{
150+
groupName: 'Process & Concurrency',
151+
items: [
152+
'process.html',
153+
'child_process.html',
154+
'cluster.html',
155+
'worker_threads.html',
156+
'os.html',
157+
],
158+
},
159+
{
160+
groupName: 'Security & Cryptography',
161+
items: ['crypto.html', 'webcrypto.html', 'permissions.html', 'tls.html'],
162+
},
163+
{
164+
groupName: 'Data & URL Utilities',
165+
items: ['url.html', 'querystring.html', 'punycode.html', 'util.html'],
166+
},
167+
{
168+
groupName: 'Debugging & Diagnostics',
169+
items: [
170+
'debugger.html',
171+
'inspector.html',
172+
'console.html',
173+
'report.html',
174+
'tracing.html',
175+
'diagnostics_channel.html',
176+
'errors.html',
177+
],
178+
},
179+
{
180+
groupName: 'Testing & Assertion',
181+
items: ['test.html', 'assert.html', 'repl.html'],
182+
},
183+
{
184+
groupName: 'Performance & Observability',
185+
items: ['perf_hooks.html', 'v8.html'],
186+
},
187+
{
188+
groupName: 'Runtime & Advanced APIs',
189+
items: [
190+
'vm.html',
191+
'wasi.html',
192+
'sqlite.html',
193+
'single-executable-applications.html',
194+
'intl.html',
195+
],
196+
},
197+
{
198+
groupName: 'Native & Low-level Extensions',
199+
items: ['addons.html', 'n-api.html', 'embedding.html'],
200+
},
201+
{
202+
groupName: 'Legacy & Deprecated',
203+
items: ['deprecations.html', 'domain.html'],
204+
},
205+
];

src/generators/web/ui/components/SideBar/index.jsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SideBar from '@node-core/ui-components/Containers/Sidebar';
33

44
import styles from './index.module.css';
55
import { relative } from '../../../../../utils/url.mjs';
6+
import { buildSideBarGroups } from '../../utils/sidebar.mjs';
67

78
import { title, version, versions, pages } from '#theme/config';
89

@@ -36,32 +37,30 @@ export default ({ metadata }) => {
3637
label,
3738
}));
3839

39-
const items = pages.map(([heading, path]) => ({
40+
const items = pages.map(([heading, path, category]) => ({
4041
label: heading,
4142
link:
4243
metadata.path === path
4344
? `${metadata.basename}.html`
4445
: `${relative(path, metadata.path)}.html`,
46+
category,
4547
}));
4648

4749
return (
4850
<SideBar
4951
pathname={`${metadata.basename}.html`}
50-
groups={[{ groupName: 'API Documentation', items }]}
52+
groups={buildSideBarGroups(items)}
5153
onSelect={redirect}
5254
as={props => <a {...props} rel="prefetch" />}
5355
title="Navigation"
5456
>
55-
<div>
56-
<Select
57-
label={`${title} version`}
58-
values={compatibleVersions}
59-
inline={true}
60-
className={styles.select}
61-
placeholder={version}
62-
onChange={redirect}
63-
/>
64-
</div>
57+
<Select
58+
label={`${title} version`}
59+
values={compatibleVersions}
60+
className={styles.select}
61+
placeholder={version}
62+
onChange={redirect}
63+
/>
6564
</SideBar>
6665
);
6766
};

src/generators/web/ui/index.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,8 @@ main {
118118
}
119119
}
120120
}
121+
122+
/* Override the min-width of the select component used for version selection in the sidebar */
123+
[class*='select'] button[role='combobox'] {
124+
min-width: initial;
125+
}

src/generators/web/ui/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ declare module '#theme/config' {
1515
major: number;
1616
}>;
1717
export const editURL: string;
18-
export const pages: Array<[string, string]>;
18+
export const pages: Array<[string, string, string?]>;
1919
export const languageDisplayNameMap: Map<string, string>;
2020
}
2121

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict';
2+
3+
import assert from 'node:assert/strict';
4+
import { describe, it } from 'node:test';
5+
6+
import { buildSideBarGroups } from '../sidebar.mjs';
7+
8+
describe('buildSideBarGroups', () => {
9+
it('groups entries by category and preserves insertion order', () => {
10+
const frontmatter = [
11+
{ label: 'FS', link: '/api/fs.html', category: 'File System' },
12+
{ label: 'HTTP', link: '/api/http.html', category: 'Networking' },
13+
{ label: 'Path', link: '/api/path.html', category: 'File System' },
14+
];
15+
16+
const result = buildSideBarGroups(frontmatter);
17+
18+
assert.deepStrictEqual(result, [
19+
{
20+
groupName: 'File System',
21+
items: [
22+
{ label: 'FS', link: '/api/fs.html' },
23+
{ label: 'Path', link: '/api/path.html' },
24+
],
25+
},
26+
{
27+
groupName: 'Networking',
28+
items: [{ label: 'HTTP', link: '/api/http.html' }],
29+
},
30+
]);
31+
});
32+
33+
it('puts entries without category into an Others group at the end by default', () => {
34+
const frontmatter = [
35+
{ label: 'Buffer', link: '/api/buffer.html', category: 'Binary' },
36+
{ label: 'Unknown', link: '/api/unknown.html' },
37+
{ label: 'Config', link: '/api/config.html', category: '' },
38+
];
39+
40+
const result = buildSideBarGroups(frontmatter);
41+
42+
assert.equal(result.at(-1).groupName, 'Others');
43+
assert.deepStrictEqual(result.at(-1).items, [
44+
{ label: 'Unknown', link: '/api/unknown.html' },
45+
{ label: 'Config', link: '/api/config.html' },
46+
]);
47+
});
48+
49+
it('uses a custom default group name when provided', () => {
50+
const result = buildSideBarGroups(
51+
[{ label: 'Unknown', link: '/api/unknown.html' }],
52+
'General'
53+
);
54+
55+
assert.deepStrictEqual(result, [
56+
{
57+
groupName: 'General',
58+
items: [{ label: 'Unknown', link: '/api/unknown.html' }],
59+
},
60+
]);
61+
});
62+
63+
it('returns an empty array when given no entries', () => {
64+
assert.deepStrictEqual(buildSideBarGroups([]), []);
65+
});
66+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { SIDEBAR_GROUPS } from '../../constants.mjs';
2+
3+
/**
4+
* @deprecated This is being exported temporarily during the transition period.
5+
* Reverse lookup: filename (e.g. 'fs.html') → groupName, used as category
6+
* fallback for pages without explicit category in metadata.
7+
*/
8+
export const fileToGroup = new Map(
9+
SIDEBAR_GROUPS.flatMap(({ groupName, items }) =>
10+
items.map(item => [item, groupName])
11+
)
12+
);
13+
14+
/**
15+
* Builds grouped sidebar navigation from categorized page entries.
16+
* Pages without a category are placed under the provided default group.
17+
*
18+
* @param {Array<{ label: string, link: string, category?: string }>} frontmatter
19+
* @param {string} [defaultGroupName='Others']
20+
* @returns {Array<{ groupName: string, items: Array<{ label: string, link: string }> }>}
21+
*/
22+
export const buildSideBarGroups = (
23+
frontmatter,
24+
defaultGroupName = 'Others'
25+
) => {
26+
const groups = new Map();
27+
const others = [];
28+
29+
// Group entries by category while preserving insertion order
30+
for (const { label, link, category } of frontmatter) {
31+
const linkFilename = link.split('/').at(-1);
32+
33+
// Skip index pages as they are typically the main entry point for a section
34+
// and may not need to be listed separately in the sidebar.
35+
if (linkFilename === 'index.html') {
36+
continue;
37+
}
38+
39+
const resolvedCategory = category ?? fileToGroup.get(linkFilename);
40+
41+
if (!resolvedCategory) {
42+
others.push({ label, link });
43+
continue;
44+
}
45+
46+
const items = groups.get(resolvedCategory) ?? [];
47+
items.push({ label, link });
48+
groups.set(resolvedCategory, items);
49+
}
50+
51+
// Convert the groups map to an array while preserving the original order of categories
52+
const orderedGroups = [...groups.entries()].map(([groupName, items]) => ({
53+
groupName,
54+
items,
55+
}));
56+
57+
if (others.length > 0) {
58+
orderedGroups.push({ groupName: defaultGroupName, items: others });
59+
}
60+
61+
return orderedGroups;
62+
};

src/generators/web/utils/__tests__/config.test.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const makeEntry = (api, name, path) => ({
4343
data: {
4444
api,
4545
path,
46+
category: api === 'fs' ? 'File System' : undefined,
4647
heading: { depth: 1, data: { name } },
4748
},
4849
});
@@ -101,7 +102,7 @@ describe('buildVersionEntries', () => {
101102
});
102103

103104
describe('buildPageList', () => {
104-
it('returns sorted [name, path] tuples from input entries', () => {
105+
it('returns sorted [name, path, category] tuples from input entries', () => {
105106
const input = [
106107
makeEntry('http', 'HTTP', '/http'),
107108
makeEntry('fs', 'File System', '/fs'),
@@ -111,8 +112,8 @@ describe('buildPageList', () => {
111112

112113
assert.equal(result.length, 2);
113114
// Sorted alphabetically by name
114-
assert.deepStrictEqual(result[0], ['File System', '/fs']);
115-
assert.deepStrictEqual(result[1], ['HTTP', '/http']);
115+
assert.deepStrictEqual(result[0], ['File System', '/fs', 'File System']);
116+
assert.deepStrictEqual(result[1], ['HTTP', '/http', undefined]);
116117
});
117118

118119
it('filters out entries whose heading depth is not 1', () => {
@@ -130,7 +131,7 @@ describe('buildPageList', () => {
130131
const result = buildPageList(input);
131132

132133
assert.equal(result.length, 1);
133-
assert.deepStrictEqual(result[0], ['File System', '/fs']);
134+
assert.deepStrictEqual(result[0], ['File System', '/fs', 'File System']);
134135
});
135136
});
136137

src/generators/web/utils/config.mjs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ export function buildVersionEntries(config, pageURLBase) {
3333
* Pre-compute sorted page list for sidebar navigation.
3434
*
3535
* @param {Array<import('../../jsx-ast/utils/buildContent.mjs').JSXContent>} input
36-
* @returns {Array<[string, string]>}
36+
* @returns {Array<[string, string, string?]>}
3737
*/
3838
export function buildPageList(input) {
39-
const headNodes = getSortedHeadNodes(input.map(e => e.data));
40-
return headNodes.map(node => [node.heading.data.name, node.path]);
39+
const headNodes = getSortedHeadNodes(input.map(({ data }) => data));
40+
41+
return headNodes.map(({ path, category, heading }) => [
42+
heading.data.name,
43+
path,
44+
category,
45+
]);
4146
}
4247

4348
/**

0 commit comments

Comments
 (0)