polish: tokenize remaining sheets, page headers, and widgets

Phase 1 — Page headers for the 9 non-mockup feature views: Skills,
Gateway, Platforms, Personalities, QuickCommands, CredentialPools,
Plugins, Webhooks, Profiles. Each now ships a ScarfPageHeader with
title + subtitle + tokenized trailing actions (ScarfPrimary /
Secondary / Ghost buttons), wrapped in .fixedSize so labels can't
wrap at narrow widths. Outer .background(ScarfColor.backgroundPrimary).

Phase 2 — Modal sheets: ModelPickerSheet, NousSignInSheet,
RenameProjectSheet, MoveToFolderSheet, the five Templates sheets
(TemplateInstall / TemplateConfig / TemplateExport / TemplateUninstall
/ ConfigEditorSheet), three MCPServer sheets (AddCustom / Editor /
PresetPicker), AddServerSheet, ManageServersView, MissingServerView.
.font(.headline) -> .scarfStyle(.headline);
.buttonStyle(.borderedProminent) -> ScarfPrimaryButton(); raw text
fields where touched -> ScarfTextField; cancel buttons -> ScarfGhostButton.

Phase 3 — All 12 platform setup views (Discord / Email / Feishu /
HomeAssistant / IMessage / Matrix / Mattermost / Signal / Slack /
Telegram / Webhook / WhatsApp). Connect buttons swapped to
ScarfPrimaryButton.

Phase 4 — All 7 project dashboard widgets (Chart / List / Progress /
Stat / Table / Text / Webview). .font(.caption) -> .scarfStyle(.caption);
.background(.quaternary.opacity(0.5)) -> ScarfColor.backgroundSecondary;
RoundedRectangle(cornerRadius: 8) -> ScarfRadius.lg.

Phase 5 — Project sub-views: ProjectSessionsView, ProjectsSidebar,
ProjectSlashCommandsView. Same token sweep.

Phase 6 — Common chrome:
- LoadingOverlay: .font(.callout/caption) -> .scarfStyle; secondary
  foreground -> ScarfColor.foregroundMuted; window-background ->
  ScarfColor.backgroundPrimary.
- ServerSwitcherToolbar: status dot + label tokenized.
- ConnectionStatusPill: status colors -> ScarfColor.success/warning/
  danger; error sheet header -> ScarfPrimaryButton retry.

