<!-- skill: JavaScript Interactive | version: 1.0.0 | description: Modern ES2024 JS, DOM events, fetch, modules and debugging tricks | category: Web -->

## Instructions
- Write ES2024: `const/let` only (no `var`), arrow functions, template literals, destructuring, spread, optional chaining (`?.`) and nullish coalescing (`??`).
- Use strict mode + ES modules (`import/export`, `type="module"`); one concern per module, no globals except explicit app namespace.
- DOM: cache selectors, prefer `querySelector(All)`, delegate events on containers (`closest()`), never inline `onclick` attributes.
- Events: `addEventListener` with `{ once, passive, signal }` where apt; call `preventDefault()` deliberately; clean up with `AbortController`.
- Async: `async/await` + `try/catch` everywhere; wrap `fetch` in a helper that checks `res.ok`, parses JSON safely, and surfaces typed errors.
- JSON/storage: `JSON.parse` inside try/catch, schema-check shapes, version `localStorage` keys (`app:v1:`), handle quota/private-mode failures gracefully.
- Forms: read via `FormData` + `Object.fromEntries()`, validate on submit + blur, disable submit while pending, render inline errors with `aria-describedby`.
- Equality: default `===/!==`; know coercion traps (`==`, `0 == ""`, `NaN !== NaN`); convert explicitly with `Number()/String()/Boolean()`.
- `this` rules: arrows inherit, methods need shorthand/`bind`, never `this` in free functions; hoisting: `let/const`/TDZ vs hoisted `function` declarations.
- Debugging: `console.table/error`, breakpoints over `console.log` sprawl, Network tab for fetch, preserve-log + source maps; fail loudly in dev, gracefully in prod.
- Security/perf: escape HTML (use `textContent`, never raw `innerHTML` with user data), debounce search inputs, lazy-load below-fold work.
- Compatibility: target evergreen + feature-detect (`if ('fetch' in window)`), transpile/bundle only when shipping to older browsers.

## Examples
- Live-filter list: input event (debounced 150ms) filters 500-row list via delegation, empty-state message, result count with `aria-live="polite"`.
- Fetch wrapper + dashboard: `api()` helper with timeout/retries fetches user + posts, renders cards, shows skeleton → data → error states distinctly.
- Theme + persistence: light/dark toggle stored in `localStorage` (`app:v1:theme`), respects `prefers-color-scheme`, syncs across tabs via `storage` event.

## Workflows
1. Sketch module map: `api.js`, `store.js`, `ui.js`, `main.js` — wire imports before writing logic.
2. Build static DOM first, then attach one delegated listener per region, then add state + rendering functions.
3. Wrap every network call in `api()` with loading/error UI; test offline, 404, 500, and slow-3G paths.
4. Persist minimal state to `localStorage` with versioned keys; add migration/clear on shape change.
5. Debug pass: breakpoints, strict console (zero errors), keyboard test, then minify/bundle and retest prod build.

## Templates
```js
// api.js — fetch wrapper with timeout + typed errors
export async function api(path, { timeout = 8000, ...opts } = {}) {
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), timeout);
  try {
    const res = await fetch(path, {
      headers: { 'Content-Type': 'application/json' },
      signal: ctrl.signal, ...opts,
      body: opts.body ? JSON.stringify(opts.body) : undefined,
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error(data.message || `Request failed: ${res.status}`);
    return data;
  } catch (err) {
    if (err.name === 'AbortError') throw new Error('Request timed out');
    throw err;
  } finally { clearTimeout(t); }
}
// store.js — versioned localStorage
const KEY = 'app:v1:state';
export const store = {
  load(fallback = {}) { try { return { ...fallback, ...JSON.parse(localStorage.getItem(KEY)) }; } catch { return fallback; } },
  save(s) { try { localStorage.setItem(KEY, JSON.stringify(s)); } catch {} },
};
```
```js
// delegated list filter + debounce
const list = document.querySelector('#results');
document.querySelector('#search').addEventListener('input', debounce(onSearch, 150));
function debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }
function onSearch(e) {
  const q = e.target.value.trim().toLowerCase();
  let n = 0;
  for (const li of list.children) {
    const hit = li.textContent.toLowerCase().includes(q);
    li.hidden = !hit; if (hit) n++;
  }
  document.querySelector('#count').textContent = `${n} results`;
}
```

## Tools
- SARAH `file_write`: `app.js` as `type="module"` entry + `api.js` helper; keep `<script type="module" src="app.js">` at end of body.
- SARAH `shell_run`: `node --check app.js` for syntax; `npx eslint .` / `npx prettier --write .` when configs exist.
- SARAH preview: open page with DevTools console visible; confirm zero errors, working fetch, and keyboard operability.

## Advanced
- Pitfalls: stale closures in loops (use `let`/forEach), floating promises (always await or `.catch`), `NaN` checks via `Number.isNaN`, date handling via `Intl` + UTC storage.
- Events: prefer delegation (`list.addEventListener('click', e => e.target.closest('button'))`); use `AbortController` to remove listeners on teardown.
- Perf: `requestAnimationFrame` for scroll anims, `IntersectionObserver` for lazy-load, `Web Worker` for heavy loops; avoid layout thrash (batch reads/writes).
- Troubleshooting: `this` undefined → strict/module context; double-fetch in dev → StrictMode/remount; CORS error → missing server header, not a fetch bug.
- Pro tip: `structuredClone()` for deep copy, `URLSearchParams` for query strings, `crypto.randomUUID()` for IDs — stop hand-rolling these.

## Documentation
- MDN JavaScript: https://developer.mozilla.org/en-US/docs/Web/JavaScript
- javascript.info: https://javascript.info
- Node.js docs: https://nodejs.org/en/docs
