@defra/docusaurus-theme-govuk 0.0.5-alpha → 0.0.6-alpha
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/README.md +60 -9
- package/index.js +92 -0
- package/package.json +3 -1
- package/src/css/components.scss +10 -0
- package/src/theme/Layout/index.js +44 -8
- package/src/theme/SidebarNav/index.js +84 -0
package/README.md
CHANGED
|
@@ -69,17 +69,23 @@ module.exports = {
|
|
|
69
69
|
|
|
70
70
|
navigation: [
|
|
71
71
|
{
|
|
72
|
+
// Auto sidebar: headings from docs/index.md become sidebar items
|
|
72
73
|
text: 'Documentation',
|
|
73
74
|
href: '/',
|
|
75
|
+
sidebar: 'auto',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
// Hardcoded sidebar: full control over labels, ordering, and nesting
|
|
79
|
+
text: 'API Reference',
|
|
80
|
+
href: '/api',
|
|
74
81
|
sidebar: [
|
|
75
|
-
{ text: 'Introduction', href: '/' },
|
|
76
|
-
{ text: 'Getting Started', href: '/getting-started' },
|
|
82
|
+
{ text: 'Introduction', href: '/api' },
|
|
77
83
|
{
|
|
78
|
-
text: '
|
|
79
|
-
href: '/api',
|
|
84
|
+
text: 'Methods',
|
|
85
|
+
href: '/api/methods',
|
|
80
86
|
items: [
|
|
81
|
-
{ text: '
|
|
82
|
-
{ text: '
|
|
87
|
+
{ text: 'Initialise', href: '/api/methods#initialise' },
|
|
88
|
+
{ text: 'Destroy', href: '/api/methods#destroy' },
|
|
83
89
|
],
|
|
84
90
|
},
|
|
85
91
|
],
|
|
@@ -121,14 +127,14 @@ Array of top-level navigation items. Each item appears in the Service Navigation
|
|
|
121
127
|
|----------|------|-------------|
|
|
122
128
|
| `text` | `string` | Navigation link text |
|
|
123
129
|
| `href` | `string` | Base path for this section |
|
|
124
|
-
| `sidebar` | `array` | Optional
|
|
130
|
+
| `sidebar` | `array \| 'auto'` | Optional. Sidebar items for this section, or `'auto'` to generate from headings (see [Sidebar Configuration](#sidebar-configuration)) |
|
|
125
131
|
|
|
126
|
-
|
|
132
|
+
When `sidebar` is an array, each item:
|
|
127
133
|
|
|
128
134
|
| Property | Type | Description |
|
|
129
135
|
|----------|------|-------------|
|
|
130
136
|
| `text` | `string` | Sidebar link text |
|
|
131
|
-
| `href` | `string` | Path
|
|
137
|
+
| `href` | `string` | Path for this item (absolute, e.g. `/api/methods`) |
|
|
132
138
|
| `items` | `array` | Optional nested sidebar items (one level of nesting) |
|
|
133
139
|
|
|
134
140
|
#### `themeConfig.govuk.phaseBanner`
|
|
@@ -154,6 +160,51 @@ The sidebar supports up to 3 levels:
|
|
|
154
160
|
2. **Level 2**: Sidebar items
|
|
155
161
|
3. **Level 3**: Nested sidebar items (collapsible groups)
|
|
156
162
|
|
|
163
|
+
### Hardcoded sidebar
|
|
164
|
+
|
|
165
|
+
Pass an array to `sidebar` to define the structure explicitly. This gives full control over labels, ordering, and anchor links:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
sidebar: [
|
|
169
|
+
{ text: 'Overview', href: '/api' },
|
|
170
|
+
{
|
|
171
|
+
text: 'Constructor',
|
|
172
|
+
href: '/api#constructor',
|
|
173
|
+
items: [
|
|
174
|
+
{ text: 'Options', href: '/api#options' },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Nested groups are collapsible. A group is expanded when the current URL hash matches either the group's own `href` anchor or any child anchor.
|
|
181
|
+
|
|
182
|
+
### Auto sidebar
|
|
183
|
+
|
|
184
|
+
Set `sidebar: 'auto'` on a navigation section to generate the sidebar automatically from the section's corresponding Markdown document at build time:
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
{
|
|
188
|
+
text: 'API Reference',
|
|
189
|
+
href: '/api',
|
|
190
|
+
sidebar: 'auto',
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The theme reads `docs/<slug>.md` (or `.mdx`) where `<slug>` is derived from `href` (e.g. `href: '/api'` → `docs/api.md`). It parses the document's headings and builds the sidebar as follows:
|
|
195
|
+
|
|
196
|
+
- `##` (h2) headings become top-level sidebar items
|
|
197
|
+
- `###` (h3) headings become nested items under the preceding h2
|
|
198
|
+
- Heading text is stripped of Markdown syntax (bold, italic, inline code, links) to produce plain-text labels
|
|
199
|
+
|
|
200
|
+
The sidebar is resolved once at build time and serialised into the site configuration. No runtime file reads occur.
|
|
201
|
+
|
|
202
|
+
#### Limitations
|
|
203
|
+
|
|
204
|
+
- Only h2 and h3 headings are included; h4 and deeper are ignored.
|
|
205
|
+
- The document must be in the `docs/` directory at the root of your Docusaurus site.
|
|
206
|
+
- Heading IDs set via the `{#custom-id}` syntax are not yet respected — the generated anchor will use the slugified heading text.
|
|
207
|
+
|
|
157
208
|
## Overriding Components
|
|
158
209
|
|
|
159
210
|
You can override any theme component by creating a file at the same path in your project's `src/theme/` directory. For example, to override the 404 page:
|
package/index.js
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
// remove-markdown strips markdown syntax leaving plain text — used for sidebar labels.
|
|
4
|
+
const removeMarkdown = require('remove-markdown');
|
|
5
|
+
// github-slugger is what Docusaurus itself uses internally to generate heading
|
|
6
|
+
// anchor IDs. Using the same library guarantees our sidebar hrefs match the
|
|
7
|
+
// IDs emitted in the built HTML. A stateful GithubSlugger instance also
|
|
8
|
+
// handles deduplication automatically (second "Options" → "options-1", etc.).
|
|
9
|
+
const GithubSlugger = require('github-slugger');
|
|
10
|
+
|
|
11
|
+
// Parse markdown content and build a sidebar config from h2/h3 headings.
|
|
12
|
+
// h2 → top-level items; h3 → nested items under the preceding h2.
|
|
13
|
+
// Items include both the display text and an anchor href (basePath + '#' + anchor).
|
|
14
|
+
function parseHeadingsToSidebar(content, basePath) {
|
|
15
|
+
// Strip YAML front-matter only if the file genuinely starts with ---
|
|
16
|
+
// (do NOT use a greedy regex: api.md and other docs use --- as horizontal rules)
|
|
17
|
+
let stripped = content;
|
|
18
|
+
if (content.startsWith('---\n') || content.startsWith('---\r\n')) {
|
|
19
|
+
const closeIdx = content.indexOf('\n---', 3);
|
|
20
|
+
if (closeIdx !== -1) {
|
|
21
|
+
// Skip past the closing --- and any trailing newline
|
|
22
|
+
stripped = content.slice(closeIdx + 4).replace(/^\n/, '');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const lines = stripped.split('\n');
|
|
26
|
+
|
|
27
|
+
// One slugger instance per document — its internal counter provides the
|
|
28
|
+
// same -1, -2 deduplication that Docusaurus emits in the built HTML.
|
|
29
|
+
const slugger = new GithubSlugger();
|
|
30
|
+
|
|
31
|
+
const items = [];
|
|
32
|
+
let currentH2 = null;
|
|
33
|
+
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const h2 = line.match(/^## (.+)$/);
|
|
36
|
+
const h3 = line.match(/^### (.+)$/);
|
|
37
|
+
|
|
38
|
+
if (h2) {
|
|
39
|
+
const raw = h2[1].trim();
|
|
40
|
+
const text = removeMarkdown(raw);
|
|
41
|
+
const anchor = slugger.slug(raw);
|
|
42
|
+
currentH2 = { text, href: `${basePath}#${anchor}`, _anchor: anchor };
|
|
43
|
+
items.push(currentH2);
|
|
44
|
+
} else if (h3 && currentH2) {
|
|
45
|
+
const raw = h3[1].trim();
|
|
46
|
+
const text = removeMarkdown(raw);
|
|
47
|
+
const anchor = slugger.slug(raw);
|
|
48
|
+
if (!currentH2.items) currentH2.items = [];
|
|
49
|
+
currentH2.items.push({ text, href: `${basePath}#${anchor}` });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return items.map(({ _anchor, ...item }) => item);
|
|
54
|
+
}
|
|
3
55
|
|
|
4
56
|
module.exports = function themeGovuk(context, options) {
|
|
5
57
|
const siteDir = context.siteDir;
|
|
@@ -21,6 +73,41 @@ module.exports = function themeGovuk(context, options) {
|
|
|
21
73
|
// The base URL for this Docusaurus site (e.g. '/interactive-map/')
|
|
22
74
|
const baseUrl = context.baseUrl || '/';
|
|
23
75
|
|
|
76
|
+
// Pre-resolve sidebar: 'auto' entries in the navigation config by mutating
|
|
77
|
+
// the themeConfig object in-place. This runs synchronously before Docusaurus
|
|
78
|
+
// serialises siteConfig into the client bundle, so useDocusaurusContext()
|
|
79
|
+
// will already see the resolved sidebar arrays — no new client imports needed.
|
|
80
|
+
const govukNav = context.siteConfig.themeConfig?.govuk?.navigation;
|
|
81
|
+
if (Array.isArray(govukNav)) {
|
|
82
|
+
const docsDir = path.join(siteDir, 'docs');
|
|
83
|
+
for (const section of govukNav) {
|
|
84
|
+
if (section.sidebar !== 'auto') continue;
|
|
85
|
+
const href = section.href || '/';
|
|
86
|
+
const slug = href.replace(/^\//, '') || 'index';
|
|
87
|
+
let resolved = false;
|
|
88
|
+
for (const ext of ['.md', '.mdx']) {
|
|
89
|
+
const candidate = path.join(docsDir, `${slug}${ext}`);
|
|
90
|
+
if (fs.existsSync(candidate)) {
|
|
91
|
+
section.sidebar = {
|
|
92
|
+
_auto: true,
|
|
93
|
+
items: parseHeadingsToSidebar(
|
|
94
|
+
fs.readFileSync(candidate, 'utf-8'),
|
|
95
|
+
href
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
resolved = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!resolved) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`[docusaurus-theme-govuk] sidebar: "auto" on "${href}" — could not find markdown file at ${path.join(docsDir, slug)}.(md|mdx)`
|
|
105
|
+
);
|
|
106
|
+
section.sidebar = { _auto: true, items: [] };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
24
111
|
return {
|
|
25
112
|
name: 'docusaurus-theme-govuk',
|
|
26
113
|
|
|
@@ -108,6 +195,11 @@ module.exports = function themeGovuk(context, options) {
|
|
|
108
195
|
resolve: {
|
|
109
196
|
extensions: ['.mjs', '.js', '.jsx', '.json', '.scss'],
|
|
110
197
|
fullySpecified: false,
|
|
198
|
+
// When the theme is consumed via a file: symlink, webpack resolves
|
|
199
|
+
// imports from the theme's real directory which has no node_modules.
|
|
200
|
+
// Adding the site's node_modules here ensures @docusaurus/* and any
|
|
201
|
+
// other peer dependency imports are found regardless of symlink depth.
|
|
202
|
+
modules: ['node_modules', path.resolve(siteDir, 'node_modules')],
|
|
111
203
|
alias: {
|
|
112
204
|
// Deduplicate React — always use the consumer's copy
|
|
113
205
|
'react': resolveFromSite('react'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/docusaurus-theme-govuk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6-alpha",
|
|
4
4
|
"description": "A Docusaurus theme implementing the GOV.UK Design System for consistent, accessible documentation sites",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
"clsx": "^2.0.0",
|
|
33
33
|
"copy-webpack-plugin": "^11.0.0",
|
|
34
34
|
"css-loader": "^7.1.0",
|
|
35
|
+
"github-slugger": "^2.0.0",
|
|
35
36
|
"govuk-frontend": "^5.14.0",
|
|
36
37
|
"postcss-loader": "^8.2.0",
|
|
37
38
|
"prism-react-renderer": "^2.3.0",
|
|
39
|
+
"remove-markdown": "^0.6.3",
|
|
38
40
|
"sass": "^1.97.0",
|
|
39
41
|
"sass-loader": "^16.0.0",
|
|
40
42
|
"style-loader": "^4.0.0"
|
package/src/css/components.scss
CHANGED
|
@@ -9,7 +9,17 @@
|
|
|
9
9
|
|
|
10
10
|
.app-layout-sidebar__nav {
|
|
11
11
|
width: 250px;
|
|
12
|
+
// Padding-left gives room for the nav's active indicator (margin-left: -14px)
|
|
13
|
+
// which would otherwise be clipped by the overflow-y containment.
|
|
14
|
+
padding-left: 15px;
|
|
12
15
|
flex-shrink: 0;
|
|
16
|
+
position: sticky;
|
|
17
|
+
top: 1rem;
|
|
18
|
+
max-height: calc(100vh - 2rem);
|
|
19
|
+
overflow-y: auto;
|
|
20
|
+
// Always reserve the scrollbar gutter so expanding subnav items don't
|
|
21
|
+
// cause a horizontal layout shift when the scrollbar appears.
|
|
22
|
+
scrollbar-gutter: stable;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
.app-layout-sidebar__content {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import '../../css/theme.scss';
|
|
3
3
|
import {SkipLink, Header, Footer, PhaseBanner, ServiceNavigation, NavigationMenu} from '@not-govuk/simple-components';
|
|
4
|
+
import SidebarNav from '../SidebarNav';
|
|
4
5
|
import {useLocation} from '@docusaurus/router';
|
|
5
6
|
import Head from '@docusaurus/Head';
|
|
6
7
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
@@ -37,6 +38,21 @@ function resolvePath(basePath, relativePath) {
|
|
|
37
38
|
return `${cleanBase}/${relativePath}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Return the effective sidebar descriptor for a navigation section.
|
|
42
|
+
// Auto-generated sidebars are { _auto: true, items: [...] } (a plain object
|
|
43
|
+
// that survives JSON serialisation, unlike array non-index properties).
|
|
44
|
+
// Manual sidebars remain plain arrays and are wrapped here for uniform access.
|
|
45
|
+
function getEffectiveSidebar(section) {
|
|
46
|
+
const s = section.sidebar;
|
|
47
|
+
if (s && typeof s === 'object' && !Array.isArray(s) && s._auto === true) {
|
|
48
|
+
return s; // { _auto: true, items: [...] }
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(s)) {
|
|
51
|
+
return { _auto: false, items: s };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
// Resolve all paths in sidebar items
|
|
41
57
|
function resolveSidebarPaths(items, basePath) {
|
|
42
58
|
return items.map(item => {
|
|
@@ -54,13 +70,21 @@ function resolveSidebarPaths(items, basePath) {
|
|
|
54
70
|
});
|
|
55
71
|
}
|
|
56
72
|
|
|
57
|
-
// Find the active navigation section based on current path
|
|
73
|
+
// Find the active navigation section based on current path.
|
|
58
74
|
function getActiveSection(pathname, navigation) {
|
|
59
75
|
return navigation.find(section => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
const sidebar = getEffectiveSidebar(section);
|
|
77
|
+
if (!sidebar) return false;
|
|
78
|
+
// First: check if current pathname matches the section's own base page or
|
|
79
|
+
// any sub-page. This handles auto-generated sidebars whose item hrefs
|
|
80
|
+
// include anchors (#...) and would never match a plain pathname.
|
|
81
|
+
const sectionPath = section.href || '/';
|
|
82
|
+
if (pathname === sectionPath || pathname.startsWith(sectionPath + '/')) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// Fallback: match by exact item href (for array sidebars with sub-pages
|
|
86
|
+
// whose hrefs differ from the section's base href).
|
|
87
|
+
const resolvedSidebar = resolveSidebarPaths(sidebar.items, sectionPath);
|
|
64
88
|
return resolvedSidebar.some(item => {
|
|
65
89
|
if (pathname === item.href) return true;
|
|
66
90
|
if (item.items) {
|
|
@@ -99,11 +123,16 @@ export default function Layout(props) {
|
|
|
99
123
|
return `${baseUrl}${href.startsWith('/') ? href : `/${href}`}`;
|
|
100
124
|
}
|
|
101
125
|
|
|
126
|
+
// Build-time auto-generated sidebars were resolved into the navigation
|
|
127
|
+
// array directly (sidebar: 'auto' replaced with sidebar: [...]) so we
|
|
128
|
+
// just read the array from siteConfig as normal.
|
|
129
|
+
|
|
102
130
|
// Get active section for sidebar
|
|
103
131
|
const activeSection = getActiveSection(pathname, navigation);
|
|
104
132
|
const basePath = activeSection?.href || '/';
|
|
105
|
-
const
|
|
106
|
-
|
|
133
|
+
const effectiveSidebar = activeSection ? getEffectiveSidebar(activeSection) : null;
|
|
134
|
+
const sidebarItems = effectiveSidebar
|
|
135
|
+
? resolveSidebarPaths(effectiveSidebar.items, basePath).map(item => ({
|
|
107
136
|
...item,
|
|
108
137
|
href: withBase(item.href),
|
|
109
138
|
...(item.items && {
|
|
@@ -112,6 +141,11 @@ export default function Layout(props) {
|
|
|
112
141
|
}))
|
|
113
142
|
: null;
|
|
114
143
|
|
|
144
|
+
// Anchor-based sidebars (auto-generated) use a hash-aware component.
|
|
145
|
+
// Page-based sidebars (manually configured arrays) use NavigationMenu which
|
|
146
|
+
// uses React Router's active detection for sub-page expansion.
|
|
147
|
+
const isAnchorSidebar = effectiveSidebar?._auto === true;
|
|
148
|
+
|
|
115
149
|
// Convert navigation to service navigation format (Level 1 only)
|
|
116
150
|
const serviceNavItems = navigation.map(item => ({
|
|
117
151
|
href: withBase(item.href),
|
|
@@ -164,7 +198,9 @@ export default function Layout(props) {
|
|
|
164
198
|
{sidebarItems ? (
|
|
165
199
|
<div className="app-layout-sidebar">
|
|
166
200
|
<aside className="app-layout-sidebar__nav">
|
|
167
|
-
|
|
201
|
+
{isAnchorSidebar
|
|
202
|
+
? <SidebarNav items={sidebarItems} />
|
|
203
|
+
: <NavigationMenu items={sidebarItems} />}
|
|
168
204
|
</aside>
|
|
169
205
|
<div className="app-layout-sidebar__content">
|
|
170
206
|
{children}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hash-aware sidebar navigation for anchor-based (auto-generated) sidebars.
|
|
5
|
+
*
|
|
6
|
+
* SSR / no-JS: all groups are rendered expanded so content is accessible.
|
|
7
|
+
* CSR after hydration: exactly one group expands based on the URL hash.
|
|
8
|
+
* - hash matches the group's h2 anchor → that group expands
|
|
9
|
+
* - hash matches any child's anchor → that group expands
|
|
10
|
+
* - no hash → all groups collapse
|
|
11
|
+
*
|
|
12
|
+
* State sentinel:
|
|
13
|
+
* null = not yet hydrated (SSR or before first useEffect) → expand all
|
|
14
|
+
* '' = JS loaded, no hash → collapse all
|
|
15
|
+
* str = JS loaded, hash present → expand matching group only
|
|
16
|
+
*/
|
|
17
|
+
export default function SidebarNav({ items }) {
|
|
18
|
+
const [hash, setHash] = useState(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const update = () => setHash(window.location.hash.slice(1));
|
|
22
|
+
update();
|
|
23
|
+
window.addEventListener('hashchange', update);
|
|
24
|
+
return () => window.removeEventListener('hashchange', update);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const cls = 'not-govuk-navigation-menu';
|
|
28
|
+
const lCls = `${cls}__list`;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<nav className={cls}>
|
|
32
|
+
<ul className={lCls}>
|
|
33
|
+
{items.map((item, i) => {
|
|
34
|
+
const anchor = item.href ? (item.href.split('#')[1] ?? '') : '';
|
|
35
|
+
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
|
36
|
+
const childAnchors = hasChildren
|
|
37
|
+
? item.items.map(sub => sub.href ? (sub.href.split('#')[1] ?? '') : '')
|
|
38
|
+
: [];
|
|
39
|
+
|
|
40
|
+
// null → not yet hydrated, expand everything
|
|
41
|
+
// '' → no hash, collapse everything
|
|
42
|
+
// value → expand if hash is this group's h2 anchor or any child anchor
|
|
43
|
+
const hashMatchesGroup =
|
|
44
|
+
hash !== '' && (
|
|
45
|
+
hash === anchor ||
|
|
46
|
+
childAnchors.includes(hash)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const expanded = hasChildren && (hash === null || hashMatchesGroup);
|
|
50
|
+
const active = hash !== null && hashMatchesGroup;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<li
|
|
54
|
+
key={i}
|
|
55
|
+
className={`${lCls}__item${active ? ` ${lCls}__item--active` : ''}`}
|
|
56
|
+
>
|
|
57
|
+
<a href={item.href} className={`${lCls}__link`}>
|
|
58
|
+
{item.text}
|
|
59
|
+
</a>
|
|
60
|
+
{expanded && (
|
|
61
|
+
<ul className={`${lCls}__subitems`}>
|
|
62
|
+
{item.items.map((sub, j) => {
|
|
63
|
+
const subAnchor = sub.href ? (sub.href.split('#')[1] ?? '') : '';
|
|
64
|
+
const subActive = hash !== null && hash === subAnchor;
|
|
65
|
+
return (
|
|
66
|
+
<li
|
|
67
|
+
key={j}
|
|
68
|
+
className={`${lCls}__item${subActive ? ` ${lCls}__item--active` : ''}`}
|
|
69
|
+
>
|
|
70
|
+
<a href={sub.href} className={`${lCls}__link`}>
|
|
71
|
+
{sub.text}
|
|
72
|
+
</a>
|
|
73
|
+
</li>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</ul>
|
|
77
|
+
)}
|
|
78
|
+
</li>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</ul>
|
|
82
|
+
</nav>
|
|
83
|
+
);
|
|
84
|
+
}
|