╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │476 if (entry.ti.options.name.len > 0) { │ │477 try out.appendSlice(allocator, "\" name=\""); │ │478 try appendEscapedHtml(allocator, &out, entry.ti.options.name); │ │446 - } │ │447 - // mark the wasm-focused element so JS can mirror it onto DOM focus │ │448 - // — otherwise moving focus via arrow keys leaves the TUI border │ │449 - // showing focus but the underlying <input> unfocused. │ │450 - if (entry.focus_id == root_focus.grandchild_id) { │ │451 - try out.appendSlice(allocator, "\" data-focused=\"true"); │ │452 - } │ │453 - try out.appendSlice(allocator, "\" value=\""); │ │454 - var value_buf: std.ArrayList(u8) = .empty; │ │455 - defer value_buf.deinit(allocator); │ │456 - for (entry.ti.content.items) |cp| { │ │457 - try value_buf.appendSlice(allocator, cp); │ │479 } │ │459 - try appendEscapedHtml(allocator, &out, value_buf.items); │ │480 + // intentionally no `value` attribute: the browser tracks user input │ │481 + // natively, and including the wasm-side value here would make the │ │482 + // overlay HTML differ on every keystroke — that would trip the diff │ │483 + // in _setOverlay and rebuild the <input>, eating the user's caret. │ │484 try out.appendSlice(allocator, "\" style=\"left:"); │ │485 var pos_buf: [64]u8 = undefined; │ │486 try out.appendSlice(allocator, try std.fmt.bufPrint(&pos_buf, "{d}ch;top:{d}em;width:{d}ch;height:1em", .{ inner_left, inner_top, inner_width }));│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │487 try out.appendSlice(allocator, "\">"); │ │488 } │ │489 │ │466 - for (0..grid.size.height) |y| { │ │467 - // wrap runs of cells belonging to a focusable widget in a span tagged │ │468 - // with that widget's focus id. CSS paints them with a pointer cursor; │ │469 - // JS reads the id on click and dispatches focus directly. │ │470 - var current_id: ?usize = null; │ │471 - for (0..grid.size.width) |x| { │ │472 - const cell_id = cellFocusId(root_focus, x, y); │ │473 - if (cell_id != current_id) { │ │474 - if (current_id != null) try out.appendSlice(allocator, "</span>"); │ │475 - if (cell_id) |id| { │ │476 - const kind = if (root_focus.children.get(id)) |entry| entry.focus.kind else .container; │ │477 - const focused_attr: []const u8 = if (id == root_focus.grandchild_id) " data-focused=\"true\"" else ""; │ │478 - var buf: [256]u8 = undefined; │ │479 - // tabindex="0" on submit buttons puts them in the browser's │ │480 - // natural Tab order alongside the inputs above. │ │481 - const tag = if (kind == .submit_button) │ │482 - try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\" data-action=\"submit\" data-url=\"{s}\" tabindex=\"0\"{s}>", .{ id, submit_url, focused_attr })│ │483 - else │ │484 - try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\"{s}>", .{ id, focused_attr }); │ │485 - try out.appendSlice(allocator, tag); │ │486 - } │ │487 - current_id = cell_id; │ │488 - } │ │489 - const rune = grid.cells.items[try grid.cells.at(.{ y, x })].rune orelse " "; │ │490 - try appendEscapedHtml(allocator, &out, rune); │ │491 - } │ │492 - if (current_id != null) try out.appendSlice(allocator, "</span>"); │ │493 - try out.append(allocator, '\n'); │ │490 + if (submit) |btn| { │ │491 + // inside the dashed border the TUI cells draw around the button. │ │492 + const inner_left = btn.x + 1; │ │493 + const inner_top = btn.y + 1; │ │494 + const inner_width = if (btn.width > 2) btn.width - 2 else 0; │ │495 + const inner_height = if (btn.height > 2) btn.height - 2 else 0; │ │496 + try out.appendSlice(allocator, "<button type=\"submit\" data-focus-id=\""); │ │497 + var id_buf: [32]u8 = undefined; │ │498 + try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{btn.focus_id})); │ │499 + try out.appendSlice(allocator, "\" style=\"left:"); │ │500 + var pos_buf: [128]u8 = undefined; │ │501 + try out.appendSlice(allocator, try std.fmt.bufPrint(&pos_buf, "{d}ch;top:{d}em;width:{d}ch;height:{d}em", .{ inner_left, inner_top, inner_width, inner_height })); │ │502 + try out.appendSlice(allocator, "\"></button>"); │ │503 } │ │504 │ │505 + try out.appendSlice(allocator, "</form>"); │ │506 + │ │507 return try out.toOwnedSlice(allocator); │ │508 } │ │509 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