feat(web): the title bar is painted, a click survives an editor, and the twin door is gone
improvise-5jna, improvise-0x9u, improvise-seik — three browser-page defects with one shape: a thing that looks done from every check except running it. improvise-5jna. `doc_name`/`doc_dirty`/`doc_stored_as` were projected by z754 and painted by nothing. `render_surface.test.js` could not see it — it is a SUBSTRING SEARCH over the page's source, so a field named by a function nothing calls reads as painted, which is also how `model.minibuffer` shipped dead. So the guard is a harness assertion that loads the page and reads `#doc-name`, proved by writing it first (`element(s) not found`). The joining is `shell.js::documentTitle`, the TUI's own `name (stored_as) [+]` minus the chrome; the `[+]` is a node test, not a harness one, because this deployment persists on every accepted batch and clears the flag in the same round trip, so the marker is transient. UNPAINTED is now empty. improvise-0x9u. From an EDITING state a cell click did nothing at all: the mousedown moved focus, the input's blur ran `close`, `close` repainted, `replaceChildren` destroyed the `<td>` the mousedown landed on, and no `click` was ever dispatched. The old spec pressed Escape between the two clicks, so it only ever clicked from a non-editing page — this repo's characteristic defect. Worth recording: moving to `mousedown` is NECESSARY AND NOT SUFFICIENT, and the first attempt failed the same assertion with the same symptom. Removing a focused `<input>` fires `blur` SYNCHRONOUSLY, so `close` rebuilt the table from inside itself and detached the `<td>` again. `close` therefore no longer repaints, the `blur` arm checks `closed` first, and the handler `preventDefault()`s so no focus change happens at all — otherwise the default action focuses `#grid` after `input.focus()` and blurs the new editor. `close` restores the DRAWN text, since nothing repaints behind it any more. improvise-seik. `Client::viewport` was a wasm-bindgen door with no caller, kept for a reason `web/app.js` falsified in the same wave. It is one letter from `set_viewport` and fails SILENTLY — identical frame, forgotten window, every cursor key an upstream black hole. Deleted, along with the exported `init()` free function that duplicated `new Client()`. The guard is not the deletion: `wasm_surface.test.js` now reads the generated `.d.ts` and requires every declared method to be one `app.js` calls or to be named in `UNCALLED` with its reason. Proved by injecting a spare `static viewport(...)` into the built `.d.ts` and watching it fail by name. Tests: 1405 Rust (unchanged), 27 node + 4 bundle (was 26 + 3), 24 chromium harness (was 23). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
+35
-8
@@ -7,6 +7,7 @@ import {
|
||||
HANDSHAKE_TIMEOUT_MS,
|
||||
ROW_PX,
|
||||
createShell,
|
||||
documentTitle,
|
||||
extentOf,
|
||||
frameCostOf,
|
||||
gridPlan,
|
||||
@@ -35,6 +36,7 @@ const el = {
|
||||
help: document.getElementById("help"),
|
||||
status: document.getElementById("status"),
|
||||
viewName: document.getElementById("view-name"),
|
||||
docName: document.getElementById("doc-name"),
|
||||
};
|
||||
|
||||
await init();
|
||||
@@ -60,8 +62,8 @@ function sendViewport() {
|
||||
const box = el.grid.getBoundingClientRect();
|
||||
const metrics = { rowPx: ROW_PX, colPx: COL_PX };
|
||||
const { rows, cols } = extentOf(box, frameCost, metrics);
|
||||
// `client.set_viewport`, NOT the associated `Client.viewport`: the same
|
||||
// frame, but it also RECORDS the window. Why that matters: clients.md.
|
||||
// `set_viewport` RECORDS the window as well as framing it; the frame-only
|
||||
// twin beside it is deleted, so there is nothing to call by mistake (seik).
|
||||
transport.send(client.set_viewport(rows, cols));
|
||||
}
|
||||
|
||||
@@ -162,6 +164,13 @@ function repaint() {
|
||||
paintMinibuffer(model);
|
||||
shell.setModel(model.status);
|
||||
el.viewName.textContent = model.view_name;
|
||||
// The title bar, off the cache `src/draw.rs::draw_title` folds — one
|
||||
// derivation, both front ends (improvise-z754, improvise-5jna).
|
||||
el.docName.textContent = documentTitle(
|
||||
model.doc_name,
|
||||
model.doc_dirty,
|
||||
model.doc_stored_as,
|
||||
);
|
||||
el.grid.replaceChildren(gridTable(model));
|
||||
// The flag is the client's answer, not the page's: `C` toggles it and the
|
||||
// TUI draws the panel on exactly the same condition (improvise-wsaw).
|
||||
@@ -299,7 +308,12 @@ function gridTable(model) {
|
||||
// Every cell moves the selection; an editable one also opens its editor,
|
||||
// which is the pointer's `i`. Not repainted in that branch: `edit` has
|
||||
// set `editing`, and a repaint would take the <input> away.
|
||||
td.addEventListener("click", () => {
|
||||
// On MOUSEDOWN, and defaulted away: from an editing state the input's
|
||||
// blur repainted first and this <td> was gone before `click` could fire
|
||||
// (improvise-0x9u). See context/code/clients.md.
|
||||
td.addEventListener("mousedown", (event) => {
|
||||
event.preventDefault();
|
||||
if (editing) editing.close(false);
|
||||
const frame = client.click_cell(cell.row, cell.col);
|
||||
if (frame) transport.send(frame);
|
||||
if (cell.editable) edit(td, cell.row, cell.col, cell.editText);
|
||||
@@ -314,10 +328,12 @@ function gridTable(model) {
|
||||
* back is the server's, never the string typed here. */
|
||||
function edit(td, row, col, seed) {
|
||||
if (editing) return;
|
||||
editing = { row, col };
|
||||
// `seed` is `RenderCell::edit_text` -- the STORED value, not the drawn one:
|
||||
// `td.textContent` has been through the number format.
|
||||
const original = seed;
|
||||
// What was on the screen, kept so closing can put it back: `close` no longer
|
||||
// always repaints, so restoring `seed` would leave `1200` where `1,200` was.
|
||||
const drawn = td.textContent;
|
||||
const input = document.createElement("input");
|
||||
input.value = original;
|
||||
td.replaceChildren(input);
|
||||
@@ -327,6 +343,8 @@ function edit(td, row, col, seed) {
|
||||
// Enter removes the focused <input>, which fires `blur`, which calls
|
||||
// `close` again. Harmless before, but only by accident.
|
||||
let closed = false;
|
||||
// `close` does NOT repaint: a caller about to open another editor on a live
|
||||
// <td> must not have the table rebuilt under it (improvise-0x9u).
|
||||
const close = (commit) => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
@@ -337,19 +355,28 @@ function edit(td, row, col, seed) {
|
||||
// follows restores whatever the server now says is there.
|
||||
if (frame) transport.send(frame);
|
||||
}
|
||||
td.replaceChildren(original);
|
||||
td.replaceChildren(drawn);
|
||||
painted = -1;
|
||||
};
|
||||
const dismiss = (commit) => {
|
||||
close(commit);
|
||||
repaint();
|
||||
};
|
||||
editing = { row, col, close };
|
||||
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") close(true);
|
||||
else if (e.key === "Escape") close(false);
|
||||
if (e.key === "Enter") dismiss(true);
|
||||
else if (e.key === "Escape") dismiss(false);
|
||||
// The editor owns the keyboard while it is open; the page-level handler
|
||||
// below must not also see these.
|
||||
e.stopPropagation();
|
||||
});
|
||||
input.addEventListener("blur", () => close(false));
|
||||
// Removing the focused <input> fires `blur` SYNCHRONOUSLY, so a caller that
|
||||
// already closed this editor must not have the table rebuilt from inside its
|
||||
// own `close` — that is what detached the <td> in improvise-0x9u's repair.
|
||||
input.addEventListener("blur", () => {
|
||||
if (!closed) dismiss(false);
|
||||
});
|
||||
}
|
||||
|
||||
/** A side panel. An empty row list is what says it is closed — the page asks
|
||||
|
||||
Reference in New Issue
Block a user