@defra/docusaurus-theme-govuk 0.0.1-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/LICENSE +21 -0
- package/README.md +163 -0
- package/index.js +215 -0
- package/package.json +44 -0
- package/src/css/components.scss +90 -0
- package/src/css/doc-overrides.css +19 -0
- package/src/css/prose-scope.scss +59 -0
- package/src/css/theme.scss +18 -0
- package/src/lib/empty-module.js +2 -0
- package/src/lib/react-foundry-router-shim.js +68 -0
- package/src/theme/Admonition/index.js +33 -0
- package/src/theme/AnnouncementBar/index.js +5 -0
- package/src/theme/CodeBlock/index.js +55 -0
- package/src/theme/DocItem/Content/index.js +28 -0
- package/src/theme/DocItem/Footer/index.js +5 -0
- package/src/theme/DocItem/Layout/index.js +10 -0
- package/src/theme/DocItem/Metadata/index.js +17 -0
- package/src/theme/DocItem/Paginator/index.js +36 -0
- package/src/theme/DocItem/TOC/Desktop/index.js +5 -0
- package/src/theme/DocItem/TOC/Mobile/index.js +5 -0
- package/src/theme/DocItem/docContext.js +37 -0
- package/src/theme/DocItem/index.js +22 -0
- package/src/theme/DocPage/Layout/index.js +5 -0
- package/src/theme/DocRoot/Layout/index.js +10 -0
- package/src/theme/DocRoot/index.js +18 -0
- package/src/theme/DocVersionRoot/index.js +6 -0
- package/src/theme/DocsRoot/index.js +6 -0
- package/src/theme/Heading/index.js +29 -0
- package/src/theme/Homepage/index.js +47 -0
- package/src/theme/Layout/Provider/index.js +5 -0
- package/src/theme/Layout/index.js +181 -0
- package/src/theme/MDXComponents/index.js +42 -0
- package/src/theme/MDXContent/index.js +7 -0
- package/src/theme/NotFound/Content/index.js +23 -0
- package/src/theme/NotFound/index.js +11 -0
- package/src/theme/Root/index.js +5 -0
- package/src/theme/TOCItems/index.js +5 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Shim for @react-foundry/router that bridges React Router v5 (Docusaurus) to
|
|
2
|
+
// the v6-style API that @react-foundry/router expects.
|
|
3
|
+
//
|
|
4
|
+
// This is needed because @react-foundry/router re-exports `Link` and
|
|
5
|
+
// `useNavigate` from react-router v6, but Docusaurus ships react-router v5.
|
|
6
|
+
// The shim maps v5 equivalents (useHistory, Link from react-router-dom) and
|
|
7
|
+
// wires up useIsActive with the real location so @not-govuk components like
|
|
8
|
+
// ServiceNavigation can correctly detect the active page.
|
|
9
|
+
|
|
10
|
+
import { useLocation as _useLocation, useParams, Link } from 'react-router-dom';
|
|
11
|
+
import { URI } from '@react-foundry/uri';
|
|
12
|
+
|
|
13
|
+
export { useParams, Link };
|
|
14
|
+
|
|
15
|
+
export const needSuspense = false;
|
|
16
|
+
|
|
17
|
+
// Enhance location with parsed query string (matches @react-foundry/router API)
|
|
18
|
+
const enhanceLocation = (location) => {
|
|
19
|
+
const search = location.search || '';
|
|
20
|
+
const params = new URLSearchParams(search);
|
|
21
|
+
const query = {};
|
|
22
|
+
for (const [key, value] of params.entries()) {
|
|
23
|
+
query[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return { ...location, query };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const useLocation = () => enhanceLocation(_useLocation());
|
|
29
|
+
|
|
30
|
+
// Build useIsActive from the real location (mirrors @react-foundry/router/is-active)
|
|
31
|
+
const includes = (haystack, needle) => {
|
|
32
|
+
const subIncludes = (h, n) =>
|
|
33
|
+
Array.isArray(n)
|
|
34
|
+
? n.length === h.length && n.every((v, i) => subIncludes(h[i], v))
|
|
35
|
+
: typeof n === 'object'
|
|
36
|
+
? typeof h === 'object' && Object.keys(n).every((k) => subIncludes(h[k], n[k]))
|
|
37
|
+
: n === h;
|
|
38
|
+
return subIncludes(haystack, needle);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const useIsActive = () => {
|
|
42
|
+
const location = useLocation();
|
|
43
|
+
|
|
44
|
+
return (href, exact = true) => {
|
|
45
|
+
const target = URI.parse(href, location.pathname);
|
|
46
|
+
const dir = target.pathname.endsWith('/') ? target.pathname : target.pathname + '/';
|
|
47
|
+
// Root path '/' should only match exactly, not as a prefix for all paths
|
|
48
|
+
const pathStart = target.pathname === '' || (target.pathname !== '/' && location.pathname.startsWith(dir));
|
|
49
|
+
const pathMatch = target.pathname === '' || location.pathname === target.pathname;
|
|
50
|
+
const queryMatch = includes(location.query, target.query);
|
|
51
|
+
const activeExact = !!(pathMatch && queryMatch);
|
|
52
|
+
return exact ? activeExact : !!(activeExact || (pathStart && queryMatch));
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// React Router v5 has useHistory, not useNavigate — wrap it
|
|
57
|
+
export const useNavigate = () => {
|
|
58
|
+
const { useHistory } = require('react-router-dom');
|
|
59
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
60
|
+
const history = useHistory();
|
|
61
|
+
return (to) => {
|
|
62
|
+
if (typeof to === 'number') {
|
|
63
|
+
history.go(to);
|
|
64
|
+
} else {
|
|
65
|
+
history.push(to);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {InsetText, WarningText} from '@not-govuk/simple-components';
|
|
3
|
+
|
|
4
|
+
const admonitionTitles = {
|
|
5
|
+
note: 'Note',
|
|
6
|
+
tip: 'Tip',
|
|
7
|
+
info: 'Info',
|
|
8
|
+
warning: 'Warning',
|
|
9
|
+
danger: 'Danger',
|
|
10
|
+
caution: 'Caution',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function Admonition({type = 'note', title, children}) {
|
|
14
|
+
const displayTitle = title || admonitionTitles[type] || 'Note';
|
|
15
|
+
|
|
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
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// All other types use GOV.UK InsetText
|
|
27
|
+
return (
|
|
28
|
+
<InsetText>
|
|
29
|
+
{title && <strong>{displayTitle}: </strong>}
|
|
30
|
+
{children}
|
|
31
|
+
</InsetText>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, {useState} from 'react';
|
|
2
|
+
import {Highlight, themes} from 'prism-react-renderer';
|
|
3
|
+
|
|
4
|
+
export default function CodeBlock({children, className: classNameProp, title}) {
|
|
5
|
+
const [copied, setCopied] = useState(false);
|
|
6
|
+
|
|
7
|
+
// Extract language from className (e.g. 'language-javascript')
|
|
8
|
+
const language = classNameProp
|
|
9
|
+
? classNameProp.replace(/language-/, '')
|
|
10
|
+
: 'text';
|
|
11
|
+
|
|
12
|
+
const codeString = typeof children === 'string'
|
|
13
|
+
? children.replace(/\n$/, '')
|
|
14
|
+
: '';
|
|
15
|
+
|
|
16
|
+
const handleCopy = () => {
|
|
17
|
+
navigator.clipboard.writeText(codeString).then(() => {
|
|
18
|
+
setCopied(true);
|
|
19
|
+
setTimeout(() => setCopied(false), 2000);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="app-code-block">
|
|
25
|
+
{title && (
|
|
26
|
+
<div className="app-code-block__title">
|
|
27
|
+
{title}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
<Highlight theme={themes.github} code={codeString} language={language}>
|
|
31
|
+
{({style, tokens, getLineProps, getTokenProps}) => (
|
|
32
|
+
<pre className="app-code-block__pre" style={style}>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={handleCopy}
|
|
36
|
+
className="app-code-block__copy"
|
|
37
|
+
aria-label="Copy code to clipboard"
|
|
38
|
+
>
|
|
39
|
+
{copied ? 'Copied' : 'Copy'}
|
|
40
|
+
</button>
|
|
41
|
+
<code>
|
|
42
|
+
{tokens.map((line, i) => (
|
|
43
|
+
<div key={i} {...getLineProps({line})}>
|
|
44
|
+
{line.map((token, key) => (
|
|
45
|
+
<span key={key} {...getTokenProps({token})} />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</code>
|
|
50
|
+
</pre>
|
|
51
|
+
)}
|
|
52
|
+
</Highlight>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {useDoc} from '../docContext';
|
|
3
|
+
import MDXContent from '@theme/MDXContent';
|
|
4
|
+
|
|
5
|
+
// Render a synthetic title if needed
|
|
6
|
+
function useSyntheticTitle() {
|
|
7
|
+
const {metadata, frontMatter, contentTitle} = useDoc();
|
|
8
|
+
const shouldRender =
|
|
9
|
+
!frontMatter.hide_title && typeof contentTitle === 'undefined';
|
|
10
|
+
if (!shouldRender) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return metadata.title;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function DocItemContent({children}) {
|
|
17
|
+
const syntheticTitle = useSyntheticTitle();
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{syntheticTitle && (
|
|
21
|
+
<header>
|
|
22
|
+
<h1 className="govuk-heading-xl">{syntheticTitle}</h1>
|
|
23
|
+
</header>
|
|
24
|
+
)}
|
|
25
|
+
<MDXContent>{children}</MDXContent>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Head from '@docusaurus/Head';
|
|
3
|
+
import {useDoc} from '../docContext';
|
|
4
|
+
|
|
5
|
+
export default function DocItemMetadata() {
|
|
6
|
+
const {metadata} = useDoc();
|
|
7
|
+
const {title, description} = metadata;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Head>
|
|
11
|
+
{title && <title>{title}</title>}
|
|
12
|
+
{description && <meta name="description" content={description} />}
|
|
13
|
+
{title && <meta property="og:title" content={title} />}
|
|
14
|
+
{description && <meta property="og:description" content={description} />}
|
|
15
|
+
</Head>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {useDoc} from '../docContext';
|
|
3
|
+
|
|
4
|
+
export default function DocItemPaginator() {
|
|
5
|
+
const {metadata} = useDoc();
|
|
6
|
+
const {previous, next} = metadata;
|
|
7
|
+
|
|
8
|
+
if (!previous && !next) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<nav className="app-pagination govuk-!-margin-top-8" aria-label="Pagination">
|
|
14
|
+
<div className="app-pagination__container">
|
|
15
|
+
{previous ? (
|
|
16
|
+
<div className="app-pagination__prev">
|
|
17
|
+
<span className="govuk-body-s app-text-secondary">Previous</span>
|
|
18
|
+
<br />
|
|
19
|
+
<a href={previous.permalink} className="govuk-link">
|
|
20
|
+
{previous.title}
|
|
21
|
+
</a>
|
|
22
|
+
</div>
|
|
23
|
+
) : <div />}
|
|
24
|
+
{next ? (
|
|
25
|
+
<div className="app-pagination__next">
|
|
26
|
+
<span className="govuk-body-s app-text-secondary">Next</span>
|
|
27
|
+
<br />
|
|
28
|
+
<a href={next.permalink} className="govuk-link">
|
|
29
|
+
{next.title}
|
|
30
|
+
</a>
|
|
31
|
+
</div>
|
|
32
|
+
) : <div />}
|
|
33
|
+
</div>
|
|
34
|
+
</nav>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React, {createContext, useContext} from 'react';
|
|
2
|
+
|
|
3
|
+
const DocContext = createContext(null);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lightweight DocProvider that works across Docusaurus 3.x versions.
|
|
7
|
+
* Replaces the version-specific DocProvider from @docusaurus/plugin-content-docs/client
|
|
8
|
+
* which was only added in Docusaurus 3.5+.
|
|
9
|
+
*/
|
|
10
|
+
export function DocProvider({content, children}) {
|
|
11
|
+
const Content = content;
|
|
12
|
+
const value = {
|
|
13
|
+
content: Content,
|
|
14
|
+
metadata: Content.metadata || {},
|
|
15
|
+
frontMatter: Content.frontMatter || {},
|
|
16
|
+
contentTitle: Content.contentTitle,
|
|
17
|
+
assets: Content.assets || {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<DocContext.Provider value={value}>
|
|
22
|
+
{children}
|
|
23
|
+
</DocContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hook to access doc data from DocProvider context.
|
|
29
|
+
* Replaces the version-specific useDoc() from @docusaurus/plugin-content-docs/client.
|
|
30
|
+
*/
|
|
31
|
+
export function useDoc() {
|
|
32
|
+
const ctx = useContext(DocContext);
|
|
33
|
+
if (!ctx) {
|
|
34
|
+
throw new Error('useDoc() must be used within a <DocProvider>');
|
|
35
|
+
}
|
|
36
|
+
return ctx;
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {DocProvider} from './docContext';
|
|
3
|
+
import DocItemMetadata from '@theme/DocItem/Metadata';
|
|
4
|
+
import DocItemLayout from '@theme/DocItem/Layout';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DocItem — wraps a single doc page.
|
|
8
|
+
* Uses our own DocProvider (not the version-specific one from
|
|
9
|
+
* @docusaurus/plugin-content-docs/client which requires 3.5+).
|
|
10
|
+
*/
|
|
11
|
+
export default function DocItem(props) {
|
|
12
|
+
const {content: Content} = props;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<DocProvider content={Content}>
|
|
16
|
+
<DocItemMetadata />
|
|
17
|
+
<DocItemLayout>
|
|
18
|
+
<Content />
|
|
19
|
+
</DocItemLayout>
|
|
20
|
+
</DocProvider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderRoutes from '@docusaurus/renderRoutes';
|
|
3
|
+
import DocRootLayout from '@theme/DocRoot/Layout';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DocRoot — version-independent wrapper for the doc routes.
|
|
7
|
+
* Does not depend on useDocRootMetadata/DocsSidebarProvider
|
|
8
|
+
* which are only available in Docusaurus 3.5+.
|
|
9
|
+
* Instead, it renders the matched child routes directly,
|
|
10
|
+
* wrapped in the DocRootLayout (which includes the GOV.UK shell).
|
|
11
|
+
*/
|
|
12
|
+
export default function DocRoot({route}) {
|
|
13
|
+
return (
|
|
14
|
+
<DocRootLayout>
|
|
15
|
+
{renderRoutes(route.routes)}
|
|
16
|
+
</DocRootLayout>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const headingClasses = {
|
|
4
|
+
h1: 'govuk-heading-xl',
|
|
5
|
+
h2: 'govuk-heading-l',
|
|
6
|
+
h3: 'govuk-heading-m',
|
|
7
|
+
h4: 'govuk-heading-s',
|
|
8
|
+
h5: 'govuk-heading-s',
|
|
9
|
+
h6: 'govuk-heading-s',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function Heading({as: Tag = 'h2', id, children, ...props}) {
|
|
13
|
+
const className = headingClasses[Tag] || 'govuk-heading-m';
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Tag id={id} className={className} {...props}>
|
|
17
|
+
{children}
|
|
18
|
+
{id && (
|
|
19
|
+
<a
|
|
20
|
+
href={`#${id}`}
|
|
21
|
+
className="govuk-link app-heading-anchor"
|
|
22
|
+
aria-label={`Direct link to ${typeof children === 'string' ? children : 'heading'}`}
|
|
23
|
+
>
|
|
24
|
+
#
|
|
25
|
+
</a>
|
|
26
|
+
)}
|
|
27
|
+
</Tag>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
3
|
+
import useBaseUrl from '@docusaurus/useBaseUrl';
|
|
4
|
+
import Layout from '@theme/Layout';
|
|
5
|
+
|
|
6
|
+
export default function Homepage() {
|
|
7
|
+
const {siteConfig} = useDocusaurusContext();
|
|
8
|
+
const baseUrl = useBaseUrl('/');
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Layout title={siteConfig.title} description={siteConfig.tagline}>
|
|
12
|
+
<div className="govuk-grid-row">
|
|
13
|
+
<div className="govuk-grid-column-two-thirds">
|
|
14
|
+
<h1 className="govuk-heading-xl govuk-!-margin-top-8">
|
|
15
|
+
{siteConfig.title}
|
|
16
|
+
</h1>
|
|
17
|
+
|
|
18
|
+
{siteConfig.tagline && (
|
|
19
|
+
<p className="govuk-body-l">
|
|
20
|
+
{siteConfig.tagline}
|
|
21
|
+
</p>
|
|
22
|
+
)}
|
|
23
|
+
|
|
24
|
+
<a
|
|
25
|
+
href={baseUrl}
|
|
26
|
+
role="button"
|
|
27
|
+
draggable="false"
|
|
28
|
+
className="govuk-button govuk-button--start govuk-!-margin-top-4"
|
|
29
|
+
>
|
|
30
|
+
Get started
|
|
31
|
+
<svg
|
|
32
|
+
className="govuk-button__start-icon"
|
|
33
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
34
|
+
width="17.5"
|
|
35
|
+
height="19"
|
|
36
|
+
viewBox="0 0 33 40"
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
focusable="false"
|
|
39
|
+
>
|
|
40
|
+
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z" />
|
|
41
|
+
</svg>
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</Layout>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {SkipLink, Header, Footer, PhaseBanner, ServiceNavigation, NavigationMenu} from '@not-govuk/simple-components';
|
|
3
|
+
import {useLocation} from '@docusaurus/router';
|
|
4
|
+
import Head from '@docusaurus/Head';
|
|
5
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
6
|
+
import LayoutProvider from '@theme/Layout/Provider';
|
|
7
|
+
import AnnouncementBar from '@theme/AnnouncementBar';
|
|
8
|
+
|
|
9
|
+
// Read GOV.UK config from themeConfig
|
|
10
|
+
function useGovukConfig() {
|
|
11
|
+
const {siteConfig} = useDocusaurusContext();
|
|
12
|
+
return siteConfig.themeConfig?.govuk || {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Strip the Docusaurus baseUrl prefix from a pathname
|
|
16
|
+
function stripBaseUrl(pathname, baseUrl) {
|
|
17
|
+
if (baseUrl === '/') return pathname;
|
|
18
|
+
const prefix = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
19
|
+
if (pathname.startsWith(prefix)) {
|
|
20
|
+
const stripped = pathname.slice(prefix.length);
|
|
21
|
+
return stripped || '/';
|
|
22
|
+
}
|
|
23
|
+
return pathname;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Resolve sidebar paths.
|
|
27
|
+
// Paths starting with '/' are absolute (from site root).
|
|
28
|
+
// Paths without '/' are relative to the section's basePath.
|
|
29
|
+
function resolvePath(basePath, relativePath) {
|
|
30
|
+
// Absolute path — return as-is
|
|
31
|
+
if (relativePath.startsWith('/')) return relativePath;
|
|
32
|
+
// Relative to root
|
|
33
|
+
if (basePath === '/') return `/${relativePath}`;
|
|
34
|
+
// Relative to section
|
|
35
|
+
const cleanBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
36
|
+
return `${cleanBase}/${relativePath}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Resolve all paths in sidebar items
|
|
40
|
+
function resolveSidebarPaths(items, basePath) {
|
|
41
|
+
return items.map(item => {
|
|
42
|
+
const resolvedItem = {
|
|
43
|
+
...item,
|
|
44
|
+
href: resolvePath(basePath, item.href),
|
|
45
|
+
};
|
|
46
|
+
if (item.items && item.items.length > 0) {
|
|
47
|
+
resolvedItem.items = item.items.map(nestedItem => ({
|
|
48
|
+
...nestedItem,
|
|
49
|
+
href: resolvePath(resolvedItem.href, nestedItem.href),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
return resolvedItem;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Find the active navigation section based on current path
|
|
57
|
+
function getActiveSection(pathname, navigation) {
|
|
58
|
+
return navigation.find(section => {
|
|
59
|
+
if (!section.sidebar) return false;
|
|
60
|
+
// Use the section's configured href as the base path
|
|
61
|
+
const basePath = section.href || '/';
|
|
62
|
+
const resolvedSidebar = resolveSidebarPaths(section.sidebar, basePath);
|
|
63
|
+
return resolvedSidebar.some(item => {
|
|
64
|
+
if (pathname === item.href) return true;
|
|
65
|
+
if (item.items) {
|
|
66
|
+
return item.items.some(nestedItem => pathname === nestedItem.href);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function Layout(props) {
|
|
74
|
+
const location = useLocation();
|
|
75
|
+
const {siteConfig} = useDocusaurusContext();
|
|
76
|
+
const govukConfig = siteConfig.themeConfig?.govuk || {};
|
|
77
|
+
const {
|
|
78
|
+
children,
|
|
79
|
+
title,
|
|
80
|
+
description,
|
|
81
|
+
noFooter,
|
|
82
|
+
} = props;
|
|
83
|
+
|
|
84
|
+
const navigation = govukConfig.navigation || [];
|
|
85
|
+
const header = govukConfig.header || {};
|
|
86
|
+
const phaseBanner = govukConfig.phaseBanner;
|
|
87
|
+
const footer = govukConfig.footer || {};
|
|
88
|
+
|
|
89
|
+
// Strip baseUrl so sidebar matching works regardless of deployment path
|
|
90
|
+
const pathname = stripBaseUrl(location.pathname, siteConfig.baseUrl);
|
|
91
|
+
const baseUrl = siteConfig.baseUrl.endsWith('/')
|
|
92
|
+
? siteConfig.baseUrl.slice(0, -1)
|
|
93
|
+
: siteConfig.baseUrl;
|
|
94
|
+
|
|
95
|
+
// Prepend baseUrl to a site-root path for use in actual links
|
|
96
|
+
function withBase(href) {
|
|
97
|
+
if (!href || href.startsWith('http')) return href;
|
|
98
|
+
return `${baseUrl}${href.startsWith('/') ? href : `/${href}`}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get active section for sidebar
|
|
102
|
+
const activeSection = getActiveSection(pathname, navigation);
|
|
103
|
+
const basePath = activeSection?.href || '/';
|
|
104
|
+
const sidebarItems = activeSection?.sidebar
|
|
105
|
+
? resolveSidebarPaths(activeSection.sidebar, basePath).map(item => ({
|
|
106
|
+
...item,
|
|
107
|
+
href: withBase(item.href),
|
|
108
|
+
...(item.items && {
|
|
109
|
+
items: item.items.map(nested => ({...nested, href: withBase(nested.href)})),
|
|
110
|
+
}),
|
|
111
|
+
}))
|
|
112
|
+
: null;
|
|
113
|
+
|
|
114
|
+
// Convert navigation to service navigation format (Level 1 only)
|
|
115
|
+
const serviceNavItems = navigation.map(item => ({
|
|
116
|
+
href: withBase(item.href),
|
|
117
|
+
text: item.text,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<LayoutProvider>
|
|
122
|
+
<Head>
|
|
123
|
+
<html lang="en-GB" className="govuk-template" />
|
|
124
|
+
<body className="govuk-template__body" />
|
|
125
|
+
<meta name="theme-color" content="#0b0c0c" />
|
|
126
|
+
{title && <title>{title}</title>}
|
|
127
|
+
{description && <meta name="description" content={description} />}
|
|
128
|
+
</Head>
|
|
129
|
+
|
|
130
|
+
<div className="govuk-template--rebranded">
|
|
131
|
+
<AnnouncementBar />
|
|
132
|
+
|
|
133
|
+
{/* Hidden navbar element for Docusaurus hooks */}
|
|
134
|
+
<nav className="navbar" style={{display: 'none'}} />
|
|
135
|
+
|
|
136
|
+
<SkipLink for="main-content">Skip to main content</SkipLink>
|
|
137
|
+
|
|
138
|
+
<Header
|
|
139
|
+
govUK
|
|
140
|
+
rebrand
|
|
141
|
+
serviceName={header.serviceName}
|
|
142
|
+
serviceHref={withBase(header.serviceHref || '/')}
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
<ServiceNavigation items={serviceNavItems} />
|
|
146
|
+
|
|
147
|
+
<div className="govuk-width-container">
|
|
148
|
+
{phaseBanner && (
|
|
149
|
+
<PhaseBanner phase={phaseBanner.phase}>
|
|
150
|
+
{phaseBanner.text}{' '}
|
|
151
|
+
{phaseBanner.feedbackHref && (
|
|
152
|
+
<a href={phaseBanner.feedbackHref} className="govuk-link">
|
|
153
|
+
feedback
|
|
154
|
+
</a>
|
|
155
|
+
)}
|
|
156
|
+
</PhaseBanner>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
<main id="main-content" className="govuk-main-wrapper">
|
|
160
|
+
{sidebarItems ? (
|
|
161
|
+
<div className="app-layout-sidebar">
|
|
162
|
+
<aside className="app-layout-sidebar__nav">
|
|
163
|
+
<NavigationMenu items={sidebarItems} />
|
|
164
|
+
</aside>
|
|
165
|
+
<div className="app-layout-sidebar__content">
|
|
166
|
+
{children}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
) : (
|
|
170
|
+
children
|
|
171
|
+
)}
|
|
172
|
+
</main>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{!noFooter && (
|
|
176
|
+
<Footer govUK rebrand meta={footer.meta} />
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</LayoutProvider>
|
|
180
|
+
);
|
|
181
|
+
}
|