landing page UI tweaks (#57996)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user