@defra/docusaurus-theme-govuk 0.0.13-alpha → 0.0.14-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/index.js +71 -2
- package/package.json +1 -1
- package/src/css/components.scss +158 -19
- package/src/css/prose-scope.scss +13 -13
- package/src/css/theme.scss +13 -3
- package/src/theme/Layout/index.js +60 -13
- package/src/theme/SearchBar/index.js +28 -23
- package/src/theme/SidebarNav/index.js +77 -43
package/index.js
CHANGED
|
@@ -8,6 +8,34 @@ const removeMarkdown = require('remove-markdown');
|
|
|
8
8
|
// handles deduplication automatically (second "Options" → "options-1", etc.).
|
|
9
9
|
const GithubSlugger = require('github-slugger');
|
|
10
10
|
|
|
11
|
+
// Remark plugin: converts `<!-- no-sidebar -->` inline HTML comments inside
|
|
12
|
+
// headings into a `data-no-sidebar` HTML attribute (via mdast hProperties) so
|
|
13
|
+
// the runtime DOM scanner can detect and skip them. MDX/remark-rehype drops
|
|
14
|
+
// raw HTML comment nodes before they reach the browser DOM, so a compile-time
|
|
15
|
+
// transformation is the only reliable way to carry this signal through.
|
|
16
|
+
function remarkNoSidebar() {
|
|
17
|
+
return function (tree) {
|
|
18
|
+
if (!tree || !Array.isArray(tree.children)) return;
|
|
19
|
+
for (const node of tree.children) {
|
|
20
|
+
if (node.type !== 'heading') continue;
|
|
21
|
+
const children = node.children || [];
|
|
22
|
+
const commentIdx = children.findIndex(
|
|
23
|
+
child => child.type === 'html' && child.value?.includes('no-sidebar')
|
|
24
|
+
);
|
|
25
|
+
if (commentIdx === -1) continue;
|
|
26
|
+
// Strip the comment node from the heading children
|
|
27
|
+
node.children = children.filter((_, i) => i !== commentIdx);
|
|
28
|
+
// Trim trailing whitespace in the preceding text node, if any
|
|
29
|
+
const prev = node.children[commentIdx - 1];
|
|
30
|
+
if (prev?.type === 'text') prev.value = prev.value.trimEnd();
|
|
31
|
+
// Attach hProperties so remark-rehype passes data-no-sidebar to the element
|
|
32
|
+
node.data = node.data || {};
|
|
33
|
+
node.data.hProperties = node.data.hProperties || {};
|
|
34
|
+
node.data.hProperties['data-no-sidebar'] = 'true';
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
// Parse markdown content and build a sidebar config from h2/h3 headings.
|
|
12
40
|
// h2 → top-level items; h3 → nested items under the preceding h2.
|
|
13
41
|
// Items include both the display text and an anchor href (basePath + '#' + anchor).
|
|
@@ -32,8 +60,8 @@ function parseHeadingsToSidebar(content, basePath) {
|
|
|
32
60
|
let currentH2 = null;
|
|
33
61
|
|
|
34
62
|
for (const line of lines) {
|
|
35
|
-
const h2 = line.match(/^## (.+)$/);
|
|
36
|
-
const h3 = line.match(/^### (.+)$/);
|
|
63
|
+
const h2 = !line.includes('<!-- no-sidebar -->') && line.match(/^## (.+)$/);
|
|
64
|
+
const h3 = !line.includes('<!-- no-sidebar -->') && line.match(/^### (.+)$/);
|
|
37
65
|
|
|
38
66
|
if (h2) {
|
|
39
67
|
const raw = h2[1].trim();
|
|
@@ -53,6 +81,40 @@ function parseHeadingsToSidebar(content, basePath) {
|
|
|
53
81
|
return items.map(({ _anchor, ...item }) => item);
|
|
54
82
|
}
|
|
55
83
|
|
|
84
|
+
// Mutate the webpack config to inject remarkNoSidebar into the MDX loader.
|
|
85
|
+
// Extracted to keep configureWebpack's cognitive complexity within limits.
|
|
86
|
+
function injectNoSidebarPlugin(config) {
|
|
87
|
+
try {
|
|
88
|
+
injectIntoRules(config.module?.rules || []);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Non-fatal: the build-time parseHeadingsToSidebar still handles static sidebars.
|
|
91
|
+
console.warn('[docusaurus-theme-govuk] Could not inject no-sidebar remark plugin:', e.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normaliseUses(use) {
|
|
96
|
+
if (Array.isArray(use)) return use;
|
|
97
|
+
return use ? [use] : [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function injectIntoUses(uses) {
|
|
101
|
+
for (const use of uses) {
|
|
102
|
+
if (typeof use?.loader === 'string' && use.loader.includes('mdx-loader')) {
|
|
103
|
+
use.options = use.options || {};
|
|
104
|
+
use.options.beforeDefaultRemarkPlugins = use.options.beforeDefaultRemarkPlugins || [];
|
|
105
|
+
use.options.beforeDefaultRemarkPlugins.push(remarkNoSidebar);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function injectIntoRules(rules) {
|
|
111
|
+
for (const rule of rules) {
|
|
112
|
+
if (!rule || typeof rule !== 'object') continue;
|
|
113
|
+
if (Array.isArray(rule.oneOf)) injectIntoRules(rule.oneOf);
|
|
114
|
+
injectIntoUses(normaliseUses(rule.use));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
56
118
|
module.exports = function themeGovuk(context, options) {
|
|
57
119
|
const siteDir = context.siteDir;
|
|
58
120
|
|
|
@@ -144,6 +206,13 @@ module.exports = function themeGovuk(context, options) {
|
|
|
144
206
|
},
|
|
145
207
|
|
|
146
208
|
configureWebpack(config, isServer, utils) {
|
|
209
|
+
// Inject our no-sidebar remark plugin into the MDX loader so that
|
|
210
|
+
// `<!-- no-sidebar -->` comments in headings are converted to
|
|
211
|
+
// data-no-sidebar attributes before the DOM is rendered. We mutate the
|
|
212
|
+
// existing config directly since webpack-merge cannot deep-merge loader
|
|
213
|
+
// options arrays correctly.
|
|
214
|
+
injectNoSidebarPlugin(config);
|
|
215
|
+
|
|
147
216
|
// Helper: resolve a package from the consumer's siteDir.
|
|
148
217
|
// Uses require.resolve with paths so it follows Node's resolution
|
|
149
218
|
// (handles hoisted AND nested node_modules like @docusaurus/core/node_modules/react-router-dom).
|
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.14-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",
|
package/src/css/components.scss
CHANGED
|
@@ -9,9 +9,6 @@
|
|
|
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;
|
|
15
12
|
flex-shrink: 0;
|
|
16
13
|
position: sticky;
|
|
17
14
|
top: 1rem;
|
|
@@ -84,6 +81,120 @@
|
|
|
84
81
|
}
|
|
85
82
|
}
|
|
86
83
|
|
|
84
|
+
// Remove side bar top border
|
|
85
|
+
.app-layout-sidebar__nav {
|
|
86
|
+
overflow: auto;
|
|
87
|
+
padding-top: 0;
|
|
88
|
+
border: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Navigation menu mobile styling
|
|
92
|
+
.app-layout-sidebar__nav .not-govuk-navigation-menu__list > .not-govuk-navigation-menu__list__item {
|
|
93
|
+
border-bottom: 1px solid #cecece;
|
|
94
|
+
padding-top: 5px;
|
|
95
|
+
padding-bottom: 5px;
|
|
96
|
+
margin-bottom: 5px;
|
|
97
|
+
font-size: 1rem;
|
|
98
|
+
|
|
99
|
+
@media (min-width: 40.0625em) {
|
|
100
|
+
border-bottom: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.not-govuk-navigation-menu__list__link {
|
|
104
|
+
display: flex;
|
|
105
|
+
padding: 2px 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&--active {
|
|
109
|
+
border-left: 4px solid #1d70b8;
|
|
110
|
+
padding-left: 11px;
|
|
111
|
+
font-weight: bold;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Active indicator on sub-items.
|
|
116
|
+
// __subitems has padding-left: 15px; margin-left: -15px shifts the item back
|
|
117
|
+
// to the container edge so the 4px border sits flush on the left.
|
|
118
|
+
|
|
119
|
+
.app-layout-sidebar__nav .not-govuk-navigation-menu__list__subitems {
|
|
120
|
+
|
|
121
|
+
.not-govuk-navigation-menu__list__item {
|
|
122
|
+
padding: 5px 0 5px 15px;
|
|
123
|
+
margin-bottom: 5px;
|
|
124
|
+
border-left-width: 0px;
|
|
125
|
+
border-left-style: solid;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.not-govuk-navigation-menu__list__item--active {
|
|
129
|
+
border-left-width: 4px;
|
|
130
|
+
padding-left: 11px;
|
|
131
|
+
font-weight: bold;
|
|
132
|
+
|
|
133
|
+
.not-govuk-navigation-menu__list__link {
|
|
134
|
+
font-weight: bold;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Remove the '— ' dash from all sub-items.
|
|
139
|
+
.not-govuk-navigation-menu__list__item::before {
|
|
140
|
+
content: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
padding-left: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Sidebar nav section headings and mobile toggle
|
|
147
|
+
// Desktop: headings are non-link spans, visually distinct from link items
|
|
148
|
+
.not-govuk-navigation-menu__list__heading {
|
|
149
|
+
display: block;
|
|
150
|
+
padding: 2px 0;
|
|
151
|
+
color: #0b0c0c;
|
|
152
|
+
font-weight: bold;
|
|
153
|
+
cursor: default;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Mobile: headings become toggle buttons
|
|
157
|
+
.not-govuk-navigation-menu__list__heading-toggle {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
width: 100%;
|
|
161
|
+
padding: 2px 0;
|
|
162
|
+
background: none;
|
|
163
|
+
border: none;
|
|
164
|
+
text-align: left;
|
|
165
|
+
font-family: inherit;
|
|
166
|
+
font-size: inherit;
|
|
167
|
+
font-weight: bold;
|
|
168
|
+
color: #1d70b8;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
|
|
171
|
+
// Chevron indicator on the left
|
|
172
|
+
&::before {
|
|
173
|
+
content: '';
|
|
174
|
+
display: inline-block;
|
|
175
|
+
width: 0;
|
|
176
|
+
height: 0;
|
|
177
|
+
border-top: 4px solid transparent;
|
|
178
|
+
border-bottom: 4px solid transparent;
|
|
179
|
+
border-left: 5px solid currentColor;
|
|
180
|
+
flex-shrink: 0;
|
|
181
|
+
margin-right: 8px;
|
|
182
|
+
transition: transform 0.2s ease;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
&[aria-expanded="true"]::before {
|
|
186
|
+
transform: rotate(90deg);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
&:focus {
|
|
190
|
+
outline: 3px solid #fd0;
|
|
191
|
+
outline-offset: 0;
|
|
192
|
+
background-color: #fd0;
|
|
193
|
+
color: #0b0c0c;
|
|
194
|
+
box-shadow: 0 -2px #fd0, 0 4px #0b0c0c;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
87
198
|
// Pagination
|
|
88
199
|
.app-pagination__container {
|
|
89
200
|
display: flex;
|
|
@@ -172,6 +283,16 @@
|
|
|
172
283
|
flex-wrap: nowrap;
|
|
173
284
|
}
|
|
174
285
|
|
|
286
|
+
.govuk-header__link--homepage:focus svg g {
|
|
287
|
+
fill: #0b0c0c !important;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Mirror GDS Design System
|
|
291
|
+
.govuk-header__logotype-text {
|
|
292
|
+
font-size: 1.5rem;
|
|
293
|
+
line-height: 1.25;
|
|
294
|
+
}
|
|
295
|
+
|
|
175
296
|
.app-header-search {
|
|
176
297
|
margin-left: auto;
|
|
177
298
|
flex-shrink: 0;
|
|
@@ -185,29 +306,36 @@
|
|
|
185
306
|
width: 300px;
|
|
186
307
|
}
|
|
187
308
|
|
|
188
|
-
// Input field —
|
|
309
|
+
// Input field — white background, GOV.UK dark border, search icon on the left
|
|
189
310
|
.autocomplete__input {
|
|
190
311
|
width: 100%;
|
|
191
|
-
padding
|
|
192
|
-
|
|
312
|
+
// GOV.UK inputs use 5px vertical padding; left padding reserves space for
|
|
313
|
+
// the search icon, right padding keeps text clear of the autocomplete clear button
|
|
314
|
+
padding: 5px 8px 5px 36px;
|
|
315
|
+
// Dark border matches govuk-input — visible on white and meets contrast requirements
|
|
316
|
+
border: 2px solid transparent;
|
|
193
317
|
border-radius: 0;
|
|
194
318
|
background-color: #fff;
|
|
195
319
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230b0c0c' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
|
|
196
320
|
background-repeat: no-repeat;
|
|
197
|
-
background-position:
|
|
198
|
-
background-size:
|
|
321
|
+
background-position: 8px center;
|
|
322
|
+
background-size: 18px 18px;
|
|
199
323
|
color: #0b0c0c;
|
|
200
324
|
font-family: Helvetica, Arial, sans-serif;
|
|
201
|
-
font-size: 0.875rem;
|
|
202
325
|
box-sizing: border-box;
|
|
326
|
+
// Prevent browser-native search-cancel button (replaced by autocomplete's own)
|
|
327
|
+
appearance: none;
|
|
203
328
|
|
|
204
329
|
&::placeholder {
|
|
205
330
|
color: #505a5f;
|
|
206
331
|
}
|
|
207
332
|
|
|
333
|
+
// GOV.UK focus style: yellow outline with dark inner border
|
|
208
334
|
&:focus {
|
|
209
335
|
outline: 3px solid #fd0;
|
|
210
336
|
outline-offset: 0;
|
|
337
|
+
box-shadow: inset 0 0 0 2px #0b0c0c;
|
|
338
|
+
border-color:#0b0c0c;
|
|
211
339
|
}
|
|
212
340
|
}
|
|
213
341
|
|
|
@@ -218,7 +346,6 @@
|
|
|
218
346
|
left: 0;
|
|
219
347
|
right: 0;
|
|
220
348
|
background: #fff;
|
|
221
|
-
border: 2px solid #0b0c0c;
|
|
222
349
|
border-radius: 0;
|
|
223
350
|
margin: 0;
|
|
224
351
|
padding: 0;
|
|
@@ -233,10 +360,13 @@
|
|
|
233
360
|
position: relative;
|
|
234
361
|
}
|
|
235
362
|
|
|
363
|
+
.autocomplete__menu--overlay {
|
|
364
|
+
box-shadow: rgba(11, 12, 12, .256863) 0 5px 5px;
|
|
365
|
+
}
|
|
366
|
+
|
|
236
367
|
.autocomplete__option {
|
|
237
368
|
padding: 8px 12px;
|
|
238
369
|
font-family: Helvetica, Arial, sans-serif;
|
|
239
|
-
font-size: 0.875rem;
|
|
240
370
|
color: #0b0c0c;
|
|
241
371
|
cursor: pointer;
|
|
242
372
|
border-bottom: 1px solid #f3f2f1;
|
|
@@ -264,7 +394,6 @@
|
|
|
264
394
|
|
|
265
395
|
.app-search__context {
|
|
266
396
|
display: block;
|
|
267
|
-
font-size: 0.75rem;
|
|
268
397
|
color: #505a5f;
|
|
269
398
|
margin-top: 2px;
|
|
270
399
|
}
|
|
@@ -272,7 +401,6 @@
|
|
|
272
401
|
.autocomplete__option--no-results {
|
|
273
402
|
padding: 8px 12px;
|
|
274
403
|
font-family: Helvetica, Arial, sans-serif;
|
|
275
|
-
font-size: 0.875rem;
|
|
276
404
|
color: #505a5f;
|
|
277
405
|
}
|
|
278
406
|
|
|
@@ -291,6 +419,22 @@
|
|
|
291
419
|
}
|
|
292
420
|
|
|
293
421
|
@media (max-width: 767px) {
|
|
422
|
+
.govuk-header__container {
|
|
423
|
+
flex-direction: column;
|
|
424
|
+
align-items: flex-start;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.app-header-search {
|
|
428
|
+
margin-left: 0;
|
|
429
|
+
margin-bottom: 10px;
|
|
430
|
+
width: 100%;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.app-header-search div[role="search"],
|
|
434
|
+
.app-header-search .autocomplete__wrapper {
|
|
435
|
+
width: 100%;
|
|
436
|
+
}
|
|
437
|
+
|
|
294
438
|
.app-layout-sidebar {
|
|
295
439
|
flex-direction: column;
|
|
296
440
|
gap: 0;
|
|
@@ -310,9 +454,4 @@
|
|
|
310
454
|
width: 100%;
|
|
311
455
|
min-width: 0;
|
|
312
456
|
}
|
|
313
|
-
|
|
314
|
-
.app-header-search .autocomplete__wrapper {
|
|
315
|
-
width: 180px;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
457
|
+
}
|
package/src/css/prose-scope.scss
CHANGED
|
@@ -4,54 +4,54 @@
|
|
|
4
4
|
|
|
5
5
|
.app-prose-scope {
|
|
6
6
|
// Headings
|
|
7
|
-
h1 {
|
|
7
|
+
h1:not(.app-no-prose *) {
|
|
8
8
|
@extend %govuk-heading-xl;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
h2 {
|
|
11
|
+
h2:not(.app-no-prose *) {
|
|
12
12
|
@extend %govuk-heading-l;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
h3 {
|
|
15
|
+
h3:not(.app-no-prose *) {
|
|
16
16
|
@extend %govuk-heading-m;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
h4 {
|
|
19
|
+
h4:not(.app-no-prose *) {
|
|
20
20
|
@extend %govuk-heading-s;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// Body text
|
|
24
|
-
p {
|
|
24
|
+
p:not(.app-no-prose *) {
|
|
25
25
|
@extend %govuk-body-m;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// Bold text
|
|
29
|
-
strong,
|
|
30
|
-
b {
|
|
29
|
+
strong:not(.app-no-prose *),
|
|
30
|
+
b:not(.app-no-prose *) {
|
|
31
31
|
font-weight: 700;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Lists
|
|
35
|
-
ul,
|
|
36
|
-
ol {
|
|
35
|
+
ul:not(.app-no-prose *),
|
|
36
|
+
ol:not(.app-no-prose *) {
|
|
37
37
|
@extend %govuk-list;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
ol {
|
|
40
|
+
ol:not(.app-no-prose *) {
|
|
41
41
|
@extend %govuk-list--number;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
ul {
|
|
44
|
+
ul:not(.app-no-prose *) {
|
|
45
45
|
@extend %govuk-list--bullet;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Links
|
|
49
|
-
a {
|
|
49
|
+
a:not(.app-no-prose *) {
|
|
50
50
|
@extend %govuk-link;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Section breaks
|
|
54
|
-
hr {
|
|
54
|
+
hr:not(.app-no-prose *) {
|
|
55
55
|
@extend %govuk-section-break;
|
|
56
56
|
@extend %govuk-section-break--visible;
|
|
57
57
|
@extend %govuk-section-break--xl;
|
package/src/css/theme.scss
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
/* Import component styles */
|
|
8
8
|
@import './components.scss';
|
|
9
9
|
|
|
10
|
+
/* GOV.UK grid system */
|
|
11
|
+
@import 'govuk-frontend/dist/govuk/objects/grid';
|
|
12
|
+
|
|
10
13
|
/*
|
|
11
14
|
* Set the default font on the body so that ALL elements
|
|
12
15
|
* (header, footer, service-nav, etc.) inherit the same stack.
|
|
@@ -23,13 +26,20 @@ body {
|
|
|
23
26
|
* and our layout, breaking GOV.UK Frontend's flex sticky-footer chain.
|
|
24
27
|
* Rather than patching the chain, own the full-height context here.
|
|
25
28
|
*/
|
|
26
|
-
.govuk-
|
|
29
|
+
.govuk-template__body-inner {
|
|
30
|
+
background-color: #ffffff;
|
|
27
31
|
display: flex;
|
|
28
32
|
flex-direction: column;
|
|
29
33
|
min-height: 100vh;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
.govuk-
|
|
36
|
+
.govuk-template__body-inner > .govuk-width-container {
|
|
33
37
|
flex: 1 0 auto;
|
|
34
|
-
width: 100
|
|
38
|
+
width: calc(100% - 30px);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (min-width: 40.0625em) {
|
|
42
|
+
.govuk-template__body-inner > .govuk-width-container {
|
|
43
|
+
width: calc(100% - 60px);
|
|
44
|
+
}
|
|
35
45
|
}
|
|
@@ -98,6 +98,41 @@ function getActiveSection(pathname, navigation) {
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Initialise the govuk-frontend ServiceNavigation JS for the mobile menu toggle.
|
|
103
|
+
* The @not-govuk React component renders the toggle button with `hidden` by default
|
|
104
|
+
* (progressive enhancement); this hook removes `hidden` on mobile and wires up the
|
|
105
|
+
* click handler exactly as govuk-frontend expects.
|
|
106
|
+
*/
|
|
107
|
+
function useServiceNavigationToggle() {
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
let cancelled = false;
|
|
110
|
+
import('govuk-frontend').then(({ ServiceNavigation }) => {
|
|
111
|
+
if (cancelled) return;
|
|
112
|
+
// govuk-frontend checks for this class before initialising any component.
|
|
113
|
+
// Normally added by an inline <script> snippet in the HTML template; we
|
|
114
|
+
// add it here since Docusaurus doesn't use that template pattern.
|
|
115
|
+
document.body.classList.add('govuk-frontend-supported');
|
|
116
|
+
document.querySelectorAll('[data-module="govuk-service_navigation"]').forEach((el) => {
|
|
117
|
+
try {
|
|
118
|
+
const instance = new ServiceNavigation(el);
|
|
119
|
+
el._govukServiceNav = instance;
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// May fail if the CSS custom property --govuk-breakpoint-tablet isn't set.
|
|
122
|
+
// Fall back to injecting it directly so the toggle still works.
|
|
123
|
+
if (e.message?.includes('CSS custom property')) {
|
|
124
|
+
document.documentElement.style.setProperty('--govuk-breakpoint-tablet', '40.0625em');
|
|
125
|
+
el._govukServiceNav = new ServiceNavigation(el);
|
|
126
|
+
} else {
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
return () => { cancelled = true; };
|
|
133
|
+
}, []);
|
|
134
|
+
}
|
|
135
|
+
|
|
101
136
|
export default function Layout(props) {
|
|
102
137
|
const location = useLocation();
|
|
103
138
|
const {siteConfig} = useDocusaurusContext();
|
|
@@ -109,6 +144,8 @@ export default function Layout(props) {
|
|
|
109
144
|
noFooter,
|
|
110
145
|
} = props;
|
|
111
146
|
|
|
147
|
+
useServiceNavigationToggle();
|
|
148
|
+
|
|
112
149
|
const navigation = govukConfig.navigation || [];
|
|
113
150
|
const header = govukConfig.header || {};
|
|
114
151
|
const phaseBanner = govukConfig.phaseBanner;
|
|
@@ -170,6 +207,10 @@ export default function Layout(props) {
|
|
|
170
207
|
for (const el of headings) {
|
|
171
208
|
const id = el.id;
|
|
172
209
|
if (!id) continue;
|
|
210
|
+
// Skip headings marked with <!-- no-sidebar --> in the source.
|
|
211
|
+
// The remarkNoSidebar plugin (index.js) converts the comment to a
|
|
212
|
+
// data-no-sidebar attribute at compile time so it survives MDX rendering.
|
|
213
|
+
if (el.dataset?.noSidebar) continue;
|
|
173
214
|
const text = el.textContent?.trim() ?? '';
|
|
174
215
|
const href = withBase(`${pathname}#${id}`);
|
|
175
216
|
if (el.tagName === 'H2') {
|
|
@@ -196,14 +237,14 @@ export default function Layout(props) {
|
|
|
196
237
|
return (
|
|
197
238
|
<LayoutProvider>
|
|
198
239
|
<Head>
|
|
199
|
-
<html lang="en-GB" className="govuk-template" />
|
|
200
|
-
<body className=
|
|
240
|
+
<html lang="en-GB" className="govuk-template govuk-template--rebranded" />
|
|
241
|
+
<body className={isHomepage ? 'govuk-template__body app-homepage' : 'govuk-template__body'} />
|
|
201
242
|
<meta name="theme-color" content="#0b0c0c" />
|
|
202
243
|
{title && <title>{title}</title>}
|
|
203
244
|
{description && <meta name="description" content={description} />}
|
|
204
245
|
</Head>
|
|
205
246
|
|
|
206
|
-
<div className="govuk-
|
|
247
|
+
<div className="govuk-template__body-inner">
|
|
207
248
|
<AnnouncementBar />
|
|
208
249
|
|
|
209
250
|
{/* Hidden navbar element for Docusaurus hooks */}
|
|
@@ -266,8 +307,8 @@ export default function Layout(props) {
|
|
|
266
307
|
</div>
|
|
267
308
|
)}
|
|
268
309
|
|
|
269
|
-
|
|
270
|
-
|
|
310
|
+
{phaseBanner && (
|
|
311
|
+
<div className="govuk-width-container">
|
|
271
312
|
<PhaseBanner phase={phaseBanner.phase}>
|
|
272
313
|
{phaseBanner.text}{' '}
|
|
273
314
|
{phaseBanner.feedbackHref && (
|
|
@@ -276,10 +317,12 @@ export default function Layout(props) {
|
|
|
276
317
|
</a>
|
|
277
318
|
)}
|
|
278
319
|
</PhaseBanner>
|
|
279
|
-
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
280
322
|
|
|
281
|
-
|
|
282
|
-
|
|
323
|
+
<main id="main-content" className={isHomepage ? undefined : 'govuk-main-wrapper'}>
|
|
324
|
+
{sidebarItems ? (
|
|
325
|
+
<div className="govuk-width-container">
|
|
283
326
|
<div className="app-layout-sidebar">
|
|
284
327
|
<aside className="app-layout-sidebar__nav">
|
|
285
328
|
<SidebarNav items={sidebarItems} />
|
|
@@ -288,11 +331,15 @@ export default function Layout(props) {
|
|
|
288
331
|
{children}
|
|
289
332
|
</div>
|
|
290
333
|
</div>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
334
|
+
</div>
|
|
335
|
+
) : isHomepage ? (
|
|
336
|
+
children
|
|
337
|
+
) : (
|
|
338
|
+
<div className="govuk-width-container">
|
|
339
|
+
{children}
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</main>
|
|
296
343
|
|
|
297
344
|
{!noFooter && (
|
|
298
345
|
<Footer rebrand meta={footer.meta} />
|
|
@@ -170,29 +170,34 @@ function SearchBarInner() {
|
|
|
170
170
|
const Autocomplete = require('accessible-autocomplete/react').default;
|
|
171
171
|
|
|
172
172
|
return (
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
173
|
+
<div role="search">
|
|
174
|
+
<label htmlFor="govuk-search" className="govuk-visually-hidden">
|
|
175
|
+
Search
|
|
176
|
+
</label>
|
|
177
|
+
<Autocomplete
|
|
178
|
+
id="govuk-search"
|
|
179
|
+
source={source}
|
|
180
|
+
templates={{
|
|
181
|
+
inputValue: (result) => (result ? result._label : ''),
|
|
182
|
+
suggestion: (result) => {
|
|
183
|
+
if (!result) return '';
|
|
184
|
+
const title = escapeHtml(result._label);
|
|
185
|
+
const context = result._context
|
|
186
|
+
? `<span class="app-search__context">${escapeHtml(result._context)}</span>`
|
|
187
|
+
: '';
|
|
188
|
+
return `<span class="app-search__title">${title}</span>${context}`;
|
|
189
|
+
},
|
|
190
|
+
}}
|
|
191
|
+
displayMenu="overlay"
|
|
192
|
+
minLength={2}
|
|
193
|
+
placeholder="Search"
|
|
194
|
+
onConfirm={onConfirm}
|
|
195
|
+
tNoResults={() => 'No results found'}
|
|
196
|
+
tAssistiveHint={() =>
|
|
197
|
+
'When autocomplete results are available use up and down arrows to review and enter to select.'
|
|
198
|
+
}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
196
201
|
);
|
|
197
202
|
}
|
|
198
203
|
|
|
@@ -4,30 +4,37 @@ import { useLocation } from '@docusaurus/router';
|
|
|
4
4
|
/**
|
|
5
5
|
* Hash- and pathname-aware sidebar navigation.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Desktop: section headings (items with children) are rendered as plain text,
|
|
8
|
+
* all groups are permanently expanded, only sub-items are links.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Anchor hrefs (e.g. /api#constructor): active when pathname AND hash both match
|
|
12
|
-
* - Page hrefs (e.g. /building-a-plugin): active when pathname matches
|
|
13
|
-
* - Groups expand when the group itself or any child is active
|
|
10
|
+
* Mobile: section headings become toggle buttons; groups collapse and expand.
|
|
11
|
+
* Active groups start expanded. Items without children are always links.
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* str = JS loaded (empty string means no hash present)
|
|
13
|
+
* SSR / no-JS: renders in desktop mode (all groups expanded) so content is
|
|
14
|
+
* accessible without JavaScript.
|
|
18
15
|
*/
|
|
19
16
|
export default function SidebarNav({ items }) {
|
|
20
17
|
const [hash, setHash] = useState(null);
|
|
18
|
+
// null = not yet hydrated; false = desktop; true = mobile
|
|
19
|
+
const [isMobile, setIsMobile] = useState(null);
|
|
20
|
+
const [openGroups, setOpenGroups] = useState(new Set());
|
|
21
21
|
const location = useLocation();
|
|
22
22
|
|
|
23
23
|
useEffect(() => {
|
|
24
|
-
const update = () => setHash(
|
|
24
|
+
const update = () => setHash(globalThis.location.hash.slice(1));
|
|
25
25
|
update();
|
|
26
|
-
|
|
27
|
-
return () =>
|
|
26
|
+
globalThis.addEventListener('hashchange', update);
|
|
27
|
+
return () => globalThis.removeEventListener('hashchange', update);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const mql = globalThis.matchMedia('(max-width: 767px)');
|
|
32
|
+
const update = () => setIsMobile(mql.matches);
|
|
33
|
+
update();
|
|
34
|
+
mql.addEventListener('change', update);
|
|
35
|
+
return () => mql.removeEventListener('change', update);
|
|
28
36
|
}, []);
|
|
29
37
|
|
|
30
|
-
// Split an href into its path and anchor components.
|
|
31
38
|
function parseHref(href) {
|
|
32
39
|
if (!href) return { path: '', anchor: '' };
|
|
33
40
|
const idx = href.indexOf('#');
|
|
@@ -35,9 +42,6 @@ export default function SidebarNav({ items }) {
|
|
|
35
42
|
return { path: href.slice(0, idx) || '/', anchor: href.slice(idx + 1) };
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
// Return true if the given href matches the current browser location.
|
|
39
|
-
// Anchor hrefs: both pathname and hash must match.
|
|
40
|
-
// Page hrefs: pathname match is sufficient (exact or child path).
|
|
41
45
|
function isActive(href) {
|
|
42
46
|
const { path, anchor } = parseHref(href);
|
|
43
47
|
if (anchor) {
|
|
@@ -49,44 +53,75 @@ export default function SidebarNav({ items }) {
|
|
|
49
53
|
);
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
// When switching to mobile, open any groups that contain the active page.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!isMobile || hash === null) return;
|
|
59
|
+
const active = new Set();
|
|
60
|
+
(items || []).forEach((item, i) => {
|
|
61
|
+
if (!Array.isArray(item.items) || !item.items.length) return;
|
|
62
|
+
if (isActive(item.href) || item.items.some(sub => isActive(sub.href))) {
|
|
63
|
+
active.add(i);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
setOpenGroups(active);
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
}, [isMobile, location.pathname, hash]);
|
|
69
|
+
|
|
70
|
+
function toggleGroup(i) {
|
|
71
|
+
setOpenGroups(prev => {
|
|
72
|
+
const next = new Set(prev);
|
|
73
|
+
if (next.has(i)) next.delete(i);
|
|
74
|
+
else next.add(i);
|
|
75
|
+
return next;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
const cls = 'not-govuk-navigation-menu';
|
|
53
80
|
const lCls = `${cls}__list`;
|
|
81
|
+
const itemCls = `${lCls}__item`;
|
|
82
|
+
const activeCls = `${itemCls}--active`;
|
|
83
|
+
|
|
84
|
+
function renderHeading(item, i, sublistId) {
|
|
85
|
+
if (isMobile) {
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className={`${lCls}__heading-toggle`}
|
|
90
|
+
aria-expanded={openGroups.has(i)}
|
|
91
|
+
aria-controls={sublistId}
|
|
92
|
+
onClick={() => toggleGroup(i)}
|
|
93
|
+
>
|
|
94
|
+
{item.text}
|
|
95
|
+
</button>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return <span className={`${lCls}__heading`}>{item.text}</span>;
|
|
99
|
+
}
|
|
54
100
|
|
|
55
101
|
return (
|
|
56
102
|
<nav className={cls}>
|
|
57
103
|
<ul className={lCls}>
|
|
58
|
-
{items.map((item, i) => {
|
|
104
|
+
{(items || []).map((item, i) => {
|
|
59
105
|
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
|
60
|
-
|
|
61
|
-
// Pre-hydration (hash === null): expand everything so content is
|
|
62
|
-
// accessible without JS. After hydration: expand only if this group
|
|
63
|
-
// or one of its children is the active location.
|
|
64
|
-
const expanded =
|
|
65
|
-
hasChildren &&
|
|
66
|
-
(hash === null || isActive(item.href) || item.items.some(sub => isActive(sub.href)));
|
|
67
|
-
|
|
68
106
|
const active = hash !== null && isActive(item.href);
|
|
107
|
+
const showChildren =
|
|
108
|
+
hasChildren && (isMobile === null || !isMobile || openGroups.has(i));
|
|
109
|
+
const liCls = active && !hasChildren ? `${itemCls} ${activeCls}` : itemCls;
|
|
110
|
+
const sublistId = hasChildren ? `sidebar-group-${i}` : undefined;
|
|
69
111
|
|
|
70
112
|
return (
|
|
71
|
-
<li
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
{expanded && (
|
|
79
|
-
<ul className={`${lCls}__subitems`}>
|
|
80
|
-
{item.items.map((sub, j) => {
|
|
113
|
+
<li key={item.href || item.text} className={liCls}>
|
|
114
|
+
{hasChildren ? renderHeading(item, i, sublistId) : (
|
|
115
|
+
<a href={item.href} className={`${lCls}__link`}>{item.text}</a>
|
|
116
|
+
)}
|
|
117
|
+
{showChildren && (
|
|
118
|
+
<ul id={sublistId} className={`${lCls}__subitems`}>
|
|
119
|
+
{item.items.map((sub) => {
|
|
81
120
|
const subActive = hash !== null && isActive(sub.href);
|
|
121
|
+
const subCls = subActive ? `${itemCls} ${activeCls}` : itemCls;
|
|
82
122
|
return (
|
|
83
|
-
<li
|
|
84
|
-
|
|
85
|
-
className={`${lCls}__item${subActive ? ` ${lCls}__item--active` : ''}`}
|
|
86
|
-
>
|
|
87
|
-
<a href={sub.href} className={`${lCls}__link`}>
|
|
88
|
-
{sub.text}
|
|
89
|
-
</a>
|
|
123
|
+
<li key={sub.href || sub.text} className={subCls}>
|
|
124
|
+
<a href={sub.href} className={`${lCls}__link`}>{sub.text}</a>
|
|
90
125
|
</li>
|
|
91
126
|
);
|
|
92
127
|
})}
|
|
@@ -99,4 +134,3 @@ export default function SidebarNav({ items }) {
|
|
|
99
134
|
</nav>
|
|
100
135
|
);
|
|
101
136
|
}
|
|
102
|
-
|