feat(ios): rust page background + dashboard switch-server button

Sweeps the rust ScarfDesign page background onto the screens that
were still rendering against the iOS default: Skills/Hub, Skills/Updates,
all three project sub-views, and Skill Detail. Lists adopt
.scrollContentBackground(.hidden) + ScarfColor.backgroundPrimary, with
.listRowBackground(ScarfColor.backgroundSecondary) on rows so the
Mac-style elevated-card density carries through.

Adds a "Switch server" toolbar button to Dashboard's top-right, threaded
through ScarfGoTabRoot from the connected-server host. One tap soft-
disconnects and returns to the server list — same code path the System
tab already exposes, just reachable without first navigating away from
Dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-25 15:30:39 +02:00
parent 295f2dfefc
commit 21e3cc9361
9 changed files with 69 additions and 2 deletions
+12 -1
View File
@@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfIOS import ScarfIOS
import ScarfDesign
/// ScarfGo's primary navigation surface. v2.5 expands the original /// ScarfGo's primary navigation surface. v2.5 expands the original
/// 4-tab layout (Chat | Dashboard | Memory | More) to 5 primary tabs /// 4-tab layout (Chat | Dashboard | Memory | More) to 5 primary tabs
@@ -46,7 +47,7 @@ struct ScarfGoTabRoot: View {
TabView(selection: $coordinator.selectedTab) { TabView(selection: $coordinator.selectedTab) {
// 1 Dashboard: stats + recent sessions. // 1 Dashboard: stats + recent sessions.
NavigationStack { NavigationStack {
DashboardView(config: config, key: key) DashboardView(config: config, key: key, onSoftDisconnect: onSoftDisconnect)
} }
.tabItem { .tabItem {
Label("Dashboard", systemImage: "gauge.with.needle") Label("Dashboard", systemImage: "gauge.with.needle")
@@ -142,11 +143,14 @@ private struct SystemTab: View {
List { List {
Section("Server") { Section("Server") {
LabeledContent("Host", value: config.host) LabeledContent("Host", value: config.host)
.listRowBackground(ScarfColor.backgroundSecondary)
if let user = config.user { if let user = config.user {
LabeledContent("User", value: user) LabeledContent("User", value: user)
.listRowBackground(ScarfColor.backgroundSecondary)
} }
if let port = config.port { if let port = config.port {
LabeledContent("Port", value: String(port)) LabeledContent("Port", value: String(port))
.listRowBackground(ScarfColor.backgroundSecondary)
} }
} }
@@ -157,18 +161,21 @@ private struct SystemTab: View {
Label("Memory", systemImage: "brain.head.profile") Label("Memory", systemImage: "brain.head.profile")
} }
.scarfGoCompactListRow() .scarfGoCompactListRow()
.listRowBackground(ScarfColor.backgroundSecondary)
NavigationLink { NavigationLink {
CronListView(config: config) CronListView(config: config)
} label: { } label: {
Label("Cron jobs", systemImage: "clock.arrow.circlepath") Label("Cron jobs", systemImage: "clock.arrow.circlepath")
} }
.scarfGoCompactListRow() .scarfGoCompactListRow()
.listRowBackground(ScarfColor.backgroundSecondary)
NavigationLink { NavigationLink {
SettingsView(config: config) SettingsView(config: config)
} label: { } label: {
Label("Settings", systemImage: "gearshape.fill") Label("Settings", systemImage: "gearshape.fill")
} }
.scarfGoCompactListRow() .scarfGoCompactListRow()
.listRowBackground(ScarfColor.backgroundSecondary)
} }
Section { Section {
@@ -189,6 +196,7 @@ private struct SystemTab: View {
} }
} }
.disabled(isDisconnecting || isForgetting) .disabled(isDisconnecting || isForgetting)
.listRowBackground(ScarfColor.backgroundSecondary)
} footer: { } footer: {
Text("Closes the live connection. Your key and host details stay on this device; tapping the server from the list reconnects with no re-onboarding.") Text("Closes the live connection. Your key and host details stay on this device; tapping the server from the list reconnects with no re-onboarding.")
.font(.caption) .font(.caption)
@@ -209,12 +217,15 @@ private struct SystemTab: View {
} }
} }
.disabled(isForgetting || isDisconnecting) .disabled(isForgetting || isDisconnecting)
.listRowBackground(ScarfColor.backgroundSecondary)
} footer: { } footer: {
Text("Removes this server's SSH key and host info from the device. You'll need to add the public key back to `~/.ssh/authorized_keys` to reconnect.") Text("Removes this server's SSH key and host info from the device. You'll need to add the public key back to `~/.ssh/authorized_keys` to reconnect.")
.font(.caption) .font(.caption)
} }
} }
.scarfGoListDensity() .scarfGoListDensity()
.scrollContentBackground(.hidden)
.background(ScarfColor.backgroundPrimary)
.navigationTitle("System") .navigationTitle("System")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.confirmationDialog( .confirmationDialog(
+1
View File
@@ -53,6 +53,7 @@ struct ChatView: View {
} }
composer composer
} }
.background(ScarfColor.backgroundPrimary.ignoresSafeArea())
.navigationTitle("Chat") .navigationTitle("Chat")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
+30 -1
View File
@@ -10,11 +10,17 @@ import ScarfDesign
struct DashboardView: View { struct DashboardView: View {
let config: IOSServerConfig let config: IOSServerConfig
let key: SSHKeyBundle let key: SSHKeyBundle
/// Soft-disconnect closure threaded down from the connected-server
/// host. Surfaced in the nav bar as a "Switch server" button so
/// users can hop back to the server list without first navigating
/// to the System tab.
let onSoftDisconnect: (@MainActor () async -> Void)?
@Environment(\.scarfGoCoordinator) private var coordinator @Environment(\.scarfGoCoordinator) private var coordinator
@State private var vm: IOSDashboardViewModel @State private var vm: IOSDashboardViewModel
@State private var selectedSection: Section = .overview @State private var selectedSection: Section = .overview
@State private var sessionProjectFilter: String? = nil @State private var sessionProjectFilter: String? = nil
@State private var isDisconnecting = false
enum Section: Hashable { case overview, sessions } enum Section: Hashable { case overview, sessions }
@@ -24,10 +30,12 @@ struct DashboardView: View {
init( init(
config: IOSServerConfig, config: IOSServerConfig,
key: SSHKeyBundle key: SSHKeyBundle,
onSoftDisconnect: (@MainActor () async -> Void)? = nil
) { ) {
self.config = config self.config = config
self.key = key self.key = key
self.onSoftDisconnect = onSoftDisconnect
let ctx = config.toServerContext(id: Self.contextID) let ctx = config.toServerContext(id: Self.contextID)
_vm = State(initialValue: IOSDashboardViewModel(context: ctx)) _vm = State(initialValue: IOSDashboardViewModel(context: ctx))
} }
@@ -53,6 +61,27 @@ struct DashboardView: View {
.background(ScarfColor.backgroundPrimary.ignoresSafeArea()) .background(ScarfColor.backgroundPrimary.ignoresSafeArea())
.navigationTitle(config.displayName) .navigationTitle(config.displayName)
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
.toolbar {
if let onSoftDisconnect {
ToolbarItem(placement: .topBarTrailing) {
Button {
Task {
isDisconnecting = true
await onSoftDisconnect()
}
} label: {
if isDisconnecting {
ProgressView()
} else {
Label("Switch server", systemImage: "rectangle.portrait.and.arrow.right")
}
}
.disabled(isDisconnecting)
.accessibilityLabel("Switch server")
.accessibilityHint("Disconnects from this server and returns to the server list")
}
}
}
.refreshable { await vm.refresh() } .refreshable { await vm.refresh() }
.overlay { .overlay {
if vm.isLoading, vm.recentSessions.isEmpty { if vm.isLoading, vm.recentSessions.isEmpty {
@@ -67,6 +67,7 @@ struct ProjectDetailView: View {
Divider() Divider()
tabContent tabContent
} }
.background(ScarfColor.backgroundPrimary)
.navigationTitle(project.name) .navigationTitle(project.name)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
@@ -23,6 +23,7 @@ struct ProjectSessionsView_iOS: View {
Divider() Divider()
content content
} }
.background(ScarfColor.backgroundPrimary)
.task(id: project.id) { .task(id: project.id) {
// Rebuild the VM when the project changes so stale state // Rebuild the VM when the project changes so stale state
// from a previously-selected project doesn't bleed // from a previously-selected project doesn't bleed
@@ -116,9 +117,12 @@ struct ProjectSessionsView_iOS: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.scarfGoCompactListRow() .scarfGoCompactListRow()
.listRowBackground(ScarfColor.backgroundSecondary)
} }
} }
.scarfGoListDensity() .scarfGoListDensity()
.scrollContentBackground(.hidden)
.background(ScarfColor.backgroundPrimary)
} }
} }
@@ -13,5 +13,7 @@ struct ProjectSiteView: View {
WebviewWidgetView(widget: widget, fullCanvas: true) WebviewWidgetView(widget: widget, fullCanvas: true)
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 8) .padding(.vertical, 8)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(ScarfColor.backgroundPrimary)
} }
} }
@@ -15,6 +15,7 @@ struct HubBrowseView: View {
Divider() Divider()
content content
} }
.background(ScarfColor.backgroundPrimary)
} }
@ViewBuilder @ViewBuilder
@@ -94,9 +95,12 @@ struct HubBrowseView: View {
vm.installHubSkill(hubSkill) vm.installHubSkill(hubSkill)
} }
.scarfGoCompactListRow() .scarfGoCompactListRow()
.listRowBackground(ScarfColor.backgroundSecondary)
} }
} }
.scarfGoListDensity() .scarfGoListDensity()
.scrollContentBackground(.hidden)
.background(ScarfColor.backgroundPrimary)
} }
} }
} }
@@ -32,6 +32,7 @@ struct SkillDetailView: View {
.foregroundStyle(ScarfColor.foregroundMuted) .foregroundStyle(ScarfColor.foregroundMuted)
.textSelection(.enabled) .textSelection(.enabled)
} }
.listRowBackground(ScarfColor.backgroundSecondary)
// v2.5 design-md prereq banner. Only when this is the // v2.5 design-md prereq banner. Only when this is the
// design-md skill AND `which npx` came back missing. // design-md skill AND `which npx` came back missing.
@@ -53,6 +54,7 @@ struct SkillDetailView: View {
} }
.padding(.vertical, 4) .padding(.vertical, 4)
} }
.listRowBackground(ScarfColor.backgroundSecondary)
} }
// v2.5 Spotify auth note. iOS doesn't run the OAuth flow // v2.5 Spotify auth note. iOS doesn't run the OAuth flow
@@ -76,6 +78,7 @@ struct SkillDetailView: View {
} }
.padding(.vertical, 4) .padding(.vertical, 4)
} }
.listRowBackground(ScarfColor.backgroundSecondary)
} }
// v2.5 SKILL.md frontmatter chip rows. Each section // v2.5 SKILL.md frontmatter chip rows. Each section
@@ -86,16 +89,19 @@ struct SkillDetailView: View {
Section("Allowed tools") { Section("Allowed tools") {
chipRow(tools) chipRow(tools)
} }
.listRowBackground(ScarfColor.backgroundSecondary)
} }
if let related = skill.relatedSkills, !related.isEmpty { if let related = skill.relatedSkills, !related.isEmpty {
Section("Related skills") { Section("Related skills") {
chipRow(related) chipRow(related)
} }
.listRowBackground(ScarfColor.backgroundSecondary)
} }
if let deps = skill.dependencies, !deps.isEmpty { if let deps = skill.dependencies, !deps.isEmpty {
Section("Dependencies") { Section("Dependencies") {
chipRow(deps) chipRow(deps)
} }
.listRowBackground(ScarfColor.backgroundSecondary)
} }
if !vm.missingConfig.isEmpty { if !vm.missingConfig.isEmpty {
@@ -118,6 +124,7 @@ struct SkillDetailView: View {
} }
.padding(.vertical, 6) .padding(.vertical, 6)
} }
.listRowBackground(ScarfColor.backgroundSecondary)
} }
if !skill.files.isEmpty { if !skill.files.isEmpty {
@@ -139,6 +146,7 @@ struct SkillDetailView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.scarfGoCompactListRow() .scarfGoCompactListRow()
.listRowBackground(ScarfColor.backgroundSecondary)
} }
} }
} }
@@ -159,9 +167,12 @@ struct SkillDetailView: View {
.textSelection(.enabled) .textSelection(.enabled)
} }
} }
.listRowBackground(ScarfColor.backgroundSecondary)
} }
} }
.scarfGoListDensity() .scarfGoListDensity()
.scrollContentBackground(.hidden)
.background(ScarfColor.backgroundPrimary)
.navigationTitle(skill.name) .navigationTitle(skill.name)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.task { .task {
@@ -15,6 +15,7 @@ struct UpdatesView: View {
Divider() Divider()
content content
} }
.background(ScarfColor.backgroundPrimary)
} }
@ViewBuilder @ViewBuilder
@@ -80,9 +81,12 @@ struct UpdatesView: View {
} }
.padding(.vertical, 4) .padding(.vertical, 4)
.scarfGoCompactListRow() .scarfGoCompactListRow()
.listRowBackground(ScarfColor.backgroundSecondary)
} }
} }
.scarfGoListDensity() .scarfGoListDensity()
.scrollContentBackground(.hidden)
.background(ScarfColor.backgroundPrimary)
} }
} }
} }