feat(design): adopt ScarfDesign system across Mac UI

Add a typed design-system package (Packages/ScarfDesign) with rust-tone
color tokens, type scale, spacing/radius tokens, ScarfPageHeader and
component primitives (ScarfCard, ScarfBadge, ScarfTextField,
ScarfSectionHeader, ScarfDivider, four button styles). Both Mac and iOS
targets `import ScarfDesign`.

Sidebar redesigned per design/static-site/ui-kit/Sidebar.jsx — glassy
translucent background, 224 px width, app-icon header with server pill,
custom tokenized rows with rust accent-tint when active, footer with
live Hermes-running indicator (wired to ServerLiveStatusRegistry).

14 mockup-backed feature screens redesigned: Settings, Dashboard,
Sessions, Memory, Chat (visual sweep), Activity, Cron, Insights,
MCPServers, Health, Logs, Tools (full); Projects light-touch.
Non-mockup features inherit rust through AccentColor.colorset repoint.

Mac AppIcon.appiconset replaced with the rust set. AccentColor.colorset
repointed to BrandRust hex (light + dark variants).

Visual sweep: every multi-button page-header / action-bar cluster now
wraps in .fixedSize(horizontal: true, vertical: false) so labels can't
wrap letter-by-letter at narrow widths (regression seen on the MCP
detail pane with 4 buttons).

Follow-ups landed:
- Sidebar Hermes-running probe wired to per-window
  ServerLiveStatusRegistry (no more placeholder green).
- Sessions: today filter predicate (isDateInToday(startedAt)); pill
  count reflects real count. Starred stays a no-op pending an upstream
  pinned/starred field on HermesSession.
- Dashboard: Recent activity column rendered alongside Recent sessions
  in a ViewThatFits 2-col grid. Populated from
  HermesDataService.fetchRecentToolCalls(limit:) flattened to
  ActivityEntry. ActivityEntry gains a public memberwise init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-25 13:27:54 +02:00
