mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(ios): improve light and dark appearance contrast (#98443)
This commit is contained in:
committed by
GitHub
parent
6495358f17
commit
b2355ef6a2
@@ -29,6 +29,14 @@ enum AppAppearancePreference: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
var detail: String {
|
||||
switch self {
|
||||
case .system: "Matches the system appearance."
|
||||
case .light: "Always uses light appearance."
|
||||
case .dark: "Always uses dark appearance."
|
||||
}
|
||||
}
|
||||
|
||||
var colorScheme: ColorScheme? {
|
||||
switch self {
|
||||
case .system: nil
|
||||
@@ -47,36 +55,19 @@ enum AppAppearancePreference: String, CaseIterable, Identifiable {
|
||||
}
|
||||
|
||||
enum OpenClawBrand {
|
||||
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 uiAccent = adaptiveUIColor(light: (183, 56, 51), dark: (198, 62, 56))
|
||||
static let uiOK = adaptiveUIColor(light: (19, 122, 62), dark: (48, 209, 88))
|
||||
static let uiWarn = adaptiveUIColor(light: (154, 87, 0), dark: (255, 214, 10))
|
||||
static let uiInfo = adaptiveUIColor(light: (0, 91, 196), dark: (100, 168, 255))
|
||||
|
||||
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)
|
||||
: UIColor(red: 204 / 255.0, green: 75 / 255.0, blue: 69 / 255.0, alpha: 1)
|
||||
})
|
||||
static let danger = Color(uiColor: UIColor { traits in
|
||||
traits.userInterfaceStyle == .dark
|
||||
? UIColor(red: 252 / 255.0, green: 165 / 255.0, blue: 165 / 255.0, alpha: 1)
|
||||
: UIColor(red: 185 / 255.0, green: 28 / 255.0, blue: 28 / 255.0, alpha: 1)
|
||||
})
|
||||
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)
|
||||
: UIColor(red: 246 / 255.0, green: 247 / 255.0, blue: 249 / 255.0, alpha: 1)
|
||||
})
|
||||
static let graphiteElevated = Color(uiColor: UIColor { traits in
|
||||
traits.userInterfaceStyle == .dark
|
||||
? UIColor(red: 34 / 255.0, green: 36 / 255.0, blue: 39 / 255.0, alpha: 1)
|
||||
: UIColor.white
|
||||
})
|
||||
static let accentHot = Color(uiColor: adaptiveUIColor(light: (204, 75, 69), dark: (232, 92, 86)))
|
||||
static let danger = Color(uiColor: adaptiveUIColor(light: (185, 28, 28), dark: (252, 165, 165)))
|
||||
static let ok = Color(uiColor: Self.uiOK)
|
||||
static let warn = Color(uiColor: Self.uiWarn)
|
||||
static let info = Color(uiColor: Self.uiInfo)
|
||||
static let graphite = Color(uiColor: adaptiveUIColor(light: (246, 247, 249), dark: (20, 22, 24)))
|
||||
static let graphiteElevated = Color(uiColor: adaptiveUIColor(light: (255, 255, 255), dark: (34, 36, 39)))
|
||||
|
||||
static var sheetBackground: LinearGradient {
|
||||
LinearGradient(
|
||||
@@ -88,6 +79,20 @@ enum OpenClawBrand {
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing)
|
||||
}
|
||||
|
||||
private static func adaptiveUIColor(
|
||||
light: (red: CGFloat, green: CGFloat, blue: CGFloat),
|
||||
dark: (red: CGFloat, green: CGFloat, blue: CGFloat)) -> UIColor
|
||||
{
|
||||
UIColor { traits in
|
||||
let components = traits.userInterfaceStyle == .dark ? dark : light
|
||||
return UIColor(
|
||||
red: components.red / 255,
|
||||
green: components.green / 255,
|
||||
blue: components.blue / 255,
|
||||
alpha: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
@@ -89,6 +89,10 @@ struct SettingsProTab: View {
|
||||
self.settingsContent))
|
||||
}
|
||||
|
||||
var appearancePreference: AppAppearancePreference {
|
||||
AppAppearancePreference(rawValue: self.appearancePreferenceRaw) ?? .system
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var settingsContent: some View {
|
||||
if let directRoute {
|
||||
|
||||
@@ -30,7 +30,7 @@ extension SettingsProTab {
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Text("Follows iOS appearance.")
|
||||
Text(self.appearancePreference.detail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import Testing
|
||||
import UIKit
|
||||
@testable import OpenClaw
|
||||
|
||||
struct OpenClawBrandTests {
|
||||
@Test func `appearance preference details match selection`() {
|
||||
#expect(AppAppearancePreference.system.detail == "Matches the system appearance.")
|
||||
#expect(AppAppearancePreference.light.detail == "Always uses light appearance.")
|
||||
#expect(AppAppearancePreference.dark.detail == "Always uses dark appearance.")
|
||||
}
|
||||
|
||||
@Test func `semantic colors meet text contrast in both appearances`() {
|
||||
let colors = [OpenClawBrand.uiOK, OpenClawBrand.uiWarn, OpenClawBrand.uiInfo]
|
||||
let backgrounds = [UIColor.systemBackground, UIColor.secondarySystemBackground]
|
||||
|
||||
for style in [UIUserInterfaceStyle.light, .dark] {
|
||||
let traits = UITraitCollection(userInterfaceStyle: style)
|
||||
for color in colors {
|
||||
for background in backgrounds {
|
||||
#expect(Self.contrastRatio(color, background, traits: traits) >= 4.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func contrastRatio(
|
||||
_ foreground: UIColor,
|
||||
_ background: UIColor,
|
||||
traits: UITraitCollection) -> CGFloat
|
||||
{
|
||||
let foregroundLuminance = Self.relativeLuminance(foreground, traits: traits)
|
||||
let backgroundLuminance = Self.relativeLuminance(background, traits: traits)
|
||||
let lighter = max(foregroundLuminance, backgroundLuminance)
|
||||
let darker = min(foregroundLuminance, backgroundLuminance)
|
||||
return (lighter + 0.05) / (darker + 0.05)
|
||||
}
|
||||
|
||||
private static func relativeLuminance(_ color: UIColor, traits: UITraitCollection) -> CGFloat {
|
||||
let resolved = color.resolvedColor(with: traits)
|
||||
var red: CGFloat = 0
|
||||
var green: CGFloat = 0
|
||||
var blue: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
guard resolved.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { return 0 }
|
||||
|
||||
func linearize(_ component: CGFloat) -> CGFloat {
|
||||
component <= 0.04045
|
||||
? component / 12.92
|
||||
: pow((component + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
|
||||
return 0.2126 * linearize(red) + 0.7152 * linearize(green) + 0.0722 * linearize(blue)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@ import XCTest
|
||||
|
||||
@MainActor
|
||||
final class OpenClawSnapshotUITests: XCTestCase {
|
||||
private enum ScreenshotAppearance: String, CaseIterable {
|
||||
case light
|
||||
case dark
|
||||
}
|
||||
|
||||
private struct ScreenshotTarget {
|
||||
let initialTab: String
|
||||
let initialDestination: String
|
||||
@@ -31,9 +36,12 @@ final class OpenClawSnapshotUITests: XCTestCase {
|
||||
}
|
||||
|
||||
func testConnectedGatewayTabs() {
|
||||
for target in Self.screenshotTargets {
|
||||
launchApp(for: target)
|
||||
snapshot(target.name, timeWaitingForIdle: 5)
|
||||
for appearance in ScreenshotAppearance.allCases {
|
||||
for target in Self.screenshotTargets {
|
||||
launchApp(for: target, appearance: appearance)
|
||||
let name = appearance == .light ? target.name : "\(target.name)-dark"
|
||||
snapshot(name, timeWaitingForIdle: 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,13 +111,18 @@ final class OpenClawSnapshotUITests: XCTestCase {
|
||||
XCTAssertEqual(app.state, .runningForeground)
|
||||
}
|
||||
|
||||
private func launchApp(for target: ScreenshotTarget) {
|
||||
private func launchApp(
|
||||
for target: ScreenshotTarget,
|
||||
appearance: ScreenshotAppearance = .light)
|
||||
{
|
||||
self.app?.terminate()
|
||||
|
||||
let app = XCUIApplication()
|
||||
setupSnapshot(app, waitForAnimations: false)
|
||||
app.launchArguments += [
|
||||
"--openclaw-screenshot-mode",
|
||||
"--openclaw-appearance",
|
||||
appearance.rawValue,
|
||||
"--openclaw-initial-tab",
|
||||
target.initialTab,
|
||||
"--openclaw-initial-destination",
|
||||
|
||||
Reference in New Issue
Block a user