Skip to content

Releases: robertblust/design

0.9.0 — shared check code

Choose a tag to compare

@robertblust robertblust released this 01 Sep 13:21
cb0851a

Three new exports. The three sites re-pin and delete their copies next.

  • ./verify/design — 472 lines that were byte-identical in all three sites.
  • ./verify/httphttpStatus, which reads and discards the response body. Node 22's undici asserts assert(!this.paused) when a socket ends with an unread body; that crashed 6 of 12 CI runs before this helper existed.
  • ./verify/pagespageChecks({ SITE, BASE }), the nineteen shared check bodies. A factory because seo and card close over those two names, which lets all nineteen bodies move unchanged.

All nineteen were compared against the sites' text with their preceding comments, mechanically; the only accepted differences are a uniform two-space indent shift and two orphaned prefaces belonging to neighbouring functions.

Tests 120 → 131, every one mutated and seen red before being trusted. Zero dependencies, zero devDependencies, no Playwright import.

0.8.0 — remove the narration fallback that could not run

Choose a tag to compare

@robertblust robertblust released this 01 Sep 09:56

deck runtime v4, deck transport v4.

startNarration() opened on !pickVoice() && !hasClips(). The gate is synchronous and every way to detect a recording is asynchronous — under file:// a request is not available at all — so hasClips() could only ever return a constant true and the branch behind it never ran on any deck.

Removed: lcdMessage, msgTimer, lcdn, both novoice strings, and deck transport's .lcd:has(.n.msg) rule, which existed only to reveal that message below 400px. The transport block holds 56 rules now, not 57.

What actually handles a missing recording stays: clip.onerror in narrateCurrent() falls back to the speech synthesiser, per slide, after the failure rather than predicting it.

No behaviour change. Sites re-pin and run npm run design.

v0.7.0 — the disabled control kept the focus it was taking away

Choose a tag to compare

@robertblust robertblust released this 01 Sep 09:22

Two corrections to 0.6.x, both found by reviewing the branch as one change rather than task by task.

A disabled button dropped the keyboard user's focus

render() set the disabled property on prev, first and next at the ends of the deck. Correct
about intent, wrong about mechanism: the control becomes disabled as a result of the user's own
activation, while it holds focus
.

Measured in Chromium — focus a deck's Previous button and press Enter until slide zero:

focus
before tPrev
after reaching slide 0 BODY

The control deactivates under the finger and the user loses their place. It now sets
aria-disabled and guards each handler: the button stays where the user put it, stays announced,
and does nothing. Re-measured, focus stays on tPrev, and pressing it again is a no-op.

.lcd:has(.n.msg) shipped with a justification that is false on every deck

0.6.0's comment claimed the rule fixed a gap where companygraph's and guestgraph's decks "could
never show a message at that width at all". No deck can, at any width.

  • lcdMessage() has exactly one call site, gated on if (!pickVoice() && !hasClips())
  • hasClips() returns clipsSeen
  • clipsSeen is var clipsSeen = true, never reassigned, on all four decks

So .msg is never added and the rule never matches. lcdMessage, msgTimer and both novoice
strings are dead today.

The rule stays. It is correct and costs nothing, and it becomes live the moment anything
actually probes for clips. What changed is the comment: it now says the path is unreachable and
why, rather than claiming a fix nobody can observe. Whether the deck should probe is a design
question, deliberately not decided here.

Also corrected

  • A seam comment that quoted the value substituted into it, so each deck shipped a sentence
    contradicting itself — "its own LANG_KEY line reads rb-lang, a template, not a value".
  • A claim that a missing TALK leaves "every check green". It does not: that is a load-time
    ReferenceError, and opensFromFile fails on it. The identical sentence inherited from
    blocks/lang.js is accurate there, so only the new one changed.
  • Three documents describing a fence set two releases out of date — "Six blocks"/"Five blocks"
    where there are ten, and footerVersion still documented as the deck footer's assertion in all
    three sites' conventions files, though that check was retired a plan ago.

deck runtime and deck transport both to v3. 120 tests.

v0.6.1 — the deck runtime baked in one site's storage key

Choose a tag to compare

@robertblust robertblust released this 01 Sep 08:00

A defect in 0.6.0, and it had no fixed point.

What was wrong

blocks/deck-runtime.js carried:

var LANG_KEY = "rb-lang";

