#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