Files
ai-for-dummies/src/components/islands/ReadingProgress.astro
T
Marcos Paulo c6e6657086 feat(islands): add CopyPrompt and ReadingProgress for full-guide
Two islands extracted from app.js for the full-guide migration (15d):

- CopyPrompt: one instance per button. Reads #<target>.textContent,
  copies via navigator.clipboard.writeText with a document.execCommand
  textarea fallback (kept because workshop venues serve the site over
  plain HTTP, where the clipboard API is undefined — deleting the
  fallback silently breaks the lab). Writes a bilingual result string
  to the page-owned #copy-status live region and swaps the <span> to
  COPIED/COPIADO for 1800ms. Language comes from document.documentElement
  .lang via a MutationObserver, so 15c's toggle stays the single
  mechanism.

- ReadingProgress: renders .reading-progress span and attaches a passive
  scroll listener that mirrors the existing app.js line 406 handler.

Both scripts use <script is:inline> with a wire-once window flag, so a
page that mounts the same island multiple times still ends up with
exactly one set of listeners.

Not done in this task:
- app.js copyPrompt and reading-progress lines stay intact (verify.mjs
  still asserts the copyPrompt token against app.js; the verification-
  engineer owns that swap, scheduled for 15d)
- src/pages/full-guide.astro (15d)
- verify.mjs, tokens.css, src/content/config.ts
- reformat of app.js

For 15d:
- import CopyPrompt three times (one per target: prompt-install-skills,
  prompt-basic, prompt-skills)
- render <p id="copy-status" role="status" aria-live="polite"></p>
  once on the page; the island writes to it
- import ReadingProgress and place it where the current .reading-
  progress div sits
- prompt bodies for the <pre><code id="prompt-..."> elements come
  from src/content/{handsOnPrompts,skillInstallPrompts}; verified
  byte-equal to app.js — what lands on the clipboard is whatever
  those elements contain

For 15c:
- language mechanism is document.documentElement.lang via MutationObserver;
  do not invent a parallel signal

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-05 17:49:48 +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.width = (height > 0 ? (window.scrollY / height) * 100 : 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>