parent f04d95c960
commit 8a2d89654b
135 changed files with 11752 additions and 1434 deletions
+47
View File
@@ -0,0 +1,47 @@
// swift-tools-version: 6.0
// Scarf Design System typed token bridge + SwiftUI component primitives
// for the macOS and iOS apps.
//
// Ships color tokens (rust/amber brand, surfaces, foregrounds, semantic, tool
// kinds) as an Xcode asset catalog and a thin Swift API
// (`ScarfColor`, `ScarfFont`, `ScarfSpace`, `ScarfRadius`, `ScarfShadow`,
// `ScarfPrimaryButton`, `ScarfCard`, `ScarfBadge`, `ScarfTextField`,
// `ScarfSectionHeader`, `ScarfDivider`).
//
// Both app targets (`scarf`, `scarf mobile`) link this package and `import
// ScarfDesign`. The asset catalog is bundled as a `.process` resource so the
// `Color(name, bundle: .module)` lookups in `ScarfTheme.swift` resolve at
// runtime regardless of which app is hosting the bundle.
//
// Platform minimums match the rest of the workspace: macOS 14 + iOS 18.
import PackageDescription
let package = Package(
name: "ScarfDesign",
defaultLocalization: "en",
platforms: [
.macOS(.v14),
.iOS(.v18),
],
products: [
.library(
name: "ScarfDesign",
targets: ["ScarfDesign"]
),
],
targets: [
.target(
name: "ScarfDesign",
path: "Sources/ScarfDesign",
resources: [
.process("ScarfBrand.xcassets"),
],
swiftSettings: [
// Match ScarfCore / ScarfIOS Swift 5 language mode pending
// the workspace-wide bump to strict Swift 6 concurrency.
.swiftLanguageMode(.v5),
]
),
]
)
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.376",
"green": "0.576",
"red": "0.910"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.475",
"green": "0.659",
"red": "0.941"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.165",
"green": "0.353",
"red": "0.761"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.376",
"green": "0.576",
"red": "0.910"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.078",
"green": "0.180",
"red": "0.478"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.267",
"green": "0.471",
"red": "0.847"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.078",
"green": "0.180",
"red": "0.478"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.118",
"green": "0.282",
"red": "0.651"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.118",
"green": "0.282",
"red": "0.651"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.475",
"green": "0.659",
"red": "0.941"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.482",
"green": "0.522",
"red": "0.549"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.400",
"green": "0.435",
"red": "0.459"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.357",
"green": "0.392",
"red": "0.416"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.573",
"green": "0.612",
"red": "0.639"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.078",
"green": "0.094",
"red": "0.102"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.878",
"green": "0.910",
"red": "0.929"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "1.000",
"green": "1.000",
"red": "1.000"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "1.000",
"green": "1.000",
"red": "1.000"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.310",
"green": "0.325",
"red": "0.851"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.392",
"green": "0.408",
"red": "0.890"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.859",
"green": "0.596",
"red": "0.204"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.890",
"green": "0.686",
"red": "0.357"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.463",
"green": "0.659",
"red": "0.165"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.537",
"green": "0.745",
"red": "0.239"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.306",
"green": "0.678",
"red": "0.941"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.443",
"green": "0.745",
"red": "0.957"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.965",
"green": "0.976",
"red": "0.984"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.059",
"green": "0.075",
"red": "0.082"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "1.000",
"green": "1.000",
"red": "1.000"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.094",
"green": "0.110",
"red": "0.122"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.925",
"green": "0.945",
"red": "0.957"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.106",
"green": "0.125",
"red": "0.137"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.145",
"green": "0.165",
"red": "0.176"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.922",
"green": "0.973",
"red": "1.000"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.145",
"green": "0.165",
"red": "0.176"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.922",
"green": "0.973",
"red": "1.000"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.463",
"green": "0.659",
"red": "0.165"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.537",
"green": "0.745",
"red": "0.239"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.859",
"green": "0.596",
"red": "0.204"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.890",
"green": "0.686",
"red": "0.357"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.851",
"green": "0.424",
"red": "0.357"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.890",
"green": "0.549",
"red": "0.494"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.306",
"green": "0.678",
"red": "0.941"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.443",
"green": "0.745",
"red": "0.957"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.788",
"green": "0.357",
"red": "0.557"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.831",
"green": "0.494",
"red": "0.655"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,266 @@
//
// ScarfComponents.swift
// Scarf Design System opinionated SwiftUI component primitives.
//
// These mirror the buttons, cards, badges, and inputs used in the Scarf UI kit.
// Keep them small. Reach for them instead of inlining the same `.padding()
// .background() .clipShape()` chain across screens.
//
import SwiftUI
// MARK: - Buttons
public struct ScarfPrimaryButton: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.scarfStyle(.bodyEmph)
.foregroundStyle(ScarfColor.onAccent)
.padding(.horizontal, ScarfSpace.s4)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(configuration.isPressed ? ScarfColor.accentActive : ScarfColor.accent)
)
.scarfShadow(.sm)
.opacity(configuration.isPressed ? 0.95 : 1)
}
}
public struct ScarfSecondaryButton: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.scarfStyle(.bodyEmph)
.foregroundStyle(ScarfColor.foregroundPrimary)
.padding(.horizontal, ScarfSpace.s4)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(configuration.isPressed
? ScarfColor.borderStrong
: ScarfColor.backgroundSecondary)
.overlay(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.strokeBorder(ScarfColor.borderStrong, lineWidth: 1)
)
)
}
}
public struct ScarfGhostButton: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.scarfStyle(.bodyEmph)
.foregroundStyle(ScarfColor.foregroundPrimary)
.padding(.horizontal, ScarfSpace.s3)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(configuration.isPressed
? ScarfColor.accentTint
: Color.clear)
)
}
}
public struct ScarfDestructiveButton: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.scarfStyle(.bodyEmph)
.foregroundStyle(.white)
.padding(.horizontal, ScarfSpace.s4)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(ScarfColor.danger.opacity(configuration.isPressed ? 0.85 : 1.0))
)
}
}
// MARK: - Card
public struct ScarfCard<Content: View>: View {
let padding: CGFloat
let content: () -> Content
public init(padding: CGFloat = ScarfSpace.s4, @ViewBuilder content: @escaping () -> Content) {
self.padding = padding
self.content = content
}
public var body: some View {
content()
.padding(padding)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous)
.fill(ScarfColor.backgroundSecondary)
)
.overlay(
RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous)
.strokeBorder(ScarfColor.border, lineWidth: 1)
)
.scarfShadow(.sm)
}
}
// MARK: - Badge / Pill
public enum ScarfBadgeKind {
case neutral, brand, success, danger, warning, info
var fill: Color {
switch self {
case .neutral: return ScarfColor.backgroundTertiary
case .brand: return ScarfColor.accentTint
case .success: return ScarfColor.success.opacity(0.16)
case .danger: return ScarfColor.danger.opacity(0.16)
case .warning: return ScarfColor.warning.opacity(0.18)
case .info: return ScarfColor.info.opacity(0.16)
}
}
var fg: Color {
switch self {
case .neutral: return ScarfColor.foregroundMuted
case .brand: return ScarfColor.accent
case .success: return ScarfColor.success
case .danger: return ScarfColor.danger
case .warning: return ScarfColor.warning
case .info: return ScarfColor.info
}
}
}
public struct ScarfBadge: View {
let text: String
let kind: ScarfBadgeKind
public init(_ text: String, kind: ScarfBadgeKind = .neutral) {
self.text = text
self.kind = kind
}
public var body: some View {
Text(text)
.scarfStyle(.captionStrong)
.foregroundStyle(kind.fg)
.padding(.horizontal, ScarfSpace.s2)
.padding(.vertical, 3)
.background(
Capsule().fill(kind.fill)
)
}
}
// MARK: - Inputs
public struct ScarfTextField: View {
let placeholder: String
@Binding var text: String
public init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
}
public var body: some View {
TextField(placeholder, text: $text)
.textFieldStyle(.plain)
.scarfStyle(.body)
.padding(.horizontal, ScarfSpace.s3)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(ScarfColor.backgroundSecondary)
)
.overlay(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.strokeBorder(ScarfColor.borderStrong, lineWidth: 1)
)
}
}
// MARK: - Section header
public struct ScarfSectionHeader: View {
let title: String
let subtitle: String?
public init(_ title: String, subtitle: String? = nil) {
self.title = title
self.subtitle = subtitle
}
public var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(title)
.scarfStyle(.captionUppercase)
.foregroundStyle(ScarfColor.foregroundMuted)
if let subtitle {
Text(subtitle)
.scarfStyle(.footnote)
.foregroundStyle(ScarfColor.foregroundFaint)
}
}
}
}
// MARK: - Divider
public struct ScarfDivider: View {
public init() {}
public var body: some View {
Rectangle()
.fill(ScarfColor.border)
.frame(height: 1)
}
}
// MARK: - Page header
/// Standard page-level title/subtitle/actions header used at the top of
/// every feature route. Mirrors the `ContentHeader` component in the
/// design system's static-site / ui-kit. Drops a hairline divider at the
/// bottom so feature content can flush against it.
public struct ScarfPageHeader<Trailing: View>: View {
let title: String
let subtitle: String?
let trailing: Trailing
public init(_ title: String,
subtitle: String? = nil,
@ViewBuilder trailing: () -> Trailing = { EmptyView() }) {
self.title = title
self.subtitle = subtitle
self.trailing = trailing()
}
public var body: some View {
HStack(alignment: .top, spacing: ScarfSpace.s3) {
VStack(alignment: .leading, spacing: 2) {
Text(title)
.scarfStyle(.title2)
.foregroundStyle(ScarfColor.foregroundPrimary)
if let subtitle {
Text(subtitle)
.scarfStyle(.footnote)
.foregroundStyle(ScarfColor.foregroundMuted)
}
}
Spacer()
trailing
}
.padding(.horizontal, ScarfSpace.s6)
.padding(.top, ScarfSpace.s5)
.padding(.bottom, ScarfSpace.s4)
.overlay(
Rectangle()
.fill(ScarfColor.border)
.frame(height: 1),
alignment: .bottom
)
}
}
@@ -0,0 +1,146 @@
//
// ScarfPreview.swift
// Scarf Design System quick component preview.
//
// Open this file in Xcode and the canvas (P) shows every component at once,
// in light and dark. Use it to sanity-check the bundle works after install.
//
import SwiftUI
public struct ScarfPreviewGallery: View {
@State private var query = ""
@State private var draft = "Hello, Scarf"
public init() {}
public var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: ScarfSpace.s8) {
// Header
VStack(alignment: .leading, spacing: ScarfSpace.s2) {
Text("Scarf Design System")
.scarfStyle(.largeTitle)
.foregroundStyle(ScarfColor.foregroundPrimary)
Text("Component preview — light / dark resolves from the asset catalog.")
.scarfStyle(.subhead)
.foregroundStyle(ScarfColor.foregroundMuted)
}
// Buttons
section("Buttons") {
HStack(spacing: ScarfSpace.s3) {
Button("Primary") {}.buttonStyle(ScarfPrimaryButton())
Button("Secondary") {}.buttonStyle(ScarfSecondaryButton())
Button("Ghost") {}.buttonStyle(ScarfGhostButton())
Button("Delete") {}.buttonStyle(ScarfDestructiveButton())
}
}
// Badges
section("Badges") {
HStack(spacing: ScarfSpace.s2) {
ScarfBadge("Neutral")
ScarfBadge("Brand", kind: .brand)
ScarfBadge("Success", kind: .success)
ScarfBadge("Warning", kind: .warning)
ScarfBadge("Danger", kind: .danger)
ScarfBadge("Info", kind: .info)
}
}
// Inputs
section("Inputs") {
VStack(alignment: .leading, spacing: ScarfSpace.s3) {
ScarfTextField("Search", text: $query)
ScarfTextField("Compose a message", text: $draft)
}
}
// Card
section("Card") {
ScarfCard {
VStack(alignment: .leading, spacing: ScarfSpace.s2) {
ScarfSectionHeader("Connection", subtitle: "anthropic.com")
ScarfDivider()
HStack {
Text("Status")
.scarfStyle(.body)
.foregroundStyle(ScarfColor.foregroundMuted)
Spacer()
ScarfBadge("Connected", kind: .success)
}
HStack {
Text("Last run")
.scarfStyle(.body)
.foregroundStyle(ScarfColor.foregroundMuted)
Spacer()
Text("2 min ago")
.scarfStyle(.bodyEmph)
.foregroundStyle(ScarfColor.foregroundPrimary)
}
}
}
}
// Tool kind swatches (chat)
section("Tool kinds") {
HStack(spacing: ScarfSpace.s3) {
toolSwatch("Bash", ScarfColor.Tool.bash)
toolSwatch("Edit", ScarfColor.Tool.edit)
toolSwatch("Search", ScarfColor.Tool.search)
toolSwatch("Web", ScarfColor.Tool.web)
toolSwatch("Think", ScarfColor.Tool.think)
}
}
// Brand gradient
section("Brand gradient") {
RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous)
.fill(ScarfGradient.brand)
.frame(height: 80)
.overlay(
Text("amber → rust → deep")
.scarfStyle(.subhead)
.foregroundStyle(.white)
)
}
}
.padding(ScarfSpace.s8)
}
.background(ScarfColor.backgroundPrimary.ignoresSafeArea())
}
@ViewBuilder
private func section<Content: View>(_ title: String,
@ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: ScarfSpace.s3) {
Text(title)
.scarfStyle(.captionUppercase)
.foregroundStyle(ScarfColor.foregroundMuted)
content()
}
}
private func toolSwatch(_ name: String, _ color: Color) -> some View {
VStack(spacing: ScarfSpace.s1) {
Circle().fill(color).frame(width: 24, height: 24)
Text(name)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
}
}
}
#Preview("Light") {
ScarfPreviewGallery()
.frame(width: 720, height: 900)
.preferredColorScheme(.light)
}
#Preview("Dark") {
ScarfPreviewGallery()
.frame(width: 720, height: 900)
.preferredColorScheme(.dark)
}
@@ -0,0 +1,154 @@
//
// ScarfTheme.swift
// Scarf Design System Swift token bridge
//
// Mirrors colors_and_type.css. All colors resolve from ScarfBrand.xcassets,
// so light/dark variants come from the asset catalog automatically.
//
// Usage:
// Text("Hello").foregroundStyle(ScarfColor.foregroundPrimary)
// RoundedRectangle(cornerRadius: ScarfRadius.lg)
// .fill(ScarfColor.backgroundSecondary)
// .overlay(RoundedRectangle(cornerRadius: ScarfRadius.lg)
// .strokeBorder(ScarfColor.border, lineWidth: 1))
//
// Drop-in: add this file + ScarfBrand.xcassets to your target. Nothing else.
//
import SwiftUI
// MARK: - Colors
/// All Scarf brand colors. Resolves from ScarfBrand.xcassets (light + dark).
public enum ScarfColor {
fileprivate static func asset(_ name: String) -> Color {
Color(name, bundle: .module)
}
// Brand
public static let brandRust = asset("Brand/BrandRust")
public static let brandRustHover = asset("Brand/BrandRustHover")
public static let brandRustActive = asset("Brand/BrandRustActive")
public static let brandAmber = asset("Brand/BrandAmber")
public static let brandRustDeep = asset("Brand/BrandRustDeep")
/// Semantic alias: the "primary" accent. Use this in component code,
/// not `brandRust` directly it lets you re-skin without a refactor.
public static var accent: Color { brandRust }
public static var accentHover: Color { brandRustHover }
public static var accentActive: Color { brandRustActive }
/// Tinted accent for hover halos, selection backgrounds.
public static var accentTint: Color { brandRust.opacity(0.10) }
public static var accentTintStrong: Color { brandRust.opacity(0.18) }
// Surfaces
public static let backgroundPrimary = asset("Surface/BackgroundPrimary")
public static let backgroundSecondary = asset("Surface/BackgroundSecondary")
public static let backgroundTertiary = asset("Surface/BackgroundTertiary")
/// Use at low alpha (0.040.10) for subtle fills/dividers.
public static var border: Color { asset("Surface/Border").opacity(0.08) }
public static var borderStrong: Color { asset("Surface/BorderStrong").opacity(0.14) }
// Foreground
public static let foregroundPrimary = asset("Foreground/ForegroundPrimary")
public static let foregroundMuted = asset("Foreground/ForegroundMuted")
public static let foregroundFaint = asset("Foreground/ForegroundFaint")
public static let onAccent = asset("Foreground/OnAccent")
// Semantic
public static let success = asset("Semantic/SemanticSuccess")
public static let danger = asset("Semantic/SemanticDanger")
public static let warning = asset("Semantic/SemanticWarning")
public static let info = asset("Semantic/SemanticInfo")
// Tool kinds (chat message decorations)
public enum Tool {
public static let bash = ScarfColor.asset("Tool/ToolBash")
public static let edit = ScarfColor.asset("Tool/ToolEdit")
public static let search = ScarfColor.asset("Tool/ToolSearch")
public static let web = ScarfColor.asset("Tool/ToolWeb")
public static let think = ScarfColor.asset("Tool/ToolThink")
}
}
// MARK: - Gradients
public enum ScarfGradient {
/// Tri-stop amber rust deep rust. Used on app icon, hero buttons, brand splashes.
public static let brand = LinearGradient(
colors: [
Color(red: 0.910, green: 0.576, blue: 0.376), // #E89360
Color(red: 0.761, green: 0.353, blue: 0.165), // #C25A2A
Color(red: 0.478, green: 0.180, blue: 0.078) // #7A2E14
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
/// Soft amber wash for empty states, onboarding moments.
public static let brandSoft = LinearGradient(
colors: [
Color(red: 0.965, green: 0.878, blue: 0.796), // #F6E0CB
Color(red: 0.937, green: 0.773, blue: 0.620) // #EFC59E
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
// MARK: - Radii / spacing / shadow
public enum ScarfRadius {
public static let sm: CGFloat = 4
public static let md: CGFloat = 6
public static let lg: CGFloat = 8
public static let xl: CGFloat = 12
public static let xxl: CGFloat = 14
public static let pill: CGFloat = 999
}
public enum ScarfSpace {
public static let s1: CGFloat = 4
public static let s2: CGFloat = 8
public static let s3: CGFloat = 12
public static let s4: CGFloat = 16
public static let s5: CGFloat = 20
public static let s6: CGFloat = 24
public static let s8: CGFloat = 32
public static let s10: CGFloat = 40
}
public struct ScarfShadow {
public let color: Color
public let radius: CGFloat
public let x: CGFloat
public let y: CGFloat
public static let sm = ScarfShadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
public static let md = ScarfShadow(color: .black.opacity(0.07), radius: 12, x: 0, y: 4)
public static let lg = ScarfShadow(color: .black.opacity(0.10), radius: 24, x: 0, y: 8)
public static let xl = ScarfShadow(color: .black.opacity(0.14), radius: 40, x: 0, y: 16)
}
public extension View {
func scarfShadow(_ s: ScarfShadow) -> some View {
self.shadow(color: s.color, radius: s.radius, x: s.x, y: s.y)
}
}
// MARK: - Motion
public enum ScarfDuration {
public static let fast: Double = 0.12
public static let base: Double = 0.20
public static let slow: Double = 0.30
}
public enum ScarfAnimation {
/// "Smooth" spring matching the cubic-bezier(0.32, 0.72, 0, 1) easing in CSS.
public static let smooth = Animation.spring(response: 0.35, dampingFraction: 0.85)
public static let fast = Animation.easeOut(duration: ScarfDuration.fast)
public static let base = Animation.easeOut(duration: ScarfDuration.base)
}
@@ -0,0 +1,85 @@
//
// ScarfTypography.swift
// Scarf Design System Apple HIG-aligned type scale
//
// Uses SF Pro (system) for UI text and SF Mono for code/transcripts.
// Sizes mirror the CSS tokens (--text-caption ... --text-largeTitle).
//
// Usage:
// Text("Settings").font(ScarfFont.title2)
// Text(message).font(ScarfFont.body)
// Text("v1.2.0").font(ScarfFont.caption).foregroundStyle(ScarfColor.foregroundMuted)
//
import SwiftUI
public enum ScarfFont {
// Display / titles use rounded SF Pro Display (`.default` design + tight tracking).
public static let largeTitle = Font.system(size: 34, weight: .semibold, design: .default)
public static let title1 = Font.system(size: 28, weight: .semibold, design: .default)
public static let title2 = Font.system(size: 22, weight: .semibold, design: .default)
public static let title3 = Font.system(size: 20, weight: .semibold, design: .default)
// Body & labels
public static let headline = Font.system(size: 17, weight: .semibold)
public static let subhead = Font.system(size: 16, weight: .medium)
public static let callout = Font.system(size: 15, weight: .regular)
public static let body = Font.system(size: 14, weight: .regular)
public static let bodyEmph = Font.system(size: 14, weight: .medium)
public static let footnote = Font.system(size: 13, weight: .regular)
public static let caption = Font.system(size: 12, weight: .regular)
public static let captionStrong = Font.system(size: 12, weight: .semibold)
public static let caption2 = Font.system(size: 10, weight: .medium)
// Code / mono for transcripts, command output, file paths.
public static let mono = Font.system(size: 13, weight: .regular, design: .monospaced)
public static let monoSmall = Font.system(size: 12, weight: .regular, design: .monospaced)
}
/// Convenience text styles. Apply with `.scarfStyle(.headline)`.
public enum ScarfTextStyle {
case largeTitle, title1, title2, title3
case headline, subhead, body, bodyEmph, callout, footnote
case caption, captionStrong, captionUppercase
case mono, code
var font: Font {
switch self {
case .largeTitle: return ScarfFont.largeTitle
case .title1: return ScarfFont.title1
case .title2: return ScarfFont.title2
case .title3: return ScarfFont.title3
case .headline: return ScarfFont.headline
case .subhead: return ScarfFont.subhead
case .body: return ScarfFont.body
case .bodyEmph: return ScarfFont.bodyEmph
case .callout: return ScarfFont.callout
case .footnote: return ScarfFont.footnote
case .caption,
.captionUppercase: return ScarfFont.caption
case .captionStrong: return ScarfFont.captionStrong
case .mono, .code: return ScarfFont.mono
}
}
}
public extension View {
/// Apply a Scarf type style. Handles font + tracking + (for `captionUppercase`) text-case.
@ViewBuilder
func scarfStyle(_ style: ScarfTextStyle) -> some View {
switch style {
case .largeTitle:
self.font(style.font).tracking(-0.7)
case .title1:
self.font(style.font).tracking(-0.5)
case .title2, .title3:
self.font(style.font).tracking(-0.3)
case .captionUppercase:
self.font(ScarfFont.captionStrong)
.textCase(.uppercase)
.tracking(0.5)
default:
self.font(style.font)
}
}
}