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).
This commit is contained in:
Yaojia Wang
2026-07-06 07:25:32 +02:00
parent c44725618c
commit d01a69eb72
4 changed files with 56 additions and 31 deletions

View File

@@ -150,7 +150,10 @@ struct DSButtonStyle: ButtonStyle {
private var foreground: Color {
switch kind {
case .primary, .destructive: return .white
// Gold accent fill needs DARK ink for contrast (mirrors web
// --on-accent), NOT white. Red destructive fill keeps white.
case .primary: return DS.Palette.onAccent
case .destructive: return .white
case .secondary: return DS.Palette.accent
}
}

View File

@@ -6,11 +6,11 @@ import UIKit
/// 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, indigo/violet accent
/// (refined native, Apple HIG), dark-mode-first, amber-gold accent (desktop-matched)
/// continuing the web selection color.
///
/// Vocabulary (all under `DS.`):
/// - `Palette` adaptive `accent` (indigo) · semantic `status*` colors
/// - `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`.
@@ -33,9 +33,13 @@ enum DS {
/// status colors use the direction's exact hex so light/dark stay on-brand.
enum Palette {
// Accent (indigo/violet web selection color #7C8CFF)
// Adaptive: dark #7C8CFF, light #4F5BD5. Used sparingly primary
// actions, selection, active state only.
// 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())
@@ -46,21 +50,30 @@ enum DS {
static func accentUIColor() -> UIColor {
UIColor { trait in
trait.userInterfaceStyle == .dark
? UIColor(red: 0x7C / 255.0, green: 0x8C / 255.0, blue: 0xFF / 255.0, alpha: 1)
: UIColor(red: 0x4F / 255.0, green: 0x5B / 255.0, blue: 0xD5 / 255.0, alpha: 1)
? 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
}
}
// Semantic status colors (exact hex from the direction)
// These are the ONLY status colors. `StatusStyle` pairs each with a
// distinct SF Symbol so status is never conveyed by color alone.
/// 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)
/// working #34C759 (green).
static let statusWorking = rgb(52, 199, 89)
/// waiting / needs-me #FF9F0A (amber).
static let statusWaiting = rgb(255, 159, 10)
/// stuck #FF453A (red).
static let statusStuck = rgb(255, 69, 58)
// 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.
@@ -93,6 +106,14 @@ enum DS {
/// 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)

View File

@@ -125,11 +125,12 @@ struct TerminalScreen: View {
// MARK: - Terminal theme
/// Refined dark terminal theme (). The canvas follows the app surface
/// near-black in dark mode, so the terminal reads as part of the chrome while
/// the caret and selection use the one accent indigo. Every value routes through
/// `DS` (no literals); the glyph font is Dynamic-Type-aware SF Mono so terminal
/// text respects the user's text-size choice at launch.
/// Refined terminal theme () matching the desktop: a fixed warm-dark
/// canvas (#100F0D bg / #ECE9E3 fg, web --bg/--text) so the terminal reads the
/// same on iOS and desktop regardless of app appearance, with caret/selection
/// in the one gold accent. Every value routes through `DS` (no literals); the
/// glyph font is Dynamic-Type-aware SF Mono so terminal text respects the
/// user's text-size choice at launch.
private enum TerminalTheme {
@MainActor
static func apply(to terminal: TerminalView) {
@@ -137,8 +138,8 @@ private enum TerminalTheme {
ofSize: UIFont.preferredFont(forTextStyle: .footnote).pointSize,
weight: .regular
)
terminal.nativeBackgroundColor = UIColor(DS.Palette.surface)
terminal.nativeForegroundColor = UIColor(DS.Palette.textPrimary)
terminal.nativeBackgroundColor = UIColor(DS.Palette.terminalBackground)
terminal.nativeForegroundColor = UIColor(DS.Palette.terminalForeground)
terminal.caretColor = DS.Palette.accentUIColor()
terminal.selectedTextBackgroundColor = DS.Palette.accentUIColor()
}

View File

@@ -44,11 +44,11 @@ struct DesignSystemTests {
#expect(!approxEqual(waiting, stuck))
}
@Test("semantic status colors match the direction's hex")
@Test("semantic status colors match the desktop/web status palette")
func statusColorsMatchDirection() {
assertColor(StatusStyle.style(for: DisplayStatus.working).color, r: 52, g: 199, b: 89) // #34C759
assertColor(StatusStyle.style(for: DisplayStatus.waiting).color, r: 255, g: 159, b: 10) // #FF9F0A
assertColor(StatusStyle.style(for: DisplayStatus.stuck).color, r: 255, g: 69, b: 58) // #FF453A
assertColor(StatusStyle.style(for: DisplayStatus.working).color, r: 70, g: 208, b: 127) // #46D07F web --green
assertColor(StatusStyle.style(for: DisplayStatus.waiting).color, r: 245, g: 177, b: 76) // #F5B14C web --amber
assertColor(StatusStyle.style(for: DisplayStatus.stuck).color, r: 255, g: 107, b: 107) // #FF6B6B web --red
}
@Test("the wire ClaudeStatus bridge covers all five cases")
@@ -117,9 +117,9 @@ struct DesignSystemTests {
#expect(darkRGBA.a == 1)
#expect(lightRGBA.a == 1)
#expect(!approxEqual(darkRGBA, lightRGBA))
// On-brand indigo: dark #7C8CFF, light #4F5BD5.
assertRGBA(darkRGBA, r: 0x7C, g: 0x8C, b: 0xFF)
assertRGBA(lightRGBA, r: 0x4F, g: 0x5B, b: 0xD5)
// Desktop amber gold: dark #E3A64A (--accent), light #C9892F (--accent-2).
assertRGBA(darkRGBA, r: 0xE3, g: 0xA6, b: 0x4A)
assertRGBA(lightRGBA, r: 0xC9, g: 0x89, b: 0x2F)
}
// MARK: - Helpers