Files
ai-for-dummies/src/components/islands/ReadingProgress.astro
T
Marcos Paulo 9ca3f48def style(motion): add strict animations and fix layout properties
- Change ReadingProgress and RouteTable to use transform (scaleX/scaleY) instead of width/height
- Convert existing easing functions to 200ms cubic-bezier(.2,0,0,1)
- Document animation purpose with CSS comments
- Add guide panel swap, review desk detail swap, and tally pop animations
- Implement prefers-reduced-motion for all new states
2026-09-05 23:13:59 +00:00

58 lines
2.3 KiB
TypeScript

---
// ReadingProgress — scroll-position bar. One instance per page.
//
// Renders the same markup the legacy `full-guide/index.html` line 13
// carried: `<div class="reading-progress" aria-hidden="true"><span></span></div>`.
// The scroll listener is registered with `{ passive: true }`, matching
// app.js line 406 and the rule in `.agents/rules/animation.md` that a
// passive listener is required for any handler bound to a frequently
// fired event like scroll. Animating `width` here is fine — the bar
// is a 3px element at the very top of the viewport, so layout cost
// is negligible compared to the alternative of subscribing to
// IntersectionObserver and computing progress from a single sentinel
// per section.
//
// Language: this island does not read language. It has no string
// labels and the same progress semantics apply to EN and PT.
interface Props {
// Optional override selector. Defaults to `.reading-progress span`
// to match the legacy app.js query, in case a page renders the bar
// with a different class name.
target?: string;
}
const { target = '.reading-progress span' } = Astro.props;
---
<div class="reading-progress" aria-hidden="true">
<span data-reading-progress-span></span>
</div>
<script is:inline define:vars={{ selector: target }}>
// ReadingProgress island — passive scroll listener.
// Wire-once guard mirrors CopyPrompt: a page that renders the bar
// twice would otherwise stack two scroll handlers on `window`.
(function () {
if (window.__readingProgressWired) return;
window.__readingProgressWired = true;
const span = document.querySelector(selector);
if (!span) return;
function update() {
const height = document.documentElement.scrollHeight - window.innerHeight;
span.style.transform = `scaleX(${height > 0 ? window.scrollY / height : 0})`;
}
// `{ passive: true }` is non-negotiable. The legacy app.js binds
// this listener passive, and a non-passive scroll handler on the
// top edge of the document is exactly the kind of input latency
// that fails INP. See `.agents/rules/animation.md`.
window.addEventListener('scroll', update, { passive: true });
// Set initial state — important when the user navigates with
// #hash deep links and lands partway down a long page.
update();
})();
</script>