That is blust.ch's own key — a substituted value, captured when the block was extracted from
blust.ch's deck. blocks/lang.js, the standalone language fence, correctly carries the
{{langKey}} template; the runtime carried what that template had already become on one
particular page. And the deck runtime fence declared no params, so it had no way to substitute
anything.

The consequence was a loop

  1. Syncing deck runtime writes rb-lang into the page.
  2. The nested language fence's own pass rewrites it to the site's key.
  3. deck runtime's content no longer matches what the package emits, so design:check reports it
    as differing.
  4. The next sync starts over.

companygraph.io sat exactly there: functionally correct at cg-lang — which its own storageKeys
check requires — with design:check permanently red. guestgraph.io would have been the same.

blust.ch passed by coincidence. Its key is the one that was baked in.

The fix

The block carries {{langKey}}, byte-identical to what blocks/lang.js carries, and the fence
declares params: ["langKey"] the way "language" does. Both passes emit the same bytes, so the
sync settles.

runtimev2. Sites on 0.6.0 re-sync; there is no action beyond npm run design.

Two tests, and the second is the one that matters

  • No literal may follow LANG_KEY = " in the block. Fails when rb-lang is put back.
  • A fixed point, driven through bin/design.mjs: checksyncchecksynccheck,
    asserting the second sync writes nothing and the check after it is still clean.

That second test is the property that was missing. The first verification of this area checked that
the value came out right and never checked that the result was stable — which is the entire
difference between a working substitution and a loop.

120 tests.

v0.6.0 — the deck runtime

Choose a tag to compare

@robertblust robertblust released this 01 Sep 07:03

The four decks' runtimes were ~420 lines each, of which ~349 were already identical. What
differed was not layout but behaviour — and each difference had a reason written beside it, so this
release began by deciding them rather than taking a majority.

The four decisions

difference decided
companygraph's #langtoggle container, its click handler, the stopPropagation it forced onto each button, toggleLang(), and the divider between DE and EN removed — all four take the plain two-button control
companygraph's N and L key bindings removed — all four keep only the navigation keys
clipsSeen blust.ch's two lines — and it was never a difference at all
per-talk title and desc stay per-deck, as a payload the fence reads

clipsSeen is worth the detail. companygraph's eight-line version read as a lazy check and was not
one: it memoised true and probed nothing, under a comment claiming it "makes no network request
until someone actually presses play"
— which neither version does. All four decks carry recordings,
40 to 48 clips each, so the optimism was correct everywhere and only the ceremony differed.

blocks/deck-runtime.js

UI's 28 transport labels stay in the block — they describe the bar, not the talk, and are
identical on all four decks. The 4 that describe the talk come from a TALK payload the page
declares above the fence:

var TALK = { de:{ title:'…', desc:'…' }, en:{ title:'…', desc:'…' } };

That seam is invisible to design:check, which compares only the bytes between markers, so the
block states the contract in its own comment.

deck transport → v2

Two things. The transport had no disabled styling at all — not even a hover suppression, which
matters more than the dimming: without it a disabled button still lights up under the pointer and
reads as clickable. go() has always clamped at both ends of the deck; render() now says so.

And .lcd:has(.n.msg) joins the block. Below 400px .lcd{display:none} hides the counter to make
room, but lcdMessage() puts transient text in that same element — so companygraph's and
guestgraph's decks could never show a message at that width
. blust.ch's two had the rule; the
other two never did.

One thing corrected across all four

Two comments quoted "muss zuerst wissen, Gast" as an example. That phrase lives in guestgraph's
speaker notes and in no other deck's content — copied from deck to deck until three of the four
files carrying it quoted something they do not contain. All four now use the deck-agnostic wording.

118 tests.

v0.5.0 — the deck chrome

Choose a tag to compare

@robertblust robertblust released this 01 Sep 01:43

Three fences for the four presentation decks, and the repair of the one marker in this system that
never closed.

The marker that never ended

deck footer · v1 opened a region and never closed it. That is why four copies of it have never
been comparable, and why the spec called closing it "the first fix, and it is worth doing whether
or not the rest of this spec is adopted."
Closing it showed that it held two components wearing
one name.

deck transport — one form, and it never drifted

The spec put the transport at "162 / 150 / 154 / 160 lines, four forms, all different." Those
are the whole region counted by line, its prose comment and the lockup included. Compared as CSS
rules, all four decks are byte-identical. It needed no reconciliation — only an end marker and a
source.

One .name rule lives in this block rather than the lockup fence: .name{display:none}, inside the
860px query. It is not lockup identity, it is the bar saying it drops the lockup on narrow screens,
and it is identical everywhere.

