@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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -0
  3. package/index.js +215 -0
  4. package/package.json +44 -0
  5. package/src/css/components.scss +90 -0
  6. package/src/css/doc-overrides.css +19 -0
  7. package/src/css/prose-scope.scss +59 -0
  8. package/src/css/theme.scss +18 -0
  9. package/src/lib/empty-module.js +2 -0
  10. package/src/lib/react-foundry-router-shim.js +68 -0
  11. package/src/theme/Admonition/index.js +33 -0
  12. package/src/theme/AnnouncementBar/index.js +5 -0
  13. package/src/theme/CodeBlock/index.js +55 -0
  14. package/src/theme/DocItem/Content/index.js +28 -0
  15. package/src/theme/DocItem/Footer/index.js +5 -0
  16. package/src/theme/DocItem/Layout/index.js +10 -0
  17. package/src/theme/DocItem/Metadata/index.js +17 -0
  18. package/src/theme/DocItem/Paginator/index.js +36 -0
  19. package/src/theme/DocItem/TOC/Desktop/index.js +5 -0
  20. package/src/theme/DocItem/TOC/Mobile/index.js +5 -0
  21. package/src/theme/DocItem/docContext.js +37 -0
  22. package/src/theme/DocItem/index.js +22 -0
  23. package/src/theme/DocPage/Layout/index.js +5 -0
  24. package/src/theme/DocRoot/Layout/index.js +10 -0
  25. package/src/theme/DocRoot/index.js +18 -0
  26. package/src/theme/DocVersionRoot/index.js +6 -0
  27. package/src/theme/DocsRoot/index.js +6 -0
  28. package/src/theme/Heading/index.js +29 -0
  29. package/src/theme/Homepage/index.js +47 -0
  30. package/src/theme/Layout/Provider/index.js +5 -0
  31. package/src/theme/Layout/index.js +181 -0
  32. package/src/theme/MDXComponents/index.js +42 -0
  33. package/src/theme/MDXContent/index.js +7 -0
  34. package/src/theme/NotFound/Content/index.js +23 -0
  35. package/src/theme/NotFound/index.js +11 -0
  36. package/src/theme/Root/index.js +5 -0
  37. 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,5 @@
1
+ import React from 'react';
2
+
3
+ export default function AnnouncementBar() {
4
+ return null;
5
+ }
@@ -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,5 @@
1
+ import React from 'react';
2
+
3
+ export default function DocItemFooter() {
4
+ return null;
5
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import DocItemContent from '@theme/DocItem/Content';
3
+
4
+ export default function DocItemLayout({children}) {
5
+ return (
6
+ <article className="app-prose-scope">
7
+ <DocItemContent>{children}</DocItemContent>
8
+ </article>
9
+ );
10
+ }
@@ -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,5 @@
1
+ import React from 'react';
2
+
3
+ export default function TOCDesktop() {
4
+ return null;
5
+ }
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ export default function TOCMobile() {
4
+ return null;
5
+ }
@@ -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,5 @@
1
+ import React from 'react';
2
+
3
+ export default function DocPageLayout({children}) {
4
+ return <>{children}</>;
5
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import Layout from '@theme/Layout';
3
+
4
+ export default function DocRootLayout({children}) {
5
+ return (
6
+ <Layout>
7
+ {children}
8
+ </Layout>
9
+ );
10
+ }
@@ -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,6 @@
1
+ import React from 'react';
2
+ import renderRoutes from '@docusaurus/renderRoutes';
3
+
4
+ export default function DocVersionRoot({route}) {
5
+ return renderRoutes(route.routes);
6
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import renderRoutes from '@docusaurus/renderRoutes';
3
+
4
+ export default function DocsRoot({route}) {
5
+ return renderRoutes(route.routes);
6
+ }
@@ -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,5 @@
1
+ import React from 'react';
2
+
3
+ export default function LayoutProvider({children}) {
4
+ return <>{children}</>;
5
+ }
@@ -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
+ }