╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────╮ │18 pub const Title = @import("./ui/Title.zig"); │ │19 pub const SubTitle = @import("./ui/SubTitle.zig"); │ │20 pub const Quit = @import("./ui/Quit.zig"); │ │21 +pub const Unauthorized = @import("./ui/Unauthorized.zig");│ │22 │ │23 pub const PageKind = enum { │ │24 home, │ ╰──────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────╮ │1358 user: User.View, │ │1359 repo: Repo.View, │ │1360 quit: Quit.View, │ │1361 + unauthorized: Unauthorized.View,│ │1362 title: Title.View, │ │1363 sub_title: SubTitle.View, │ │1364 home_header: Home.Header.View, │ ╰──────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Home.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │79 try stack.children.put(allocator, repos_view.getFocus().id, .{ .home_repos = repos_view }); │ │80 } │ │81 │ │82 - { │ │82 + // the header has no settings tab without a login, so keep the │ │83 + // stack's children 1:1 with the tabs by skipping the view too │ │84 + if (session.data.user_id != null) { │ │85 var settings_view = try Settings.View.init(allocator, session); │ │86 errdefer settings_view.deinit(allocator); │ │87 try stack.children.put(allocator, settings_view.getFocus().id, .{ .home_settings = settings_view });│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Home/Header.zig│ ╰──────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 69 }; │ │ 70 var selected_tab: ?usize = null; │ │ 71 │ │ 72 - // tabs │ │ 73 - for ( │ │ 74 - [_][]const u8{ "users", "repos", "", "settings", "" }, │ │ 75 - [_][]const u8{ "ai:/users", "ai:/repos", "", "ai:/settings", "ai:/auth" }, │ │ 76 - ) |name, focus_name| { │ │ 77 - // spacer │ │ 78 - if (focus_name.len == 0) { │ │ 79 - var spacer = try ui.Spacer.init(allocator); │ │ 80 - errdefer spacer.deinit(allocator); │ │ 81 - try box.children.put(allocator, spacer.getFocus().id, .{ │ │ 82 - .widget = .{ .spacer = spacer }, │ │ 83 - .rect = null, │ │ 84 - .min_size = null, │ │ 85 - }); │ │ 86 - } │ │ 87 - // auth │ │ 88 - else if (std.mem.eql(u8, "ai:/auth", focus_name)) { │ │ 89 - var auth_tab = try AuthTab.View.init(allocator, session); │ │ 90 - errdefer auth_tab.deinit(allocator); │ │ 91 - try tab_ids.put(allocator, auth_tab.getFocus().id, {}); │ │ 92 - if (std.mem.eql(u8, focus_name, current_link)) selected_tab = auth_tab.getFocus().id; │ │ 93 - try box.children.put(allocator, auth_tab.getFocus().id, .{ │ │ 94 - .widget = .{ .auth_tab = auth_tab }, │ │ 95 - .rect = null, │ │ 96 - .min_size = .{ .width = auth_tab.minWidth(), .height = null }, │ │ 97 - }); │ │ 98 - } │ │ 99 - // other tabs │ │100 - else { │ │101 - var text_box = try wgt.TextBox(ui.Widget).init(allocator, name, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │102 - errdefer text_box.deinit(allocator); │ │103 - text_box.getFocus().focusable = true; │ │104 - text_box.getFocus().kind = .{ .custom = focus_name }; │ │105 - try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │106 - if (std.mem.eql(u8, focus_name, current_link)) selected_tab = text_box.getFocus().id; │ │107 - try box.children.put(allocator, text_box.getFocus().id, .{ │ │108 - .widget = .{ .text_box = text_box }, │ │109 - .rect = null, │ │110 - .min_size = .{ .width = name.len + 2, .height = null }, │ │111 - }); │ │112 - } │ │ 72 + // users tab │ │ 73 + { │ │ 74 + var text_box = try wgt.TextBox(ui.Widget).init(allocator, "users", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │ 75 + errdefer text_box.deinit(allocator); │ │ 76 + text_box.getFocus().focusable = true; │ │ 77 + text_box.getFocus().kind = .{ .custom = "ai:/users" }; │ │ 78 + try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │ 79 + if (std.mem.eql(u8, "ai:/users", current_link)) selected_tab = text_box.getFocus().id; │ │ 80 + try box.children.put(allocator, text_box.getFocus().id, .{ │ │ 81 + .widget = .{ .text_box = text_box }, │ │ 82 + .rect = null, │ │ 83 + .min_size = .{ .width = "users".len + 2, .height = null }, │ │ 84 + }); │ │ 85 + } │ │ 86 + │ │ 87 + // repos tab │ │ 88 + { │ │ 89 + var text_box = try wgt.TextBox(ui.Widget).init(allocator, "repos", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │ 90 + errdefer text_box.deinit(allocator); │ │ 91 + text_box.getFocus().focusable = true; │ │ 92 + text_box.getFocus().kind = .{ .custom = "ai:/repos" }; │ │ 93 + try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │ 94 + if (std.mem.eql(u8, "ai:/repos", current_link)) selected_tab = text_box.getFocus().id; │ │ 95 + try box.children.put(allocator, text_box.getFocus().id, .{ │ │ 96 + .widget = .{ .text_box = text_box }, │ │ 97 + .rect = null, │ │ 98 + .min_size = .{ .width = "repos".len + 2, .height = null }, │ │ 99 + }); │ │100 } │ │101 │ │102 + // spacer pushes settings + auth to the right │ │103 + { │ │104 + var spacer = try ui.Spacer.init(allocator); │ │105 + errdefer spacer.deinit(allocator); │ │106 + try box.children.put(allocator, spacer.getFocus().id, .{ │ │107 + .widget = .{ .spacer = spacer }, │ │108 + .rect = null, │ │109 + .min_size = null, │ │110 + }); │ │111 + } │ │112 + │ │113 + // settings tab. settings are account preferences, so it needs a login. │ │114 + if (session.data.user_id != null) { │ │115 + var text_box = try wgt.TextBox(ui.Widget).init(allocator, "settings", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │116 + errdefer text_box.deinit(allocator); │ │117 + text_box.getFocus().focusable = true; │ │118 + text_box.getFocus().kind = .{ .custom = "ai:/settings" }; │ │119 + try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │120 + if (std.mem.eql(u8, "ai:/settings", current_link)) selected_tab = text_box.getFocus().id; │ │121 + try box.children.put(allocator, text_box.getFocus().id, .{ │ │122 + .widget = .{ .text_box = text_box }, │ │123 + .rect = null, │ │124 + .min_size = .{ .width = "settings".len + 2, .height = null }, │ │125 + }); │ │126 + } │ │127 + │ │128 + // auth tab (login / logout) │ │129 + { │ │130 + var auth_tab = try AuthTab.View.init(allocator, session); │ │131 + errdefer auth_tab.deinit(allocator); │ │132 + try tab_ids.put(allocator, auth_tab.getFocus().id, {}); │ │133 + if (std.mem.eql(u8, "ai:/auth", current_link)) selected_tab = auth_tab.getFocus().id; │ │134 + try box.children.put(allocator, auth_tab.getFocus().id, .{ │ │135 + .widget = .{ .auth_tab = auth_tab }, │ │136 + .rect = null, │ │137 + .min_size = .{ .width = auth_tab.minWidth(), .height = null }, │ │138 + }); │ │139 + } │ │140 + │ │141 // quit tab │ │142 if (session.is_terminal) { │ │143 var text_box = try wgt.TextBox(ui.Widget).init(allocator, ui.Quit.tab_label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │168 for (self.box.children.keys(), self.box.children.values()) |id, *child| { │ │169 const tb: ?*wgt.TextBox(ui.Widget) = switch (child.widget) { │ │170 .text_box => |*x| x, │ │145 - .auth_tab => |*at| &at.text_box, │ │171 + .auth_tab => |*at| blk: { │ │172 + // the label tracks login state per frame, so the width must too│ │173 + child.min_size = .{ .width = at.minWidth(), .height = null }; │ │174 + break :blk &at.text_box; │ │175 + }, │ │176 else => null, │ │177 }; │ │178 if (tb) |t| { │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │274 try stack.children.put(allocator, issues_view.getFocus().id, .{ .repo_issues = issues_view }); │ │275 } │ │276 │ │277 - { │ │277 + // the header only shows the settings tab with a login and the auth │ │278 + // tab outside local mode, so keep the stack's children 1:1 with │ │279 + // the tabs by skipping the same views. │ │280 + if (session.data.user_id != null) { │ │281 var settings_view = try Settings.View.init(allocator, session); │ │282 errdefer settings_view.deinit(allocator); │ │283 try stack.children.put(allocator, settings_view.getFocus().id, .{ .home_settings = settings_view });│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │284 } │ │285 │ │283 - // the header has no auth tab in local mode, so keep the stack's │ │284 - // children 1:1 with the tabs by skipping the auth view too. │ │286 if (!session.data.is_local) { │ │287 var auth_view = try Auth.View.init(allocator, &data.auth, session);│ │288 errdefer auth_view.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Header.zig│ ╰──────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │208 }); │ │209 } │ │210 │ │211 - // settings tab │ │212 - { │ │211 + // settings tab. settings are account preferences, so it needs a login │ │212 + // (which also rules out local mode, which has no accounts). │ │213 + if (session.data.user_id != null) { │ │214 var text_box = try wgt.TextBox(ui.Widget).init(allocator, "settings", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │215 errdefer text_box.deinit(allocator); │ │216 text_box.getFocus().focusable = true; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │271 for (self.box.children.keys(), self.box.children.values()) |id, *child| { │ │272 const tb: ?*wgt.TextBox(ui.Widget) = switch (child.widget) { │ │273 .text_box => |*x| x, │ │273 - .auth_tab => |*at| &at.text_box, │ │274 + .auth_tab => |*at| blk: { │ │275 + // the label tracks login state per frame, so the width must too│ │276 + child.min_size = .{ .width = at.minWidth(), .height = null }; │ │277 + break :blk &at.text_box; │ │278 + }, │ │279 else => null, │ │280 }; │ │281 if (tb) |t| { │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │422 } │ │423 │ │424 // the new-issue form, or — on an edit url — the edit form in its │ │425 - // place, prefilled with the selected issue's content. │ │426 - { │ │425 + // place, prefilled with the selected issue's content. a logged-out│ │426 + // session can't create events, so the unauthorized view stands in.│ │427 + if (session.data.is_local or session.data.user_id != null) { │ │428 const aa = session.page_arena.allocator(); │ │429 const action = if (data.view == .edit) │ │430 (if (data.identity.len == 0) │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