feat(browser): paint the page bar and the totals strip, and delegate the pointer
improvise-p7lh: `GridCache::page_coords` and `GridCache::totals` were projected to every session and folded correctly by every client, and NO front end read either. The shipped front page — `web/demo.improv` is `Quarter: page, Q1` — drew a grid filtered to Q1 with nothing saying so, and `[`/`]` resolve locally off that same `page_coords`, so a user could page through quarters watching every number change with no way to tell which one they were looking at. `RenderModel` gains `page_coords` and `totals`, fed straight off `state.grid`. The Page/Filter split is asked of `PageAxis::cycle` rather than re-derived from `items.is_some()`, which disagrees with it on an empty item list (gotcha 27). The totals are RELAYED, never summed in the client: the sum runs over every row including ones a viewport scrolled past (gotcha 44). `web/tests/render_surface.test.js` gains the direction that would have caught this when improvise-n10 added the fields. The existing guard is closed under what `RenderModel` chose to expose, so a cache field that never reaches it is invisible to it; the new one asks that every `pub` field of every widget cache is read somewhere the browser runs, or is named as debt with a reason. `grid.col_widths` is the one entry, pointing at improvise-pcje. improvise-b8ot: the five clickable surfaces moved from one listener per node, reallocated inside the render loops on every repaint, to one handler per stable delegation root — the user's ruling. The operands are carried on the node as data attributes and read back by a pure `cellAction`/`indexAction` in `web/shell.js`, so the handler is a LOOKUP rather than a second derivation of which data row was clicked: `gridPlan` counts data rows because a group header is not one, and walking the table back up would disagree the moment one appears. The editor's own keydown/blur stay direct — one transient node, one at a time, and `blur` does not bubble. All four documented behaviours a naive delegation loses are preserved and each is now asserted in `harness/tests/contract.spec.js`: mousedown rather than click (asserted by dispatching mousedown ALONE), `preventDefault` and only over a cell, the conditionally skipped repaint (asserted by the editor's survival), and closing a live editor before composing the click. Gates: 1598 passed / 0 failed / 7 ignored over 37 binaries (baseline 1595); node --test 47 over 6 files (baseline 36 over 5); Playwright 47 passed / 4 skipped (baseline 43 / 4); clippy, fmt and --check-evidence clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
@@ -23,6 +23,7 @@ function grouped() {
|
||||
row_levels: 2,
|
||||
col_levels: [["Q1", ""], ["Revenue", "Cost"]],
|
||||
col_groups: [{ span: 2, label: "2026", collapsed: false }],
|
||||
totals: [],
|
||||
editing: null,
|
||||
rows: [
|
||||
{ kind: "group", label: "Hot", collapsed: false },
|
||||
@@ -108,3 +109,29 @@ test("the editor is seeded from the stored value, not the drawn one", () => {
|
||||
assert.equal(plan.body[1].cells[0].text, "20,000");
|
||||
assert.equal(plan.body[1].cells[0].editText, "20000");
|
||||
});
|
||||
|
||||
test("an empty totals strip is no strip at all, not a row of blanks", () => {
|
||||
// Gotcha 33: an EMPTY strip means this grid has no totals — records mode,
|
||||
// or an empty grid — so there is nothing to draw and nothing to pad.
|
||||
assert.equal(gridPlan(grouped()).totals, null);
|
||||
});
|
||||
|
||||
test("the totals strip is relayed, and its label spans the row headers", () => {
|
||||
const model = grouped();
|
||||
model.totals = [
|
||||
{ text: "9,001", numeric: true, error: false },
|
||||
{ text: "err", numeric: false, error: true },
|
||||
];
|
||||
const plan = gridPlan(model);
|
||||
assert.equal(plan.totals.span, 2, "the label spans every row-header level");
|
||||
assert.equal(plan.totals.label, "Total");
|
||||
assert.deepEqual(
|
||||
plan.totals.cells.map((c) => [c.text, c.classes.numeric, c.classes.error]),
|
||||
[
|
||||
["9,001", true, false],
|
||||
["err", false, true],
|
||||
],
|
||||
);
|
||||
// And it carries no coordinates: a total is not a cell a click can name.
|
||||
assert.ok(!("row" in plan.totals.cells[0]));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user