diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index c6c2ee2eb78..5ae764d0e73 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -27,7 +27,6 @@ import { ArticleSpecial, Pillar, } from '../lib/articleFormat'; -import { getZIndex } from '../lib/getZIndex'; import { palette as themePalette } from '../palette'; import type { StarRating as Rating } from '../types/content'; import type { TagType } from '../types/tag'; @@ -45,6 +44,7 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; + isInverted?: boolean; }; const topPadding = css` @@ -205,30 +205,20 @@ const invertedStyles = css` box-decoration-break: clone; `; -const immersiveStyles = css` - min-height: 112px; - padding-bottom: ${space[6]}px; - padding-left: ${space[1]}px; - - ${from.mobileLandscape} { - padding-left: ${space[3]}px; - } - - ${from.tablet} { - padding-left: ${space[1]}px; - } - - margin-right: ${space[5]}px; -`; - const darkBackground = css` background-color: ${themePalette('--headline-background')}; `; const invertedText = css` - white-space: pre-wrap; - padding-bottom: ${space[1]}px; - padding-right: ${space[1]}px; + ${from.desktop} { + color: white; + background-color: black; + white-space: pre-wrap; + padding-bottom: ${space[1]}px; + padding-right: ${space[1]}px; + margin-left: -10px; + padding-left: 10px; + } `; const maxWidth = css` @@ -246,35 +236,6 @@ const invertedWrapper = css` margin-left: 6px; `; -const immersiveWrapper = css` - /* - Make sure we vertically align the headline font with the body font - */ - margin-left: 6px; - - ${from.tablet} { - margin-left: 16px; - } - - ${from.leftCol} { - margin-left: 25px; - } - - /* - We need this grow to ensure the headline fills the main content column - */ - flex-grow: 1; - /* - This z-index is what ensures the headline text shows above the pseudo black - box that extends the black background to the right - */ - z-index: ${getZIndex('articleHeadline')}; - - ${until.mobileLandscape} { - margin-right: 40px; - } -`; - // Due to MainMedia using position: relative, this seems to effect the rendering order // To mitigate we use z-index // TODO: find a cleaner solution @@ -283,59 +244,58 @@ const zIndex = css` `; const ageWarningMargins = (format: ArticleFormat) => { - if (format.design === ArticleDesign.Gallery) { + if ( + format.design === ArticleDesign.Gallery || + format.display === ArticleDisplay.Immersive + ) { return ''; } - return format.display === ArticleDisplay.Immersive - ? css` - margin-left: 0px; - margin-bottom: 0px; - - ${from.tablet} { - margin-left: 10px; - } + return css` + margin-top: 12px; + margin-left: -10px; + margin-bottom: 6px; - ${from.leftCol} { - margin-left: 20px; - } - ` - : css` - margin-top: 12px; - margin-left: -10px; - margin-bottom: 6px; - - ${from.tablet} { - margin-left: -20px; - } + ${from.tablet} { + margin-left: -20px; + } - ${from.leftCol} { - margin-left: -10px; - margin-top: 0; - } - `; + ${from.leftCol} { + margin-left: -10px; + margin-top: 0; + } + `; }; -const backgroundStyles = css` - background-color: ${themePalette('--age-warning-wrapper-background')}; -`; - const WithAgeWarning = ({ tags, webPublicationDateDeprecated, format, children, + snapToInverted = false, }: { tags: TagType[]; webPublicationDateDeprecated: string; format: ArticleFormat; children: React.ReactNode; + snapToInverted?: boolean; }) => { const age = getAgeWarning(tags, webPublicationDateDeprecated); if (age) { return ( <> -
+
{children} @@ -430,6 +390,7 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, + isInverted = false, }: Props) => { switch (format.display) { case ArticleDisplay.Immersive: { @@ -456,12 +417,13 @@ export const ArticleHeadline = ({ format.theme === ArticleSpecial.Labs ? labsFont : headlineFont(format), - invertedText, - css` - color: ${themePalette( - '--headline-colour', - )}; - `, + isInverted + ? [invertedText, darkBackground] + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > {headlineString} @@ -487,16 +449,17 @@ export const ArticleHeadline = ({ webPublicationDateDeprecated } format={format} + snapToInverted={true} >

diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index 61be63fe1a3..b178b92930b 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -264,7 +264,6 @@ const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { `; default: return css` - max-width: 280px; ${from.tablet} { max-width: 460px; } diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index ad089db43e1..455229450c5 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -88,35 +88,55 @@ const paddedContainer = ` // ----- Vertical Rules ----- // type VerticalRuleOptions = { - centre?: boolean; + plusChild?: number; }; /** * Render Guardian grid vertical rules. * * Left and right rules are always present. - * A centre rule can optionally be enabled. + * A centre rule can optionally be enabled, anchored to the top of the first + * child by default, or to the nth child if a number is passed. * * Usage: * css([grid.container, grid.verticalRules()]) * css([grid.container, grid.verticalRules({ centre: true })]) + * css([grid.container, grid.verticalRules({ centre: 3 })]) */ -const optionalCentreRule = `/* CENTRE RULE */ - & > *:first-child::before { - grid-column: centre-column-start; - transform: translateX(-${columnGap}); - ${fromBreakpoint.leftCol} { - transform: translateX(calc(-${columnGap} / 2)); - } + +// The centre rule is self-contained on the nth child element rather than on +// the grid container, so that `top: 0` aligns to that element's top edge. +// `bottom` uses a large negative value to extend the rule down to the +// container's bottom; ensure `overflow: hidden` is set on the container +const optionalCentreRule = (nth: number): string => `/* CENTRE RULE */ + & > *:nth-child(${nth}) { + position: relative; + + &::before { + position: absolute; + top: 0; + bottom: -9999px; + width: 1px; + background-color: ${palette('--article-border')}; + content: ''; + grid-column: centre-column-start; + transform: translateX(-${columnGap}); + + ${fromBreakpoint.leftCol} { + transform: translateX(calc(-${columnGap} / 2)); + } + } }`; -const verticalRules = (options: VerticalRuleOptions = {}): string => ` +const verticalRules = (options: VerticalRuleOptions = {}): string => { + const { plusChild: centreChild } = options; + + return ` ${fromBreakpoint.tablet} { position: relative; &::before, - &::after - ${options.centre ? ', & > *:first-child::before' : ''} { + &::after { position: absolute; top: 0; bottom: 0; @@ -145,8 +165,9 @@ const verticalRules = (options: VerticalRuleOptions = {}): string => ` } } - ${options.centre ? optionalCentreRule : ''} -`; + ${centreChild !== undefined ? optionalCentreRule(centreChild) : ''} + }`; +}; // ----- API ----- // @@ -251,6 +272,10 @@ const grid = { verticalRules, } as const; +// ----- Types ----- // +type ColumnPreset = keyof typeof grid.column; + // ----- Exports ----- // +export type { Line, ColumnPreset }; export { grid }; diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index aae1cbfca5c..d26077f8348 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -10,7 +10,6 @@ import { GalleryLayout } from './GalleryLayout'; import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; -import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; @@ -58,7 +57,7 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { } default: { return ( - { } default: { return ( - ( -
- {children} -
-); - -const maxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; - -const linesMargin = css` - ${from.leftCol} { - margin-top: ${space[5]}px; - } -`; - -const stretchLines = css` - ${until.phablet} { - margin-left: -20px; - margin-right: -20px; - } - ${until.mobileLandscape} { - margin-left: -10px; - margin-right: -10px; - } -`; - -interface CommonProps { - article: ArticleDeprecated; - format: ArticleFormat; - serverTime?: number; -} - -interface WebProps extends CommonProps { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppProps extends CommonProps { - renderingTarget: 'Apps'; -} - -const Box = ({ children }: { children: React.ReactNode }) => ( -
- {children} -
-); - -export const ImmersiveLayout = (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 mainMedia = article.mainMediaElements[0]; - - const captionText = decideMainMediaCaption(mainMedia); - - const HEADLINE_OFFSET = mainMedia ? 120 : 0; - - const { branding } = article.commercialProperties[article.editionId]; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const isLabs = format.theme === ArticleSpecial.Labs; - - /** - We need change the height values depending on whether the labs header is there or not to keep - the headlines appearing at a consistent height between labs and non labs immersive articles. - */ - - const labsHeaderHeight = LABS_HEADER_HEIGHT; - const combinedHeight = (minHeaderHeightPx + labsHeaderHeight).toString(); - - const navAndLabsHeaderHeight = isLabs - ? `${combinedHeight}px` - : `${minHeaderHeightPx}px`; - - const hasMainMediaStyles = css` - height: calc(80vh - ${navAndLabsHeaderHeight}); - /** - 80vh is normally enough but don't let the content shrink vertically too - much just in case - */ - min-height: calc(25rem - ${navAndLabsHeaderHeight}); - ${from.desktop} { - height: calc(100vh - ${navAndLabsHeaderHeight}); - min-height: calc(31.25rem - ${navAndLabsHeaderHeight}); - } - ${from.wide} { - min-height: calc(50rem - ${navAndLabsHeaderHeight}); - } - `; - const LeftColCaption = () => ( -
- -
- ); - - const renderAds = canRenderAds(article); - - return ( - <> - {isWeb && ( - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> - )} - - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - -
-
- -
- {mainMedia && ( - <> -
-
} - > - -
- -
- -
-
-
- - )} -
- - {isWeb && renderAds && hasSurveyAd && ( - - )} - -
- {isApps && renderAds && ( - - - - )} -
- - {/* Above leftCol, the Caption is controlled by Section ^^ */} - - - - - - - {format.design === ArticleDesign.PhotoEssay ? ( - <> - ) : ( - - )} - - - <> - {!mainMedia && ( -
- -
- )} - -
- - <> - {!mainMedia && ( -
- -
- )} - -
- - - - - {!!article.byline && ( - - )} - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} -
- - {format.design === ArticleDesign.PhotoEssay && - !isLabs ? ( - <> - ) : ( -
-
- {format.theme === - ArticleSpecial.Labs ? ( - - ) : ( - - )} -
-
- )} -
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - <> - - {!!article.affiliateLinksDisclaimer && ( - - )} - - )} -
-
- - - - {showBodyEndSlot && ( - - - - )} - - - - - -
- - <> - {mainMedia && isWeb && renderAds && ( -
- { - - } -
- )} - -
-
-
-
-
- {!isLabs && isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - - {showComments && ( -
- -
- )} - {!isPaidContent && ( -
- - - - - -
- )} - {!isLabs && isWeb && renderAds && ( -
- -
- )} -
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - - {isWeb && ( - <> -
-
-
- - - - - - - {renderAds && ( - - )} - - )} - {isApps && ( -
- - - -
- )} - - ); -}; diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 65ebfbc5994..82a459582f8 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/react'; +import { css, type SerializedStyles } from '@emotion/react'; import { log } from '@guardian/libs'; import { from, @@ -19,7 +19,6 @@ 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'; @@ -27,7 +26,6 @@ import { DiscussionLayout } from '../components/DiscussionLayout'; import { FootballMatchHeaderWrapper } from '../components/FootballMatchHeaderWrapper.island'; import { FootballMatchInfoWrapper } from '../components/FootballMatchInfoWrapper.island'; import { Footer } from '../components/Footer'; -import { GridItem } from '../components/GridItem'; import { GuardianLabsLines } from '../components/GuardianLabsLines'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; import { Island } from '../components/Island'; @@ -39,15 +37,16 @@ import { MostViewedFooterData } from '../components/MostViewedFooterData.island' import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; import { MostViewedRightWithAd } from '../components/MostViewedRightWithAd.island'; import { OnwardsUpper } from '../components/OnwardsUpper.island'; -import { RightColumn } from '../components/RightColumn'; 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 { grid } from '../grid'; import { ArticleDesign, + ArticleDisplay, type ArticleFormat, ArticleSpecial, } from '../lib/articleFormat'; @@ -61,247 +60,13 @@ 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/furnitureArrangements'; import { BannerWrapper, Stuck } from './lib/stickiness'; -const StandardGrid = ({ - children, - isMatchReport, - isMedia, -}: { - children: React.ReactNode; - isMatchReport: boolean; - isMedia: boolean; -}) => ( -
- {children} -
-); - -const maxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; - const stretchLines = css` ${until.phablet} { margin-left: -20px; @@ -313,6 +78,29 @@ const stretchLines = css` } `; +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; @@ -385,6 +173,37 @@ export const StandardLayout = (props: WebProps | AppProps) => { const renderAds = canRenderAds(article); + const isImmersive = format.display === ArticleDisplay.Immersive; + + const firstMainMediaElement = article.mainMediaElements[0]; + const mainMediaUrl: string | undefined = + firstMainMediaElement?._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ? firstMainMediaElement.media.allImages[0]?.url + : undefined; + + const orientation = (url: string): 'portrait' | 'landscape' | 'square' => { + const match = url.match(/\/\d+_\d+_(\d+)_(\d+)\/\d+\.\w+$/); + if (!match) return 'landscape'; + const [, width, height] = match.map(Number); + if (width == null || height == null) return 'landscape'; + if (height > width) return 'portrait'; + if (width > height) return 'landscape'; + return 'square'; + }; + + const mainMediaOrientation = + mainMediaUrl != null ? orientation(mainMediaUrl) : 'landscape'; + + const layoutType: LayoutType = + isImmersive && mainMediaOrientation === 'landscape' + ? 'immersiveLandscape' + : isImmersive && mainMediaOrientation === 'portrait' + ? 'immersivePortrait' + : isVideo + ? 'media' + : 'standard'; + return ( <> {isWeb && ( @@ -459,163 +278,174 @@ export const StandardLayout = (props: WebProps | AppProps) => { pageId={article.pageId} pageTags={article.tags} /> -
- - -
- -
-
- - - - - - {format.theme === ArticleSpecial.Labs ? ( - <> - ) : ( - - )} - - -
- -
-
- - - - -
-
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
+ + + + + + + + + + + + + {layoutType !== 'immersivePortrait' && ( +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )}
- {isApps ? ( - <> - -
- -
-
- -
- -
- {!!article.affiliateLinksDisclaimer && ( - - )} -
- - ) : ( -
+ )} + {isApps ? ( + <> + + + + { {!!article.affiliateLinksDisclaimer && ( )} -
- )} -
- - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - - {!isVideo && ( -
- - - -
- )}
- )} - - - - - {isApps && ( - + ) : ( + + )} +
+ + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + + {!isVideo && ( +
- - + + + +
)} +
+ )} + + + - {showBodyEndSlot && ( - - - - )} - - + + + )} + + {showBodyEndSlot && ( + + + + )} + + + +
+ + + + - - - -
- - - - - -
-
- -
+ + + + {isWeb && renderAds && !isLabs && (
= { + mobile: until.tablet, + tablet: from.tablet, + desktop: from.desktop, + leftCol: from.leftCol, + wide: from.wide, +}; + +// 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 mediaCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + leftCol: + 'grid-row: 1; grid-column: left-column-start / left-column-end;', + }, + headline: { + mobile: 'grid-row: 2;', + leftCol: 'grid-row: 1;', + }, + 'main-media': { + mobile: 'grid-row: 3;', + desktop: + 'grid-row: 3; grid-column: centre-column-start / right-column-start;', + leftCol: + 'grid-row: 2; grid-column: centre-column-start / right-column-start;', + }, + standfirst: { + mobile: 'grid-row: 4;', + desktop: + 'grid-row: 4; grid-column: centre-column-start / right-column-start;', + leftCol: + 'grid-row: 3; grid-column: centre-column-start / right-column-start;', + }, + meta: { + mobile: 'grid-row: 5;', + leftCol: + 'grid-row: 2 / span 3; grid-column: left-column-start / left-column-end;', + }, + body: { + desktop: + 'grid-row: 6; grid-column: centre-column-start / right-column-start;', + leftCol: + 'grid-row: 4; grid-column: centre-column-start / right-column-start;', + }, + 'right-column': { + desktop: + 'grid-row: 3 / span 4; grid-column: right-column-start / right-column-end;', + leftCol: + 'grid-row: 2 / span 3; grid-column: right-column-start / right-column-end;', + }, +}; + +const immersivePortraitCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: 'grid-row: 1; grid-column: centre-column-start / 8;', + leftCol: 'grid-row: 1; grid-column: left-column-start / 9;', + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: 'grid-row: 2; grid-column: centre-column-start / 8;', + leftCol: 'grid-row: 2; grid-column: left-column-start / 9;', + wide: 'grid-row: 2; grid-column: left-column-start / 10;', + }, + 'main-media': { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: 'grid-row: 1 / span 4; grid-column: 8 / right-column-end;', + leftCol: 'grid-row: 1 / span 3; grid-column: 9 / right-column-end;', + wide: 'grid-row: 1 / span 3; grid-column: 10 / right-column-end;', + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: 'grid-row: 3; grid-column: centre-column-start / 7;', + leftCol: 'grid-row: 3; grid-column: centre-column-start / 8;', + wide: 'grid-row: 3; grid-column: centre-column-start / 9;', + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: 'grid-row: 4; grid-column: centre-column-start / 8;', + leftCol: + 'grid-row: 3; grid-column: left-column-start / left-column-end;', + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: + 'grid-row: 5; grid-column: right-column-start / right-column-end;', + leftCol: + 'grid-row: 4; grid-column: right-column-start / right-column-end;', + }, +}; + +const immersiveLandscapeCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: 'grid-row: 2;', + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: 'grid-row: 3;', + }, + 'main-media': { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: + 'grid-row: 1 / span 3; grid-column: centre-column-start / right-column-end;', + leftCol: + 'grid-row: 1 / span 3; grid-column: left-column-start / right-column-end;', + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: 'grid-row: 5; grid-column: centre-column-start / 8;', + leftCol: + 'grid-row: 4 / span 2; grid-column: left-column-start / left-column-end;', + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: + 'grid-row: 5; grid-column: right-column-start / right-column-end;', + leftCol: + 'grid-row: 4; grid-column: right-column-start / right-column-end;', + }, +}; + +const layoutCssMaps: Record = { + standard: standardCss, + media: mediaCss, + immersiveLandscape: immersiveLandscapeCss, + immersivePortrait: immersivePortraitCss, +}; + +/** + * 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} + `; +}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index a3199607e75..d5e5a94dcd1 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -88,7 +88,7 @@ const textblockTextDark: PaletteFunction = () => 'inherit'; const headlineTextLight: PaletteFunction = ({ design, display, theme }) => { switch (display) { case ArticleDisplay.Immersive: - return sourcePalette.neutral[97]; + return sourcePalette.neutral[7]; default: { switch (design) { case ArticleDesign.Editorial: @@ -193,19 +193,8 @@ const headlineMatchTextLight: PaletteFunction = (format) => const headlineMatchTextDark: PaletteFunction = (format) => seriesTitleMatchTextDark(format); -const headlineBackgroundLight: PaletteFunction = ({ - display, - design, - theme, -}) => { +const headlineBackgroundLight: PaletteFunction = ({ display, design }) => { switch (display) { - case ArticleDisplay.Immersive: - switch (theme) { - case ArticleSpecial.SpecialReport: - return sourcePalette.specialReport[300]; - default: - return sourcePalette.neutral[7]; - } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: