Files
OSGKeyboard/OSGKeyboardMac/MacTheme.swift
T
Rocky 498f407585 feat(account): add managed credits and cloud gateway
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
2026-08-20 11:43:21 +08:00

118 lines
5.3 KiB
Swift

// MacTheme.swift
// OSGKeyboard · Mac
//
// System-native colour palette for the desktop app. Instead of the custom
// near-black brand palette, the Mac app maps every design token onto AppKit
// semantic colours, resolved to a concrete value for the *active* appearance.
// The brand green is kept only as the accent. Light mode uses a warm,
// iOS-matched surface set (the default `windowBackgroundColor` reads cold
// grey on macOS); Dark mode uses stepped elevated greys so cards stay
// readable against the page background.
import AppKit
import SwiftUI
enum MacSystemPalette {
/// Returns the palette for the given colour scheme. Because the two
/// palettes hold *concrete* (already-resolved) colours, the value changes
/// identity when the scheme flips — so injecting it via `@Environment`
/// (see `macSystemPalette()`) reliably re-renders every dependent view the
/// instant the appearance changes, instead of lagging until the next
/// view rebuild.
static func palette(for scheme: ColorScheme) -> ThemePalette {
scheme == .dark ? darkPalette : lightPalette
}
private static let lightPalette = makePalette(dark: false)
private static let darkPalette = makePalette(dark: true)
private static func makePalette(dark: Bool) -> ThemePalette {
ThemePalette(
background: resolved(dark ? darkBackground : warmBackground, dark: dark),
surface: resolved(dark ? darkSurface : warmSurface, dark: dark),
surfaceElevated: resolved(dark ? darkElevated : warmElevated, dark: dark),
surfaceMuted: resolved(dark ? darkMuted : warmMuted, dark: dark),
accent: Palette.accent,
accentMuted: Palette.accent.opacity(dark ? 0.22 : 0.14),
accentGlow: Palette.accent.opacity(dark ? 0.40 : 0.32),
aiTeal: Palette.aiTeal,
danger: resolved(.systemRed, dark: dark),
success: Palette.accent,
warning: resolved(.systemOrange, dark: dark),
textPrimary: resolved(.labelColor, dark: dark),
textSecondary: resolved(.secondaryLabelColor, dark: dark),
textTertiary: resolved(.tertiaryLabelColor, dark: dark),
textOnAccent: Color.white,
divider: dark
? Color.white.opacity(0.08)
: Color.black.opacity(0.06),
dividerStrong: dark
? Color.white.opacity(0.12)
: Color.black.opacity(0.10),
recordRed: resolved(.systemRed, dark: dark),
recordBlue: resolved(.systemBlue, dark: dark)
)
}
// MARK: - Warm Light-mode surfaces (matched to iOS `Palette.light`)
/// #F2F1EE — warm gray page background.
private static let warmBackground = NSColor(srgbRed: 0.949, green: 0.945, blue: 0.933, alpha: 1)
/// #FCFBF9 — warm off-white card/control surface.
private static let warmSurface = NSColor(srgbRed: 0.988, green: 0.984, blue: 0.976, alpha: 1)
/// #EBEAE7 — slightly recessed elevated surface.
private static let warmElevated = NSColor(srgbRed: 0.922, green: 0.918, blue: 0.906, alpha: 1)
/// #EEEDE9 — muted fill between background and surface.
private static let warmMuted = NSColor(srgbRed: 0.933, green: 0.929, blue: 0.918, alpha: 1)
// MARK: - Dark-mode surfaces (Apple standard elevated grays)
//
// AppKit's `controlBackgroundColor` is *darker* than `windowBackgroundColor`
// in Dark Aqua, so cards using it recede into the page. Instead we step the
// surfaces explicitly (systemGray6→4 equivalents) so every card reads as
// clearly elevated above the background — mirroring the iOS dark palette.
/// #1C1C1E — page background.
private static let darkBackground = NSColor(srgbRed: 0.110, green: 0.110, blue: 0.118, alpha: 1)
/// #2C2C2E — card / control surface, clearly lighter than the background.
private static let darkSurface = NSColor(srgbRed: 0.173, green: 0.173, blue: 0.180, alpha: 1)
/// #3A3A3C — elevated fill for selected / raised chrome.
private static let darkElevated = NSColor(srgbRed: 0.227, green: 0.227, blue: 0.235, alpha: 1)
/// #242426 — muted fill between background and surface.
private static let darkMuted = NSColor(srgbRed: 0.141, green: 0.141, blue: 0.149, alpha: 1)
/// Resolves a (possibly dynamic) AppKit colour to its concrete value under
/// the requested appearance, so the two static palettes differ by value.
private static func resolved(_ nsColor: NSColor, dark: Bool) -> Color {
guard let appearance = NSAppearance(named: dark ? .darkAqua : .aqua) else {
return Color(nsColor: nsColor)
}
var result = nsColor
appearance.performAsCurrentDrawingAppearance {
result = nsColor.usingColorSpace(.sRGB) ?? nsColor
}
return Color(nsColor: result)
}
}
private struct MacSystemPaletteModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
content.environment(\.themePalette, MacSystemPalette.palette(for: colorScheme))
}
}
extension View {
/// Injects the system-native palette used across the macOS app, refreshed
/// automatically whenever the effective colour scheme changes.
func macSystemPalette() -> some View {
modifier(MacSystemPaletteModifier())
}
}