Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .github/assets/digest-apart.webp
Binary file not shown.
Binary file modified .github/assets/digest-card-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/assets/digest-keyed.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/assets/walkthrough-keyed.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Until the first release, changes are recorded under [Unreleased].

### Changed

- `anatomy`: the flat and screenshot views put each key's text in the margin, joined to its part by a hairline (a bracket for a key with `"span": true`, a dot otherwise), instead of numbered discs and a legend. Below about 560px they fall back to small numbers and the legend; `"notes": "keys"` keeps the old hotspots, and iso always uses them. The flat view takes the iso view's tones with an offset shadow, and its list rows are drawn as raised slabs. The weekly-digests card is drawn flat instead of iso, and the committed weekly-digests and walkthrough-videos posts and the README's hero and example images are re-rendered with margin notes. The examples page and `docs/components.md` show the new hero in place of the iso "apart" picture (`digest-apart.webp` is removed), and the link previews are rebuilt. `lint.mjs` counts margin notes as hotspots, and `inline-assets.mjs` and the gallery handle an SVG `<image href>`.

- Length guidance sizes a post by the scope of the change, with bands measured against published Cloudflare and Astro posts (`references/story.md`). The deep dive gets a soft ceiling of about 3,000 words: `lint.mjs` warns past it and the author can let it stand.
- Lessons from comparing two outside deep dives with the projects' own announcements: stage a cause a reader would hit in a demo run and show the fix (`references/evidence.md`), read a source's claim in its context before calling it wrong (`references/evidence.md`), walk a report from the top down to a cause even in a deep dive, and let the try-it prove the feature's promise (`references/story.md`), and read the release a feature shipped in for where it fits (`references/brief.md`).
- The skill's argument hint and description name the deck (`[post|video|deck] …`, with a release story and a deep dive as examples), and the site's Deck card shows a `/buildnotes deck …` request like the other outputs. The home page's hero shows the request's shape under the install command: `/buildnotes [post | video | deck] what to write about`.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
</p>

<p align="center">
<img src=".github/assets/digest-card-hero.webp" width="720" alt="An isometric line drawing of the 'week at a glance' card from a weekly digest: product tiles split into columns, the largest one in blue, flame marks on two tiles, and five release rows below. Five numbered keys name its parts: flames, tile size, columns, click a tile, and the five biggest releases. Above it, an Assembled / Apart toggle; below it, the post's figure caption.">
<img src=".github/assets/digest-card-hero.webp" width="720" alt="A hand-drawn, straight-on drawing of the 'week at a glance' card from a weekly digest: product tiles split into grey columns, the largest one in blue, flame marks on two tiles, and five release rows below. Notes in the margins name its parts, each joined to its part by a hairline: flames, tile size, columns, click a tile, and the five biggest releases. Below it, the post's figure caption.">
</p>

<p align="center"><sub><code>examples/releases-weekly-digests</code>: Fig. 1 of a build story the skill wrote from a public repo's PRs. Its parts are measured from the shipped card, and on the page they lift, come apart and explain themselves.</sub></p>
<p align="center"><sub><code>examples/releases-weekly-digests</code>: Fig. 1 of a build story the skill wrote from a public repo's PRs. Its parts are measured from the shipped card and named in the margin, where pointing at a note picks out its part.</sub></p>

---

Expand Down Expand Up @@ -63,15 +63,15 @@ Three full runs are kept as committed examples. Each folder holds the finished p

