diff --git a/README.md b/README.md index 30b0bc0..ac5cf49 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,13 @@ This is the living history of Chimera's evolution. Each entry represents a day o --- +### Day 64: 2026-05-03 +**Feature/Change**: Frontend Polish — Back-to-Top Button & Animated Progress Bars +**Description**: Two focused, high-impact visual improvements. **(1) Back-to-top floating button**: A polished fixed-position button appears with a spring animation (`ease-bounce`) after the user scrolls more than 400 px, making navigation effortless on the long single-page app. The button uses the brand gradient (`--color-accent → --color-accent-secondary`), has a soft accent glow (`--shadow-accent`) and a satisfying hover-lift/scale effect. Clicking it smoothly scrolls back to the top. The button is fully accessible (ARIA label, focus-visible ring) and is suppressed for users who prefer reduced motion. It adapts cleanly to both dark and light themes and scales down on mobile. **(2) Animated category progress bars**: The "Evolution Categories" breakdown bars previously snapped to their final width immediately because the inline `style.width` was set before the browser had painted, making the CSS `transition: width 1s` a no-op. The dashboard now sets each bar's target width as a `--bar-target-width` CSS custom property and starts the bar at `0%`, then triggers the fill via a CSS `@keyframes bar-fill` animation on the next paint using `requestAnimationFrame`. The result is a smooth, sequenced bar-fill on every page load — giving the statistics section a living, premium feel. The animation is suppressed for `prefers-reduced-motion`. All 2,653 tests continue to pass and the TypeScript/Vite build succeeds. +**Files Modified**: src/style.css, src/main.ts, src/dashboard.ts, README.md, public/README.md + +--- + ### Day 63: 2026-04-19 **Feature/Change**: Frontend Polish - Hero Surface, Heading Rhythm & Timeline Card Refinement diff --git a/public/README.md b/public/README.md index 30b0bc0..ac5cf49 100644 --- a/public/README.md +++ b/public/README.md @@ -19,6 +19,13 @@ This is the living history of Chimera's evolution. Each entry represents a day o --- +### Day 64: 2026-05-03 +**Feature/Change**: Frontend Polish — Back-to-Top Button & Animated Progress Bars +**Description**: Two focused, high-impact visual improvements. **(1) Back-to-top floating button**: A polished fixed-position button appears with a spring animation (`ease-bounce`) after the user scrolls more than 400 px, making navigation effortless on the long single-page app. The button uses the brand gradient (`--color-accent → --color-accent-secondary`), has a soft accent glow (`--shadow-accent`) and a satisfying hover-lift/scale effect. Clicking it smoothly scrolls back to the top. The button is fully accessible (ARIA label, focus-visible ring) and is suppressed for users who prefer reduced motion. It adapts cleanly to both dark and light themes and scales down on mobile. **(2) Animated category progress bars**: The "Evolution Categories" breakdown bars previously snapped to their final width immediately because the inline `style.width` was set before the browser had painted, making the CSS `transition: width 1s` a no-op. The dashboard now sets each bar's target width as a `--bar-target-width` CSS custom property and starts the bar at `0%`, then triggers the fill via a CSS `@keyframes bar-fill` animation on the next paint using `requestAnimationFrame`. The result is a smooth, sequenced bar-fill on every page load — giving the statistics section a living, premium feel. The animation is suppressed for `prefers-reduced-motion`. All 2,653 tests continue to pass and the TypeScript/Vite build succeeds. +**Files Modified**: src/style.css, src/main.ts, src/dashboard.ts, README.md, public/README.md + +--- + ### Day 63: 2026-04-19 **Feature/Change**: Frontend Polish - Hero Surface, Heading Rhythm & Timeline Card Refinement diff --git a/src/dashboard.ts b/src/dashboard.ts index fcb3c2c..9f638ad 100644 --- a/src/dashboard.ts +++ b/src/dashboard.ts @@ -151,7 +151,14 @@ function createCategoryItem(name: string, count: number, maxCount: number): HTML bar.className = 'category-bar'; // Safety check to prevent division by zero const percentage = maxCount > 0 ? (count / maxCount) * 100 : 0; - bar.style.width = `${percentage}%`; + // Store the target width as a CSS custom property for the CSS animation, + // and start the bar at 0 width so it can animate in. + bar.style.setProperty('--bar-target-width', `${percentage}%`); + bar.style.width = '0%'; + // Trigger the fill animation on the next paint so the CSS transition fires + requestAnimationFrame(() => { + bar.classList.add('animate'); + }); barContainer.appendChild(bar); diff --git a/src/main.ts b/src/main.ts index 17a340b..2851be4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -251,6 +251,32 @@ document.querySelector('#app')!.innerHTML = ` const themeToggle = createThemeToggle() document.body.appendChild(themeToggle) +// Add back-to-top button +const backToTopBtn = document.createElement('button') +backToTopBtn.className = 'back-to-top' +backToTopBtn.setAttribute('aria-label', 'Back to top') +backToTopBtn.innerHTML = '⇧' +document.body.appendChild(backToTopBtn) + +let scrollTicking = false +window.addEventListener('scroll', () => { + if (!scrollTicking) { + requestAnimationFrame(() => { + if (window.scrollY > 400) { + backToTopBtn.classList.add('is-visible') + } else { + backToTopBtn.classList.remove('is-visible') + } + scrollTicking = false + }) + scrollTicking = true + } +}, { passive: true }) + +backToTopBtn.addEventListener('click', () => { + window.scrollTo({ top: 0, behavior: 'smooth' }) +}) + // Add tutorial launcher button to the page const tutorialLauncher = createTutorialLauncher() document.body.appendChild(tutorialLauncher) diff --git a/src/style.css b/src/style.css index 62a1be0..09db409 100644 --- a/src/style.css +++ b/src/style.css @@ -14445,3 +14445,120 @@ textarea:focus::placeholder { transform: translateY(-2px); } } + +/* ================================ + Day 64: Frontend Polish — Back-to-Top Button & Animated Progress Bars + ================================ */ + +/* ── 1. Back-to-top floating button ── */ + +/* + * --back-to-top-clearance: vertical gap above the theme-toggle button (56px + * button + 8px gap = 64px; align to spacing grid → 72px desktop, 68px mobile). + */ +:root { + --back-to-top-clearance: 72px; +} + +@media (max-width: 600px) { + :root { + --back-to-top-clearance: 68px; + } +} + +.back-to-top { + position: fixed; + bottom: calc(var(--space-lg) + var(--back-to-top-clearance)); + right: var(--space-lg); + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-secondary) 100%); + border: 2px solid rgba(255, 255, 255, 0.15); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 1.3rem; + font-weight: 700; + line-height: 1; + box-shadow: var(--shadow-accent); + z-index: 999; + + /* Hidden by default */ + opacity: 0; + transform: translateY(16px) scale(0.85); + pointer-events: none; + transition: + opacity 0.3s var(--ease-out), + transform 0.3s var(--ease-bounce), + box-shadow 0.2s var(--ease-out); +} + +.back-to-top.is-visible { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; +} + +.back-to-top:hover { + transform: translateY(-4px) scale(1.1); + box-shadow: var(--shadow-accent-hover); +} + +.back-to-top:active { + transform: translateY(-1px) scale(0.96); + transition-duration: 80ms; +} + +.back-to-top:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 4px; +} + +[data-theme="light"] .back-to-top { + border-color: rgba(124, 58, 237, 0.2); + color: #fff; +} + +@media (max-width: 600px) { + .back-to-top { + bottom: calc(var(--space-md) + var(--back-to-top-clearance)); + right: var(--space-md); + width: 44px; + height: 44px; + font-size: 1.15rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .back-to-top { + transition: opacity 0.15s ease; + transform: none !important; + } + .back-to-top.is-visible { + transform: none; + } +} + +/* ── 2. Category bar entrance animation ── */ +/* + Bars start at width 0 and expand to their data-target-width value + via a CSS animation triggered by the .animate class added in JS. +*/ +@keyframes bar-fill { + from { width: 0; } + to { width: var(--bar-target-width, 100%); } +} + +.category-bar.animate { + animation: bar-fill 1s var(--ease-out) forwards; +} + +@media (prefers-reduced-motion: reduce) { + .category-bar.animate { + animation: none; + width: var(--bar-target-width, 100%); + } +}