fix(ios): centralize app accent colors (#94627)

Move iOS accent and status colors through design tokens so raw SwiftUI color literals are blocked outside token definitions.

Set the app-wide tint in SwiftUI and UIKit from code, without relying on Assets.xcassets AccentColor.
This commit is contained in:
Sash Zats
2026-06-22 16:20:37 -04:00
committed by GitHub
parent 2dc2d73b07
commit 5d9daea2b0
19 changed files with 74 additions and 34 deletions
+17 -1
View File
@@ -2,8 +2,24 @@ parent_config: ../../config/swiftlint.yml
included:
- Sources
- ../shared/ClawdisNodeKit/Sources
- ShareExtension
- ActivityWidget
- WatchApp
- ../shared/OpenClawKit/Sources/OpenClawChatUI
excluded:
- ../macos
type_body_length:
warning: 900
error: 1300
custom_rules:
openclaw_design_colors:
name: "OpenClaw design colors"
excluded:
- Sources/Design/OpenClawBrand.swift
- ../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTheme.swift
regex: '(Color\.accentColor|(^|[^A-Za-z0-9_])\.accentColor\b|Color\.(red|green|orange|cyan|blue|yellow|purple|pink)\b|\.(foregroundStyle|tint|fill|stroke|strokeBorder|background)\(\s*\.(red|green|orange|cyan|blue|yellow|purple|pink)\b|Color\(red:\s*0\s*/\s*255\.0,\s*green:\s*122\s*/\s*255\.0,\s*blue:\s*255\s*/\s*255\.0\))'
message: "Use OpenClawBrand or OpenClawChatTheme design tokens instead of raw accent/status colors."
severity: error
@@ -74,23 +74,30 @@ struct OpenClawLiveActivity: Widget {
private func statusIcon(state: OpenClawActivityAttributes.ContentState) -> some View {
if state.isConnecting {
Image(systemName: "arrow.triangle.2.circlepath")
.foregroundStyle(.cyan)
.foregroundStyle(OpenClawActivityStyle.info)
} else if state.isDisconnected {
Image(systemName: "wifi.slash")
.foregroundStyle(.red)
.foregroundStyle(OpenClawActivityStyle.danger)
} else if state.isIdle {
Image(systemName: "checkmark")
.foregroundStyle(.green)
.foregroundStyle(OpenClawActivityStyle.ok)
} else {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
.foregroundStyle(OpenClawActivityStyle.warn)
}
}
private func dotColor(state: OpenClawActivityAttributes.ContentState) -> Color {
if state.isDisconnected { return .red }
if state.isConnecting { return .cyan }
if state.isIdle { return .green }
return .orange
if state.isDisconnected { return OpenClawActivityStyle.danger }
if state.isConnecting { return OpenClawActivityStyle.info }
if state.isIdle { return OpenClawActivityStyle.ok }
return OpenClawActivityStyle.warn
}
}
private enum OpenClawActivityStyle {
static let info = Color(red: 0, green: 122 / 255.0, blue: 1)
static let danger = Color(red: 185 / 255.0, green: 28 / 255.0, blue: 28 / 255.0)
static let ok = Color(red: 34 / 255.0, green: 197 / 255.0, blue: 94 / 255.0)
static let warn = Color(red: 245 / 255.0, green: 158 / 255.0, blue: 11 / 255.0)
}
@@ -506,7 +506,7 @@ extension AgentProTab {
func skillEditorSwitchIndicator(isOn: Bool) -> some View {
Capsule()
.fill(isOn ? Color.accentColor : Color.secondary.opacity(0.35))
.fill(isOn ? OpenClawBrand.accent : Color.secondary.opacity(0.35))
.frame(width: 52, height: 32)
.overlay(alignment: isOn ? .trailing : .leading) {
Circle()
+1 -1
View File
@@ -105,7 +105,7 @@ struct AgentProTab: View {
var color: Color {
switch self {
case .online: OpenClawBrand.ok
case .ready: Color(red: 0 / 255.0, green: 122 / 255.0, blue: 255 / 255.0)
case .ready: OpenClawBrand.info
}
}
}
+1 -1
View File
@@ -277,7 +277,7 @@ struct ChatProTab: View {
}
private var chatUserAccent: Color {
self.colorScheme == .light ? Color(red: 0 / 255.0, green: 122 / 255.0, blue: 255 / 255.0) : OpenClawBrand.accent
self.colorScheme == .light ? OpenClawBrand.info : OpenClawBrand.accent
}
private var activeAgent: AgentSummary? {
@@ -1036,7 +1036,7 @@ struct IPadSkillProposalRow: View {
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
self.isSelected ? Color.red.opacity(0.08) : Color.clear,
self.isSelected ? OpenClawBrand.danger.opacity(0.08) : Color.clear,
in: RoundedRectangle(cornerRadius: 8, style: .continuous))
}
}
+5 -2
View File
@@ -47,11 +47,13 @@ enum AppAppearancePreference: String, CaseIterable, Identifiable {
}
enum OpenClawBrand {
static let accent = Color(uiColor: UIColor { traits in
static let uiAccent = UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 198 / 255.0, green: 62 / 255.0, blue: 56 / 255.0, alpha: 1)
: UIColor(red: 183 / 255.0, green: 56 / 255.0, blue: 51 / 255.0, alpha: 1)
})
}
static let accent = Color(uiColor: Self.uiAccent)
static let accentHot = Color(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 232 / 255.0, green: 92 / 255.0, blue: 86 / 255.0, alpha: 1)
@@ -64,6 +66,7 @@ enum OpenClawBrand {
})
static let ok = Color(red: 34 / 255.0, green: 197 / 255.0, blue: 94 / 255.0)
static let warn = Color(red: 245 / 255.0, green: 158 / 255.0, blue: 11 / 255.0)
static let info = Color(red: 0 / 255.0, green: 122 / 255.0, blue: 255 / 255.0)
static let graphite = Color(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 20 / 255.0, green: 22 / 255.0, blue: 24 / 255.0, alpha: 1)
@@ -119,7 +119,7 @@ extension SettingsProTab {
self.gatewayActionButton(
title: "Diagnose",
icon: "cross.case",
color: Color(red: 0 / 255.0, green: 122 / 255.0, blue: 255 / 255.0),
color: OpenClawBrand.info,
isBusy: self.isRefreshingGateway)
{
Task { await self.runDiagnostics() }
@@ -476,7 +476,7 @@ extension SettingsProTab {
self.gatewayActionButton(
title: "Run Diagnostics",
icon: "cross.case",
color: Color(red: 0 / 255.0, green: 122 / 255.0, blue: 255 / 255.0),
color: OpenClawBrand.info,
isBusy: self.isRefreshingGateway)
{
Task { await self.runDiagnostics() }
@@ -1040,7 +1040,7 @@ extension SettingsProTab {
func settingsSwitchIndicator(isOn: Bool) -> some View {
Capsule()
.fill(isOn ? Color.accentColor : Color.secondary.opacity(0.35))
.fill(isOn ? OpenClawBrand.accent : Color.secondary.opacity(0.35))
.frame(width: 52, height: 32)
.overlay(alignment: isOn ? .trailing : .leading) {
Circle()
@@ -90,7 +90,7 @@ private struct ExecApprovalPromptCard: View {
if let errorText = self.normalized(self.errorText) {
Text(errorText)
.font(.footnote)
.foregroundStyle(.red)
.foregroundStyle(OpenClawBrand.danger)
}
if self.isResolving {
@@ -40,7 +40,7 @@ struct OnboardingIntroStep: View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.title3.weight(.semibold))
.foregroundStyle(.orange)
.foregroundStyle(OpenClawBrand.warn)
.frame(width: 24)
.padding(.top, 2)
@@ -177,7 +177,7 @@ struct OnboardingModeRow: View {
}
Spacer()
Image(systemName: self.selected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(self.selected ? Color.accentColor : Color.secondary)
.foregroundStyle(self.selected ? OpenClawBrand.accent : Color.secondary)
}
.contentShape(Rectangle())
}
@@ -378,7 +378,7 @@ struct OnboardingWizardView: View {
private func onboardingSwitchIndicator(isOn: Bool) -> some View {
Capsule()
.fill(isOn ? Color.accentColor : Color.secondary.opacity(0.35))
.fill(isOn ? OpenClawBrand.accent : Color.secondary.opacity(0.35))
.frame(width: 52, height: 32)
.overlay(alignment: isOn ? .trailing : .leading) {
Circle()
@@ -575,7 +575,7 @@ struct OnboardingWizardView: View {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 64))
.foregroundStyle(.green)
.foregroundStyle(OpenClawBrand.ok)
.padding(.bottom, 20)
Text("Connected")
+2
View File
@@ -632,6 +632,7 @@ struct OpenClawApp: App {
var body: some Scene {
WindowGroup {
RootTabs()
.tint(OpenClawBrand.accent)
.preferredColorScheme(self.appearancePreference.colorScheme)
.environment(self.appModel)
.environment(self.appModel.voiceWake)
@@ -686,6 +687,7 @@ struct OpenClawApp: App {
.flatMap(\.windows)
.forEach { window in
window.overrideUserInterfaceStyle = style
window.tintColor = OpenClawBrand.uiAccent
}
}
}
@@ -35,7 +35,7 @@ struct TalkPermissionPromptView: View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: self.iconSystemName)
.font(.title3.weight(.semibold))
.foregroundStyle(self.requestIsPending ? Color.orange : Color.accentColor)
.foregroundStyle(self.requestIsPending ? OpenClawBrand.warn : OpenClawBrand.accent)
.frame(width: 28, height: 28)
VStack(alignment: .leading, spacing: 6) {
@@ -51,7 +51,7 @@ struct TalkPermissionPromptView: View {
if let failureMessage = self.state.failureMessage {
Label(failureMessage, systemImage: "exclamationmark.triangle.fill")
.font(.footnote)
.foregroundStyle(.red)
.foregroundStyle(OpenClawBrand.danger)
.fixedSize(horizontal: false, vertical: true)
}
@@ -99,7 +99,7 @@ struct TalkPermissionPromptView: View {
.overlay {
if self.style == .card || self.style == .sheet {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.accentColor.opacity(0.20), lineWidth: 1)
.stroke(OpenClawBrand.accent.opacity(0.20), lineWidth: 1)
}
}
.task(id: self.pollTaskKey) {
@@ -283,7 +283,7 @@ struct OpenClawChatComposer: View {
}
.padding(.horizontal, 8)
.padding(.vertical, 5)
.background(Color.accentColor.opacity(0.08))
.background(OpenClawChatTheme.accent.opacity(0.08))
.clipShape(Capsule())
}
}
@@ -550,7 +550,7 @@ struct OpenClawChatComposer: View {
.frame(width: self.sendButtonSize, height: self.sendButtonSize)
.background(
RoundedRectangle(cornerRadius: self.sendButtonCornerRadius, style: .continuous)
.fill(Color.red))
.fill(OpenClawChatTheme.danger))
.contentShape(RoundedRectangle(cornerRadius: self.sendButtonCornerRadius, style: .continuous))
.accessibilityLabel("Stop response")
.disabled(self.viewModel.isAborting)
@@ -57,7 +57,7 @@ private struct ChatMarkdownStyle: ViewModifier {
}
private var inlineStyle: InlineStyle {
let linkColor: Color = self.context == .user ? self.textColor : .accentColor
let linkColor: Color = self.context == .user ? self.textColor : OpenClawChatTheme.accent
let codeScale: CGFloat = self.variant == .compact ? 0.85 : 0.9
return InlineStyle()
.code(.monospaced, .fontScale(codeScale))
@@ -25,7 +25,7 @@ struct ChatAgentAvatar: View {
.fill(
LinearGradient(
colors: [
(self.tint ?? Color.accentColor).opacity(0.95),
(self.tint ?? OpenClawChatTheme.accent).opacity(0.95),
Color(red: 38 / 255.0, green: 40 / 255.0, blue: 43 / 255.0),
],
startPoint: .topLeading,
@@ -33,7 +33,7 @@ struct ChatAgentAvatar: View {
.overlay(
Circle()
.strokeBorder(Color.white.opacity(0.18), lineWidth: 1))
.shadow(color: (self.tint ?? Color.accentColor).opacity(0.18), radius: 8, y: 4)
.shadow(color: (self.tint ?? OpenClawChatTheme.accent).opacity(0.18), radius: 8, y: 4)
.accessibilityLabel(self.name.map { "\($0) avatar" } ?? "Agent avatar")
}
@@ -152,6 +152,18 @@ enum OpenClawChatTheme {
#endif
}
static var accent: Color {
self.userBubble
}
static var danger: Color {
#if os(macOS)
Color(nsColor: .systemRed)
#else
Color(uiColor: .systemRed)
#endif
}
static var assistantBubble: Color {
#if os(macOS)
Color(nsColor: self.assistantBubbleDynamicNSColor)
@@ -235,7 +235,7 @@ private struct OpenClawChatPreviewTransport: OpenClawChatTransport {
showsSessionSwitcher: false,
style: .onboarding,
markdownVariant: .standard,
userAccent: .blue)
userAccent: OpenClawChatTheme.accent)
}
private struct OpenClawChatPreview: View {
@@ -250,7 +250,7 @@ private struct OpenClawChatPreview: View {
showsSessionSwitcher: true,
style: .standard,
markdownVariant: .standard,
userAccent: .blue,
userAccent: OpenClawChatTheme.accent,
showsAssistantTrace: true)
}
}
@@ -372,7 +372,7 @@ public struct OpenClawChatView: View {
systemImage: "bubble.left.and.bubble.right.fill",
title: self.emptyStateTitle,
message: self.emptyStateMessage,
tint: .accentColor,
tint: OpenClawChatTheme.accent,
actionTitle: nil,
action: nil)
.padding(.horizontal, 24)