From 21e3cc93619d6a8c33c4ccfe4502f1656e330340 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Sat, 25 Apr 2026 15:30:39 +0200 Subject: [PATCH] feat(ios): rust page background + dashboard switch-server button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scarf/Scarf iOS/App/ScarfGoTabRoot.swift | 13 +++++++- scarf/Scarf iOS/Chat/ChatView.swift | 1 + scarf/Scarf iOS/Dashboard/DashboardView.swift | 31 ++++++++++++++++++- .../Projects/ProjectDetailView.swift | 1 + .../Projects/ProjectSessionsView_iOS.swift | 4 +++ .../Scarf iOS/Projects/ProjectSiteView.swift | 2 ++ .../Scarf iOS/Skills/Hub/HubBrowseView.swift | 4 +++ .../Skills/Installed/SkillDetailView.swift | 11 +++++++ .../Skills/Updates/UpdatesView.swift | 4 +++ 9 files changed, 69 insertions(+), 2 deletions(-) diff --git a/scarf/Scarf iOS/App/ScarfGoTabRoot.swift b/scarf/Scarf iOS/App/ScarfGoTabRoot.swift index 4ee9de6..10add3a 100644 --- a/scarf/Scarf iOS/App/ScarfGoTabRoot.swift +++ b/scarf/Scarf iOS/App/ScarfGoTabRoot.swift @@ -1,6 +1,7 @@ import SwiftUI import ScarfCore import ScarfIOS +import ScarfDesign /// ScarfGo's primary navigation surface. v2.5 expands the original /// 4-tab layout (Chat | Dashboard | Memory | More) to 5 primary tabs @@ -46,7 +47,7 @@ struct ScarfGoTabRoot: View { TabView(selection: $coordinator.selectedTab) { // 1 — Dashboard: stats + recent sessions. NavigationStack { - DashboardView(config: config, key: key) + DashboardView(config: config, key: key, onSoftDisconnect: onSoftDisconnect) } .tabItem { Label("Dashboard", systemImage: "gauge.with.needle") @@ -142,11 +143,14 @@ private struct SystemTab: View { List { Section("Server") { LabeledContent("Host", value: config.host) + .listRowBackground(ScarfColor.backgroundSecondary) if let user = config.user { LabeledContent("User", value: user) + .listRowBackground(ScarfColor.backgroundSecondary) } if let port = config.port { LabeledContent("Port", value: String(port)) + .listRowBackground(ScarfColor.backgroundSecondary) } } @@ -157,18 +161,21 @@ private struct SystemTab: View { Label("Memory", systemImage: "brain.head.profile") } .scarfGoCompactListRow() + .listRowBackground(ScarfColor.backgroundSecondary) NavigationLink { CronListView(config: config) } label: { Label("Cron jobs", systemImage: "clock.arrow.circlepath") } .scarfGoCompactListRow() + .listRowBackground(ScarfColor.backgroundSecondary) NavigationLink { SettingsView(config: config) } label: { Label("Settings", systemImage: "gearshape.fill") } .scarfGoCompactListRow() + .listRowBackground(ScarfColor.backgroundSecondary) } Section { @@ -189,6 +196,7 @@ private struct SystemTab: View { } } .disabled(isDisconnecting || isForgetting) + .listRowBackground(ScarfColor.backgroundSecondary) } 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.") .font(.caption) @@ -209,12 +217,15 @@ private struct SystemTab: View { } } .disabled(isForgetting || isDisconnecting) + .listRowBackground(ScarfColor.backgroundSecondary) } 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.") .font(.caption) } } .scarfGoListDensity() + .scrollContentBackground(.hidden) + .background(ScarfColor.backgroundPrimary) .navigationTitle("System") .navigationBarTitleDisplayMode(.inline) .confirmationDialog( diff --git a/scarf/Scarf iOS/Chat/ChatView.swift b/scarf/Scarf iOS/Chat/ChatView.swift index 42789ae..405fb8b 100644 --- a/scarf/Scarf iOS/Chat/ChatView.swift +++ b/scarf/Scarf iOS/Chat/ChatView.swift @@ -53,6 +53,7 @@ struct ChatView: View { } composer } + .background(ScarfColor.backgroundPrimary.ignoresSafeArea()) .navigationTitle("Chat") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/scarf/Scarf iOS/Dashboard/DashboardView.swift b/scarf/Scarf iOS/Dashboard/DashboardView.swift index 39894ae..d5b291c 100644 --- a/scarf/Scarf iOS/Dashboard/DashboardView.swift +++ b/scarf/Scarf iOS/Dashboard/DashboardView.swift @@ -10,11 +10,17 @@ import ScarfDesign struct DashboardView: View { let config: IOSServerConfig 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 @State private var vm: IOSDashboardViewModel @State private var selectedSection: Section = .overview @State private var sessionProjectFilter: String? = nil + @State private var isDisconnecting = false enum Section: Hashable { case overview, sessions } @@ -24,10 +30,12 @@ struct DashboardView: View { init( config: IOSServerConfig, - key: SSHKeyBundle + key: SSHKeyBundle, + onSoftDisconnect: (@MainActor () async -> Void)? = nil ) { self.config = config self.key = key + self.onSoftDisconnect = onSoftDisconnect let ctx = config.toServerContext(id: Self.contextID) _vm = State(initialValue: IOSDashboardViewModel(context: ctx)) } @@ -53,6 +61,27 @@ struct DashboardView: View { .background(ScarfColor.backgroundPrimary.ignoresSafeArea()) .navigationTitle(config.displayName) .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() } .overlay { if vm.isLoading, vm.recentSessions.isEmpty { diff --git a/scarf/Scarf iOS/Projects/ProjectDetailView.swift b/scarf/Scarf iOS/Projects/ProjectDetailView.swift index c9dbca3..2a74ec2 100644 --- a/scarf/Scarf iOS/Projects/ProjectDetailView.swift +++ b/scarf/Scarf iOS/Projects/ProjectDetailView.swift @@ -67,6 +67,7 @@ struct ProjectDetailView: View { Divider() tabContent } + .background(ScarfColor.backgroundPrimary) .navigationTitle(project.name) .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/scarf/Scarf iOS/Projects/ProjectSessionsView_iOS.swift b/scarf/Scarf iOS/Projects/ProjectSessionsView_iOS.swift index 6f9fdeb..444af37 100644 --- a/scarf/Scarf iOS/Projects/ProjectSessionsView_iOS.swift +++ b/scarf/Scarf iOS/Projects/ProjectSessionsView_iOS.swift @@ -23,6 +23,7 @@ struct ProjectSessionsView_iOS: View { Divider() content } + .background(ScarfColor.backgroundPrimary) .task(id: project.id) { // Rebuild the VM when the project changes so stale state // from a previously-selected project doesn't bleed @@ -116,9 +117,12 @@ struct ProjectSessionsView_iOS: View { } .buttonStyle(.plain) .scarfGoCompactListRow() + .listRowBackground(ScarfColor.backgroundSecondary) } } .scarfGoListDensity() + .scrollContentBackground(.hidden) + .background(ScarfColor.backgroundPrimary) } } diff --git a/scarf/Scarf iOS/Projects/ProjectSiteView.swift b/scarf/Scarf iOS/Projects/ProjectSiteView.swift index f96d5d0..0a54068 100644 --- a/scarf/Scarf iOS/Projects/ProjectSiteView.swift +++ b/scarf/Scarf iOS/Projects/ProjectSiteView.swift @@ -13,5 +13,7 @@ struct ProjectSiteView: View { WebviewWidgetView(widget: widget, fullCanvas: true) .padding(.horizontal, 8) .padding(.vertical, 8) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(ScarfColor.backgroundPrimary) } } diff --git a/scarf/Scarf iOS/Skills/Hub/HubBrowseView.swift b/scarf/Scarf iOS/Skills/Hub/HubBrowseView.swift index 3938a05..525b6f1 100644 --- a/scarf/Scarf iOS/Skills/Hub/HubBrowseView.swift +++ b/scarf/Scarf iOS/Skills/Hub/HubBrowseView.swift @@ -15,6 +15,7 @@ struct HubBrowseView: View { Divider() content } + .background(ScarfColor.backgroundPrimary) } @ViewBuilder @@ -94,9 +95,12 @@ struct HubBrowseView: View { vm.installHubSkill(hubSkill) } .scarfGoCompactListRow() + .listRowBackground(ScarfColor.backgroundSecondary) } } .scarfGoListDensity() + .scrollContentBackground(.hidden) + .background(ScarfColor.backgroundPrimary) } } } diff --git a/scarf/Scarf iOS/Skills/Installed/SkillDetailView.swift b/scarf/Scarf iOS/Skills/Installed/SkillDetailView.swift index e096cb5..3e11b8b 100644 --- a/scarf/Scarf iOS/Skills/Installed/SkillDetailView.swift +++ b/scarf/Scarf iOS/Skills/Installed/SkillDetailView.swift @@ -32,6 +32,7 @@ struct SkillDetailView: View { .foregroundStyle(ScarfColor.foregroundMuted) .textSelection(.enabled) } + .listRowBackground(ScarfColor.backgroundSecondary) // v2.5 design-md prereq banner. Only when this is the // design-md skill AND `which npx` came back missing. @@ -53,6 +54,7 @@ struct SkillDetailView: View { } .padding(.vertical, 4) } + .listRowBackground(ScarfColor.backgroundSecondary) } // v2.5 Spotify auth note. iOS doesn't run the OAuth flow @@ -76,6 +78,7 @@ struct SkillDetailView: View { } .padding(.vertical, 4) } + .listRowBackground(ScarfColor.backgroundSecondary) } // v2.5 SKILL.md frontmatter chip rows. Each section @@ -86,16 +89,19 @@ struct SkillDetailView: View { Section("Allowed tools") { chipRow(tools) } + .listRowBackground(ScarfColor.backgroundSecondary) } if let related = skill.relatedSkills, !related.isEmpty { Section("Related skills") { chipRow(related) } + .listRowBackground(ScarfColor.backgroundSecondary) } if let deps = skill.dependencies, !deps.isEmpty { Section("Dependencies") { chipRow(deps) } + .listRowBackground(ScarfColor.backgroundSecondary) } if !vm.missingConfig.isEmpty { @@ -118,6 +124,7 @@ struct SkillDetailView: View { } .padding(.vertical, 6) } + .listRowBackground(ScarfColor.backgroundSecondary) } if !skill.files.isEmpty { @@ -139,6 +146,7 @@ struct SkillDetailView: View { } .buttonStyle(.plain) .scarfGoCompactListRow() + .listRowBackground(ScarfColor.backgroundSecondary) } } } @@ -159,9 +167,12 @@ struct SkillDetailView: View { .textSelection(.enabled) } } + .listRowBackground(ScarfColor.backgroundSecondary) } } .scarfGoListDensity() + .scrollContentBackground(.hidden) + .background(ScarfColor.backgroundPrimary) .navigationTitle(skill.name) .navigationBarTitleDisplayMode(.inline) .task { diff --git a/scarf/Scarf iOS/Skills/Updates/UpdatesView.swift b/scarf/Scarf iOS/Skills/Updates/UpdatesView.swift index 3c5854c..1e53936 100644 --- a/scarf/Scarf iOS/Skills/Updates/UpdatesView.swift +++ b/scarf/Scarf iOS/Skills/Updates/UpdatesView.swift @@ -15,6 +15,7 @@ struct UpdatesView: View { Divider() content } + .background(ScarfColor.backgroundPrimary) } @ViewBuilder @@ -80,9 +81,12 @@ struct UpdatesView: View { } .padding(.vertical, 4) .scarfGoCompactListRow() + .listRowBackground(ScarfColor.backgroundSecondary) } } .scarfGoListDensity() + .scrollContentBackground(.hidden) + .background(ScarfColor.backgroundPrimary) } } }