diff --git a/dev/test/site.test.mjs b/dev/test/site.test.mjs index 6f3ba9e..42eff73 100644 --- a/dev/test/site.test.mjs +++ b/dev/test/site.test.mjs @@ -24,7 +24,7 @@ test("the site builds: markers filled, files copied, every command copyable", () const rows = html.match(/
/g) ?? []; assert.equal(rows.length, (html.match(/
+
npx skills add buildinternet/buildnotes

For Claude Code, Codex and OpenCode

Then, inside your repo:/buildnotes [post | video | deck] what to explain
@@ -387,7 +399,7 @@

More examples

Install

Needs Node 22, git and Chrome.

-
npx skills add buildinternet/buildnotes
+
npx skills add buildinternet/buildnotes

Then, inside your repo:

/buildnotes write up how we built preview links, from PRs 120, 131 and 140
@@ -407,9 +419,47 @@

Install

} catch (e) { selectIt(); } }); }); -if (matchMedia('(prefers-reduced-motion: reduce)').matches) { +var still = matchMedia('(prefers-reduced-motion: reduce)').matches; +if (still) { document.querySelectorAll('video[autoplay]').forEach(function (v) { v.removeAttribute('autoplay'); v.pause(); v.setAttribute('controls', ''); }); } + +// Touch screens: nothing to point with, so the words say tap, and the post panel becomes a scroll box. It +// opens at the post's first figure and, once it's well in view, drifts down the post (at the hover scroll's +// pace) until a finger takes over. Scrolling inside it never opens the post; a tap still does. +if (matchMedia('(hover: none)').matches) { + document.querySelectorAll('.cap-top').forEach(function (p) { p.textContent = p.textContent.replace('Point at a note', 'Tap a note'); }); + document.querySelectorAll('.scroller').forEach(function (a) { + var img = a.querySelector('img'), box = document.createElement('div'), hint = a.querySelector('.hint'); + box.className = 'box'; a.prepend(box); box.append(img); + if (hint) hint.textContent = 'swipe to scroll'; + a.setAttribute('aria-label', a.getAttribute('aria-label').replace('Point at it to scroll through the whole post.', 'Swipe inside it to scroll through the whole post.')); + var pct = function (n) { return -parseFloat(a.style.getPropertyValue(n)) / 100; }; + var drift = 0, held = false; + var start = function () { box.scrollTop = pct('--rest') * img.offsetHeight; }; + if (img.complete) start(); else img.addEventListener('load', start); + ['pointerdown', 'wheel', 'touchstart'].forEach(function (t) { box.addEventListener(t, function () { held = true; cancelAnimationFrame(drift); }, { passive: true }); }); + if (still) return; + new IntersectionObserver(function (es) { + cancelAnimationFrame(drift); + if (!es[0].isIntersecting || held) return; + var end = Math.min(pct('--end') * img.offsetHeight, box.scrollHeight - box.clientHeight); + var speed = (pct('--end') - pct('--rest')) * img.offsetHeight / 14000, y = box.scrollTop, t0 = 0; // px per ms + var step = function (t) { + if (t0) y = Math.min(end, y + speed * (t - t0)); + t0 = t; box.scrollTop = y; + if (y < end) drift = requestAnimationFrame(step); + }; + drift = requestAnimationFrame(step); + }, { threshold: 0.6 }).observe(a); + }); +} + +// The pipeline drawing is wider than a phone: until it's scrolled to its end, its right edge fades out. +document.querySelectorAll('.drawing').forEach(function (d) { + var check = function () { d.classList.toggle('more', d.scrollLeft + d.clientWidth < d.scrollWidth - 4); }; + d.addEventListener('scroll', check, { passive: true }); addEventListener('resize', check); check(); +}); diff --git a/skills/buildnotes/assets/template.html b/skills/buildnotes/assets/template.html index 301f3d3..2f071a3 100644 --- a/skills/buildnotes/assets/template.html +++ b/skills/buildnotes/assets/template.html @@ -488,7 +488,7 @@ /* A note shows only while its hotspot is pointed at or focused; hidden, it takes no room (so it never widens the page). */ .hs .tip { display: none; position: absolute; top: 50%; left: calc(100% - 2px); width: max-content; max-width: 15rem; transform: translateY(-50%); padding: 0.55rem 0.75rem; background: var(--paper); border: 1px solid var(--rule); border-radius: 6px; box-shadow: 0 1px 0 var(--rule); font: 400 0.875rem/1.4 var(--font-body); color: var(--ink); text-align: left; pointer-events: none; z-index: 2; } .hs.left .tip { left: auto; right: calc(100% - 2px); } -.hs:is(:hover, :focus-visible) .tip { display: block; animation: tip-in 0.16s ease-out; } +.hs:is(:hover, :focus-visible, .tap) .tip { display: block; animation: tip-in 0.16s ease-out; } /* .tap: tapped on a touch screen */ .keyed:has(.hs:hover) .hs:not(:hover) .tip { display: none; } @keyframes tip-in { from { opacity: 0; } } /* In a narrow figure the note opens below its hotspot, shifted by the hotspot's own position so it stays inside. */ @@ -1085,7 +1085,19 @@

Further reading

} const light = (k) => fig.querySelectorAll("[data-k]").forEach((el) => el.classList.toggle("on", el.dataset.k === k)); fig.addEventListener("pointerover", (e) => { const k = e.target.closest?.("[data-k]")?.dataset.k; light(k ?? null); }); - fig.addEventListener("pointerleave", () => light(null)); + fig.addEventListener("pointerleave", (e) => { if (e.pointerType !== "touch") light(null); }); + // A touch screen has no hover to hold a note open: a tap lights it, and opens a hotspot's note, until the + // next tap, inside the figure or out of it. + let tapped = null; + const tap = (el) => { + tapped?.classList.remove("tap"); + tapped = el?.classList.contains("hs") ? el : null; + tapped?.classList.add("tap"); + light(el?.dataset.k ?? null); + }; + document.addEventListener("pointerdown", (e) => { + if (e.pointerType === "touch") tap(fig.contains(e.target) ? e.target.closest?.("[data-k]") ?? null : null); + }); fig.addEventListener("focusin", (e) => light(e.target.closest?.(".hs")?.dataset.k ?? null)); fig.addEventListener("focusout", () => light(null)); }