╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │197 window.addEventListener("resize", () => { │ │198 clearTimeout(resizeTimer); │ │199 resizeTimer = setTimeout(() => { │ │183 - wasmInstance.exports._tick(maxCols()); │ │200 + wasmInstance.exports._tick(minRows(), maxCols());│ │201 }, 100); │ │202 }); │ │203 }); │ ╰──────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │23 var page: ?ui.Page = null; │ │24 var session: ui.Session = .{}; │ │25 │ │26 -fn start(json: []const u8, max_width: u32) !void { │ │26 +fn start(json: []const u8, min_height: u32, max_width: u32) !void {│ │27 if (page_arena) |*a| a.deinit(); │ │28 page_arena = std.heap.ArenaAllocator.init(allocator); │ │29 │ ╰───────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │46 if (root) |*old_root| old_root.deinit(allocator); │ │47 root = next_root; │ │48 │ │49 - try tick(max_width); │ │49 + try tick(min_height, max_width); │ │50 } │ │51 │ │52 -fn tick(max_width: u32) !void { │ │52 +fn tick(min_height: u32, max_width: u32) !void { │ │53 const root_ptr = if (root) |*root_value| root_value else return error.NotStarted;│ │54 try root_ptr.build(allocator, .{ │ │55 - .min_size = .{ .width = null, .height = null }, │ │56 - // height is null so the TUI grows to fit all its content; the browser │ │57 - // page handles vertical scrolling. │ │55 + // min_height lets the TUI fill the viewport when its content is │ │56 + // short; max height stays null so taller content extends downward │ │57 + // and the browser scrolls. │ │58 + .min_size = .{ .width = null, .height = min_height }, │ │59 .max_size = .{ .width = max_width, .height = null }, │ │60 }, root_ptr.getFocus()); │ │61 try updateHtml(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │100 return slice.ptr; │ │101 } │ │102 │ │102 -export fn _start(json_ptr: [*]u8, json_len: u32, max_width: u32) void { │ │103 +export fn _start(json_ptr: [*]u8, json_len: u32, min_height: u32, max_width: u32) void {│ │104 const json = json_ptr[0..json_len]; │ │105 defer allocator.free(json); │ │105 - start(json, max_width) catch |err| { │ │106 + start(json, min_height, max_width) catch |err| { │ │107 var buf: [256]u8 = undefined; │ │108 const str = std.fmt.bufPrint(&buf, "start: {}", .{err}) catch unreachable; │ │109 consoleLog(str); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │110 }; │ │111 } │ │112 │ │112 -export fn _tick(max_width: u32) bool { │ │113 - tick(max_width) catch |err| { │ │113 +export fn _tick(min_height: u32, max_width: u32) bool { │ │114 + tick(min_height, max_width) catch |err| { │ │115 var buf: [256]u8 = undefined; │ │116 const str = std.fmt.bufPrint(&buf, "tick: {}", .{err}) catch unreachable;│ │117 consoleLog(str); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │431 │ │432 const child_grid = self.child.getGrid() orelse return; │ │433 │ │434 - const width = constraint.max_size.width orelse child_grid.size.width; │ │435 - const height = constraint.max_size.height orelse child_grid.size.height; │ │434 + // prefer max when given; otherwise grow to max(min, child) so that │ │435 + // when only a min is set (e.g. the wasm path passes viewport rows │ │436 + // as min_height with no max), we can still vertically center while │ │437 + // letting taller content extend past the min. │ │438 + const width = if (constraint.max_size.width) |w| w │ │439 + else if (constraint.min_size.width) |min_w| @max(min_w, child_grid.size.width) │ │440 + else child_grid.size.width; │ │441 + const height = if (constraint.max_size.height) |h| h │ │442 + else if (constraint.min_size.height) |min_h| @max(min_h, child_grid.size.height)│ │443 + else child_grid.size.height; │ │444 if (width == 0 or height == 0) return; │ │445 │ │446 const offset_x: usize = if (self.direction == .horiz or self.direction == .both) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │407 if (entry.ti.options.name.len > 0) { │ │408 try out.appendSlice(allocator, "\" name=\""); │ │409 try appendEscapedHtml(allocator, &out, entry.ti.options.name); │ │410 + } │ │411 + // mark the wasm-focused element so JS can mirror it onto DOM focus│ │412 + // — otherwise moving focus via arrow keys leaves the TUI border │ │413 + // showing focus but the underlying <input> unfocused. │ │414 + if (entry.focus_id == root_focus.grandchild_id) { │ │415 + try out.appendSlice(allocator, "\" data-focused=\"true"); │ │416 } │ │417 try out.appendSlice(allocator, "\" value=\""); │ │418 var value_buf: std.ArrayList(u8) = .empty; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │438 if (current_id != null) try out.appendSlice(allocator, "</span>"); │ │439 if (cell_id) |id| { │ │440 const kind = if (root_focus.children.get(id)) |entry| entry.focus.kind else .container; │ │441 + const focused_attr: []const u8 = if (id == root_focus.grandchild_id) " data-focused=\"true\"" else "";│ │442 var buf: [256]u8 = undefined; │ │443 // tabindex="0" on submit buttons puts them in the browser's │ │444 // natural Tab order alongside the inputs above. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │445 const tag = if (kind == .submit_button) │ │439 - try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\" data-action=\"submit\" data-url=\"{s}\" tabindex=\"0\">", .{ id, submit_url }) │ │446 + 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 })│ │447 else │ │441 - try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\">", .{id}); │ │448 + try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\"{s}>", .{ id, focused_attr }); │ │449 try out.appendSlice(allocator, tag); │ │450 } │ │451 current_id = cell_id; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