From d01a69eb72ea5c88c84930ebbac1f198042898b0 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Mon, 6 Jul 2026 07:25:32 +0200 Subject: [PATCH] feat(ios): switch accent to desktop amber-gold theme (was indigo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- ios/App/WebTerm/DesignSystem/Primitives.swift | 5 +- ios/App/WebTerm/DesignSystem/Tokens.swift | 53 +++++++++++++------ ios/App/WebTerm/Screens/TerminalScreen.swift | 15 +++--- ios/App/WebTermTests/DesignSystemTests.swift | 14 ++--- 4 files changed, 56 insertions(+), 31 deletions(-) diff --git a/ios/App/WebTerm/DesignSystem/Primitives.swift b/ios/App/WebTerm/DesignSystem/Primitives.swift index 0b38cbe..e95ef79 100644 --- a/ios/App/WebTerm/DesignSystem/Primitives.swift +++ b/ios/App/WebTerm/DesignSystem/Primitives.swift @@ -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 } } diff --git a/ios/App/WebTerm/DesignSystem/Tokens.swift b/ios/App/WebTerm/DesignSystem/Tokens.swift index 55037c9..822004c 100644 --- a/ios/App/WebTerm/DesignSystem/Tokens.swift +++ b/ios/App/WebTerm/DesignSystem/Tokens.swift @@ -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 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) diff --git a/ios/App/WebTerm/Screens/TerminalScreen.swift b/ios/App/WebTerm/Screens/TerminalScreen.swift index 3701bd4..3f12e0b 100644 --- a/ios/App/WebTerm/Screens/TerminalScreen.swift +++ b/ios/App/WebTerm/Screens/TerminalScreen.swift @@ -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() } diff --git a/ios/App/WebTermTests/DesignSystemTests.swift b/ios/App/WebTermTests/DesignSystemTests.swift index ed35716..d906f3b 100644 --- a/ios/App/WebTermTests/DesignSystemTests.swift +++ b/ios/App/WebTermTests/DesignSystemTests.swift @@ -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