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:
@@ -4,7 +4,15 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { cellAction, cellAttrs, gridPlan, indexAction, pageEntries } from "../shell.js";
|
||||
import {
|
||||
cellAction,
|
||||
cellAttrs,
|
||||
gridPlan,
|
||||
indexAction,
|
||||
menuKey,
|
||||
pageEntries,
|
||||
pageItemAction,
|
||||
} from "../shell.js";
|
||||
|
||||
const cell = (text, extra = {}) => ({
|
||||
text,
|
||||
@@ -85,7 +93,13 @@ test("an index surface reads its row back, and nothing else", () => {
|
||||
// moved it in silence. The bar is what says which quarter this is.
|
||||
test("the page bar names the category and the item it is fixed to", () => {
|
||||
const [entry] = pageEntries({
|
||||
page_coords: [{ cat: "Quarter", selected: "Q2", cycle: { at: 1, count: 4 } }],
|
||||
page_coords: [
|
||||
{
|
||||
cat: "Quarter",
|
||||
selected: "Q2",
|
||||
cycle: { at: 1, count: 4, items: ["Q1", "Q2", "Q3", "Q4"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(entry.text, "Quarter = Q2");
|
||||
assert.equal(entry.position, "2/4", "one-based, as a user counts");
|
||||
@@ -106,3 +120,75 @@ test("a filter axis is shown, and is not offered as something to cycle", () => {
|
||||
test("no fixed categories is an empty bar, not a blank one", () => {
|
||||
assert.deepEqual(pageEntries({ page_coords: [] }), []);
|
||||
});
|
||||
|
||||
// improvise-rw6l: the arrow and the menu follow `cycle`, which is the Page /
|
||||
// Filter split decided in Rust — never "is there an entry on the bar".
|
||||
test("a page axis offers its items, and a filter axis offers none", () => {
|
||||
const [page, filter] = pageEntries({
|
||||
page_coords: [
|
||||
{
|
||||
cat: "Quarter",
|
||||
selected: "Q3",
|
||||
cycle: { at: 2, count: 3, items: ["Q1", "Q2", "Q3"] },
|
||||
},
|
||||
{ cat: "Region", selected: "North", cycle: null },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(page.items, [
|
||||
{ item: 0, label: "Q1", selected: false },
|
||||
{ item: 1, label: "Q2", selected: false },
|
||||
{ item: 2, label: "Q3", selected: true },
|
||||
]);
|
||||
assert.equal(page.index, 0, "the axis names itself by its place on the bar");
|
||||
assert.deepEqual(
|
||||
filter.items,
|
||||
[],
|
||||
"a Filter axis constrains without being navigable: nothing to pick",
|
||||
);
|
||||
assert.equal(filter.index, 1);
|
||||
});
|
||||
|
||||
// One meaning across two surfaces (improvise-b8ot): a chip carries an axis, a
|
||||
// menu row carries an axis AND an item. Nothing infers from structure.
|
||||
test("a chip opens the menu and a menu row picks, one attribute apart", () => {
|
||||
assert.deepEqual(pageItemAction({ axis: "1" }), { kind: "open", axis: 1 });
|
||||
assert.deepEqual(pageItemAction({ axis: "1", item: "0" }), {
|
||||
kind: "pick",
|
||||
axis: 1,
|
||||
item: 0,
|
||||
});
|
||||
assert.equal(pageItemAction({}), null, "the gaps between the chips are not a chip");
|
||||
assert.equal(pageItemAction({ axis: "" }), null);
|
||||
assert.equal(pageItemAction({ axis: "-1" }), null);
|
||||
assert.deepEqual(
|
||||
pageItemAction({ axis: "0", item: "" }),
|
||||
{ kind: "open", axis: 0 },
|
||||
"an empty item attribute is NO item, where Number('') would say 0",
|
||||
);
|
||||
});
|
||||
|
||||
// The menu owns the keyboard while it is open, so every key is answered and
|
||||
// nothing falls through to the grid it is anchored to.
|
||||
test("the menu answers its own keys and swallows the rest", () => {
|
||||
const at = (active) => ({ count: 3, active });
|
||||
assert.deepEqual(menuKey("ArrowDown", at(0)), { kind: "move", item: 1 });
|
||||
assert.deepEqual(menuKey("j", at(2)), { kind: "move", item: 0 }, "and it wraps");
|
||||
assert.deepEqual(menuKey("ArrowUp", at(0)), { kind: "move", item: 2 }, "both ways");
|
||||
assert.deepEqual(menuKey("k", at(1)), { kind: "move", item: 0 });
|
||||
assert.deepEqual(menuKey("Home", at(2)), { kind: "move", item: 0 });
|
||||
assert.deepEqual(menuKey("End", at(0)), { kind: "move", item: 2 });
|
||||
assert.deepEqual(menuKey("Enter", at(1)), { kind: "pick", item: 1 });
|
||||
assert.deepEqual(menuKey(" ", at(1)), { kind: "pick", item: 1 });
|
||||
assert.deepEqual(menuKey("Escape", at(1)), { kind: "close" });
|
||||
assert.deepEqual(menuKey("Tab", at(1)), { kind: "close" });
|
||||
assert.deepEqual(
|
||||
menuKey("h", at(1)),
|
||||
{ kind: "ignore" },
|
||||
"a grid key is SWALLOWED, not passed on — the grid is behind the menu",
|
||||
);
|
||||
assert.deepEqual(
|
||||
menuKey("ArrowDown", { count: 0, active: 0 }),
|
||||
{ kind: "close" },
|
||||
"a menu with nothing in it cannot be moved around",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user