The working surface. We iterate here and the doc records what this settles —
docs/design-codebook-v2.md is the record, not the driver.
Baselines below are the shipped lens rendered faithfully, so each v2 slot has something to be measured against. Nothing here is a proposal yet.
.is-new — invented, not inherited
.is-new so invention stays visible and countable.Rendered from the shipped tokens and component recipes, not redrawn. This is the thing v2 iterates against.
Three encodings, one column. Blue dot on, grey dot off, transparent slot available — and the section heading is the only thing separating “installed” from “installable”.
The floor gets a bare slot so labels stay left-aligned. Rows are navigation for installed codebooks and a Library door for the rest — the same affordance doing two jobs.
Empty until the issues and fixes land. Each iteration gets its own frame here, captioned in the chrome bar, with its reasoning in a commentary block below it.
The header emptied itself. Three decisions, each removing one control, none adding any: D1 said Apply and Uninstall must not be twin buttons; D3 moved the enable toggle into the sidebar row; D4 deleted Apply outright, because installing now is applying. What is left is identity, and one irreversible button a level deeper — which is Safari's shape exactly.
Nothing here wears .is-new. Four decisions in and the count is still zero:
every step has been subtraction or relocation of something already shipped.
The space this frees is the point. An emptied header is room for the description —
and author_bio and author_links, which today exist only in the pre-install
preview and vanish the moment you adopt a codebook (I4), now have somewhere to live that
does not disappear.
Not shown, deliberately: where the install cost is stated. D4 moves register B1's
missing {{count}} onto the install control, which is now the spending one. That
control lives on the browse surface, which is still undecided.
Same place, same size, now live. The dot was already first-line aligned in the right column — it just was not operable. Promoting it beats deleting it and adding a control beside it: one element, one meaning, and the row does not grow horizontally.
The mini toggle is the first thing to wear .is-new, deliberately — and it is
the mildest kind, a size modifier on the shipped .sw. 26×15 preserves the
38×22 track ratio exactly (1.73). Marking it keeps the counter honest; excusing it would
make the counter useless.
The width was already 280px. This mockup previously drew 230 and was flattering the
problem — the right frame is now the shipped --bn-sidebar-width. The v2 rail is
320, +40, which is what the byline and the toggle need before titles start wrapping.
The author earns its place twice. It is how a researcher recognises a framework — "Norman" lands faster than "The Design of Everyday Things" — and it puts provenance in the list rather than only on the page, which is the same information loss I4 describes, one surface up. The toggle stays aligned to the title line, not centred on the block: the bullet rule the shipped dot already follows.
Full-size .sw, unmodified. The page gets the shipped 38×22 macOS-matched
switch; only the rail needed a smaller variant. The new thing is the mini; the canonical
control stays untouched.
Two entry points, one state. Both read and write the same disabledFrameworks
value, so they cannot disagree. Chrome does exactly this — toggle on the card and on the
details page; Safari does not repeat it. Repeating is right here, because the page is where
"am I working with this?" gets decided, and returning to the rail to act on the answer
would be a wasted trip.
Toggle before button, in that order. The header reads reversible-then-irreversible left to right, matching the cost gradient in D5.
Render if present, omit if not — no "unversioned" label, no placeholder, no dash. Two
of the nine YAMLs carry version: "1.0" (cli-ux, sentiment);
the other seven have none and show none.
It needs one field, not a schema change. The YAML already parses it —
TemplateOut simply does not declare it, so it is dropped at the model boundary.
Adding version: str | None surfaces what already exists. That is the smallest
possible step outside presentation-only, and worth taking.
Install, not Get — and this is settled, not preference. glossary.csv registers
Install across 20 locales, noted as "Apple's imperative, measured", replacing
Import codebook on 26 Jul 2026. Choosing Get would reopen a measured 20-locale
decision.
And there is a reason beyond inertia. The stores say Get because getting is
free — the Mac App Store uses it for free apps and shows a price otherwise; Edge Add-ons
uses it; Chrome says Add to Chrome; Claude's directory uses a bare +.
Under D4 installing spends tokens, so borrowing Get would import an idiom that
specifically signals this costs nothing. Claude's bare + is the most
cost-silent of the four and is the worst fit for the same reason.
So the card states the cost. That is register B1 — the missing
{{count}} — landing where D4 moved it. Shown here as a quiet figure beside the
button rather than inside the label, so the button stays one word in 21 languages.
Version and publisher are drawn but marked .is-new, because they do not exist.
version is present in exactly two of the nine YAMLs (cli-ux,
sentiment, both "1.0") and TemplateOut has no version
field at all — it is dropped at the model boundary and rendered nowhere. Publisher does
not exist in any form. Both are forward slots for the far-future community library, and
wiring either is a schema change, which is outside this scope.
The installed card carries the toggle, not just the button. Same mini control as the rail, so enable is reachable from the catalogue without a trip to the codebook page — and the card shows both axes exactly as D8 assigns them: toggle for enable, button for install.
Every width is a shipped one. Not invented for this drawing: 200px and 240px are the two
auto-fill minimums already in codebook-panel.css, 416px is the analysis
card's 26rem, and 832px is --bn-max-width (52rem).
832px is "full width" on a 14-inch Mac. 1512pt of screen, less the ~280pt project sidebar and the 320pt codebook rail, leaves ~912 — so the content cap binds before the display does. Drawing anything wider would be drawing a case that cannot occur.
What drops as it narrows. Description goes below 300; author goes below 240; at 200 only the title and the control survive. That is the trim order, and it says the minimum viable card is title + Install — which is what you proposed, now with a width attached to it.
On the visual. Book jackets are not hypothetical — Assets.xcassets already
ships welcome-book-nielsen, welcome-book-norman,
welcome-book-braun-clarke and welcome-book-lazarus for the Welcome
screen. So a jacket for the frameworks with a book behind them is inherited, not commissioned.
The blocks here are stand-ins, not artwork.
Corrected 29 Aug — where the jackets actually live. They are on the Welcome
shelf (WelcomeIllustrations.swift), not on the codebook details page, which
renders no image of any kind. The shelf is richer than a jacket: each book carries a
spine colour as well as artwork — Norman #334155, Nielsen
#0f5c9e, Braun & Clarke #7c3aed, Lazarus #b45309.
The overlap with our codebooks is two, not four. Norman and Nielsen are shipped codebooks; Braun & Clarke (thematic analysis) and Lazarus (appraisal theory) are method credits, not codebooks. So of nine codebooks, two have a jacket and seven do not — and a grid where two cards have artwork and seven have a gap reads as broken, not varied.
The spine colour is the way out. It is a per-book brand token that exists independently of the artwork, so it can dress a card that has no jacket — a coloured spine edge or a tinted initial for the seven, the real jacket for the two, on the full page where there is room to earn it. That inherits an existing decision rather than commissioning seven covers. Still needs a call; the ladder shows both so the gap is visible at each width.
The slot is the durable decision; the fill is not. The earlier pass committed to an assigned colour per codebook — a whole generated palette — which was a commitment to one answer when what the grid actually needs is a consistent footprint. Reserving the space fixes the rhythm; what goes in it can change later without redrawing anything.
Every fill stays open. A codebook may have a book, a website, an author with a face, or none of those. Jacket, initials, headshot, neutral monogram — all fit the same reservation, and which one applies can be per-codebook rather than systematic.
One thing cannot be deferred: the aspect. A jacket is 2:3 portrait, a headshot or an initials disc is 1:1 — they do not share a box. A 2:3 slot leaves a circle floating in dead space; a 1:1 slot shrinks the jacket, and jackets are the richest asset we have. Picking the ratio now costs nothing and keeps every fill viable; discovering it later means redrawing both surfaces.
The safe default needs no decision. Neutral surface, monogram in
--bn-colour-badge-bg — existing tokens, no new colour, no artwork. It ships as-is
and gets replaced fill by fill as assets arrive.
Dropped: the chevrons are a complexity that is not earning its keep. They existed only for the rail-closed case, which is Focus Mode — a reading mode, not a navigating one. If you want to move to another codebook, reopening the rail is one keystroke and shows you where you are going. Arrow-key traversal stays, because it costs no chrome. The frames below are kept as the record of what was considered.
Taken from the Welcome slot rotator (WelcomeHomeView.swift:707): the frosted
disk, hairline, soft shadow, hover-reveal — and critically inert-until-revealed.
That file uses allowsHitTesting(revealed) so an invisible edge column never
shadows the content beneath; here it is pointer-events:none while hidden.
Forgetting it would reintroduce a bug already designed out once.
Left behind: the page dots and the cross-fade. Dots say "small unordered set", and next/prev only exists when the rail is closed — so dots would re-implement the rail, badly, in the one situation the rail is absent. The title is the better position cue. The in-place cross-fade suits a slot and not a whole codebook page, where it would read as a slideshow rather than navigation.
Two things did not survive the port. .regularMaterial has no true CSS
equivalent — backdrop-filter: blur() approximates it without vibrancy, guarded
here by prefers-reduced-transparency. And SF Symbols are unavailable to CSS,
so the glyph is a Lucide chevron. Acceptable: the rule is don't fake a symbol at the
seam, and this lens is fully web content.
The open choice, drawn both ways. Hover-revealed is quieter and matches the house
precedent exactly; always-present never leaves a keyboard or touch user hunting for an
affordance that only appears on hover. Both are reachable by :focus-visible.
The regression, precisely. D4 fires the job on install and hands progress to the activity chip — but a chip is transient. Dismiss it, come back tomorrow, and v1's persistent "View Report · 12" is gone with nothing in its place. Pending proposals become unreachable. That is function lost, not chrome lost.
Two homes, because they answer different questions. The rail badge answers "where is there work?" — scannable across every installed codebook at once, without navigating. The page button answers "take me to it." Mail's split exactly: unread counts in the sidebar, the door in the content pane.
It does not reintroduce D1's problem. D1 objected to a routine and a
destructive control being the same shape, weight and colour. Review is accent-filled,
Uninstall is neutral — the shipped .apply-btn / .add-btn pair.
Primary beside secondary reads correctly; twin neutrals did not.
Nothing invented. The pill is .proposed-count, already shipped. But it
carries a defect worth fixing in the same pass: the real rule hardcodes
rgba(37,99,235,.08) — light-mode accent. Dark mode uses #0a84ff and
palette-edo uses #0f5c9e, so the tint is wrong on two of the four theme
combinations. color-mix() against the token fixes it, exactly as island-doc
decision 6 did for .merge-target.
Deliberately not decided here: whether Review opens a modal or a route. The door works either way, and the last-modal-standing question stays open.
The first version of this frame was wrong. It showed installed cards with a green check and no control, reading Claude's directory too literally. Claude can do that because its directory has no uninstall anywhere on the card — removal lives on a different surface entirely. Ours has to be reachable, so an installed card shows Uninstall.
What survives from that pass, and it was the actual complaint: no enable toggle on the card or the page. The toggle lives in the rail, once. That is untouched.
So the card carries one button that swaps verb — Install out, Uninstall in, on the
shipped .picker-card-toggle with its min-width so the control column
never shifts. That is the "one footprint" rule the Library doc already settled, and V10 had
it right while this frame did not.
Future enhancement, noted not built: a transition on the button as Install becomes Uninstall — the moment the card changes state is the one moment worth animating here, because it is the only feedback that the (paid) install actually took. Deferred.
Open, and it needs a call: if the card carries Uninstall, does the page still? Having it in both places undercuts D1's gradient (irreversible act a level deeper). The cleaner split is card owns lifecycle, page owns content — which would leave Review as the page's only button. Not decided here.
To spec: title, author, version (rendered only where present — Garrett and Nielsen
have none), a 9–18 word description, a status line for installed codebooks, no enable
toggle, no "New" badge, and grey/low-contrast when disabled. The whole card navigates to the
full page; only the Install/Uninstall button does not — it sits at
z-index:1 above the card's own click target, which is how the shipped tile
already keeps its stopPropagation honest.
One button that swaps verb, one footprint. Install when out, Uninstall when in —
the shipped .picker-card-toggle with its min-width, so the control
column never shifts between states. That also satisfies the Claude precedent: an installed
codebook never shows an add affordance.
Gap 1 — the descriptions do not fit, and truncation will not save them. All nine shipped descriptions run 44–75 words (Yablonski 75, Plato 72, Nielsen 61) against a 9–18 word budget. First-sentence-only lands in budget for just 5 of 9: sentiment (7w) and uxr (8w) fall short, cli-ux (19w) and Plato (25w) overrun. So the card needs a dedicated short field — nine hand-written lines. First sentences make a decent starting draft for five of them, not a rule.
Gap 2 — "on 34 quotes" cannot be computed today. total_quotes is
len(seen_quotes) — distinct within a group. But
summariseFramework sums those across groups, so a quote carrying tags from
two groups of the same framework is counted twice. The framework-level distinct count the
card needs is not exposed by the API. The shipped foldedSummary ("N tags ·
M coded") has the same flaw, so this is an existing overstatement, not a new one.
This dissolves a problem I raised wrongly. I argued the aspect ratio could not be deferred — that a 2:3 slot would leave a circle floating in dead space, and a 1:1 slot would shrink the jacket. Both follow from assuming a fixed box. Fix the width and let the height be free, and the conflict disappears: a circle is 44×44, a jacket is 44×66, nothing is 44×0, and the text starts at the same x in all three.
The blue rule is the point. It marks the shared left edge of the text column, held across all three fills. That alignment is what makes a grid read as coherent — not whether every card's graphic is the same shape. A commentary device, not product chrome.
Top-aligned, so nothing reflows. The graphic hangs from the top of the row; the title sits on the same baseline whether there is a jacket, a disc, or nothing beside it.
And "nothing" is a real option, not a fallback. Reserving the width even when there is no graphic is what keeps a bookless codebook from looking broken next to one with a jacket. No colour has to be invented, and no artwork commissioned, for the grid to hold together — which is what the earlier assigned-palette attempt was reaching for and overshot.
Everything here is real. The description, the bio, and all four links are the shipped
nielsen.yaml — including the two Amazon links, which is the "purchase links"
half of the rich page. The group names and layout are the shipped
.preview-* classes, lifted verbatim: two columns, tag groups left, author card
right.
And it no longer disappears on install. That is I4 closing in one frame: today this
layout exists only in the pre-install preview, so author_bio and
author_links vanish the moment you adopt a codebook. One page, reached from the
rail or from a browse card, differing only in state and controls.
Uninstall on the page as well as the card — confirmed. Chrome and Edge both do this (Remove on the card and on the details page). It costs D1's strict "irreversible act one level deeper" gradient, but that gradient was always about not putting Uninstall beside a routine button in the always-visible rail — and it is not in the rail. Review is accent, Uninstall is neutral; primary beside secondary still reads correctly.
No enable toggle, per D11 — the rail owns it. The page shows enabled state by not being greyed.
The gutter scales, the rule holds. 72px wide here against 44px on the card; the jacket is 108px tall because 2:3 is what a jacket is, and the text still starts at a fixed x. Same D13 rule at a different size, no second decision needed.
Three controls, permanently present. Apply, Uninstall and the switch share one row. The Library spec had Apply morph into the switch once spent — a one-way handoff — and had the Uninstall button leave the lens entirely for the Library tile. Neither happened, so the row carries all three lifecycles at once: run, install, enable.
Apply carries no number. The spec string was
Apply to {{count}} quotes. Shipped is bareApply— the one control that spends money does not say what it will cost.