This commit is contained in:
@@ -38,8 +38,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "4kB",
|
"maximumWarning": "5kB",
|
||||||
"maximumError": "8kB"
|
"maximumError": "5kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ h2 {
|
|||||||
|
|
||||||
.stack-carousel-shell {
|
.stack-carousel-shell {
|
||||||
--carousel-nav-space: 4.25rem;
|
--carousel-nav-space: 4.25rem;
|
||||||
|
--carousel-edge-fade-size: clamp(1.25rem, 3vw, 2.75rem);
|
||||||
position: relative;
|
position: relative;
|
||||||
width: calc(100% + (var(--carousel-nav-space) * 2));
|
width: calc(100% + (var(--carousel-nav-space) * 2));
|
||||||
margin-inline: calc(var(--carousel-nav-space) * -1);
|
margin-inline: calc(var(--carousel-nav-space) * -1);
|
||||||
@@ -35,8 +36,36 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stack-carousel-column {
|
.stack-carousel-column {
|
||||||
|
position: relative;
|
||||||
|
--left-fade-size: 0px;
|
||||||
|
--right-fade-size: 0px;
|
||||||
width: calc(100% - (var(--carousel-nav-space) * 2));
|
width: calc(100% - (var(--carousel-nav-space) * 2));
|
||||||
margin-inline: auto;
|
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 {
|
.stack-carousel {
|
||||||
@@ -180,6 +209,7 @@ h2 {
|
|||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.stack-carousel-shell {
|
.stack-carousel-shell {
|
||||||
--carousel-nav-space: 0;
|
--carousel-nav-space: 0;
|
||||||
|
--carousel-edge-fade-size: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-inline: 0;
|
margin-inline: 0;
|
||||||
}
|
}
|
||||||
@@ -199,7 +229,8 @@ h2 {
|
|||||||
|
|
||||||
@media (min-width: 1000px) {
|
@media (min-width: 1000px) {
|
||||||
.stack-slide {
|
.stack-slide {
|
||||||
flex-basis: calc((100% - 2rem) / 3);
|
flex-basis: calc((100% - 3rem) / 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,12 @@
|
|||||||
<span aria-hidden="true">←</span>
|
<span aria-hidden="true">←</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="stack-carousel-column">
|
<div class="stack-carousel-column" [class.has-left-fade]="showLeftFade" [class.has-right-fade]="showRightFade">
|
||||||
<div
|
<div
|
||||||
class="stack-carousel"
|
class="stack-carousel"
|
||||||
[class.is-auto-scrolling]="isAutoScrolling"
|
[class.is-auto-scrolling]="isAutoScrolling"
|
||||||
#stackCarousel
|
#stackCarousel
|
||||||
|
(scroll)="onCarouselScroll()"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Tech stack carousel"
|
aria-label="Tech stack carousel"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
|||||||
private virtualScrollLeft = 0;
|
private virtualScrollLeft = 0;
|
||||||
private readonly autoScrollSpeedPxPerSecond = 72;
|
private readonly autoScrollSpeedPxPerSecond = 72;
|
||||||
isAutoScrolling = false;
|
isAutoScrolling = false;
|
||||||
|
showLeftFade = false;
|
||||||
|
showRightFade = false;
|
||||||
|
|
||||||
readonly skills: Skill[] = [
|
readonly skills: Skill[] = [
|
||||||
{ name: 'Angular', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/angular.svg', fallbackLabel: 'NG' },
|
{ 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;
|
typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
if (prefersReducedMotion) {
|
if (prefersReducedMotion) {
|
||||||
|
this.updateFadeState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defer start to the next task to avoid NG0100 in dev mode.
|
// Defer start to the next task to avoid NG0100 in dev mode.
|
||||||
this.startAutoScrollTimeout = setTimeout(() => {
|
this.startAutoScrollTimeout = setTimeout(() => {
|
||||||
this.startAutoScrollTimeout = null;
|
this.startAutoScrollTimeout = null;
|
||||||
|
this.updateFadeState();
|
||||||
this.resumeAutoScroll();
|
this.resumeAutoScroll();
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
@@ -73,6 +77,11 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
carousel.scrollBy({ left: this.getScrollStep(carousel) * direction, behavior: 'smooth' });
|
carousel.scrollBy({ left: this.getScrollStep(carousel) * direction, behavior: 'smooth' });
|
||||||
|
requestAnimationFrame(() => this.updateFadeState(carousel));
|
||||||
|
}
|
||||||
|
|
||||||
|
onCarouselScroll(): void {
|
||||||
|
this.updateFadeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
pauseAutoScroll(): void {
|
pauseAutoScroll(): void {
|
||||||
@@ -111,6 +120,7 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
|||||||
this.lastAutoScrollTime = timestamp;
|
this.lastAutoScrollTime = timestamp;
|
||||||
|
|
||||||
this.autoAdvanceStack(carousel, deltaMs);
|
this.autoAdvanceStack(carousel, deltaMs);
|
||||||
|
this.updateFadeState(carousel);
|
||||||
this.autoScrollFrameId = requestAnimationFrame(step);
|
this.autoScrollFrameId = requestAnimationFrame(step);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,6 +160,25 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
|||||||
return carousel.scrollWidth / 2;
|
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 {
|
private getScrollStep(carousel: HTMLElement): number {
|
||||||
return Math.max(carousel.clientWidth * 0.82, 260);
|
return Math.max(carousel.clientWidth * 0.82, 260);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user