diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index ad089db43e1..9161153a16d 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -251,6 +251,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/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 65ebfbc5994..2b340fbeb64 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -1,7 +1,6 @@ -import { css } from '@emotion/react'; +import { css, type SerializedStyles } from '@emotion/react'; import { log } from '@guardian/libs'; import { - from, palette as sourcePalette, space, until, @@ -19,7 +18,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 +25,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,13 +36,13 @@ 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, type ArticleFormat, @@ -61,247 +58,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 +76,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 +171,8 @@ export const StandardLayout = (props: WebProps | AppProps) => { const renderAds = canRenderAds(article); + const layoutType: LayoutType = isMedia ? 'media' : 'standard'; + return ( <> {isWeb && ( @@ -459,163 +247,112 @@ 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 ? ( + ) : ( - - )} - - -
- -
-
- - - - -
-
- {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, +}; + +// 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: { + 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;', + }, + 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 layoutCssMaps: Record = { + standard: standardCss, + media: mediaCss, +}; + +/** + * 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} + `; +};