patx/afterdarklabs
update progressbar in portfolio
Commit ae7568c · patx · 2026-07-16T18:29:59-04:00
Comments
No comments yet.
Diff
diff --git a/docs/index.html b/docs/index.html
index f27c8ee..268b41c 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -174,8 +174,51 @@
font-weight: 600;
}
+ .work-pagination {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ margin-top: 1rem;
+ }
+
+ .work-pagination-dot {
+ display: grid;
+ width: 1.75rem;
+ height: 1.75rem;
+ place-items: center;
+ border-radius: 9999px;
+ color: rgba(255,255,255,0.32);
+ }
+
+ .work-pagination-dot::before {
+ content: "";
+ width: 0.425rem;
+ height: 0.425rem;
+ border-radius: inherit;
+ background: currentColor;
+ transition: color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
+ }
+
+ .work-pagination-dot:hover {
+ color: rgba(255,255,255,0.72);
+ }
+
+ .work-pagination-dot[aria-current="true"] {
+ color: var(--neon);
+ }
+
+ .work-pagination-dot[aria-current="true"]::before {
+ transform: scale(1.2);
+ box-shadow: 0 0 0.55rem rgba(204,255,0,0.32);
+ }
+
+ .work-pagination-dot:focus-visible {
+ outline: 2px solid var(--neon);
+ outline-offset: -3px;
+ }
+
@media (min-width: 768px) {
- .work-scroll-controls {
+ .work-pagination {
display: flex;
}
@@ -308,16 +351,8 @@
<section id="work" class="scroll-mt-24 border-t border-white/10 bg-black">
<div class="py-20 md:py-24">
- <div class="mx-auto mb-10 flex max-w-5xl items-end justify-between gap-6 px-6">
+ <div class="mx-auto mb-10 max-w-5xl px-6">
<h2 class="heading text-4xl font-semibold tracking-tighter md:text-6xl">Recent projects</h2>
- <div class="work-scroll-controls hidden shrink-0 gap-3 md:flex">
- <button type="button" data-work-scroll="-1" class="flex h-11 w-11 items-center justify-center rounded-full border border-white/15 text-gray-300 transition-colors hover:border-[#ccff00] hover:text-[#ccff00] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00]" aria-label="Scroll projects left">
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M10 3 5 8l5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
- </button>
- <button type="button" data-work-scroll="1" class="flex h-11 w-11 items-center justify-center rounded-full border border-white/15 text-gray-300 transition-colors hover:border-[#ccff00] hover:text-[#ccff00] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00]" aria-label="Scroll projects right">
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="m6 3 5 5-5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
- </button>
- </div>
</div>
<div id="work-scroller" class="no-scrollbar flex snap-x snap-mandatory gap-5 overflow-x-auto scroll-smooth px-6 pb-2 scroll-px-6">
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
@@ -361,6 +396,7 @@
</div>
</article>
</div>
+ <div id="work-pagination" class="work-pagination" role="group" aria-label="Choose a portfolio project"></div>
</div>
</section>
@@ -438,15 +474,83 @@
(function () {
const scroller = document.getElementById('work-scroller');
- if (!scroller) return;
+ const pagination = document.getElementById('work-pagination');
+ if (!scroller || !pagination) return;
+
+ const cards = Array.from(scroller.querySelectorAll(':scope > article'));
+ if (!cards.length) return;
+
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
+ const dots = cards.map((card, index) => {
+ const dot = document.createElement('button');
+ const projectName = card.querySelector('.p-6 p')?.textContent.split('•')[0].trim() || `project ${index + 1}`;
+
+ dot.type = 'button';
+ dot.className = 'work-pagination-dot';
+ dot.setAttribute('aria-label', `Show ${projectName} project`);
+ dot.setAttribute('aria-controls', 'work-scroller');
+ dot.addEventListener('click', () => {
+ const maxScroll = scroller.scrollWidth - scroller.clientWidth;
+ const target = Math.min(cards[index].offsetLeft - cards[0].offsetLeft, maxScroll);
+ scroller.scrollTo({
+ left: Math.max(0, target),
+ behavior: reduceMotion.matches ? 'auto' : 'smooth'
+ });
+ });
+
+ pagination.appendChild(dot);
+ return dot;
+ });
+
+ let updateFrame = 0;
+
+ function updatePagination() {
+ updateFrame = 0;
+ const maxScroll = scroller.scrollWidth - scroller.clientWidth;
+ let activeIndex = 0;
+ let closestDistance = Infinity;
+
+ cards.forEach((card, index) => {
+ const position = Math.min(card.offsetLeft - cards[0].offsetLeft, maxScroll);
+ const distance = Math.abs(scroller.scrollLeft - Math.max(0, position));
+
+ if (distance < closestDistance) {
+ closestDistance = distance;
+ activeIndex = index;
+ }
+ });
- document.querySelectorAll('[data-work-scroll]').forEach(button => {
- button.addEventListener('click', () => {
- const card = scroller.querySelector('article');
- const step = card ? card.offsetWidth + 20 : scroller.clientWidth * 0.8;
- scroller.scrollBy({ left: step * Number(button.dataset.workScroll), behavior: 'smooth' });
+ dots.forEach((dot, index) => {
+ const isActive = index === activeIndex;
+ dot.toggleAttribute('aria-current', isActive);
+ if (isActive) dot.setAttribute('aria-current', 'true');
+ dot.tabIndex = isActive ? 0 : -1;
});
+ }
+
+ function schedulePaginationUpdate() {
+ if (!updateFrame) updateFrame = window.requestAnimationFrame(updatePagination);
+ }
+
+ pagination.addEventListener('keydown', event => {
+ const currentIndex = dots.indexOf(document.activeElement);
+ if (currentIndex < 0) return;
+
+ let nextIndex = currentIndex;
+ if (event.key === 'ArrowLeft') nextIndex = Math.max(0, currentIndex - 1);
+ if (event.key === 'ArrowRight') nextIndex = Math.min(dots.length - 1, currentIndex + 1);
+ if (event.key === 'Home') nextIndex = 0;
+ if (event.key === 'End') nextIndex = dots.length - 1;
+ if (nextIndex === currentIndex && !['Home', 'End'].includes(event.key)) return;
+
+ event.preventDefault();
+ dots[nextIndex].focus();
+ dots[nextIndex].click();
});
+
+ scroller.addEventListener('scroll', schedulePaginationUpdate, { passive: true });
+ window.addEventListener('resize', schedulePaginationUpdate);
+ updatePagination();
})();
(function () {