Files
scarf/scarf/scarf/Features/Common/LoadingOverlay.swift
T
Alan Wizemann 23dd8becb9 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>
2026-04-25 14:39:13 +02:00

76 lines
3.2 KiB
Swift

import SwiftUI
import ScarfDesign
/// Translucent loading overlay used by feature views while their VM's
/// `load()` runs in the background. Shows a centered ProgressView with
/// optional label; the underlying content stays visible (just dimmed)
/// when it's already populated, or the overlay fully covers an empty
/// section so the user sees activity instead of "nothing here yet".
///
/// Usage:
/// ```swift
/// SomeContent()
/// .loadingOverlay(viewModel.isLoading, label: "Loading credentials", isEmpty: viewModel.pools.isEmpty)
/// ```
///
/// The `isEmpty` flag controls whether the overlay covers the full view
/// (when there's no stale content to show under it) or just dims it
/// (when refreshing existing data).
struct LoadingOverlay: ViewModifier {
let isLoading: Bool
let label: String
let isEmpty: Bool
func body(content: Content) -> some View {
content
.overlay {
if isLoading {
if isEmpty {
// Full cover: empty state. User has no data to look at,
// so own the whole pane with the spinner.
VStack(spacing: ScarfSpace.s3) {
ProgressView()
.controlSize(.large)
Text(label)
.scarfStyle(.callout)
.foregroundStyle(ScarfColor.foregroundMuted)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(ScarfColor.backgroundPrimary)
} else {
// Stale-content refresh: top-trailing pill so the
// user sees data is being refreshed without losing
// their place.
VStack {
HStack {
Spacer()
HStack(spacing: 6) {
ProgressView()
.controlSize(.small)
Text(label)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
}
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(.thinMaterial, in: Capsule())
.padding(ScarfSpace.s2)
}
Spacer()
}
}
}
}
}
}
extension View {
/// Show a loading indicator while `isLoading` is true. If `isEmpty` is
/// also true, the indicator covers the full view; otherwise it shows
/// as a small refresh pill in the top-trailing corner so existing
/// content stays visible.
func loadingOverlay(_ isLoading: Bool, label: String = "Loading…", isEmpty: Bool = false) -> some View {
modifier(LoadingOverlay(isLoading: isLoading, label: label, isEmpty: isEmpty))
}
}