diff --git a/angular.json b/angular.json
index 1c07f58..faa069a 100644
--- a/angular.json
+++ b/angular.json
@@ -38,8 +38,8 @@
},
{
"type": "anyComponentStyle",
- "maximumWarning": "4kB",
- "maximumError": "8kB"
+ "maximumWarning": "5kB",
+ "maximumError": "5kB"
}
],
"outputHashing": "all"
diff --git a/src/components/tech-stack-carousel/tech-stack-carousel.css b/src/components/tech-stack-carousel/tech-stack-carousel.css
index fd7e591..3d80b57 100644
--- a/src/components/tech-stack-carousel/tech-stack-carousel.css
+++ b/src/components/tech-stack-carousel/tech-stack-carousel.css
@@ -28,6 +28,7 @@ h2 {
.stack-carousel-shell {
--carousel-nav-space: 4.25rem;
+ --carousel-edge-fade-size: clamp(1.25rem, 3vw, 2.75rem);
position: relative;
width: calc(100% + (var(--carousel-nav-space) * 2));
margin-inline: calc(var(--carousel-nav-space) * -1);
@@ -35,8 +36,36 @@ h2 {
}
.stack-carousel-column {
+ position: relative;
+ --left-fade-size: 0px;
+ --right-fade-size: 0px;
width: calc(100% - (var(--carousel-nav-space) * 2));
margin-inline: auto;
+ overflow: hidden;
+ -webkit-mask-image: linear-gradient(
+ to right,
+ transparent 0,
+ #000 var(--left-fade-size),
+ #000 calc(100% - var(--right-fade-size)),
+ transparent 100%
+ );
+ mask-image: linear-gradient(
+ to right,
+ transparent 0,
+ #000 var(--left-fade-size),
+ #000 calc(100% - var(--right-fade-size)),
+ transparent 100%
+ );
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+}
+
+.stack-carousel-column.has-left-fade {
+ --left-fade-size: var(--carousel-edge-fade-size);
+}
+
+.stack-carousel-column.has-right-fade {
+ --right-fade-size: var(--carousel-edge-fade-size);
}
.stack-carousel {
@@ -180,6 +209,7 @@ h2 {
@media (max-width: 760px) {
.stack-carousel-shell {
--carousel-nav-space: 0;
+ --carousel-edge-fade-size: 1rem;
width: 100%;
margin-inline: 0;
}
@@ -199,7 +229,8 @@ h2 {
@media (min-width: 1000px) {
.stack-slide {
- flex-basis: calc((100% - 2rem) / 3);
+ flex-basis: calc((100% - 3rem) / 4);
}
}
+
diff --git a/src/components/tech-stack-carousel/tech-stack-carousel.html b/src/components/tech-stack-carousel/tech-stack-carousel.html
index 8f69ecc..ca2df20 100644
--- a/src/components/tech-stack-carousel/tech-stack-carousel.html
+++ b/src/components/tech-stack-carousel/tech-stack-carousel.html
@@ -17,11 +17,12 @@
←
-
+
diff --git a/src/components/tech-stack-carousel/tech-stack-carousel.ts b/src/components/tech-stack-carousel/tech-stack-carousel.ts
index c7b5949..ef0fd06 100644
--- a/src/components/tech-stack-carousel/tech-stack-carousel.ts
+++ b/src/components/tech-stack-carousel/tech-stack-carousel.ts
@@ -23,6 +23,8 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
private virtualScrollLeft = 0;
private readonly autoScrollSpeedPxPerSecond = 72;
isAutoScrolling = false;
+ showLeftFade = false;
+ showRightFade = false;
readonly skills: Skill[] = [
{ name: 'Angular', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/angular.svg', fallbackLabel: 'NG' },
@@ -46,12 +48,14 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
+ this.updateFadeState();
return;
}
// Defer start to the next task to avoid NG0100 in dev mode.
this.startAutoScrollTimeout = setTimeout(() => {
this.startAutoScrollTimeout = null;
+ this.updateFadeState();
this.resumeAutoScroll();
}, 0);
}
@@ -73,6 +77,11 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
}
carousel.scrollBy({ left: this.getScrollStep(carousel) * direction, behavior: 'smooth' });
+ requestAnimationFrame(() => this.updateFadeState(carousel));
+ }
+
+ onCarouselScroll(): void {
+ this.updateFadeState();
}
pauseAutoScroll(): void {
@@ -111,6 +120,7 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
this.lastAutoScrollTime = timestamp;
this.autoAdvanceStack(carousel, deltaMs);
+ this.updateFadeState(carousel);
this.autoScrollFrameId = requestAnimationFrame(step);
};
@@ -150,6 +160,25 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
return carousel.scrollWidth / 2;
}
+ private updateFadeState(carousel = this.stackCarousel?.nativeElement): void {
+ if (!carousel) {
+ this.showLeftFade = false;
+ this.showRightFade = false;
+ return;
+ }
+
+ const maxScroll = Math.max(carousel.scrollWidth - carousel.clientWidth, 0);
+ if (maxScroll <= 1) {
+ this.showLeftFade = false;
+ this.showRightFade = false;
+ return;
+ }
+
+ const left = carousel.scrollLeft;
+ this.showLeftFade = left > 1;
+ this.showRightFade = left < maxScroll - 1;
+ }
+
private getScrollStep(carousel: HTMLElement): number {
return Math.max(carousel.clientWidth * 0.82, 260);
}