fix(projects): improve carousel controls and case study actions

This commit is contained in:
2026-09-06 21:59:20 +03:30
parent d0de63bb7a
commit 31a52fed2d
2 changed files with 75 additions and 115 deletions

View File

@@ -52,6 +52,7 @@
.navButton:disabled {
cursor: default;
opacity: 0.36;
}
[data-theme="dark"] .navButtonPrevious {
@@ -115,7 +116,7 @@
box-sizing: border-box;
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
background: var(--bg-surface);
background: var(--bg-raised);
scroll-snap-align: start;
transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease;
}
@@ -225,6 +226,7 @@
font-size: var(--font-size-body-small);
font-weight: 600;
transition: color 0.2s ease, gap 0.2s ease;
min-height: 2.75rem;
}
.caseStudyLink:focus-visible {
@@ -243,67 +245,29 @@
margin-top: 2.5rem;
}
.scrollbar {
--thumb-width: 13.5rem;
.progressTrack {
position: relative;
overflow: hidden;
width: 30rem;
max-width: 100%;
height: 1.25rem;
margin: 0;
appearance: none;
background: transparent;
cursor: pointer;
}
.scrollbar::-webkit-slider-runnable-track {
height: 0.375rem;
border-radius: var(--radius-pill);
background: var(--color-blue-200);
}
.scrollbar::-webkit-slider-thumb {
width: var(--thumb-width, 13.5rem);
height: 0.375rem;
margin-top: 0;
appearance: none;
border: 0;
.progressValue {
display: block;
height: 100%;
border-radius: var(--radius-pill);
background: var(--color-blue-400);
transition: width var(--motion-duration-default) var(--motion-ease-out);
}
.scrollbar::-moz-range-track {
height: 0.375rem;
border: 0;
border-radius: var(--radius-pill);
background: var(--color-blue-200);
}
.scrollbar::-moz-range-thumb {
width: var(--thumb-width, 13.5rem);
height: 0.375rem;
border: 0;
border-radius: var(--radius-pill);
background: var(--color-blue-400);
}
.scrollbar:focus-visible {
border-radius: var(--radius-pill);
outline: 3px solid var(--focus-ring);
outline-offset: 3px;
}
[data-theme="dark"] .scrollbar::-webkit-slider-runnable-track {
[data-theme="dark"] .progressTrack {
background: var(--color-blue-800);
}
[data-theme="dark"] .scrollbar::-webkit-slider-thumb {
background: var(--color-blue-300);
}
[data-theme="dark"] .scrollbar::-moz-range-track {
background: var(--color-blue-800);
}
[data-theme="dark"] .scrollbar::-moz-range-thumb {
[data-theme="dark"] .progressValue {
background: var(--color-blue-300);
}
@@ -321,9 +285,8 @@
width: clamp(18.125rem, 23vw, 20.625rem);
}
.scrollbar {
.progressTrack {
width: min(30rem, 50vw);
--thumb-width: 7.5rem;
}
}
@@ -398,9 +361,8 @@
margin-top: 2rem;
}
.scrollbar {
.progressTrack {
width: min(22.5rem, 60vw);
--thumb-width: 5.625rem;
}
}
@@ -463,8 +425,8 @@
}
.cardDesc {
font-size: 0.8125rem;
line-height: 1.5;
font-size: 1rem;
line-height: 1.6;
margin-bottom: 0.875rem;
white-space: normal;
word-break: normal;
@@ -478,8 +440,8 @@
}
.tagBadge {
padding: 0.25rem 0.5625rem;
font-size: 0.6875rem;
padding: 0.375rem 0.625rem;
font-size: 0.75rem;
}
.scrollbarWrapper {
@@ -488,10 +450,8 @@
margin-top: 1.5rem;
}
.scrollbar {
.progressTrack {
width: min(12.5rem, 60vw);
height: 1rem;
--thumb-width: 3.75rem;
}
}

View File

@@ -39,8 +39,12 @@ export const ProjectsSection: React.FC = () => {
useEffect(() => {
syncScrollState();
window.addEventListener('resize', syncScrollState);
return () => window.removeEventListener('resize', syncScrollState);
const viewport = viewportRef.current;
if (!viewport) return;
const resizeObserver = new ResizeObserver(syncScrollState);
resizeObserver.observe(viewport);
return () => resizeObserver.disconnect();
}, []);
const scrollByCard = (direction: -1 | 1) => {
@@ -182,14 +186,6 @@ export const ProjectsSection: React.FC = () => {
dragState.current.moved = false;
};
const handleScrollbarChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const viewport = viewportRef.current;
if (!viewport) return;
const maxScroll = Math.max(0, viewport.scrollWidth - viewport.clientWidth);
viewport.scrollLeft = (Number(event.target.value) / 100) * maxScroll;
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
@@ -253,67 +249,71 @@ export const ProjectsSection: React.FC = () => {
onKeyDown={handleKeyDown}
tabIndex={0}
aria-label="Scrollable project cards"
aria-roledescription="carousel"
>
<div className={styles.carouselTrack}>
{projects.map((project) => (
<article key={project.id} className={styles.card}>
<div className={styles.mediaContainer}>
<Image
src={project.image}
alt={project.title}
fill
sizes="(max-width: 650px) 100vw, (max-width: 1100px) 100vw, 360px"
priority={project.id === 'qlockify'}
draggable={false}
className={styles.projectImage}
/>
</div>
<div className={styles.cardBody}>
<div className={styles.cardTitleRow}>
<h3 className={styles.cardTitle}>{project.title}</h3>
<ArrowUpRight
size={19}
strokeWidth={2.5}
className={styles.titleLinkIcon}
aria-hidden="true"
<article key={project.id} className={styles.card} aria-label={project.title}>
<div className={styles.mediaContainer}>
<Image
src={project.image}
alt={project.title}
fill
sizes="(max-width: 650px) 100vw, (max-width: 1100px) 100vw, 360px"
priority={project.id === 'qlockify'}
draggable={false}
className={styles.projectImage}
/>
</div>
<div className={styles.cardDate}>{project.date}</div>
<p className={styles.cardDesc}>{project.description}</p>
<div className={styles.cardBody}>
<div className={styles.cardTitleRow}>
<h3 className={styles.cardTitle}>{project.title}</h3>
<ArrowUpRight
size={19}
strokeWidth={2.5}
className={styles.titleLinkIcon}
aria-hidden="true"
/>
</div>
<div className={styles.tagList}>
{project.tags.map((tag) => (
<span key={tag} className={styles.tagBadge}>
{tag}
</span>
))}
</div>
<div className={styles.cardDate}>{project.date}</div>
<p className={styles.cardDesc}>{project.description}</p>
<div className={styles.cardFooter}>
<a href={project.link || '#'} className={styles.caseStudyLink}>
<span>View case study</span>
<ArrowRight size={18} strokeWidth={2.25} aria-hidden="true" />
</a>
<div className={styles.tagList}>
{project.tags.map((tag) => (
<span key={tag} className={styles.tagBadge}>
{tag}
</span>
))}
</div>
<div className={styles.cardFooter}>
<a href={project.link || '#'} className={styles.caseStudyLink}>
<span>View case study</span>
<ArrowRight size={18} strokeWidth={2.25} aria-hidden="true" />
</a>
</div>
</div>
</div>
</article>
</article>
))}
</div>
</div>
<div className={styles.scrollbarWrapper}>
<input
type="range"
min="0"
max="100"
step="0.1"
value={scrollProgress}
onChange={handleScrollbarChange}
className={styles.scrollbar}
<div
className={styles.progressTrack}
role="progressbar"
aria-label="Project carousel position"
/>
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(scrollProgress)}
>
<span
className={styles.progressValue}
style={{ width: `${12.5 + scrollProgress * 0.875}%` }}
/>
</div>
</div>
</div>
</section>