krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
f1169f2adcdeabdec3f496cec2b52a2594065f0f
signed_unknown_key
author: Christian Cleberg <hello@cleberg.net> · 2026-08-23T05:37:45Z
committer: <noreply@github.com>
Hutch.xcodeproj/project.pbxproj | 24 ++++++++-------- Hutch/Extensions/ErrorViews.swift | 1 + Hutch/Views/Builds/BuildTaskLogView.swift | 2 ++ Hutch/Views/Inbox/ThreadDetailView.swift | 1 + Hutch/Views/More/MoreView.swift | 1 + Hutch/Views/Pastes/PasteDetailView.swift | 1 + Hutch/Views/Pastes/PasteListView.swift | 1 + Hutch/Views/Projects/ProjectDetailView.swift | 1 + Hutch/Views/Repositories/ArtifactsView.swift | 1 + Hutch/Views/Repositories/CommitDetailView.swift | 1 + Hutch/Views/Repositories/DiffView.swift | 2 ++ Hutch/Views/Tickets/TicketDetailView.swift | 3 ++ Hutch/Views/Tickets/TicketListView.swift | 7 +++++ Hutch/Views/Tickets/TrackerListView.swift | 1 + Hutch/Views/Tickets/TrackerManagementView.swift | 2 ++ ROADMAP.txt | 37 +++++++++++++++++++++---- 16 files changed, 68 insertions(+), 18 deletions(-) @@ -597,7 +597,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -614,7 +614,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.11.1; + MARKETING_VERSION = 3.12.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -634,7 +634,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -651,7 +651,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.11.1; + MARKETING_VERSION = 3.12.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -714,7 +714,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -724,7 +724,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.11.1; + MARKETING_VERSION = 3.12.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -743,7 +743,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -753,7 +753,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.11.1; + MARKETING_VERSION = 3.12.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -772,7 +772,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchSafariExtension/Info.plist; @@ -782,7 +782,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.11.1; + MARKETING_VERSION = 3.12.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -801,7 +801,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchSafariExtension/Info.plist; @@ -811,7 +811,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.11.1; + MARKETING_VERSION = 3.12.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -37,6 +37,7 @@ struct SRHTErrorBanner: ViewModifier { Image(systemName: "xmark.circle.fill") .foregroundStyle(.white.opacity(0.8)) } + .accessibilityLabel("Dismiss error") } .padding(12) .background(Color.red.gradient, in: RoundedRectangle(cornerRadius: 12)) @@ -164,6 +164,7 @@ private struct BuildTaskLogContentView: View { Image(systemName: "chevron.up") } .disabled(matches.isEmpty) + .accessibilityLabel("Previous match") Button { moveSelection(step: 1) @@ -171,6 +172,7 @@ private struct BuildTaskLogContentView: View { Image(systemName: "chevron.down") } .disabled(matches.isEmpty) + .accessibilityLabel("Next match") } .buttonStyle(.borderless) .labelStyle(.iconOnly) @@ -295,6 +295,7 @@ private struct InboxMessageRow: View { } } .buttonStyle(.plain) + .accessibilityHint(isCollapsed ? "Expands this message" : "Collapses this message") if !isCollapsed { ForEach(Array(message.contentBlocks.enumerated()), id: \.offset) { _, block in @@ -96,6 +96,7 @@ struct MoreView: View { } label: { Image(systemName: "person.crop.circle.badge.plus") } + .accessibilityLabel("Switch account") } } .sheet(isPresented: $showAccountSwitcher) { @@ -413,6 +413,7 @@ private struct PasteVisibilitySheet: View { .contentShape(Rectangle()) } .buttonStyle(.plain) + .accessibilityAddTraits(visibility == option ? [.isSelected] : []) } .themedRow() } @@ -25,6 +25,7 @@ struct PasteListView: View { } label: { Image(systemName: "plus") } + .accessibilityLabel("Create paste") } } } @@ -217,6 +217,7 @@ struct ProjectDetailView: View { } } .buttonStyle(.plain) + .accessibilityHint("Opens in your browser") } .themedRow() } @@ -206,6 +206,7 @@ private struct ArtifactRow: View { Image(systemName: "arrow.down.circle") .imageScale(.large) } + .accessibilityLabel("Download artifact") } } } @@ -153,6 +153,7 @@ struct CommitDetailView: View { } .foregroundStyle(.secondary) } + .accessibilityHint("Copies the commit SHA") // Author HStack { @@ -58,6 +58,7 @@ private struct DiffFileSectionView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) + .accessibilityHint(isExpanded ? "Collapses this file" : "Expands this file") .background(Color(.tertiarySystemBackground)) if isExpanded { @@ -169,6 +170,7 @@ private struct DiffHunkView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) + .accessibilityHint(isExpanded ? "Collapses this hunk" : "Expands this hunk") .background(Color(.systemBackground).opacity(0.5)) if isExpanded { @@ -606,6 +606,7 @@ private struct EventRow: View { .foregroundStyle(.gray) } .buttonStyle(.plain) + .accessibilityLabel("About this status change") } } else { Text(descriptionText) @@ -948,6 +949,7 @@ private struct AssignSheet: View { .foregroundStyle(.red) } .buttonStyle(.plain) + .accessibilityLabel("Unassign \(assignee.canonicalName)") } } .themedRow() @@ -1152,5 +1154,6 @@ private struct LabelToggleRow: View { } } .disabled(isLoading) + .accessibilityAddTraits(isApplied ? [.isSelected] : []) } } @@ -857,6 +857,9 @@ private struct TicketQuickFilterBar: View { in: Capsule() ) } + .accessibilityAddTraits( + activeSavedFilterID == savedFilter.id ? [.isSelected] : [] + ) .contextMenu { Button(role: .destructive) { onDeleteSavedFilter(savedFilter) @@ -964,6 +967,9 @@ private struct TicketFilterLabelsSheet: View { } } .buttonStyle(.plain) + .accessibilityAddTraits( + viewModel.selectedLabelIDs.contains(label.id) ? [.isSelected] : [] + ) } .themedRow() } @@ -1051,6 +1057,7 @@ private struct TicketListLabelToggleRow: View { } } .disabled(isLoading) + .accessibilityAddTraits(isApplied ? [.isSelected] : []) } } @@ -25,6 +25,7 @@ struct TrackerListView: View { } label: { Image(systemName: "plus") } + .accessibilityLabel("Create tracker") } } } @@ -785,6 +785,7 @@ struct TrackerACLManagementSheet: View { Image(systemName: "plus") } .disabled(viewModel.isSavingACL) + .accessibilityLabel("Add ACL") } } .task { @@ -1155,6 +1156,7 @@ struct TrackerLabelManagementSheet: View { Image(systemName: "plus") } .disabled(viewModel.isSavingLabel) + .accessibilityLabel("Create label") } } .task { @@ -156,7 +156,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*. | v3.9.0 | ~~hub.sr.ht project writes + discovery (#12–#15); multi-language highlighting (#16); App Intents expansion (#17); man-page catalog sync (#7); checklist / recent-activity / pull-to-refresh fixes (#18, #11, #9)~~ | Shipped — the cut this session | | v3.10.0 | ~~git.sr.ht deploy keys~~ (shipped); ~~"What's cooking" ingest + doc truth-up~~ (done) | Ships one feature, corrects the map | | v3.11.0 | ~~Mailing list subscribe/unsubscribe toggle~~ (shipped) | Ingest-surfaced; state via the `subscriptions` query (the `subscription` field is a trap) | -| v3.12.0 | Accessibility | Independent, device-verified | +| v3.12.0 | ~~Accessibility~~ (done in code) | Still wants a VoiceOver pass on a device | | v4.0.0 | Localization *with* translations | The only true re-presentation | | — | ~~Swift 6 language mode~~ (done); cache reads | Internal; ride along, no tag | @@ -206,11 +206,36 @@ the major number on regression risk while delivering nothing — the wrong trade Hold the catalog until a translation lands. If it ever ships unbundled, it is groundwork and belongs in a quiet minor, not a 4.0. -### Accessibility — v3.11.0 - -Labels and hints appear in 17 of 89 view files. Mechanical and low-risk, but it -cannot be verified from a build — it needs VoiceOver driven on a device. -Independent of every other bucket, so it can move if a device pass is convenient. +### Accessibility — done in code (v3.12.0) + +The earlier count here (17 of 89 view files) was wrong on both numbers: 22 of 95 +carried a modifier, and of those, labels were the whole story — one hint in the +whole app, no traits at all. Counting files also hid the shape of the gap, which +was not spread evenly but concentrated in three kinds of control: + +- **Icon-only controls**, which VoiceOver reaches with nothing to announce. + Eleven of them: the error banner's dismiss, log search next/previous match, + account switching, four `plus` creates (paste, tracker, ACL, label), artifact + download, the system-status info button, and assignee unassign. +- **Selection state carried only by a checkmark or a tint.** Five: applied + ticket labels in two views, the label filter, the saved-filter chip, and the + paste visibility picker. These now carry `.isSelected` rather than relying on + an icon VoiceOver does not read. +- **Disclosure state carried only by a chevron.** Three: diff file, diff hunk, + and inbox message. `PatchsetDetailView` already had the hint; the others now + match it. + +Two more: the project external-link row says it leaves the app, and the commit +SHA button says it copies — both read as bare text before. + +Decorative chevrons inside a control that already carries text were left alone. +An unlabelled SF Symbol contributes nothing to a combined label, so hiding them +would be churn with no announced difference. + +Still open: **this is not device-verified.** It builds and the suite is green, +but neither proves a VoiceOver pass. What is verified is that no icon-only +control is left unlabelled — a static check over the enclosing control's brace +span, not a guess at line proximity. ### SonarCloud backlog — done in code (v3.8.1)