fix: the app bar is reserved, not fitted, and a message is a toast (improvise-n6t8)

The rule is the deliverable: nothing a user expects to be fixed may move.

Four children of <header> were sized by their own content and all four
changed on the SAME event, so the bar reflowed on a mode change with no
message at all: #mode (mode_label, 4-14 chars), #hint (hint_line, one
line per mode), #doc-name (gains and loses " [+]") and #view-name, which
had :empty { display: none } and so appeared out of nothing.

Every child that changes with a keystroke now has an allocated width, and
#hint is the single elastic child, so nothing is positioned by it. The
mode pill's reserve is a FLOOR and deliberately not a cap: a cap absorbs
an over-long label inside a box that never grows, which is a reserve that
hides its own failure. AppMode's exhaustive-match methods make the
compiler demand mode_label for a new variant; nothing checked the CSS
still fit it. harness/tests/header.spec.js is that check.

The spec found what the ticket did not predict: the compounding one was
not the pill. #status was flex: 0 1 auto beside a flex: 1 1 auto hint, so
a longer hint took the shrink out of #status, which WRAPPED to two and
three lines and grew the header by a row -- 52.5px to 71.0px. Reserving
the pill alone would have left that untouched.

ViewState::status_msg is now a toast in a position: fixed dock anchored at
its bottom, so it takes part in no layout and stacks ABOVE the minibuffer:
the prompt is neither covered nor displaced. composeStatus is gone -- the
two facts go to two elements, so a repaint reaches onModel and cannot
address onShell at all. That is gotcha 46's rule made structural rather
than kept by a join's convention; its unit test was replaced, not deleted.

Dismissal keys off message IDENTITY, not a timer. status_msg is STATE and
repaint() re-offers it on every revision, so a setTimeout alone resurrects
the toast on the next keypress. Cost stated rather than hidden: two
identical messages in a row show once, because nothing carries a sequence
and the alternative reading of an ambiguous offer is exactly the bug.

Nine of seventeen modes are reachable in a browser and the spec names them
individually; three injected faults are recorded in the evidence file.

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-22 00:17:22 -07:00
parent 7470b61476
commit d750b433b8
11 changed files with 698 additions and 70 deletions
+13 -4
View File
@@ -123,11 +123,20 @@
</main>
<!--
The bottom bar. The TUI draws the minibuffer INSTEAD of the status line;
here it is a line of its own, because #status also carries the transport's
own failures and a repaint must not erase one (gotcha 48).
The bottom dock (improvise-n6t8). It is `position: fixed`, so nothing
inside it takes part in the page's layout and a message cannot push the
header or the grid. It is anchored at its BOTTOM and stacks upward, so
the minibuffer sits where it always sat and a toast appears above it:
the prompt is never covered and never displaced. context/code/clients.md.
#toast keeps its text when it hides — the element is the record of the
last message shown, and an emptied one could not tell "nothing was said"
from "something else was said".
-->
<div id="minibuffer" role="status" hidden></div>
<div id="bottom-dock">
<div id="toast" role="status" aria-live="polite" hidden></div>
<div id="minibuffer" role="status" hidden></div>
</div>
<div id="help" role="dialog" aria-label="Help" hidden></div>