╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────╮ │ 92 pub const active_event_id_set_key = "active-event-id-set"; │ │ 93 pub const removed_event_id_set_key = "removed-event-id-set";│ │ 94 const event_order_key = "event-order"; │ │ 95 + │ │ 96 +pub fn titleValid(title: []const u8) bool { │ │ 97 + return std.mem.trim(u8, title, " \t\r\n").len != 0; │ │ 98 +} │ │ 99 │ │100 // a null payload removes the record │ │101 pub const Event = union(EventKind) { │ ╰─────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/event/Discussion.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │31 } │ │32 │ │33 pub fn fieldsValid(title: []const u8, tags: []const u8) bool {│ │34 - if (title.len == 0) return false; │ │34 + if (!evt.titleValid(title)) return false; │ │35 var tag_iter = tagIterator(tags); │ │36 while (tag_iter.next()) |tag| { │ │37 if (tag.len > tag_max_len) return false; │ ╰──────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │56 │ │57 // a title is required, and a too-long tag would go unindexed once consumed│ │58 pub fn fieldsValid(title: []const u8, tags: []const u8) bool { │ │59 - if (title.len == 0) return false; │ │59 + if (!evt.titleValid(title)) return false; │ │60 var tag_iter = tagIterator(tags); │ │61 while (tag_iter.next()) |tag| { │ │62 if (tag.len > tag_max_len) return false; │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/event/Patch.zig│ ╰───────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │144 } │ │145 │ │146 pub fn fieldsValid(title: []const u8, tags: []const u8) bool {│ │147 - if (title.len == 0) return false; │ │147 + if (!evt.titleValid(title)) return false; │ │148 var tag_iter = tagIterator(tags); │ │149 while (tag_iter.next()) |tag| { │ │150 if (tag.len > tag_max_len) return false; │ ╰───────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │244 } │ │245 const ti = session.text_inputs.get(focus_id) orelse return error.UnknownFocusId;│ │246 try ti.setContent(allocator, bytes); │ │247 - // mirror the TTY behavior in Login.View.input: editing an input clears │ │248 - // the stale failure flag so the "(invalid)" label doesn't linger after │ │249 - // the user has typed a correction │ │250 - session.data.login_failure = null; │ │247 } │ │248 │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │1681 sync_events, │ │1682 }; │ │1683 │ │1684 + pub const FormFeedback = union(enum) { │ │1685 + pub const LoginFailure = enum { unknown_user, wrong_password }; │ │1686 + pub const ThreadFailure = enum { required_title }; │ │1687 + pub const PatchFailure = enum { required_title, invalid_target_branch };│ │1688 + │ │1689 + login: struct { │ │1690 + failure: LoginFailure, │ │1691 + username: []const u8, │ │1692 + }, │ │1693 + issue: struct { │ │1694 + failure: ThreadFailure, │ │1695 + fields: ?struct { │ │1696 + title: []const u8, │ │1697 + tags: []const u8, │ │1698 + description: []const u8, │ │1699 + } = null, │ │1700 + }, │ │1701 + patch: struct { │ │1702 + failure: PatchFailure, │ │1703 + fields: ?struct { │ │1704 + title: []const u8, │ │1705 + tags: []const u8, │ │1706 + description: []const u8, │ │1707 + target_branch: []const u8, │ │1708 + } = null, │ │1709 + }, │ │1710 + discussion: struct { │ │1711 + failure: ThreadFailure, │ │1712 + fields: ?struct { │ │1713 + title: []const u8, │ │1714 + tags: []const u8, │ │1715 + description: []const u8, │ │1716 + } = null, │ │1717 + }, │ │1718 + }; │ │1719 + │ │1720 const Self = @This(); │ │1721 │ │1722 // serializable data sent down to web client │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1724 user_id: ?[]const u8 = null, │ │1725 // the logged-in user's name, for the views that show it │ │1726 user_name: ?[]const u8 = null, │ │1691 - // a transient outcome to surface from the last /login POST attempt│ │1692 - login_failure: ?Home.Auth.Login.Failure = null, │ │1727 + // a failed form retained long enough to render it for correction │ │1728 + form_feedback: ?FormFeedback = null, │ │1729 // a transient local event-sync error │ │1730 sync_failure: ?[]const u8 = null, │ │1731 current_page: RoutablePage = .default, │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1790 var id: [evt.event_id_size]u8 = undefined; │ │1791 @memcpy(&id, bytes); │ │1792 return id; │ │1793 + } │ │1794 + │ │1795 + pub fn formFeedback(self: *const Self, comptime tag: std.meta.Tag(FormFeedback)) ?@FieldType(FormFeedback, @tagName(tag)) {│ │1796 + const feedback = self.data.form_feedback orelse return null; │ │1797 + if (std.meta.activeTag(feedback) != tag) return null; │ │1798 + return @field(feedback, @tagName(tag)); │ │1799 + } │ │1800 + │ │1801 + pub fn clearFormFeedback(self: *Self) void { │ │1802 + self.data.form_feedback = null; │ │1803 } │ │1804 │ │1805 // the commit author for an event this session creates, or null when it may │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1870 // request a forward navigation to `route`; the host consumes next_page│ │1871 // (Nav.sync on the terminal, the wasm tick on the web). │ │1872 pub fn navigate(self: *Session, route: RoutablePage) !void { │ │1873 + self.clearFormFeedback(); │ │1874 self.next_page = route; │ │1875 } │ │1876 }; │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Auth/Login.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │17 const wasm = builtin.target.cpu.arch == .wasm32; │ │18 │ │19 const Self = @This(); │ │20 - │ │21 -// classifies a failed /login attempt; null in session.data.login_failure means│ │22 -// "no failure to surface". │ │23 -pub const Failure = enum { wrong_password, unknown_user }; │ │20 │ │21 pub fn init() Self { │ │22 return .{}; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