List screen: ditch nav bar, build flush top chrome

The system nav bar floated low on iOS 26 and left a wedge of black above the
title. Replaced it with a custom top row pinned to the safe-area top: gear ⟶
big "Screens" wordmark ⟶ +. Search bar lives directly beneath, sections start
right after — no centered-in-the-void layout. Background gets a subtle blue
radial bloom so the floating glass buttons have something to anchor to.

Saved-empty state is now a glass card with an icon and a gradient CTA button.
Connection rows are full-width glass cards with rounded corners; long-press
gives Edit / Open in New Window / Delete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-16 21:19:41 -05:00
parent 333c08724f
commit fcdd19ceb9

View File

@@ -19,51 +19,30 @@ public struct ConnectionListView: View {
public var body: some View { public var body: some View {
NavigationStack(path: $path) { NavigationStack(path: $path) {
List { ZStack(alignment: .top) {
if !discovery.hosts.isEmpty || discovery.isBrowsing { backgroundGradient.ignoresSafeArea()
discoveredSection
} ScrollView {
savedSection VStack(spacing: 22) {
} topChrome
#if os(iOS)
.listStyle(.insetGrouped) if !discovery.hosts.isEmpty || discovery.isBrowsing {
.scrollContentBackground(.hidden) discoveredSection
#endif }
.softScrollEdge()
.background(backgroundGradient.ignoresSafeArea()) savedSection
.navigationTitle("Screens")
#if os(iOS)
.searchable(text: $search,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search connections")
#else
.searchable(text: $search, prompt: "Search connections")
#endif
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
addPrefill = nil
showingAdd = true
} label: {
Image(systemName: "plus")
} }
.accessibilityLabel("Add connection") .padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 32)
} }
#if os(iOS) #if os(iOS)
ToolbarItem(placement: .topBarLeading) { .scrollIndicators(.hidden)
Button {
showingSettings = true
} label: {
Image(systemName: "gear")
}
.accessibilityLabel("Settings")
}
#else
ToolbarItem {
Button { showingSettings = true } label: { Image(systemName: "gear") }
}
#endif #endif
} }
#if os(iOS)
.toolbar(.hidden, for: .navigationBar)
#endif
.sheet(isPresented: $showingAdd) { .sheet(isPresented: $showingAdd) {
AddConnectionView(prefill: addPrefill) AddConnectionView(prefill: addPrefill)
} }
@@ -90,143 +69,279 @@ public struct ConnectionListView: View {
} }
} }
// MARK: Background
private var backgroundGradient: some View { private var backgroundGradient: some View {
LinearGradient( ZStack {
colors: [ LinearGradient(
Color(red: 0.04, green: 0.05, blue: 0.10), colors: [
Color(red: 0.02, green: 0.02, blue: 0.05) Color(red: 0.06, green: 0.08, blue: 0.16),
], Color(red: 0.02, green: 0.02, blue: 0.06)
startPoint: .top, ],
endPoint: .bottom startPoint: .top,
) endPoint: .bottom
)
// soft accent bloom up top so the floating buttons feel anchored
RadialGradient(
colors: [Color.blue.opacity(0.22), .clear],
center: .init(x: 0.5, y: 0.0),
startRadius: 20,
endRadius: 320
)
.blendMode(.screen)
.allowsHitTesting(false)
}
} }
// MARK: Top chrome (custom replaces nav bar)
private var topChrome: some View {
VStack(spacing: 14) {
HStack(alignment: .center) {
circularGlassButton(systemName: "gearshape", label: "Settings") {
showingSettings = true
}
Spacer()
Text("Screens")
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundStyle(.white)
Spacer()
circularGlassButton(systemName: "plus", label: "Add connection") {
addPrefill = nil
showingAdd = true
}
}
searchField
}
.padding(.top, 4)
}
private func circularGlassButton(systemName: String,
label: String,
action: @escaping () -> Void) -> some View {
Button(action: action) {
Image(systemName: systemName)
.font(.callout.weight(.semibold))
.foregroundStyle(.white)
.frame(width: 40, height: 40)
}
.buttonStyle(.plain)
.glassSurface(in: Circle())
.accessibilityLabel(label)
}
private var searchField: some View {
HStack(spacing: 10) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.white.opacity(0.65))
TextField("", text: $search, prompt: Text("Search connections")
.foregroundStyle(.white.opacity(0.45)))
.textFieldStyle(.plain)
.foregroundStyle(.white)
#if os(iOS)
.textInputAutocapitalization(.never)
#endif
.autocorrectionDisabled()
.submitLabel(.search)
if !search.isEmpty {
Button {
search = ""
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.white.opacity(0.45))
}
.buttonStyle(.plain)
.accessibilityLabel("Clear search")
}
}
.padding(.horizontal, 14)
.padding(.vertical, 11)
.glassSurface(in: Capsule())
}
// MARK: Discovered
private var discoveredSection: some View { private var discoveredSection: some View {
Section { VStack(alignment: .leading, spacing: 10) {
if discovery.hosts.isEmpty { sectionHeader("On this network",
HStack(spacing: 12) { systemImage: "antenna.radiowaves.left.and.right")
ProgressView().controlSize(.small)
Text("Looking for computers on this network…") VStack(spacing: 10) {
.font(.callout) if discovery.hosts.isEmpty {
.foregroundStyle(.secondary) HStack(spacing: 12) {
} ProgressView().controlSize(.small).tint(.white)
.padding(.vertical, 6) Text("Looking for computers…")
} else { .font(.callout)
ForEach(discovery.hosts) { host in .foregroundStyle(.white.opacity(0.7))
Button { Spacer()
Task { await prepareDiscoveredHost(host) } }
} label: { .padding(16)
HStack(spacing: 14) { .glassSurface(in: RoundedRectangle(cornerRadius: 18, style: .continuous))
ZStack { } else {
Circle() ForEach(discovery.hosts) { host in
.fill(.tint.opacity(0.15)) Button {
.frame(width: 38, height: 38) Task { await prepareDiscoveredHost(host) }
Image(systemName: "wifi") } label: {
.font(.callout) HStack(spacing: 14) {
.foregroundStyle(.tint) ZStack {
} Circle()
VStack(alignment: .leading, spacing: 2) { .fill(Color.blue.opacity(0.25))
Text(host.displayName) .frame(width: 38, height: 38)
.font(.headline) Image(systemName: "wifi")
.foregroundStyle(.primary) .font(.callout)
Text(serviceLabel(host.serviceType)) .foregroundStyle(.blue)
.font(.caption) }
.foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 2) {
} Text(host.displayName)
Spacer() .font(.headline)
if resolvingHostID == host.id { .foregroundStyle(.white)
ProgressView().controlSize(.small) Text(serviceLabel(host.serviceType))
} else { .font(.caption)
Image(systemName: "plus.circle.fill") .foregroundStyle(.white.opacity(0.6))
.font(.title3) }
.foregroundStyle(.tint) Spacer()
} if resolvingHostID == host.id {
} ProgressView().controlSize(.small).tint(.white)
.padding(.vertical, 4) } else {
.contentShape(Rectangle()) Image(systemName: "plus.circle.fill")
.font(.title3)
.foregroundStyle(.blue)
}
}
.padding(14)
}
.buttonStyle(.plain)
.glassSurface(in: RoundedRectangle(cornerRadius: 18, style: .continuous))
.disabled(resolvingHostID != nil)
} }
.buttonStyle(.plain)
.disabled(resolvingHostID != nil)
} }
} }
} header: {
Label("On this network", systemImage: "antenna.radiowaves.left.and.right")
.font(.footnote.weight(.semibold))
.textCase(nil)
.foregroundStyle(.secondary)
} }
} }
// MARK: Saved
private var savedSection: some View { private var savedSection: some View {
Section { VStack(alignment: .leading, spacing: 10) {
sectionHeader("Saved", systemImage: "bookmark.fill")
if filteredConnections.isEmpty { if filteredConnections.isEmpty {
if connections.isEmpty { if connections.isEmpty {
ContentUnavailableView { emptySavedState
Label("No saved connections", systemImage: "display")
} description: {
Text("Tap the + button above to add a Mac, Linux box, or Raspberry Pi.")
} actions: {
Button {
addPrefill = nil
showingAdd = true
} label: {
Label("Add a computer", systemImage: "plus")
}
.glassButton(prominent: true)
}
.padding(.vertical, 10)
} else { } else {
ContentUnavailableView.search(text: search) noSearchResultsState
} }
} else { } else {
ForEach(filteredConnections) { connection in VStack(spacing: 10) {
NavigationLink(value: SessionRoute(connectionID: connection.id)) { ForEach(filteredConnections) { connection in
ConnectionCard(connection: connection) connectionRow(connection)
}
.contextMenu {
Button {
editingConnection = connection
} label: {
Label("Edit", systemImage: "pencil")
}
#if os(iOS)
Button {
openWindow(value: connection.id)
} label: {
Label("Open in New Window", systemImage: "rectangle.stack.badge.plus")
}
#endif
Button(role: .destructive) {
delete(connection)
} label: {
Label("Delete", systemImage: "trash")
}
}
.swipeActions(edge: .leading) {
Button {
editingConnection = connection
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.blue)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
delete(connection)
} label: {
Label("Delete", systemImage: "trash")
}
} }
} }
} }
} header: {
Label("Saved", systemImage: "bookmark")
.font(.footnote.weight(.semibold))
.textCase(nil)
.foregroundStyle(.secondary)
} }
} }
private var emptySavedState: some View {
VStack(spacing: 14) {
ZStack {
Circle()
.fill(LinearGradient(colors: [.blue.opacity(0.4), .purple.opacity(0.3)],
startPoint: .topLeading,
endPoint: .bottomTrailing))
.frame(width: 64, height: 64)
Image(systemName: "display")
.font(.title.weight(.semibold))
.foregroundStyle(.white)
}
Text("No saved connections")
.font(.headline)
.foregroundStyle(.white)
Text("Tap the + button up top to add a Mac, Linux box, or Raspberry Pi.")
.font(.callout)
.foregroundStyle(.white.opacity(0.65))
.multilineTextAlignment(.center)
.padding(.horizontal, 8)
Button {
addPrefill = nil
showingAdd = true
} label: {
Label("Add a computer", systemImage: "plus")
.font(.callout.weight(.semibold))
.padding(.horizontal, 18)
.padding(.vertical, 12)
}
.buttonStyle(.plain)
.background(
Capsule().fill(LinearGradient(colors: [.blue, .purple],
startPoint: .leading,
endPoint: .trailing))
)
.foregroundStyle(.white)
}
.frame(maxWidth: .infinity)
.padding(28)
.glassSurface(in: RoundedRectangle(cornerRadius: 22, style: .continuous))
}
private var noSearchResultsState: some View {
VStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.font(.title)
.foregroundStyle(.white.opacity(0.5))
Text("No matches for \"\(search)\"")
.font(.callout)
.foregroundStyle(.white.opacity(0.65))
}
.frame(maxWidth: .infinity)
.padding(24)
.glassSurface(in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
private func connectionRow(_ connection: SavedConnection) -> some View {
NavigationLink(value: SessionRoute(connectionID: connection.id)) {
ConnectionCard(connection: connection)
.padding(14)
}
.buttonStyle(.plain)
.glassSurface(in: RoundedRectangle(cornerRadius: 18, style: .continuous))
.contextMenu {
Button {
editingConnection = connection
} label: {
Label("Edit", systemImage: "pencil")
}
#if os(iOS)
Button {
openWindow(value: connection.id)
} label: {
Label("Open in New Window", systemImage: "rectangle.stack.badge.plus")
}
#endif
Divider()
Button(role: .destructive) {
delete(connection)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
// MARK: Helpers
private func sectionHeader(_ title: String, systemImage: String) -> some View {
HStack(spacing: 8) {
Image(systemName: systemImage)
.font(.caption.weight(.semibold))
Text(title)
.font(.subheadline.weight(.semibold))
.textCase(.uppercase)
.tracking(0.5)
}
.foregroundStyle(.white.opacity(0.55))
.padding(.horizontal, 4)
}
private var filteredConnections: [SavedConnection] { private var filteredConnections: [SavedConnection] {
let trimmed = search.trimmingCharacters(in: .whitespaces) let trimmed = search.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return connections } guard !trimmed.isEmpty else { return connections }