feat(web): dismissable notice, a raised minibuffer, and a styled shell

Three things the user reported after looking at the deployed page, in the
order they cost a visitor something.

The deployment notice was ten dense lines of configuration reference —
?doc=, ?seed=, ?ws=, wss:// and reverse proxies — above the tile bar, the
category tree and the grid on every visit: about 230px of an 820px window.
It is now a <details>, so the collapsing is the BROWSER's (keyboard
operable, and correct with app.js absent or still loading) and the only
thing the page adds is remembering the answer. It starts COLLAPSED, and
collapsing withholds nothing: the one fact a first visitor needs — the
document lives in this tab, is saved to this browser's private storage and
never leaves the machine — is the <summary>, which never collapses.

What a stored value means is a decision and lives in shell.js; touching
localStorage is a browser API and stays in app.js, wrapped, with every
value that is not one of the two spellings read as "unanswered".

The minibuffer was a 14px line in the page's bottom-left corner, up to
800px from the cell being typed into. It is now a raised bar over the
grid. The CONTENT is untouched — RenderMinibuffer is built from
`state.view.mode.minibuffer()`, the same semantic value the TUI draws — so
this is position, weight and contrast, and there is no second derivation of
what a prompt says. MinibufColor still resolves here exactly as
draw.rs::minibuf_fg resolves it for the terminal.

style.css stops being "legibility only": layered surfaces, one accent, a
filled badge for the mode, chips for the two axis strips, one selection
treatment, tabular numerals in the grid, and a focus ring on the one
keyboard surface the page has. The palette is the landing page's, so the
two read as one product.

Asserted (harness/tests/contract.spec.js, Chromium): the notice starts
collapsed and remembers, in BOTH directions across a reload; it opens from
the keyboard, which the page's own keydown handler must not claim; the
minibuffer is inside the viewport, OVERLAPS #grid — the assertion a static
bottom bar cannot satisfy — and is larger than body text. Each was proved
discriminating by injecting the fault and watching it fail.

NOT asserted, and named as such in context/code/clients.md: colour,
contrast, spacing, weight, the grouped and multi-level header DOM, either
theme, and the localStorage-throws path. Those are argued and read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-21 22:29:18 -07:00
parent f34334cfca
commit 8ad5c0806d
8 changed files with 822 additions and 146 deletions
+24
View File
@@ -28,6 +28,30 @@ export function indicator(collapsed) {
return collapsed ? "▶" : "▼";
}
/** Where the deployment notice's open/closed state is remembered. Per-viewer
* and per-origin by construction: it never reaches Rust and never travels. */
export const NOTICE_KEY = "improvise.notice.open";
/** Whether a visitor who has never answered sees the notice OPEN. It is a
* product call and the argument for `false` is in context/code/clients.md. */
export const NOTICE_OPEN_BY_DEFAULT = false;
/** What a stored notice state MEANS. Everything that is not one of the two
* spellings — absent, empty, a value from an older page, a string a private
* window handed back — is "unanswered", which is the default and not an
* error: storage is allowed to forget, and a page that treated forgetting as
* a third state would flicker between them. */
export function noticeOpen(stored) {
if (stored === "open") return true;
if (stored === "closed") return false;
return NOTICE_OPEN_BY_DEFAULT;
}
/** …and the inverse, so the two spellings have exactly one home. */
export function noticeState(open) {
return open ? "open" : "closed";
}
// Rough character metrics, used only to turn the window size into an extent.
// Being wrong here costs a row of over- or under-fetch, nothing more.
export const ROW_PX = 26;