Commit 91f4fc6315

91f4fc6315bc6ebd6517cd17461705af6a948f9f

parent: c1f3cadd80

Unsigned

cmc <hello@cleberg.net> · 2026-08-23 05:37 UTC
committer: <noreply@github.com>

Accessibility pass; bump to 3.12.0 (#42)

VoiceOver had nothing to announce for three kinds of control:

Icon-only buttons, which reached VoiceOver with no label at all — 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 carried only by a checkmark or a tint: applied ticket labels in two
views, the label filter, the saved-filter chip, and the paste visibility
picker. These now carry `.isSelected`.

Disclosure carried only by a chevron: diff file, diff hunk, inbox message.
`PatchsetDetailView` already had the hint; these match it.

Also: the project external-link row now says it leaves the app, and the commit
SHA button says it copies.

Decorative chevrons inside controls that already carry text were left alone —
an unlabelled SF Symbol contributes nothing to a combined label, so hiding them
would change no announcement.

Not device-verified. Builds clean and 260 tests pass, but neither is a VoiceOver
pass; ROADMAP.txt keeps that open.

Layout: unified · split

Hutch.xcodeproj/project.pbxproj +12 −12
@@ -597,7 +597,7 @@
597597 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
598598 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
599599 CODE_SIGN_STYLE = Automatic;
600 CURRENT_PROJECT_VERSION = 97;
600 CURRENT_PROJECT_VERSION = 98;
601601 DEVELOPMENT_TEAM = ZCNAX3VL9D;
602602 ENABLE_PREVIEWS = YES;
603603 GENERATE_INFOPLIST_FILE = YES;
@@ -614,7 +614,7 @@
614614 "$(inherited)",
615615 "@executable_path/Frameworks",
616616 );
617 MARKETING_VERSION = 3.11.1;
617 MARKETING_VERSION = 3.12.0;
618618 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
619619 PRODUCT_NAME = "$(TARGET_NAME)";
620620 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -634,7 +634,7 @@
634634 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
635635 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
636636 CODE_SIGN_STYLE = Automatic;
637 CURRENT_PROJECT_VERSION = 97;
637 CURRENT_PROJECT_VERSION = 98;
638638 DEVELOPMENT_TEAM = ZCNAX3VL9D;
639639 ENABLE_PREVIEWS = YES;
640640 GENERATE_INFOPLIST_FILE = YES;
@@ -651,7 +651,7 @@
651651 "$(inherited)",
652652 "@executable_path/Frameworks",
653653 );
654 MARKETING_VERSION = 3.11.1;
654 MARKETING_VERSION = 3.12.0;
655655 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
656656 PRODUCT_NAME = "$(TARGET_NAME)";
657657 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -714,7 +714,7 @@
714714 APPLICATION_EXTENSION_API_ONLY = YES;
715715 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
716716 CODE_SIGN_STYLE = Automatic;
717 CURRENT_PROJECT_VERSION = 97;
717 CURRENT_PROJECT_VERSION = 98;
718718 DEVELOPMENT_TEAM = ZCNAX3VL9D;
719719 GENERATE_INFOPLIST_FILE = NO;
720720 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -724,7 +724,7 @@
724724 "@executable_path/Frameworks",
725725 "@executable_path/../../Frameworks",
726726 );
727 MARKETING_VERSION = 3.11.1;
727 MARKETING_VERSION = 3.12.0;
728728 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
729729 PRODUCT_NAME = "$(TARGET_NAME)";
730730 SKIP_INSTALL = YES;
@@ -743,7 +743,7 @@
743743 APPLICATION_EXTENSION_API_ONLY = YES;
744744 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
745745 CODE_SIGN_STYLE = Automatic;
746 CURRENT_PROJECT_VERSION = 97;
746 CURRENT_PROJECT_VERSION = 98;
747747 DEVELOPMENT_TEAM = ZCNAX3VL9D;
748748 GENERATE_INFOPLIST_FILE = NO;
749749 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -753,7 +753,7 @@
753753 "@executable_path/Frameworks",
754754 "@executable_path/../../Frameworks",
755755 );
756 MARKETING_VERSION = 3.11.1;
756 MARKETING_VERSION = 3.12.0;
757757 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
758758 PRODUCT_NAME = "$(TARGET_NAME)";
759759 SKIP_INSTALL = YES;
@@ -772,7 +772,7 @@
772772 APPLICATION_EXTENSION_API_ONLY = YES;
773773 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
774774 CODE_SIGN_STYLE = Automatic;
775 CURRENT_PROJECT_VERSION = 97;
775 CURRENT_PROJECT_VERSION = 98;
776776 DEVELOPMENT_TEAM = ZCNAX3VL9D;
777777 GENERATE_INFOPLIST_FILE = NO;
778778 INFOPLIST_FILE = HutchSafariExtension/Info.plist;
@@ -782,7 +782,7 @@
782782 "@executable_path/Frameworks",
783783 "@executable_path/../../Frameworks",
784784 );
785 MARKETING_VERSION = 3.11.1;
785 MARKETING_VERSION = 3.12.0;
786786 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension;
787787 PRODUCT_NAME = "$(TARGET_NAME)";
788788 SKIP_INSTALL = YES;
@@ -801,7 +801,7 @@
801801 APPLICATION_EXTENSION_API_ONLY = YES;
802802 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
803803 CODE_SIGN_STYLE = Automatic;
804 CURRENT_PROJECT_VERSION = 97;
804 CURRENT_PROJECT_VERSION = 98;
805805 DEVELOPMENT_TEAM = ZCNAX3VL9D;
806806 GENERATE_INFOPLIST_FILE = NO;
807807 INFOPLIST_FILE = HutchSafariExtension/Info.plist;
@@ -811,7 +811,7 @@
811811 "@executable_path/Frameworks",
812812 "@executable_path/../../Frameworks",
813813 );
814 MARKETING_VERSION = 3.11.1;
814 MARKETING_VERSION = 3.12.0;
815815 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension;
816816 PRODUCT_NAME = "$(TARGET_NAME)";
817817 SKIP_INSTALL = YES;
Hutch/Extensions/ErrorViews.swift +1
@@ -37,6 +37,7 @@ struct SRHTErrorBanner: ViewModifier {
3737 Image(systemName: "xmark.circle.fill")
3838 .foregroundStyle(.white.opacity(0.8))
3939 }
40 .accessibilityLabel("Dismiss error")
4041 }
4142 .padding(12)
4243 .background(Color.red.gradient, in: RoundedRectangle(cornerRadius: 12))
Hutch/Views/Builds/BuildTaskLogView.swift +2
@@ -164,6 +164,7 @@ private struct BuildTaskLogContentView: View {
164164 Image(systemName: "chevron.up")
165165 }
166166 .disabled(matches.isEmpty)
167 .accessibilityLabel("Previous match")
167168
168169 Button {
169170 moveSelection(step: 1)
@@ -171,6 +172,7 @@ private struct BuildTaskLogContentView: View {
171172 Image(systemName: "chevron.down")
172173 }
173174 .disabled(matches.isEmpty)
175 .accessibilityLabel("Next match")
174176 }
175177 .buttonStyle(.borderless)
176178 .labelStyle(.iconOnly)
Hutch/Views/Inbox/ThreadDetailView.swift +1
@@ -295,6 +295,7 @@ private struct InboxMessageRow: View {
295295 }
296296 }
297297 .buttonStyle(.plain)
298 .accessibilityHint(isCollapsed ? "Expands this message" : "Collapses this message")
298299
299300 if !isCollapsed {
300301 ForEach(Array(message.contentBlocks.enumerated()), id: \.offset) { _, block in
Hutch/Views/More/MoreView.swift +1
@@ -96,6 +96,7 @@ struct MoreView: View {
9696 } label: {
9797 Image(systemName: "person.crop.circle.badge.plus")
9898 }
99 .accessibilityLabel("Switch account")
99100 }
100101 }
101102 .sheet(isPresented: $showAccountSwitcher) {
Hutch/Views/Pastes/PasteDetailView.swift +1
@@ -413,6 +413,7 @@ private struct PasteVisibilitySheet: View {
413413 .contentShape(Rectangle())
414414 }
415415 .buttonStyle(.plain)
416 .accessibilityAddTraits(visibility == option ? [.isSelected] : [])
416417 }
417418 .themedRow()
418419 }
Hutch/Views/Pastes/PasteListView.swift +1
@@ -25,6 +25,7 @@ struct PasteListView: View {
2525 } label: {
2626 Image(systemName: "plus")
2727 }
28 .accessibilityLabel("Create paste")
2829 }
2930 }
3031 }
Hutch/Views/Projects/ProjectDetailView.swift +1
@@ -217,6 +217,7 @@ struct ProjectDetailView: View {
217217 }
218218 }
219219 .buttonStyle(.plain)
220 .accessibilityHint("Opens in your browser")
220221 }
221222 .themedRow()
222223 }
Hutch/Views/Repositories/ArtifactsView.swift +1
@@ -206,6 +206,7 @@ private struct ArtifactRow: View {
206206 Image(systemName: "arrow.down.circle")
207207 .imageScale(.large)
208208 }
209 .accessibilityLabel("Download artifact")
209210 }
210211 }
211212}
Hutch/Views/Repositories/CommitDetailView.swift +1
@@ -153,6 +153,7 @@ struct CommitDetailView: View {
153153 }
154154 .foregroundStyle(.secondary)
155155 }
156 .accessibilityHint("Copies the commit SHA")
156157
157158 // Author
158159 HStack {
Hutch/Views/Repositories/DiffView.swift +2
@@ -58,6 +58,7 @@ private struct DiffFileSectionView: View {
5858 .contentShape(Rectangle())
5959 }
6060 .buttonStyle(.plain)
61 .accessibilityHint(isExpanded ? "Collapses this file" : "Expands this file")
6162 .background(Color(.tertiarySystemBackground))
6263
6364 if isExpanded {
@@ -169,6 +170,7 @@ private struct DiffHunkView: View {
169170 .contentShape(Rectangle())
170171 }
171172 .buttonStyle(.plain)
173 .accessibilityHint(isExpanded ? "Collapses this hunk" : "Expands this hunk")
172174 .background(Color(.systemBackground).opacity(0.5))
173175
174176 if isExpanded {
Hutch/Views/Tickets/TicketDetailView.swift +3
@@ -606,6 +606,7 @@ private struct EventRow: View {
606606 .foregroundStyle(.gray)
607607 }
608608 .buttonStyle(.plain)
609 .accessibilityLabel("About this status change")
609610 }
610611 } else {
611612 Text(descriptionText)
@@ -948,6 +949,7 @@ private struct AssignSheet: View {
948949 .foregroundStyle(.red)
949950 }
950951 .buttonStyle(.plain)
952 .accessibilityLabel("Unassign \(assignee.canonicalName)")
951953 }
952954 }
953955 .themedRow()
@@ -1152,5 +1154,6 @@ private struct LabelToggleRow: View {
11521154 }
11531155 }
11541156 .disabled(isLoading)
1157 .accessibilityAddTraits(isApplied ? [.isSelected] : [])
11551158 }
11561159}
Hutch/Views/Tickets/TicketListView.swift +7
@@ -857,6 +857,9 @@ private struct TicketQuickFilterBar: View {
857857 in: Capsule()
858858 )
859859 }
860 .accessibilityAddTraits(
861 activeSavedFilterID == savedFilter.id ? [.isSelected] : []
862 )
860863 .contextMenu {
861864 Button(role: .destructive) {
862865 onDeleteSavedFilter(savedFilter)
@@ -964,6 +967,9 @@ private struct TicketFilterLabelsSheet: View {
964967 }
965968 }
966969 .buttonStyle(.plain)
970 .accessibilityAddTraits(
971 viewModel.selectedLabelIDs.contains(label.id) ? [.isSelected] : []
972 )
967973 }
968974 .themedRow()
969975 }
@@ -1051,6 +1057,7 @@ private struct TicketListLabelToggleRow: View {
10511057 }
10521058 }
10531059 .disabled(isLoading)
1060 .accessibilityAddTraits(isApplied ? [.isSelected] : [])
10541061 }
10551062}
10561063
Hutch/Views/Tickets/TrackerListView.swift +1
@@ -25,6 +25,7 @@ struct TrackerListView: View {
2525 } label: {
2626 Image(systemName: "plus")
2727 }
28 .accessibilityLabel("Create tracker")
2829 }
2930 }
3031 }
Hutch/Views/Tickets/TrackerManagementView.swift +2
@@ -785,6 +785,7 @@ struct TrackerACLManagementSheet: View {
785785 Image(systemName: "plus")
786786 }
787787 .disabled(viewModel.isSavingACL)
788 .accessibilityLabel("Add ACL")
788789 }
789790 }
790791 .task {
@@ -1155,6 +1156,7 @@ struct TrackerLabelManagementSheet: View {
11551156 Image(systemName: "plus")
11561157 }
11571158 .disabled(viewModel.isSavingLabel)
1159 .accessibilityLabel("Create label")
11581160 }
11591161 }
11601162 .task {
ROADMAP.txt +31 −6
@@ -156,7 +156,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*.
156156| 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 |
157157| v3.10.0 | ~~git.sr.ht deploy keys~~ (shipped); ~~"What's cooking" ingest + doc truth-up~~ (done) | Ships one feature, corrects the map |
158158| v3.11.0 | ~~Mailing list subscribe/unsubscribe toggle~~ (shipped) | Ingest-surfaced; state via the `subscriptions` query (the `subscription` field is a trap) |
159| v3.12.0 | Accessibility | Independent, device-verified |
159| v3.12.0 | ~~Accessibility~~ (done in code) | Still wants a VoiceOver pass on a device |
160160| v4.0.0 | Localization *with* translations | The only true re-presentation |
161161| — | ~~Swift 6 language mode~~ (done); cache reads | Internal; ride along, no tag |
162162
@@ -206,11 +206,36 @@ the major number on regression risk while delivering nothing — the wrong trade
206206Hold the catalog until a translation lands. If it ever ships unbundled, it is
207207groundwork and belongs in a quiet minor, not a 4.0.
208208
209### Accessibility — v3.11.0
210
211Labels and hints appear in 17 of 89 view files. Mechanical and low-risk, but it
212cannot be verified from a build — it needs VoiceOver driven on a device.
213Independent of every other bucket, so it can move if a device pass is convenient.
209### Accessibility — done in code (v3.12.0)
210
211The earlier count here (17 of 89 view files) was wrong on both numbers: 22 of 95
212carried a modifier, and of those, labels were the whole story — one hint in the
213whole app, no traits at all. Counting files also hid the shape of the gap, which
214was not spread evenly but concentrated in three kinds of control:
215
216- **Icon-only controls**, which VoiceOver reaches with nothing to announce.
217 Eleven of them: the error banner's dismiss, log search next/previous match,
218 account switching, four `plus` creates (paste, tracker, ACL, label), artifact
219 download, the system-status info button, and assignee unassign.
220- **Selection state carried only by a checkmark or a tint.** Five: applied
221 ticket labels in two views, the label filter, the saved-filter chip, and the
222 paste visibility picker. These now carry `.isSelected` rather than relying on
223 an icon VoiceOver does not read.
224- **Disclosure state carried only by a chevron.** Three: diff file, diff hunk,
225 and inbox message. `PatchsetDetailView` already had the hint; the others now
226 match it.
227
228Two more: the project external-link row says it leaves the app, and the commit
229SHA button says it copies — both read as bare text before.
230
231Decorative chevrons inside a control that already carries text were left alone.
232An unlabelled SF Symbol contributes nothing to a combined label, so hiding them
233would be churn with no announced difference.
234
235Still open: **this is not device-verified.** It builds and the suite is green,
236but neither proves a VoiceOver pass. What is verified is that no icon-only
237control is left unlabelled — a static check over the enclosing control's brace
238span, not a guess at line proximity.
214239
215240### SonarCloud backlog — done in code (v3.8.1)
216241