deck lockup — two forms, which is why the split was worth making

blust.ch's two decks one tier — the mark plus Robert Blust
companygraph + guestgraph two tiers — the product lockup, then · ROBERT BLUST

There the product and the presenter are different names; here they are the same name, and a second
tier would read "Robert Blust · ROBERT BLUST". Eight rules are genuinely shared; each family's own
five and eight are spliced in by variant, verbatim from what those decks carry.

deck fit — the canvas scaler, and a comment describing a bug

Identical on all four decks but for one word: two said it scales "the 1600×900 canvas". That is
the fixed 16:9 shape that put 96px of black top and bottom on a 4:3 screen, and it was removed. Only
the height is pinned, at 900, and the width follows the screen — which is what the code does:
CH = 900, width derived as innerWidth / s.

One correction to what this project has been telling itself

Every one of these blocks used to carry, or sit beside, the claim that the copies exist because a
deck opens from file://, so there is nothing to import
. That is false. A relative <link> and a
relative @font-face both work from file:// — measured, fonts genuinely loading. Copies are a
deliberate choice: one rule for everything, and every page self-contained. The blocks say that
now, so nobody overturns the architecture on discovering the stated reason was wrong.

112 tests.

v0.4.0 — the prose reset and footer blocks

Choose a tag to compare

@robertblust robertblust released this 31 Aug 19:57

Two new blocks, and a defect in the variant machinery that needing them exposed.

blocks/reset.css — the five rules every prose page opens with

Four of the five had drifted across the sixteen prose pages:

rule what was wrong
.mono missing from two landing pages — and on one, an element marked as data was rendering as prose, in production, with every check green
a{color:inherit} missing from one
body font stack gained "Segoe UI" on five pages; lost ui-sans-serif, -apple-system and -webkit-font-smoothing on one

"Segoe UI" is dropped rather than adopted: ten pages omit it, five carry it, the split follows no
site and no page type, and system-ui already resolves to it on Windows.

blocks/footer.css + blocks/footer-credit.css — the footer, in two variants

companygraph.io and guestgraph.io credit blust.ch in their footer; blust.ch is that site and does
not credit itself. Ten prose pages carry nine credit rules and six carry none. That is a fact about
the sites, not a preference, so the wrong variant word is an error rather than a style choice.

parts — a third kind of substitution

blockFor could fill a variant word the package owns, and parameters the site owns.
Package-owned content appearing under one variant and not another had no name. Two whole block
files would have duplicated the four rules the variants share, which is the duplication this
package exists to remove.

The defect this exposed, shipping since v0.3.1

findFence hardcoded VARIANTS = new Set(["page","deck"]) while FENCES declares variants per
fence. A marker reading · credit therefore parsed as no variant at all, and the tool rejected
it saying "declares no variant" — about a fence whose opening line plainly declared one. The parser
now reports the word it finds; the manifest decides whether it is allowed, and says both what it
found and what is permitted.

This is also why the general variant-consistency guard shipped in v0.3.0 had never been driven end
to end. The stated reason was that no fence had variants differing by more than a :root brace.
The real reason underneath it: no fence could have, because a new variant word was unparseable.
It now fires on a real fence.

Also

  • headerv3. Its block said the row is byte-identical on all fifteen pages; there are sixteen. Correcting one word changes the block's bytes, so it waited for a release that re-syncs every page.
  • Every fence's emitted version is now asserted against versions.json. It was asserted for one fence out of four, and the test that appeared to cover the rest compared versions.json with itself.
  • The font-family guard covers every block the package ships, not one. This project shipped IBM Plex Mono — declared in no @font-face, present on no machine — for months.

106 tests.

v0.3.1 — a general variant guard, and parameters that survive readConfig

Choose a tag to compare

@robertblust robertblust released this 31 Aug 13:05

Fixes from the whole-branch review of v0.3.0. The language block's bytes change (a comment
gains the seam contract), so lang moves to v2 and all three sites need npm run design again.

Two of these are load-bearing for the next release, which cuts a deck-footer fence in two and
gives it a second parameter plus two form-variants that emit different bytes. Both would have bitten
there, and both were cheap now.

readConfig was dropping every parameter but the first

It returned a hardcoded {groups, langKey} while paramsFor read config values generically, by
parameter name
. A closed reader and an open consumer.

