From 4ea010bd804cac88bb26965d51c6f3ce8e791919 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Fri, 15 May 2026 21:59:10 +0100 Subject: [PATCH 01/16] Rough first pass --- dotcom-rendering/src/components/SubMeta.tsx | 9 +- dotcom-rendering/src/layouts/DecideLayout.tsx | 61 +- .../src/layouts/FullPageInteractiveLayout.tsx | 2 +- .../src/layouts/InteractiveLayout.tsx | 901 ++++++++---------- .../layouts/InteractiveLayoutDeprecated.tsx | 836 ++++++++++++++++ .../src/layouts/lib/furnitureArrangements.ts | 106 +++ 6 files changed, 1409 insertions(+), 506 deletions(-) create mode 100644 dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx create mode 100644 dotcom-rendering/src/layouts/lib/furnitureArrangements.ts diff --git a/dotcom-rendering/src/components/SubMeta.tsx b/dotcom-rendering/src/components/SubMeta.tsx index 7be376741d0..71307c19d2e 100644 --- a/dotcom-rendering/src/components/SubMeta.tsx +++ b/dotcom-rendering/src/components/SubMeta.tsx @@ -16,6 +16,7 @@ import type { BaseLinkType } from '../model/extract-nav'; import { palette } from '../palette'; import { Island } from './Island'; import { ShareButton } from './ShareButton.island'; +import { interactiveLayoutSwitchoverDate } from '../layouts/DecideLayout'; const labelStyles = (design: ArticleDesign): SerializedStyles => css` ${design === ArticleDesign.Gallery ? grid.column.centre : undefined}; @@ -225,13 +226,15 @@ export const SubMeta = ({ format.design !== ArticleDesign.Interactive && format.design !== ArticleDesign.Gallery; + const usesDeprecatedInteractiveLayout = + format.design === ArticleDesign.Interactive && + interactiveLayoutSwitchoverDate > new Date(); + return (
{ const format = { design: article.design, @@ -40,6 +43,7 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { }; const serverTime = article.serverTime; + const publicationDate = new Date(article.frontendData.webPublicationDate); switch (article.display) { case ArticleDisplay.Immersive: { @@ -113,15 +117,24 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { default: { switch (article.design) { case ArticleDesign.Interactive: - return ( - - ); - + if (publicationDate < interactiveLayoutSwitchoverDate) { + return ( + + ); + } else { + return ( + + ); + } case ArticleDesign.FullPageInteractive: { return ( { }; const serverTime = article.serverTime; + const publicationDate = new Date(article.frontendData.webPublicationDate); switch (article.display) { case ArticleDisplay.Immersive: { @@ -294,15 +308,26 @@ const DecideLayoutWeb = ({ article, NAV, renderingTarget }: WebProps) => { default: { switch (article.design) { case ArticleDesign.Interactive: - return ( - - ); + if (publicationDate < interactiveLayoutSwitchoverDate) { + return ( + + ); + } else { + return ( + + ); + } case ArticleDesign.FullPageInteractive: { return ( ( -
- {children} -
-); - -const maxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; +import { RoleType } from '../types/content'; +import { InteractivesScrollbarWidth } from '../components/InteractivesScrollbarWidth.island'; +import { InteractivesNativePlatformWrapper } from '../components/InteractivesNativePlatformWrapper.island'; +import { InteractivesDisableArticleSwipe } from '../components/InteractivesDisableArticleSwipe.island'; const stretchLines = css` ${until.phablet} { @@ -181,38 +74,63 @@ const stretchLines = css` } `; -export const temporaryBodyCopyColourOverride = css` - .content__main-column--interactive p { - /* stylelint-disable-next-line declaration-no-important */ - color: ${themePalette('--article-text')} !important; - } -`; +interface GridItemProps { + area: Area; + layoutType: LayoutType; + element?: 'div' | 'aside'; + customCss?: SerializedStyles; + children: React.ReactNode; +} -interface CommonProps { +const GridItem = ({ + area, + layoutType, + element: Element = 'div', + customCss, + children, +}: GridItemProps) => ( + + {children} + +); + +interface Props { article: ArticleDeprecated; format: ArticleFormat; renderingTarget: RenderingTarget; serverTime?: number; } -interface WebProps extends CommonProps { +interface WebProps extends Props { NAV: NavType; renderingTarget: 'Web'; } -interface AppsProps extends CommonProps { +interface AppProps extends Props { renderingTarget: 'Apps'; } -export const InteractiveLayout = (props: WebProps | AppsProps) => { +export const InteractiveLayout = (props: WebProps | AppProps) => { const { article, format, renderingTarget, serverTime } = props; const { config: { isPaidContent, host, hasSurveyAd }, editionId, } = article; - const isApps = renderingTarget === 'Apps'; const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; + + const showBodyEndSlot = + isWeb && + (parse(article.slotMachineFlags ?? '').showBodyEnd || + article.config.switches.slotBodyEnd); + + // TODO: + // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render + // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. const showComments = article.isCommentable && !isPaidContent; @@ -247,365 +165,375 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { - )} - {article.isLegacyInteractive && ( - - )} {isWeb && ( - <> -
- {renderAds && ( - -
-
- -
-
-
- )} - - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> -
- - {format.theme === ArticleSpecial.Labs && ( - +
+ {renderAds && ( +
- +
)} + tag.id)} + sectionId={article.config.section} + contentType={article.contentType} + /> +
+ )} - {renderAds && hasSurveyAd && ( - - )} - + {format.theme === ArticleSpecial.Labs && ( + +
+ +
+
)} + + {isWeb && renderAds && hasSurveyAd && ( + + )} +
-
-
+ + + - - -
- -
-
- -
- + + + + + + + + +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ {isApps ? ( + <> + + -
-
- - {format.theme === ArticleSpecial.Labs ? ( - <> - ) : ( - - )} - - -
- -
-
- - - - -
-
- -
-
-
- -
- {isApps ? ( - <> - - - - - - - - ) : ( - - )} -
-
- - - + + + - - -
-
-
- -
-
- - + )} + + + ) : ( + + )} + + + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + +
+ + + +
+
+ )} + + -
-
-
-
- + + + )} + + {showBodyEndSlot && ( + + + + )} + + + + + -
- -
- -
+ > + + + + + + + {isWeb && renderAds && (
{ {article.storyPackage && (
{ webURL={article.webURL} /> - {showComments && (
{ '--article-section-background', )} borderColour={themePalette('--article-border')} - fontColour={themePalette('--article-section-title')} > {
)}
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - {isWeb && ( <> + {props.NAV.subNavSections && ( +
+ + + +
+ )}
{ editionId={article.editionId} />
- { !!article.config.switches.remoteBanner } tags={article.tags} + host={host} /> @@ -813,19 +743,22 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { )} )} + {isApps && ( -
- - - -
+ <> +
+ + + +
+ )} ); diff --git a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx new file mode 100644 index 00000000000..cef71817073 --- /dev/null +++ b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx @@ -0,0 +1,836 @@ +import { css, Global } 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 type React from 'react'; +import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; +import { AppsFooter } from '../components/AppsFooter.island'; +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 { Border } from '../components/Border'; +import { Carousel } from '../components/Carousel.island'; +import { DecideLines } from '../components/DecideLines'; +import { DirectoryPageNav } from '../components/DirectoryPageNav'; +import { DiscussionLayout } from '../components/DiscussionLayout'; +import { Footer } from '../components/Footer'; +import { GridItem } from '../components/GridItem'; +import { HeaderAdSlot } from '../components/HeaderAdSlot'; +import { InteractivesDisableArticleSwipe } from '../components/InteractivesDisableArticleSwipe.island'; +import { InteractivesNativePlatformWrapper } from '../components/InteractivesNativePlatformWrapper.island'; +import { InteractivesScrollbarWidth } from '../components/InteractivesScrollbarWidth.island'; +import { Island } from '../components/Island'; +import { LabsHeader } from '../components/LabsHeader'; +import { MainMedia } from '../components/MainMedia'; +import { Masthead } from '../components/Masthead/Masthead'; +import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; +import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; +import { OnwardsUpper } from '../components/OnwardsUpper.island'; +import { Section } from '../components/Section'; +import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; +import { Standfirst } from '../components/Standfirst'; +import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; +import { SubMeta } from '../components/SubMeta'; +import { SubNav } from '../components/SubNav.island'; +import { type ArticleFormat, ArticleSpecial } from '../lib/articleFormat'; +import { canRenderAds } from '../lib/canRenderAds'; +import { getContributionsServiceUrl } from '../lib/contributions'; +import { decideStoryPackageTrails } from '../lib/decideTrail'; +import type { NavType } from '../model/extract-nav'; +import { palette as themePalette } from '../palette'; +import type { ArticleDeprecated } from '../types/article'; +import type { RoleType } from '../types/content'; +import type { RenderingTarget } from '../types/renderingTarget'; +import { + interactiveGlobalStyles, + interactiveLegacyClasses, +} from './lib/interactiveLegacyStyling'; +import { BannerWrapper, Stuck } from './lib/stickiness'; + +const InteractiveGrid = ({ 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; + } +`; + +export const temporaryBodyCopyColourOverride = css` + .content__main-column--interactive p { + /* stylelint-disable-next-line declaration-no-important */ + color: ${themePalette('--article-text')} !important; + } +`; + +interface CommonProps { + article: ArticleDeprecated; + format: ArticleFormat; + renderingTarget: RenderingTarget; + serverTime?: number; +} + +interface WebProps extends CommonProps { + NAV: NavType; + renderingTarget: 'Web'; +} + +interface AppsProps extends CommonProps { + renderingTarget: 'Apps'; +} + +export const InteractiveLayoutDeprecated = (props: WebProps | AppsProps) => { + const { article, format, renderingTarget, serverTime } = props; + const { + config: { isPaidContent, host, hasSurveyAd }, + editionId, + } = 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 renderAds = canRenderAds(article); + + const includesFullWidthElement = article.blocks.some((block) => + block.elements.some((element) => { + const role = + 'role' in element + ? (element.role as RoleType | 'fullWidth' | undefined) + : undefined; + return role === 'fullWidth'; + }), + ); + + return ( + <> + {includesFullWidthElement && ( + + + + )} + {isApps && ( + <> + + + + + + + + + )} + {article.isLegacyInteractive && ( + + )} + {isWeb && ( + <> +
+ {renderAds && ( + +
+
+ +
+
+
+ )} + + tag.id)} + sectionId={article.config.section} + contentType={article.contentType} + /> +
+ + {format.theme === ArticleSpecial.Labs && ( + +
+ +
+
+ )} + + {renderAds && hasSurveyAd && ( + + )} + + )} +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + {format.theme === ArticleSpecial.Labs ? ( + <> + ) : ( + + )} + + +
+ +
+
+ + + + +
+
+ +
+
+
+ +
+ {isApps ? ( + <> + + + + + + + + ) : ( + + )} +
+
+ + + + + +
+
+
+ +
+
+ + + +
+
+ +
+ +
+ +
+ +
+ + {isWeb && renderAds && ( +
+ +
+ )} + + {article.storyPackage && ( +
+ + + +
+ )} + + + + + + {showComments && ( +
+ +
+ )} + + {!isPaidContent && ( +
+ + + + + +
+ )} + + {isWeb && renderAds && ( +
+ +
+ )} +
+ + {isWeb && props.NAV.subNavSections && ( +
+ + + +
+ )} + + {isWeb && ( + <> +
+
+
+ + + + + + + + + )} + {isApps && ( +
+ + + +
+ )} + + ); +}; diff --git a/dotcom-rendering/src/layouts/lib/furnitureArrangements.ts b/dotcom-rendering/src/layouts/lib/furnitureArrangements.ts new file mode 100644 index 00000000000..d7e0409bfbf --- /dev/null +++ b/dotcom-rendering/src/layouts/lib/furnitureArrangements.ts @@ -0,0 +1,106 @@ +import { css, type SerializedStyles } from '@emotion/react'; +import { from, until } from '@guardian/source/foundations'; + +export type LayoutType = 'standard'; + +export type Area = + | 'title' + | 'headline' + | 'standfirst' + | 'main-media' + | 'meta' + | 'body' + | 'right-column'; + +type Breakpoint = 'mobile' | 'tablet' | 'desktop' | 'leftCol'; + +const breakpointQueries: Record = { + mobile: until.tablet, + tablet: from.tablet, + desktop: from.desktop, + leftCol: from.leftCol, +}; + +// Raw CSS overrides per area per breakpoint. Entries are only needed when an area +// deviates from the default: centre column, single-column mobile layout with areas +// in DOM order (main-media → title → headline → standfirst → meta → body → right-column). + +type AreaCss = Partial>; +type LayoutCssMap = Partial>; + +const standardCss: LayoutCssMap = { + title: { + tablet: 'grid-row: 1;', + leftCol: + 'grid-row: 1; grid-column: left-column-start / left-column-end;', + }, + headline: { + tablet: 'grid-row: 2;', + leftCol: 'grid-row: 1;', + }, + standfirst: { + tablet: 'grid-row: 3;', + leftCol: 'grid-row: 2;', + }, + 'main-media': { + tablet: 'grid-row: 4;', + leftCol: 'grid-row: 3;', + }, + meta: { + tablet: 'grid-row: 5;', + leftCol: + 'grid-row: 3 / span 2; grid-column: left-column-start / left-column-end;', + }, + body: { + tablet: 'grid-row: 6;', + leftCol: 'grid-row: 4;', + }, + 'right-column': { + desktop: + 'grid-row: 1 / span 6; grid-column: right-column-start / right-column-end;', + leftCol: + 'grid-row: 1 / span 4; grid-column: right-column-start / right-column-end;', + }, +}; + +const layoutCssMaps: Record = { + standard: standardCss, +}; + +/** + * Returns the Emotion CSS needed to position a single grid item — its + * default column, its row at each breakpoint, and any column overrides. + * The grid item _must_ be inside a {@link grid} module container. + * + * All items default to the centre column. Per-breakpoint overrides for + * `grid-row` and `grid-column` are applied on top via media queries, + * looked up from the plain CSS maps defined in this file. + * + * @param area - The named piece of article furniture to position (e.g. `'headline'`, `'body'`). + * @param layoutType - See {@link LayoutType}. Determines which CSS map to use for lookups. + * + * @example + * // In a React component: + *
+ */ +export const gridItemCss = ( + area: Area, + layoutType: LayoutType, +): SerializedStyles => { + const areaOverrides = layoutCssMaps[layoutType][area] ?? {}; + + const breakpointCss = Object.entries(areaOverrides).map( + ([bp, styles]) => css` + ${breakpointQueries[bp as Breakpoint]} { + ${styles} + } + `, + ); + + // All items default to the centre column; breakpoint entries above + // override grid-row and grid-column as needed. + return css` + grid-column: centre-column-start / centre-column-end; + ${breakpointCss} + `; +}; From ec3ece3eca7d9f14f3ec94d6b5e807da6551e61b Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Fri, 15 May 2026 22:35:55 +0100 Subject: [PATCH 02/16] Combine into one InteractiveLayout --- dotcom-rendering/src/layouts/DecideLayout.tsx | 62 +- .../src/layouts/FullPageInteractiveLayout.tsx | 2 +- .../src/layouts/InteractiveLayout.tsx | 1079 ++++++++++++----- .../layouts/InteractiveLayoutDeprecated.tsx | 836 ------------- dotcom-rendering/src/lib/ArticleRenderer.tsx | 1 + 5 files changed, 784 insertions(+), 1196 deletions(-) delete mode 100644 dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index bded595b5dc..95238ca468d 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -11,7 +11,6 @@ import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; -import { InteractiveLayoutDeprecated } from './InteractiveLayoutDeprecated'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; import { PictureLayout } from './PictureLayout'; @@ -117,24 +116,17 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { default: { switch (article.design) { case ArticleDesign.Interactive: - if (publicationDate < interactiveLayoutSwitchoverDate) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); case ArticleDesign.FullPageInteractive: { return ( { default: { switch (article.design) { case ArticleDesign.Interactive: - if (publicationDate < interactiveLayoutSwitchoverDate) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); case ArticleDesign.FullPageInteractive: { return ( ); -interface Props { +interface NewArticleGridProps { + article: ArticleDeprecated; + format: ArticleFormat; + branding: Branding | undefined; + contributionsServiceUrl: string; + isApps: boolean; + isWeb: boolean; + renderAds: boolean; + showBodyEndSlot: boolean; + host: string | undefined; +} + +const ArticleGrid = ({ + article, + format, + branding, + contributionsServiceUrl, + isApps, + isWeb, + renderAds, + showBodyEndSlot, + host, +}: NewArticleGridProps) => ( + /* GridItem order matters — mobile layout relies on DOM order for grid placement. + See furnitureArrangements.ts if reordering. */ +
+ + + + + + + + + + + + + +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ {isApps ? ( + <> + + + + + + {!!article.affiliateLinksDisclaimer && ( + + )} + + + ) : ( + + )} +
+ + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + +
+ + + +
+
+ )} + + + + {isApps && ( + + + + )} + + {showBodyEndSlot && ( + + + + )} + + + +
+ + + + + + + +
+); + +// --------------------------------------------------------------------------- +// Main layout +// --------------------------------------------------------------------------- + +interface CommonProps { article: ArticleDeprecated; format: ArticleFormat; renderingTarget: RenderingTarget; serverTime?: number; + useDeprecatedGrid?: boolean; } -interface WebProps extends Props { +interface WebProps extends CommonProps { NAV: NavType; renderingTarget: 'Web'; } -interface AppProps extends Props { +interface AppsProps extends CommonProps { renderingTarget: 'Apps'; } -export const InteractiveLayout = (props: WebProps | AppProps) => { - const { article, format, renderingTarget, serverTime } = props; +export const InteractiveLayout = (props: WebProps | AppsProps) => { + const { article, format, renderingTarget, serverTime, useDeprecatedGrid } = + props; const { config: { isPaidContent, host, hasSurveyAd }, editionId, } = article; - const isWeb = renderingTarget === 'Web'; const isApps = renderingTarget === 'Apps'; + const isWeb = renderingTarget === 'Web'; const showBodyEndSlot = isWeb && - (parse(article.slotMachineFlags ?? '').showBodyEnd || - article.config.switches.slotBodyEnd); - - // TODO: - // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render - // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. + ((parse(article.slotMachineFlags ?? '').showBodyEnd || + article.config.switches.slotBodyEnd) ?? + false); const showComments = article.isCommentable && !isPaidContent; @@ -165,8 +458,15 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { + {useDeprecatedGrid && ( + + )} )} + {useDeprecatedGrid && article.isLegacyInteractive && ( + + )} + {isWeb && (
{renderAds && ( @@ -190,9 +490,10 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} - showSlimNav={false} - hasPageSkinContentSelfConstrain={true} + showSubNav={!useDeprecatedGrid} + showSlimNav={useDeprecatedGrid} + hasPageSkin={useDeprecatedGrid ? false : undefined} + hasPageSkinContentSelfConstrain={!useDeprecatedGrid} pageId={article.pageId} tagIds={article.tags.map((tag) => tag.id)} sectionId={article.config.section} @@ -209,7 +510,7 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" - element="aside" + element={useDeprecatedGrid ? undefined : 'aside'} > @@ -225,264 +526,107 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { pageId={article.pageId} pageTags={article.tags} /> - {/* GridItem order matters — mobile layout relies on DOM order for grid placement. - See furnitureArrangements.ts if reordering. */} -
- - - - + ) : ( + + )} + + {/* SlotBodyEnd is handled inside NewArticleGrid for the new layout. + For the deprecated layout it lives here, matching the original structure. */} + {useDeprecatedGrid && ( +
- - - - - - - - - -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - + + - )} +
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - - )} -
- - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} - - - - {isApps && ( - - - - )} +
+ )} - {showBodyEndSlot && ( - - - + {useDeprecatedGrid && ( + <> +
+
+
{ webUrl={article.webURL} webTitle={article.webTitle} showBottomSocialButtons={ - article.showBottomSocialButtons && - renderingTarget === 'Web' + article.showBottomSocialButtons && isWeb } /> - - - - - - - - - -
+ + + )} {isWeb && renderAds && (
{ {article.storyPackage && (
{ webURL={article.webURL} /> + {showComments && (
{ '--article-section-background', )} borderColour={themePalette('--article-border')} + fontColour={ + useDeprecatedGrid + ? themePalette('--article-section-title') + : undefined + } > {
)} + {isWeb && ( <> {props.NAV.subNavSections && ( @@ -734,7 +853,7 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { !!article.config.switches.remoteBanner } tags={article.tags} - host={host} + host={useDeprecatedGrid ? undefined : host} /> @@ -745,21 +864,341 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { )} {isApps && ( - <> -
- - - -
- +
+ + + +
)} ); }; + +// Temporary override until deprecated interactive articles are migrated to the +// new grid. Can be removed once useDeprecatedGrid is no longer needed. +export const temporaryBodyCopyColourOverride = css` + .content__main-column--interactive p { + /* stylelint-disable-next-line declaration-no-important */ + color: ${themePalette('--article-text')} !important; + } +`; + +// --------------------------------------------------------------------------- +// Deprecated grid (pre-switchover articles) +// --------------------------------------------------------------------------- + +const deprecatedMaxWidth = css` + ${from.desktop} { + max-width: 620px; + } +`; + +const DeprecatedInteractiveGridWrapper = ({ + children, +}: { + children: React.ReactNode; +}) => ( +
+ {children} +
+); + +interface DeprecatedArticleGridProps { + article: ArticleDeprecated; + format: ArticleFormat; + branding: Branding | undefined; + contributionsServiceUrl: string; + isApps: boolean; + host: string | undefined; +} + +const DeprecatedArticleGrid = ({ + article, + format, + branding, + contributionsServiceUrl, + isApps, + host, +}: DeprecatedArticleGridProps) => ( +
+
+ + +
+ +
+
+ +
+ +
+
+ + {format.theme === ArticleSpecial.Labs ? <> : } + + +
+ +
+
+ + + + +
+
+ +
+
+
+ +
+ {isApps ? ( + <> + + + + + + + + ) : ( + + )} +
+
+ + + + + +
+
+
+); diff --git a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx deleted file mode 100644 index cef71817073..00000000000 --- a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx +++ /dev/null @@ -1,836 +0,0 @@ -import { css, Global } 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 type React from 'react'; -import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; -import { AppsFooter } from '../components/AppsFooter.island'; -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 { Border } from '../components/Border'; -import { Carousel } from '../components/Carousel.island'; -import { DecideLines } from '../components/DecideLines'; -import { DirectoryPageNav } from '../components/DirectoryPageNav'; -import { DiscussionLayout } from '../components/DiscussionLayout'; -import { Footer } from '../components/Footer'; -import { GridItem } from '../components/GridItem'; -import { HeaderAdSlot } from '../components/HeaderAdSlot'; -import { InteractivesDisableArticleSwipe } from '../components/InteractivesDisableArticleSwipe.island'; -import { InteractivesNativePlatformWrapper } from '../components/InteractivesNativePlatformWrapper.island'; -import { InteractivesScrollbarWidth } from '../components/InteractivesScrollbarWidth.island'; -import { Island } from '../components/Island'; -import { LabsHeader } from '../components/LabsHeader'; -import { MainMedia } from '../components/MainMedia'; -import { Masthead } from '../components/Masthead/Masthead'; -import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; -import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; -import { OnwardsUpper } from '../components/OnwardsUpper.island'; -import { Section } from '../components/Section'; -import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; -import { Standfirst } from '../components/Standfirst'; -import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; -import { SubMeta } from '../components/SubMeta'; -import { SubNav } from '../components/SubNav.island'; -import { type ArticleFormat, ArticleSpecial } from '../lib/articleFormat'; -import { canRenderAds } from '../lib/canRenderAds'; -import { getContributionsServiceUrl } from '../lib/contributions'; -import { decideStoryPackageTrails } from '../lib/decideTrail'; -import type { NavType } from '../model/extract-nav'; -import { palette as themePalette } from '../palette'; -import type { ArticleDeprecated } from '../types/article'; -import type { RoleType } from '../types/content'; -import type { RenderingTarget } from '../types/renderingTarget'; -import { - interactiveGlobalStyles, - interactiveLegacyClasses, -} from './lib/interactiveLegacyStyling'; -import { BannerWrapper, Stuck } from './lib/stickiness'; - -const InteractiveGrid = ({ 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; - } -`; - -export const temporaryBodyCopyColourOverride = css` - .content__main-column--interactive p { - /* stylelint-disable-next-line declaration-no-important */ - color: ${themePalette('--article-text')} !important; - } -`; - -interface CommonProps { - article: ArticleDeprecated; - format: ArticleFormat; - renderingTarget: RenderingTarget; - serverTime?: number; -} - -interface WebProps extends CommonProps { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppsProps extends CommonProps { - renderingTarget: 'Apps'; -} - -export const InteractiveLayoutDeprecated = (props: WebProps | AppsProps) => { - const { article, format, renderingTarget, serverTime } = props; - const { - config: { isPaidContent, host, hasSurveyAd }, - editionId, - } = 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 renderAds = canRenderAds(article); - - const includesFullWidthElement = article.blocks.some((block) => - block.elements.some((element) => { - const role = - 'role' in element - ? (element.role as RoleType | 'fullWidth' | undefined) - : undefined; - return role === 'fullWidth'; - }), - ); - - return ( - <> - {includesFullWidthElement && ( - - - - )} - {isApps && ( - <> - - - - - - - - - )} - {article.isLegacyInteractive && ( - - )} - {isWeb && ( - <> -
- {renderAds && ( - -
-
- -
-
-
- )} - - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> -
- - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - - {renderAds && hasSurveyAd && ( - - )} - - )} -
- -
-
- - -
- -
-
- -
- -
-
- - {format.theme === ArticleSpecial.Labs ? ( - <> - ) : ( - - )} - - -
- -
-
- - - - -
-
- -
-
-
- -
- {isApps ? ( - <> - - - - - - - - ) : ( - - )} -
-
- - - - - -
-
-
- -
-
- - - -
-
- -
- -
- -
- -
- - {isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - - {showComments && ( -
- -
- )} - - {!isPaidContent && ( -
- - - - - -
- )} - - {isWeb && renderAds && ( -
- -
- )} -
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - - {isWeb && ( - <> -
-
-
- - - - - - - - - )} - {isApps && ( -
- - - -
- )} - - ); -}; diff --git a/dotcom-rendering/src/lib/ArticleRenderer.tsx b/dotcom-rendering/src/lib/ArticleRenderer.tsx index d7fd529642a..483848d6cd3 100644 --- a/dotcom-rendering/src/lib/ArticleRenderer.tsx +++ b/dotcom-rendering/src/lib/ArticleRenderer.tsx @@ -218,6 +218,7 @@ export const ArticleRenderer = ({ ? interactiveLegacyClasses.contentMainColumn : '', ].join(' ')} + // TODO: Conditionally apply grid for interactives? css={[commercialPosition, spacefinderAdStyles]} > {renderingTarget === 'Apps' From e2f9c829a0c3f8993200147faf81e40dc71e66b4 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Fri, 15 May 2026 23:09:22 +0100 Subject: [PATCH 03/16] Gridception --- .../src/components/ArticleBody.tsx | 5 ++ .../src/layouts/InteractiveLayout.tsx | 50 ++++++++++++------- .../src/layouts/lib/furnitureArrangements.ts | 4 +- dotcom-rendering/src/lib/ArticleRenderer.tsx | 17 ++++++- dotcom-rendering/src/lib/renderElement.tsx | 3 ++ 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleBody.tsx b/dotcom-rendering/src/components/ArticleBody.tsx index 68d530f071d..c91a5101f47 100644 --- a/dotcom-rendering/src/components/ArticleBody.tsx +++ b/dotcom-rendering/src/components/ArticleBody.tsx @@ -55,6 +55,8 @@ type Props = { shouldHideAds: boolean; serverTime?: number; idApiUrl?: string; + accentColor?: string; + isShinyNewInteractiveLayout?: boolean; }; const globalOlStyles = () => css` @@ -145,6 +147,8 @@ export const ArticleBody = ({ shouldHideAds, serverTime, idApiUrl, + accentColor, + isShinyNewInteractiveLayout = false, }: Props) => { const isInteractiveContent = format.design === ArticleDesign.Interactive || @@ -266,6 +270,7 @@ export const ArticleBody = ({ contributionsServiceUrl={contributionsServiceUrl} shouldHideAds={shouldHideAds} idApiUrl={idApiUrl} + isShinyNewInteractiveLayout={isShinyNewInteractiveLayout} />
{hasObserverPublicationTag && } diff --git a/dotcom-rendering/src/layouts/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/InteractiveLayout.tsx index ce1ba0c3241..098c5290216 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayout.tsx @@ -305,6 +305,7 @@ const ArticleGrid = ({ editionId={article.editionId} shouldHideAds={article.shouldHideAds} idApiUrl={article.config.idApiUrl} + isShinyNewInteractiveLayout={true} /> {isApps && ( @@ -336,25 +337,38 @@ const ArticleGrid = ({ /> )} - - + > +
+ + +
+
{ const isSectionedMiniProfilesArticle = elements.filter( @@ -90,6 +93,7 @@ export const ArticleRenderer = ({ isSectionedMiniProfilesArticle={isSectionedMiniProfilesArticle} shouldHideAds={shouldHideAds} idApiUrl={idApiUrl} + isShinyNewInteractiveLayout={isShinyNewInteractiveLayout} /> ); }); @@ -206,6 +210,13 @@ export const ArticleRenderer = ({ // ^^ Until we decide where to do the "isomorphism split" in this this code is not safe here. // But should be soon. + const interactiveLayoutCSS = css` + ${grid.container} + > * { + ${grid.column.centre} + } + `; + return (
{renderingTarget === 'Apps' ? augmentedElements diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index 03043d1bef0..2d7b0f7c3c3 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -108,6 +108,7 @@ type Props = { contentType?: string; contentLayout?: string; idApiUrl?: string; + isShinyNewInteractiveLayout?: boolean; }; // updateRole modifies the role of an element in a way appropriate for most @@ -179,6 +180,7 @@ export const renderElement = ({ contentType, contentLayout, idApiUrl, + isShinyNewInteractiveLayout = false, }: Props) => { const isBlog = format.design === ArticleDesign.LiveBlog || @@ -1051,6 +1053,7 @@ export const RenderArticleElement = ({ contentType, contentLayout, idApiUrl, + isShinyNewInteractiveLayout = false, }: Props) => { const withUpdatedRole = updateRole(element, format); From 781995028f13c244681660685b0af1fc4739584a Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Fri, 15 May 2026 23:33:16 +0100 Subject: [PATCH 04/16] Separate out two layouts again --- dotcom-rendering/src/components/SubMeta.tsx | 9 +- dotcom-rendering/src/layouts/DecideLayout.tsx | 62 +- .../src/layouts/FullPageInteractiveLayout.tsx | 2 +- .../src/layouts/InteractiveLayout.tsx | 1131 +++++------------ .../layouts/InteractiveLayoutDeprecated.tsx | 837 ++++++++++++ dotcom-rendering/src/lib/ArticleRenderer.tsx | 3 +- dotcom-rendering/src/lib/renderElement.tsx | 3 - 7 files changed, 1231 insertions(+), 816 deletions(-) create mode 100644 dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx diff --git a/dotcom-rendering/src/components/SubMeta.tsx b/dotcom-rendering/src/components/SubMeta.tsx index 71307c19d2e..c7f116e5cf4 100644 --- a/dotcom-rendering/src/components/SubMeta.tsx +++ b/dotcom-rendering/src/components/SubMeta.tsx @@ -16,7 +16,6 @@ import type { BaseLinkType } from '../model/extract-nav'; import { palette } from '../palette'; import { Island } from './Island'; import { ShareButton } from './ShareButton.island'; -import { interactiveLayoutSwitchoverDate } from '../layouts/DecideLayout'; const labelStyles = (design: ArticleDesign): SerializedStyles => css` ${design === ArticleDesign.Gallery ? grid.column.centre : undefined}; @@ -131,6 +130,7 @@ type Props = { webUrl: string; webTitle: string; showBottomSocialButtons: boolean; + isDeprecatedInteractiveLayout?: boolean; }; const syndicationButtonOverrides = css` @@ -206,6 +206,7 @@ export const SubMeta = ({ webUrl, webTitle, showBottomSocialButtons, + isDeprecatedInteractiveLayout = false, }: Props) => { const createLinks = () => { const links: BaseLinkType[] = []; @@ -226,15 +227,11 @@ export const SubMeta = ({ format.design !== ArticleDesign.Interactive && format.design !== ArticleDesign.Gallery; - const usesDeprecatedInteractiveLayout = - format.design === ArticleDesign.Interactive && - interactiveLayoutSwitchoverDate > new Date(); - return (
{ default: { switch (article.design) { case ArticleDesign.Interactive: - return ( - - ); + if (publicationDate < interactiveLayoutSwitchoverDate) { + return ( + + ); + } else { + return ( + + ); + } case ArticleDesign.FullPageInteractive: { return ( { default: { switch (article.design) { case ArticleDesign.Interactive: - return ( - - ); + if (publicationDate < interactiveLayoutSwitchoverDate) { + return ( + + ); + } else { + return ( + + ); + } case ArticleDesign.FullPageInteractive: { return ( ); -interface NewArticleGridProps { - article: ArticleDeprecated; - format: ArticleFormat; - branding: Branding | undefined; - contributionsServiceUrl: string; - isApps: boolean; - isWeb: boolean; - renderAds: boolean; - showBodyEndSlot: boolean; - host: string | undefined; -} - -const ArticleGrid = ({ - article, - format, - branding, - contributionsServiceUrl, - isApps, - isWeb, - renderAds, - showBodyEndSlot, - host, -}: NewArticleGridProps) => ( - /* GridItem order matters — mobile layout relies on DOM order for grid placement. - See furnitureArrangements.ts if reordering. */ -
- - - - - - - - - - - - - -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - - )} -
- - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} - - - - {isApps && ( - - - - )} - - {showBodyEndSlot && ( - - - - )} -
-
- - -
-
-
-
- - - - - - - -
-); - -// --------------------------------------------------------------------------- -// Main layout -// --------------------------------------------------------------------------- - -interface CommonProps { +interface Props { article: ArticleDeprecated; format: ArticleFormat; renderingTarget: RenderingTarget; serverTime?: number; - useDeprecatedGrid?: boolean; } -interface WebProps extends CommonProps { +interface WebProps extends Props { NAV: NavType; renderingTarget: 'Web'; } -interface AppsProps extends CommonProps { +interface AppProps extends Props { renderingTarget: 'Apps'; } -export const InteractiveLayout = (props: WebProps | AppsProps) => { - const { article, format, renderingTarget, serverTime, useDeprecatedGrid } = - props; +export const InteractiveLayout = (props: WebProps | AppProps) => { + const { article, format, renderingTarget, serverTime } = props; const { config: { isPaidContent, host, hasSurveyAd }, editionId, } = article; - const isApps = renderingTarget === 'Apps'; const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; const showBodyEndSlot = isWeb && - ((parse(article.slotMachineFlags ?? '').showBodyEnd || - article.config.switches.slotBodyEnd) ?? - false); + (parse(article.slotMachineFlags ?? '').showBodyEnd || + article.config.switches.slotBodyEnd); + + // TODO: + // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render + // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. const showComments = article.isCommentable && !isPaidContent; @@ -472,15 +165,8 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { - {useDeprecatedGrid && ( - - )} )} - {useDeprecatedGrid && article.isLegacyInteractive && ( - - )} - {isWeb && (
{renderAds && ( @@ -504,10 +190,9 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={!useDeprecatedGrid} - showSlimNav={useDeprecatedGrid} - hasPageSkin={useDeprecatedGrid ? false : undefined} - hasPageSkinContentSelfConstrain={!useDeprecatedGrid} + showSubNav={true} + showSlimNav={false} + hasPageSkinContentSelfConstrain={true} pageId={article.pageId} tagIds={article.tags.map((tag) => tag.id)} sectionId={article.config.section} @@ -524,7 +209,7 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" - element={useDeprecatedGrid ? undefined : 'aside'} + element="aside" > @@ -540,125 +225,337 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { pageId={article.pageId} pageTags={article.tags} /> + {/* GridItem order matters — mobile layout relies on DOM order for grid placement. + See furnitureArrangements.ts if reordering. */} +
+ + + + + + + + + + + + + +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ {isApps ? ( + <> + + + + + + {!!article.affiliateLinksDisclaimer && ( + + )} + + + ) : ( + + )} +
+ + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + +
+ + + +
+
+ )} + + - {useDeprecatedGrid ? ( - - ) : ( - - )} + {isApps && ( + + + + )} - {/* SlotBodyEnd is handled inside NewArticleGrid for the new layout. - For the deprecated layout it lives here, matching the original structure. */} - {useDeprecatedGrid && ( -
+ + + )} +
+
+ + +
+
+ + + -
+ - -
-
- )} - - {useDeprecatedGrid && ( - <> -
- -
-
- -
- - )} + +
+
{isWeb && renderAds && (
{ {article.storyPackage && (
{ webURL={article.webURL} /> - {showComments && (
{ '--article-section-background', )} borderColour={themePalette('--article-border')} - fontColour={ - useDeprecatedGrid - ? themePalette('--article-section-title') - : undefined - } > {
)} - {isWeb && ( <> {props.NAV.subNavSections && ( @@ -867,7 +756,7 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { !!article.config.switches.remoteBanner } tags={article.tags} - host={useDeprecatedGrid ? undefined : host} + host={host} /> @@ -878,341 +767,21 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { )} {isApps && ( -
- - - -
+ <> +
+ + + +
+ )} ); }; - -// Temporary override until deprecated interactive articles are migrated to the -// new grid. Can be removed once useDeprecatedGrid is no longer needed. -export const temporaryBodyCopyColourOverride = css` - .content__main-column--interactive p { - /* stylelint-disable-next-line declaration-no-important */ - color: ${themePalette('--article-text')} !important; - } -`; - -// --------------------------------------------------------------------------- -// Deprecated grid (pre-switchover articles) -// --------------------------------------------------------------------------- - -const deprecatedMaxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; - -const DeprecatedInteractiveGridWrapper = ({ - children, -}: { - children: React.ReactNode; -}) => ( -
- {children} -
-); - -interface DeprecatedArticleGridProps { - article: ArticleDeprecated; - format: ArticleFormat; - branding: Branding | undefined; - contributionsServiceUrl: string; - isApps: boolean; - host: string | undefined; -} - -const DeprecatedArticleGrid = ({ - article, - format, - branding, - contributionsServiceUrl, - isApps, - host, -}: DeprecatedArticleGridProps) => ( -
-
- - -
- -
-
- -
- -
-
- - {format.theme === ArticleSpecial.Labs ? <> : } - - -
- -
-
- - - - -
-
- -
-
-
- -
- {isApps ? ( - <> - - - - - - - - ) : ( - - )} -
-
- - - - - -
-
-
-); diff --git a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx new file mode 100644 index 00000000000..b75a8b2a2be --- /dev/null +++ b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx @@ -0,0 +1,837 @@ +import { css, Global } 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 type React from 'react'; +import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; +import { AppsFooter } from '../components/AppsFooter.island'; +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 { Border } from '../components/Border'; +import { Carousel } from '../components/Carousel.island'; +import { DecideLines } from '../components/DecideLines'; +import { DirectoryPageNav } from '../components/DirectoryPageNav'; +import { DiscussionLayout } from '../components/DiscussionLayout'; +import { Footer } from '../components/Footer'; +import { GridItem } from '../components/GridItem'; +import { HeaderAdSlot } from '../components/HeaderAdSlot'; +import { InteractivesDisableArticleSwipe } from '../components/InteractivesDisableArticleSwipe.island'; +import { InteractivesNativePlatformWrapper } from '../components/InteractivesNativePlatformWrapper.island'; +import { InteractivesScrollbarWidth } from '../components/InteractivesScrollbarWidth.island'; +import { Island } from '../components/Island'; +import { LabsHeader } from '../components/LabsHeader'; +import { MainMedia } from '../components/MainMedia'; +import { Masthead } from '../components/Masthead/Masthead'; +import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; +import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; +import { OnwardsUpper } from '../components/OnwardsUpper.island'; +import { Section } from '../components/Section'; +import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; +import { Standfirst } from '../components/Standfirst'; +import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; +import { SubMeta } from '../components/SubMeta'; +import { SubNav } from '../components/SubNav.island'; +import { type ArticleFormat, ArticleSpecial } from '../lib/articleFormat'; +import { canRenderAds } from '../lib/canRenderAds'; +import { getContributionsServiceUrl } from '../lib/contributions'; +import { decideStoryPackageTrails } from '../lib/decideTrail'; +import type { NavType } from '../model/extract-nav'; +import { palette as themePalette } from '../palette'; +import type { ArticleDeprecated } from '../types/article'; +import type { RoleType } from '../types/content'; +import type { RenderingTarget } from '../types/renderingTarget'; +import { + interactiveGlobalStyles, + interactiveLegacyClasses, +} from './lib/interactiveLegacyStyling'; +import { BannerWrapper, Stuck } from './lib/stickiness'; + +const InteractiveGrid = ({ 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; + } +`; + +export const temporaryBodyCopyColourOverride = css` + .content__main-column--interactive p { + /* stylelint-disable-next-line declaration-no-important */ + color: ${themePalette('--article-text')} !important; + } +`; + +interface CommonProps { + article: ArticleDeprecated; + format: ArticleFormat; + renderingTarget: RenderingTarget; + serverTime?: number; +} + +interface WebProps extends CommonProps { + NAV: NavType; + renderingTarget: 'Web'; +} + +interface AppsProps extends CommonProps { + renderingTarget: 'Apps'; +} + +export const InteractiveLayoutDeprecated = (props: WebProps | AppsProps) => { + const { article, format, renderingTarget, serverTime } = props; + const { + config: { isPaidContent, host, hasSurveyAd }, + editionId, + } = 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 renderAds = canRenderAds(article); + + const includesFullWidthElement = article.blocks.some((block) => + block.elements.some((element) => { + const role = + 'role' in element + ? (element.role as RoleType | 'fullWidth' | undefined) + : undefined; + return role === 'fullWidth'; + }), + ); + + return ( + <> + {includesFullWidthElement && ( + + + + )} + {isApps && ( + <> + + + + + + + + + )} + {article.isLegacyInteractive && ( + + )} + {isWeb && ( + <> +
+ {renderAds && ( + +
+
+ +
+
+
+ )} + + tag.id)} + sectionId={article.config.section} + contentType={article.contentType} + /> +
+ + {format.theme === ArticleSpecial.Labs && ( + +
+ +
+
+ )} + + {renderAds && hasSurveyAd && ( + + )} + + )} +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + {format.theme === ArticleSpecial.Labs ? ( + <> + ) : ( + + )} + + +
+ +
+
+ + + + +
+
+ +
+
+
+ +
+ {isApps ? ( + <> + + + + + + + + ) : ( + + )} +
+
+ + + + + +
+
+
+ +
+
+ + + +
+
+ +
+ +
+ +
+ +
+ + {isWeb && renderAds && ( +
+ +
+ )} + + {article.storyPackage && ( +
+ + + +
+ )} + + + + + + {showComments && ( +
+ +
+ )} + + {!isPaidContent && ( +
+ + + + + +
+ )} + + {isWeb && renderAds && ( +
+ +
+ )} +
+ + {isWeb && props.NAV.subNavSections && ( +
+ + + +
+ )} + + {isWeb && ( + <> +
+
+
+ + + + + + + + + )} + {isApps && ( +
+ + + +
+ )} + + ); +}; diff --git a/dotcom-rendering/src/lib/ArticleRenderer.tsx b/dotcom-rendering/src/lib/ArticleRenderer.tsx index bea6eb6adb4..c9ae38b1511 100644 --- a/dotcom-rendering/src/lib/ArticleRenderer.tsx +++ b/dotcom-rendering/src/lib/ArticleRenderer.tsx @@ -3,6 +3,7 @@ import { Fragment } from 'react'; import { useConfig } from '../components/ConfigContext'; import { FeastContextualNudge } from '../components/FeastContextualNudge.island'; import { Island } from '../components/Island'; +import { grid } from '../grid'; import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling'; import type { Switches } from '../types/config'; import type { FEElement, RecipeBlockElement } from '../types/content'; @@ -12,7 +13,6 @@ import { ArticleDesign, type ArticleFormat } from './articleFormat'; import type { EditionId } from './edition'; import { RenderArticleElement } from './renderElement'; import { withSignInGateSlot } from './withSignInGateSlot'; -import { grid } from '../grid'; // This is required for spacefinder to work! const commercialPosition = css` @@ -93,7 +93,6 @@ export const ArticleRenderer = ({ isSectionedMiniProfilesArticle={isSectionedMiniProfilesArticle} shouldHideAds={shouldHideAds} idApiUrl={idApiUrl} - isShinyNewInteractiveLayout={isShinyNewInteractiveLayout} /> ); }); diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index 2d7b0f7c3c3..03043d1bef0 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -108,7 +108,6 @@ type Props = { contentType?: string; contentLayout?: string; idApiUrl?: string; - isShinyNewInteractiveLayout?: boolean; }; // updateRole modifies the role of an element in a way appropriate for most @@ -180,7 +179,6 @@ export const renderElement = ({ contentType, contentLayout, idApiUrl, - isShinyNewInteractiveLayout = false, }: Props) => { const isBlog = format.design === ArticleDesign.LiveBlog || @@ -1053,7 +1051,6 @@ export const RenderArticleElement = ({ contentType, contentLayout, idApiUrl, - isShinyNewInteractiveLayout = false, }: Props) => { const withUpdatedRole = updateRole(element, format); From bce5cee6ed088a63d6b7ac8583a44c2b95ae3da7 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 19 May 2026 17:54:55 +0100 Subject: [PATCH 05/16] Adjust fullWidth element styling Bye bye scrollbar-width --- dotcom-rendering/src/components/Figure.tsx | 44 +------------------ .../src/layouts/InteractiveLayout.tsx | 19 +------- .../src/layouts/lib/furnitureArrangements.ts | 6 +-- dotcom-rendering/src/lib/ArticleRenderer.tsx | 4 +- 4 files changed, 9 insertions(+), 64 deletions(-) diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ccb113fb4b1..d69c3414b1b 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/react'; -import { breakpoints, from, space, until } from '@guardian/source/foundations'; +import { from, space, until } from '@guardian/source/foundations'; import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import type { FEElement, RoleType } from '../types/content'; @@ -74,47 +74,7 @@ const roleCss = { margin-top: ${space[3]}px; margin-bottom: ${space[3]}px; - ${until.tablet} { - margin-left: -20px; - margin-right: -20px; - } - ${until.mobileLandscape} { - margin-left: -10px; - margin-right: -10px; - } - ${from.tablet} { - --scrollbar-width-fallback: 15px; - --half-scrollbar-width-fallback: 7.5px; - - width: calc( - 100vw - var(--scrollbar-width, var(--scrollbar-width-fallback)) - ); - max-width: calc( - 100vw - var(--scrollbar-width, var(--scrollbar-width-fallback)) - ); - - --grid-container-max-width: 740px; - --grid-container-left-margin: calc( - ((-100vw + (var(--grid-container-max-width) - 42px)) / 2) + - var( - --half-scrollbar-width, - var(--half-scrollbar-width-fallback) - ) - ); - - margin-left: var(--grid-container-left-margin); - } - ${from.desktop} { - --grid-container-max-width: ${breakpoints.desktop}px; - } - ${from.leftCol} { - --grid-container-max-width: ${breakpoints.leftCol}px; - --grid-left-col-width: 140px; - } - ${from.wide} { - --grid-container-max-width: ${breakpoints.wide}px; - --grid-left-col-width: 219px; - } + grid-column: 1 / -1; `, showcase: css` diff --git a/dotcom-rendering/src/layouts/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/InteractiveLayout.tsx index 85061885fd4..8e2ae718493 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayout.tsx @@ -25,7 +25,6 @@ import { GuardianLabsLines } from '../components/GuardianLabsLines'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; import { InteractivesDisableArticleSwipe } from '../components/InteractivesDisableArticleSwipe.island'; import { InteractivesNativePlatformWrapper } from '../components/InteractivesNativePlatformWrapper.island'; -import { InteractivesScrollbarWidth } from '../components/InteractivesScrollbarWidth.island'; import { Island } from '../components/Island'; import { LabsHeader } from '../components/LabsHeader'; import { ListenToArticle } from '../components/ListenToArticle.island'; @@ -54,7 +53,6 @@ import { parse } from '../lib/slot-machine-flags'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; import type { ArticleDeprecated } from '../types/article'; -import type { RoleType } from '../types/content'; import type { RenderingTarget } from '../types/renderingTarget'; import { type Area, @@ -140,23 +138,8 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { const renderAds = canRenderAds(article); - const includesFullWidthElement = article.blocks.some((block) => - block.elements.some((element) => { - const role = - 'role' in element - ? (element.role as RoleType | 'fullWidth' | undefined) - : undefined; - return role === 'fullWidth'; - }), - ); - return ( <> - {includesFullWidthElement && ( - - - - )} {isApps && ( <> @@ -240,7 +223,7 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { }), ]} > - + = { // Raw CSS overrides per area per breakpoint. Entries are only needed when an area // deviates from the default: centre column, single-column mobile layout with areas -// in DOM order (main-media → title → headline → standfirst → meta → body → right-column). +// in DOM order (media → title → headline → standfirst → meta → body → right-column). type AreaCss = Partial>; type LayoutCssMap = Partial>; @@ -43,7 +43,7 @@ const standardCss: LayoutCssMap = { tablet: 'grid-row: 3;', leftCol: 'grid-row: 2;', }, - 'main-media': { + media: { tablet: 'grid-row: 4;', leftCol: 'grid-row: 3;', }, diff --git a/dotcom-rendering/src/lib/ArticleRenderer.tsx b/dotcom-rendering/src/lib/ArticleRenderer.tsx index c9ae38b1511..0ce95c600e9 100644 --- a/dotcom-rendering/src/lib/ArticleRenderer.tsx +++ b/dotcom-rendering/src/lib/ArticleRenderer.tsx @@ -13,6 +13,7 @@ import { ArticleDesign, type ArticleFormat } from './articleFormat'; import type { EditionId } from './edition'; import { RenderArticleElement } from './renderElement'; import { withSignInGateSlot } from './withSignInGateSlot'; +import { interactiveLayoutSwitchoverDate } from '../layouts/DecideLayout'; // This is required for spacefinder to work! const commercialPosition = css` @@ -224,7 +225,8 @@ export const ArticleRenderer = ({ // Note, this class MUST be on the *direct parent* of the // elements for some legacy interactive styling to work. - format.design === ArticleDesign.Interactive + format.design === ArticleDesign.Interactive && + interactiveLayoutSwitchoverDate > new Date() ? interactiveLegacyClasses.contentMainColumn : '', ].join(' ')} From ae6db0a05ab6efc8f900073a66b98e7c785d2b87 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 19 May 2026 18:18:30 +0100 Subject: [PATCH 06/16] Stop applying legacy class to article element --- dotcom-rendering/src/lib/ArticleRenderer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dotcom-rendering/src/lib/ArticleRenderer.tsx b/dotcom-rendering/src/lib/ArticleRenderer.tsx index 0ce95c600e9..78492764f87 100644 --- a/dotcom-rendering/src/lib/ArticleRenderer.tsx +++ b/dotcom-rendering/src/lib/ArticleRenderer.tsx @@ -13,7 +13,6 @@ import { ArticleDesign, type ArticleFormat } from './articleFormat'; import type { EditionId } from './edition'; import { RenderArticleElement } from './renderElement'; import { withSignInGateSlot } from './withSignInGateSlot'; -import { interactiveLayoutSwitchoverDate } from '../layouts/DecideLayout'; // This is required for spacefinder to work! const commercialPosition = css` @@ -226,7 +225,7 @@ export const ArticleRenderer = ({ // Note, this class MUST be on the *direct parent* of the // elements for some legacy interactive styling to work. format.design === ArticleDesign.Interactive && - interactiveLayoutSwitchoverDate > new Date() + !isShinyNewInteractiveLayout ? interactiveLegacyClasses.contentMainColumn : '', ].join(' ')} From 87ad1f315d2ae6077fc43d45d7c95ed71b75f4c5 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 20 May 2026 15:33:37 +0100 Subject: [PATCH 07/16] Invert old/new interactive prop --- dotcom-rendering/src/components/ArticleBody.tsx | 6 +++--- dotcom-rendering/src/layouts/InteractiveLayout.tsx | 1 - .../src/layouts/InteractiveLayoutDeprecated.tsx | 1 + dotcom-rendering/src/lib/ArticleRenderer.tsx | 9 ++++----- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleBody.tsx b/dotcom-rendering/src/components/ArticleBody.tsx index c91a5101f47..864b6dc54e6 100644 --- a/dotcom-rendering/src/components/ArticleBody.tsx +++ b/dotcom-rendering/src/components/ArticleBody.tsx @@ -56,7 +56,7 @@ type Props = { serverTime?: number; idApiUrl?: string; accentColor?: string; - isShinyNewInteractiveLayout?: boolean; + isOldInteractive?: boolean; }; const globalOlStyles = () => css` @@ -148,7 +148,7 @@ export const ArticleBody = ({ serverTime, idApiUrl, accentColor, - isShinyNewInteractiveLayout = false, + isOldInteractive = false, }: Props) => { const isInteractiveContent = format.design === ArticleDesign.Interactive || @@ -270,7 +270,7 @@ export const ArticleBody = ({ contributionsServiceUrl={contributionsServiceUrl} shouldHideAds={shouldHideAds} idApiUrl={idApiUrl} - isShinyNewInteractiveLayout={isShinyNewInteractiveLayout} + isOldInteractive={isOldInteractive} />
{hasObserverPublicationTag && } diff --git a/dotcom-rendering/src/layouts/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/InteractiveLayout.tsx index 8e2ae718493..d29078e14fc 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayout.tsx @@ -422,7 +422,6 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { editionId={article.editionId} shouldHideAds={article.shouldHideAds} idApiUrl={article.config.idApiUrl} - isShinyNewInteractiveLayout={true} /> {isApps && ( diff --git a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx index b75a8b2a2be..6141e64a78a 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx @@ -525,6 +525,7 @@ export const InteractiveLayoutDeprecated = (props: WebProps | AppsProps) => { editionId={article.editionId} shouldHideAds={article.shouldHideAds} idApiUrl={article.config.idApiUrl} + isOldInteractive={true} /> diff --git a/dotcom-rendering/src/lib/ArticleRenderer.tsx b/dotcom-rendering/src/lib/ArticleRenderer.tsx index 78492764f87..618600ec032 100644 --- a/dotcom-rendering/src/lib/ArticleRenderer.tsx +++ b/dotcom-rendering/src/lib/ArticleRenderer.tsx @@ -40,7 +40,7 @@ type Props = { contributionsServiceUrl: string; shouldHideAds: boolean; idApiUrl?: string; - isShinyNewInteractiveLayout?: boolean; + isOldInteractive?: boolean; }; export const ArticleRenderer = ({ @@ -64,7 +64,7 @@ export const ArticleRenderer = ({ contributionsServiceUrl, shouldHideAds, idApiUrl, - isShinyNewInteractiveLayout = false, + isOldInteractive = false, }: Props) => { const isSectionedMiniProfilesArticle = elements.filter( @@ -224,8 +224,7 @@ export const ArticleRenderer = ({ // Note, this class MUST be on the *direct parent* of the // elements for some legacy interactive styling to work. - format.design === ArticleDesign.Interactive && - !isShinyNewInteractiveLayout + format.design === ArticleDesign.Interactive && isOldInteractive ? interactiveLegacyClasses.contentMainColumn : '', ].join(' ')} @@ -233,7 +232,7 @@ export const ArticleRenderer = ({ css={[ commercialPosition, spacefinderAdStyles, - isShinyNewInteractiveLayout && interactiveLayoutCSS, + !isOldInteractive && interactiveLayoutCSS, ]} > {renderingTarget === 'Apps' From 058a165212376cd825490babbdfbffd34d2a939c Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 3 Jun 2026 18:27:13 +0100 Subject: [PATCH 08/16] Rebase tidying --- dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx index 6141e64a78a..316d923d260 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.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 InteractiveLayoutDeprecated = (props: WebProps | AppsProps) => { )}
- From 1f537b7af11cc7eee9083ec9e0930e30aab9f6c2 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 4 Jun 2026 10:27:06 +0100 Subject: [PATCH 09/16] Remove unused accentColor --- dotcom-rendering/src/components/ArticleBody.tsx | 2 -- dotcom-rendering/src/layouts/InteractiveLayout.tsx | 10 +++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleBody.tsx b/dotcom-rendering/src/components/ArticleBody.tsx index 864b6dc54e6..d6f9b463a05 100644 --- a/dotcom-rendering/src/components/ArticleBody.tsx +++ b/dotcom-rendering/src/components/ArticleBody.tsx @@ -55,7 +55,6 @@ type Props = { shouldHideAds: boolean; serverTime?: number; idApiUrl?: string; - accentColor?: string; isOldInteractive?: boolean; }; @@ -147,7 +146,6 @@ export const ArticleBody = ({ shouldHideAds, serverTime, idApiUrl, - accentColor, isOldInteractive = false, }: Props) => { const isInteractiveContent = diff --git a/dotcom-rendering/src/layouts/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/InteractiveLayout.tsx index d29078e14fc..828a27ca94c 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayout.tsx @@ -1,5 +1,6 @@ import { css, type SerializedStyles } from '@emotion/react'; import { + from, palette as sourcePalette, space, until, @@ -218,9 +219,12 @@ export const InteractiveLayout = (props: WebProps | AppProps) => { )}; `, grid.container, - grid.verticalRules({ - centre: true, - }), + grid.outerRules(), + css` + ${from.leftCol} { + ${grid.centreRule(3)} + } + `, ]} > From 46e698583d7e5b672580c834c8d5fec3623b3192 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Fri, 26 Jun 2026 16:21:00 +0100 Subject: [PATCH 10/16] Make layout content the point of forking --- dotcom-rendering/src/layouts/DecideLayout.tsx | 60 +- .../src/layouts/FullPageInteractiveLayout.tsx | 2 +- .../src/layouts/InteractiveLayout.tsx | 773 ---------------- .../layouts/InteractiveLayoutDeprecated.tsx | 838 ------------------ .../interactives/InteractiveGridV1.tsx | 382 ++++++++ .../interactives/InteractiveGridV2.tsx | 414 +++++++++ .../interactives/InteractiveLayout.tsx | 379 ++++++++ .../src/layouts/lib/articleArrangements.ts | 34 +- .../src/layouts/lib/furnitureArrangements.ts | 108 --- 9 files changed, 1225 insertions(+), 1765 deletions(-) delete mode 100644 dotcom-rendering/src/layouts/InteractiveLayout.tsx delete mode 100644 dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx create mode 100644 dotcom-rendering/src/layouts/interactives/InteractiveGridV1.tsx create mode 100644 dotcom-rendering/src/layouts/interactives/InteractiveGridV2.tsx create mode 100644 dotcom-rendering/src/layouts/interactives/InteractiveLayout.tsx delete mode 100644 dotcom-rendering/src/layouts/lib/furnitureArrangements.ts diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index bded595b5dc..ea5baa4099a 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -10,8 +10,7 @@ import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; import { ImmersiveLayout } from './ImmersiveLayout'; -import { InteractiveLayout } from './InteractiveLayout'; -import { InteractiveLayoutDeprecated } from './InteractiveLayoutDeprecated'; +import { InteractiveLayout } from './interactives/InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; import { PictureLayout } from './PictureLayout'; @@ -33,8 +32,6 @@ interface WebProps extends BaseProps { export type Props = WebProps | AppProps; -export const interactiveLayoutSwitchoverDate = new Date('2024-06-01T00:00:00Z'); - const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { const format = { design: article.design, @@ -43,7 +40,6 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { }; const serverTime = article.serverTime; - const publicationDate = new Date(article.frontendData.webPublicationDate); switch (article.display) { case ArticleDisplay.Immersive: { @@ -117,24 +113,13 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { default: { switch (article.design) { case ArticleDesign.Interactive: - if (publicationDate < interactiveLayoutSwitchoverDate) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); case ArticleDesign.FullPageInteractive: { return ( { }; const serverTime = article.serverTime; - const publicationDate = new Date(article.frontendData.webPublicationDate); switch (article.display) { case ArticleDisplay.Immersive: { @@ -308,26 +292,14 @@ const DecideLayoutWeb = ({ article, NAV, renderingTarget }: WebProps) => { default: { switch (article.design) { case ArticleDesign.Interactive: - if (publicationDate < interactiveLayoutSwitchoverDate) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); case ArticleDesign.FullPageInteractive: { return ( ( - - {children} - -); - -interface Props { - article: ArticleDeprecated; - format: ArticleFormat; - renderingTarget: RenderingTarget; - serverTime?: number; -} - -interface WebProps extends Props { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppProps extends Props { - renderingTarget: 'Apps'; -} - -export const InteractiveLayout = (props: WebProps | AppProps) => { - const { article, format, renderingTarget, serverTime } = props; - const { - config: { isPaidContent, host, hasSurveyAd }, - editionId, - } = article; - - const isWeb = renderingTarget === 'Web'; - const isApps = renderingTarget === 'Apps'; - - const showBodyEndSlot = - isWeb && - (parse(article.slotMachineFlags ?? '').showBodyEnd || - article.config.switches.slotBodyEnd); - - // TODO: - // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render - // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. - - const showComments = article.isCommentable && !isPaidContent; - - const { branding } = article.commercialProperties[article.editionId]; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const renderAds = canRenderAds(article); - - return ( - <> - {isApps && ( - <> - - - - - - - - )} - {isWeb && ( -
- {renderAds && ( - -
- -
-
- )} - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> -
- )} - - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - - {isWeb && renderAds && hasSurveyAd && ( - - )} - -
- - {/* GridItem order matters — mobile layout relies on DOM order for grid placement. - See furnitureArrangements.ts if reordering. */} -
- - - - - - - - - - - - - -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - - )} -
- - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} - - - - {isApps && ( - - - - )} - - {showBodyEndSlot && ( - - - - )} -
-
- - -
-
-
-
- - - - - - - -
- - {isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - {showComments && ( -
- -
- )} - - {!isPaidContent && ( -
- - - - - -
- )} - - {isWeb && renderAds && ( -
- -
- )} -
- {isWeb && ( - <> - {props.NAV.subNavSections && ( -
- - - -
- )} -
-
-
- - - - - - {renderAds && ( - - )} - - )} - - {isApps && ( - <> -
- - - -
- - )} - - ); -}; diff --git a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx b/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx deleted file mode 100644 index 316d923d260..00000000000 --- a/dotcom-rendering/src/layouts/InteractiveLayoutDeprecated.tsx +++ /dev/null @@ -1,838 +0,0 @@ -import { css, Global } 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 type React from 'react'; -import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; -import { AppsFooter } from '../components/AppsFooter.island'; -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 { Border } from '../components/Border'; -import { Carousel } from '../components/Carousel.island'; -import { DecideLines } from '../components/DecideLines'; -import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; -import { DiscussionLayout } from '../components/DiscussionLayout'; -import { Footer } from '../components/Footer'; -import { GridItem } from '../components/GridItem'; -import { HeaderAdSlot } from '../components/HeaderAdSlot'; -import { InteractivesDisableArticleSwipe } from '../components/InteractivesDisableArticleSwipe.island'; -import { InteractivesNativePlatformWrapper } from '../components/InteractivesNativePlatformWrapper.island'; -import { InteractivesScrollbarWidth } from '../components/InteractivesScrollbarWidth.island'; -import { Island } from '../components/Island'; -import { LabsHeader } from '../components/LabsHeader'; -import { MainMedia } from '../components/MainMedia'; -import { Masthead } from '../components/Masthead/Masthead'; -import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; -import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; -import { OnwardsUpper } from '../components/OnwardsUpper.island'; -import { Section } from '../components/Section'; -import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; -import { Standfirst } from '../components/Standfirst'; -import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; -import { SubMeta } from '../components/SubMeta'; -import { SubNav } from '../components/SubNav.island'; -import { type ArticleFormat, ArticleSpecial } from '../lib/articleFormat'; -import { canRenderAds } from '../lib/canRenderAds'; -import { getContributionsServiceUrl } from '../lib/contributions'; -import { decideStoryPackageTrails } from '../lib/decideTrail'; -import type { NavType } from '../model/extract-nav'; -import { palette as themePalette } from '../palette'; -import type { ArticleDeprecated } from '../types/article'; -import type { RoleType } from '../types/content'; -import type { RenderingTarget } from '../types/renderingTarget'; -import { - interactiveGlobalStyles, - interactiveLegacyClasses, -} from './lib/interactiveLegacyStyling'; -import { BannerWrapper, Stuck } from './lib/stickiness'; - -const InteractiveGrid = ({ 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; - } -`; - -export const temporaryBodyCopyColourOverride = css` - .content__main-column--interactive p { - /* stylelint-disable-next-line declaration-no-important */ - color: ${themePalette('--article-text')} !important; - } -`; - -interface CommonProps { - article: ArticleDeprecated; - format: ArticleFormat; - renderingTarget: RenderingTarget; - serverTime?: number; -} - -interface WebProps extends CommonProps { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppsProps extends CommonProps { - renderingTarget: 'Apps'; -} - -export const InteractiveLayoutDeprecated = (props: WebProps | AppsProps) => { - const { article, format, renderingTarget, serverTime } = props; - const { - config: { isPaidContent, host, hasSurveyAd }, - editionId, - } = 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 renderAds = canRenderAds(article); - - const includesFullWidthElement = article.blocks.some((block) => - block.elements.some((element) => { - const role = - 'role' in element - ? (element.role as RoleType | 'fullWidth' | undefined) - : undefined; - return role === 'fullWidth'; - }), - ); - - return ( - <> - {includesFullWidthElement && ( - - - - )} - {isApps && ( - <> - - - - - - - - - )} - {article.isLegacyInteractive && ( - - )} - {isWeb && ( - <> -
- {renderAds && ( - -
-
- -
-
-
- )} - - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> -
- - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - - {renderAds && hasSurveyAd && ( - - )} - - )} -
- -
-
- - -
- -
-
- -
- -
-
- - {format.theme === ArticleSpecial.Labs ? ( - <> - ) : ( - - )} - - -
- -
-
- - - - -
-
- -
-
-
- -
- {isApps ? ( - <> - - - - - - - - ) : ( - - )} -
-
- - - - - -
-
-
- -
-
- - - -
-
- -
- -
- -
- -
- - {isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - - {showComments && ( -
- -
- )} - - {!isPaidContent && ( -
- - - - - -
- )} - - {isWeb && renderAds && ( -
- -
- )} -
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - - {isWeb && ( - <> -
-
-
- - - - - - - - - )} - {isApps && ( -
- - - -
- )} - - ); -}; diff --git a/dotcom-rendering/src/layouts/interactives/InteractiveGridV1.tsx b/dotcom-rendering/src/layouts/interactives/InteractiveGridV1.tsx new file mode 100644 index 00000000000..63b40f26f9f --- /dev/null +++ b/dotcom-rendering/src/layouts/interactives/InteractiveGridV1.tsx @@ -0,0 +1,382 @@ +import { css } from '@emotion/react'; +import { from, until } from '@guardian/source/foundations'; +import { Hide } from '@guardian/source/react-components'; +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 { Border } from '../../components/Border'; +import { DecideLines } from '../../components/DecideLines'; +import { GridItem } from '../../components/GridItem'; +import { MainMedia } from '../../components/MainMedia'; +import { Section } from '../../components/Section'; +import { Standfirst } from '../../components/Standfirst'; +import { type ArticleFormat, ArticleSpecial } from '../../lib/articleFormat'; +import { getContributionsServiceUrl } from '../../lib/contributions'; +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 { interactiveLegacyClasses } from '../lib/interactiveLegacyStyling'; + +const InteractiveGrid = ({ 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 Props { + article: ArticleDeprecated; + format: ArticleFormat; + renderingTarget: RenderingTarget; + serverTime?: number; +} + +interface WebProps extends Props { + NAV: NavType; + renderingTarget: 'Web'; +} + +interface AppProps extends Props { + renderingTarget: 'Apps'; +} + +export const temporaryBodyCopyColourOverride = css` + .content__main-column--interactive p { + /* stylelint-disable-next-line declaration-no-important */ + color: ${themePalette('--article-text')} !important; + } +`; + +export const InteractiveGridV1 = (props: WebProps | AppProps) => { + const { article, format, renderingTarget } = props; + const { + config: { host }, + } = article; + const isApps = renderingTarget === 'Apps'; + + const contributionsServiceUrl = getContributionsServiceUrl(article); + + const { branding } = article.commercialProperties[article.editionId]; + + return ( +
+
+ + +
+ +
+
+ +
+ +
+
+ + {format.theme === ArticleSpecial.Labs ? ( + <> + ) : ( + + )} + + +
+ +
+
+ + + + +
+
+ +
+
+
+ +
+ {isApps ? ( + <> + + + + + + + + ) : ( + + )} +
+
+ + + + + +
+
+
+ ); +}; diff --git a/dotcom-rendering/src/layouts/interactives/InteractiveGridV2.tsx b/dotcom-rendering/src/layouts/interactives/InteractiveGridV2.tsx new file mode 100644 index 00000000000..f124864a1fc --- /dev/null +++ b/dotcom-rendering/src/layouts/interactives/InteractiveGridV2.tsx @@ -0,0 +1,414 @@ +import { css, type SerializedStyles } from '@emotion/react'; +import { from, space, until } from '@guardian/source/foundations'; +import { Hide } from '@guardian/source/react-components'; +import { StraightLines } from '@guardian/source-development-kitchen/react-components'; +import { AffiliateDisclaimer } from '../../components/AffiliateDisclaimer'; +import { AppsEpic } from '../../components/AppsEpic.island'; +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 { DecideLines } from '../../components/DecideLines'; +import { GuardianLabsLines } from '../../components/GuardianLabsLines'; +import { Island } from '../../components/Island'; +import { ListenToArticle } from '../../components/ListenToArticle.island'; +import { MainMedia } from '../../components/MainMedia'; +import { MostViewedRightWithAd } from '../../components/MostViewedRightWithAd.island'; +import { SlotBodyEnd } from '../../components/SlotBodyEnd.island'; +import { Standfirst } from '../../components/Standfirst'; +import { SubMeta } from '../../components/SubMeta'; +import { grid } from '../../grid'; +import { + ArticleDesign, + type ArticleFormat, + ArticleSpecial, +} from '../../lib/articleFormat'; +import { getContributionsServiceUrl } from '../../lib/contributions'; +import { parse } from '../../lib/slot-machine-flags'; +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 { + type Area, + gridItemCss, + type LayoutType, +} from '../lib/articleArrangements'; + +const stretchLines = css` + ${until.phablet} { + margin-left: -20px; + margin-right: -20px; + } + ${until.mobileLandscape} { + margin-left: -10px; + margin-right: -10px; + } +`; + +interface GridItemProps { + area: Area; + layoutType: LayoutType; + element?: 'div' | 'aside'; + customCss?: SerializedStyles; + children: React.ReactNode; +} + +const GridItem = ({ + area, + layoutType, + element: Element = 'div', + customCss, + children, +}: GridItemProps) => ( + + {children} + +); + +interface Props { + article: ArticleDeprecated; + format: ArticleFormat; + renderingTarget: RenderingTarget; + serverTime?: number; +} + +interface WebProps extends Props { + NAV: NavType; + renderingTarget: 'Web'; +} + +interface AppProps extends Props { + renderingTarget: 'Apps'; +} + +export const InteractiveGridV2 = (props: WebProps | AppProps) => { + const { article, format, renderingTarget } = props; + const { + config: { host }, + } = article; + const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; + const renderAds = isWeb && !article.shouldHideAds; + + const contributionsServiceUrl = getContributionsServiceUrl(article); + + const showBodyEndSlot = + isWeb && + (parse(article.slotMachineFlags ?? '').showBodyEnd || + article.config.switches.slotBodyEnd); + + const { branding } = article.commercialProperties[article.editionId]; + + return ( +
+ + + + + + + + + + + + + +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ {isApps ? ( + <> + + + + + + {!!article.affiliateLinksDisclaimer && ( + + )} + + + ) : ( + + )} +
+ + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + +
+ + + +
+
+ )} + + + + {isApps && ( + + + + )} + + {showBodyEndSlot && ( + + + + )} +
+
+ + +
+
+
+
+ + + + + + + +
+ ); +}; diff --git a/dotcom-rendering/src/layouts/interactives/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/interactives/InteractiveLayout.tsx new file mode 100644 index 00000000000..b79ecd2409c --- /dev/null +++ b/dotcom-rendering/src/layouts/interactives/InteractiveLayout.tsx @@ -0,0 +1,379 @@ +import { palette as sourcePalette } from '@guardian/source/foundations'; +import { AdSlot, MobileStickyContainer } from '../../components/AdSlot.web'; +import { AppsFooter } from '../../components/AppsFooter.island'; +import { Carousel } from '../../components/Carousel.island'; +import { DirectoryPageNavIsland } from '../../components/DirectoryPageNavIsland'; +import { DiscussionLayout } from '../../components/DiscussionLayout'; +import { Footer } from '../../components/Footer'; +import { HeaderAdSlot } from '../../components/HeaderAdSlot'; +import { InteractivesDisableArticleSwipe } from '../../components/InteractivesDisableArticleSwipe.island'; +import { InteractivesNativePlatformWrapper } from '../../components/InteractivesNativePlatformWrapper.island'; +import { Island } from '../../components/Island'; +import { LabsHeader } from '../../components/LabsHeader'; +import { Masthead } from '../../components/Masthead/Masthead'; +import { MostViewedFooterData } from '../../components/MostViewedFooterData.island'; +import { MostViewedFooterLayout } from '../../components/MostViewedFooterLayout'; +import { OnwardsUpper } from '../../components/OnwardsUpper.island'; +import { Section } from '../../components/Section'; +import { StickyBottomBanner } from '../../components/StickyBottomBanner.island'; +import { SubNav } from '../../components/SubNav.island'; +import { type ArticleFormat, ArticleSpecial } from '../../lib/articleFormat'; +import { canRenderAds } from '../../lib/canRenderAds'; +import { getContributionsServiceUrl } from '../../lib/contributions'; +import { decideStoryPackageTrails } 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'; +import { InteractiveGridV1 } from './InteractiveGridV1'; +import { InteractiveGridV2 } from './InteractiveGridV2'; + +interface Props { + article: ArticleDeprecated; + format: ArticleFormat; + renderingTarget: RenderingTarget; + serverTime?: number; +} + +interface WebProps extends Props { + NAV: NavType; + renderingTarget: 'Web'; +} + +interface AppProps extends Props { + renderingTarget: 'Apps'; +} + +export const InteractiveLayout = (props: WebProps | AppProps) => { + const { article, format, renderingTarget, serverTime } = props; + const { + config: { isPaidContent, host, hasSurveyAd }, + editionId, + } = article; + + const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; + + // TODO: + // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render + // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. + + const showComments = article.isCommentable && !isPaidContent; + + const contributionsServiceUrl = getContributionsServiceUrl(article); + + const renderAds = canRenderAds(article); + + const interactiveLayoutSwitchoverDate = new Date('2024-06-01T00:00:00Z'); + const publicationDate = new Date(article.webPublicationDate); + const isLegacyInteractive = + publicationDate < interactiveLayoutSwitchoverDate; + + return ( + <> + {isApps && ( + <> + + + + + + + + )} + {isWeb && ( +
+ {renderAds && ( + +
+ +
+
+ )} + tag.id)} + sectionId={article.config.section} + contentType={article.contentType} + /> +
+ )} + + {format.theme === ArticleSpecial.Labs && ( + +
+ +
+
+ )} + + {isWeb && renderAds && hasSurveyAd && ( + + )} + +
+ + + {isLegacyInteractive ? ( + + ) : ( + + )} + + {isWeb && renderAds && ( +
+ +
+ )} + + {article.storyPackage && ( +
+ + + +
+ )} + + + + + {showComments && ( +
+ +
+ )} + + {!isPaidContent && ( +
+ + + + + +
+ )} + + {isWeb && renderAds && ( +
+ +
+ )} +
+ {isWeb && ( + <> + {props.NAV.subNavSections && ( +
+ + + +
+ )} +
+
+
+ + + + + + + + )} + + {isApps && ( + <> +
+ + + +
+ + )} + + ); +}; diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2cf5fb6843f..2e4323eb67e 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -2,7 +2,7 @@ import { css, type SerializedStyles } from '@emotion/react'; import { from, until } from '@guardian/source/foundations'; import { grid } from '../../grid'; -export type LayoutType = 'standard' | 'showcase' | 'media'; +export type LayoutType = 'standard' | 'showcase' | 'media' | 'interactive'; export type Area = | 'title' @@ -149,10 +149,42 @@ const mediaCss: LayoutCssMap = { }, }; +const interactiveCss: LayoutCssMap = { + title: { + tablet: 'grid-row: 1;', + leftCol: `grid-row: 1; ${grid.column.left}`, + }, + headline: { + tablet: 'grid-row: 2;', + leftCol: 'grid-row: 1;', + }, + standfirst: { + tablet: 'grid-row: 3;', + leftCol: 'grid-row: 2;', + }, + media: { + tablet: 'grid-row: 4;', + leftCol: 'grid-row: 3;', + }, + meta: { + tablet: 'grid-row: 5;', + leftCol: `grid-row: 3 / span 2; ${grid.column.left};`, + }, + body: { + tablet: `grid-row: 6; ${grid.column.all};`, + leftCol: 'grid-row: 4;', + }, + 'right-column': { + desktop: `grid-row: 1 / span 6; ${grid.column.right};`, + leftCol: `grid-row: 1 / span 4; ${grid.column.right};`, + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, + interactive: interactiveCss, }; /** diff --git a/dotcom-rendering/src/layouts/lib/furnitureArrangements.ts b/dotcom-rendering/src/layouts/lib/furnitureArrangements.ts deleted file mode 100644 index 229d620e6f7..00000000000 --- a/dotcom-rendering/src/layouts/lib/furnitureArrangements.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { css, type SerializedStyles } from '@emotion/react'; -import { from, until } from '@guardian/source/foundations'; -import { grid } from '../../grid'; - -export type LayoutType = 'standard'; - -export type Area = - | 'title' - | 'headline' - | 'standfirst' - | 'media' - | 'meta' - | 'body' - | 'right-column'; - -type Breakpoint = 'mobile' | 'tablet' | 'desktop' | 'leftCol'; - -const breakpointQueries: Record = { - mobile: until.tablet, - tablet: from.tablet, - desktop: from.desktop, - leftCol: from.leftCol, -}; - -// Raw CSS overrides per area per breakpoint. Entries are only needed when an area -// deviates from the default: centre column, single-column mobile layout with areas -// in DOM order (media → title → headline → standfirst → meta → body → right-column). - -type AreaCss = Partial>; -type LayoutCssMap = Partial>; - -const standardCss: LayoutCssMap = { - title: { - tablet: 'grid-row: 1;', - leftCol: - 'grid-row: 1; grid-column: left-column-start / left-column-end;', - }, - headline: { - tablet: 'grid-row: 2;', - leftCol: 'grid-row: 1;', - }, - standfirst: { - tablet: 'grid-row: 3;', - leftCol: 'grid-row: 2;', - }, - media: { - tablet: 'grid-row: 4;', - leftCol: 'grid-row: 3;', - }, - meta: { - tablet: 'grid-row: 5;', - leftCol: - 'grid-row: 3 / span 2; grid-column: left-column-start / left-column-end;', - }, - body: { - mobile: grid.column.all, - tablet: `grid-row: 6; ${grid.column.all}`, - leftCol: 'grid-row: 4;', - }, - 'right-column': { - desktop: - 'grid-row: 1 / span 6; grid-column: right-column-start / right-column-end;', - leftCol: - 'grid-row: 1 / span 4; grid-column: right-column-start / right-column-end;', - }, -}; - -const layoutCssMaps: Record = { - standard: standardCss, -}; - -/** - * Returns the Emotion CSS needed to position a single grid item — its - * default column, its row at each breakpoint, and any column overrides. - * The grid item _must_ be inside a {@link grid} module container. - * - * All items default to the centre column. Per-breakpoint overrides for - * `grid-row` and `grid-column` are applied on top via media queries, - * looked up from the plain CSS maps defined in this file. - * - * @param area - The named piece of article furniture to position (e.g. `'headline'`, `'body'`). - * @param layoutType - See {@link LayoutType}. Determines which CSS map to use for lookups. - * - * @example - * // In a React component: - *
- */ -export const gridItemCss = ( - area: Area, - layoutType: LayoutType, -): SerializedStyles => { - const areaOverrides = layoutCssMaps[layoutType][area] ?? {}; - - const breakpointCss = Object.entries(areaOverrides).map( - ([bp, styles]) => css` - ${breakpointQueries[bp as Breakpoint]} { - ${styles} - } - `, - ); - - // All items default to the centre column; breakpoint entries above - // override grid-row and grid-column as needed. - return css` - grid-column: centre-column-start / centre-column-end; - ${breakpointCss} - `; -}; From 7f8b0adaadd4384a2a83437c620c2629378684ea Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 1 Jul 2026 16:18:34 +0100 Subject: [PATCH 11/16] Rewire to fork at article content point rather than top level layout --- dotcom-rendering/src/layouts/DecideLayout.tsx | 5 +- .../src/layouts/FullPageInteractiveLayout.tsx | 2 +- .../src/layouts/StandardLayout.tsx | 24 +- .../src/layouts/StandardLayoutArticleGrid.tsx | 77 ++-- ...x => InteractiveArticleGridDeprecated.tsx} | 4 +- .../interactives/InteractiveGridV2.tsx | 414 ------------------ .../interactives/InteractiveLayout.tsx | 379 ---------------- 7 files changed, 64 insertions(+), 841 deletions(-) rename dotcom-rendering/src/layouts/interactives/{InteractiveGridV1.tsx => InteractiveArticleGridDeprecated.tsx} (99%) delete mode 100644 dotcom-rendering/src/layouts/interactives/InteractiveGridV2.tsx delete mode 100644 dotcom-rendering/src/layouts/interactives/InteractiveLayout.tsx diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index ea5baa4099a..bb2b5c1917b 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -10,7 +10,6 @@ import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; import { ImmersiveLayout } from './ImmersiveLayout'; -import { InteractiveLayout } from './interactives/InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; import { PictureLayout } from './PictureLayout'; @@ -114,7 +113,7 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { switch (article.design) { case ArticleDesign.Interactive: return ( - { switch (article.design) { case ArticleDesign.Interactive: return ( - { const isCricketMatchReport = format.design === ArticleDesign.MatchReport && !!cricketMatchUrl; + const interactiveLayoutSwitchoverDate = new Date('2024-06-01T00:00:00Z'); + const publicationDate = new Date(article.webPublicationDate); + const isLegacyInteractive = + publicationDate < interactiveLayoutSwitchoverDate; + const showComments = article.isCommentable && !isPaidContent; const contributionsServiceUrl = getContributionsServiceUrl(article); @@ -166,11 +172,15 @@ export const StandardLayout = (props: WebProps | AppProps) => { {/* This element is used to replace the article with the scorecard when the scorecard tab is clicked */}
- + {isLegacyInteractive ? ( + + ) : ( + + )}
{isWeb && renderAds && !isLabs && (
- - - - - - - + {layoutType !== 'interactive' && ( + + + + + + + + )} ); }; diff --git a/dotcom-rendering/src/layouts/interactives/InteractiveGridV1.tsx b/dotcom-rendering/src/layouts/interactives/InteractiveArticleGridDeprecated.tsx similarity index 99% rename from dotcom-rendering/src/layouts/interactives/InteractiveGridV1.tsx rename to dotcom-rendering/src/layouts/interactives/InteractiveArticleGridDeprecated.tsx index 63b40f26f9f..3f2635c63d9 100644 --- a/dotcom-rendering/src/layouts/interactives/InteractiveGridV1.tsx +++ b/dotcom-rendering/src/layouts/interactives/InteractiveArticleGridDeprecated.tsx @@ -171,7 +171,9 @@ export const temporaryBodyCopyColourOverride = css` } `; -export const InteractiveGridV1 = (props: WebProps | AppProps) => { +export const InteractiveArticleGridDeprecated = ( + props: WebProps | AppProps, +) => { const { article, format, renderingTarget } = props; const { config: { host }, diff --git a/dotcom-rendering/src/layouts/interactives/InteractiveGridV2.tsx b/dotcom-rendering/src/layouts/interactives/InteractiveGridV2.tsx deleted file mode 100644 index f124864a1fc..00000000000 --- a/dotcom-rendering/src/layouts/interactives/InteractiveGridV2.tsx +++ /dev/null @@ -1,414 +0,0 @@ -import { css, type SerializedStyles } from '@emotion/react'; -import { from, space, until } from '@guardian/source/foundations'; -import { Hide } from '@guardian/source/react-components'; -import { StraightLines } from '@guardian/source-development-kitchen/react-components'; -import { AffiliateDisclaimer } from '../../components/AffiliateDisclaimer'; -import { AppsEpic } from '../../components/AppsEpic.island'; -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 { DecideLines } from '../../components/DecideLines'; -import { GuardianLabsLines } from '../../components/GuardianLabsLines'; -import { Island } from '../../components/Island'; -import { ListenToArticle } from '../../components/ListenToArticle.island'; -import { MainMedia } from '../../components/MainMedia'; -import { MostViewedRightWithAd } from '../../components/MostViewedRightWithAd.island'; -import { SlotBodyEnd } from '../../components/SlotBodyEnd.island'; -import { Standfirst } from '../../components/Standfirst'; -import { SubMeta } from '../../components/SubMeta'; -import { grid } from '../../grid'; -import { - ArticleDesign, - type ArticleFormat, - ArticleSpecial, -} from '../../lib/articleFormat'; -import { getContributionsServiceUrl } from '../../lib/contributions'; -import { parse } from '../../lib/slot-machine-flags'; -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 { - type Area, - gridItemCss, - type LayoutType, -} from '../lib/articleArrangements'; - -const stretchLines = css` - ${until.phablet} { - margin-left: -20px; - margin-right: -20px; - } - ${until.mobileLandscape} { - margin-left: -10px; - margin-right: -10px; - } -`; - -interface GridItemProps { - area: Area; - layoutType: LayoutType; - element?: 'div' | 'aside'; - customCss?: SerializedStyles; - children: React.ReactNode; -} - -const GridItem = ({ - area, - layoutType, - element: Element = 'div', - customCss, - children, -}: GridItemProps) => ( - - {children} - -); - -interface Props { - article: ArticleDeprecated; - format: ArticleFormat; - renderingTarget: RenderingTarget; - serverTime?: number; -} - -interface WebProps extends Props { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppProps extends Props { - renderingTarget: 'Apps'; -} - -export const InteractiveGridV2 = (props: WebProps | AppProps) => { - const { article, format, renderingTarget } = props; - const { - config: { host }, - } = article; - const isWeb = renderingTarget === 'Web'; - const isApps = renderingTarget === 'Apps'; - const renderAds = isWeb && !article.shouldHideAds; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const showBodyEndSlot = - isWeb && - (parse(article.slotMachineFlags ?? '').showBodyEnd || - article.config.switches.slotBodyEnd); - - const { branding } = article.commercialProperties[article.editionId]; - - return ( -
- - - - - - - - - - - - - -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - - )} -
- - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} - - - - {isApps && ( - - - - )} - - {showBodyEndSlot && ( - - - - )} -
-
- - -
-
-
-
- - - - - - - -
- ); -}; diff --git a/dotcom-rendering/src/layouts/interactives/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/interactives/InteractiveLayout.tsx deleted file mode 100644 index b79ecd2409c..00000000000 --- a/dotcom-rendering/src/layouts/interactives/InteractiveLayout.tsx +++ /dev/null @@ -1,379 +0,0 @@ -import { palette as sourcePalette } from '@guardian/source/foundations'; -import { AdSlot, MobileStickyContainer } from '../../components/AdSlot.web'; -import { AppsFooter } from '../../components/AppsFooter.island'; -import { Carousel } from '../../components/Carousel.island'; -import { DirectoryPageNavIsland } from '../../components/DirectoryPageNavIsland'; -import { DiscussionLayout } from '../../components/DiscussionLayout'; -import { Footer } from '../../components/Footer'; -import { HeaderAdSlot } from '../../components/HeaderAdSlot'; -import { InteractivesDisableArticleSwipe } from '../../components/InteractivesDisableArticleSwipe.island'; -import { InteractivesNativePlatformWrapper } from '../../components/InteractivesNativePlatformWrapper.island'; -import { Island } from '../../components/Island'; -import { LabsHeader } from '../../components/LabsHeader'; -import { Masthead } from '../../components/Masthead/Masthead'; -import { MostViewedFooterData } from '../../components/MostViewedFooterData.island'; -import { MostViewedFooterLayout } from '../../components/MostViewedFooterLayout'; -import { OnwardsUpper } from '../../components/OnwardsUpper.island'; -import { Section } from '../../components/Section'; -import { StickyBottomBanner } from '../../components/StickyBottomBanner.island'; -import { SubNav } from '../../components/SubNav.island'; -import { type ArticleFormat, ArticleSpecial } from '../../lib/articleFormat'; -import { canRenderAds } from '../../lib/canRenderAds'; -import { getContributionsServiceUrl } from '../../lib/contributions'; -import { decideStoryPackageTrails } 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'; -import { InteractiveGridV1 } from './InteractiveGridV1'; -import { InteractiveGridV2 } from './InteractiveGridV2'; - -interface Props { - article: ArticleDeprecated; - format: ArticleFormat; - renderingTarget: RenderingTarget; - serverTime?: number; -} - -interface WebProps extends Props { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppProps extends Props { - renderingTarget: 'Apps'; -} - -export const InteractiveLayout = (props: WebProps | AppProps) => { - const { article, format, renderingTarget, serverTime } = props; - const { - config: { isPaidContent, host, hasSurveyAd }, - editionId, - } = article; - - const isWeb = renderingTarget === 'Web'; - const isApps = renderingTarget === 'Apps'; - - // TODO: - // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render - // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. - - const showComments = article.isCommentable && !isPaidContent; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const renderAds = canRenderAds(article); - - const interactiveLayoutSwitchoverDate = new Date('2024-06-01T00:00:00Z'); - const publicationDate = new Date(article.webPublicationDate); - const isLegacyInteractive = - publicationDate < interactiveLayoutSwitchoverDate; - - return ( - <> - {isApps && ( - <> - - - - - - - - )} - {isWeb && ( -
- {renderAds && ( - -
- -
-
- )} - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> -
- )} - - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - - {isWeb && renderAds && hasSurveyAd && ( - - )} - -
- - - {isLegacyInteractive ? ( - - ) : ( - - )} - - {isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - {showComments && ( -
- -
- )} - - {!isPaidContent && ( -
- - - - - -
- )} - - {isWeb && renderAds && ( -
- -
- )} -
- {isWeb && ( - <> - {props.NAV.subNavSections && ( -
- - - -
- )} -
-
-
- - - - - - - - )} - - {isApps && ( - <> -
- - - -
- - )} - - ); -}; From 093693dc2a896fa84a2dc5a8628562f1315b1406 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 1 Jul 2026 16:23:48 +0100 Subject: [PATCH 12/16] Fold interactive grid CSS into standard --- .../src/layouts/lib/articleArrangements.ts | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2e4323eb67e..56ae419ee81 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -149,42 +149,11 @@ const mediaCss: LayoutCssMap = { }, }; -const interactiveCss: LayoutCssMap = { - title: { - tablet: 'grid-row: 1;', - leftCol: `grid-row: 1; ${grid.column.left}`, - }, - headline: { - tablet: 'grid-row: 2;', - leftCol: 'grid-row: 1;', - }, - standfirst: { - tablet: 'grid-row: 3;', - leftCol: 'grid-row: 2;', - }, - media: { - tablet: 'grid-row: 4;', - leftCol: 'grid-row: 3;', - }, - meta: { - tablet: 'grid-row: 5;', - leftCol: `grid-row: 3 / span 2; ${grid.column.left};`, - }, - body: { - tablet: `grid-row: 6; ${grid.column.all};`, - leftCol: 'grid-row: 4;', - }, - 'right-column': { - desktop: `grid-row: 1 / span 6; ${grid.column.right};`, - leftCol: `grid-row: 1 / span 4; ${grid.column.right};`, - }, -}; - const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, - interactive: interactiveCss, + interactive: standardCss, }; /** From 796c14f29f5e29a4272121f2c798a38dc27ec7cd Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 1 Jul 2026 16:25:48 +0100 Subject: [PATCH 13/16] Revert "Fold interactive grid CSS into standard" This reverts commit 4978a42d4941f556650bc306eafbf7e65f80ca33. --- .../src/layouts/lib/articleArrangements.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 56ae419ee81..2e4323eb67e 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -149,11 +149,42 @@ const mediaCss: LayoutCssMap = { }, }; +const interactiveCss: LayoutCssMap = { + title: { + tablet: 'grid-row: 1;', + leftCol: `grid-row: 1; ${grid.column.left}`, + }, + headline: { + tablet: 'grid-row: 2;', + leftCol: 'grid-row: 1;', + }, + standfirst: { + tablet: 'grid-row: 3;', + leftCol: 'grid-row: 2;', + }, + media: { + tablet: 'grid-row: 4;', + leftCol: 'grid-row: 3;', + }, + meta: { + tablet: 'grid-row: 5;', + leftCol: `grid-row: 3 / span 2; ${grid.column.left};`, + }, + body: { + tablet: `grid-row: 6; ${grid.column.all};`, + leftCol: 'grid-row: 4;', + }, + 'right-column': { + desktop: `grid-row: 1 / span 6; ${grid.column.right};`, + leftCol: `grid-row: 1 / span 4; ${grid.column.right};`, + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, - interactive: standardCss, + interactive: interactiveCss, }; /** From 34cc76f06beda157756ccc90b3125241f1053743 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 7 Jul 2026 17:02:30 +0100 Subject: [PATCH 14/16] Rebase tidying --- .../layouts/interactives/InteractiveArticleGridDeprecated.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/dotcom-rendering/src/layouts/interactives/InteractiveArticleGridDeprecated.tsx b/dotcom-rendering/src/layouts/interactives/InteractiveArticleGridDeprecated.tsx index 3f2635c63d9..31e12e72702 100644 --- a/dotcom-rendering/src/layouts/interactives/InteractiveArticleGridDeprecated.tsx +++ b/dotcom-rendering/src/layouts/interactives/InteractiveArticleGridDeprecated.tsx @@ -204,7 +204,6 @@ export const InteractiveArticleGridDeprecated = ( pageId={article.pageId} webTitle={article.webTitle} ajaxUrl={article.config.ajaxUrl} - abTests={article.config.abTests} switches={article.config.switches} isAdFreeUser={article.isAdFreeUser} isSensitive={article.config.isSensitive} @@ -349,7 +348,6 @@ export const InteractiveArticleGridDeprecated = ( pageId={article.pageId} webTitle={article.webTitle} ajaxUrl={article.config.ajaxUrl} - abTests={article.config.abTests} switches={article.config.switches} isSensitive={article.config.isSensitive} isAdFreeUser={article.isAdFreeUser} From 70a3d61e2ffb4ac4dda35358d91c3cb394bb420a Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 16 Jul 2026 10:11:49 +0100 Subject: [PATCH 15/16] Apply grid to sub meta Probably one to break out into its own PR --- dotcom-rendering/src/components/SubMeta.tsx | 15 +++ .../src/layouts/StandardLayoutArticleGrid.tsx | 93 +++++++++---------- 2 files changed, 61 insertions(+), 47 deletions(-) diff --git a/dotcom-rendering/src/components/SubMeta.tsx b/dotcom-rendering/src/components/SubMeta.tsx index c7f116e5cf4..fd44d0df3d1 100644 --- a/dotcom-rendering/src/components/SubMeta.tsx +++ b/dotcom-rendering/src/components/SubMeta.tsx @@ -14,6 +14,7 @@ import { grid } from '../grid'; import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import type { BaseLinkType } from '../model/extract-nav'; import { palette } from '../palette'; +import { palette as themePalette } from '../palette'; import { Island } from './Island'; import { ShareButton } from './ShareButton.island'; @@ -235,8 +236,22 @@ export const SubMeta = ({ format.design === ArticleDesign.Gallery ? galleryStyles : bottomPadding, + grid.container, + css` + > * { + ${grid.column.centre} + } + `, ]} > + {format.design === ArticleDesign.Gallery && (
diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index dd5465aee59..2270a69e659 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -2,7 +2,6 @@ import { css } from '@emotion/react'; import { log } from '@guardian/libs'; import { from, space, until } from '@guardian/source/foundations'; import { Hide } from '@guardian/source/react-components'; -import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; import { AppsEpic } from '../components/AppsEpic.island'; import { ArticleBody } from '../components/ArticleBody'; @@ -187,7 +186,17 @@ export const StandardLayoutArticleGrid = ({ - + {format.design !== ArticleDesign.Audio && (
{isWeb && @@ -372,14 +381,6 @@ export const StandardLayoutArticleGrid = ({ /> )} - - {layoutType !== 'interactive' && ( - - - - - - - - )} + + + + + + + ); }; From ee74010d305775d2699d38374d75b677b83c968c Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Fri, 24 Jul 2026 12:07:30 +0100 Subject: [PATCH 16/16] Ed review adjustments --- .../components/MostViewedRightWithAd.island.tsx | 4 ++-- .../src/components/SlotBodyEnd.island.tsx | 5 +++++ dotcom-rendering/src/components/SubMeta.tsx | 15 +++++++++------ dotcom-rendering/src/layouts/StandardLayout.tsx | 3 ++- .../src/layouts/StandardLayoutArticleGrid.tsx | 3 ++- .../src/layouts/lib/articleArrangements.ts | 1 + dotcom-rendering/src/lib/ArticleRenderer.tsx | 4 +++- dotcom-rendering/src/lib/adStyles.ts | 2 ++ 8 files changed, 26 insertions(+), 11 deletions(-) diff --git a/dotcom-rendering/src/components/MostViewedRightWithAd.island.tsx b/dotcom-rendering/src/components/MostViewedRightWithAd.island.tsx index f2d7c2de9ed..71b490c4249 100644 --- a/dotcom-rendering/src/components/MostViewedRightWithAd.island.tsx +++ b/dotcom-rendering/src/components/MostViewedRightWithAd.island.tsx @@ -68,13 +68,13 @@ export const MostViewedRightWithAd = ({ /> ) : null} - {!isPaidContent && !shouldHideMostViewed ? ( + {!isPaidContent && !shouldHideMostViewed && ( - ) : null} + )} {isApps && }
diff --git a/dotcom-rendering/src/components/SlotBodyEnd.island.tsx b/dotcom-rendering/src/components/SlotBodyEnd.island.tsx index fc4bd849945..387a376dd62 100644 --- a/dotcom-rendering/src/components/SlotBodyEnd.island.tsx +++ b/dotcom-rendering/src/components/SlotBodyEnd.island.tsx @@ -13,6 +13,7 @@ import type { } from '@guardian/support-dotcom-components/dist/dotcom/types'; import type { EpicProps } from '@guardian/support-dotcom-components/dist/shared/types'; import { useEffect, useState } from 'react'; +import { grid } from '../grid'; import { getArticleCounts } from '../lib/articleCount'; import { BrazeBannersSystemPlacementId, @@ -57,6 +58,10 @@ type Props = { const slotStyles = css` color: ${palette.neutral[7]}; + ${grid.container}; + & > *:first-child { + ${grid.column.centre}; + } `; const buildReaderRevenueEpicConfig = ( diff --git a/dotcom-rendering/src/components/SubMeta.tsx b/dotcom-rendering/src/components/SubMeta.tsx index fd44d0df3d1..18f9eb90aaa 100644 --- a/dotcom-rendering/src/components/SubMeta.tsx +++ b/dotcom-rendering/src/components/SubMeta.tsx @@ -236,12 +236,15 @@ export const SubMeta = ({ format.design === ArticleDesign.Gallery ? galleryStyles : bottomPadding, - grid.container, - css` - > * { - ${grid.column.centre} - } - `, + format.design === ArticleDesign.Interactive && + !isDeprecatedInteractiveLayout + ? css` + ${grid.container}; + > * { + ${grid.column.centre} + } + ` + : undefined, ]} > { const interactiveLayoutSwitchoverDate = new Date('2024-06-01T00:00:00Z'); const publicationDate = new Date(article.webPublicationDate); const isLegacyInteractive = - publicationDate < interactiveLayoutSwitchoverDate; + publicationDate < interactiveLayoutSwitchoverDate && + format.design === ArticleDesign.Interactive; const showComments = article.isCommentable && !isPaidContent; diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 2270a69e659..280bd45a620 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -424,7 +424,8 @@ export const StandardLayoutArticleGrid = ({ !!article.config.shouldHideReaderRevenue } shouldHideMostViewed={ - format.design === ArticleDesign.Audio + format.design === ArticleDesign.Audio || + format.design === ArticleDesign.Interactive } /> diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2e4323eb67e..0dd0b418916 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -171,6 +171,7 @@ const interactiveCss: LayoutCssMap = { leftCol: `grid-row: 3 / span 2; ${grid.column.left};`, }, body: { + mobile: `${grid.column.all};`, tablet: `grid-row: 6; ${grid.column.all};`, leftCol: 'grid-row: 4;', }, diff --git a/dotcom-rendering/src/lib/ArticleRenderer.tsx b/dotcom-rendering/src/lib/ArticleRenderer.tsx index 618600ec032..60542c31f76 100644 --- a/dotcom-rendering/src/lib/ArticleRenderer.tsx +++ b/dotcom-rendering/src/lib/ArticleRenderer.tsx @@ -232,7 +232,9 @@ export const ArticleRenderer = ({ css={[ commercialPosition, spacefinderAdStyles, - !isOldInteractive && interactiveLayoutCSS, + format.design === ArticleDesign.Interactive && + !isOldInteractive && + interactiveLayoutCSS, ]} > {renderingTarget === 'Apps' diff --git a/dotcom-rendering/src/lib/adStyles.ts b/dotcom-rendering/src/lib/adStyles.ts index 87d4df26645..a1b74b2460e 100644 --- a/dotcom-rendering/src/lib/adStyles.ts +++ b/dotcom-rendering/src/lib/adStyles.ts @@ -122,6 +122,8 @@ const spacefinderAdSlotContainerStyles = css` text-align: center; display: flex; justify-content: center; + /* TODO: Revisit ads positioning/sizing nicely in interactive body grid */ + width: 100%; max-width: 620px; margin-left: 0;