╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/ui/Repo/Settings.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────╮ │175 - return self.center.getGrid(); │ │260 + return self.scroll.getGrid(); │ │261 } │ │262 │ │263 pub fn getFocus(self: *View) *Focus {│ ╰──────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────╮ │179 - return self.center.getFocus(); │ │264 + return self.scroll.getFocus(); │ │265 } │ │266 │ │267 // up leaves the form from its first control│ ╰─────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────╮ │268 pub fn atTop(self: View) bool { │ │184 - const box = &self.center.child.box; │ │269 + const box = &self.scroll.child.center.child.box; │ │270 return box.focus.child_id == box.children.keys()[0];│ │271 } │ │272 }; │ ╰─────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │282 name: []const u8, │ │283 description: []const u8, │ │284 access: evt.Repo.Access, │ │285 + discuss_role: ?evt.Repo.Role, │ │286 + issue_role: ?evt.Repo.Role, │ │287 + patch_role: ?evt.Repo.Role, │ │288 ) !ui.RoutablePage { │ │289 const parsed = ui.RoutablePage.RepoIdentity.parse(identity) orelse return error.NotFound;│ │290 const owner_id = actor.repo_user_id orelse return error.NotFound; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │203 - try evt.updateRepo(io, allocator, users_dir, &owner_id, actor.author, parsed.name, name, description, access); │ │291 + try evt.updateRepo(io, allocator, users_dir, &owner_id, actor.author, parsed.name, name, description, access, discuss_role, issue_role, patch_role);│ │292 var buf: [ui.RoutablePage.repo_route_max_len]u8 = undefined; │ │293 const new_identity = std.fmt.bufPrint(&buf, "{s}:{s}", .{ parsed.owner, name }) catch return error.RouteTooLong; │ │294 return ui.RoutablePage.repoRepoRoute(new_identity) orelse error.RouteTooLong; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │370 │ │371 // the repo an owner may act on, or null when they may not │ │372 fn authorizedSource(session: *ui.Session, identity: []const u8) !?ui.RepoSource {│ │373 - if (try session.authorize(identity, .owner) == null) return null; │ │373 + if (try session.authorize(identity, .owner, null) == null) return null; │ │374 if (session.local) |local| return local; │ │375 const admin_repo = session.admin_repo orelse return error.NotFound; │ │376 const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/User.zig│ ╰───────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │135 const owner_id = record.event.repo_user_id[0..evt.event_id_size]; │ │136 const target = (try evt.readRepoById(io, gpa, arena, users_dir, owner_id, record.event.repo_id[0..evt.event_id_size], session.userId())) orelse continue;│ │137 if (target.role == null) continue; │ │138 + // patches that are off take their drafts with them │ │139 + if (target.repo.event.patch_role == null) continue; │ │140 const target_repo = target.repo; │ │141 │ │142 var title: []const u8 = "(unavailable)"; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │1047 } │ │1048 }; │ │1049 │ │1050 -// a row of bordered options posted as one form field. the selection is the │ │1051 -// focused option, so a click selects on the web too. │ │1050 +// a wrapping row of bordered options posted as one form field. the selection│ │1051 +// is the focused option, so a click selects on the web too. │ │1052 pub const Radio = struct { │ │1053 + // holds the options' flow, framed under the label when there is one │ │1054 box: wgt.Box(Widget), │ │1055 name: []const u8, │ │1056 values: []const []const u8, │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1057 │ │1057 - pub fn init(allocator: std.mem.Allocator, session: *ui.Session, name: []const u8, values: []const []const u8, initial: []const u8) !Radio { │ │1058 - var box = try wgt.Box(Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │1058 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, name: []const u8, values: []const []const u8, initial: []const u8, label: ?[]const u8) !Radio { │ │1059 + var box = try wgt.Box(Widget).init(allocator, .{ │ │1060 + .border = if (label != null) .single else null, │ │1061 + .round_corners = true, │ │1062 + .direction = .vert, │ │1063 + .top_label = .{ .text = label orelse "" }, │ │1064 + }); │ │1065 errdefer box.deinit(allocator); │ │1060 - for (values) |value| { │ │1061 - var option = try wgt.TextBox.init(allocator, value, .{ .border = .single, .round_corners = true, .wrap_kind = .none }); │ │1062 - errdefer option.deinit(allocator); │ │1063 - option.getFocus().mode = .all; │ │1064 - option.getFocus().kind = .{ .custom = try session.page_arena.allocator().print(ui.radio_prefix ++ "{s}={s}", .{ name, value }) }; │ │1065 - if (std.mem.eql(u8, value, initial)) box.getFocus().child_id = option.getFocus().id; │ │1066 - try box.children.put(allocator, option.getFocus().id, .{ .widget = .{ .text_box = option }, .rect = null, .min_size = .{ .width = try xitui.width.displayWidth(value) + 2, .height = 3 } });│ │1066 + │ │1067 + var flow = try WordFlow.init(allocator); │ │1068 + errdefer flow.deinit(allocator); │ │1069 + const pa = session.page_arena.allocator(); │ │1070 + const items = try pa.alloc(WordFlow.Item, values.len); │ │1071 + for (items, values) |*item, value| item.* = .{ .text = value, .link = try pa.print(ui.radio_prefix ++ "{s}={s}", .{ name, value }) }; │ │1072 + try flow.setItems(allocator, items); │ │1073 + for (flow.text_boxes.items, values) |*option, value| { │ │1074 + if (std.mem.eql(u8, value, initial)) flow.focus.child_id = option.getFocus().id; │ │1075 } │ │1076 + │ │1077 + box.getFocus().child_id = flow.getFocus().id; │ │1078 + try box.children.put(allocator, flow.getFocus().id, .{ .widget = .{ .word_flow = flow }, .rect = null, .min_size = null }); │ │1079 return .{ .box = box, .name = name, .values = values }; │ │1080 } │ │1081 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │1082 pub fn deinit(self: *Radio, allocator: std.mem.Allocator) void {│ │1083 self.box.deinit(allocator); │ │1084 + } │ │1085 + │ │1086 + fn optionFlow(self: *Radio) *WordFlow { │ │1087 + return &self.box.children.values()[0].widget.word_flow; │ │1088 } │ │1089 │ │1090 // the focused option's value │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