feat: the page bar becomes a control, and [/] reach the modes about the document

Three tickets on one subject: paging.

improvise-rw6l — clicking a page chip opens a menu of that axis's items, with
a down-arrow saying so. Almost nothing had to be derived: `PageAxis::items` was
already projected and `ModelCommand::SetPageSelection` was already on the wire.
The one gap was that `RenderPageAxis` dropped the names, keeping the position
and count improvise-p1ym needed for `[`/`]`. `items` is relayed INSIDE `cycle`
rather than beside it, so there is one predicate and not two: a Filter axis has
no cycle, therefore no arrow, no menu, no items, and composes nothing if a click
reaches it — all four off `PageAxis::cycle`, the one home of gotcha 27's split.

improvise-771e — the browser tile bar said "Quarter Pag". `short_label` is three
characters so the TUI aligns in a fixed-width terminal (gotcha 26); a browser has
no column to fit. `render.rs` asks `plain_label` instead; `short_label` is
untouched. Note what pinned the truncation was `harness/tests/drill.spec.js`,
not `tests/grid_frame_agreement.rs`, which does not mention the tile bar.

improvise-wiew — `[`/`]` move from Normal onto a new `document_keymap` between
the global and the modes whose subject IS the document (Normal, the three
panels, TileSelect; RecordsNormal through Normal). Help and the import wizard
take the global directly. Per the user's ruling: an overlay has a subject of its
own, and these are the first shared keys that MUTATE the document — under the
help overlay that is an invisible change to the grid it is covering.

Boundary crossings to flag, both outside the brief's stated write scope and
neither a wire change:
  - `resolve.rs` gains `Click::PageItem` and `lib.rs` a `click_page_item`
    binding. Composing the command anywhere else would put a derivation in the
    page, which design-principles §5 forbids; `Command`/`ModelCommand` are
    untouched.
  - `improvise-command/src/command/keymap.rs` gains three tests. The two
    existing guards were the mechanism that forced the wiew argument, so they
    had to be answered where they live.

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 23:24:38 -07:00
parent ee0f57d227
commit a913390322
17 changed files with 1076 additions and 42 deletions
+63
View File
@@ -1487,6 +1487,69 @@ agreement, and it is what `no_mode_binds_a_panel_toggle_of_its_own` now keeps.
is the thing being removed, and two equal-but-separate globals would satisfy
any value comparison.
### The layer below it: `document_keymap` (improvise-wiew)
The global above is **all seventeen modes**, and that answer is unchanged. What
changed is that a second shared layer now sits between it and some of them.
The user asked for `[`/`]` "probably in the global keymap", and then answered
the question that left open himself: *"the help display (and import wizard,
etc.) shouldn't respond to these keys, but the 'normal' modes like
categories/tiles/formulas/etc. should."*
The distinction is **is the document the subject right now**, and it is genuinely
a new one rather than the global's argument applied again. Every key the global
binds — `?`, `:`, the three panel toggles — is a **UI** action: it opens a room,
it does not change the document. `page-next`/`page-prev` resolve to
`ModelCommand::SetPageSelection`, which is `EffectKind::Model(LayoutGeometry)`.
They are the first shared keys that *mutate*, and under the Help overlay that is
a mutation the user cannot see, because the overlay is covering the grid it
just moved.
So the chain is three deep:
```
global ? : and the panel toggles — every mode
└── document [ ] — Normal, the three
├── Normal panels, TileSelect; RecordsNormal
│ └── RecordsNormal through Normal
├── FormulaPanel / CategoryPanel / ViewPanel
└── TileSelect
├── Help global only
├── ImportWizard global only
└── the text-entry modes and SearchMode, unchanged
```
**The name is the guard.** Nothing in the code stops the next person parenting
Help to this layer; what stops them is that it is called `document_keymap` and
not `common` or `shared`, so its name answers "should my new mode inherit it?"
A name describing *who* inherits a layer invites everything to join, and the
distinction dies within two waves.
**RecordsNormal inherits it once, through Normal.** It already parents Normal,
so it reaches the layer transitively; parenting it directly as well would be a
second edge with nothing keeping the two in step. That is asserted on the chain
identity rather than on behaviour, because a duplicated edge answers `page-next`
correctly on the day it is added and only diverges later.
**The layer owes the same Char-only constraint as the global**, and for the
identical reason: it is reached by *fallthrough*, so a non-Char binding on it
would fire while a descendant's `AnyChar` was typing. No descendant binds
`AnyChar` today — which is exactly why the constraint is written down as a test
rather than left to the arrangement. An unconstrained shared layer is where the
next "just put it in the shared one" lands.
**Two things that cost nothing and were checked anyway.** Text entry is
untouched: the fallback order is `exact → Char(NONE) → AnyChar → Any → parent`,
so a text-entry mode's `AnyChar` answers `[` before any parent is consulted, and
typing a bracket into a category name still types it. And `prefix_keymaps_are_flat`
is about the *sub-keymaps a prefix opens*, not about inheritance depth, so a
third level of parents violates nothing it asserts.
The browser follows for free: `resolve.rs` dispatches on the command **name**,
so every mode that now reaches `page-next` composes it in a browser too, with no
second table to update. That is the whole point of one shared keymap.
### `TextEntry` — the spine, welded
Every text-entry mode has the same spine: characters append to one named