Build green on both Mac (scarf) and iOS (scarf mobile) schemes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-25 14:39:13 +02:00
parent 41769e289c
commit 23dd8becb9
49 changed files with 373 additions and 265 deletions
@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import ScarfDesign
/// Translucent loading overlay used by feature views while their VM's /// Translucent loading overlay used by feature views while their VM's
/// `load()` runs in the background. Shows a centered ProgressView with /// `load()` runs in the background. Shows a centered ProgressView with
@@ -27,15 +28,15 @@ struct LoadingOverlay: ViewModifier {
if isEmpty { if isEmpty {
// Full cover: empty state. User has no data to look at, // Full cover: empty state. User has no data to look at,
// so own the whole pane with the spinner. // so own the whole pane with the spinner.
VStack(spacing: 12) { VStack(spacing: ScarfSpace.s3) {
ProgressView() ProgressView()
.controlSize(.large) .controlSize(.large)
Text(label) Text(label)
.font(.callout) .scarfStyle(.callout)
.foregroundStyle(.secondary) .foregroundStyle(ScarfColor.foregroundMuted)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(NSColor.windowBackgroundColor)) .background(ScarfColor.backgroundPrimary)
} else { } else {
// Stale-content refresh: top-trailing pill so the // Stale-content refresh: top-trailing pill so the
// user sees data is being refreshed without losing // user sees data is being refreshed without losing
@@ -47,13 +48,13 @@ struct LoadingOverlay: ViewModifier {
ProgressView() ProgressView()
.controlSize(.small) .controlSize(.small)
Text(label) Text(label)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(ScarfColor.foregroundMuted)
} }
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 5) .padding(.vertical, 5)
.background(.thinMaterial, in: Capsule()) .background(.thinMaterial, in: Capsule())
.padding(8) .padding(ScarfSpace.s2)
} }
Spacer() Spacer()
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct CredentialPoolsView: View { struct CredentialPoolsView: View {
@State private var viewModel: CredentialPoolsViewModel @State private var viewModel: CredentialPoolsViewModel
@@ -12,9 +13,10 @@ struct CredentialPoolsView: View {
var body: some View { var body: some View {
VStack(spacing: 0) {
header
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
header
safetyNotice safetyNotice
if viewModel.isLoading { if viewModel.isLoading {
ProgressView().padding() ProgressView().padding()
@@ -29,6 +31,8 @@ struct CredentialPoolsView: View {
.padding() .padding()
.frame(maxWidth: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .topLeading)
} }
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Credential Pools") .navigationTitle("Credential Pools")
.loadingOverlay( .loadingOverlay(
viewModel.isLoading, viewModel.isLoading,
@@ -58,21 +62,26 @@ struct CredentialPoolsView: View {
} }
private var header: some View { private var header: some View {
HStack { ScarfPageHeader(
"Credential Pools",
subtitle: "Shared OAuth + token pools rotated across runs."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message { if let msg = viewModel.message {
Label(msg, systemImage: "info.circle.fill") Label(msg, systemImage: "info.circle.fill")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(ScarfColor.foregroundMuted)
} }
Spacer() Button("Reload") { viewModel.load() }
.buttonStyle(ScarfGhostButton())
Button { Button {
showAddSheet = true showAddSheet = true
} label: { } label: {
Label("Add Credential", systemImage: "plus") Label("Add Credential", systemImage: "plus")
} }
.controlSize(.small) .buttonStyle(ScarfPrimaryButton())
Button("Reload") { viewModel.load() } }
.controlSize(.small) .fixedSize(horizontal: true, vertical: false)
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct GatewayView: View { struct GatewayView: View {
@State private var viewModel: GatewayViewModel @State private var viewModel: GatewayViewModel
@@ -11,6 +12,11 @@ struct GatewayView: View {
var body: some View { var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Messaging Gateway",
subtitle: "Outbound channel bridge — Discord, Telegram, Slack, etc."
)
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 24) { VStack(alignment: .leading, spacing: 24) {
serviceSection serviceSection
@@ -20,6 +26,8 @@ struct GatewayView: View {
.padding() .padding()
.frame(maxWidth: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .topLeading)
} }
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Messaging Gateway") .navigationTitle("Messaging Gateway")
.onAppear { viewModel.load() } .onAppear { viewModel.load() }
.onChange(of: fileWatcher.lastChangeDate) { viewModel.load() } .onChange(of: fileWatcher.lastChangeDate) { viewModel.load() }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct MCPServerAddCustomView: View { struct MCPServerAddCustomView: View {
let viewModel: MCPServersViewModel let viewModel: MCPServersViewModel
@@ -16,13 +17,13 @@ struct MCPServerAddCustomView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack { HStack {
Text("Add Custom MCP Server") Text("Add Custom MCP Server")
.font(.headline) .scarfStyle(.headline)
Spacer() Spacer()
Button("Cancel") { dismiss() } Button("Cancel") { dismiss() }
Button("Add") { Button("Add") {
submit() submit()
} }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.disabled(!canSubmit) .disabled(!canSubmit)
} }
.padding() .padding()
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct MCPServerEditorView: View { struct MCPServerEditorView: View {
@State var viewModel: MCPServerEditorViewModel @State var viewModel: MCPServerEditorViewModel
@@ -11,7 +12,7 @@ struct MCPServerEditorView: View {
HStack { HStack {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Edit \(viewModel.server.name)") Text("Edit \(viewModel.server.name)")
.font(.headline) .scarfStyle(.headline)
Text(viewModel.server.transport.displayName) Text(viewModel.server.transport.displayName)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -30,7 +31,7 @@ struct MCPServerEditorView: View {
Text("Save") Text("Save")
} }
} }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.disabled(viewModel.isSaving) .disabled(viewModel.isSaving)
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct MCPServerPresetPickerView: View { struct MCPServerPresetPickerView: View {
let viewModel: MCPServersViewModel let viewModel: MCPServersViewModel
@@ -35,7 +36,7 @@ struct MCPServerPresetPickerView: View {
} }
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
(selectedPreset.map { Text(verbatim: $0.displayName) } ?? Text("Add from Preset")) (selectedPreset.map { Text(verbatim: $0.displayName) } ?? Text("Add from Preset"))
.font(.headline) .scarfStyle(.headline)
(selectedPreset.map { Text(verbatim: $0.description) } ?? Text("Pick an MCP server to add.")) (selectedPreset.map { Text(verbatim: $0.description) } ?? Text("Pick an MCP server to add."))
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -132,7 +133,7 @@ struct MCPServerPresetPickerView: View {
Button("Add Server") { Button("Add Server") {
submit(preset: preset) submit(preset: preset)
} }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.disabled(!canSubmit(preset: preset)) .disabled(!canSubmit(preset: preset))
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct PersonalitiesView: View { struct PersonalitiesView: View {
@State private var viewModel: PersonalitiesViewModel @State private var viewModel: PersonalitiesViewModel
@@ -12,9 +13,26 @@ struct PersonalitiesView: View {
var body: some View { var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Personalities",
subtitle: "Per-personality model + prompt overrides defined in config.yaml."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message {
Label(msg, systemImage: "checkmark.circle.fill")
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.success)
}
Button("Edit config.yaml") { viewModel.openConfigInEditor() }
.buttonStyle(ScarfGhostButton())
Button("Reload") { viewModel.load(); soulDraft = viewModel.soulMarkdown }
.buttonStyle(ScarfSecondaryButton())
}
.fixedSize(horizontal: true, vertical: false)
}
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 20) {
header
activeSection activeSection
listSection listSection
soulSection soulSection
@@ -22,6 +40,8 @@ struct PersonalitiesView: View {
.padding() .padding()
.frame(maxWidth: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .topLeading)
} }
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Personalities") .navigationTitle("Personalities")
.onAppear { .onAppear {
viewModel.load() viewModel.load()
@@ -29,21 +49,6 @@ struct PersonalitiesView: View {
} }
} }
private var header: some View {
HStack {
if let msg = viewModel.message {
Label(msg, systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundStyle(.green)
}
Spacer()
Button("Edit config.yaml") { viewModel.openConfigInEditor() }
.controlSize(.small)
Button("Reload") { viewModel.load(); soulDraft = viewModel.soulMarkdown }
.controlSize(.small)
}
}
private var activeSection: some View { private var activeSection: some View {
SettingsSection(title: "Active Personality", icon: "theatermasks.fill") { SettingsSection(title: "Active Personality", icon: "theatermasks.fill") {
if viewModel.personalities.isEmpty { if viewModel.personalities.isEmpty {
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct DiscordSetupView: View { struct DiscordSetupView: View {
@State private var viewModel: DiscordSetupViewModel @State private var viewModel: DiscordSetupViewModel
@@ -57,7 +58,7 @@ struct DiscordSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct EmailSetupView: View { struct EmailSetupView: View {
@State private var viewModel: EmailSetupViewModel @State private var viewModel: EmailSetupViewModel
@@ -75,7 +76,7 @@ struct EmailSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct FeishuSetupView: View { struct FeishuSetupView: View {
@State private var viewModel: FeishuSetupViewModel @State private var viewModel: FeishuSetupViewModel
@@ -52,7 +53,7 @@ struct FeishuSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct HomeAssistantSetupView: View { struct HomeAssistantSetupView: View {
@State private var viewModel: HomeAssistantSetupViewModel @State private var viewModel: HomeAssistantSetupViewModel
@@ -72,7 +73,7 @@ struct HomeAssistantSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct IMessageSetupView: View { struct IMessageSetupView: View {
@State private var viewModel: IMessageSetupViewModel @State private var viewModel: IMessageSetupViewModel
@@ -61,7 +62,7 @@ struct IMessageSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct MatrixSetupView: View { struct MatrixSetupView: View {
@State private var viewModel: MatrixSetupViewModel @State private var viewModel: MatrixSetupViewModel
@@ -69,7 +70,7 @@ struct MatrixSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct MattermostSetupView: View { struct MattermostSetupView: View {
@State private var viewModel: MattermostSetupViewModel @State private var viewModel: MattermostSetupViewModel
@@ -52,7 +53,7 @@ struct MattermostSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct SignalSetupView: View { struct SignalSetupView: View {
@State private var viewModel: SignalSetupViewModel @State private var viewModel: SignalSetupViewModel
@@ -73,7 +74,7 @@ struct SignalSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
@@ -87,7 +88,7 @@ struct SignalSetupView: View {
case .none: case .none:
Button("Link Device") { viewModel.startLink() }.controlSize(.small) Button("Link Device") { viewModel.startLink() }.controlSize(.small)
.disabled(!viewModel.signalCLIInstalled) .disabled(!viewModel.signalCLIInstalled)
Button("Start Daemon") { viewModel.startDaemon() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Start Daemon") { viewModel.startDaemon() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
.disabled(!viewModel.signalCLIInstalled || viewModel.account.isEmpty) .disabled(!viewModel.signalCLIInstalled || viewModel.account.isEmpty)
case .link: case .link:
Text("Linking…").font(.caption).foregroundStyle(.secondary) Text("Linking…").font(.caption).foregroundStyle(.secondary)
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct SlackSetupView: View { struct SlackSetupView: View {
@State private var viewModel: SlackSetupViewModel @State private var viewModel: SlackSetupViewModel
@@ -56,7 +57,7 @@ struct SlackSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct TelegramSetupView: View { struct TelegramSetupView: View {
@State private var viewModel: TelegramSetupViewModel @State private var viewModel: TelegramSetupViewModel
@@ -57,7 +58,7 @@ struct TelegramSetupView: View {
Button("Reload") { viewModel.load() } Button("Reload") { viewModel.load() }
.controlSize(.small) .controlSize(.small)
Button("Save") { viewModel.save() } Button("Save") { viewModel.save() }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.controlSize(.small) .controlSize(.small)
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct WebhookSetupView: View { struct WebhookSetupView: View {
@State private var viewModel: WebhookSetupViewModel @State private var viewModel: WebhookSetupViewModel
@@ -53,7 +54,7 @@ struct WebhookSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct WhatsAppSetupView: View { struct WhatsAppSetupView: View {
@State private var viewModel: WhatsAppSetupViewModel @State private var viewModel: WhatsAppSetupViewModel
@@ -56,7 +57,7 @@ struct WhatsAppSetupView: View {
} }
Spacer() Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small) Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small) Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
} }
} }
@@ -71,7 +72,7 @@ struct WhatsAppSetupView: View {
.controlSize(.small) .controlSize(.small)
} else { } else {
Button("Start Pairing") { viewModel.startPairing() } Button("Start Pairing") { viewModel.startPairing() }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.controlSize(.small) .controlSize(.small)
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct PlatformsView: View { struct PlatformsView: View {
@State private var viewModel: PlatformsViewModel @State private var viewModel: PlatformsViewModel
@@ -13,12 +14,19 @@ struct PlatformsView: View {
// HSplitView (not nested NavigationSplitView) because ContentView already // HSplitView (not nested NavigationSplitView) because ContentView already
// hosts the outer NavigationSplitView nesting them breaks layout on macOS. // hosts the outer NavigationSplitView nesting them breaks layout on macOS.
var body: some View { var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Platforms",
subtitle: "Inbound channels the agent listens on. Set up tokens per platform."
)
HSplitView { HSplitView {
platformList platformList
.frame(minWidth: 220, idealWidth: 240, maxWidth: 300) .frame(minWidth: 220, idealWidth: 240, maxWidth: 300)
detail detail
.frame(minWidth: 480) .frame(minWidth: 480)
} }
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Platforms") .navigationTitle("Platforms")
.onAppear { viewModel.load() } .onAppear { viewModel.load() }
// Re-read config.yaml / .env / gateway_state.json when any of them // Re-read config.yaml / .env / gateway_state.json when any of them
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct PluginsView: View { struct PluginsView: View {
@State private var viewModel: PluginsViewModel @State private var viewModel: PluginsViewModel
@@ -15,7 +16,6 @@ struct PluginsView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
header header
Divider()
if viewModel.isLoading && viewModel.plugins.isEmpty { if viewModel.isLoading && viewModel.plugins.isEmpty {
ProgressView().padding() ProgressView().padding()
} else if viewModel.plugins.isEmpty { } else if viewModel.plugins.isEmpty {
@@ -24,6 +24,7 @@ struct PluginsView: View {
list list
} }
} }
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Plugins") .navigationTitle("Plugins")
.loadingOverlay( .loadingOverlay(
viewModel.isLoading, viewModel.isLoading,
@@ -45,25 +46,28 @@ struct PluginsView: View {
} }
private var header: some View { private var header: some View {
HStack { ScarfPageHeader(
"Plugins",
subtitle: "Hermes plugins discovered from `~/.hermes/plugins/`."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message { if let msg = viewModel.message {
Label(msg, systemImage: "info.circle.fill") Label(msg, systemImage: "info.circle.fill")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.green) .foregroundStyle(ScarfColor.success)
} }
Spacer() Button("Reload") { viewModel.load() }
.buttonStyle(ScarfGhostButton())
Button { Button {
installIdentifier = "" installIdentifier = ""
showInstall = true showInstall = true
} label: { } label: {
Label("Install", systemImage: "plus") Label("Install", systemImage: "plus")
} }
.controlSize(.small) .buttonStyle(ScarfPrimaryButton())
Button("Reload") { viewModel.load() } }
.controlSize(.small) .fixedSize(horizontal: true, vertical: false)
} }
.padding(.horizontal)
.padding(.vertical, 8)
} }
private var emptyState: some View { private var emptyState: some View {
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
import AppKit import AppKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
@@ -21,12 +22,19 @@ struct ProfilesView: View {
@State private var pendingDelete: HermesProfile? @State private var pendingDelete: HermesProfile?
var body: some View { var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Profiles",
subtitle: "Named config bundles you can swap between."
)
HSplitView { HSplitView {
listSection listSection
.frame(minWidth: 260, idealWidth: 300) .frame(minWidth: 260, idealWidth: 300)
detailSection detailSection
.frame(minWidth: 400) .frame(minWidth: 400)
} }
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Profiles") .navigationTitle("Profiles")
.onAppear { viewModel.load() } .onAppear { viewModel.load() }
.sheet(isPresented: $showCreate) { createSheet } .sheet(isPresented: $showCreate) { createSheet }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Sheet for assigning a project to a folder in the sidebar. Folders /// Sheet for assigning a project to a folder in the sidebar. Folders
/// are implicit they exist because at least one project references /// are implicit they exist because at least one project references
@@ -55,11 +56,13 @@ struct MoveToFolderSheet: View {
} }
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: ScarfSpace.s3) {
Text("Move \"\(project.name)\" to folder").font(.headline) Text("Move \"\(project.name)\" to folder")
.scarfStyle(.headline)
.foregroundStyle(ScarfColor.foregroundPrimary)
Text("Folders only affect how projects are grouped in Scarf's sidebar. Nothing on disk changes.") Text("Folders only affect how projects are grouped in Scarf's sidebar. Nothing on disk changes.")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(ScarfColor.foregroundMuted)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
Picker("Destination", selection: $mode) { Picker("Destination", selection: $mode) {
@@ -77,8 +80,7 @@ struct MoveToFolderSheet: View {
.pickerStyle(.inline) .pickerStyle(.inline)
if case .new = mode { if case .new = mode {
TextField("New folder name", text: $newFolderName) ScarfTextField("New folder name", text: $newFolderName)
.textFieldStyle(.roundedBorder)
.onSubmit { .onSubmit {
if canMove { commit() } if canMove { commit() }
} }
@@ -86,15 +88,16 @@ struct MoveToFolderSheet: View {
HStack { HStack {
Button("Cancel") { dismiss() } Button("Cancel") { dismiss() }
.buttonStyle(ScarfGhostButton())
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
Spacer() Spacer()
Button("Move") { commit() } Button("Move") { commit() }
.buttonStyle(ScarfPrimaryButton())
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.disabled(!canMove) .disabled(!canMove)
} }
} }
.padding() .padding(ScarfSpace.s5)
.frame(minWidth: 420, minHeight: 320) .frame(minWidth: 420, minHeight: 320)
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Per-project Sessions tab (v2.3). Lives beside the Dashboard and /// Per-project Sessions tab (v2.3). Lives beside the Dashboard and
/// Site tabs in the project view; populated from the session /// Site tabs in the project view; populated from the session
@@ -59,9 +60,9 @@ struct ProjectSessionsView: View {
HStack(spacing: 12) { HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Sessions in this project") Text("Sessions in this project")
.font(.headline) .scarfStyle(.headline)
Text("Chats you start here get attributed automatically. Older CLI-started sessions live in the global Sessions sidebar.") Text("Chats you start here get attributed automatically. Older CLI-started sessions live in the global Sessions sidebar.")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
@@ -75,7 +76,7 @@ struct ProjectSessionsView: View {
} label: { } label: {
Label("New Chat", systemImage: "message.badge.filled.fill") Label("New Chat", systemImage: "message.badge.filled.fill")
} }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
} }
.padding() .padding()
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// The "Slash Commands" tab on the per-project surface. Lists the /// The "Slash Commands" tab on the per-project surface. Lists the
/// project-scoped commands stored at `<project>/.scarf/slash-commands/` /// project-scoped commands stored at `<project>/.scarf/slash-commands/`
@@ -52,9 +53,9 @@ struct ProjectSlashCommandsView: View {
HStack { HStack {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Slash Commands") Text("Slash Commands")
.font(.headline) .scarfStyle(.headline)
Text("`/<name>` shortcuts that expand into prompt templates. Stored at `<project>/.scarf/slash-commands/` so they ship with `.scarftemplate` bundles.") Text("`/<name>` shortcuts that expand into prompt templates. Stored at `<project>/.scarf/slash-commands/` so they ship with `.scarftemplate` bundles.")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(2) .lineLimit(2)
} }
@@ -83,7 +84,7 @@ struct ProjectSlashCommandsView: View {
Text("Add reusable prompt templates here. Each command shows up in the chat slash menu when you're chatting in this project.") Text("Add reusable prompt templates here. Each command shows up in the chat slash menu when you're chatting in this project.")
} actions: { } actions: {
Button("Add Command") { viewModel.beginNew() } Button("Add Command") { viewModel.beginNew() }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
} else { } else {
@@ -117,7 +118,7 @@ struct ProjectSlashCommandsView: View {
Text("Couldn't update slash commands") Text("Couldn't update slash commands")
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
Text(message) Text(message)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
Spacer() Spacer()
@@ -165,7 +166,7 @@ private struct CommandRow: View {
} }
} }
Text(command.description) Text(command.description)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(2) .lineLimit(2)
if let tags = command.tags, !tags.isEmpty { if let tags = command.tags, !tags.isEmpty {
@@ -208,7 +209,7 @@ struct SlashCommandEditorSheet: View {
Button("Save") { Button("Save") {
Task { await viewModel.saveDraft() } Task { await viewModel.saveDraft() }
} }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.disabled(saveDisabled) .disabled(saveDisabled)
} }
@@ -244,7 +245,7 @@ struct SlashCommandEditorSheet: View {
.help("Lowercase letters, digits, and hyphens. Must start with a letter.") .help("Lowercase letters, digits, and hyphens. Must start with a letter.")
if let nameError = nameValidationMessage { if let nameError = nameValidationMessage {
Text(nameError) Text(nameError)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.orange) .foregroundStyle(.orange)
} }
TextField("Description", text: Binding( TextField("Description", text: Binding(
@@ -277,7 +278,7 @@ struct SlashCommandEditorSheet: View {
Section("Prompt template") { Section("Prompt template") {
Text("Use `{{argument}}` to substitute the user's input. `{{argument | default: \"\"}}` provides a fallback when the user invokes the command without arguments.") Text("Use `{{argument}}` to substitute the user's input. `{{argument | default: \"\"}}` provides a fallback when the user invokes the command without arguments.")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
TextEditor(text: Binding( TextEditor(text: Binding(
get: { viewModel.draft?.body ?? "" }, get: { viewModel.draft?.body ?? "" },
@@ -305,12 +306,12 @@ struct SlashCommandEditorSheet: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
Text("Preview") Text("Preview")
.font(.headline) .scarfStyle(.headline)
Spacer() Spacer()
} }
HStack { HStack {
Text("Sample argument") Text("Sample argument")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
TextField("(empty)", text: $sampleArgument) TextField("(empty)", text: $sampleArgument)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@@ -324,7 +325,7 @@ struct SlashCommandEditorSheet: View {
.textSelection(.enabled) .textSelection(.enabled)
} }
Text("This is the prompt Hermes will receive. The user sees the literal `/\(viewModel.draft?.name ?? "name")` they typed in their own bubble; the expanded body goes to the agent with a `<!-- scarf-slash:<name> -->` marker.") Text("This is the prompt Hermes will receive. The user sees the literal `/\(viewModel.draft?.name ?? "name")` they typed in their own bubble; the expanded body goes to the agent with a `<!-- scarf-slash:<name> -->` marker.")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Sidebar view for the Projects feature. Renders the registry as: /// Sidebar view for the Projects feature. Renders the registry as:
/// - A search field at the top (F focus). /// - A search field at the top (F focus).
@@ -68,18 +69,18 @@ struct ProjectsSidebar: View {
HStack { HStack {
Image(systemName: "magnifyingglass") Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.font(.caption) .scarfStyle(.caption)
TextField("Filter projects", text: $filterText) TextField("Filter projects", text: $filterText)
.textFieldStyle(.plain) .textFieldStyle(.plain)
.focused($searchFocused) .focused($searchFocused)
.font(.caption) .scarfStyle(.caption)
if !filterText.isEmpty { if !filterText.isEmpty {
Button { Button {
filterText = "" filterText = ""
} label: { } label: {
Image(systemName: "xmark.circle.fill") Image(systemName: "xmark.circle.fill")
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
.font(.caption) .scarfStyle(.caption)
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
@@ -122,7 +123,7 @@ struct ProjectsSidebar: View {
} }
} label: { } label: {
Label(folder, systemImage: "folder") Label(folder, systemImage: "folder")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
@@ -137,7 +138,7 @@ struct ProjectsSidebar: View {
} }
} label: { } label: {
Label("Archived (\(archivedVisible.count))", systemImage: "archivebox") Label("Archived (\(archivedVisible.count))", systemImage: "archivebox")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
@@ -208,7 +209,7 @@ struct ProjectsSidebar: View {
Toggle(isOn: $showArchived) { Toggle(isOn: $showArchived) {
Image(systemName: showArchived ? "archivebox.fill" : "archivebox") Image(systemName: showArchived ? "archivebox.fill" : "archivebox")
.font(.caption) .scarfStyle(.caption)
} }
.toggleStyle(.button) .toggleStyle(.button)
.buttonStyle(.borderless) .buttonStyle(.borderless)
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Sheet for renaming a project in the registry. Preserves the /// Sheet for renaming a project in the registry. Preserves the
/// project's `path`, `folder`, and `archived` fields the rename /// project's `path`, `folder`, and `archived` fields the rename
@@ -45,15 +46,16 @@ struct RenameProjectSheet: View {
} }
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: ScarfSpace.s3) {
Text("Rename project").font(.headline) Text("Rename project")
.scarfStyle(.headline)
.foregroundStyle(ScarfColor.foregroundPrimary)
Text("The project directory on disk isn't changed — only the label Scarf shows in the sidebar.") Text("The project directory on disk isn't changed — only the label Scarf shows in the sidebar.")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(ScarfColor.foregroundMuted)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
TextField("Project name", text: $newName) ScarfTextField("Project name", text: $newName)
.textFieldStyle(.roundedBorder)
.onSubmit { .onSubmit {
if validation.isValid { if validation.isValid {
save() save()
@@ -62,21 +64,22 @@ struct RenameProjectSheet: View {
if let message = validation.message { if let message = validation.message {
Label(message, systemImage: "exclamationmark.triangle.fill") Label(message, systemImage: "exclamationmark.triangle.fill")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.red) .foregroundStyle(ScarfColor.danger)
} }
HStack { HStack {
Button("Cancel") { dismiss() } Button("Cancel") { dismiss() }
.buttonStyle(ScarfGhostButton())
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
Spacer() Spacer()
Button("Save") { save() } Button("Save") { save() }
.buttonStyle(ScarfPrimaryButton())
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.disabled(!validation.isValid) .disabled(!validation.isValid)
} }
} }
.padding() .padding(ScarfSpace.s5)
.frame(minWidth: 420) .frame(minWidth: 420)
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
import Charts import Charts
// Flattened data point for Charts to avoid complex nested generic inference // Flattened data point for Charts to avoid complex nested generic inference
@@ -27,15 +28,15 @@ struct ChartWidgetView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(widget.title) Text(widget.title)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
chartContent chartContent
.frame(height: 150) .frame(height: 150)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(12) .padding(12)
.background(.quaternary.opacity(0.5)) .background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} }
@ViewBuilder @ViewBuilder
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct ListWidgetView: View { struct ListWidgetView: View {
let widget: DashboardWidget let widget: DashboardWidget
@@ -10,10 +11,10 @@ struct ListWidgetView: View {
if let icon = widget.icon { if let icon = widget.icon {
Image(systemName: icon) Image(systemName: icon)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.font(.caption) .scarfStyle(.caption)
} }
Text(widget.title) Text(widget.title)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
if let items = widget.items { if let items = widget.items {
@@ -32,8 +33,8 @@ struct ListWidgetView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(12) .padding(12)
.background(.quaternary.opacity(0.5)) .background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} }
private func statusIcon(_ status: String?) -> String { private func statusIcon(_ status: String?) -> String {
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct ProgressWidgetView: View { struct ProgressWidgetView: View {
let widget: DashboardWidget let widget: DashboardWidget
@@ -14,7 +15,7 @@ struct ProgressWidgetView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text(widget.title) Text(widget.title)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
ProgressView(value: progressValue) { ProgressView(value: progressValue) {
if let label = widget.label { if let label = widget.label {
@@ -27,7 +28,7 @@ struct ProgressWidgetView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(12) .padding(12)
.background(.quaternary.opacity(0.5)) .background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct StatWidgetView: View { struct StatWidgetView: View {
let widget: DashboardWidget let widget: DashboardWidget
@@ -14,10 +15,10 @@ struct StatWidgetView: View {
if let icon = widget.icon { if let icon = widget.icon {
Image(systemName: icon) Image(systemName: icon)
.foregroundStyle(widgetColor) .foregroundStyle(widgetColor)
.font(.caption) .scarfStyle(.caption)
} }
Text(widget.title) Text(widget.title)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
if let value = widget.value { if let value = widget.value {
@@ -32,7 +33,7 @@ struct StatWidgetView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(12) .padding(12)
.background(.quaternary.opacity(0.5)) .background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct TableWidgetView: View { struct TableWidgetView: View {
let widget: DashboardWidget let widget: DashboardWidget
@@ -7,7 +8,7 @@ struct TableWidgetView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(widget.title) Text(widget.title)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
if let columns = widget.columns, let rows = widget.rows { if let columns = widget.columns, let rows = widget.rows {
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 4) { Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 4) {
@@ -32,7 +33,7 @@ struct TableWidgetView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(12) .padding(12)
.background(.quaternary.opacity(0.5)) .background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct TextWidgetView: View { struct TextWidgetView: View {
let widget: DashboardWidget let widget: DashboardWidget
@@ -7,7 +8,7 @@ struct TextWidgetView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(widget.title) Text(widget.title)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
if let content = widget.content { if let content = widget.content {
if widget.format == "markdown" { if widget.format == "markdown" {
@@ -20,7 +21,7 @@ struct TextWidgetView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(12) .padding(12)
.background(.quaternary.opacity(0.5)) .background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
import WebKit import WebKit
struct WebviewWidgetView: View { struct WebviewWidgetView: View {
@@ -29,7 +30,7 @@ struct WebviewWidgetView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
if let url = webURL { if let url = webURL {
WebViewRepresentable(url: url) WebViewRepresentable(url: url)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} else { } else {
ContentUnavailableView { ContentUnavailableView {
Label("Invalid URL", systemImage: "globe") Label("Invalid URL", systemImage: "globe")
@@ -49,10 +50,10 @@ struct WebviewWidgetView: View {
if let icon = widget.icon { if let icon = widget.icon {
Image(systemName: icon) Image(systemName: icon)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.font(.caption) .scarfStyle(.caption)
} }
Text(widget.title) Text(widget.title)
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Spacer() Spacer()
if let urlString = widget.url { if let urlString = widget.url {
@@ -76,8 +77,8 @@ struct WebviewWidgetView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(12) .padding(12)
.background(.quaternary.opacity(0.5)) .background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} }
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct QuickCommandsView: View { struct QuickCommandsView: View {
@State private var viewModel: QuickCommandsViewModel @State private var viewModel: QuickCommandsViewModel
@@ -12,9 +13,10 @@ struct QuickCommandsView: View {
var body: some View { var body: some View {
VStack(spacing: 0) {
header
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
header
intro intro
if viewModel.commands.isEmpty { if viewModel.commands.isEmpty {
emptyState emptyState
@@ -25,6 +27,8 @@ struct QuickCommandsView: View {
.padding() .padding()
.frame(maxWidth: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .topLeading)
} }
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Quick Commands") .navigationTitle("Quick Commands")
.onAppear { viewModel.load() } .onAppear { viewModel.load() }
.sheet(isPresented: $showAddSheet) { .sheet(isPresented: $showAddSheet) {
@@ -46,28 +50,33 @@ struct QuickCommandsView: View {
} }
private var header: some View { private var header: some View {
HStack { ScarfPageHeader(
"Quick Commands",
subtitle: "Shell shortcuts hermes exposes in chat as `/command_name`."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message { if let msg = viewModel.message {
Label(msg, systemImage: "checkmark.circle.fill") Label(msg, systemImage: "checkmark.circle.fill")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.green) .foregroundStyle(ScarfColor.success)
} }
Spacer() Button("Reload") { viewModel.load() }
.buttonStyle(ScarfGhostButton())
Button { Button {
showAddSheet = true showAddSheet = true
} label: { } label: {
Label("Add Command", systemImage: "plus") Label("Add Command", systemImage: "plus")
} }
.controlSize(.small) .buttonStyle(ScarfPrimaryButton())
Button("Reload") { viewModel.load() } }
.controlSize(.small) .fixedSize(horizontal: true, vertical: false)
} }
} }
private var intro: some View { private var intro: some View {
Text("Quick commands are shell shortcuts hermes exposes in chat as `/command_name`. They live under `quick_commands:` in config.yaml.") Text("Stored under `quick_commands:` in config.yaml.")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(ScarfColor.foregroundMuted)
} }
private var emptyState: some View { private var emptyState: some View {
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Sheet for adding a new remote server. Collects SSH connection details, /// Sheet for adding a new remote server. Collects SSH connection details,
/// runs a "Test Connection" probe, and on save hands the persisted /// runs a "Test Connection" probe, and on save hands the persisted
@@ -36,7 +37,7 @@ struct AddServerSheet: View {
Image(systemName: "server.rack") Image(systemName: "server.rack")
.font(.title2) .font(.title2)
Text("Add Remote Server") Text("Add Remote Server")
.font(.headline) .scarfStyle(.headline)
Spacer() Spacer()
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Small colored pill shown in the toolbar reflecting the server's reach- /// Small colored pill shown in the toolbar reflecting the server's reach-
/// ability. Green = connected, yellow = probing, red = unreachable. /// ability. Green = connected, yellow = probing, red = unreachable.
@@ -33,8 +34,8 @@ struct ConnectionStatusPill: View {
.foregroundStyle(color) .foregroundStyle(color)
.symbolRenderingMode(.hierarchical) .symbolRenderingMode(.hierarchical)
labelText labelText
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.secondary) .foregroundStyle(ScarfColor.foregroundMuted)
.lineLimit(1) .lineLimit(1)
} }
.padding(.horizontal, 4) .padding(.horizontal, 4)
@@ -51,10 +52,10 @@ struct ConnectionStatusPill: View {
private var color: Color { private var color: Color {
switch status.status { switch status.status {
case .connected: return .green case .connected: return ScarfColor.success
case .degraded: return .orange case .degraded: return ScarfColor.warning
case .idle: return .yellow case .idle: return ScarfColor.warning.opacity(0.7)
case .error: return .red case .error: return ScarfColor.danger
} }
} }
@@ -101,13 +102,14 @@ struct ConnectionStatusPill: View {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
HStack { HStack {
Label(message, systemImage: "xmark.octagon.fill") Label(message, systemImage: "xmark.octagon.fill")
.foregroundStyle(.red) .foregroundStyle(ScarfColor.danger)
.font(.headline) .scarfStyle(.headline)
Spacer() Spacer()
Button("Retry") { Button("Retry") {
status.retry() status.retry()
showDetails = false showDetails = false
} }
.buttonStyle(ScarfPrimaryButton())
} }
Divider() Divider()
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// List of registered remote servers with add/remove actions. Rendered as a /// List of registered remote servers with add/remove actions. Rendered as a
/// popover from the toolbar switcher. /// popover from the toolbar switcher.
@@ -59,7 +60,7 @@ struct ManageServersView: View {
private var header: some View { private var header: some View {
HStack { HStack {
Text("Servers").font(.headline) Text("Servers").scarfStyle(.headline)
Spacer() Spacer()
Button { Button {
showAddSheet = true showAddSheet = true
@@ -76,7 +77,7 @@ struct ManageServersView: View {
Image(systemName: "server.rack") Image(systemName: "server.rack")
.font(.system(size: 28)) .font(.system(size: 28))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text("No remote servers").font(.headline) Text("No remote servers").scarfStyle(.headline)
Text("Click Add to connect to a remote Hermes installation over SSH.") Text("Click Add to connect to a remote Hermes installation over SSH.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Shown when a window is restored after the user removed the server it /// Shown when a window is restored after the user removed the server it
/// was bound to. Lets them open Local or any remaining registered server /// was bound to. Lets them open Local or any remaining registered server
@@ -33,7 +34,7 @@ struct MissingServerView: View {
openWindow(value: ServerContext.local.id) openWindow(value: ServerContext.local.id)
dismissWindow() dismissWindow()
} }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
if !registry.entries.isEmpty { if !registry.entries.isEmpty {
Menu { Menu {
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Toolbar control that shows the current window's server and exposes a /// Toolbar control that shows the current window's server and exposes a
/// menu for opening *other* servers in additional windows. Multi-window is /// menu for opening *other* servers in additional windows. Multi-window is
@@ -36,13 +37,15 @@ struct ServerSwitcherToolbar: View {
} label: { } label: {
HStack(spacing: 6) { HStack(spacing: 6) {
Circle() Circle()
.fill(current.isRemote ? Color.blue : Color.green) .fill(current.isRemote ? ScarfColor.info : ScarfColor.success)
.frame(width: 8, height: 8) .frame(width: 8, height: 8)
Text(verbatim: current.displayName) Text(verbatim: current.displayName)
.font(.callout) .scarfStyle(.callout)
.foregroundStyle(ScarfColor.foregroundPrimary)
.lineLimit(1) .lineLimit(1)
Image(systemName: "chevron.down") Image(systemName: "chevron.down")
.font(.caption2) .font(.system(size: 10))
.foregroundStyle(ScarfColor.foregroundMuted)
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 4) .padding(.vertical, 4)
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
/// Two-column model browser sheet. Left column lists providers, right column /// Two-column model browser sheet. Left column lists providers, right column
/// lists models for the selected provider. Supports filtering and a "Custom" /// lists models for the selected provider. Supports filtering and a "Custom"
@@ -109,7 +110,7 @@ struct ModelPickerSheet: View {
HStack(spacing: 8) { HStack(spacing: 8) {
Image(systemName: "cpu") Image(systemName: "cpu")
Text("Select Model") Text("Select Model")
.font(.headline) .scarfStyle(.headline)
Spacer() Spacer()
if !customMode { if !customMode {
TextField("Search…", text: $searchText) TextField("Search…", text: $searchText)
@@ -294,7 +295,7 @@ struct ModelPickerSheet: View {
} label: { } label: {
Label("Sign in to Nous Portal", systemImage: "person.badge.key.fill") Label("Sign in to Nous Portal", systemImage: "person.badge.key.fill")
} }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.controlSize(.regular) .controlSize(.regular)
} }
} }
@@ -361,7 +362,7 @@ struct ModelPickerSheet: View {
Spacer() Spacer()
Button("Cancel") { onCancel() } Button("Cancel") { onCancel() }
Button("Select") { submitSelection() } Button("Select") { submitSelection() }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.disabled(!canSubmit) .disabled(!canSubmit)
} }
.padding() .padding()
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
import ScarfDesign
/// In-app sign-in sheet for Nous Portal hosts a ``NousAuthFlow`` and /// In-app sign-in sheet for Nous Portal hosts a ``NousAuthFlow`` and
/// renders one of four sub-views keyed on `flow.state`. Reached from the /// renders one of four sub-views keyed on `flow.state`. Reached from the
@@ -69,7 +70,7 @@ struct NousSignInSheet: View {
Image(systemName: "person.badge.key.fill") Image(systemName: "person.badge.key.fill")
.foregroundStyle(.tint) .foregroundStyle(.tint)
Text("Sign in to Nous Portal") Text("Sign in to Nous Portal")
.font(.headline) .scarfStyle(.headline)
Spacer() Spacer()
if case .waitingForApproval = flowState { if case .waitingForApproval = flowState {
Button("Cancel") { dismiss() } Button("Cancel") { dismiss() }
@@ -123,7 +124,7 @@ struct NousSignInSheet: View {
VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 14) {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Approve in your browser") Text("Approve in your browser")
.font(.headline) .scarfStyle(.headline)
Text("We opened the Nous Portal approval page. Confirm this code matches what it shows, then approve.") Text("We opened the Nous Portal approval page. Confirm this code matches what it shows, then approve.")
.font(.callout) .font(.callout)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -176,7 +177,7 @@ struct NousSignInSheet: View {
.foregroundStyle(.green) .foregroundStyle(.green)
.font(.system(size: 48)) .font(.system(size: 48))
Text("Signed in to Nous Portal") Text("Signed in to Nous Portal")
.font(.headline) .scarfStyle(.headline)
Text("Your tools will now route through your subscription.") Text("Your tools will now route through your subscription.")
.font(.callout) .font(.callout)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -193,7 +194,7 @@ struct NousSignInSheet: View {
Image(systemName: "exclamationmark.triangle.fill") Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange) .foregroundStyle(.orange)
Text(billingURL == nil ? "Sign-in didn't complete" : "Subscription required") Text(billingURL == nil ? "Sign-in didn't complete" : "Subscription required")
.font(.headline) .scarfStyle(.headline)
} }
Text(reason) Text(reason)
@@ -209,7 +210,7 @@ struct NousSignInSheet: View {
Label("Subscribe", systemImage: "creditcard") Label("Subscribe", systemImage: "creditcard")
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.controlSize(.large) .controlSize(.large)
} }
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
struct SkillsView: View { struct SkillsView: View {
@State private var viewModel: SkillsViewModel @State private var viewModel: SkillsViewModel
@@ -38,6 +39,10 @@ struct SkillsView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
ScarfPageHeader(
"Skills",
subtitle: "Pre-packaged prompt collections the agent can call into. \(viewModel.totalSkillCount) installed."
)
modePicker modePicker
// v2.5 "What's New" pill only renders when the diff has // v2.5 "What's New" pill only renders when the diff has
// changes against a non-empty prior snapshot (first launch // changes against a non-empty prior snapshot (first launch
@@ -55,7 +60,8 @@ struct SkillsView: View {
case .updates: updatesContent case .updates: updatesContent
} }
} }
.navigationTitle("Skills (\(viewModel.totalSkillCount))") .background(ScarfColor.backgroundPrimary)
.navigationTitle("Skills")
// SkillsViewModel.load() is async after the v2.5 ScarfCore // SkillsViewModel.load() is async after the v2.5 ScarfCore
// promotion. Wrap in a Task here so the existing onAppear // promotion. Wrap in a Task here so the existing onAppear
// contract (fire-and-forget) keeps working without making // contract (fire-and-forget) keeps working without making
@@ -1,4 +1,5 @@
import ScarfCore import ScarfCore
import ScarfDesign
import SwiftUI import SwiftUI
/// Post-install configuration editor. Thin wrapper around the same /// Post-install configuration editor. Thin wrapper around the same
@@ -73,7 +74,7 @@ struct ConfigEditorSheet: View {
Text("Configuration saved").font(.title2.bold()) Text("Configuration saved").font(.title2.bold())
Button("Done") { dismiss() } Button("Done") { dismiss() }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(minWidth: 560, minHeight: 280) .frame(minWidth: 560, minHeight: 280)
@@ -1,4 +1,5 @@
import ScarfCore import ScarfCore
import ScarfDesign
import SwiftUI import SwiftUI
/// The configure form rendered for template install + post-install /// The configure form rendered for template install + post-install
@@ -106,7 +107,7 @@ struct TemplateConfigSheet: View {
// handles dismissal via the success view's Done button. // handles dismissal via the success view's Done button.
} }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
} }
.padding(16) .padding(16)
} }
@@ -117,10 +118,10 @@ struct TemplateConfigSheet: View {
private func fieldRow(_ field: TemplateConfigField) -> some View { private func fieldRow(_ field: TemplateConfigField) -> some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 4) { HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(field.label).font(.headline) Text(field.label).scarfStyle(.headline)
if field.required { if field.required {
Text("*") Text("*")
.font(.headline) .scarfStyle(.headline)
.foregroundStyle(.red) .foregroundStyle(.red)
} }
Spacer() Spacer()
@@ -2,6 +2,7 @@ import SwiftUI
import AppKit import AppKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
import ScarfCore import ScarfCore
import ScarfDesign
/// Author-facing sheet for exporting an existing project as a /// Author-facing sheet for exporting an existing project as a
/// `.scarftemplate`. Mirrors the profile-export flow: fill in a few fields, /// `.scarftemplate`. Mirrors the profile-export flow: fill in a few fields,
@@ -89,7 +90,7 @@ struct TemplateExportSheet: View {
Spacer() Spacer()
Button("Export…") { runExport() } Button("Export…") { runExport() }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.disabled(!canExport) .disabled(!canExport)
} }
.padding(.top, 8) .padding(.top, 8)
@@ -97,7 +98,7 @@ struct TemplateExportSheet: View {
private var metadataGroup: some View { private var metadataGroup: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("Metadata").font(.headline) Text("Metadata").scarfStyle(.headline)
LabeledContent("Template ID") { LabeledContent("Template ID") {
TextField("owner/name", text: $viewModel.templateId) TextField("owner/name", text: $viewModel.templateId)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@@ -137,7 +138,7 @@ struct TemplateExportSheet: View {
private var requiredFilesGroup: some View { private var requiredFilesGroup: some View {
let plan = viewModel.previewPlan() let plan = viewModel.previewPlan()
return VStack(alignment: .leading, spacing: 6) { return VStack(alignment: .leading, spacing: 6) {
Text("Required Files").font(.headline) Text("Required Files").scarfStyle(.headline)
check(label: "dashboard.json (\(plan.projectDir)/.scarf/dashboard.json)", ok: plan.dashboardPresent) check(label: "dashboard.json (\(plan.projectDir)/.scarf/dashboard.json)", ok: plan.dashboardPresent)
check(label: "README.md (\(plan.projectDir)/README.md)", ok: plan.readmePresent) check(label: "README.md (\(plan.projectDir)/README.md)", ok: plan.readmePresent)
check(label: "AGENTS.md (\(plan.projectDir)/AGENTS.md)", ok: plan.agentsMdPresent) check(label: "AGENTS.md (\(plan.projectDir)/AGENTS.md)", ok: plan.agentsMdPresent)
@@ -147,7 +148,7 @@ struct TemplateExportSheet: View {
private var instructionsGroup: some View { private var instructionsGroup: some View {
let plan = viewModel.previewPlan() let plan = viewModel.previewPlan()
return VStack(alignment: .leading, spacing: 4) { return VStack(alignment: .leading, spacing: 4) {
Text("Agent-specific instructions (optional)").font(.headline) Text("Agent-specific instructions (optional)").scarfStyle(.headline)
if plan.instructionFiles.isEmpty { if plan.instructionFiles.isEmpty {
Text("No per-agent instruction files found in the project root.") Text("No per-agent instruction files found in the project root.")
.font(.caption) .font(.caption)
@@ -163,7 +164,7 @@ struct TemplateExportSheet: View {
private var skillsGroup: some View { private var skillsGroup: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text("Include Skills").font(.headline) Text("Include Skills").scarfStyle(.headline)
if viewModel.availableSkills.isEmpty { if viewModel.availableSkills.isEmpty {
Text("No skills found.") Text("No skills found.")
.font(.caption) .font(.caption)
@@ -186,7 +187,7 @@ struct TemplateExportSheet: View {
private var cronGroup: some View { private var cronGroup: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text("Include Cron Jobs").font(.headline) Text("Include Cron Jobs").scarfStyle(.headline)
if viewModel.availableCronJobs.isEmpty { if viewModel.availableCronJobs.isEmpty {
Text("No cron jobs found.") Text("No cron jobs found.")
.font(.caption) .font(.caption)
@@ -214,7 +215,7 @@ struct TemplateExportSheet: View {
private var memoryGroup: some View { private var memoryGroup: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text("Memory Appendix (optional)").font(.headline) Text("Memory Appendix (optional)").scarfStyle(.headline)
Text("Markdown that will be appended to the installer's MEMORY.md, wrapped in template-specific markers so it can be removed cleanly later.") Text("Markdown that will be appended to the installer's MEMORY.md, wrapped in template-specific markers so it can be removed cleanly later.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -1,5 +1,6 @@
import AppKit import AppKit
import ScarfCore import ScarfCore
import ScarfDesign
import SwiftUI import SwiftUI
/// Preview-and-confirm sheet for installing a `.scarftemplate`. Honest /// Preview-and-confirm sheet for installing a `.scarftemplate`. Honest
@@ -47,7 +48,7 @@ struct TemplateInstallSheet: View {
private var idleView: some View { private var idleView: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
Text("No template loaded.") Text("No template loaded.")
.font(.headline) .scarfStyle(.headline)
Button("Close") { dismiss() } Button("Close") { dismiss() }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -70,7 +71,7 @@ struct TemplateInstallSheet: View {
Divider() Divider()
} }
Text("Where should this project live?") Text("Where should this project live?")
.font(.headline) .scarfStyle(.headline)
Text("Scarf will create a new folder inside the directory you pick, named after the template id.") Text("Scarf will create a new folder inside the directory you pick, named after the template id.")
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -169,7 +170,7 @@ struct TemplateInstallSheet: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Button("Install") { viewModel.confirmInstall() } Button("Install") { viewModel.confirmInstall() }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
} }
.padding(.top, 8) .padding(.top, 8)
} }
@@ -353,7 +354,7 @@ struct TemplateInstallSheet: View {
@ViewBuilder @ViewBuilder
private func section<Content: View>(title: String, subtitle: String?, @ViewBuilder content: () -> Content) -> some View { private func section<Content: View>(title: String, subtitle: String?, @ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline) Text(title).scarfStyle(.headline)
if let subtitle { if let subtitle {
Text(subtitle) Text(subtitle)
.font(.caption.monospaced()) .font(.caption.monospaced())
@@ -391,7 +392,7 @@ struct TemplateInstallSheet: View {
dismiss() dismiss()
} }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
} }
@@ -1,4 +1,5 @@
import ScarfCore import ScarfCore
import ScarfDesign
import SwiftUI import SwiftUI
/// Preview-and-confirm sheet for uninstalling a template-installed /// Preview-and-confirm sheet for uninstalling a template-installed
@@ -41,7 +42,7 @@ struct TemplateUninstallSheet: View {
private var idleView: some View { private var idleView: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
Text("No template loaded.") Text("No template loaded.")
.font(.headline) .scarfStyle(.headline)
Button("Close") { dismiss() } Button("Close") { dismiss() }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -87,7 +88,7 @@ struct TemplateUninstallSheet: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Button("Remove") { viewModel.confirmUninstall() } Button("Remove") { viewModel.confirmUninstall() }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
.tint(.red) .tint(.red)
} }
.padding(.top, 8) .padding(.top, 8)
@@ -244,7 +245,7 @@ struct TemplateUninstallSheet: View {
@ViewBuilder content: () -> Content @ViewBuilder content: () -> Content
) -> some View { ) -> some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline) Text(title).scarfStyle(.headline)
if let subtitle { if let subtitle {
Text(subtitle) Text(subtitle)
.font(.caption.monospaced()) .font(.caption.monospaced())
@@ -296,7 +297,7 @@ struct TemplateUninstallSheet: View {
dismiss() dismiss()
} }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent) .buttonStyle(ScarfPrimaryButton())
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding() .padding()
@@ -316,7 +317,7 @@ struct TemplateUninstallSheet: View {
Image(systemName: "folder.badge.questionmark") Image(systemName: "folder.badge.questionmark")
.foregroundStyle(.orange) .foregroundStyle(.orange)
Text("Project folder kept") Text("Project folder kept")
.font(.headline) .scarfStyle(.headline)
} }
Text("These files weren't installed by the template (the agent or you created them after install), so Scarf left them in place along with the folder itself.") Text("These files weren't installed by the template (the agent or you created them after install), so Scarf left them in place along with the folder itself.")
.font(.caption) .font(.caption)
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ScarfCore import ScarfCore
import ScarfDesign
import AppKit import AppKit
struct WebhooksView: View { struct WebhooksView: View {
@@ -25,7 +26,6 @@ struct WebhooksView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
header header
Divider()
if viewModel.isLoading && viewModel.webhooks.isEmpty { if viewModel.isLoading && viewModel.webhooks.isEmpty {
ProgressView().padding() ProgressView().padding()
} else if viewModel.webhookPlatformNotEnabled { } else if viewModel.webhookPlatformNotEnabled {
@@ -36,6 +36,7 @@ struct WebhooksView: View {
list list
} }
} }
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Webhooks") .navigationTitle("Webhooks")
.onAppear { viewModel.load() } .onAppear { viewModel.load() }
.sheet(isPresented: $showAddSheet) { addSheet } .sheet(isPresented: $showAddSheet) { addSheet }
@@ -52,25 +53,28 @@ struct WebhooksView: View {
} }
private var header: some View { private var header: some View {
HStack { ScarfPageHeader(
"Webhooks",
subtitle: "HTTP receivers that trigger sessions on incoming events."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message { if let msg = viewModel.message {
Label(msg, systemImage: "info.circle.fill") Label(msg, systemImage: "info.circle.fill")
.font(.caption) .scarfStyle(.caption)
.foregroundStyle(.green) .foregroundStyle(ScarfColor.success)
} }
Spacer() Button("Reload") { viewModel.load() }
.buttonStyle(ScarfGhostButton())
Button { Button {
resetAddForm() resetAddForm()
showAddSheet = true showAddSheet = true
} label: { } label: {
Label("Subscribe", systemImage: "plus") Label("Subscribe", systemImage: "plus")
} }
.controlSize(.small) .buttonStyle(ScarfPrimaryButton())
Button("Reload") { viewModel.load() } }
.controlSize(.small) .fixedSize(horizontal: true, vertical: false)
} }
.padding(.horizontal)
.padding(.vertical, 8)
} }
/// Shown when hermes reports the webhook platform isn't enabled. Direct users /// Shown when hermes reports the webhook platform isn't enabled. Direct users