So a site adding a second parameter exactly as instructed would have been told
"…has no lockup — add it to design.config.json" — instructed to do the thing it had already
done. A confidently wrong message is worse than a confusing one. readConfig now returns every
parameter the config carries and validates each the way langKey was validated.

The variant guard only ran for fences with a :root brace

FENCES["language"].closes is null, so the guard was skipped — the variant word on that fence
was decorative, which is the opposite of what it was introduced for.

Harmless there, because both language variants emit identical bytes. Not harmless for a fence
whose variants differ. The guard is now general: for any fence with variants, a page whose content
matches what a different variant would emit is an error
, whether or not the fence has a brace.
The brace test stays where it applies — it catches the case before any sync has run, which the
general check cannot.

Where variants emit identical bytes, the general check honestly finds nothing, and the comment says
so rather than implying a guarantee that is not there.

Also

  • blockFor now rejects an undeclared parameter, not just a missing one — a typo'd parameter
    was previously ignored in silence.
  • blocks/lang.js states its seam contract: the page must supply a lang variable in scope and
    callers for langStored/langRemember. Those live outside the fence, so design:check cannot
    see them — a page that renames lang keeps a byte-perfect fence and throws on every click.
  • Two comments that described mechanisms which do not exist are corrected.

Adopting

npm install --save-dev "github:robertblust/design#v0.3.1"
npm run design && npm run og

v0.3.0 — the language block, and site-supplied parameters

Choose a tag to compare

@robertblust robertblust released this 31 Aug 11:59

A fourth block, and the package's first parameter that comes from your site rather than from
the package.

The block

The 37 lines that carry a reader's chosen language across all three domains — click German on one
site, follow a link to another, arrive in German. It is on all 20 pages and until now had no
fence, no version and nothing checking it
, which is exactly how it drifted into two dialects.

design:check will go red in all three sites until each adds one line of config and runs
npm run design.

The new thing: langKey

Every substitution so far came from the package — {{variant}} is chosen from a set the package
owns. This one does not. Each site declares its own storage key:

// design.config.json
{ "groups": ["fonts", "stage"], "langKey": "rb-lang" }

It cannot be derived: blust.ch stores under rb-lang, and nothing about the domain yields
that. And it is not a value to compute even if it could be — changing a storage key silently
discards every visitor's saved language.
It is a constant with a migration cost, so it lives
where changing it is a visible act.

A fenced page whose site declares no key is an error — exit 2, not a default. An empty key
would throw nowhere and would quietly give every visitor of that site the same nameless slot.

FAMILY is not a parameter, on purpose

The regex naming the three domains was hardcoded in 23 places — all 20 pages plus the
carriesLang check in three suites. It is now lib/family.mjs, importable as
@robertblust/design/family, and substituted into the block. Adding a fourth site becomes one
edit instead of twenty-three.

It is a constant, not a parameter, and the distinction is the point: a parameter is for what a
site is entitled to choose, not for what merely happens to be shared.

Adopting

npm install --save-dev "github:robertblust/design#v0.3.0"
# add "langKey" to design.config.json
npm run design && npm run og

The sync rewrites fence content only — no behaviour changes, and your storage key stays exactly
what it was.

v0.2.1 — a guard for the variant word

Choose a tag to compare

@robertblust robertblust released this 31 Aug 08:48

A guard release. No block content changed, so no site needs to re-sync — only to pin this
version to get the guard.

The hole it closes

A slide deck whose design tokens fence declares the wrong variant — page where it should say
deck — was silently broken, and every check reported green. That is what you get by seeding a
new deck from a prose page.

design sync would write the page variant, closing :root inside the fence. The deck's own
--warn, --slab and --lcd then sat outside :root as a parse error at stylesheet top level,
and CSS error recovery swallowed the stray } and the *{box-sizing:border-box; margin:0} rule
after it — so the deck lost its transport colours and its box reset. design sync --check then
reported , correctly: the page really did match what the package emits for page.

planFences now asserts the declared variant against the page's own shape — whether its existing
block closes :root — and throws when the two disagree. Exit 2, the same class as a malformed
fence, because that is what it is.

This is a consistency check between two signals that already exist, not the variant inference
the design deliberately refused. Its whole purpose is to fail loudly.

What was already safe and stays so: a missing variant throws, any misspelling throws, and a prose
page wrongly marked deck was never silent — :root simply never closed and the page rendered
unstyled. The one silent direction is now covered.

Also

design sync --check no longer prints design tokens is v4, this release ships v4 for a block
someone edited by hand. It says the block was edited locally and names where to change it instead.