9ca3f48def
- 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
58 lines
2.3 KiB
TypeScript
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>
|