diff --git a/dotcom-rendering/index.d.ts b/dotcom-rendering/index.d.ts
index 43cc9db9544..eabc3930621 100644
--- a/dotcom-rendering/index.d.ts
+++ b/dotcom-rendering/index.d.ts
@@ -2,6 +2,9 @@
// 3rd party type declarations //
// ------------------------------
+type GuardianCrossword =
+ import('@guardian/react-crossword-next').CrosswordProps['data'];
+
declare module 'chromatic/isChromatic';
declare module 'dynamic-import-polyfill' {
diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json
index 25e40e2e627..ab9d62de7b0 100644
--- a/dotcom-rendering/package.json
+++ b/dotcom-rendering/package.json
@@ -50,6 +50,7 @@
"@guardian/libs": "19.2.1",
"@guardian/ophan-tracker-js": "2.2.5",
"@guardian/react-crossword": "2.0.2",
+ "@guardian/react-crossword-next": "npm:@guardian/react-crossword@0.0.0-canary-20241209150926",
"@guardian/shimport": "1.0.2",
"@guardian/source": "8.0.0",
"@guardian/source-development-kitchen": "12.0.0",
diff --git a/dotcom-rendering/scripts/env/check-deps.js b/dotcom-rendering/scripts/env/check-deps.js
index c34c6ed0fec..2a9110c4195 100644
--- a/dotcom-rendering/scripts/env/check-deps.js
+++ b/dotcom-rendering/scripts/env/check-deps.js
@@ -8,10 +8,24 @@ if (pkg.devDependencies) {
process.exit(1);
}
-const mismatches = Object.entries(pkg.dependencies).filter(
- ([, version]) =>
- !semver.valid(version) && !version.startsWith('workspace:'),
-);
+/**
+ * We don't check packages that are not semver-compatible
+ * @type {RegExp[]}
+ */
+const exceptions = /** @type {const} */ ([
+ /npm:@guardian\/react-crossword@0.0.0-canary/,
+]);
+
+const mismatches = Object.entries(pkg.dependencies)
+ .filter(
+ ([, version]) =>
+ !exceptions.some((exception) => exception.test(version)),
+ )
+
+ .filter(
+ ([, version]) =>
+ !semver.valid(version) && !version.startsWith('workspace:'),
+ );
if (mismatches.length !== 0) {
warn('dotcom-rendering dependencies should be pinned.');
diff --git a/dotcom-rendering/src/components/ArticleContainer.tsx b/dotcom-rendering/src/components/ArticleContainer.tsx
index 94e02c55792..5a1e46222e4 100644
--- a/dotcom-rendering/src/components/ArticleContainer.tsx
+++ b/dotcom-rendering/src/components/ArticleContainer.tsx
@@ -24,6 +24,9 @@ const articleWidth = (format: ArticleFormat) => {
}
`;
}
+ case ArticleDesign.Crossword:
+ /* The crossword player manages its own width; */
+ return null;
case ArticleDesign.Video:
case ArticleDesign.Audio:
return css`
diff --git a/dotcom-rendering/src/components/BylineLink.tsx b/dotcom-rendering/src/components/BylineLink.tsx
index 3c9c4beca07..e7c1eb745d0 100644
--- a/dotcom-rendering/src/components/BylineLink.tsx
+++ b/dotcom-rendering/src/components/BylineLink.tsx
@@ -1,6 +1,7 @@
import { isString } from '@guardian/libs';
import { Hide } from '@guardian/source/react-components';
import { DottedLines } from '@guardian/source-development-kitchen/react-components';
+import { Fragment } from 'react';
import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat';
import {
getBylineComponentsFromTokens,
@@ -160,6 +161,10 @@ const getRenderedTokens = (
);
}
+ if (design === ArticleDesign.Crossword && renderedTokens.length > 0) {
+ return [Set by: , ...renderedTokens];
+ }
+
return renderedTokens;
};
diff --git a/dotcom-rendering/src/components/Crossword.importable.tsx b/dotcom-rendering/src/components/Crossword.importable.tsx
new file mode 100644
index 00000000000..87ad0d53796
--- /dev/null
+++ b/dotcom-rendering/src/components/Crossword.importable.tsx
@@ -0,0 +1,6 @@
+import { Crossword as ReactCrossword } from '@guardian/react-crossword-next';
+import type { CrosswordProps } from '@guardian/react-crossword-next';
+
+export const Crossword = ({ data }: CrosswordProps) => (
+
+);
diff --git a/dotcom-rendering/src/components/CrosswordInstructions.tsx b/dotcom-rendering/src/components/CrosswordInstructions.tsx
new file mode 100644
index 00000000000..1d59bdb966b
--- /dev/null
+++ b/dotcom-rendering/src/components/CrosswordInstructions.tsx
@@ -0,0 +1,28 @@
+import { css } from '@emotion/react';
+import {
+ textEgyptian17,
+ textEgyptianBold17,
+} from '@guardian/source/foundations';
+
+const instructionsStyles = css`
+ ${textEgyptian17};
+ white-space: pre-line;
+`;
+
+const headerStyles = css`
+ ${textEgyptianBold17};
+ white-space: pre-line;
+`;
+
+export const CrosswordInstructions = ({
+ instructions,
+ className,
+}: {
+ instructions: string;
+ className?: string;
+}) => (
+
+ Special instructions:
+ {instructions}
+
+);
diff --git a/dotcom-rendering/src/components/CrosswordLinks.tsx b/dotcom-rendering/src/components/CrosswordLinks.tsx
new file mode 100644
index 00000000000..363a70a4113
--- /dev/null
+++ b/dotcom-rendering/src/components/CrosswordLinks.tsx
@@ -0,0 +1,34 @@
+import { css } from '@emotion/react';
+import { isUndefined } from '@guardian/libs';
+import { textSans15 } from '@guardian/source/foundations';
+import { palette } from '../palette';
+
+const crosswordLinkStyles = css`
+ ${textSans15};
+
+ a {
+ color: ${palette('--standfirst-link-text')};
+ text-decoration: none;
+ :hover {
+ border-bottom: 1px solid ${palette('--standfirst-link-border')};
+ }
+ }
+`;
+
+export const CrosswordLinks = ({
+ crossword,
+ className,
+}: {
+ crossword: GuardianCrossword;
+ className?: string;
+}) => {
+ return (
+ isUndefined(crossword.pdf) || (
+
+
+ PDF version
+
+
+ )
+ );
+};
diff --git a/dotcom-rendering/src/layouts/CrosswordLayout.tsx b/dotcom-rendering/src/layouts/CrosswordLayout.tsx
new file mode 100644
index 00000000000..072420a8eb3
--- /dev/null
+++ b/dotcom-rendering/src/layouts/CrosswordLayout.tsx
@@ -0,0 +1,790 @@
+import { css } from '@emotion/react';
+import {
+ from,
+ palette as sourcePalette,
+ until,
+} from '@guardian/source/foundations';
+import { Hide } from '@guardian/source/react-components';
+import { StraightLines } from '@guardian/source-development-kitchen/react-components';
+import React from 'react';
+import { AdPortals } from '../components/AdPortals.importable';
+import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web';
+import { AppsFooter } from '../components/AppsFooter.importable';
+import { ArticleBody } from '../components/ArticleBody';
+import { ArticleContainer } from '../components/ArticleContainer';
+import { ArticleHeadline } from '../components/ArticleHeadline';
+import { ArticleMetaApps } from '../components/ArticleMeta.apps';
+import { ArticleMeta } from '../components/ArticleMeta.web';
+import { ArticleTitle } from '../components/ArticleTitle';
+import { Carousel } from '../components/Carousel.importable';
+import { CrosswordInstructions } from '../components/CrosswordInstructions';
+import { CrosswordLinks } from '../components/CrosswordLinks';
+import { DecideLines } from '../components/DecideLines';
+import { DiscussionLayout } from '../components/DiscussionLayout';
+import { Footer } from '../components/Footer';
+import { GridItem } from '../components/GridItem';
+import { HeaderAdSlot } from '../components/HeaderAdSlot';
+import { Island } from '../components/Island';
+import { Masthead } from '../components/Masthead/Masthead';
+import { MostViewedFooterData } from '../components/MostViewedFooterData.importable';
+import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout';
+import { OnwardsUpper } from '../components/OnwardsUpper.importable';
+import { RightColumn } from '../components/RightColumn';
+import { Section } from '../components/Section';
+import { SlotBodyEnd } from '../components/SlotBodyEnd.importable';
+import { Standfirst } from '../components/Standfirst';
+import { StickyBottomBanner } from '../components/StickyBottomBanner.importable';
+import { SubMeta } from '../components/SubMeta';
+import { SubNav } from '../components/SubNav.importable';
+import { type ArticleFormat, ArticleSpecial } from '../lib/articleFormat';
+import { canRenderAds } from '../lib/canRenderAds';
+import { getContributionsServiceUrl } from '../lib/contributions';
+import { decideTrail } from '../lib/decideTrail';
+import type { NavType } from '../model/extract-nav';
+import { palette as themePalette } from '../palette';
+import type { ArticleDeprecated } from '../types/article';
+import type { RenderingTarget } from '../types/renderingTarget';
+import { BannerWrapper, Stuck } from './lib/stickiness';
+
+const CrosswordGrid = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+const maxWidth = css`
+ ${from.desktop} {
+ max-width: 620px;
+ }
+`;
+
+const stretchLines = css`
+ ${until.phablet} {
+ margin-left: -20px;
+ margin-right: -20px;
+ }
+ ${until.mobileLandscape} {
+ margin-left: -10px;
+ margin-right: -10px;
+ }
+`;
+
+interface CommonProps {
+ article: ArticleDeprecated;
+ format: ArticleFormat;
+ renderingTarget: RenderingTarget;
+}
+
+interface WebProps extends CommonProps {
+ NAV: NavType;
+ renderingTarget: 'Web';
+}
+
+interface AppsProps extends CommonProps {
+ renderingTarget: 'Apps';
+}
+
+export const CrosswordLayout = (props: WebProps | AppsProps) => {
+ const { article, format, renderingTarget } = props;
+ const {
+ config: { isPaidContent, host, hasSurveyAd },
+ } = article;
+
+ const isApps = renderingTarget === 'Apps';
+ const isWeb = renderingTarget === 'Web';
+
+ const showComments = article.isCommentable && !isPaidContent;
+
+ const { branding } = article.commercialProperties[article.editionId];
+
+ const contributionsServiceUrl = getContributionsServiceUrl(article);
+
+ const { absoluteServerTimes = false } = article.config.switches;
+
+ /**
+ * This property currently only applies to the header and merchandising slots
+ */
+ const renderAds = isWeb && canRenderAds(article);
+ return (
+ <>
+ {isWeb && (
+ <>
+
+ {renderAds && (
+
+
+
+
+
+ )}
+
+
+
+
+ {renderAds && hasSurveyAd && (
+
+ )}
+ >
+ )}
+
+ {isApps && (
+ <>
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ {article.crossword && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {isApps ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+
+ {!!article.crossword?.instructions && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {renderAds ? (
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+ {renderAds && (
+
+ )}
+
+ {article.storyPackage && (
+
+ )}
+
+
+
+
+
+ {showComments && (
+
+ )}
+
+ {!isPaidContent && (
+
+ )}
+
+ {renderAds && (
+
+ )}
+
+
+ {isWeb && props.NAV.subNavSections && (
+
+ )}
+
+ {isWeb && (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )}
+ {isApps && (
+
+ )}
+ >
+ );
+};
diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx
index b55f4973791..9920cb9d19c 100644
--- a/dotcom-rendering/src/layouts/DecideLayout.tsx
+++ b/dotcom-rendering/src/layouts/DecideLayout.tsx
@@ -8,6 +8,7 @@ import type { ArticleDeprecated } from '../types/article';
import type { RenderingTarget } from '../types/renderingTarget';
import { AudioLayout } from './AudioLayout';
import { CommentLayout } from './CommentLayout';
+import { CrosswordLayout } from './CrosswordLayout';
import { FullPageInteractiveLayout } from './FullPageInteractiveLayout';
import { ImmersiveLayout } from './ImmersiveLayout';
import { InteractiveLayout } from './InteractiveLayout';
@@ -273,6 +274,15 @@ const DecideLayoutWeb = ({
NAV={NAV}
/>
);
+ case ArticleDesign.Crossword:
+ return (
+
+ );
default:
return (
): ArticleDesign => {
return ArticleDesign.Timeline;
case 'ProfileDesign':
return ArticleDesign.Profile;
+ case 'CrosswordDesign':
+ return ArticleDesign.Crossword;
default:
return ArticleDesign.Standard;
}
@@ -270,6 +273,8 @@ const designToFEDesign = (design: ArticleDesign): FEDesign => {
return 'TimelineDesign';
case ArticleDesign.Profile:
return 'ProfileDesign';
+ case ArticleDesign.Crossword:
+ return 'CrosswordDesign';
}
};
diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx
index a220303bfc8..8bfe959409e 100644
--- a/dotcom-rendering/src/lib/renderElement.tsx
+++ b/dotcom-rendering/src/lib/renderElement.tsx
@@ -9,6 +9,7 @@ import { CartoonComponent } from '../components/CartoonComponent';
import { ChartAtom } from '../components/ChartAtom.importable';
import { CodeBlockComponent } from '../components/CodeBlockComponent';
import { CommentBlockComponent } from '../components/CommentBlockComponent';
+import { Crossword } from '../components/Crossword.importable';
import { DividerBlockComponent } from '../components/DividerBlockComponent';
import { DocumentBlockComponent } from '../components/DocumentBlockComponent.importable';
import { EmailSignUpWrapper } from '../components/EmailSignUpWrapper';
@@ -844,6 +845,14 @@ export const renderElement = ({
case 'model.dotcomrendering.pageElements.DisclaimerBlockElement': {
return ;
}
+ case 'model.dotcomrendering.pageElements.CrosswordElement':
+ return (
+
+
+
+
+
+ );
case 'model.dotcomrendering.pageElements.AudioBlockElement':
case 'model.dotcomrendering.pageElements.ContentAtomBlockElement':
case 'model.dotcomrendering.pageElements.GenericAtomBlockElement':
diff --git a/dotcom-rendering/src/server/handler.article.web.ts b/dotcom-rendering/src/server/handler.article.web.ts
index f98084f570a..25d071303f1 100644
--- a/dotcom-rendering/src/server/handler.article.web.ts
+++ b/dotcom-rendering/src/server/handler.article.web.ts
@@ -3,7 +3,7 @@ import { Standard as ExampleArticle } from '../../fixtures/generated/fe-articles
import { decideFormat } from '../lib/articleFormat';
import { enhanceBlocks } from '../model/enhanceBlocks';
import { validateAsArticleType, validateAsBlock } from '../model/validate';
-import { enhanceArticleType } from '../types/article';
+import { enhanceArticleType, enhanceCrossword } from '../types/article';
import type { FEBlocksRequest } from '../types/frontend';
import { makePrefetchHeader } from './lib/header';
import { recordTypeAndPlatform } from './lib/logging-store';
@@ -54,6 +54,18 @@ export const handleInteractive: RequestHandler = ({ body }, res) => {
res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html);
};
+export const handleCrossword: RequestHandler = ({ body }, res) => {
+ recordTypeAndPlatform('crossword', 'web');
+
+ const article = enhanceArticleType(body, 'Web');
+ const crosswordArticle = enhanceCrossword(article);
+ const { html, prefetchScripts } = renderHtml({
+ article: crosswordArticle,
+ });
+
+ res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html);
+};
+
export const handleBlocks: RequestHandler = ({ body }, res) => {
recordTypeAndPlatform('blocks');
const {
diff --git a/dotcom-rendering/src/server/server.dev.ts b/dotcom-rendering/src/server/server.dev.ts
index 0e9c0fb3762..1df15031170 100644
--- a/dotcom-rendering/src/server/server.dev.ts
+++ b/dotcom-rendering/src/server/server.dev.ts
@@ -10,6 +10,7 @@ import {
handleArticle,
handleArticleJson,
handleBlocks,
+ handleCrossword,
handleInteractive,
} from './handler.article.web';
import { handleEditionsCrossword } from './handler.editionsCrossword';
@@ -71,6 +72,8 @@ export const devServer = (): Handler => {
return handleInteractive(req, res, next);
case 'AMPInteractive':
return handleAMPArticle(req, res, next);
+ case 'Crossword':
+ return handleCrossword(req, res, next);
case 'Blocks':
return handleBlocks(req, res, next);
case 'Front':
diff --git a/dotcom-rendering/src/server/server.prod.ts b/dotcom-rendering/src/server/server.prod.ts
index 7704baaeed3..27cc8e39fa6 100644
--- a/dotcom-rendering/src/server/server.prod.ts
+++ b/dotcom-rendering/src/server/server.prod.ts
@@ -18,6 +18,7 @@ import {
handleArticleJson,
handleArticlePerfTest,
handleBlocks,
+ handleCrossword,
handleInteractive,
} from './handler.article.web';
import { handleEditionsCrossword } from './handler.editionsCrossword';
@@ -67,6 +68,7 @@ export const prodServer = (): void => {
app.post('/AMPArticle', logRenderTime, handleAMPArticle);
app.post('/Interactive', logRenderTime, handleInteractive);
app.post('/AMPInteractive', logRenderTime, handleAMPArticle);
+ app.post('/Crossword', logRenderTime, handleCrossword);
app.post('/Blocks', logRenderTime, handleBlocks);
app.post('/Front', logRenderTime, handleFront);
app.post('/FrontJSON', logRenderTime, handleFrontJson);
diff --git a/dotcom-rendering/src/types/article.ts b/dotcom-rendering/src/types/article.ts
index e1875c1f123..a15e90c8398 100644
--- a/dotcom-rendering/src/types/article.ts
+++ b/dotcom-rendering/src/types/article.ts
@@ -1,4 +1,9 @@
-import { type ArticleFormat, decideFormat } from '../lib/articleFormat';
+import { randomUUID } from 'node:crypto';
+import {
+ ArticleDesign,
+ type ArticleFormat,
+ decideFormat,
+} from '../lib/articleFormat';
import type { ImageForAppsLightbox } from '../model/appsLightboxImages';
import { appsLightboxImages } from '../model/appsLightboxImages';
import { buildLightboxImages } from '../model/buildLightboxImages';
@@ -30,6 +35,40 @@ export type Article = {
frontendData: ArticleDeprecated;
};
+export const enhanceCrossword = (article: Article): Article => {
+ if (article.frontendData.crossword) {
+ const element = {
+ _type: 'model.dotcomrendering.pageElements.CrosswordElement' as const,
+ crossword: article.frontendData.crossword,
+ };
+ return {
+ ...article,
+ format: { ...article.format, design: ArticleDesign.Crossword },
+ frontendData: {
+ ...article.frontendData,
+ blocks: [
+ {
+ id: randomUUID(),
+ elements: [element],
+ attributes: {
+ pinned: false,
+ keyEvent: false,
+ summary: false,
+ },
+ primaryDateLine:
+ article.frontendData.webPublicationDateDisplay,
+ secondaryDateLine:
+ article.frontendData
+ .webPublicationSecondaryDateDisplay,
+ },
+ ],
+ },
+ };
+ }
+
+ throw new TypeError('article did not contain a crossword');
+};
+
export const enhanceArticleType = (
data: FEArticleType,
renderingTarget: RenderingTarget,
diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts
index d481eacf9c8..c809556a4bf 100644
--- a/dotcom-rendering/src/types/content.ts
+++ b/dotcom-rendering/src/types/content.ts
@@ -732,6 +732,12 @@ interface WitnessTypeBlockElement extends ThirdPartyEmbeddedContent {
| WitnessTypeDataVideo
| WitnessTypeDataText;
}
+
+export interface CrosswordElement {
+ _type: 'model.dotcomrendering.pageElements.CrosswordElement';
+ crossword: GuardianCrossword & { instructions?: string };
+}
+
export type FEElement =
| AdPlaceholderBlockElement
| AudioAtomBlockElement
@@ -791,7 +797,8 @@ export type FEElement =
| VideoYoutubeBlockElement
| VineBlockElement
| YoutubeBlockElement
- | WitnessTypeBlockElement;
+ | WitnessTypeBlockElement
+ | CrosswordElement;
// -------------------------------------
// Misc
diff --git a/dotcom-rendering/src/types/frontend.ts b/dotcom-rendering/src/types/frontend.ts
index 2f91fe527d7..42b66f9b773 100644
--- a/dotcom-rendering/src/types/frontend.ts
+++ b/dotcom-rendering/src/types/frontend.ts
@@ -126,6 +126,7 @@ export interface FEArticleType {
showTableOfContents: boolean;
lang?: string;
isRightToLeftLang?: boolean;
+ crossword?: GuardianCrossword & { instructions: string };
}
type PageTypeType = {
@@ -202,7 +203,8 @@ export type FEDesign =
| 'FullPageInteractiveDesign'
| 'NewsletterSignupDesign'
| 'TimelineDesign'
- | 'ProfileDesign'; // FEDisplay is the display information passed through from frontend (originating in the capi scala client) and dictates the displaystyle of the content e.g. Immersive
+ | 'ProfileDesign'
+ | 'CrosswordDesign'; // FEDisplay is the display information passed through from frontend (originating in the capi scala client) and dictates the displaystyle of the content e.g. Immersive
// https://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Display.scala
export type FEDisplay =
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b64bb6a0eeb..c23796ef371 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -364,6 +364,9 @@ importers:
'@guardian/react-crossword':
specifier: 2.0.2
version: 2.0.2
+ '@guardian/react-crossword-next':
+ specifier: npm:@guardian/react-crossword@0.0.0-canary-20241209150926
+ version: /@guardian/react-crossword@0.0.0-canary-20241209150926(@emotion/react@11.11.3)(@guardian/libs@19.2.1)(@guardian/source@8.0.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.3)
'@guardian/shimport':
specifier: 1.0.2
version: 1.0.2
@@ -4024,7 +4027,7 @@ packages:
'@typescript-eslint/parser': 6.18.0(eslint@8.56.0)(typescript@5.5.3)
eslint: 8.56.0
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.18.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.18.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.18.0)(eslint@8.56.0)
tslib: 2.6.2
typescript: 5.5.3
transitivePeerDependencies:
@@ -4256,6 +4259,33 @@ packages:
tslib: 2.6.2
dev: false
+ /@guardian/react-crossword@0.0.0-canary-20241209150926(@emotion/react@11.11.3)(@guardian/libs@19.2.1)(@guardian/source@8.0.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.3):
+ resolution: {integrity: sha512-nJ9vi454SqMynQ0UDz+jBmO/l7YxgVIq6Gvfyy4p/b5cnBNsxh8n4OZtIexhJx/dOhKYsMSPw/5KU8YOnVhF9A==}
+ peerDependencies:
+ '@emotion/react': ^11.11.3
+ '@guardian/libs': ^19.1.0
+ '@guardian/source': ^8.0.0
+ '@types/react': ^18.2.79
+ react: ^18.2.0
+ typescript: ~5.5.2
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ typescript:
+ optional: true
+ dependencies:
+ '@emotion/react': 11.11.3(@types/react@18.3.1)(react@18.3.1)
+ '@guardian/libs': 19.2.1(tslib@2.6.2)(typescript@5.5.3)
+ '@guardian/source': 8.0.0(@emotion/react@11.11.3)(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@5.5.3)
+ '@types/react': 18.3.1
+ react: 18.3.1
+ tslib: 2.6.2
+ typescript: 5.5.3
+ use-local-storage-state: 19.5.0(react-dom@18.3.1)(react@18.3.1)
+ transitivePeerDependencies:
+ - react-dom
+ dev: false
+
/@guardian/react-crossword@2.0.2:
resolution: {integrity: sha512-pFvCpuUH+GKz12uUzW4+Lck/ZhDWvqLodr1UwXIE7qjJCz8V4NEfuiGZkkIpVoPh+dEHTkiDQ6Ks4653KdH01g==}
dependencies:
@@ -6201,7 +6231,7 @@ packages:
react-docgen-typescript: 2.2.2(typescript@5.5.3)
tslib: 2.6.2
typescript: 5.5.3
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
transitivePeerDependencies:
- supports-color
dev: false
@@ -7762,8 +7792,8 @@ packages:
webpack: 5.x.x
webpack-cli: 5.x.x
dependencies:
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
- webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@5.0.4)(webpack@5.94.0)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.94.0)
dev: false
/@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.94.0):
@@ -7773,8 +7803,8 @@ packages:
webpack: 5.x.x
webpack-cli: 5.x.x
dependencies:
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
- webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@5.0.4)(webpack@5.94.0)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.94.0)
dev: false
/@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.0.4)(webpack@5.94.0):
@@ -7788,8 +7818,8 @@ packages:
webpack-dev-server:
optional: true
dependencies:
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
- webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@5.0.4)(webpack@5.94.0)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.94.0)
webpack-dev-server: 5.0.4(webpack-cli@5.1.4)(webpack@5.94.0)
dev: false
@@ -8297,7 +8327,7 @@ packages:
'@babel/core': 7.26.0
find-cache-dir: 4.0.0
schema-utils: 4.2.0
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
dev: false
/babel-plugin-istanbul@6.1.1:
@@ -9460,7 +9490,7 @@ packages:
postcss-modules-values: 4.0.0(postcss@8.4.47)
postcss-value-parser: 4.2.0
semver: 7.5.4
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
dev: false
/css-loader@7.1.2(webpack@5.94.0):
@@ -10446,7 +10476,7 @@ packages:
enhanced-resolve: 5.17.0
eslint: 8.56.0
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.18.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.18.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.18.0)(eslint@8.56.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.2
is-core-module: 2.15.1
@@ -11430,7 +11460,7 @@ packages:
semver: 7.5.4
tapable: 2.2.1
typescript: 5.5.3
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
dev: false
/form-data@3.0.1:
@@ -16651,7 +16681,7 @@ packages:
peerDependencies:
webpack: ^5.0.0
dependencies:
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
dev: false
/stylelint-config-recommended@14.0.0(stylelint@16.5.0):
@@ -17130,7 +17160,7 @@ packages:
semver: 7.5.4
source-map: 0.7.4
typescript: 5.5.3
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
dev: false
/ts-node@10.9.2(@swc/core@1.9.2)(@types/node@16.18.68)(typescript@5.1.6):
@@ -17603,6 +17633,17 @@ packages:
qs: 6.13.0
dev: false
+ /use-local-storage-state@19.5.0(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-sUJAyFvsmqMpBhdwaRr7GTKkkoxb6PWeNVvpBDrLuwQF1PpbJRKIbOYeLLeqJI7B3wdfFlLLCBbmOdopiSTBOw==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
@@ -17892,7 +17933,7 @@ packages:
mime-types: 2.1.35
range-parser: 1.2.1
schema-utils: 4.2.0
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
dev: false
/webpack-dev-middleware@7.2.1(webpack@5.94.0):
@@ -17910,7 +17951,7 @@ packages:
on-finished: 2.4.1
range-parser: 1.2.1
schema-utils: 4.2.0
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
dev: false
/webpack-dev-middleware@7.4.2(webpack@5.94.0):
@@ -17972,8 +18013,8 @@ packages:
serve-index: 1.9.1
sockjs: 0.3.24
spdy: 4.0.2
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
- webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@5.0.4)(webpack@5.94.0)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.94.0)
webpack-dev-middleware: 7.2.1(webpack@5.94.0)
ws: 8.17.1
transitivePeerDependencies:
@@ -18018,7 +18059,7 @@ packages:
webpack: ^5.47.0
dependencies:
tapable: 2.2.1
- webpack: 5.94.0(@swc/core@1.9.2)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.94.0(esbuild@0.18.20)(webpack-cli@5.1.4)
webpack-sources: 2.3.1
dev: false
patched: true