Skip to content

Commit ed28bbb

Browse files
authored
Give the Mo's Diner landing page an event-themed hero banner (#64435)
1 parent aff61b2 commit ed28bbb

8 files changed

Lines changed: 176 additions & 9 deletions

File tree

Lines changed: 1 addition & 0 deletions
Loading

‎content/mos-diner/index.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
---
22
title: Mo's Diner
33
shortTitle: Mo's Diner
4-
intro: "Thanks for coming to Mo's Diner! Now enjoy the recipes to help you get the most out of GitHub, from planning your projects to maintaining main."
4+
intro: |-
5+
Thanks for coming to Mo's Diner!\
6+
Now enjoy the recipes to help you get the most out of GitHub, from planning your projects to maintaining main.
57
versions:
68
fpt: '*'
79
ghec: '*'
810
contentType: landing
911
layout: bespoke-landing
10-
heroImage: /assets/images/banner-images/mos-diner-pink
12+
heroTheme: mos-diner
1113
children:
1214
- /content/copilot/how-tos/copilot-on-github/copilot-for-github-tasks/use-copilot-to-create-or-update-issues
1315
- /content/copilot/tutorials/copilot-cookbook/communicate-effectively/summarize-repository-activity

‎src/frame/lib/frontmatter.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ export const schema: Schema = {
255255
heroImage: {
256256
type: 'string',
257257
},
258+
// Event-specific hero styling that replaces the hero image on landing pages
259+
heroTheme: {
260+
type: 'string',
261+
enum: ['mos-diner'],
262+
},
258263
interactive: {
259264
type: 'boolean',
260265
},

‎src/landings/components/bespoke/BespokeLanding.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const BespokeLanding = () => {
1212
title,
1313
intro,
1414
heroImage,
15+
heroTheme,
1516
introLinks,
1617
tocItems,
1718
carousels,
@@ -27,7 +28,13 @@ export const BespokeLanding = () => {
2728
<DefaultLayout>
2829
<UtmPreserver />
2930
<div data-search="article-body">
30-
<LandingHero title={title} intro={intro} heroImage={heroImage} introLinks={introLinks} />
31+
<LandingHero
32+
title={title}
33+
intro={intro}
34+
heroImage={heroImage}
35+
heroTheme={heroTheme}
36+
introLinks={introLinks}
37+
/>
3138

3239
{carousels &&
3340
Object.entries(carousels).map(([carouselKey, articles]) => (

‎src/landings/components/shared/LandingHero.module.scss‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,91 @@
101101
margin: -0.5rem -0.75rem;
102102
}
103103
}
104+
105+
// Mo's Diner event theme, set by the heroTheme frontmatter key. A wordmark cell
106+
// and an intro cell sit above a checkered strip, all bleeding to the section's
107+
// side rules. Colors follow the color mode, so the wordmark uses the text color.
108+
.mosDiner {
109+
--mos-diner-checker: color-mix(
110+
in srgb,
111+
#d5336b 45%,
112+
var(--brand-color-canvas-default)
113+
);
114+
--mos-diner-square: 0.75rem;
115+
116+
display: grid;
117+
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
118+
width: auto;
119+
margin-inline: -16px;
120+
121+
@media (min-width: 768px) {
122+
margin-inline: -32px;
123+
}
124+
125+
// Heading and intro become grid cells of their own.
126+
.heroContent {
127+
display: contents;
128+
}
129+
130+
.heroHeading {
131+
display: flex;
132+
align-items: center;
133+
justify-content: center;
134+
max-width: none;
135+
padding: 2.5rem 2rem;
136+
border-right: 1px solid var(--brand-color-border-subtle);
137+
}
138+
139+
.heroDescription,
140+
.heroButtons {
141+
grid-column: 2;
142+
align-self: center;
143+
padding-inline: 3.5rem 2rem;
144+
}
145+
146+
.themeAccent {
147+
grid-column: 1 / -1;
148+
}
149+
}
150+
151+
// The artwork's aspect ratio keeps the mask from cropping.
152+
.heroWordmark {
153+
display: block;
154+
width: clamp(12rem, 75%, 21rem);
155+
aspect-ratio: 470 / 238;
156+
background-color: currentColor;
157+
mask-repeat: no-repeat;
158+
mask-size: contain;
159+
}
160+
161+
// Offset conic gradients draw a two-row checkerboard strip.
162+
.themeAccent {
163+
height: calc(var(--mos-diner-square) * 2);
164+
background-image: conic-gradient(
165+
var(--mos-diner-checker) 25%,
166+
transparent 0 50%,
167+
var(--mos-diner-checker) 0 75%,
168+
transparent 0
169+
);
170+
background-size: calc(var(--mos-diner-square) * 2)
171+
calc(var(--mos-diner-square) * 2);
172+
background-position: left top;
173+
}
174+
175+
@media (max-width: 865px) {
176+
.mosDiner {
177+
grid-template-columns: minmax(0, 1fr);
178+
179+
.heroHeading {
180+
justify-content: flex-start;
181+
padding: 2rem 1.5rem 1rem;
182+
border-right: 0;
183+
}
184+
185+
.heroDescription,
186+
.heroButtons {
187+
grid-column: 1;
188+
padding: 0 1.5rem 2rem;
189+
}
190+
}
191+
}

‎src/landings/components/shared/LandingHero.tsx‎

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import { useRouter } from 'next/router'
2+
import cx from 'clsx'
23
import { LinkExternalIcon } from '@primer/octicons-react'
34
import { Button, ButtonGroup, Heading, Text } from '@primer/react-brand'
45
import styles from './LandingHero.module.scss'
56
import { LandingSection } from './LandingSection'
67
import { useTranslation } from '@/languages/components/useTranslation'
78
import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion'
89
import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
10+
import type { HeroTheme } from '@/landings/context/LandingContext'
911

1012
type LandingHeroProps = {
1113
title: string
1214
intro?: string
1315
heroImage?: string
16+
heroTheme?: HeroTheme
1417
introLinks?: Record<string, string> | null
1518
}
1619

20+
const heroThemes: Record<HeroTheme, { className: string; wordmark?: string }> = {
21+
'mos-diner': {
22+
className: styles.mosDiner,
23+
wordmark: '/assets/images/banner-images/mos-diner-logo.svg',
24+
},
25+
}
26+
1727
function heroBackgroundCss(heroImage: string | undefined) {
1828
if (!heroImage) return undefined
1929
return {
@@ -24,7 +34,13 @@ function heroBackgroundCss(heroImage: string | undefined) {
2434
}
2535
}
2636

27-
export const LandingHero = ({ title, intro, heroImage, introLinks }: LandingHeroProps) => {
37+
export const LandingHero = ({
38+
title,
39+
intro,
40+
heroImage,
41+
heroTheme,
42+
introLinks,
43+
}: LandingHeroProps) => {
2844
const { t } = useTranslation(['product_landing'])
2945
const router = useRouter()
3046
const { currentVersion } = useVersion()
@@ -36,21 +52,38 @@ export const LandingHero = ({ title, intro, heroImage, introLinks }: LandingHero
3652
return `/${router.locale}${versionPrefix}${href}`
3753
}
3854

55+
const theme = heroTheme ? heroThemes[heroTheme] : undefined
3956
const linkEntries = introLinks ? Object.entries(introLinks) : []
4057

4158
return (
4259
<LandingSection>
43-
<div className={styles.landingHero} style={heroBackgroundCss(heroImage)}>
60+
<div
61+
className={cx(styles.landingHero, theme?.className)}
62+
style={heroTheme ? undefined : heroBackgroundCss(heroImage)}
63+
data-hero-theme={heroTheme}
64+
>
4465
<div className={styles.heroContent}>
4566
<Heading as="h1" size="2" className={styles.heroHeading}>
46-
{title}
67+
{theme?.wordmark ? (
68+
<>
69+
<span className="visually-hidden">{title}</span>
70+
{/* The mask paints the artwork in the current text color, so it follows color mode. */}
71+
<span
72+
className={styles.heroWordmark}
73+
style={{ maskImage: `url("${theme.wordmark}")` }}
74+
aria-hidden="true"
75+
/>
76+
</>
77+
) : (
78+
title
79+
)}
4780
</Heading>
4881
{intro && (
4982
<Text
5083
as="div"
51-
size="200"
52-
variant="muted"
53-
className={`${styles.heroDescription} ${styles.heroIntroScrim}`}
84+
size={theme ? '300' : '200'}
85+
variant={theme ? 'default' : 'muted'}
86+
className={cx(styles.heroDescription, !heroTheme && styles.heroIntroScrim)}
5487
>
5588
<RenderedHTML as="div" html={intro} />
5689
</Text>
@@ -77,6 +110,7 @@ export const LandingHero = ({ title, intro, heroImage, introLinks }: LandingHero
77110
</div>
78111
)}
79112
</div>
113+
{heroTheme && <div className={styles.themeAccent} aria-hidden="true" />}
80114
</div>
81115
</LandingSection>
82116
)

‎src/landings/context/LandingContext.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type { JourneyTrack } from '@/journeys/lib/journey-path-resolver'
77

88
export type LandingType = 'bespoke' | 'discovery' | 'journey'
99

10+
export type HeroTheme = 'mos-diner'
11+
1012
export type LandingContextT = {
1113
landingType: LandingType
1214
title: string
@@ -19,6 +21,7 @@ export type LandingContextT = {
1921
renderedPage: string
2022
currentLayout: string
2123
heroImage?: string
24+
heroTheme?: HeroTheme
2225
carousels?: Record<
2326
string,
2427
Array<{ title: string; intro: string; href: string; category: string[] }>
@@ -32,6 +35,7 @@ export type LandingContextT = {
3235
type LandingPage = NonNullable<Context['page']> & {
3336
includedCategories?: string[]
3437
heroImage?: string
38+
heroTheme?: HeroTheme
3539
product?: string
3640
permissions?: string
3741
rawPermissions?: string
@@ -92,6 +96,7 @@ export const getLandingContextFromRequest = async (
9296
renderedPage: context.renderedPage ?? '',
9397
currentLayout: context.currentLayoutName ?? '',
9498
heroImage: page.heroImage || '/assets/images/banner-images/hero-1',
99+
...(page.heroTheme && { heroTheme: page.heroTheme }),
95100
introLinks: page.introLinks || null,
96101
carousels,
97102
journeyTracks,

‎src/landings/tests/landing-hero.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,28 @@ describe('LandingHero introLinks', () => {
6161
).toEqual(['https://github.com/features', '//example.com/x'])
6262
})
6363
})
64+
65+
describe('LandingHero heroTheme', () => {
66+
function renderHero(props: { heroImage?: string; heroTheme?: 'mos-diner' }) {
67+
return load(renderToStaticMarkup(createElement(LandingHero, { title: 'Title', ...props })))
68+
}
69+
70+
test('draws the hero image when no theme is set', () => {
71+
const $ = renderHero({ heroImage: '/assets/images/banner-images/hero-1' })
72+
expect($('[style*="background-image"]').length).toBe(1)
73+
expect($('[data-hero-theme]').length).toBe(0)
74+
expect($('[aria-hidden="true"]').length).toBe(0)
75+
expect($('.visually-hidden').length).toBe(0)
76+
})
77+
78+
test('replaces the hero image with the theme and its decorative accent', () => {
79+
const $ = renderHero({
80+
heroImage: '/assets/images/banner-images/hero-1',
81+
heroTheme: 'mos-diner',
82+
})
83+
expect($('[data-hero-theme="mos-diner"]').length).toBe(1)
84+
expect($('[style*="background-image"]').length).toBe(0)
85+
expect($('[data-hero-theme="mos-diner"] > [aria-hidden="true"]').length).toBe(1)
86+
expect($('.visually-hidden').text()).toBe('Title')
87+
})
88+
})

0 commit comments

Comments
 (0)