Skip to content

Commit 65991b9

Browse files
authored
Migrate the Docs homepage to the Docs 2026 design (#62443)
1 parent b9eeff1 commit 65991b9

13 files changed

Lines changed: 318 additions & 226 deletions

data/ui.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ homepage:
9999
explore_by_product: Explore by product
100100
version_picker: Version
101101
description: Help for wherever you are on your GitHub journey.
102+
search_placeholder: Search for anything...
103+
all_docs: All docs
102104

103105
toc:
104106
getting_started: Getting started

src/fixtures/fixtures/data/ui.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ homepage:
9999
explore_by_product: Explore by product
100100
version_picker: Version
101101
description: Help for wherever you are on your GitHub journey.
102+
search_placeholder: Search for anything...
103+
all_docs: All docs
102104

103105
toc:
104106
getting_started: Getting started

src/fixtures/tests/playwright-rendering.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,9 +1212,12 @@ test('open search, Ask AI returns 400 error and shows general search results', a
12121212
// Pressing enter should trigger Ask AI, get 400 error, and show general search results
12131213
await page.keyboard.press('Enter')
12141214

1215-
// Wait for general search results to appear
1216-
await expect(page.getByRole('link', { name: 'Foo' })).toBeVisible()
1217-
await expect(page.getByRole('link', { name: 'Bar' })).toBeVisible()
1215+
// Wait for the general search results to appear inside the overlay's suggestions
1216+
// group. These render as ActionList items (buttons), so scope the lookup to the
1217+
// group rather than matching page-level links of the same name.
1218+
const generalSuggestions = page.getByTestId('general-autocomplete-suggestions')
1219+
await expect(generalSuggestions.getByRole('button', { name: 'Foo' })).toBeVisible()
1220+
await expect(generalSuggestions.getByRole('button', { name: 'Bar' })).toBeVisible()
12181221

12191222
// Wait for the AI error message to appear
12201223
// This is a canned response for the 400 error
Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,73 @@
1-
// Applied to the brand Hero's root <section>. The hero background image is
2-
// baked in here (not a Hero.Image visual slot), so these rules must stay.
3-
.section {
4-
height: 21rem;
5-
// Brand Hero's root is display:flex/column but defaults to top alignment;
6-
// restore the vertical centering the old `flex-justify-center` provided.
1+
// Docs 2026 homepage hero: a left-aligned title block stacked above a muted
2+
// search band, both inside bordered rails. No background image — the mountains
3+
// graphic is a separate, out-of-scope band.
4+
.hero {
5+
border-bottom: var(--brand-borderWidth-thin, 1px) solid
6+
var(--brand-color-border-muted);
7+
}
8+
9+
// Bordered rails, centred with the same max width as the "All Docs" grid.
10+
.rails {
11+
max-width: 82rem;
12+
margin: 0 auto;
13+
border-inline: var(--brand-borderWidth-thin, 1px) solid
14+
var(--brand-color-border-muted);
15+
}
16+
17+
.titleRow {
18+
padding: 4rem 1.5rem 1.5rem;
19+
}
20+
21+
.title {
22+
margin: 0;
23+
}
24+
25+
.lede {
26+
margin: 1rem 0 0;
27+
}
28+
29+
// Search entry that mimics an input but opens the shared SearchOverlay on
30+
// activation. A full-width muted band with the placeholder on the left and a
31+
// trailing "/" key hint, separated from the title by a top border.
32+
.searchRow {
33+
display: flex;
34+
align-items: center;
35+
justify-content: space-between;
36+
gap: 1rem;
37+
width: 100%;
38+
padding: 1.5rem;
39+
border: 0;
40+
border-top: var(--brand-borderWidth-thin, 1px) solid
41+
var(--brand-color-border-muted);
42+
background-color: var(--color-canvas-subtle);
43+
cursor: text;
44+
text-align: left;
45+
46+
&:hover {
47+
background-color: var(--color-canvas-inset);
48+
}
49+
50+
&:focus-visible {
51+
outline: 2px solid var(--color-accent-fg);
52+
outline-offset: -2px;
53+
}
54+
}
55+
56+
.searchPlaceholder {
57+
color: var(--color-fg-muted);
58+
font-size: 1.5rem;
59+
}
60+
61+
.searchHint {
62+
display: inline-flex;
63+
align-items: center;
764
justify-content: center;
8-
background-image: image-set(
9-
url("/assets/images/banner-images/hero-home.webp") type("image/webp"),
10-
url("/assets/images/banner-images/hero-home.png") type("image/png")
11-
);
12-
background-size: cover;
13-
background-position: center right;
14-
background-repeat: no-repeat;
15-
}
16-
17-
.section h1 {
18-
font-size: 4rem;
19-
line-height: 1.2;
20-
}
21-
22-
// Frosted-glass panel keeping heading/lede readable over the background image.
23-
.content {
24-
backdrop-filter: blur(1rem);
25-
background-color: color-mix(
26-
in srgb,
27-
var(--color-canvas-subtle) 70%,
28-
transparent
29-
);
65+
min-width: 2rem;
66+
padding: 0.375rem 0.75rem;
67+
border-radius: var(--brand-borderRadius-medium, 8px);
68+
background-color: var(--brand-color-border-muted);
69+
color: var(--color-fg-default);
70+
font-family: inherit;
71+
font-size: 1.25rem;
72+
line-height: 1;
3073
}
Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
1-
import { Hero } from '@primer/react-brand'
1+
import { Heading, Text } from '@primer/react-brand'
2+
23
import { useTranslation } from '@/languages/components/useTranslation'
4+
import { useSearchOverlayContext } from '@/search/components/context/SearchOverlayContext'
35
import styles from './HomePageHero.module.scss'
4-
import cx from 'classnames'
56

67
export const HomePageHero = () => {
78
const { t } = useTranslation(['header', 'homepage'])
9+
const { setIsSearchOpen } = useSearchOverlayContext()
810

911
return (
10-
<Hero
11-
id="landing"
12-
align="center"
13-
className={cx('border-bottom color-border-muted color-bg-subtle', styles.section)}
14-
>
15-
<div className={cx('mx-auto px-4 rounded-3', styles.content)}>
16-
<Hero.Heading>{t('github_docs')}</Hero.Heading>
17-
<Hero.Description>{t('description')}</Hero.Description>
12+
<section id="landing" className={styles.hero}>
13+
<div className={styles.rails}>
14+
<div className={styles.titleRow}>
15+
<Heading as="h1" size="display" className={styles.title}>
16+
{t('github_docs')}
17+
</Heading>
18+
<Text as="p" size="200" variant="muted" className={styles.lede}>
19+
{t('description')}
20+
</Text>
21+
</div>
22+
23+
<button
24+
type="button"
25+
data-testid="homepage-search"
26+
className={styles.searchRow}
27+
onClick={() => setIsSearchOpen(true)}
28+
aria-haspopup="dialog"
29+
aria-label={t('search_placeholder')}
30+
>
31+
<span className={styles.searchPlaceholder}>{t('search_placeholder')}</span>
32+
<kbd className={styles.searchHint}>/</kbd>
33+
</button>
1834
</div>
19-
</Hero>
35+
</section>
2036
)
2137
}
Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,85 @@
1-
.groupIcon {
2-
height: 22px;
1+
// A single "All Docs" grid cell: category heading at the top, product links
2+
// bottom-aligned (space-between) so every cell in a row shares the tallest
3+
// height — matching the Docs 2026 design. Internal dividers are the cell's left
4+
// border (skipped on the first column of each row, per breakpoint, so they don't
5+
// double the container rail) plus a bottom border for row dividers. The column
6+
// count steps 1 -> 2 -> 3 -> 4, so each breakpoint re-applies the left border to
7+
// every cell and then clears it on the new first-of-row.
8+
.cell {
9+
display: flex;
10+
flex-direction: column;
11+
justify-content: space-between;
12+
gap: 2.25rem;
13+
padding: 2rem;
14+
border-bottom: var(--brand-borderWidth-thin, 1px) solid
15+
var(--brand-color-border-muted);
16+
17+
// Mobile: single column — no internal vertical dividers.
18+
border-left: 0;
19+
20+
@media (min-width: 34rem) {
21+
// 2 columns.
22+
border-left: var(--brand-borderWidth-thin, 1px) solid
23+
var(--brand-color-border-muted);
24+
25+
&:nth-child(2n + 1) {
26+
border-left: 0;
27+
}
28+
}
29+
30+
@media (min-width: 63.25rem) {
31+
// 3 columns.
32+
&:nth-child(n) {
33+
border-left: var(--brand-borderWidth-thin, 1px) solid
34+
var(--brand-color-border-muted);
35+
}
36+
37+
&:nth-child(3n + 1) {
38+
border-left: 0;
39+
}
40+
}
41+
42+
@media (min-width: 80rem) {
43+
// 4 columns.
44+
&:nth-child(n) {
45+
border-left: var(--brand-borderWidth-thin, 1px) solid
46+
var(--brand-color-border-muted);
47+
}
48+
49+
&:nth-child(4n + 1) {
50+
border-left: 0;
51+
}
52+
}
53+
}
54+
55+
.groupName {
56+
margin: 0;
57+
font-size: 1rem;
58+
font-weight: 600;
59+
line-height: 1.5;
60+
color: var(--color-fg-default);
61+
}
62+
63+
.linkList {
64+
display: flex;
65+
flex-direction: column;
66+
gap: 0.75rem;
67+
margin: 0;
68+
padding: 0;
69+
list-style: none;
70+
}
71+
72+
.linkList li {
73+
min-height: 1rem;
74+
}
75+
76+
.link {
77+
display: block;
78+
overflow: hidden;
79+
font-size: 1rem;
80+
font-weight: 500;
81+
line-height: 1rem;
82+
letter-spacing: 0.01rem;
83+
white-space: nowrap;
84+
text-overflow: ellipsis;
385
}
Lines changed: 25 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,45 @@
11
import type { ProductGroupT } from '@/landings/components/ProductSelections'
22

3-
import React from 'react'
43
import { Link } from '@/frame/components/Link'
4+
import { LinkExternalIcon } from '@primer/octicons-react'
55

66
import styles from './ProductSelectionCard.module.scss'
7-
import {
8-
LinkExternalIcon,
9-
RocketIcon,
10-
CommentDiscussionIcon,
11-
CopilotIcon,
12-
GearIcon,
13-
ShieldLockIcon,
14-
DeviceMobileIcon,
15-
ProjectIcon,
16-
OrganizationIcon,
17-
CodeSquareIcon,
18-
GlobeIcon,
19-
PencilIcon,
20-
} from '@primer/octicons-react'
217

228
type ProductSelectionCardProps = {
239
group: ProductGroupT
2410
}
25-
const octiconMap: { [name: string]: React.FunctionComponent } = {
26-
LinkExternalIcon,
27-
RocketIcon,
28-
CommentDiscussionIcon,
29-
CopilotIcon,
30-
GearIcon,
31-
ShieldLockIcon,
32-
DeviceMobileIcon,
33-
ProjectIcon,
34-
OrganizationIcon,
35-
CodeSquareIcon,
36-
GlobeIcon,
37-
PencilIcon,
38-
}
3911

4012
export const ProductSelectionCard = ({ group }: ProductSelectionCardProps) => {
4113
// Don't display the group if it has no children due to versioning
4214
if (!group.children || group.children.length === 0) {
4315
return null
4416
}
4517

46-
function icon(productGroup: ProductGroupT) {
47-
if (productGroup.icon) {
48-
return (
49-
<div className="pr-3">
50-
<img src={productGroup.icon} alt={productGroup.name} className={styles.groupIcon}></img>
51-
</div>
52-
)
53-
} else if (productGroup.octicon) {
54-
const octicon: React.FunctionComponent = octiconMap[productGroup.octicon]
55-
56-
if (!octicon) {
57-
throw new Error(`Octicon ${productGroup.octicon} not found`)
58-
}
59-
60-
return (
61-
<div className="mr-2">
62-
{React.createElement(octicon, { height: '22px' } as React.Attributes, null)}
63-
</div>
64-
)
65-
}
66-
}
67-
6818
return (
69-
<div className="d-flex flex-column col-12 col-sm-6 col-lg-4 col-xl-3 pb-4">
70-
<div className="flex-auto ws-normal">
71-
<div className="d-flex flex-items-center">
72-
{icon(group)}
73-
74-
<div>
75-
<h2 className="h3">{group.name}</h2>
76-
</div>
77-
</div>
78-
79-
<div className="pt-2 mb-4 text-normal">
80-
<ul role="list" className="list-style-none">
81-
{group.children.map((product) => {
82-
return (
83-
<li key={product.name} role="listitem" className="pt-2">
84-
<Link href={product.href} target={product.external ? '_blank' : undefined}>
85-
{product.name}
86-
{product.external && (
87-
<span className="ml-1">
88-
<LinkExternalIcon aria-label="(external site)" size="small" />
89-
</span>
90-
)}
91-
</Link>
92-
</li>
93-
)
94-
})}
95-
</ul>
96-
</div>
97-
</div>
19+
<div className={styles.cell}>
20+
<h3 className={styles.groupName}>{group.name}</h3>
21+
22+
<ul role="list" className={styles.linkList}>
23+
{group.children.map((product) => {
24+
return (
25+
<li key={product.name} role="listitem">
26+
<Link
27+
className={styles.link}
28+
href={product.href}
29+
title={product.name}
30+
target={product.external ? '_blank' : undefined}
31+
>
32+
{product.name}
33+
{product.external && (
34+
<span className="ml-1">
35+
<LinkExternalIcon aria-label="(external site)" size="small" />
36+
</span>
37+
)}
38+
</Link>
39+
</li>
40+
)
41+
})}
42+
</ul>
9843
</div>
9944
)
10045
}

0 commit comments

Comments
 (0)