1- import { css } from '@emotion/react' ;
1+ import { css , type SerializedStyles } from '@emotion/react' ;
22import { log } from '@guardian/libs' ;
33import {
44 from ,
@@ -83,17 +83,150 @@ const stretchLines = css`
8383 }
8484` ;
8585
86- const tabletRow = ( row : number ) => css `
87- ${ from . tablet } {
88- grid-row : ${ row } ;
89- }
90- ` ;
86+ type LayoutType = 'standard' | 'matchReport' | 'media' | 'labs' ;
9187
92- const leftColRow = ( row : number ) => css `
93- ${ from . leftCol } {
94- grid-row : ${ row } ;
95- }
96- ` ;
88+ type Area =
89+ | 'title'
90+ | 'headline'
91+ | 'standfirst'
92+ | 'main-media'
93+ | 'meta'
94+ | 'body'
95+ | 'right-column'
96+ | 'match-nav'
97+ | 'match-tabs' ;
98+
99+ type LayoutRows = Partial <
100+ Record <
101+ Area ,
102+ { mobile ?: number ; tablet ?: number ; leftCol ?: number ; desktop ?: number }
103+ >
104+ > ;
105+
106+ const rowMaps : Record < LayoutType , LayoutRows > = {
107+ standard : {
108+ title : { tablet : 1 } ,
109+ headline : { tablet : 2 , leftCol : 1 } ,
110+ standfirst : { tablet : 3 , leftCol : 2 } ,
111+ 'main-media' : { tablet : 4 , leftCol : 3 } ,
112+ meta : { tablet : 5 , leftCol : 3 } ,
113+ } ,
114+ matchReport : {
115+ 'match-nav' : { tablet : 1 } ,
116+ 'match-tabs' : { tablet : 2 } ,
117+ title : { tablet : 3 , leftCol : 1 } ,
118+ headline : { tablet : 4 , leftCol : 3 } ,
119+ standfirst : { tablet : 5 , leftCol : 4 } ,
120+ 'main-media' : { tablet : 6 , leftCol : 5 } ,
121+ meta : { tablet : 7 , leftCol : 5 } ,
122+ } ,
123+ media : {
124+ title : { mobile : 1 , tablet : 1 } ,
125+ headline : { mobile : 2 , tablet : 2 , leftCol : 1 } ,
126+ 'main-media' : { mobile : 3 , tablet : 3 , leftCol : 2 } ,
127+ standfirst : { mobile : 4 , tablet : 4 , leftCol : 3 } ,
128+ meta : { mobile : 5 , tablet : 5 , leftCol : 2 } ,
129+ } ,
130+ labs : {
131+ title : { tablet : 1 } ,
132+ headline : { tablet : 2 , leftCol : 1 } ,
133+ standfirst : { tablet : 3 , leftCol : 2 } ,
134+ 'main-media' : { tablet : 3 } ,
135+ meta : { tablet : 4 , leftCol : 3 } ,
136+ 'match-nav' : { tablet : 1 } ,
137+ 'match-tabs' : { tablet : 1 } ,
138+ } ,
139+ } ;
140+
141+ const rowCss = ( area : Area , layoutType : LayoutType ) => {
142+ const rows = rowMaps [ layoutType ] [ area ] ?? { } ;
143+
144+ return css ( [
145+ rows . mobile != null &&
146+ css `
147+ ${ until . tablet } {
148+ grid-row : ${ rows . mobile } ;
149+ }
150+ ` ,
151+ rows . tablet != null &&
152+ css `
153+ ${ from . tablet } {
154+ grid-row : ${ rows . tablet } ;
155+ }
156+ ` ,
157+ rows . leftCol != null &&
158+ css `
159+ ${ from . leftCol } {
160+ grid-row : ${ rows . leftCol } ;
161+ }
162+ ` ,
163+ rows . desktop != null &&
164+ css `
165+ ${ from . desktop } {
166+ grid-row : ${ rows . desktop } ;
167+ }
168+ ` ,
169+ ] ) ;
170+ } ;
171+ interface GridItemProps {
172+ area : Area ;
173+ layoutType : LayoutType ;
174+ columns ?: {
175+ tablet ?: 'left' | 'centre' | 'right' ;
176+ desktop ?: 'left' | 'centre' | 'right' ;
177+ leftCol ?: 'left' | 'centre' | 'right' ;
178+ } ;
179+ element ?: 'div' | 'article' | 'main' | 'aside' | 'section' ;
180+ customCss ?: SerializedStyles ;
181+ children : React . ReactNode ;
182+ }
183+
184+ const GridItem = ( {
185+ area,
186+ layoutType,
187+ columns,
188+ element : Element = 'div' ,
189+ customCss,
190+ children,
191+ } : GridItemProps ) => {
192+ const mobileCol = 'centre' ;
193+ const tabletCol = columns ?. tablet ?? mobileCol ;
194+ const leftColCol = columns ?. leftCol ?? mobileCol ;
195+ const desktopCol = columns ?. desktop ?? mobileCol ;
196+
197+ return (
198+ < Element
199+ data-gu-name = { area }
200+ css = { css ( [
201+ grid . column [ mobileCol ] ,
202+ rowCss ( area , layoutType ) ,
203+ customCss ,
204+
205+ // Override column at breakpoints if specified
206+ columns ?. tablet &&
207+ css `
208+ ${ from . tablet } {
209+ ${ grid . column [ tabletCol ] } ;
210+ }
211+ ` ,
212+ columns ?. desktop &&
213+ css `
214+ ${ from . desktop } {
215+ ${ grid . column [ desktopCol ] } ;
216+ }
217+ ` ,
218+ columns ?. leftCol &&
219+ css `
220+ ${ from . leftCol } {
221+ ${ grid . column [ leftColCol ] } ;
222+ }
223+ ` ,
224+ ] ) }
225+ >
226+ { children }
227+ </ Element >
228+ ) ;
229+ } ;
97230
98231interface Props {
99232 article : ArticleDeprecated ;
@@ -170,6 +303,14 @@ export const StandardLayout = (props: WebProps | AppProps) => {
170303
171304 const renderAds = canRenderAds ( article ) ;
172305
306+ const layoutType : LayoutType = isLabs
307+ ? 'labs'
308+ : isMatchReport
309+ ? 'matchReport'
310+ : isMedia
311+ ? 'media'
312+ : 'standard' ;
313+
173314 return (
174315 < >
175316 { isWeb && (
@@ -239,12 +380,22 @@ export const StandardLayout = (props: WebProps | AppProps) => {
239380 pageId = { article . pageId }
240381 pageTags = { article . tags }
241382 />
242- < article >
383+ < article
384+ css = { css `
385+ background- color : ${ themePalette (
386+ '--article-background' ,
387+ ) } ;
388+ ` }
389+ >
243390 < div css = { css ( [ grid . container , grid . verticalRules ] ) } >
244391 < div className = "grid-rule rule-left" />
245392 < div className = "grid-rule rule-centre" />
246393 < div className = "grid-rule rule-right" />
247- < aside css = { css ( grid . column . centre ) } >
394+ < GridItem
395+ area = "match-nav"
396+ layoutType = { layoutType }
397+ element = "aside"
398+ >
248399 < div css = { maxWidth } >
249400 { isMatchReport && (
250401 < Island
@@ -263,8 +414,12 @@ export const StandardLayout = (props: WebProps | AppProps) => {
263414 </ Island >
264415 ) }
265416 </ div >
266- </ aside >
267- < aside css = { css ( grid . column . centre ) } >
417+ </ GridItem >
418+ < GridItem
419+ area = "match-tabs"
420+ layoutType = { layoutType }
421+ element = "aside"
422+ >
268423 < div css = { maxWidth } >
269424 { isMatchReport && (
270425 < Island
@@ -278,13 +433,11 @@ export const StandardLayout = (props: WebProps | AppProps) => {
278433 </ Island >
279434 ) }
280435 </ div >
281- </ aside >
282- < div
283- css = { css (
284- grid . column . centre ,
285- tabletRow ( 4 ) ,
286- leftColRow ( 3 ) ,
287- ) }
436+ </ GridItem >
437+ < GridItem
438+ area = "main-media"
439+ layoutType = { layoutType }
440+ element = "div"
288441 >
289442 < div css = { ! isMedia && maxWidth } >
290443 < MainMedia
@@ -305,20 +458,13 @@ export const StandardLayout = (props: WebProps | AppProps) => {
305458 contentLayout = "StandardLayout"
306459 />
307460 </ div >
308- </ div >
461+ </ GridItem >
309462 { ! isInFootballRedesignVariantGroup && (
310- < aside
311- css = { [
312- css ( grid . column . centre ) ,
313- css `
314- ${ from . leftCol } {
315- ${ grid . column . left } ;
316- grid- row: 1;
317- align- self: start;
318- }
319- ` ,
320- tabletRow ( 1 ) ,
321- ] }
463+ < GridItem
464+ area = "title"
465+ layoutType = { layoutType }
466+ columns = { { leftCol : 'left' } }
467+ element = "aside"
322468 >
323469 < ArticleTitle
324470 format = { format }
@@ -328,7 +474,7 @@ export const StandardLayout = (props: WebProps | AppProps) => {
328474 guardianBaseURL = { article . guardianBaseURL }
329475 isMatch = { ! ! footballMatchUrl }
330476 />
331- </ aside >
477+ </ GridItem >
332478 ) }
333479
334480 < div css = { css ( grid . column . centre ) } >
@@ -338,12 +484,10 @@ export const StandardLayout = (props: WebProps | AppProps) => {
338484 < Border />
339485 ) }
340486 </ div >
341- < div
342- css = { css ( [
343- grid . column . centre ,
344- tabletRow ( 2 ) ,
345- leftColRow ( 1 ) ,
346- ] ) }
487+ < GridItem
488+ area = "headline"
489+ layoutType = { layoutType }
490+ element = "div"
347491 >
348492 < div css = { maxWidth } >
349493 < ArticleHeadline
@@ -357,30 +501,22 @@ export const StandardLayout = (props: WebProps | AppProps) => {
357501 starRating = { article . starRating }
358502 />
359503 </ div >
360- </ div >
361- < div
362- css = { css ( [
363- grid . column . centre ,
364- tabletRow ( 3 ) ,
365- leftColRow ( 2 ) ,
366- ] ) }
504+ </ GridItem >
505+ < GridItem
506+ area = "standfirst"
507+ layoutType = { layoutType }
508+ element = "div"
367509 >
368510 < Standfirst
369511 format = { format }
370512 standfirst = { article . standfirst }
371513 />
372- </ div >
373- < aside
374- css = { [
375- css ( grid . column . centre ) ,
376- css `
377- ${ from . leftCol } {
378- ${ grid . column . left } ;
379- grid- row: 3;
380- align- self: start;
381- }
382- ` ,
383- ] }
514+ </ GridItem >
515+ < GridItem
516+ area = "meta"
517+ layoutType = { layoutType }
518+ columns = { { leftCol : 'left' } }
519+ element = "aside"
384520 >
385521 < div css = { maxWidth } >
386522 < div css = { stretchLines } >
@@ -493,8 +629,12 @@ export const StandardLayout = (props: WebProps | AppProps) => {
493629 ) }
494630 </ div >
495631 ) }
496- </ aside >
497- < div css = { css ( grid . column . centre ) } >
632+ </ GridItem >
633+ < GridItem
634+ area = "body"
635+ layoutType = { layoutType }
636+ element = "div"
637+ >
498638 { /* Only show Listen to Article button on App landscape views */ }
499639 { isApps && (
500640 < Hide until = "leftCol" >
@@ -632,20 +772,21 @@ export const StandardLayout = (props: WebProps | AppProps) => {
632772 }
633773 />
634774 </ ArticleContainer >
635- </ div >
636- < div
637- css = { [
638- css ( grid . column . centre ) ,
639- css `
640- display : none;
641- ${ from . desktop } {
642- display : block;
643- padding-top : 6px ;
644- ${ grid . column . right } ;
645- grid- row: 1 / span 999;
646- }
647- ` ,
648- ] }
775+ </ GridItem >
776+ < GridItem
777+ area = "right-column"
778+ layoutType = { layoutType }
779+ columns = { { desktop : 'right' } }
780+ customCss = { css `
781+ dis play: none;
782+ ${ from . desktop } {
783+ dis play: block;
784+ padding- to p: 6px;
785+ ${ grid . column . right } ;
786+ grid- row: 1 / span 999;
787+ }
788+ ` }
789+ element = "aside"
649790 >
650791 < Island
651792 priority = "feature"
@@ -668,7 +809,7 @@ export const StandardLayout = (props: WebProps | AppProps) => {
668809 }
669810 />
670811 </ Island >
671- </ div >
812+ </ GridItem >
672813 </ div >
673814 </ article >
674815
0 commit comments