mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
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:
@@ -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(
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
composer
|
composer
|
||||||
}
|
}
|
||||||
|
.background(ScarfColor.backgroundPrimary.ignoresSafeArea())
|
||||||
.navigationTitle("Chat")
|
.navigationTitle("Chat")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user