<table>
<tr>
<td width="50%"><img src=".github/assets/walkthrough-keyed.webp" alt="One frame of a floor tour at eye level in the Boardroom, with five numbered keys: the shot, the mini-map in the top-right corner, the tinted room, the camera's dot and wedge, and the caption with the room's name and size."></td>
<td width="50%"><img src=".github/assets/walkthrough-keyed.webp" alt="One frame of a floor tour at eye level in the Boardroom, with five notes in the margins: the shot, the caption with the room's name and size, the camera's dot and wedge, the tinted room, and the mini-map in the top-right corner."></td>
<td width="50%"><img src=".github/assets/before-after-compare.webp" alt="The uploads.sh file page in Compare mode: a REST API docs screenshot with a purple divider down the middle, Before on the left and After on the right."></td>
</tr>
<tr>
<td><sub><b><a href="examples/sunny-walkthrough-videos">Walkthrough videos</a></b>: a build story about room, floor and project tours in <a href="https://app.sunnysdk.com">Sunny</a>, a furniture and space planner. The repo is private, so the post quotes no code and links nothing internal. Field notes style. <a href="https://buildnotes.releases.sh/examples/sunny-walkthrough-videos/">Read it on the site</a>, or watch its <a href="examples/sunny-walkthrough-videos/motion.mp4">52-second cut</a>.</sub></td>
<td><sub><b><a href="examples/uploads-before-after">Before and after</a></b>: a build story about before/after comparison on <a href="https://uploads.sh">uploads.sh</a>, from the public <code>buildinternet/uploads</code> repo. Its figures are the product in use, captured live. Plain style. <a href="https://buildnotes.releases.sh/examples/uploads-before-after/">Read it on the site</a>, or watch its <a href="examples/uploads-before-after/motion.mp4">47-second cut</a>.</sub></td>
</tr>
<tr>
<td colspan="2"><img src=".github/assets/digest-keyed.webp" alt="The shipped 'week at a glance' card with five numbered keys on its parts, and a legend under it: flames, tile size, columns, click a tile, and the five biggest releases."></td>
<td colspan="2"><img src=".github/assets/digest-keyed.webp" alt="The shipped 'week at a glance' card with five notes in the margins, each joined to its part: flames, tile size, columns, click a tile, and the five biggest releases."></td>
</tr>
<tr>
<td colspan="2"><sub><b><a href="examples/releases-weekly-digests">Weekly digests</a></b>: a build story about the weekly collection digests on Release Notes Index, from the public <code>buildinternet/releases</code> repo. <a href="https://buildnotes.releases.sh/examples/releases-weekly-digests/">Read it on the site</a> in field notes, with the card above as its first figure, or open <a href="examples/releases-weekly-digests/post.html"><code>post.html</code></a> for the same post in the Plain style. Its <a href="examples/releases-weekly-digests/motion.mp4">one-minute cut</a> plays in the <a href="#motion-cut">Motion cut</a> section below.</sub></td>
Expand Down
8 changes: 4 additions & 4 deletions dev/gallery.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const ABOUT = {
code: "Real code from git at a given commit, cut to the lines that make the point. Renders the whole figure.",
term: "One command and the output lines that matter. Renders the whole figure.",
strip: "Before/after, or a progression of up to four rounds: images, code or terminal output.",
anatomy: "The parts of one thing, named: a keyed figure drawn from a parts list measured on a capture. Flat shows it straight on with its own labels, iso at an angle as structure only (it comes apart on request), screenshot puts the keys on the capture itself. Pointing at a part lifts it; pointing at a key fades the rest.",
anatomy: "The parts of one thing, named: a keyed figure drawn from a parts list measured on a capture. Flat redraws it straight on in grey and accent tones, and screenshot uses the capture itself; both put each key's text in the margin, joined to its part by a bracket or a dot. Iso draws it at an angle with numbered keys, and comes apart on request. Pointing at a note fades the other parts.",
shader: "A WebGL figure drawn by a fragment shader, for what boxes and lines can't show: many things moving at once, a field or gradient, or pixels that are themselves the subject. Follows the theme, pauses off screen, holds a still frame for reduced motion.",
};
const ORDER = Object.keys(ABOUT);
Expand All @@ -61,8 +61,8 @@ const EXAMPLES = {
"strip-rounds.json": "Three rounds of a design, as images.",
"strip-filter.json": "Before and after, as terminal output.",
"anatomy-card.json": "At an angle: structure only, with the object's title on its base. Assembled / Apart lifts the parts off, and the keys go with them.",
"anatomy-card-flat.json": "Straight on, with the object's own labels. The same spec, through \"from\".",
"anatomy-card-shot.json": "Keys over the capture itself. Expand opens it full size.",
"anatomy-card-flat.json": "Straight on, with the object's own labels and margin notes. The same spec, through \"from\".",
"anatomy-card-shot.json": "The capture itself, with margin notes. Narrow the page to see numbers and a legend instead.",
"shader-jitter.json": "Many things at once: 80 clients retrying, and the load they put on the server. Loops every 5 s on u_phase; labels sit over the canvas.",
"shader-rehash.json": "The mechanism behind a number: which keys move when a server joins, and why the ring moves so few. Loops every 5 s.",
"shader-banding.json": "When the pixels are the subject: a rendering fix, shown on the reader's own screen. A still shader, with the level count as an extra uniform.",
Expand All @@ -85,7 +85,7 @@ for (const f of specs) {
const kind = f.split("-")[0];
const id = f.replace(/\.\w+$/, "");
const text = readFileSync(join(specDir, f), "utf8");
const frag = renderKind(kind, text, { id, base: specDir, style }).replace(/src="img\//g, `src="${imgBase}/img/`);
const frag = renderKind(kind, text, { id, base: specDir, style }).replace(/(src|href)="img\//g, (_, a) => `${a}="${imgBase}/img/`);
for (const w of takeWarnings()) { warned++; console.error(`! ${f}: ${w}`); }
const name = id.slice(kind.length + 1);
const whole = /^(code|term)$/.test(kind);
Expand Down
65 changes: 50 additions & 15 deletions dev/test/anatomy.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Checks for the anatomy renderer: one parts list, three views, each a keyed figure. Run: node --test dev/test/*.test.mjs
// Checks for the anatomy renderer: one parts list, three views, margin notes or numbered keys. Run: node --test dev/test/*.test.mjs
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
Expand All @@ -15,29 +15,64 @@ const card = spec("anatomy-card.json");
const hotspots = (html) => [...html.matchAll(/<button type="button" class="hs" data-k="(\d+)" style="--x:([\d.-]+)%;--y:([\d.-]+)%/g)].map((m) => [m[1], Number(m[2]), Number(m[3])]);
const entries = (html) => [...html.matchAll(/<li data-k="(\d+)">/g)].map((m) => m[1]);

for (const view of ["flat", "iso", "screenshot"]) {
test(`${view}: a keyed figure whose hotspots match its legend and sit on the figure`, () => {
const notes = (html) => [...html.matchAll(/<g class="nk" data-k="(\d+)">/g)].map((m) => m[1]);
const numbers = (html) => [...html.matchAll(/<g class="nn" data-k="(\d+)">/g)].map((m) => m[1]);
const viewBox = (html, cls) => html.match(new RegExp(`<svg class="${cls}" viewBox="([^"]+)"`))[1].split(" ").map(Number);

for (const view of ["flat", "screenshot"]) {
test(`${view}: margin notes by default, one per key, with numbers and a legend for narrow screens`, () => {
const html = render({ ...card, view }, { id: "t", base });
assert.deepEqual(takeWarnings(), []);
assert.match(html, new RegExp(`class="keyed anatomy anatomy-${view}"`));
const hs = hotspots(html);
assert.deepEqual(hs.map((h) => h[0]), entries(html));
assert.equal(hs.length, card.keys.length);
for (const [k, x, y] of hs) assert.ok(x >= 0 && x <= 100 && y >= 0 && y <= 100, `key ${k} at ${x}%, ${y}%`);
assert.match(html, new RegExp(`class="keyed anatomy anatomy-${view} anatomy-margin"`));
const ks = card.keys.map((_, i) => String(i + 1));
assert.deepEqual(notes(html).sort(), ks);
assert.deepEqual(numbers(html), ks);
assert.deepEqual(entries(html), ks);
assert.doesNotMatch(html, /class="hs"/, "no hotspot buttons in the margin view");
// the wide drawing grows sideways for the notes; the narrow one is the figure alone
const [wx, , ww] = viewBox(html, "wide"), [nx, , nw] = viewBox(html, "narrow");
assert.ok(wx < nx && wx + ww > nx + nw, "notes on both sides widen the drawing both ways");
});

test(`${view}: notes: "keys" keeps the numbered hotspots`, () => {
const html = render({ ...card, view, notes: "keys" }, { id: "t", base });
assert.deepEqual(takeWarnings(), []);
assert.deepEqual(hotspots(html).map((h) => h[0]), entries(html));
for (const [k, x, y] of hotspots(html)) assert.ok(x >= 0 && x <= 100 && y >= 0 && y <= 100, `key ${k} at ${x}%, ${y}%`);
});
}

test("the screenshot view puts each key where the spec says, in percent of the capture", () => {
test("iso: a keyed figure whose hotspots match its legend and sit on the figure", () => {
const html = render({ ...card, view: "iso" }, { id: "t", base });
assert.deepEqual(takeWarnings(), []);
assert.match(html, /class="keyed anatomy anatomy-iso"/);
const hs = hotspots(html);
assert.deepEqual(hs.map((h) => h[0]), entries(html));
assert.equal(hs.length, card.keys.length);
for (const [k, x, y] of hs) assert.ok(x >= 0 && x <= 100 && y >= 0 && y <= 100, `key ${k} at ${x}%, ${y}%`);
});

test("margin notes: a span key brackets its part, a point key gets a dot where the spec says", () => {
const html = render({ ...card, view: "screenshot" }, { id: "t", base });
const [k, x, y] = hotspots(html)[1];
assert.equal(k, "2");
assert.equal(x, Math.round((300 / 1792) * 1000) / 10);
assert.equal(y, Math.round((560 / 1878) * 1000) / 10);
assert.match(html, /<img src="img\/digest-card\.webp"/);
const note = (k) => html.match(new RegExp(`<g class="nk" data-k="${k}">([\\s\\S]*?)</g>`))[1];
const codex = card.parts.find((p) => p.id === "codex").box;
assert.match(note(2), new RegExp(`V${codex[1] + codex[3]} H`), "the tile key brackets the Codex tile's height");
assert.doesNotMatch(note(2), /<circle/);
assert.match(note(1), /<circle class="nd" cx="640" cy="217"/, "the flame key's dot sits at its at");
assert.match(note(1), /<tspan class="lead">Flames\.<\/tspan>/, "the bold lead-in stays bold");
assert.match(html, /<image class="img" href="img\/digest-card\.webp"/);
});

test("margin notes on one side only leave the other margin off", () => {
const keys = card.keys.map((k) => ({ ...k, side: "right" }));
const html = render({ ...card, view: "flat", keys }, { id: "t", base });
assert.equal(viewBox(html, "wide")[0], viewBox(html, "narrow")[0]);
assert.throws(() => render({ ...card, view: "flat", keys: [{ ...keys[0], side: "top" }] }, { id: "t", base }), /side "top"/);
assert.throws(() => render({ ...card, view: "flat", keys: [{ at: [10, 10], span: true, text: "x" }] }, { id: "t", base }), /"span" but no part/);
});

test("a screenshot key about the whole frame needs only an at, no part", () => {
const html = render({ ...card, view: "screenshot", keys: [...card.keys.slice(0, 2), { at: [896, 1700], text: "**The whole card.** No part to outline." }] }, { id: "t", base });
const html = render({ ...card, view: "screenshot", notes: "keys", keys: [...card.keys.slice(0, 2), { at: [896, 1700], text: "**The whole card.** No part to outline." }] }, { id: "t", base });
assert.deepEqual(takeWarnings(), []);
assert.deepEqual(hotspots(html).at(-1), ["3", 50, Math.round((1700 / 1878) * 1000) / 10]);
assert.throws(() => render({ ...card, view: "screenshot", keys: [{ text: "Nowhere." }] }, { id: "t", base }), /key 1 needs "at" or a part with a box/);
Expand Down
13 changes: 7 additions & 6 deletions dev/test/site.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ test("the site builds: markers filled, files copied, every command copyable", ()
const { html, files } = build({ out });
assert.doesNotMatch(html, /<!-- (figure|iso) |\/\* template: |\/\/ template: /);
// the two live figures, with the template's CSS and script to drive them
assert.match(html, /class="keyed anatomy anatomy-screenshot"/);
assert.match(html, /class="keyed anatomy anatomy-iso"/);
assert.match(html, /class="keyed anatomy anatomy-screenshot anatomy-margin"/);
assert.match(html, /class="keyed anatomy anatomy-flat anatomy-margin"/);
assert.match(html, /@container \(max-width: 559px\)/, "the margin notes' narrow fallback");
assert.match(html, /\.anatomy-iso:has\(\.apart:checked\)/);
assert.match(html, /for \(const fig of document\.querySelectorAll\("\.keyed"\)\)/);
assert.doesNotMatch(html, /zoom-open/, "the expand script stays out; the site has no CSS for it");
Expand All @@ -25,7 +26,7 @@ test("the site builds: markers filled, files copied, every command copyable", ()
assert.match(html, /<code data-copy>npx skills add buildinternet\/buildnotes<\/code>/);
// local references point at copied files, and .github/assets is published as assets/
for (const f of files) assert.ok(existsSync(join(out, f)), f);
assert.ok(files.includes("assets/digest-apart.webp"));
assert.ok(files.includes("assets/digest-card-hero.webp"));
assert.doesNotMatch(html, /\.github\//);
assert.match(html, /<html lang="en" data-theme="light">/);
// every example is served as a folder with its video, its post wrapped in a document and the site shell
Expand Down Expand Up @@ -55,9 +56,9 @@ test("the site builds: markers filled, files copied, every command copyable", ()
assert.match(deck, /"deck":true/);
assert.match(readFileSync(join(out, `examples/${ex.slug}/index.html`), "utf8"), /href="deck\.html"/);
}
// the weekly-digests example is the field-notes page, the one with the card taken apart
// the weekly-digests example is the field-notes page, the one with the card drawn flat
const digests = readFileSync(join(out, "examples/releases-weekly-digests/index.html"), "utf8");
assert.match(digests, /class="keyed anatomy anatomy-iso"/);
assert.match(digests, /class="keyed anatomy anatomy-flat anatomy-margin"/);
assert.throws(() => wrapPost("nope", "<style></style>"), /not in EXAMPLES/);
// the examples page: one card per example, the home page's stylesheet, links one folder up
const list = readFileSync(join(out, "examples/index.html"), "utf8");
Expand All @@ -71,7 +72,7 @@ test("the site builds: markers filled, files copied, every command copyable", ()
assert.match(list, new RegExp(`href="${ex.slug}/#site-video"`), ex.slug);
if (ex.deck) assert.match(list, new RegExp(`href="${ex.slug}/deck\\.html"`), ex.slug);
}
assert.match(list, /src="\.\.\/assets\/digest-apart\.webp"/);
assert.match(list, /src="\.\.\/assets\/digest-card-hero\.webp"/);
assert.match(list, /href="\.\.\/#install"/);
// the footer, on both pages: columns and the maintained-by line
for (const page of [html, list]) {
Expand Down
4 changes: 2 additions & 2 deletions docs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ The images on this page were exported with the skill's own [`scripts/export.mjs`
<td colspan="2"><sub><b>shader</b>: a WebGL fragment shader, for posts where the pixels are the subject, shown on the reader's own screen.</sub></td>
</tr>
<tr>
<td colspan="2"><img src="../.github/assets/digest-apart.webp" alt="The 'week at a glance' card drawn at an angle in the field notes style, taken apart: the product tiles and the list of releases lifted off the base, with dashed outlines where they sat and five numbered keys."></td>
<td colspan="2"><img src="../.github/assets/digest-card-hero.webp" alt="The 'week at a glance' card drawn straight on in the field notes style: product tiles split into grey columns, the largest in blue, and five release rows, with notes in the margins joined to each part by a hairline."></td>
</tr>
<tr>
<td colspan="2"><sub><b>anatomy</b>: one parts list, measured on a capture, drawn flat, at an angle and taken apart, or as keys over the capture itself. This one is from <code>examples/releases-weekly-digests</code>.</sub></td>
<td colspan="2"><sub><b>anatomy</b>: one parts list, measured on a capture, drawn flat or shown as the capture itself with notes in the margin, or at an angle and taken apart. This one is from <code>examples/releases-weekly-digests</code>.</sub></td>
</tr>
</table>

Expand Down
Loading
Loading