Files
web-terminal/ios/App/WebTerm/DesignSystem/Tokens.swift
Yaojia Wang d01a69eb72 feat(ios): switch accent to desktop amber-gold theme (was indigo)
Match the desktop/web theme (public/style.css): accent #E3A64A gold (dark) /
#C9892F (light) replacing the indigo, +onAccent #1A1305 dark ink for gold-fill
buttons, +accentSoft. Status colors realigned to the web warm palette
(#46D07F/#F5B14C/#FF6B6B). Terminal canvas fixed to the desktop warm-dark
(#100F0D bg / #ECE9E3 fg) with gold caret/selection. DSButtonStyle primary now
uses onAccent (dark) instead of white for contrast on gold. All via the single
accent token + DS palette — no scattered edits.

Verified: iPhone 16 290 + iPad Pro 11 290 tests green (DesignSystemTests assert
the exact gold + web-status RGB in both schemes).
2026-07-06 07:25:32 +02:00

219 lines
9.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 SwiftUIUIKit 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 0255 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.180.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)
}
}
}