diff --git a/dotcom-rendering/.storybook/decorators/themeDecorator.tsx b/dotcom-rendering/.storybook/decorators/themeDecorator.tsx index f38de153fb9..ed68aedbf86 100644 --- a/dotcom-rendering/.storybook/decorators/themeDecorator.tsx +++ b/dotcom-rendering/.storybook/decorators/themeDecorator.tsx @@ -14,6 +14,7 @@ import { import type { CSSProperties } from 'react'; import type { ArticleFormat } from '../../src/lib/articleFormat'; import { storybookPaletteDeclarations as paletteDeclarations } from '../mocks/paletteDeclarations'; +import { hostedPaletteOverrides } from '../../src/lib/hostedContentStyles'; const darkStoryCss = css` background-color: ${sourcePalette.neutral[0]}; @@ -113,3 +114,33 @@ export const browserThemeDecorator = ); + +/** + * Colour scheme decorator specifically for hosted content pages, + * where the accent colour from the branding overrides some palette colours + */ +export const hostedPaletteDecorator = + (accentColour: string): Decorator => + (Story, context) => ( +
+ +
+ ); diff --git a/dotcom-rendering/src/components/HostedContentDisclaimer.stories.tsx b/dotcom-rendering/src/components/HostedContentDisclaimer.stories.tsx index 7e8b346a956..2f16330d425 100644 --- a/dotcom-rendering/src/components/HostedContentDisclaimer.stories.tsx +++ b/dotcom-rendering/src/components/HostedContentDisclaimer.stories.tsx @@ -1,13 +1,12 @@ +import { hostedPaletteDecorator } from '../../.storybook/decorators/themeDecorator'; +import preview from '../../.storybook/preview'; import { HostedContentDisclaimer } from './HostedContentDisclaimer'; import { Section } from './Section'; -export default { +const meta = preview.meta({ component: HostedContentDisclaimer, title: 'Components/HostedContentDisclaimer', -}; - -export const Default = () => { - return ( + render: () => (
{ >
- ); -}; + ), + decorators: hostedPaletteDecorator('#d90c1f'), +}); -Default.storyName = 'default'; +export const Default = meta.story({}); diff --git a/dotcom-rendering/src/components/HostedContentHeader.stories.tsx b/dotcom-rendering/src/components/HostedContentHeader.stories.tsx index bb7f49f78d7..696f494da12 100644 --- a/dotcom-rendering/src/components/HostedContentHeader.stories.tsx +++ b/dotcom-rendering/src/components/HostedContentHeader.stories.tsx @@ -1,31 +1,32 @@ import { palette as sourcePalette } from '@guardian/source/foundations'; +import { hostedPaletteDecorator } from '../../.storybook/decorators/themeDecorator'; import preview from '../../.storybook/preview'; import type { Branding } from '../types/branding'; import { HostedContentHeader } from './HostedContentHeader.island'; import type { Props as HostedContentHeaderProps } from './HostedContentHeader.island'; import { Section } from './Section'; +const branding = { + brandingType: { name: 'paid-content' }, + sponsorName: 'We Are Still In', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/16/Aug/2018/d5e82ba3-297d-473d-8362-c04f519e5fe1-WASI-logo-grey.png', + dimensions: { + width: 1250, + height: 575, + }, + link: 'https://www.wearestillin.com/', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + hostedCampaignColour: '#d90c1f', +} satisfies Branding; + const meta = preview.meta({ component: HostedContentHeader, title: 'Components/HostedContentHeader', - args: { - branding: { - brandingType: { name: 'paid-content' }, - sponsorName: 'We Are Still In', - logo: { - src: 'https://static.theguardian.com/commercial/sponsor/16/Aug/2018/d5e82ba3-297d-473d-8362-c04f519e5fe1-WASI-logo-grey.png', - dimensions: { - width: 1250, - height: 575, - }, - link: 'https://www.wearestillin.com/', - label: 'Paid for by', - }, - aboutThisLink: - 'https://www.theguardian.com/info/2016/jan/25/content-funding', - hostedCampaignColour: '#d90c1f', - } satisfies Branding, - }, + args: { branding }, render: (args: HostedContentHeaderProps) => (
), + decorators: hostedPaletteDecorator(branding.hostedCampaignColour), }); export const Default = meta.story(); diff --git a/dotcom-rendering/src/components/HostedContentOnwards.stories.tsx b/dotcom-rendering/src/components/HostedContentOnwards.stories.tsx index 713ccb47ad5..ab8c4612f45 100644 --- a/dotcom-rendering/src/components/HostedContentOnwards.stories.tsx +++ b/dotcom-rendering/src/components/HostedContentOnwards.stories.tsx @@ -1,29 +1,20 @@ +import { hostedPaletteDecorator } from '../../.storybook/decorators/themeDecorator'; +import preview from '../../.storybook/preview'; import { hostedOnwardsTrails } from '../../fixtures/manual/onwardsTrails'; import { HostedContentOnwards } from './HostedContentOnwards'; -export default { +const meta = preview.meta({ component: HostedContentOnwards, title: 'Components/HostedContentOnwards', -}; + args: { + trails: hostedOnwardsTrails, + brandName: 'TrendAI', + }, + render: (args) => , +}); -export const Default = () => { - return ( - - ); -}; +export const Default = meta.story({}); -Default.storyName = 'default'; - -export const WithAccentColour = () => { - return ( - - ); -}; - -WithAccentColour.storyName = 'with accent colour'; +export const WithAccentColour = meta.story({ + decorators: hostedPaletteDecorator('#d90c1f'), +}); diff --git a/dotcom-rendering/src/components/HostedContentPage.tsx b/dotcom-rendering/src/components/HostedContentPage.tsx index 860d2c1cf48..b292590caf6 100644 --- a/dotcom-rendering/src/components/HostedContentPage.tsx +++ b/dotcom-rendering/src/components/HostedContentPage.tsx @@ -75,9 +75,18 @@ export const HostedContentPage = (props: WebProps | AppProps) => { const { darkModeAvailable } = useConfig(); const format = { design, display, theme }; + const { branding } = + frontendData.commercialProperties[frontendData.editionId]; + return ( - + {isWeb && } ( +
+ {Story()} +
+ ), +} satisfies Story; diff --git a/dotcom-rendering/src/layouts/HostedArticleLayout.stories.tsx b/dotcom-rendering/src/layouts/HostedArticleLayout.stories.tsx index 91def812c32..639bdb67da8 100644 --- a/dotcom-rendering/src/layouts/HostedArticleLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/HostedArticleLayout.stories.tsx @@ -1,3 +1,4 @@ +import { hostedPaletteDecorator } from '../../.storybook/decorators/themeDecorator'; import { allModes } from '../../.storybook/modes'; import preview from '../../.storybook/preview'; import { hostedArticle } from '../../fixtures/manual/hostedArticle'; @@ -8,8 +9,8 @@ import { ArticleSpecial, } from '../lib/articleFormat'; import { customMockFetch } from '../lib/mockRESTCalls'; +import type { Article } from '../types/article'; import { enhanceArticleType } from '../types/article'; -import type { Branding } from '../types/branding'; import { HostedArticleLayout } from './HostedArticleLayout'; const mockOnwardsContentFetch = customMockFetch([ @@ -21,10 +22,14 @@ const mockOnwardsContentFetch = customMockFetch([ }, ]); +const { hostedCampaignColour = '' } = + hostedArticle.commercialProperties.UK.branding ?? {}; + const meta = preview.meta({ title: 'Layouts/HostedArticle', component: HostedArticleLayout, parameters: { + config: { darkModeAvailable: true }, chromatic: { modes: { 'light leftCol': allModes['light leftCol'], @@ -35,6 +40,7 @@ const meta = preview.meta({ global.fetch = mockOnwardsContentFetch; return ; }, + decorators: hostedPaletteDecorator(hostedCampaignColour), }); const format = { @@ -56,7 +62,7 @@ export const Apps = meta.story({ }, chromatic: { modes: { - 'light mobileMedium': allModes['light mobileMedium'], + 'vertical mobileMedium': allModes['vertical mobileMedium'], }, }, }, @@ -77,27 +83,50 @@ export const Web = meta.story({ }, }); -export const WithoutAccentColour = meta.story({ - args: { - content: { - ...webHostedArticle, - frontendData: { - ...webHostedArticle.frontendData, - commercialProperties: { - ...webHostedArticle.frontendData.commercialProperties, - UK: { - ...webHostedArticle.frontendData.commercialProperties - .UK, +const overrideBranding = (article: Article): Article => { + const brandingWithoutAccentColour = { + sponsorName: 'Croatia NTB', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/27/Mar/2026/2bd1de6c-47ce-4152-9031-66ce0b1bd96f-croatia_pos_140.png', + dimensions: { + width: 140, + height: 90, + }, + link: 'https://croatia.hr/en-gb/why-visit-croatia-in-spring/spring-is-meant-to-be-felt', + label: 'Paid for by', + }, + logoForDarkBackground: { + src: 'https://static.theguardian.com/commercial/sponsor/27/Mar/2026/6f3bc0bd-41ef-4970-9cd7-c75fd1e8a0ce-croatia_pos_140.png', + dimensions: { + width: 140, + height: 90, + }, + link: 'https://croatia.hr/en-gb/why-visit-croatia-in-spring/spring-is-meant-to-be-felt', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + hostedCampaignColour: '', + }; - branding: { - ...webHostedArticle.frontendData - .commercialProperties.UK.branding, - hostedCampaignColour: undefined, - } as Branding, - }, + return { + ...article, + frontendData: { + ...article.frontendData, + commercialProperties: { + ...article.frontendData.commercialProperties, + UK: { + ...article.frontendData.commercialProperties.UK, + branding: brandingWithoutAccentColour, }, }, }, + }; +}; + +export const WithoutAccentColour = meta.story({ + args: { + content: overrideBranding(webHostedArticle), format, renderingTarget: 'Web', }, @@ -106,33 +135,37 @@ export const WithoutAccentColour = meta.story({ renderingTarget: 'Web', }, }, + decorators: hostedPaletteDecorator(''), +}); + +const overrideMainMediaCaption = (article: Article): Article => ({ + ...article, + frontendData: { + ...article.frontendData, + mainMediaElements: [ + ...article.frontendData.mainMediaElements.map((el) => { + if ( + el._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ) { + return { + ...el, + data: { + caption: '', + credit: '', + alt: '', + }, + }; + } + return el; + }), + ], + }, }); export const WithoutMainMediaCaption = meta.story({ args: { - content: { - ...webHostedArticle, - frontendData: { - ...webHostedArticle.frontendData, - mainMediaElements: - webHostedArticle.frontendData.mainMediaElements[0] - ?._type === - 'model.dotcomrendering.pageElements.ImageBlockElement' - ? [ - { - ...webHostedArticle.frontendData - .mainMediaElements[0], - data: { - ...webHostedArticle.frontendData - .mainMediaElements[0].data, - caption: undefined, - credit: undefined, - }, - }, - ] - : webHostedArticle.frontendData.mainMediaElements, - }, - }, + content: overrideMainMediaCaption(webHostedArticle), format, renderingTarget: 'Web', }, diff --git a/dotcom-rendering/src/layouts/HostedArticleLayout.tsx b/dotcom-rendering/src/layouts/HostedArticleLayout.tsx index b1660fa5473..9128571f3f1 100644 --- a/dotcom-rendering/src/layouts/HostedArticleLayout.tsx +++ b/dotcom-rendering/src/layouts/HostedArticleLayout.tsx @@ -168,30 +168,6 @@ const sideBorders = css` } `; -/** - * Overrides palette declarations in light mode to use the accent color for the hosted content. - * @param accentColor - The accentColor to use for the hosted content in light mode. - * @returns A CSS string with the overridden palette declarations. - */ -export const overridePaletteColours = (accentColor?: string) => { - return css` - @media (prefers-color-scheme: light) { - --article-link-text: ${accentColor ?? 'inherit'}; - --article-link-text-hover: ${accentColor ?? 'inherit'}; - --article-link-border-hover: ${accentColor ?? 'inherit'}; - --accent-colour: ${accentColor ?? `${sourcePalette.neutral[38]}`}; - } - /* The following styles are to reflect the current accentColor behaviour in storybook as well so we maintain consistency */ - [data-color-scheme='dark'] & { - --article-link-text: inherit; - --article-link-text-hover: inherit; - --article-link-border-hover: inherit; - /* This CSS variable only exists in the scope of hosted content and it isn't defined in the paletteDeclarations.ts */ - --accent-colour: ${sourcePalette.neutral[86]}; - } - `; -}; - export const HostedArticleLayout = (props: WebProps | AppProps) => { const { content: { frontendData }, @@ -244,10 +220,7 @@ export const HostedArticleLayout = (props: WebProps | AppProps) => { ) : null} -
+
{ ) : null}
; }, + decorators: hostedPaletteDecorator(hostedCampaignColour), }); const format = { @@ -55,7 +61,7 @@ export const Apps = meta.story({ }, chromatic: { modes: { - 'light mobileMedium': allModes['light mobileMedium'], + 'vertical mobileMedium': allModes['vertical mobileMedium'], }, }, }, diff --git a/dotcom-rendering/src/layouts/HostedVideoLayout.tsx b/dotcom-rendering/src/layouts/HostedVideoLayout.tsx index 0c414ea6ed2..50f4e50180b 100644 --- a/dotcom-rendering/src/layouts/HostedVideoLayout.tsx +++ b/dotcom-rendering/src/layouts/HostedVideoLayout.tsx @@ -26,7 +26,6 @@ import type { Article } from '../types/article'; import type { Block } from '../types/blocks'; import type { FEElement } from '../types/content'; import type { RenderingTarget } from '../types/renderingTarget'; -import { overridePaletteColours } from './HostedArticleLayout'; import { Stuck } from './lib/stickiness'; interface Props { @@ -206,10 +205,7 @@ export const HostedVideoLayout = (props: WebProps | AppProps) => { ) : null} -
+
{ + switch (colourScheme) { + case 'light': + return css` + --article-link-text: ${accentColor ?? 'inherit'}; + --article-link-text-hover: ${accentColor ?? 'inherit'}; + --article-link-border-hover: ${accentColor ?? 'inherit'}; + --lightbox-divider: ${accentColor ?? 'inherit'}; + /* + * This CSS variable only exists in the scope of hosted content + * and it isn't defined in the paletteDeclarations.ts + */ + --accent-colour: ${accentColor ?? + `${sourcePalette.neutral[38]}`}; + `; + case 'dark': + return css` + --article-link-text: inherit; + --article-link-text-hover: inherit; + --article-link-border-hover: inherit; + --lightbox-divider: ${accentColor ?? 'inherit'}; + /* + * This CSS variable only exists in the scope of hosted content + * and it isn't defined in the paletteDeclarations.ts + */ + --accent-colour: ${sourcePalette.neutral[86]}; + `; + } +}; + +export const hostedContentStyleOverrides = ( + darkModeAvailable: boolean, + accentColour?: string, +): SerializedStyles => { + return css` + /* Brute force fix for the fixed hosted header causing odd behaviour on skip to main content */ + ${from.tablet} { + scroll-padding-top: 250px; + } + + ${hostedPaletteOverrides('light', accentColour)} + + ${darkModeAvailable + ? css` + @media (prefers-color-scheme: dark) { + ${hostedPaletteOverrides('dark', accentColour)} + } + ` + : ''} + `; +}; diff --git a/dotcom-rendering/src/lib/rootStyles.ts b/dotcom-rendering/src/lib/rootStyles.ts index 9738d5ca834..54c8840a4e6 100644 --- a/dotcom-rendering/src/lib/rootStyles.ts +++ b/dotcom-rendering/src/lib/rootStyles.ts @@ -1,24 +1,12 @@ import { css, type SerializedStyles } from '@emotion/react'; import { focusHalo, - from, palette as sourcePalette, } from '@guardian/source/foundations'; import { paletteDeclarations } from '../paletteDeclarations'; import { rootAdStyles } from './adStyles'; import { type ArticleFormat, isHostedContentDesign } from './articleFormat'; - -const hostedHeaderOffset = (format: ArticleFormat) => { - if (isHostedContentDesign(format.design)) { - return css` - /* Brute force fix for the fixed hosted header causing odd behaviour on skip to main content */ - ${from.tablet} { - scroll-padding-top: 250px; - } - `; - } - return ''; -}; +import { hostedContentStyleOverrides } from './hostedContentStyles'; /** * Global styles for pages: @@ -29,6 +17,8 @@ const hostedHeaderOffset = (format: ArticleFormat) => { export const rootStyles = ( format: ArticleFormat, darkModeAvailable: boolean, + /** For hosted content only */ + hostedAccentColour?: string, ): SerializedStyles => css` :root { /* Light palette is default on all platforms */ @@ -52,8 +42,12 @@ export const rootStyles = ( } ` : ''} - ${hostedHeaderOffset(format)} + + ${isHostedContentDesign(format.design) + ? hostedContentStyleOverrides(darkModeAvailable, hostedAccentColour) + : ''} } + /* Crude but effective mechanism. Specific components may need to improve on this behaviour. */ /* The not(.src...) selector is to work with Source's FocusStyleManager. */ *:focus {