From 5f562f4f87917965042e35dbddc5ffb2bae0f3b2 Mon Sep 17 00:00:00 2001 From: Jake Lee Kennedy Date: Thu, 21 May 2026 13:30:20 +0100 Subject: [PATCH 01/12] Add article version of the subnav Co-authored-by: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> --- .../components/DirectoryPageNav.stories.tsx | 13 ++ .../src/components/DirectoryPageNav.tsx | 155 ++++++++++++++---- 2 files changed, 138 insertions(+), 30 deletions(-) diff --git a/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx b/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx index f76b9365f61..2818ff912c7 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx @@ -44,6 +44,19 @@ export const WorldCup2026MatchCenter = meta.story({ }, }); +export const WorldCup2026Article = 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', + }, + ], + }, +}); + export const OtherCompetition = meta.story({ args: { pageId: 'football/premierleague/table', diff --git a/dotcom-rendering/src/components/DirectoryPageNav.tsx b/dotcom-rendering/src/components/DirectoryPageNav.tsx index 36e8bfd0f33..d7cec42d2fc 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.tsx @@ -14,6 +14,7 @@ import { grid } from '../grid'; import { generateImageURL } from '../lib/image'; import { useBetaAB } from '../lib/useAB'; import { worldCup2026PageIds } from '../lib/worldCup2026'; +import { palette as themePalette } from '../palette'; import type { TagType } from '../types/tag'; type Props = { @@ -25,10 +26,12 @@ interface DirectoryPageNavConfig { pageIds: string[]; tagIds: string[]; textColor: string; + textHoverColor?: string; backgroundColor: string; titleIcon?: React.ReactElement; title: { label: string; id: string }; links: Array<{ label: string; id: string }>; + showHeader: boolean; backgroundImages?: { mobile: string; mobileLandscape: string; @@ -60,8 +63,53 @@ const WorldCup2026Icon = () => ( ); +// Smaller version has slightly different proportions to better fit in the nav when the header isn't shown. +const WorldCup2026IconSmall = () => ( + + + + + + +); + +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: [], @@ -72,28 +120,8 @@ const configs = [ 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', - }, - ], + showHeader: true, + links: worldCup2026Links, backgroundImages: { mobile: 'https://media.guim.co.uk/4ba0caac6d18c1fe6a5a3267b270d8c21ae6f940/0_0_750_376/750.jpg', mobileLandscape: @@ -106,6 +134,21 @@ 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: themePalette('--masthead-nav-link-text'), + textHoverColor: themePalette('--masthead-nav-link-text-hover'), + backgroundColor: palette.brand[400], + title: { + label: 'World Cup 2026', + id: 'football/world-cup-2026', + }, + showHeader: false, + titleIcon: , + links: worldCup2026Links, + }, // Winter Olympics 2026 { pageIds: [ @@ -121,6 +164,7 @@ const configs = [ label: 'Winter Olympics 2026', id: 'sport/winter-olympics-2026', }, + showHeader: true, links: [ { label: 'Schedule', @@ -165,6 +209,7 @@ const configs = [ label: 'Winter Paralympics 2026', id: 'sport/winter-paralympics-2026', }, + showHeader: true, links: [ { label: 'Results', @@ -257,7 +302,7 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { overflowX: 'scroll', scrollbarWidth: 'none', borderTop: '1px solid', - borderColor: palette.brand[600], + borderColor: themePalette('--masthead-nav-lines'), padding: `0 ${space[3]}px`, height: space[10], [from.mobileLandscape]: { @@ -268,7 +313,7 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { '&:after': { content: '""', position: 'sticky', - right: `-${space[3]}px`, + right: -space[3], top: 0, height: '100%', minWidth: 40, @@ -301,6 +346,30 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }, }); + const primaryLinkStyles = css({ + display: 'flex', + alignItems: 'center', + paddingRight: space[6], + '&:not(:hover)': { + color: palette.sport[600], + }, + 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: themePalette('--masthead-nav-lines'), + }, + }); + const smallLink = css({ ...textSans14Object, paddingRight: space[3], @@ -309,6 +378,13 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { color: textColor, textDecoration: 'none', whiteSpace: 'nowrap', + '&:hover': { + textDecoration: 'underline', + color: config.textHoverColor, + 'svg rect, svg circle': { + fill: config.textHoverColor, + }, + }, }); const boldSmallLink = css({ @@ -317,13 +393,32 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { return ( + ); }; diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index ad089db43e1..f60ae0ffc5d 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -89,6 +89,7 @@ const paddedContainer = ` type VerticalRuleOptions = { centre?: boolean; + color?: string; }; /** @@ -121,7 +122,7 @@ const verticalRules = (options: VerticalRuleOptions = {}): string => ` top: 0; bottom: 0; width: 1px; - background-color: ${palette('--article-border')}; + background-color: ${options.color ?? palette('--article-border')}; content: ''; } From f8c1ecf1edb7b7320b85afbda219b46ad58e106c Mon Sep 17 00:00:00 2001 From: Jake Lee Kennedy Date: Fri, 22 May 2026 13:04:48 +0100 Subject: [PATCH 06/12] setup different theme for apps --- .../components/DirectoryPageNav.stories.tsx | 38 +++- .../src/components/DirectoryPageNav.tsx | 180 ++++++++++++------ 2 files changed, 155 insertions(+), 63 deletions(-) diff --git a/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx b/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx index 2818ff912c7..fe668843ca7 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx @@ -3,6 +3,9 @@ import preview from '../../.storybook/preview'; import { BetaABTests } from '../experiments/lib/beta-ab-tests'; import { setBetaABTests } from '../lib/useAB'; import { DirectoryPageNav } from './DirectoryPageNav'; +import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; +import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; +import { ConfigProvider } from './ConfigContext'; const mockAB = new BetaABTests({ isServer: true, @@ -44,7 +47,7 @@ export const WorldCup2026MatchCenter = meta.story({ }, }); -export const WorldCup2026Article = meta.story({ +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: [ @@ -57,6 +60,39 @@ export const WorldCup2026Article = meta.story({ }, }); +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', diff --git a/dotcom-rendering/src/components/DirectoryPageNav.tsx b/dotcom-rendering/src/components/DirectoryPageNav.tsx index ba05dc52aa2..63ba52656ca 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.tsx @@ -20,23 +20,31 @@ import { } from '../lib/worldCup2026'; import { palette as themePalette } from '../palette'; import type { TagType } from '../types/tag'; +import { useConfig } from './ConfigContext'; +import { RenderingTarget } from '../types/renderingTarget'; type Props = { pageId: string; pageTags?: TagType[]; }; +type Color = + | string + | { + web: string; + app: string; + }; + interface DirectoryPageNavConfig { pageIds: string[]; tagIds: string[]; - textColor: string; - textHoverColor?: string; - backgroundColor: string; - containerBackgroundColor?: string; + textColor: Color; + textHoverColor?: Color; + backgroundColor: Color; titleIcon?: React.ReactElement; title: { label: string; id: string }; links: Array<{ label: string; id: string }>; - showHeader: boolean; + slimNav?: boolean; backgroundImages?: { mobile: string; mobileLandscape: string; @@ -82,7 +90,6 @@ const configs = [ id: 'football/world-cup-2026', }, titleIcon: , - showHeader: true, links: worldCup2026Links, backgroundImages: { mobile: 'https://media.guim.co.uk/4ba0caac6d18c1fe6a5a3267b270d8c21ae6f940/0_0_750_376/750.jpg', @@ -100,15 +107,20 @@ const configs = [ { pageIds: [] as string[], tagIds: ['football/world-cup-2026'], - textColor: themePalette('--masthead-nav-link-text'), + textColor: { + web: themePalette('--masthead-nav-link-text'), + app: themePalette('--article-text'), + }, textHoverColor: themePalette('--masthead-nav-link-text-hover'), - backgroundColor: palette.brand[400], - containerBackgroundColor: palette.brand[400], + backgroundColor: { + web: themePalette('--masthead-nav-background'), + app: themePalette('--article-background'), + }, title: { label: 'World Cup 2026', id: 'football/world-cup-2026', }, - showHeader: false, + slimNav: true, titleIcon: , links: worldCup2026Links, }, @@ -127,7 +139,6 @@ const configs = [ label: 'Winter Olympics 2026', id: 'sport/winter-olympics-2026', }, - showHeader: true, links: [ { label: 'Schedule', @@ -172,7 +183,6 @@ const configs = [ label: 'Winter Paralympics 2026', id: 'sport/winter-paralympics-2026', }, - showHeader: true, links: [ { label: 'Results', @@ -201,7 +211,20 @@ const configs = [ }, ] satisfies DirectoryPageNavConfig[]; +const getColour = (color: Color, 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 isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; + const ab = useBetaAB(); const config = configs.find( @@ -223,23 +246,34 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { return null; } - const { textColor, backgroundColor } = config; + const { + textColor: configTextColor, + backgroundColor: configBackgroundColour, + slimNav, + } = config; + + const backgroundColor = getColour(configBackgroundColour, renderingTarget); - const container = (backgroundColor: string) => - css({ - backgroundColor, - }); + const textColor = getColour(configTextColor, renderingTarget); + + const container = css({ + backgroundColor: slimNav ? backgroundColor : 'transparent', + }); const nav = css({ backgroundColor, + '&': css(grid.paddedContainer), + alignContent: 'space-between', + position: 'relative', + }); + + const navWeb = css({ '&': css( grid.paddedContainer, grid.verticalRules({ color: themePalette('--masthead-nav-lines'), }), ), - alignContent: 'space-between', - position: 'relative', }); const largeLinkStyles = css({ @@ -267,37 +301,38 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }, }); - const list = (hasHeader = true) => - css({ - '&': css(grid.column.all), - display: 'flex', - alignItems: 'center', - position: 'relative', - overflowX: 'scroll', - scrollbarWidth: 'none', - borderTop: hasHeader ? '1px solid' : undefined, - borderBottom: '1px solid', - borderColor: themePalette('--masthead-nav-lines'), - padding: `0 ${space[3]}px`, - height: space[10], + const list = css({ + '&': css(grid.column.all), + display: 'flex', + alignItems: 'center', + position: 'relative', + overflowX: 'scroll', + scrollbarWidth: 'none', + borderTop: slimNav ? undefined : '1px solid', + borderBottom: '1px solid', + borderColor: isWeb + ? themePalette('--masthead-nav-lines') + : themePalette('--article-border'), + padding: `0 ${space[3]}px`, + height: space[10], + [from.mobileLandscape]: { + padding: `0 ${space[5]}px`, + 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], + top: 0, + height: '100%', + minWidth: 40, + background: `linear-gradient(to left, ${backgroundColor}, transparent)`, [from.mobileLandscape]: { - padding: `0 ${space[5]}px`, - height: hasHeader ? space[12] : space[10], + right: `-${space[5]}px`, }, - // 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], - top: 0, - height: '100%', - minWidth: 40, - background: `linear-gradient(to left, ${backgroundColor}, transparent)`, - [from.mobileLandscape]: { - right: `-${space[5]}px`, - }, - }, - }); + }, + }); const listItem = css({ position: 'relative', @@ -313,21 +348,12 @@ 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], - '&:not(:hover)': { - color: palette.sport[600], - }, svg: { marginRight: space[2], }, @@ -345,6 +371,30 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }, }); + const primaryLinkHoverStylesWeb = css({ + '&:not(:hover)': { + color: palette.sport[600], + 'svg rect, svg circle': { + fill: palette.sport[600], + }, + }, + [from.desktop]: { + '&:hover': { + textDecoration: 'underline', + color: themePalette('--masthead-nav-link-text-hover'), + }, + }, + }); + + const primaryLinkHoverStylesApp = css({ + '&:not(:hover)': { + color: palette.sport[400], + 'svg rect, svg circle': { + fill: palette.sport[400], + }, + }, + }); + const smallLink = css({ ...textSans14Object, paddingRight: space[3], @@ -353,6 +403,9 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { color: textColor, textDecoration: 'none', whiteSpace: 'nowrap', + }); + + const smallLinkWeb = css({ '&:hover': { textDecoration: 'underline', color: config.textHoverColor, @@ -367,9 +420,9 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }); return ( -
-