@defra/docusaurus-theme-govuk 0.0.16-alpha → 0.0.18-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.16-alpha",
3
+ "version": "0.0.18-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",
@@ -34,6 +34,54 @@
34
34
  min-width: 0;
35
35
  }
36
36
 
37
+ // Admonition colour modifiers.
38
+ // The thick left bar is drawn via ::before so it overlaps the thin border corners
39
+ // cleanly — CSS border mitering would otherwise show a pale sliver at the joins.
40
+ // border-left-width is kept at 5px (matching GOV.UK's original) but made transparent
41
+ // so the element's layout is unchanged; ::before paints over that space.
42
+ .govuk-inset-text.govuk-inset-text--note,
43
+ .govuk-inset-text.govuk-inset-text--tip,
44
+ .govuk-inset-text.govuk-inset-text--info,
45
+ .govuk-inset-text.govuk-inset-text--important {
46
+ border: 1px solid #bad3ea;
47
+ border-left-width: 10px;
48
+ border-left-color: transparent;
49
+ background: none;
50
+ position: relative;
51
+
52
+ &::before {
53
+ content: '';
54
+ position: absolute;
55
+ top: -1px;
56
+ bottom: -1px;
57
+ left: -10px;
58
+ width: 10px;
59
+ background: #1d70b8;
60
+ }
61
+
62
+ > p:first-child strong { color: #1d70b8; }
63
+ }
64
+
65
+ .govuk-inset-text.govuk-inset-text--caution {
66
+ border: 1px solid #fbd6c3;
67
+ border-left-width: 10px;
68
+ border-left-color: transparent;
69
+ background: none;
70
+ position: relative;
71
+
72
+ &::before {
73
+ content: '';
74
+ position: absolute;
75
+ top: -1px;
76
+ bottom: -1px;
77
+ left: -10px;
78
+ width: 10px;
79
+ background: #f47738;
80
+ }
81
+
82
+ > p:first-child strong { color: #f47738; }
83
+ }
84
+
37
85
  // Code block
38
86
  .app-code-block {
39
87
  position: relative;
@@ -56,9 +104,18 @@
56
104
  font-size: 0.875rem;
57
105
  line-height: 1.6;
58
106
  font-family: Menlo, Consolas, 'Courier New', monospace;
59
- position: relative;
60
- border: 1px solid #b1b4b6;
61
- margin: 0;
107
+ background-color: #f4f8fb;
108
+
109
+ code {
110
+ display: block;
111
+
112
+ > div::after {
113
+ content: '';
114
+ display: inline-block;
115
+ width: calc(1rem + 70px);
116
+ line-height: 0;
117
+ }
118
+ }
62
119
  }
63
120
 
64
121
  .app-code-block__copy {
@@ -134,7 +191,7 @@
134
191
  .app-layout-sidebar__nav .not-govuk-navigation-menu__list__subitems {
135
192
 
136
193
  .not-govuk-navigation-menu__list__item {
137
- padding: 5px 0 5px 15px;
194
+ padding: 5px 0;
138
195
  margin-bottom: 5px;
139
196
  border-left-width: 0px;
140
197
  border-left-style: solid;
@@ -145,7 +202,8 @@
145
202
 
146
203
  @media (min-width: 48.125em) {
147
204
  border-left: 4px solid #1d70b8;
148
- padding-left: 10px;
205
+ padding-left: 11px;
206
+ margin-left: -15px;
149
207
  font-weight: bold;
150
208
 
151
209
  .not-govuk-navigation-menu__list__link {
@@ -162,6 +220,36 @@
162
220
  padding-left: 0;
163
221
  }
164
222
 
223
+ // API definition list (Type, Default, etc.)
224
+ .app-definition-list {
225
+ margin: 0 0 govuk-spacing(4);
226
+ padding: 0;
227
+
228
+ &__item {
229
+ display: flex;
230
+ gap: govuk-spacing(2);
231
+ align-items: baseline;
232
+ margin-bottom: govuk-spacing(1);
233
+
234
+ dt {
235
+ font-weight: 700;
236
+ flex-shrink: 0;
237
+ }
238
+
239
+ dd {
240
+ margin: 0;
241
+
242
+ code {
243
+ color: #484949;
244
+ font-size: 0.85em;
245
+ background-color: #f4f8fb;
246
+ padding: 1px 4px;
247
+ border-radius: 2px;
248
+ }
249
+ }
250
+ }
251
+ }
252
+
165
253
  // Sidebar nav section headings and mobile toggle
166
254
  // Desktop: headings are non-link spans, visually distinct from link items
167
255
  .not-govuk-navigation-menu__list__heading {
@@ -20,8 +20,29 @@
20
20
  @extend %govuk-heading-s;
21
21
  }
22
22
 
23
+ // Extra top padding on headings that follow a code block,
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
+
33
+ @media (min-width: 40.0625em) {
34
+ .app-code-block + h2:not(.app-no-prose *),
35
+ .app-code-block + h3:not(.app-no-prose *),
36
+ .app-code-block + h4:not(.app-no-prose *),
37
+ .app-code-block + h5:not(.app-no-prose *),
38
+ .app-code-block + h6:not(.app-no-prose *) {
39
+ padding-top: 20px;
40
+ }
41
+ }
42
+
23
43
  // Body text
24
- p:not(.app-no-prose *) {
44
+ p:not(.app-no-prose *),
45
+ .app-definition-list:not(.app-no-prose *) {
25
46
  @extend %govuk-body-m;
26
47
  }
27
48
 
@@ -55,5 +76,67 @@
55
76
  @extend %govuk-section-break;
56
77
  @extend %govuk-section-break--visible;
57
78
  @extend %govuk-section-break--xl;
79
+ border-color: #cecece;
80
+ }
81
+
82
+ // Inline code styling (paragraphs, list items, headings etc — but not inside pre blocks)
83
+ code:not(pre *):not(h1 *):not(h2 *):not(h3 *):not(h4 *):not(h5 *):not(h6 *):not(.app-no-prose *) {
84
+ color: #484949;
85
+ font-size: 0.85em;
86
+ background-color: #f4f8fb;
87
+ padding: 1px 4px;
88
+ border-radius: 2px;
89
+ }
90
+
91
+ // Lighter table borders
92
+ .govuk-table__header,
93
+ .govuk-table__cell {
94
+ border-bottom-color: #cecece;
95
+ }
96
+
97
+ // Remove the table's last row border when a section break immediately follows,
98
+ // so the two rules don't double up visually.
99
+ // Border is on td/th cells, not tr, so target those directly.
100
+ table:has(+ hr) tr:last-child th,
101
+ table:has(+ hr) tr:last-child td {
102
+ border-bottom: none;
103
+ }
104
+
105
+ // Remove bottom margin from the last paragraph inside inset and warning text
106
+ .govuk-inset-text,
107
+ .govuk-warning-text {
108
+ p:last-child:not(.app-no-prose *) {
109
+ margin-bottom: 0;
110
+ }
111
+ }
112
+
113
+ // Code block copy button
114
+ .app-code-block__copy {
115
+ font-size: 0.9rem;
116
+ min-width: 70px;
117
+ padding: 3px 10px;
118
+ border: 1px solid #1a65a6;
119
+ color: #1a65a6;
120
+ box-shadow: 0 2px 0 0 #1a65a6;
121
+ background-color: #fff;
122
+ text-align: center;
123
+ text-decoration: none;
124
+ cursor: pointer;
125
+ }
126
+
127
+ .app-code-block__copy:active {
128
+ border: 2px solid var(--govuk-focus-colour, #ffdd00);
129
+ padding: 2px 10px;
130
+ outline: 2px solid rgba(0, 0, 0, 0);
131
+ box-shadow: none;
132
+ }
133
+
134
+ .app-code-block__copy:focus:not(:hover) {
135
+ border: 2px solid var(--govuk-focus-colour, #ffdd00);
136
+ padding: 2px 10px;
137
+ outline: 2px solid rgba(0, 0, 0, 0);
138
+ color: var(--govuk-focus-text-colour, #0b0c0c);
139
+ background-color: var(--govuk-focus-colour, #ffdd00);
140
+ box-shadow: 0 2px 0 0 var(--govuk-focus-text-colour, #0b0c0c);
58
141
  }
59
142
  }
@@ -17,10 +17,38 @@
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
+ /* Wider page width — overrides the 960px default from govuk-frontend.
27
+ * !important is needed because @not-govuk/width-container injects its CSS
28
+ * via the JS bundle, which loads after static stylesheets and wins the cascade.
29
+ *
30
+ * The package's margin: auto rule fires at 1020px (960 + 30 + 30), which is too
31
+ * early for our wider container. We restore the 30px gutters from 1020px and only
32
+ * allow auto-centering once the viewport is wide enough to actually cap at 1100px
33
+ * (1100 + 30 + 30 = 1160px). */
34
+ .govuk-width-container {
35
+ max-width: 1100px !important;
36
+ }
37
+
38
+ @media (min-width: 1020px) {
39
+ .govuk-width-container {
40
+ margin-right: 30px !important;
41
+ margin-left: 30px !important;
42
+ }
43
+ }
44
+
45
+ @media (min-width: 1160px) {
46
+ .govuk-width-container {
47
+ margin-right: auto !important;
48
+ margin-left: auto !important;
49
+ }
50
+ }
51
+
24
52
  /*
25
53
  * Sticky footer: Docusaurus inserts a #__docusaurus wrapper between body
26
54
  * and our layout, breaking GOV.UK Frontend's flex sticky-footer chain.
@@ -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
  );
@@ -28,7 +28,7 @@ export default function CodeBlock({children, className: classNameProp, title}) {
28
28
  </div>
29
29
  )}
30
30
  <Highlight theme={themes.github} code={codeString} language={language}>
31
- {({style, tokens, getLineProps, getTokenProps}) => (
31
+ {({style: {backgroundColor: _bg, ...style}, tokens, getLineProps, getTokenProps}) => (
32
32
  <pre className="app-code-block__pre" style={style}>
33
33
  <button
34
34
  type="button"
@@ -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} />;
@@ -20,6 +66,61 @@ const MDXComponents = {
20
66
  }
21
67
  return <pre {...props} />;
22
68
  },
69
+ // Paragraphs: detect API definition patterns like **Type:** `string` and render as <dl>
70
+ p: ({children, ...rest}) => {
71
+ const childArray = React.Children.toArray(children);
72
+ const firstChild = childArray[0];
73
+
74
+ // A definition-list paragraph starts with a <strong> whose text ends with ':'
75
+ const isDefinitionTerm = (node) =>
76
+ React.isValidElement(node) &&
77
+ node.type === 'strong' &&
78
+ typeof node.props.children === 'string' &&
79
+ node.props.children.trim().endsWith(':');
80
+
81
+ if (!isDefinitionTerm(firstChild)) {
82
+ return <p {...rest}>{children}</p>;
83
+ }
84
+
85
+ // Group children into [{term, defs}] pairs split on each <strong> label
86
+ const items = [];
87
+ let current = null;
88
+ for (const child of childArray) {
89
+ if (isDefinitionTerm(child)) {
90
+ if (current) items.push(current);
91
+ current = {term: child.props.children, defs: []};
92
+ } else if (current) {
93
+ // Skip bare whitespace/newline separators between term and value
94
+ if (typeof child === 'string' && child.trim() === '') continue;
95
+ current.defs.push(child);
96
+ }
97
+ }
98
+ if (current) items.push(current);
99
+
100
+ // Render <dd> content, converting any **Required** (or similar flags) to (required)
101
+ const renderDefs = (defs) => defs.map((node) => {
102
+ if (
103
+ React.isValidElement(node) &&
104
+ node.type === 'strong' &&
105
+ typeof node.props.children === 'string'
106
+ ) {
107
+ const label = node.props.children.trim().toLowerCase();
108
+ return <strong key={label}> ({label})</strong>;
109
+ }
110
+ return node;
111
+ });
112
+
113
+ return (
114
+ <dl className="app-definition-list">
115
+ {items.map((item) => (
116
+ <div key={item.term} className="app-definition-list__item">
117
+ <dt>{item.term}</dt>
118
+ <dd>{renderDefs(item.defs)}</dd>
119
+ </div>
120
+ ))}
121
+ </dl>
122
+ );
123
+ },
23
124
  // GOV.UK table styling
24
125
  table: (props) => <table className="govuk-table" {...props} />,
25
126
  thead: (props) => <thead className="govuk-table__head" {...props} />,
@@ -27,8 +128,24 @@ const MDXComponents = {
27
128
  tr: (props) => <tr className="govuk-table__row" {...props} />,
28
129
  th: (props) => <th className="govuk-table__header" {...props} />,
29
130
  td: (props) => <td className="govuk-table__cell" {...props} />,
30
- // Blockquotes rendered as GOV.UK InsetText
31
- blockquote: ({children, ...rest}) => <InsetText {...rest}>{children}</InsetText>,
131
+ // Blockquotes rendered as GOV.UK InsetText, with support for GitHub-style alerts
132
+ // e.g. > [!NOTE], > [!WARNING], > [!TIP], > [!IMPORTANT], > [!CAUTION]
133
+ blockquote: ({children}) => {
134
+ const alert = parseGithubAlert(children);
135
+ if (!alert) return <InsetText>{children}</InsetText>;
136
+
137
+ const {config, allContent} = alert;
138
+ if (!config.modifier) {
139
+ return <WarningText>{allContent}</WarningText>;
140
+ }
141
+
142
+ return (
143
+ <InsetText className={config.modifier}>
144
+ <p key="title"><strong>{config.label.toUpperCase()}</strong></p>
145
+ {allContent}
146
+ </InsetText>
147
+ );
148
+ },
32
149
  h1: (props) => <Heading as="h1" {...props} />,
33
150
  h2: (props) => <Heading as="h2" {...props} />,
34
151
  h3: (props) => <Heading as="h3" {...props} />,