Zum Inhalt

apple/PhysarumApp/Sources/UI/PhysarumTheme.swift

#if !os(tvOS)
import SwiftUI

extension Color {
    /// sRGB color from a 6-digit hex string (KSP tokens are authored as hex).
    init(hex: String) {
        let h = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex
        var v: UInt64 = 0
        Scanner(string: h).scanHexInt64(&v)
        self.init(
            .sRGB,
            red: Double((v >> 16) & 0xFF) / 255,
            green: Double((v >> 8) & 0xFF) / 255,
            blue: Double(v & 0xFF) / 255,
            opacity: 1
        )
    }
}

/// The Kuro Signal Protocol design tokens, ported from src/styles/ksp-tokens.css.
/// Mono uses the system monospaced design (SF Mono) instead of bundling JetBrains
/// Mono — native-idiomatic, same characteristic look.
enum Theme {
    enum Colors {
        // Void scale (surfaces)
        static let vault = Color(hex: "060709")
        static let surface = Color(hex: "0b0d11")
        static let raised = Color(hex: "11141a")
        static let overlay = Color(hex: "181c24")
        static let borderSubtle = Color(hex: "22272f")
        static let border = Color(hex: "2e343d")
        static let borderStrong = Color(hex: "3d4450")
        static let track = Color(hex: "22272f")
        // Foreground
        static let fgPrimary = Color(hex: "e8e4d8")    // pearl
        static let fgSecondary = Color(hex: "828a97")
        static let fgTertiary = Color(hex: "5a6170")
        // Accents
        static let accent = Color(hex: "39ff7a")       // phosphor
        static let ember = Color(hex: "ffb442")
        static let crimson = Color(hex: "d4203a")

        /// tagSignal name → its signal color.
        static func signal(_ name: String) -> Color {
            switch name {
            case "crimson": return Color(hex: "d4203a")
            case "phosphor": return Color(hex: "39ff7a")
            case "ember": return Color(hex: "ffb442")
            case "spectre": return Color(hex: "a878ff")
            case "circuit": return Color(hex: "4ac8d8")
            case "ghost": return Color(hex: "7ab8c4")
            case "biolink": return Color(hex: "8bbf87")
            case "neural-bleed": return Color(hex: "e8a5a5")
            case "rust": return Color(hex: "e8b979")
            case "toxic": return Color(hex: "d9c566")
            case "voidwitch": return Color(hex: "b49bd1")
            default: return Color(hex: "e8e4d8")        // pearl
            }
        }
    }

    enum Space {
        static let s1: CGFloat = 4
        static let s2: CGFloat = 8
        static let s3: CGFloat = 12
        static let s4: CGFloat = 16
        static let s5: CGFloat = 24
        static let s6: CGFloat = 32
    }

    enum Radius {
        static let sm: CGFloat = 3
        static let md: CGFloat = 6
        static let lg: CGFloat = 10
        static let xl: CGFloat = 14
    }

    enum Fonts {
        static let eyebrow = FontRole.eyebrow.font
        static let label = FontRole.label.font
        static let value = FontRole.value.font
        static let title = FontRole.title.font
        static let body = FontRole.body.font
    }
}

/// The five semantic text roles, each mapped to a Dynamic-Type text style so the
/// UI scales with the user's preferred size. The mono roles keep the SF-Mono look.
/// Defined as a pure mapping so the scaling contract is unit-testable (SwiftUI
/// `Font` itself is opaque). The chosen styles match the previous fixed sizes at
/// the default content size (caption2≈11, caption≈12, subheadline≈15, footnote≈13).
enum FontRole: CaseIterable {
    case eyebrow, label, value, title, body

    var textStyle: Font.TextStyle {
        switch self {
        case .eyebrow: return .caption2
        case .label, .value: return .caption
        case .title: return .subheadline
        case .body: return .footnote
        }
    }

    var weight: Font.Weight {
        switch self {
        case .eyebrow: return .semibold
        case .label, .value: return .medium
        case .title: return .semibold
        case .body: return .regular
        }
    }

    var monospaced: Bool {
        switch self {
        case .eyebrow, .label, .value: return true
        case .title, .body: return false
        }
    }

    var font: Font {
        Font.system(textStyle, design: monospaced ? .monospaced : .default).weight(weight)
    }
}
#endif