Commit c108ce3805

c108ce38051dee4be3c4aed9aed7383c4478f345

parent: 215138b9ca

Unsigned

cmc <hello@cleberg.net> · 2026-09-30 03:49 UTC

Fix push order, list tops, build jump labels, More pill borders (!148)

- Push order: `navigationDestination(item:)` pushes put later value-link pushes underneath (repo → Builds → a build showed the list; Back showed the build). All five tab stacks own a `NavigationPath`; RepoView (strip, More, owner, settings, fork), the created-issue push and body reference links append via the new `pushRoute` environment value. No `navigationDestination(item:)` remains.
- Segmented-filter lists: picker in its own section so the rows' section keeps rounded top corners (issues, MRs, milestones, notifications, explore search, admin users).
- Build detail: labelled "Jump to failure" / "Jump to end" buttons under the header; iOS 26 rendered the toolbar versions icon-only.
- Repository More pills: border removed.

573 unit tests, Release build, UI target compiles. Checked in stub-hosted snapshots (issues list, failed build, repo page); push order not exercised by a test.

Layout: unified · split

gitbay/ContentView.swift +15 −3
@@ -12,6 +12,9 @@ struct ContentView: View {
12 @State private var selectedTab = Tabs.dashboard 12 @State private var selectedTab = Tabs.dashboard
13 @State private var dashboardPath = NavigationPath() 13 @State private var dashboardPath = NavigationPath()
14 @State private var repoPath = NavigationPath() 14 @State private var repoPath = NavigationPath()
15 @State private var feedPath = NavigationPath()
16 @State private var explorePath = NavigationPath()
17 @State private var profilePath = NavigationPath()
15 @State private var unread = 0 18 @State private var unread = 0
16 19
17 /// whoami's admin flag, read on the My Profile tab, decides whether 20 /// whoami's admin flag, read on the My Profile tab, decides whether
@@ -40,14 +43,16 @@ struct ContentView: View {
40 DashboardView(client: client, unread: $unread) 43 DashboardView(client: client, unread: $unread)
41 .navigationDestinations(client: client) 44 .navigationDestinations(client: client)
42 } 45 }
46 .pushesRoutes(into: $dashboardPath)
43 } 47 }
44 } 48 }
45 .badge(unread) 49 .badge(unread)
46 Tab("Feed", systemImage: "bolt", value: Tabs.feed) { 50 Tab("Feed", systemImage: "bolt", value: Tabs.feed) {
47 NavigationStack { 51 NavigationStack(path: $feedPath) {
48 FeedView(client: client) 52 FeedView(client: client)
49 .navigationDestinations(client: client) 53 .navigationDestinations(client: client)
50 } 54 }
55 .pushesRoutes(into: $feedPath)
51 } 56 }
52 Tab("Repositories", systemImage: "books.vertical", value: Tabs.repositories) { 57 Tab("Repositories", systemImage: "books.vertical", value: Tabs.repositories) {
53 if sizeClass == .regular { 58 if sizeClass == .regular {
@@ -59,16 +64,18 @@ struct ContentView: View {
59 RepoListView(client: client) 64 RepoListView(client: client)
60 .navigationDestinations(client: client) 65 .navigationDestinations(client: client)
61 } 66 }
67 .pushesRoutes(into: $repoPath)
62 } 68 }
63 } 69 }
64 Tab("Explore", systemImage: "safari", value: Tabs.explore) { 70 Tab("Explore", systemImage: "safari", value: Tabs.explore) {
65 NavigationStack { 71 NavigationStack(path: $explorePath) {
66 ExploreView(client: client) 72 ExploreView(client: client)
67 .navigationDestinations(client: client) 73 .navigationDestinations(client: client)
68 } 74 }
75 .pushesRoutes(into: $explorePath)
69 } 76 }
70 Tab("My Profile", systemImage: "person.crop.circle", value: Tabs.profile) { 77 Tab("My Profile", systemImage: "person.crop.circle", value: Tabs.profile) {
71 NavigationStack { 78 NavigationStack(path: $profilePath) {
72 ProfileView(client: client, name: account.username, 79 ProfileView(client: client, name: account.username,
73 title: "My Profile") 80 title: "My Profile")
74 .toolbar { AccountMenu(isAdmin: isAdmin) } 81 .toolbar { AccountMenu(isAdmin: isAdmin) }
@@ -78,6 +85,7 @@ struct ContentView: View {
78 } 85 }
79 .navigationDestinations(client: client) 86 .navigationDestinations(client: client)
80 } 87 }
88 .pushesRoutes(into: $profilePath)
81 } 89 }
82 } 90 }
83 .id(account.id) // a fresh tab bar and stacks per account 91 .id(account.id) // a fresh tab bar and stacks per account
@@ -123,6 +131,9 @@ struct ContentView: View {
123 if pathAccount != account.id { 131 if pathAccount != account.id {
124 dashboardPath = NavigationPath() 132 dashboardPath = NavigationPath()
125 repoPath = NavigationPath() 133 repoPath = NavigationPath()
134 feedPath = NavigationPath()
135 explorePath = NavigationPath()
136 profilePath = NavigationPath()
126 selectedTab = .dashboard 137 selectedTab = .dashboard
127 pathAccount = account.id 138 pathAccount = account.id
128 } 139 }
@@ -229,6 +240,7 @@ private struct SplitStack<Sidebar: View>: View {
229 } 240 }
230 .navigationDestinations(client: client) 241 .navigationDestinations(client: client)
231 } 242 }
243 .pushesRoutes(into: $path)
232 } 244 }
233 } 245 }
234} 246}
gitbay/Views/Account/NotificationsView.swift +8 −6
@@ -17,13 +17,15 @@ struct NotificationsView: View {
17 17
18 var body: some View { 18 var body: some View {
19 List { 19 List {
20 Picker("Filter", selection: Bindable(model).showAll) { 20 Section {
21 Text("Unread").tag(false) 21 Picker("Filter", selection: Bindable(model).showAll) {
22 Text("All").tag(true) 22 Text("Unread").tag(false)
23 Text("All").tag(true)
24 }
25 .pickerStyle(.segmented)
26 .listRowBackground(Color.clear)
27 .listRowInsets(EdgeInsets())
23 } 28 }
24 .pickerStyle(.segmented)
25 .listRowBackground(Color.clear)
26 .listRowInsets(EdgeInsets())
27 29
28 if let actionError = model.actionError { 30 if let actionError = model.actionError {
29 Section { GBNotice(actionError) } 31 Section { GBNotice(actionError) }
gitbay/Views/Admin/AdminUsersView.swift +9 −7
@@ -20,15 +20,17 @@ struct AdminUsersView: View {
20 20
21 var body: some View { 21 var body: some View {
22 List { 22 List {
23 Picker("State", selection: Bindable(model).filter) { 23 Section {
24 ForEach(AdminUserFilter.allCases) { filter in 24 Picker("State", selection: Bindable(model).filter) {
25 Text(filter.label).tag(filter) 25 ForEach(AdminUserFilter.allCases) { filter in
26 Text(filter.label).tag(filter)
27 }
26 } 28 }
29 .pickerStyle(.segmented)
30 .listRowBackground(Color.clear)
31 .listRowInsets(EdgeInsets())
32 .accessibilityIdentifier("admin-user-filter")
27 } 33 }
28 .pickerStyle(.segmented)
29 .listRowBackground(Color.clear)
30 .listRowInsets(EdgeInsets())
31 .accessibilityIdentifier("admin-user-filter")
32 34
33 if let error = model.actionError { 35 if let error = model.actionError {
34 Section { 36 Section {
gitbay/Views/Builds/BuildDetailView.swift +24 −14
@@ -29,6 +29,9 @@ struct BuildDetailView: View {
29 .padding(.vertical, 8) 29 .padding(.vertical, 8)
30 } 30 }
31 header(detail.build) 31 header(detail.build)
32 if !detail.log.isEmpty {
33 jumpBar(failed: detail.lines.failed != nil)
34 }
32 Divider() 35 Divider()
33 log(detail) 36 log(detail)
34 } 37 }
@@ -39,20 +42,6 @@ struct BuildDetailView: View {
39 .navigationBarTitleDisplayMode(.inline) 42 .navigationBarTitleDisplayMode(.inline)
40 .searchable(text: $query, prompt: "Filter log lines") 43 .searchable(text: $query, prompt: "Filter log lines")
41 .toolbar { 44 .toolbar {
42 ToolbarItemGroup(placement: .topBarTrailing) {
43 if let detail = model.state.value, !detail.log.isEmpty {
44 if detail.lines.failed != nil {
45 Button("Jump to failure", systemImage: "exclamationmark.triangle") {
46 jump = .failure
47 }
48 .accessibilityIdentifier("build-jump-failure")
49 }
50 Button("Jump to end", systemImage: "arrow.down.to.line") {
51 jump = .end
52 }
53 .accessibilityIdentifier("build-jump-end")
54 }
55 }
56 ToolbarItem(placement: .topBarTrailing) { 45 ToolbarItem(placement: .topBarTrailing) {
57 if model.isCancellable { 46 if model.isCancellable {
58 if model.working { 47 if model.working {
@@ -89,6 +78,27 @@ struct BuildDetailView: View {
89 } 78 }
90 } 79 }
91 80
81 private func jumpBar(failed: Bool) -> some View {
82 HStack(spacing: 20) {
83 if failed {
84 Button("Jump to failure", systemImage: "exclamationmark.triangle") {
85 jump = .failure
86 }
87 .accessibilityIdentifier("build-jump-failure")
88 }
89 Button("Jump to end", systemImage: "arrow.down.to.line") {
90 jump = .end
91 }
92 .accessibilityIdentifier("build-jump-end")
93 Spacer()
94 }
95 .font(.gbSans(.caption))
96 .foregroundStyle(Color.gbAccent)
97 .buttonStyle(.borderless)
98 .padding(.horizontal, 16)
99 .padding(.bottom, 10)
100 }
101
92 private func header(_ build: Build) -> some View { 102 private func header(_ build: Build) -> some View {
93 VStack(alignment: .leading, spacing: 6) { 103 VStack(alignment: .leading, spacing: 6) {
94 HStack(spacing: 8) { 104 HStack(spacing: 8) {
gitbay/Views/Discovery/ExploreView.swift +8 −6
@@ -42,14 +42,16 @@ struct ExploreView: View {
42 var body: some View { 42 var body: some View {
43 List { 43 List {
44 if search.isSearching { 44 if search.isSearching {
45 Picker("Kind", selection: Bindable(search).kind) { 45 Section {
46 ForEach(SearchKind.allCases) { kind in 46 Picker("Kind", selection: Bindable(search).kind) {
47 Text(kind.label).tag(kind) 47 ForEach(SearchKind.allCases) { kind in
48 Text(kind.label).tag(kind)
49 }
48 } 50 }
51 .pickerStyle(.segmented)
52 .listRowBackground(Color.clear)
53 .listRowInsets(EdgeInsets())
49 } 54 }
50 .pickerStyle(.segmented)
51 .listRowBackground(Color.clear)
52 .listRowInsets(EdgeInsets())
53 55
54 ForEach(search.state.value ?? []) { result in 56 ForEach(search.state.value ?? []) { result in
55 searchRow(for: result) 57 searchRow(for: result)
gitbay/Views/Issues/IssueListView.swift +10 −11
@@ -9,7 +9,7 @@ struct IssueListView: View {
9 @State private var draftTitle = "" 9 @State private var draftTitle = ""
10 @State private var draftBody = "" 10 @State private var draftBody = ""
11 @State private var draftFormat = "md" 11 @State private var draftFormat = "md"
12 @State private var createdIssue: Int64? 12 @Environment(\.pushRoute) private var pushRoute
13 13
14 private let client: GitbayClient 14 private let client: GitbayClient
15 15
@@ -21,14 +21,16 @@ struct IssueListView: View {
21 21
22 var body: some View { 22 var body: some View {
23 List { 23 List {
24 Picker("State", selection: Bindable(model).filter.state) { 24 Section {
25 ForEach(IssueFilter.State.allCases) { filter in 25 Picker("State", selection: Bindable(model).filter.state) {
26 Text(filter.label).tag(filter) 26 ForEach(IssueFilter.State.allCases) { filter in
27 Text(filter.label).tag(filter)
28 }
27 } 29 }
30 .pickerStyle(.segmented)
31 .listRowBackground(Color.clear)
32 .listRowInsets(EdgeInsets())
28 } 33 }
29 .pickerStyle(.segmented)
30 .listRowBackground(Color.clear)
31 .listRowInsets(EdgeInsets())
32 34
33 ForEach(model.state.value ?? []) { issue in 35 ForEach(model.state.value ?? []) { issue in
34 NavigationLink(value: IssueRoute.issue(repo: model.repoPath, number: issue.number)) { 36 NavigationLink(value: IssueRoute.issue(repo: model.repoPath, number: issue.number)) {
@@ -101,15 +103,12 @@ struct IssueListView: View {
101 draftBody = "" 103 draftBody = ""
102 draftFormat = "md" 104 draftFormat = "md"
103 composing = false 105 composing = false
104 createdIssue = number 106 pushRoute(IssueRoute.issue(repo: model.repoPath, number: number))
105 await model.load() 107 await model.load()
106 } 108 }
107 } 109 }
108 } 110 }
109 } 111 }
110 .navigationDestination(item: $createdIssue) { number in
111 IssueView(client: client, repo: model.repoPath, number: number)
112 }
113 .task { await model.loadIfNeeded() } 112 .task { await model.loadIfNeeded() }
114 .task { await createModel.loadTemplates() } 113 .task { await createModel.loadTemplates() }
115 .refreshable { await model.load() } 114 .refreshable { await model.load() }
gitbay/Views/Issues/MilestoneListView.swift +8 −6
@@ -16,14 +16,16 @@ struct MilestoneListView: View {
16 16
17 var body: some View { 17 var body: some View {
18 List { 18 List {
19 Picker("State", selection: Bindable(model).filter) { 19 Section {
20 ForEach(MilestoneListViewModel.StateFilter.allCases) { filter in 20 Picker("State", selection: Bindable(model).filter) {
21 Text(filter.rawValue.capitalized).tag(filter) 21 ForEach(MilestoneListViewModel.StateFilter.allCases) { filter in
22 Text(filter.rawValue.capitalized).tag(filter)
23 }
22 } 24 }
25 .pickerStyle(.segmented)
26 .listRowBackground(Color.clear)
27 .listRowInsets(EdgeInsets())
23 } 28 }
24 .pickerStyle(.segmented)
25 .listRowBackground(Color.clear)
26 .listRowInsets(EdgeInsets())
27 29
28 if let error = model.actionError { 30 if let error = model.actionError {
29 Section { 31 Section {
gitbay/Views/MRs/MRListView.swift +8 −6
@@ -14,14 +14,16 @@ struct MRListView: View {
14 14
15 var body: some View { 15 var body: some View {
16 List { 16 List {
17 Picker("State", selection: Bindable(model).filter.state) { 17 Section {
18 ForEach(MRFilter.State.allCases) { filter in 18 Picker("State", selection: Bindable(model).filter.state) {
19 Text(filter.label).tag(filter) 19 ForEach(MRFilter.State.allCases) { filter in
20 Text(filter.label).tag(filter)
21 }
20 } 22 }
23 .pickerStyle(.segmented)
24 .listRowBackground(Color.clear)
25 .listRowInsets(EdgeInsets())
21 } 26 }
22 .pickerStyle(.segmented)
23 .listRowBackground(Color.clear)
24 .listRowInsets(EdgeInsets())
25 27
26 ForEach(model.state.value ?? []) { mr in 28 ForEach(model.state.value ?? []) { mr in
27 NavigationLink(value: MRRoute.mr(repo: model.repoPath, number: mr.number)) { 29 NavigationLink(value: MRRoute.mr(repo: model.repoPath, number: mr.number)) {
gitbay/Views/Repos/RepoView.swift +24 −30
@@ -19,10 +19,9 @@ struct RepoView: View {
19 @State private var confirmingArchive = false 19 @State private var confirmingArchive = false
20 @State private var confirmingFork = false 20 @State private var confirmingFork = false
21 @State private var forkName = "" 21 @State private var forkName = ""
22 @State private var forkDestination: RepoRoute?
23 @Environment(\.dynamicTypeSize) private var typeSize 22 @Environment(\.dynamicTypeSize) private var typeSize
24 @ScaledMetric(relativeTo: .title3) private var statValueHeight: CGFloat = 26 23 @ScaledMetric(relativeTo: .title3) private var statValueHeight: CGFloat = 26
25 @State private var destination: Destination? 24 @Environment(\.pushRoute) private var pushRoute
26 25
27 /// Pushed from buttons rather than NavigationLinks: several share a 26 /// Pushed from buttons rather than NavigationLinks: several share a
28 /// list row, and a link there claims the whole row and adds a chevron. 27 /// list row, and a link there claims the whole row and adds a chevron.
@@ -31,6 +30,24 @@ struct RepoView: View {
31 case history, refs, milestones, labels, releases, wiki, grep 30 case history, refs, milestones, labels, releases, wiki, grep
32 } 31 }
33 32
33 private func open(_ target: Destination) {
34 switch target {
35 case .files: pushRoute(RepoRoute.tree(repo: path, directory: "", ref: nil))
36 case .mergeRequests: pushRoute(MRRoute.list(repo: path))
37 case .issues: pushRoute(IssueRoute.list(repo: path))
38 case .builds: pushRoute(BuildRoute.list(repo: path))
39 case .owner: pushRoute(RepoRoute.profile(owner))
40 case .settings: pushRoute(RepoRoute.settings(repo: path))
41 case .history: pushRoute(RepoRoute.log(repo: path, ref: nil, path: nil))
42 case .refs: pushRoute(RepoRoute.refs(repo: path))
43 case .milestones: pushRoute(RepoRoute.milestones(repo: path))
44 case .labels: pushRoute(RepoRoute.labels(repo: path))
45 case .releases: pushRoute(ReleaseRoute.list(repo: path))
46 case .wiki: pushRoute(RepoRoute.wiki(repo: path))
47 case .grep: pushRoute(RepoRoute.grep(repo: path))
48 }
49 }
50
34 var body: some View { 51 var body: some View {
35 List { 52 List {
36 if let detail = model.state.value { 53 if let detail = model.state.value {
@@ -80,28 +97,6 @@ struct RepoView: View {
80 .toolbar { toolbar } 97 .toolbar { toolbar }
81 .task { await model.load() } 98 .task { await model.load() }
82 .refreshable { await model.load() } 99 .refreshable { await model.load() }
83 .navigationDestination(item: $forkDestination) { route in
84 if case .repo(let forkedPath) = route {
85 RepoView(client: client, path: forkedPath)
86 }
87 }
88 .navigationDestination(item: $destination) { destination in
89 switch destination {
90 case .files: TreeView(client: client, repo: path, directory: "", ref: nil)
91 case .mergeRequests: MRListView(client: client, repo: path)
92 case .issues: IssueListView(client: client, repo: path)
93 case .builds: BuildListView(client: client, repo: path)
94 case .owner: ProfileView(client: client, name: owner)
95 case .settings: RepoSettingsView(client: client, repo: path)
96 case .history: LogView(client: client, repo: path, ref: nil, path: nil)
97 case .refs: RefsView(client: client, repo: path)
98 case .milestones: MilestoneListView(client: client, repo: path)
99 case .labels: LabelListView(client: client, repo: path)
100 case .releases: ReleaseListView(client: client, repo: path)
101 case .wiki: WikiView(client: client, repo: path)
102 case .grep: GrepView(client: client, repo: path)
103 }
104 }
105 .alert("Fork \(path)", isPresented: $confirmingFork) { 100 .alert("Fork \(path)", isPresented: $confirmingFork) {
106 TextField("New name (optional)", text: $forkName) 101 TextField("New name (optional)", text: $forkName)
107 .autocorrectionDisabled() 102 .autocorrectionDisabled()
@@ -109,7 +104,7 @@ struct RepoView: View {
109 Button("Fork") { 104 Button("Fork") {
110 Task { 105 Task {
111 if let result = await actionsModel.fork(named: forkName) { 106 if let result = await actionsModel.fork(named: forkName) {
112 forkDestination = .repo(result.path) 107 pushRoute(RepoRoute.repo(result.path))
113 } 108 }
114 } 109 }
115 } 110 }
@@ -208,7 +203,7 @@ struct RepoView: View {
208 } 203 }
209 Divider() 204 Divider()
210 Button { 205 Button {
211 destination = .settings 206 open(.settings)
212 } label: { 207 } label: {
213 Label("Settings", systemImage: "gearshape") 208 Label("Settings", systemImage: "gearshape")
214 } 209 }
@@ -239,7 +234,7 @@ struct RepoView: View {
239 VStack(alignment: .leading, spacing: 8) { 234 VStack(alignment: .leading, spacing: 8) {
240 HStack(spacing: 6) { 235 HStack(spacing: 6) {
241 HStack(spacing: 0) { 236 HStack(spacing: 0) {
242 Button(owner) { destination = .owner } 237 Button(owner) { open(.owner) }
243 .buttonStyle(.borderless) 238 .buttonStyle(.borderless)
244 .accessibilityIdentifier("repo-owner-link") 239 .accessibilityIdentifier("repo-owner-link")
245 Text("/" + (detail.path.split(separator: "/").last.map(String.init) ?? "")) 240 Text("/" + (detail.path.split(separator: "/").last.map(String.init) ?? ""))
@@ -348,7 +343,7 @@ struct RepoView: View {
348 _ title: String, label: String, value: String? = nil, id: String, 343 _ title: String, label: String, value: String? = nil, id: String,
349 to target: Destination, first: Bool, @ViewBuilder top: () -> some View 344 to target: Destination, first: Bool, @ViewBuilder top: () -> some View
350 ) -> some View { 345 ) -> some View {
351 Button { destination = target } label: { 346 Button { open(target) } label: {
352 VStack(spacing: 2) { 347 VStack(spacing: 2) {
353 top() 348 top()
354 .font(.gbSans(.title3)) 349 .font(.gbSans(.title3))
@@ -409,7 +404,7 @@ struct RepoView: View {
409 } 404 }
410 405
411 private func pill(_ title: String, systemImage: String, to target: Destination) -> some View { 406 private func pill(_ title: String, systemImage: String, to target: Destination) -> some View {
412 Button { destination = target } label: { 407 Button { open(target) } label: {
413 Label { 408 Label {
414 Text(title).lineLimit(1).minimumScaleFactor(0.8) 409 Text(title).lineLimit(1).minimumScaleFactor(0.8)
415 } icon: { 410 } icon: {
@@ -419,7 +414,6 @@ struct RepoView: View {
419 .padding(.horizontal, 10) 414 .padding(.horizontal, 10)
420 .padding(.vertical, 8) 415 .padding(.vertical, 8)
421 .frame(maxWidth: .infinity, alignment: .leading) 416 .frame(maxWidth: .infinity, alignment: .leading)
422 .overlay(RoundedRectangle(cornerRadius: 2).stroke(Color.gbLine, lineWidth: 0.5))
423 .contentShape(Rectangle()) 417 .contentShape(Rectangle())
424 } 418 }
425 .buttonStyle(.plain) 419 .buttonStyle(.plain)
gitbay/Views/Shared/BodyReferences.swift +6 −15
@@ -87,27 +87,18 @@ extension View {
87 87
88private struct ReferenceNavigation: ViewModifier { 88private struct ReferenceNavigation: ViewModifier {
89 89
90 @Environment(SessionStore.self) private var session 90 @Environment(\.pushRoute) private var pushRoute
91 @State private var target: NotificationDestination?
92 91
93 func body(content: Content) -> some View { 92 func body(content: Content) -> some View {
94 content 93 content
95 .environment(\.openURL, OpenURLAction { url in 94 .environment(\.openURL, OpenURLAction { url in
96 guard let destination = referenceDestination(url) else { return .systemAction } 95 guard let destination = referenceDestination(url) else { return .systemAction }
97 target = destination 96 switch destination {
97 case .issue(let repo, let number): pushRoute(IssueRoute.issue(repo: repo, number: number))
98 case .mr(let repo, let number): pushRoute(MRRoute.mr(repo: repo, number: number))
99 case .build(let repo, let number): pushRoute(BuildRoute.detail(repo: repo, number: number))
100 }
98 return .handled 101 return .handled
99 }) 102 })
100 .navigationDestination(item: $target) { destination in
101 if let client = session.client {
102 switch destination {
103 case .issue(let repo, let number):
104 IssueView(client: client, repo: repo, number: number)
105 case .mr(let repo, let number):
106 MRView(client: client, repo: repo, number: number)
107 case .build(let repo, let number):
108 BuildDetailView(client: client, repo: repo, number: number)
109 }
110 }
111 }
112 } 103 }
113} 104}
gitbay/Views/Shared/SidebarRouting.swift +12
@@ -4,6 +4,18 @@ extension EnvironmentValues {
4 /// Set on a list shown as a `NavigationSplitView` sidebar: what it 4 /// Set on a list shown as a `NavigationSplitView` sidebar: what it
5 /// selects opens in the detail column's stack. Nil in a plain stack. 5 /// selects opens in the detail column's stack. Nil in a plain stack.
6 @Entry var sidebarSelection: Binding<AnyHashable?>? 6 @Entry var sidebarSelection: Binding<AnyHashable?>?
7
8 /// Appends a route to the enclosing stack's path. Screens that push
9 /// from a button use it instead of `navigationDestination(item:)`,
10 /// which puts any later value-link push underneath the pushed screen.
11 @Entry var pushRoute: (any Hashable) -> Void = { _ in }
12}
13
14extension View {
15 /// Lets screens in this stack push onto `path` with `pushRoute`.
16 func pushesRoutes(into path: Binding<NavigationPath>) -> some View {
17 environment(\.pushRoute) { path.wrappedValue.append($0) }
18 }
7} 19}
8 20
9/// A `List` whose rows select into `sidebarSelection` when there is one. 21/// A `List` whose rows select into `sidebarSelection` when there is one.