1
0
mirror of synced 2025-12-19 18:10:59 -05:00

landing page UI tweaks (#57996)

This commit is contained in:
Evan Bonsignori
2025-10-17 19:05:37 -07:00
committed by GitHub
parent ab3e689e0c
commit e5d1588136
6 changed files with 132 additions and 41 deletions

View File

@@ -1015,7 +1015,7 @@ test.describe('LandingCarousel component', () => {
// Check that article cards are present
const items = page.locator('[data-testid="carousel-items"]')
const cards = items.locator('div')
const cards = items.locator('a')
await expect(cards.first()).toBeVisible()
// Verify cards have real titles (not "Unknown Article" when article not found)
@@ -1190,7 +1190,7 @@ test.describe('LandingArticleGridWithFilter component', () => {
await expect(articleCards.first()).toBeVisible()
const firstCard = articleCards.first()
const titleLink = firstCard.locator('h3 a')
const titleLink = firstCard.locator('h3 span')
await expect(titleLink).toBeVisible()
const intro = firstCard.locator('div').last() // cardIntro is the last div

View File

@@ -24,6 +24,19 @@
box-shadow:
0 0.0625rem 0.1875rem 0 rgba(31, 35, 40, 0.08),
0 0.0625rem 0 0 rgba(31, 35, 40, 0.06);
transition: all 0.2s ease-in-out;
cursor: pointer;
text-decoration: none !important;
color: inherit;
&:hover {
box-shadow:
0 0.25rem 0.5rem 0 rgba(31, 35, 40, 0.12),
0 0.125rem 0.25rem 0 rgba(31, 35, 40, 0.08);
transform: translateY(-2px);
background-color: var(--bgColor-muted, var(--color-canvas-subtle));
text-decoration: none !important;
}
}
.cardHeader {
@@ -40,10 +53,6 @@
.cardTitleLink {
color: var(--fgColor-accent);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.cardIntro {
@@ -51,6 +60,7 @@
color: var(--fgColor-muted);
font-size: 0.9rem;
line-height: 1.4;
text-decoration: none !important;
}
.tagsContainer {
@@ -145,6 +155,10 @@
margin-left: 0;
width: auto;
input {
font-size: 1rem;
}
// Medium screens: flexible width with spacing
@include breakpoint(md) {
flex: 0 1 25%;

View File

@@ -51,6 +51,7 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
const articlesPerPage = useResponsiveArticlesPerPage()
const inputRef = useRef<HTMLInputElement>(null)
const headingRef = useRef<HTMLHeadingElement>(null)
// Reset to first page when articlesPerPage changes (screen size changes)
useEffect(() => {
@@ -112,6 +113,14 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
e.preventDefault()
if (pageNumber >= 1 && pageNumber <= totalPages) {
setCurrentPage(pageNumber)
if (headingRef.current) {
const elementPosition = headingRef.current.getBoundingClientRect().top + window.scrollY
const offsetPosition = elementPosition - 140 // 140px offset from top
window.scrollTo({
top: offsetPosition,
behavior: 'smooth',
})
}
}
}
@@ -122,7 +131,7 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
{/* Title and Dropdown Row */}
<div className={styles.titleAndDropdownRow}>
{/* Title */}
<h2 className={cx(styles.headerTitle, styles.headerTitleText)}>
<h2 ref={headingRef} className={cx(styles.headerTitle, styles.headerTitleText)}>
{t('article_grid.heading')}
</h2>
@@ -156,7 +165,6 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
<form onSubmit={(e) => e.preventDefault()}>
<TextInput
leadingVisual={SearchIcon}
sx={{ width: '100%' }}
placeholder={t('article_grid.search_articles')}
ref={inputRef}
autoComplete="false"
@@ -210,7 +218,8 @@ type ArticleCardProps = {
const ArticleCard = ({ article }: ArticleCardProps) => {
return (
<div
<Link
href={article.fullPath}
className={cx(
styles.articleCard,
styles.articleCardBox,
@@ -226,12 +235,10 @@ const ArticleCard = ({ article }: ArticleCardProps) => {
</div>
<h3 className={styles.cardTitle}>
<Link href={article.fullPath} className={styles.cardTitleLink}>
{article.title}
</Link>
<span className={styles.cardTitleLink}>{article.title}</span>
</h3>
{article.intro && <div className={styles.cardIntro}>{article.intro}</div>}
</div>
</Link>
)
}

View File

@@ -1,6 +1,6 @@
.carousel {
margin-top: 3rem;
--carousel-transition-duration: 0.3s;
--carousel-transition-duration: 0.1s;
}
.header {
@@ -44,7 +44,7 @@
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
transition: opacity var(--carousel-transition-duration) ease-out;
transition: opacity var(--carousel-transition-duration) ease-in-out;
opacity: 1;
@media (min-width: 768px) {
@@ -56,7 +56,7 @@
}
&.animating {
opacity: 0;
opacity: 0.3;
}
}
@@ -68,6 +68,19 @@
box-shadow:
0px 1px 3px 0px rgba(31, 35, 40, 0.08),
0px 1px 0px 0px rgba(31, 35, 40, 0.06);
transition: all 0.2s ease-in-out;
cursor: pointer;
text-decoration: none !important;
color: inherit;
&:hover {
box-shadow:
0 0.25rem 0.5rem 0 rgba(31, 35, 40, 0.12),
0 0.125rem 0.25rem 0 rgba(31, 35, 40, 0.08);
transform: translateY(-2px);
background-color: var(--bgColor-muted, var(--color-canvas-subtle));
text-decoration: none !important;
}
}
.articleTitle {
@@ -78,10 +91,6 @@
.articleLink {
color: var(--fgColor-accent);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.articleDescription {
@@ -89,6 +98,7 @@
color: var(--fgColor-muted);
font-size: 0.9rem;
line-height: 1.4;
text-decoration: none !important;
}
.pagination {

View File

@@ -1,6 +1,5 @@
import { useState, useEffect, useRef } from 'react'
import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react'
import { Token } from '@primer/react'
import cx from 'classnames'
import type { ResolvedArticle } from '@/types'
import { useTranslation } from '@/languages/components/useTranslation'
@@ -78,11 +77,11 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
setCurrentPage((prev) => Math.max(0, prev - 1))
// Set animation state to false after transition completes
// Duration matches CSS custom property --carousel-transition-duration (300ms)
// Duration matches CSS custom property --carousel-transition-duration (100ms)
animationTimeoutRef.current = setTimeout(() => {
setIsAnimating(false)
animationTimeoutRef.current = null
}, 300)
}, 100)
}
const goToNext = () => {
@@ -97,11 +96,11 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1))
// Set animation state to false after transition completes
// Duration matches CSS custom property --carousel-transition-duration (300ms)
// Duration matches CSS custom property --carousel-transition-duration (100ms)
animationTimeoutRef.current = setTimeout(() => {
setIsAnimating(false)
animationTimeoutRef.current = null
}, 300)
}, 100)
}
// Calculate the start index based on current page
@@ -144,19 +143,13 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
data-testid="carousel-items"
>
{visibleItems.map((article: ResolvedArticle, index) => (
<div
<a
key={startIndex + index}
href={article.href}
className={cx(styles.articleCard, 'border', 'border-default', 'rounded-2')}
>
<div className="mb-2">
{article.category.map((cat: string) => (
<Token key={cat} text={cat} className="mr-1 mb-2" />
))}
</div>
<h3 className={styles.articleTitle}>
<a href={article.href} className={styles.articleLink}>
{article.title}
</a>
<span className={styles.articleLink}>{article.title}</span>
</h3>
<div
className={styles.articleDescription}
@@ -164,7 +157,7 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
__html: article.intro as TrustedHTML,
}}
/>
</div>
</a>
))}
</div>
</div>

View File

@@ -98,6 +98,14 @@
.heroSecondaryAction {
background-color: transparent;
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
@media (max-width: 865px) {
background-color: var(
--bgColor-muted,
var(--color-canvas-subtle, #f6f8fa)
) !important;
}
border-color: var(
--borderColor-default,
var(--color-border-default, #d1d9e0)
@@ -121,18 +129,37 @@
@media (max-width: 865px) {
.landingHero {
height: 24rem;
background-image: none !important;
height: 28rem;
flex-direction: column;
text-align: center;
justify-content: center;
background-color: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa));
position: relative;
background-position: unset;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: inherit;
background-size: contain;
background-position: bottom;
background-repeat: no-repeat;
opacity: 0.5;
z-index: 0;
}
}
.heroContent {
width: 100%;
order: 2;
padding: 0 1rem;
position: relative;
z-index: 1;
}
.heroHeading {
@@ -144,7 +171,11 @@
}
.heroActions {
flex-direction: column;
align-self: baseline;
margin-left: 5rem;
justify-content: center;
align-items: center;
}
.heroAction {
@@ -162,19 +193,36 @@
@media (max-width: 480px) {
.landingHero {
height: 32rem;
background-image: none !important;
height: 28rem;
background-color: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa));
flex-direction: column;
text-align: center;
padding: 2rem 0 1rem;
padding: 1rem 0 1rem;
justify-content: center;
position: relative;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: inherit;
background-size: contain;
background-position: bottom;
background-repeat: no-repeat;
opacity: 0.8;
z-index: 0;
}
}
.heroContent {
width: 100%;
order: 2;
padding: 1rem;
padding: 0 1rem;
position: relative;
z-index: 1;
}
.heroText {
@@ -184,8 +232,27 @@
align-items: center;
}
.heroHeading {
font-size: 2.5rem;
line-height: 1;
}
.heroActions {
flex-direction: column;
align-self: baseline;
margin-left: 2rem;
justify-content: center;
align-items: center;
}
.heroAction {
min-width: 9rem;
padding: 0.5rem 1rem;
}
.heroDescription {
font-size: 1rem;
max-height: 14rem;
text-overflow: ellipsis;
}
}