diff --git a/dotcom-rendering/src/components/DirectoryPageNav.tsx b/dotcom-rendering/src/components/DirectoryPageNav.island.tsx similarity index 60% rename from dotcom-rendering/src/components/DirectoryPageNav.tsx rename to dotcom-rendering/src/components/DirectoryPageNav.island.tsx index 36e8bfd0f33..a89b7254609 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.island.tsx @@ -11,25 +11,54 @@ import { textSansBold14Object, } from '@guardian/source/foundations'; import { grid } from '../grid'; +import { getInteractionClient } from '../lib/bridgetApi'; import { generateImageURL } from '../lib/image'; import { useBetaAB } from '../lib/useAB'; import { worldCup2026PageIds } from '../lib/worldCup2026'; +import { + WorldCup2026Icon, + WorldCup2026IconSmall, +} from '../lib/WorldCup2026Icons'; +import { palette as themePalette } from '../palette'; +import type { RenderingTarget } from '../types/renderingTarget'; import type { TagType } from '../types/tag'; +import { useConfig } from './ConfigContext'; type Props = { pageId: string; pageTags?: TagType[]; }; -interface DirectoryPageNavConfig { +type PlatformColor = + | string + | { + web: string; + app: string; + }; + +interface SlimDirectoryPageNavConfig { pageIds: string[]; tagIds: string[]; - textColor: string; - backgroundColor: string; + textColor: PlatformColor; + primaryLinkColor: PlatformColor; + textHoverColor: PlatformColor; + backgroundColor: PlatformColor; + borderColor: PlatformColor; titleIcon?: React.ReactElement; title: { label: string; id: string }; links: Array<{ label: string; id: string }>; - backgroundImages?: { + slimNav: true; +} + +interface FullDirectoryPageNavConfig { + pageIds: string[]; + tagIds: string[]; + textColor: PlatformColor; + backgroundColor: PlatformColor; + titleIcon?: React.ReactElement; + title: { label: string; id: string }; + links: Array<{ label: string; id: string }>; + backgroundImages: { mobile: string; mobileLandscape: string; phablet: string; @@ -37,63 +66,50 @@ interface DirectoryPageNavConfig { desktop: string; wide: string; }; + slimNav?: never; } -const WorldCup2026Icon = () => ( - - - - - - -); +type DirectoryPageNavConfig = + | SlimDirectoryPageNavConfig + | FullDirectoryPageNavConfig; + +const worldCup2026Links = [ + { + label: 'Match centre', + id: 'football/world-cup-2026/overview', + }, + { + label: 'Player guide', + id: '', + }, + { + label: 'Bracketology', + id: '', + }, + { + label: 'Golden boot', + id: '', + }, + { + label: 'More football', + id: 'football', + }, +]; const configs = [ - // World Cup 2026 + // World Cup 2026 Fronts { pageIds: worldCup2026PageIds, tagIds: [], textColor: palette.neutral[100], backgroundColor: palette.brand[400], + borderColor: themePalette('--masthead-nav-lines'), title: { label: 'World Cup', id: 'football/world-cup-2026', }, titleIcon: , - links: [ - { - label: 'Match centre', - id: 'football/world-cup-2026/overview', - }, - { - label: 'Player guide', - id: '', - }, - { - label: 'Bracketology', - id: '', - }, - { - label: 'Golden boot', - id: '', - }, - { - label: 'More football', - id: 'football', - }, - ], + links: worldCup2026Links, backgroundImages: { mobile: 'https://media.guim.co.uk/4ba0caac6d18c1fe6a5a3267b270d8c21ae6f940/0_0_750_376/750.jpg', mobileLandscape: @@ -106,6 +122,35 @@ const configs = [ wide: 'https://media.guim.co.uk/4e44f9a88fcc9a3b1b5294f7e581644baa75c904/0_0_2600_276/2600.jpg', }, }, + // World Cup 2026 Articles + { + pageIds: [] as string[], + tagIds: ['football/world-cup-2026'], + textColor: { + web: themePalette('--masthead-nav-link-text'), + app: themePalette('--article-text'), + }, + primaryLinkColor: { + web: palette.sport[600], + app: themePalette('--apps-directory-page-nav-primary-link-color'), + }, + textHoverColor: themePalette('--masthead-nav-link-text-hover'), + backgroundColor: { + web: themePalette('--masthead-nav-background'), + app: themePalette('--article-background'), + }, + borderColor: { + web: themePalette('--masthead-nav-lines'), + app: themePalette('--article-border'), + }, + title: { + label: 'World Cup 2026', + id: 'football/world-cup-2026', + }, + slimNav: true, + titleIcon: , + links: worldCup2026Links, + }, // Winter Olympics 2026 { pageIds: [ @@ -193,7 +238,22 @@ const configs = [ }, ] satisfies DirectoryPageNavConfig[]; +const getPlatformColour = ( + color: PlatformColor, + renderingTarget: RenderingTarget, +): string => { + if (typeof color === 'string') { + return color; + } + + return renderingTarget === 'Web' ? color.web : color.app; +}; + export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { + const { renderingTarget } = useConfig(); + + const isApps = renderingTarget === 'Apps'; + const ab = useBetaAB(); const config = configs.find( @@ -215,7 +275,36 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { return null; } - const { textColor, backgroundColor } = config; + const { slimNav } = config; + + const includeHeader = !slimNav; + + const onTouchStart = async () => { + await getInteractionClient().disableArticleSwipe(true); + }; + + const onTouchEnd = async () => { + await getInteractionClient().disableArticleSwipe(false); + }; + + const backgroundColor = getPlatformColour( + config.backgroundColor, + renderingTarget, + ); + + const textColor = getPlatformColour(config.textColor, renderingTarget); + + const primaryLinkColor = config.primaryLinkColor + ? getPlatformColour(config.primaryLinkColor, renderingTarget) + : textColor; + + const borderColor = + config.borderColor && + getPlatformColour(config.borderColor, renderingTarget); + + const container = css({ + backgroundColor: slimNav ? backgroundColor : 'transparent', + }); const nav = css({ backgroundColor, @@ -224,6 +313,15 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { position: 'relative', }); + const navWeb = css({ + '&': css( + grid.paddedContainer, + grid.verticalRules({ + color: borderColor, + }), + ), + }); + const largeLinkStyles = css({ position: 'absolute', top: space[3], @@ -256,19 +354,20 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { position: 'relative', overflowX: 'scroll', scrollbarWidth: 'none', - borderTop: '1px solid', - borderColor: palette.brand[600], + borderTop: slimNav ? undefined : '1px solid', + borderBottom: '1px solid', + borderColor, padding: `0 ${space[3]}px`, height: space[10], [from.mobileLandscape]: { padding: `0 ${space[5]}px`, - height: space[12], + height: slimNav ? space[10] : space[12], }, // This creates a gradient fade on the right side to indicate that there's more to scroll for. '&:after': { content: '""', position: 'sticky', - right: `-${space[3]}px`, + right: -space[3], top: 0, height: '100%', minWidth: 40, @@ -293,10 +392,41 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { backgroundColor: textColor, transition: 'height 0.3s ease-in-out, opacity 0.05s 0.1s linear', }, - [from.desktop]: { - '&:hover a': { - textDecoration: 'underline', - color: 'var(--masthead-nav-link-text-hover)', + }); + + const primaryLinkStyles = css({ + display: 'flex', + alignItems: 'center', + paddingRight: space[6], + svg: { + marginRight: space[2], + }, + // small right border + '&::after': { + content: '""', + display: 'block', + position: 'absolute', + right: space[3], + top: '50%', + transform: 'translateY(-50%)', + width: 1, + height: space[3], + backgroundColor: borderColor, + }, + '&:not(:hover)': { + color: primaryLinkColor, + 'svg rect, svg circle': { + fill: primaryLinkColor, + }, + }, + }); + + const primaryLinkHoverStyles = css({ + '&:hover': { + textDecoration: 'none', + color: config.textHoverColor, + 'svg rect, svg circle': { + fill: config.textHoverColor, }, }, }); @@ -311,44 +441,77 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { whiteSpace: 'nowrap', }); + const smallLinkWeb = css({ + '&:hover': { + textDecoration: 'underline', + color: config.textHoverColor, + }, + }); + const boldSmallLink = css({ ...textSansBold14Object, }); return ( - + + + )} + + + + ); }; const BackgroundImage = (props: { - images: DirectoryPageNavConfig['backgroundImages']; + images: FullDirectoryPageNavConfig['backgroundImages']; }) => { - if (props.images === undefined) { - return null; - } - return ( { } }; -type Images = Exclude; +type Images = Exclude< + FullDirectoryPageNavConfig['backgroundImages'], + undefined +>; type ImageSize = keyof Images; diff --git a/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx b/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx index f76b9365f61..dbd4423b59e 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx @@ -2,7 +2,8 @@ import { allModes } from '../../.storybook/modes'; import preview from '../../.storybook/preview'; import { BetaABTests } from '../experiments/lib/beta-ab-tests'; import { setBetaABTests } from '../lib/useAB'; -import { DirectoryPageNav } from './DirectoryPageNav'; +import { ConfigProvider } from './ConfigContext'; +import { DirectoryPageNav } from './DirectoryPageNav.island'; const mockAB = new BetaABTests({ isServer: true, @@ -15,43 +16,97 @@ setBetaABTests(mockAB); const meta = preview.meta({ component: DirectoryPageNav, title: 'Components/Directory Page Nav', - parameters: { - chromatic: { - modes: { - 'light mobileMedium': allModes['light mobileMedium'], - 'light desktop': allModes['light desktop'], - 'light leftCol': allModes['light leftCol'], - }, +}); + +// So that these aren't applied to the apps stories +const webChromaticParams = { + chromatic: { + modes: { + 'light mobileMedium': allModes['light mobileMedium'], + 'light desktop': allModes['light desktop'], + 'light leftCol': allModes['light leftCol'], }, }, -}); +}; export const WomensEuro2025 = meta.story({ args: { pageId: 'football/women-s-euro-2025/table', }, + parameters: webChromaticParams, }); export const WorldCup2026 = meta.story({ args: { pageId: 'football/world-cup-2026', }, + parameters: webChromaticParams, }); export const WorldCup2026MatchCenter = meta.story({ args: { pageId: 'football/world-cup-2026/overview', }, + parameters: webChromaticParams, +}); + +export const WorldCup2026ArticleWeb = meta.story({ + args: { + pageId: 'football/2026/may/19/brazils-world-cup-squad-offers-a-hint-of-the-magical-pragmatism-of-1994', + pageTags: [ + { + id: 'football/world-cup-2026', + type: 'Topic', + title: 'World Cup 2026 Fronts', + }, + ], + }, + parameters: webChromaticParams, +}); + +export const WorldCup2026ArticleApp = meta.story({ + render: (args) => ( + + + + ), + args: { + pageId: 'football/2026/may/19/brazils-world-cup-squad-offers-a-hint-of-the-magical-pragmatism-of-1994', + pageTags: [ + { + id: 'football/world-cup-2026', + type: 'Topic', + title: 'World Cup 2026', + }, + ], + }, + parameters: { + chromatic: { + modes: { + 'apps light': allModes['light'], + 'apps dark': allModes['dark'], + }, + }, + }, }); export const OtherCompetition = meta.story({ args: { pageId: 'football/premierleague/table', }, + parameters: webChromaticParams, }); export const WinterOlympics = meta.story({ args: { pageId: 'sport/winter-olympics-2026', }, + parameters: webChromaticParams, }); diff --git a/dotcom-rendering/src/components/DirectoryPageNavIsland.tsx b/dotcom-rendering/src/components/DirectoryPageNavIsland.tsx new file mode 100644 index 00000000000..c1ce249a293 --- /dev/null +++ b/dotcom-rendering/src/components/DirectoryPageNavIsland.tsx @@ -0,0 +1,14 @@ +import type { TagType } from '../types/tag'; +import { DirectoryPageNav } from './DirectoryPageNav.island'; +import { Island } from './Island'; + +type Props = { + pageId: string; + pageTags?: TagType[]; +}; + +export const DirectoryPageNavIsland = (args: Props) => ( + + + +); diff --git a/dotcom-rendering/src/components/FootballMatchesPage.tsx b/dotcom-rendering/src/components/FootballMatchesPage.tsx index 83436601ee9..b1f03f0debf 100644 --- a/dotcom-rendering/src/components/FootballMatchesPage.tsx +++ b/dotcom-rendering/src/components/FootballMatchesPage.tsx @@ -12,7 +12,7 @@ import type { Result } from '../lib/result'; import { palette } from '../palette'; import type { FootballMatchListPageKind, Region } from '../sportDataPage'; import { AdSlot } from './AdSlot.web'; -import { DirectoryPageNav } from './DirectoryPageNav'; +import { DirectoryPageNavIsland } from './DirectoryPageNavIsland'; import { FootballCompetitionSelect } from './FootballCompetitionSelect'; import { FootballMatchList } from './FootballMatchList'; @@ -59,7 +59,8 @@ export const FootballMatchesPage = ({ pageId, }: Props) => ( <> - + +
( <> - + +
` top: 0; bottom: 0; width: 1px; - background-color: ${palette('--article-border')}; + background-color: ${options.color ?? palette('--article-border')}; content: ''; } diff --git a/dotcom-rendering/src/layouts/AudioLayout.tsx b/dotcom-rendering/src/layouts/AudioLayout.tsx index d3b68d8c2ba..15265ceb77a 100644 --- a/dotcom-rendering/src/layouts/AudioLayout.tsx +++ b/dotcom-rendering/src/layouts/AudioLayout.tsx @@ -20,6 +20,7 @@ import { ArticleTitle } from '../components/ArticleTitle'; import { AudioPlayerWrapper } from '../components/AudioPlayerWrapper.island'; import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.island'; +import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; @@ -45,6 +46,8 @@ import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; import { parse } from '../lib/slot-machine-flags'; +import { useBetaAB } from '../lib/useAB'; +import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; import type { ArticleDeprecated } from '../types/article'; @@ -164,6 +167,12 @@ export const AudioLayout = (props: WebProps | AppProps) => { const isLabs = format.theme === ArticleSpecial.Labs; + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(article); return ( @@ -191,7 +200,7 @@ export const AudioLayout = (props: WebProps | AppProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={!isLabs} + showSubNav={!isLabs && !isWorldCup2026} showSlimNav={false} hasPageSkinContentSelfConstrain={true} pageId={article.pageId} @@ -227,6 +236,11 @@ export const AudioLayout = (props: WebProps | AppProps) => { )} + +
{ const contributionsServiceUrl = getContributionsServiceUrl(article); + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(article); return ( @@ -325,7 +334,7 @@ export const CommentLayout = (props: WebProps | AppsProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} @@ -343,6 +352,10 @@ export const CommentLayout = (props: WebProps | AppsProps) => { )} +
{ /> )} - + {filteredCollections.map((collection, index) => { // Backfills should be added to the end of any curated content diff --git a/dotcom-rendering/src/layouts/GalleryLayout.tsx b/dotcom-rendering/src/layouts/GalleryLayout.tsx index b851b5aa5c9..42b7bdb0a2a 100644 --- a/dotcom-rendering/src/layouts/GalleryLayout.tsx +++ b/dotcom-rendering/src/layouts/GalleryLayout.tsx @@ -16,6 +16,7 @@ import { ArticleMetaApps } from '../components/ArticleMeta.apps'; import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { Caption } from '../components/Caption'; +import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { FetchMoreGalleriesData } from '../components/FetchMoreGalleriesData.island'; import { Footer } from '../components/Footer'; @@ -45,6 +46,8 @@ import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideMainMediaCaption } from '../lib/decide-caption'; import type { EditionId } from '../lib/edition'; +import { useBetaAB } from '../lib/useAB'; +import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette } from '../palette'; import type { ArticleDeprecated, Gallery } from '../types/article'; @@ -105,6 +108,12 @@ export const GalleryLayout = (props: WebProps | AppProps) => { const isLabs = format.theme === ArticleSpecial.Labs; + const ab = useBetaAB(); + + const isWorldCup2026 = + frontendData.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(frontendData); const showMerchandisingHigh = isWeb && renderAds && !isLabs; @@ -124,6 +133,7 @@ export const GalleryLayout = (props: WebProps | AppProps) => { contributionsServiceUrl={contributionsServiceUrl} pageId={frontendData.pageId} tagIds={frontendData.tags.map((tag) => tag.id)} + showSlimNav={!isWorldCup2026} /> ) : null} { )} +
(
{props.renderAds ? ( @@ -404,7 +419,7 @@ const BannerAndMasthead = (props: { idApiUrl={props.config.idApiUrl} contributionsServiceUrl={props.contributionsServiceUrl} showSubNav={false} - showSlimNav={true} + showSlimNav={props.showSlimNav ?? true} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} pageId={props.pageId} diff --git a/dotcom-rendering/src/layouts/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/InteractiveLayout.tsx index c2f9f24deee..0fc4f979ba4 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayout.tsx @@ -18,7 +18,7 @@ import { ArticleTitle } from '../components/ArticleTitle'; import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.island'; import { DecideLines } from '../components/DecideLines'; -import { DirectoryPageNav } from '../components/DirectoryPageNav'; +import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; @@ -311,7 +311,7 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { )}
- diff --git a/dotcom-rendering/src/layouts/LiveLayout.tsx b/dotcom-rendering/src/layouts/LiveLayout.tsx index ff64da7bd6f..353a5b570dd 100644 --- a/dotcom-rendering/src/layouts/LiveLayout.tsx +++ b/dotcom-rendering/src/layouts/LiveLayout.tsx @@ -20,6 +20,7 @@ import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { Carousel } from '../components/Carousel.island'; import { DecideLines } from '../components/DecideLines'; +import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { FilterKeyEventsToggle } from '../components/FilterKeyEventsToggle.island'; import { FootballMatchHeaderFallback } from '../components/FootballMatchHeader/FootballMatchHeaderFallback'; @@ -50,6 +51,8 @@ import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; import { getZIndex } from '../lib/getZIndex'; +import { useBetaAB } from '../lib/useAB'; +import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; import type { ArticleDeprecated } from '../types/article'; @@ -305,6 +308,12 @@ export const LiveLayout = (props: WebProps | AppsProps) => { const renderAds = canRenderAds(article); + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const isWeb = renderingTarget === 'Web'; const isApps = renderingTarget === 'Apps'; @@ -337,7 +346,7 @@ export const LiveLayout = (props: WebProps | AppsProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} @@ -354,6 +363,10 @@ export const LiveLayout = (props: WebProps | AppsProps) => { )}
+ {isWeb && renderAds && hasLiveBlogTopAd && (
{ const renderAds = canRenderAds(article); + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const avatarUrl = getSoleContributor(article.tags, article.byline) ?.bylineLargeImageUrl; @@ -303,7 +312,7 @@ export const PictureLayout = (props: WebProps | AppsProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} @@ -330,6 +339,10 @@ export const PictureLayout = (props: WebProps | AppsProps) => { )} +
{ const contributionsServiceUrl = getContributionsServiceUrl(article); + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(article); const isLabs = format.theme === ArticleSpecial.Labs; @@ -282,7 +290,7 @@ export const ShowcaseLayout = (props: WebProps | AppsProps) => { contributionsServiceUrl={ contributionsServiceUrl } - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} @@ -365,7 +373,7 @@ export const ShowcaseLayout = (props: WebProps | AppsProps) => { )} - diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 65ebfbc5994..8c181465d57 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -22,7 +22,7 @@ import { ArticleTitle } from '../components/ArticleTitle'; import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.island'; import { DecideLines } from '../components/DecideLines'; -import { DirectoryPageNav } from '../components/DirectoryPageNav'; +import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { FootballMatchHeaderWrapper } from '../components/FootballMatchHeaderWrapper.island'; import { FootballMatchInfoWrapper } from '../components/FootballMatchInfoWrapper.island'; @@ -57,6 +57,8 @@ import { decideStoryPackageTrails } from '../lib/decideTrail'; import type { EditionId } from '../lib/edition'; import { safeParseURL } from '../lib/parse'; import { parse } from '../lib/slot-machine-flags'; +import { useBetaAB } from '../lib/useAB'; +import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; import type { ArticleDeprecated } from '../types/article'; @@ -383,6 +385,12 @@ export const StandardLayout = (props: WebProps | AppProps) => { const isLabs = format.theme === ArticleSpecial.Labs; + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(article); return ( @@ -410,7 +418,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={!isLabs} + showSubNav={!isLabs && !isWorldCup2026} showSlimNav={false} hasPageSkinContentSelfConstrain={true} pageId={article.pageId} @@ -455,7 +463,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { )} - diff --git a/dotcom-rendering/src/layouts/TagPageLayout.tsx b/dotcom-rendering/src/layouts/TagPageLayout.tsx index 10933b47239..4a22b11253e 100644 --- a/dotcom-rendering/src/layouts/TagPageLayout.tsx +++ b/dotcom-rendering/src/layouts/TagPageLayout.tsx @@ -3,7 +3,7 @@ import { palette } from '@guardian/source/foundations'; import { Fragment } from 'react'; import { Accessibility } from '../components/Accessibility.island'; import { DecideContainerByTrails } from '../components/DecideContainerByTrails'; -import { DirectoryPageNav } from '../components/DirectoryPageNav'; +import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; import { Footer } from '../components/Footer'; import { FrontsBannerAdSlot, @@ -110,7 +110,7 @@ export const TagPageLayout = ({ tagPage, NAV }: Props) => {
- + {isAccessibilityPage && ( diff --git a/dotcom-rendering/src/lib/WorldCup2026Icons.tsx b/dotcom-rendering/src/lib/WorldCup2026Icons.tsx new file mode 100644 index 00000000000..03e5631f37b --- /dev/null +++ b/dotcom-rendering/src/lib/WorldCup2026Icons.tsx @@ -0,0 +1,42 @@ +export const WorldCup2026Icon = () => ( + + + + + + +); + +// Smaller version has slightly different proportions to better fit in the nav when the header isn't shown. +export const WorldCup2026IconSmall = () => ( + + + + + + +); diff --git a/dotcom-rendering/src/lib/worldCup2026.ts b/dotcom-rendering/src/lib/worldCup2026.ts index e582fac57a5..14f86d253d4 100644 --- a/dotcom-rendering/src/lib/worldCup2026.ts +++ b/dotcom-rendering/src/lib/worldCup2026.ts @@ -4,4 +4,4 @@ export const worldCup2026PageIds = [ 'football/world-cup-2026/overview', ]; -export const worldCupTagIds = []; +export const worldCupTagId = 'football/world-cup-2026'; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index a3199607e75..2edf2b45a1d 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -6490,6 +6490,14 @@ const paletteColours = { light: ageWarningWrapperBackground, dark: ageWarningWrapperBackground, }, + '--apps-directory-page-nav-background': { + light: () => sourcePalette.neutral[97], + dark: () => sourcePalette.neutral[10], + }, + '--apps-directory-page-nav-primary-link-color': { + light: () => sourcePalette.sport[400], + dark: () => sourcePalette.sport[500], + }, '--apps-epic-background': { light: appsEpicBackgroundLight, dark: appsEpicBackgroundDark,