import SwiftUI import UIKit /// # WebTerm Design System — token vocabulary (FROZEN public surface) /// /// The single source of truth for every visual constant in the App layer. /// Screens/components must reference these tokens — never inline colors, /// spacings, radii, opacities, durations or haptics. Direction: "精致原生" /// (refined native, Apple HIG), dark-mode-first, amber-gold accent (desktop-matched) /// continuing the web selection color. /// /// Vocabulary (all under `DS.`): /// - `Palette` — adaptive `accent` (amber gold, matches desktop) · semantic `status*` colors /// (color is NEVER the only status signal — pair with a symbol, /// see `StatusStyle`) · `surface`/`card`/`hairline` surfaces · /// `textPrimary`/`textSecondary`/`textTertiary`. /// - `Space` — 2·4·8·12·16·20·24 scale (`xs2`…`xxl24`). No off-scale gaps. /// - `Radius` — `sm8`/`md12`/`lg16`/`pill`. /// - `Stroke` — `hairline` (1pt border width). /// - `Opacity` — `stale`/`exited`/`pressed` dimming multipliers. /// - `Layout` — `minHitTarget` (44pt HIG minimum). /// - `Motion` — `fast`/`base` eased animations + `gated(_:reduceMotion:)` /// which collapses to `nil` under Reduce Motion. /// - `Haptics` — `selection`/`success`/`warning` (`@MainActor`). /// /// Companion files: `Typography.swift` (`DS.Typography`), `StatusStyle.swift` /// (`StatusStyle` / `DisplayStatus`), `Primitives.swift` (reusable views). enum DS { // MARK: - Palette /// Colors. `accent` is asset-free adaptive (no `.xcassets`); the semantic /// status colors use the direction's exact hex so light/dark stay on-brand. enum Palette { // ── Accent (amber gold — matches the desktop/web theme) ───────────── // The desktop web/Electron UI uses --accent #E3A64A ("amber gold", // public/style.css:14) on a warm near-neutral dark surface. We match it. // Adaptive: dark = #E3A64A (gold), light = #C9892F (deeper gold = // --accent-2) for adequate contrast on a light background. Used // sparingly — primary actions, selection, active state only. Gold needs // DARK text on top → use `onAccent`, never white/textPrimary. /// The one accent token. Inject once at the root via `.tint(DS.Palette.accent)`. static let accent = Color(uiColor: accentUIColor()) /// The accent as a dynamic `UIColor`. Exposed so tests can resolve the /// two schemes deterministically (`resolvedColor(with:)`) without going /// through the SwiftUI→UIKit bridge. static func accentUIColor() -> UIColor { UIColor { trait in trait.userInterfaceStyle == .dark ? UIColor(red: 0xE3 / 255.0, green: 0xA6 / 255.0, blue: 0x4A / 255.0, alpha: 1) // #E3A64A --accent : UIColor(red: 0xC9 / 255.0, green: 0x89 / 255.0, blue: 0x2F / 255.0, alpha: 1) // #C9892F --accent-2 } } /// Text/icon color to place ON an accent-filled surface (gold needs dark /// ink for contrast — mirrors web --on-accent #1A1305). static let onAccent = rgb(26, 19, 5) /// Faint accent wash (selection/soft highlight) — web --accent-soft. static let accentSoft = Color(red: 0xE3 / 255.0, green: 0xA6 / 255.0, blue: 0x4A / 255.0, opacity: 0.15) // ── Semantic status colors (match the desktop/web status palette) ─── // These are the ONLY status colors. `StatusStyle` pairs each with a // distinct SF Symbol so status is never conveyed by color alone. Hex // mirrors the web's warm status set (public/style.css:18-20) so iOS and // desktop read the same. `waiting` amber #F5B14C stays distinct from the // gold accent #E3A64A (brighter/less brown), and is only ever a small // badge fill, never a large accent surface. /// working — #46D07F (web --green). static let statusWorking = rgb(70, 208, 127) /// waiting / needs-me — #F5B14C (web --amber). static let statusWaiting = rgb(245, 177, 76) /// stuck — #FF6B6B (web --red). static let statusStuck = rgb(255, 107, 107) /// idle — quiet secondary gray (distinguished from `unknown` by shape). static let statusIdle = Color.secondary /// unknown / no signal yet — gray. static let statusUnknown = Color.gray /// exited — dimmed secondary (apply `Opacity.exited` on the container). static let statusExited = Color.secondary // ── Timeline event classes (T-iOS-24) ────────────────────────────── // Semantic colors for the activity-timeline event classes. `waiting` // and `stuck` reuse the status tokens above (same meaning); `done` // reuses `statusWorking` (a completed run). `tool`/`user` get their own // tokens so nothing in the app hardcodes a raw SwiftUI color. /// tool run — indigo, tied to the app accent family (#5E9EFF-ish). static let timelineTool = rgb(94, 158, 255) /// user message — violet (#AF7BFF), distinct from accent & tool. static let timelineUser = rgb(175, 123, 255) // ── Surfaces & text ──────────────────────────────────────────────── /// Base screen background. static let surface = Color(uiColor: .systemBackground) /// Card / grouped-content background (a step up from `surface`). static let card = Color(uiColor: .secondarySystemBackground) /// Hairline separator/border color. static let hairline = Color(uiColor: .separator) /// Primary label color. static let textPrimary = Color.primary /// Secondary label color (meta, captions). static let textSecondary = Color.secondary /// Tertiary label color (de-emphasized detail). static let textTertiary = Color(uiColor: .tertiaryLabel) // ── Terminal canvas (fixed warm-dark, matches the desktop terminal) ── // A terminal reads as dark regardless of app appearance (like the // desktop). Values mirror the web chrome: --bg #100F0D / --text #ECE9E3. /// Terminal background — warm near-black #100F0D (web --bg). static let terminalBackground = rgb(16, 15, 13) /// Terminal foreground — warm off-white #ECE9E3 (web --text). static let terminalForeground = rgb(236, 233, 227) /// Build an opaque sRGB color from 0–255 components. private static func rgb(_ r: Double, _ g: Double, _ b: Double) -> Color { Color(.sRGB, red: r / 255, green: g / 255, blue: b / 255, opacity: 1) } } // MARK: - Space /// Spacing scale — 2·4·8·12·16·20·24. Every gap/padding picks one of these; /// there are no in-between values (the audit's 3/5/6/10/14 all round here). enum Space { static let xs2: CGFloat = 2 static let xs4: CGFloat = 4 static let sm8: CGFloat = 8 static let md12: CGFloat = 12 static let lg16: CGFloat = 16 static let xl20: CGFloat = 20 static let xxl24: CGFloat = 24 } // MARK: - Radius /// Corner radii. Legacy 6/10 both fold into `sm8`/`md12`. enum Radius { static let sm8: CGFloat = 8 static let md12: CGFloat = 12 static let lg16: CGFloat = 16 /// Fully-rounded (capsule/pill). static let pill: CGFloat = 999 } // MARK: - Stroke /// Border widths. enum Stroke { /// Hairline card/overlay border. static let hairline: CGFloat = 1 } // MARK: - Opacity /// Dimming multipliers (used with `.opacity()`, sometimes `.grayscale()`). enum Opacity { /// Telemetry gone stale (past its TTL). static let stale: Double = 0.45 /// A session that has exited. static let exited: Double = 0.55 /// Pressed-state feedback on buttons. static let pressed: Double = 0.72 } // MARK: - Layout /// Layout constants that are not spacings. enum Layout { /// HIG minimum touch target (44×44pt). static let minHitTarget: CGFloat = 44 } // MARK: - Motion /// Animation tokens. Subtle, eased (~0.18–0.25s). ALWAYS route through /// `gated(_:reduceMotion:)` so Reduce Motion collapses to no motion. enum Motion { static let fastDuration: Double = 0.18 static let baseDuration: Double = 0.25 /// Quick affordance (chips, presses). static let fast = Animation.easeInOut(duration: fastDuration) /// Standard transition (banners, sheets, list changes). static let base = Animation.easeInOut(duration: baseDuration) /// Returns `animation` normally, or `nil` (instant, no motion) when /// Reduce Motion is enabled. Callers pass the environment flag. static func gated(_ animation: Animation, reduceMotion: Bool) -> Animation? { reduceMotion ? nil : animation } } // MARK: - Haptics /// Tactile feedback for key interactions. `@MainActor` — the underlying /// UIKit generators must be touched on the main thread. Additive to the /// existing `GateViewModel` gate-arrival haptic (different call sites). @MainActor enum Haptics { /// Light selection tap — opening a session, tapping a key. static func selection() { UIImpactFeedbackGenerator(style: .light).impactOccurred() } /// Success notification — a gate approve resolved. static func success() { UINotificationFeedbackGenerator().notificationOccurred(.success) } /// Warning notification — a destructive/reject decision. static func warning() { UINotificationFeedbackGenerator().notificationOccurred(.warning) } } }