@defra/docusaurus-theme-govuk 0.0.17-alpha → 0.0.19-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 CHANGED
@@ -8,6 +8,61 @@ 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: transforms GitHub-style alert blockquotes into Docusaurus
12
+ // containerDirective nodes so they render via the Admonition component.
13
+ //
14
+ // > [!NOTE] → :::note
15
+ // > [!TIP] → :::tip
16
+ // > [!IMPORTANT] → :::important
17
+ // > [!WARNING] → :::warning
18
+ // > [!CAUTION] → :::caution
19
+ //
20
+ // Runs in beforeDefaultRemarkPlugins so containerDirective nodes are in place
21
+ // before Docusaurus's admonition remark plugin processes them.
22
+ function githubAlertToDirective(blockquote) {
23
+ const first = blockquote.children?.[0];
24
+ if (first?.type !== 'paragraph') return null;
25
+
26
+ const firstText = first.children?.[0];
27
+ if (firstText?.type !== 'text') return null;
28
+
29
+ const match = firstText.value.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*\n?/i);
30
+ if (!match) return null;
31
+
32
+ const type = match[1].toLowerCase();
33
+ const rest = firstText.value.slice(match[0].length);
34
+
35
+ let children;
36
+ if (rest.trim()) {
37
+ children = [
38
+ { ...first, children: [{ ...firstText, value: rest }, ...first.children.slice(1)] },
39
+ ...blockquote.children.slice(1),
40
+ ];
41
+ } else if (first.children.length > 1) {
42
+ children = [{ ...first, children: first.children.slice(1) }, ...blockquote.children.slice(1)];
43
+ } else {
44
+ children = blockquote.children.slice(1);
45
+ }
46
+
47
+ return { type: 'containerDirective', name: type, attributes: {}, children };
48
+ }
49
+
50
+ function walkGithubAlerts(node) {
51
+ if (!Array.isArray(node.children)) return;
52
+ for (let i = 0; i < node.children.length; i++) {
53
+ const child = node.children[i];
54
+ if (child.type === 'blockquote') {
55
+ const directive = githubAlertToDirective(child);
56
+ if (directive) { node.children[i] = directive; continue; }
57
+ }
58
+ walkGithubAlerts(child);
59
+ }
60
+ }
61
+
62
+ function remarkGithubAlerts() {
63
+ return walkGithubAlerts;
64
+ }
65
+
11
66
  // Remark plugin: converts `<!-- no-sidebar -->` inline HTML comments inside
12
67
  // headings into a `data-no-sidebar` HTML attribute (via mdast hProperties) so
13
68
  // the runtime DOM scanner can detect and skip them. MDX/remark-rehype drops
@@ -102,7 +157,19 @@ function injectIntoUses(uses) {
102
157
  if (typeof use?.loader === 'string' && use.loader.includes('mdx-loader')) {
103
158
  use.options = use.options || {};
104
159
  use.options.beforeDefaultRemarkPlugins = use.options.beforeDefaultRemarkPlugins || [];
105
- use.options.beforeDefaultRemarkPlugins.push(remarkNoSidebar);
160
+ use.options.beforeDefaultRemarkPlugins.push(remarkNoSidebar, remarkGithubAlerts);
161
+
162
+ // Add GitHub alert types not in Docusaurus's default keyword list.
163
+ if (use.options.admonitions !== false) {
164
+ const existing = use.options.admonitions || {};
165
+ const keywords = existing.keywords || ['note', 'tip', 'info', 'warning', 'danger'];
166
+ const extra = ['caution', 'important'].filter(k => !keywords.includes(k));
167
+ if (extra.length) {
168
+ use.options.admonitions = { ...existing, keywords: [...keywords, ...extra] };
169
+ }
170
+ }
171
+
172
+
106
173
  }
107
174
  }
108
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/docusaurus-theme-govuk",
3
- "version": "0.0.17-alpha",
3
+ "version": "0.0.19-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",
@@ -26,6 +26,7 @@
26
26
  overflow: auto !important;
27
27
  margin-left: -16px;
28
28
  padding-left: 16px;
29
+ margin-top: govuk-spacing(1) * -1;
29
30
  }
