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

View File

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