Simplify iOS network add flow

This commit is contained in:
Conrad Kramer 2026-03-31 13:40:13 -07:00
parent 35f3b3ce4e
commit 36a54628ba
2 changed files with 181 additions and 42 deletions

View file

@ -18,10 +18,13 @@ public struct BurrowView: View {
Text("Burrow") Text("Burrow")
.font(.largeTitle) .font(.largeTitle)
.fontWeight(.bold) .fontWeight(.bold)
if showsHeaderSubtitle {
Text("Networks and accounts") Text("Networks and accounts")
.font(.headline) .font(.headline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
}
if showsToolbarAddMenu {
Spacer() Spacer()
Menu { Menu {
Button("Add WireGuard Network") { Button("Add WireGuard Network") {
@ -39,12 +42,19 @@ public struct BurrowView: View {
.accessibilityLabel("Add") .accessibilityLabel("Add")
} }
} }
}
.padding(.top) .padding(.top)
if showsInlineQuickActions {
quickAddSection
}
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
sectionHeader( sectionHeader(
title: "Networks", title: "Networks",
detail: "Stored daemon networks and their active account selectors" detail: showsInlineQuickActions
? nil
: "Stored daemon networks and their active account selectors"
) )
if let connectionError = networkViewModel.connectionError { if let connectionError = networkViewModel.connectionError {
Text(connectionError) Text(connectionError)
@ -54,10 +64,13 @@ public struct BurrowView: View {
NetworkCarouselView(networks: networkViewModel.cards) NetworkCarouselView(networks: networkViewModel.cards)
} }
if showsAccountsSection {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
sectionHeader( sectionHeader(
title: "Accounts", title: "Accounts",
detail: "Per-network identities and sign-in state" detail: showsInlineQuickActions
? nil
: "Per-network identities and sign-in state"
) )
if accountStore.accounts.isEmpty { if accountStore.accounts.isEmpty {
ContentUnavailableView( ContentUnavailableView(
@ -77,11 +90,12 @@ public struct BurrowView: View {
} }
} }
} }
}
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
sectionHeader( sectionHeader(
title: "Tunnel", title: "Tunnel",
detail: "Current system extension state" detail: showsInlineQuickActions ? nil : "Current system extension state"
) )
TunnelStatusView() TunnelStatusView()
TunnelButton() TunnelButton()
@ -120,18 +134,58 @@ public struct BurrowView: View {
} }
@ViewBuilder @ViewBuilder
private func sectionHeader(title: String, detail: String) -> some View { private var quickAddSection: some View {
VStack(alignment: .leading, spacing: 12) {
sectionHeader(title: "Add", detail: nil)
VStack(spacing: 12) {
ForEach(ConfigurationSheet.allCases) { sheet in
QuickAddButton(sheet: sheet) {
activeSheet = sheet
}
}
}
}
}
@ViewBuilder
private func sectionHeader(title: String, detail: String?) -> some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(title) Text(title)
.font(.title2.weight(.semibold)) .font(.title2.weight(.semibold))
if let detail, !detail.isEmpty {
Text(detail) Text(detail)
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
}
private var showsInlineQuickActions: Bool {
#if os(iOS)
true
#else
false
#endif
}
private var showsToolbarAddMenu: Bool {
!showsInlineQuickActions
}
private var showsHeaderSubtitle: Bool {
!showsInlineQuickActions
}
private var showsAccountsSection: Bool {
#if os(iOS)
!accountStore.accounts.isEmpty
#else
true
#endif
}
} }
private enum ConfigurationSheet: String, Identifiable { private enum ConfigurationSheet: String, CaseIterable, Identifiable {
case wireGuard case wireGuard
case tor case tor
case tailnet case tailnet
@ -145,6 +199,75 @@ private enum ConfigurationSheet: String, Identifiable {
case .tailnet: .headscale case .tailnet: .headscale
} }
} }
var iconName: String {
switch self {
case .wireGuard:
"wave.3.right"
case .tor:
"shield.lefthalf.filled.badge.checkmark"
case .tailnet:
"network.badge.shield.half.filled"
}
}
var quickActionTitle: String {
switch self {
case .wireGuard:
"WireGuard"
case .tor:
"Tor"
case .tailnet:
"Tailnet"
}
}
var quickActionSubtitle: String {
switch self {
case .wireGuard:
"Import a tunnel"
case .tor:
"Save an Arti profile"
case .tailnet:
"Sign in or save a control plane"
}
}
var quickActionColor: Color {
switch self {
case .wireGuard:
.blue
case .tor, .tailnet:
kind.accentColor
}
}
}
private struct QuickAddButton: View {
let sheet: ConfigurationSheet
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
Image(systemName: sheet.iconName)
.font(.title3.weight(.semibold))
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
Text(sheet.quickActionTitle)
.font(.headline)
Text(sheet.quickActionSubtitle)
.font(.caption)
.opacity(0.88)
}
Spacer()
}
.frame(maxWidth: .infinity, minHeight: 64, alignment: .leading)
}
.buttonStyle(.floating(color: sheet.quickActionColor, cornerRadius: 18))
}
} }
private struct AccountDraft { private struct AccountDraft {

View file

@ -6,12 +6,28 @@ struct NetworkCarouselView: View {
var body: some View { var body: some View {
Group { Group {
if networks.isEmpty { if networks.isEmpty {
#if os(iOS)
VStack(alignment: .leading, spacing: 6) {
Text("No stored networks yet")
.font(.headline)
Text("WireGuard and Tailnet networks show up here as soon as you add one.")
.font(.footnote)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(
RoundedRectangle(cornerRadius: 18)
.fill(.thinMaterial)
)
#else
ContentUnavailableView( ContentUnavailableView(
"No Networks Yet", "No Networks Yet",
systemImage: "network.slash", systemImage: "network.slash",
description: Text("Add a WireGuard network, or save a Tailnet account so Burrow can store a managed network when the daemon is reachable.") description: Text("Add a WireGuard network, or save a Tailnet account so Burrow can store a managed network when the daemon is reachable.")
) )
.frame(maxWidth: .infinity, minHeight: 175) .frame(maxWidth: .infinity, minHeight: 175)
#endif
} else { } else {
ScrollView(.horizontal) { ScrollView(.horizontal) {
LazyHStack { LazyHStack {