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>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
---
|
||||
// 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>
|
||||
Reference in New Issue
Block a user