30
31
  }
31
32
 
@@ -34,6 +35,54 @@
34
35
  min-width: 0;
35
36
  }
36
37
 
38
+ // Admonition colour modifiers.
39
+ // The thick left bar is drawn via ::before so it overlaps the thin border corners
40
+ // cleanly — CSS border mitering would otherwise show a pale sliver at the joins.
41
+ // border-left-width is kept at 5px (matching GOV.UK's original) but made transparent
42
+ // so the element's layout is unchanged; ::before paints over that space.
43
+ .govuk-inset-text.govuk-inset-text--note,
44
+ .govuk-inset-text.govuk-inset-text--tip,
45
+ .govuk-inset-text.govuk-inset-text--info,
46
+ .govuk-inset-text.govuk-inset-text--important {
47
+ border: 1px solid #bad3ea;
48
+ border-left-width: 10px;
49
+ border-left-color: transparent;
50
+ background: none;
51
+ position: relative;
52
+
53
+ &::before {
54
+ content: '';
55
+ position: absolute;
56
+ top: -1px;
57
+ bottom: -1px;
58
+ left: -10px;
59
+ width: 10px;
60
+ background: #1d70b8;
61
+ }
62
+
63
+ > p:first-child strong { color: #1d70b8; }
64
+ }
65
+
66
+ .govuk-inset-text.govuk-inset-text--caution {
67
+ border: 1px solid #fbd6c3;
68
+ border-left-width: 10px;
69
+ border-left-color: transparent;
70
+ background: none;
71
+ position: relative;
72
+
73
+ &::before {
74
+ content: '';
75
+ position: absolute;
76
+ top: -1px;
77
+ bottom: -1px;
78
+ left: -10px;
79
+ width: 10px;
80
+ background: #f47738;
81
+ }
82
+
83
+ > p:first-child strong { color: #f47738; }
84
+ }
85
+
37
86
  // Code block
38
87
  .app-code-block {
39
88
  position: relative;
@@ -56,10 +105,18 @@
56
105
  font-size: 0.875rem;
57
106
  line-height: 1.6;
58
107
  font-family: Menlo, Consolas, 'Courier New', monospace;
59
- position: relative;
60
- border: 1px solid #cecece;
61
108
  background-color: #f4f8fb;
62
- margin: 0;
109
+
110
+ code {
111
+ display: block;
112
+
113
+ > div::after {
114
+ content: '';
115
+ display: inline-block;
116
+ width: calc(1rem + 70px);
117
+ line-height: 0;
118
+ }
119
+ }
63
120
  }
64
121
 
65
122
  .app-code-block__copy {
@@ -97,19 +154,25 @@
97
154
  border-bottom: 1px solid #cecece;
98
155
 
99
156
  position: relative;
100
- padding-top: 5px;
101
- padding-bottom: 5px;
102
- margin-bottom: 5px;
103
- font-size: 1rem;
157
+ padding-top: govuk-spacing(2);
158
+ padding-bottom: govuk-spacing(2);
159
+ margin-bottom: 0;
160
+
161
+ font-size: 1.1875rem;
104
162
  padding-left: 0;
105
163
  margin-left: 0;
106
-
164
+
107
165
  @media (min-width: 48.125em) {
166
+ font-size: 1rem;
167
+ padding-top: govuk-spacing(1);
168
+ padding-bottom: govuk-spacing(1);
169
+ margin-bottom: govuk-spacing(1);
108
170
  border-bottom: 0;
109
171
 
110
172
  &:after {
111
173
  border-bottom: 0;
112
174
  }
175
+
113
176
  }
114
177
 
115
178
  .not-govuk-navigation-menu__list__link {
@@ -136,7 +199,7 @@
136
199
 
137
200
  .not-govuk-navigation-menu__list__item {
138
201
  padding: 5px 0;
139
- margin-bottom: 5px;
202
+ margin-bottom: 0;
140
203
  border-left-width: 0px;
141
204
  border-left-style: solid;
142
205
  }
@@ -243,6 +306,27 @@
243
306
  }
244
307
  }
245
308
 
309
+ // Lock service navigation to 1.1875rem on all devices, including mobile where
310
+ // govuk-font(19) would otherwise drop to 1rem. Parent selector raises
311
+ // specificity to (0,2,0) to beat the govuk-frontend mobile breakpoint rule.
312
+ .govuk-service-navigation .govuk-service-navigation__list,
313
+ .govuk-service-navigation .govuk-service-navigation__toggle,
314
+ .govuk-service-navigation .govuk-service-navigation__service-name {
315
+ font-size: 1.1875rem;
316
+ }
317
+
318
+ // Sidebar nav links and headings: 1.1875rem on mobile, 1rem on larger devices.
319
+ // Two-class specificity (0,2,0) beats the not-govuk `aside a` rule at (0,1,1).
320
+ .app-layout-sidebar__nav .not-govuk-navigation-menu__list__link,
321
+ .app-layout-sidebar__nav .not-govuk-navigation-menu__list__heading,
322
+ .app-layout-sidebar__nav .not-govuk-navigation-menu__list__heading-toggle {
323
+ font-size: 1.1875rem;
324
+
325
+ @media (min-width: 48.125em) {
326
+ font-size: 1rem;
327
+ }
328
+ }
329
+
246
330
  // Pagination
247
331
  .app-pagination__container {
248
332
  display: flex;
@@ -22,6 +22,14 @@
22
22
 
23
23
  // Extra top padding on headings that follow a code block,
24
24
  // matching the spacing govuk-frontend adds after paragraphs and lists
25
+ .app-code-block + h2:not(.app-no-prose *),
26
+ .app-code-block + h3:not(.app-no-prose *),
27
+ .app-code-block + h4:not(.app-no-prose *),
28
+ .app-code-block + h5:not(.app-no-prose *),
29
+ .app-code-block + h6:not(.app-no-prose *) {
30
+ padding-top: 5px;
31
+ }
32
+
25
33
  @media (min-width: 40.0625em) {
26
34
  .app-code-block + h2:not(.app-no-prose *),
27
35
  .app-code-block + h3:not(.app-no-prose *),
@@ -102,6 +110,21 @@
102
110
  }
103
111
  }
104
112
 
113
+ // Override GDS Transport on all prose elements. This must come after the
114
+ // @extend rules above (which bake in "GDS Transport" from govuk-frontend) so
115
+ // that cascade order wins at equal specificity. app-no-prose content is
116
+ // excluded so host apps can render GDS Transport there if licensed.
117
+ h1:not(.app-no-prose *),
118
+ h2:not(.app-no-prose *),
119
+ h3:not(.app-no-prose *),
120
+ h4:not(.app-no-prose *),
121
+ p:not(.app-no-prose *),
122
+ li:not(.app-no-prose *),
123
+ a:not(.app-no-prose *) {
124
+ font-family: Helvetica, Arial, sans-serif;
125
+ }
126
+
127
+
105
128
  // Code block copy button
106
129
  .app-code-block__copy {
107
130
  font-size: 0.9rem;
@@ -17,10 +17,20 @@
17
17
  * without this, outer chrome inherits the browser default serif.
18
18
  * GDS Transport is not bundled — Helvetica/Arial is used instead.
19
19
  */
20
- body {
20
+ body,
21
+ [class^="govuk-"],
22
+ [class*=" govuk-"] {
21
23
  font-family: Helvetica, Arial, sans-serif;
22
24
  }
23
25
 
26
+ // Restore GDS Transport inside .app-no-prose containers (e.g. map examples in
27
+ // host apps that load the font separately and need the real GDS type stack).
28
+ .app-no-prose,
29
+ .app-no-prose [class^="govuk-"],
30
+ .app-no-prose [class*=" govuk-"] {
31
+ font-family: "GDS Transport", arial, sans-serif;
32
+ }
33
+
24
34
  /* Wider page width — overrides the 960px default from govuk-frontend.
25
35
  * !important is needed because @not-govuk/width-container injects its CSS
26
36
  * via the JS bundle, which loads after static stylesheets and wins the cascade.
@@ -1,32 +1,27 @@
1
1
  import React from 'react';
2
2
  import {InsetText, WarningText} from '@not-govuk/simple-components';
3
3
 
4
- const admonitionTitles = {
5
- note: 'Note',
6
- tip: 'Tip',
7
- info: 'Info',
8
- warning: 'Warning',
9
- danger: 'Danger',
10
- caution: 'Caution',
4
+ const ADMONITION_CONFIGS = {
5
+ note: { label: 'Note', modifier: 'govuk-inset-text--note' },
6
+ tip: { label: 'Tip', modifier: 'govuk-inset-text--tip' },
7
+ info: { label: 'Info', modifier: 'govuk-inset-text--info' },
8
+ important: { label: 'Important', modifier: 'govuk-inset-text--important' },
9
+ caution: { label: 'Caution', modifier: 'govuk-inset-text--caution' },
10
+ warning: { label: 'Warning', modifier: null },
11
+ danger: { label: 'Danger', modifier: null },
11
12
  };
12
13
 
13
14
  export default function Admonition({type = 'note', title, children}) {
14
- const displayTitle = title || admonitionTitles[type] || 'Note';
15
+ const config = ADMONITION_CONFIGS[type] ?? ADMONITION_CONFIGS.note;
16
+ const displayTitle = (title || config.label).toUpperCase();
15
17
 
16
- // Warning and danger types use GOV.UK WarningText
17
- if (type === 'warning' || type === 'danger' || type === 'caution') {
18
- return (
19
- <WarningText>
20
- <strong>{displayTitle}: </strong>
21
- {children}
22
- </WarningText>
23
- );
18
+ if (!config.modifier) {
19
+ return <WarningText>{children}</WarningText>;
24
20
  }
25
21
 
26
- // All other types use GOV.UK InsetText
27
22
  return (
28
- <InsetText>
29
- {title && <strong>{displayTitle}: </strong>}
23
+ <InsetText className={config.modifier}>
24
+ <p><strong>{displayTitle}</strong></p>
30
25
  {children}
31
26
  </InsetText>
32
27
  );
@@ -2,7 +2,53 @@ import React from 'react';
2
2
  import CodeBlock from '@theme/CodeBlock';
3
3
  import Heading from '@theme/Heading';
4
4
  import Admonition from '@theme/Admonition';
5
- import {InsetText} from '@not-govuk/simple-components';
5
+ import {InsetText, WarningText} from '@not-govuk/simple-components';
6
+
7
+ const ALERT_CONFIGS = {
8
+ NOTE: { label: 'Note', modifier: 'govuk-inset-text--note' },
9
+ TIP: { label: 'Tip', modifier: 'govuk-inset-text--tip' },
10
+ INFO: { label: 'Info', modifier: 'govuk-inset-text--info' },
11
+ IMPORTANT: { label: 'Important', modifier: 'govuk-inset-text--important' },
12
+ CAUTION: { label: 'Caution', modifier: 'govuk-inset-text--caution' },
13
+ WARNING: { label: 'Warning', modifier: null }, // uses WarningText
14
+ };
15
+
16
+ const ALERT_PATTERN = /^\s*\[!(NOTE|TIP|INFO|IMPORTANT|WARNING|CAUTION)\]\s*/i;
17
+
18
+ // Parses blockquote children for a GitHub-style alert marker.
19
+ // Returns {config, allContent} if found, null otherwise.
20
+ function parseGithubAlert(children) {
21
+ const childArray = React.Children.toArray(children);
22
+ const firstChildIndex = childArray.findIndex((c) => React.isValidElement(c));
23
+ const firstChild = childArray[firstChildIndex];
24
+ if (!firstChild?.props) return null;
25
+
26
+ const pChildren = React.Children.toArray(firstChild.props.children);
27
+ let mergedLeadingText = '';
28
+ let mergeCount = 0;
29
+ for (const child of pChildren) {
30
+ if (typeof child !== 'string') break;
31
+ mergedLeadingText += child;
32
+ mergeCount += 1;
33
+ }
34
+
35
+ const match = ALERT_PATTERN.exec(mergedLeadingText);
36
+ if (!match) return null;
37
+
38
+ const config = ALERT_CONFIGS[match[1].toUpperCase()];
39
+ const remainingText = mergedLeadingText.slice(match[0].length).trimStart();
40
+ const newPChildren = [
41
+ ...(remainingText ? [remainingText] : []),
42
+ ...pChildren.slice(mergeCount),
43
+ ].filter((c) => !(typeof c === 'string' && c.trim() === ''));
44
+
45
+ const contentParagraph = newPChildren.length > 0
46
+ ? React.cloneElement(firstChild, {key: 'content'}, ...newPChildren)
47
+ : null;
48
+ const allContent = [contentParagraph, ...childArray.slice(firstChildIndex + 1)].filter(Boolean);
49
+
50
+ return {config, allContent};
51
+ }
6
52
 
7
53
  function GovukLink(props) {
8
54
  return <a className="govuk-link" {...props} />;
@@ -84,55 +130,21 @@ const MDXComponents = {
84
130
  td: (props) => <td className="govuk-table__cell" {...props} />,
85
131
  // Blockquotes rendered as GOV.UK InsetText, with support for GitHub-style alerts
86
132
  // e.g. > [!NOTE], > [!WARNING], > [!TIP], > [!IMPORTANT], > [!CAUTION]
87
- blockquote: ({children, ...rest}) => {
88
- // Use \s* to tolerate any whitespace (newlines, \r\n, spaces) around the marker
89
- const alertPattern = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/i;
90
- const childArray = React.Children.toArray(children);
91
- // Skip leading whitespace text nodes — MDX can inject them
92
- const firstChildIndex = childArray.findIndex((c) => React.isValidElement(c));
93
- const firstChild = childArray[firstChildIndex];
94
-
95
- // Don't check type === 'p': when a p override is registered in MDXComponents,
96
- // MDX sets the element type to the override function, not the string 'p'.
97
- if (firstChild?.props) {
98
- const pChildren = React.Children.toArray(firstChild.props.children);
99
-
100
- // Remark can split "[!NOTE]" across adjacent text nodes (e.g. "[" + "!NOTE]\n...")
101
- // when the paragraph also contains inline links. Merge leading text nodes before
102
- // testing so the marker is always visible as a single string.
103
- let mergedLeadingText = '';
104
- let mergeCount = 0;
105
- for (const child of pChildren) {
106
- if (typeof child !== 'string') break;
107
- mergedLeadingText += child;
108
- mergeCount += 1;
109
- }
133
+ blockquote: ({children}) => {
134
+ const alert = parseGithubAlert(children);
135
+ if (!alert) return <InsetText>{children}</InsetText>;
110
136
 
111
- const match = alertPattern.exec(mergedLeadingText);
112
- if (match) {
113
- const alertType = match[1].toUpperCase();
114
- const remainingText = mergedLeadingText.slice(match[0].length).trimStart();
115
- const newPChildren = [
116
- ...(remainingText ? [remainingText] : []),
117
- ...pChildren.slice(mergeCount),
118
- ].filter((c) => !(typeof c === 'string' && c.trim() === ''));
119
-
120
- const contentParagraph = newPChildren.length > 0
121
- ? React.cloneElement(firstChild, {key: 'content'}, ...newPChildren)
122
- : null;
123
- // Use firstChildIndex + 1 so we don't re-include the original <p>
124
- const allContent = [contentParagraph, ...childArray.slice(firstChildIndex + 1)].filter(Boolean);
125
-
126
- return (
127
- <InsetText {...rest}>
128
- <p key="title"><strong>{alertType}</strong></p>
129
- {allContent}
130
- </InsetText>
131
- );
132
- }
137
+ const {config, allContent} = alert;
138
+ if (!config.modifier) {
139
+ return <WarningText>{allContent}</WarningText>;
133
140
  }
134
141
 
135
- return <InsetText {...rest}>{children}</InsetText>;
142
+ return (
143
+ <InsetText className={config.modifier}>
144
+ <p key="title"><strong>{config.label.toUpperCase()}</strong></p>
145
+ {allContent}
146
+ </InsetText>
147
+ );
136
148
  },
137
149
  h1: (props) => <Heading as="h1" {...props} />,
138
150
  h2: (props) => <Heading as="h2" {...props} />,