patx/afterdarklabs

update progressbar in portfolio

Commit ae7568c · patx · 2026-07-16T18:29:59-04:00

Changeset
ae7568c6b440b8aa224ab421717db30f6d7bc271
Parents
337cbb65893561b0d6cd2be843df781d4e994fee

View source at this commit

Comments

No comments yet.

Log in to comment

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 () {