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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) ───────────
|
||||
/// 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.
|
||||
|
||||
/// 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)
|
||||
// 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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user