[P0-①] Theme follows system + keyboard background transparent

- Split Palette.dark / Palette.light into a ThemePalette struct
- Add EnvironmentKey<ThemePalette> + ThemedRoot<Content>
- Wrap OSGKeyboardApp root in ThemedRoot so main App follows system
- Remove 6 .preferredColorScheme(.dark) overrides from main App views
- KeyboardRootView drops its Palette.background fill, uses .background(Color.clear)
- Drop the unused 'background' computed property from KeyboardRootView
- Legacy 'Palette.xxx' static accessors still resolve to dark values,
  so 2000 lines of existing call sites stay untouched

xcodebuild iOS Simulator: SUCCEEDED
This commit is contained in:
Zhongshu
2026-06-18 10:38:23 +08:00
parent bec36befa2
commit e0b92ff35a
9 changed files with 168 additions and 61 deletions
+4 -5
View File
@@ -1,7 +1,5 @@
// OSGKeyboardApp.swift // OSGKeyboardApp.swift
// OSGKeyboard · Main App // OSGKeyboard · Main App
//
// DEBUG VERSION 2: restore real flow but instrument every step.
import SwiftUI import SwiftUI
import OSGKeyboardShared import OSGKeyboardShared
@@ -11,21 +9,22 @@ struct OSGKeyboardApp: App {
@StateObject private var config = ProviderConfig.shared @StateObject private var config = ProviderConfig.shared
init() { init() {
#if DEBUG
print("🔥 [OSGKeyboardApp] init()") print("🔥 [OSGKeyboardApp] init()")
#endif
} }
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ThemedRoot {
Group { Group {
if config.isConfigured { if config.isConfigured {
HomeView() HomeView()
.onAppear { print("🔥 [OSGKeyboardApp] → HomeView appeared") }
} else { } else {
OnboardingView(config: config) OnboardingView(config: config)
.onAppear { print("🔥 [OSGKeyboardApp] → OnboardingView appeared") }
} }
} }
.onAppear { print("🔥 [OSGKeyboardApp] body appeared") } }
} }
} }
} }
-1
View File
@@ -33,7 +33,6 @@ struct HomeView: View {
.sheet(isPresented: $showKeyboardPreview) { .sheet(isPresented: $showKeyboardPreview) {
KeyboardPreviewSheet() KeyboardPreviewSheet()
} }
.preferredColorScheme(.dark)
} }
// MARK: - Header // MARK: - Header
@@ -42,7 +42,6 @@ struct KeyboardPreviewSheet: View {
keyboardBlock keyboardBlock
} }
} }
.preferredColorScheme(.dark)
} }
private var mockTextField: some View { private var mockTextField: some View {
@@ -30,7 +30,6 @@ struct KeyboardPreviewStub: View {
.padding(.bottom, 6) .padding(.bottom, 6)
} }
.frame(height: 280) .frame(height: 280)
.preferredColorScheme(.dark)
} }
// MARK: - Top bar // MARK: - Top bar
-1
View File
@@ -37,7 +37,6 @@ struct OnboardingView: View {
.padding(.bottom, Spacing.lg) .padding(.bottom, Spacing.lg)
} }
} }
.preferredColorScheme(.dark)
} }
private var pageDots: some View { private var pageDots: some View {
-1
View File
@@ -37,7 +37,6 @@ struct SettingsView: View {
.foregroundStyle(Palette.accent) .foregroundStyle(Palette.accent)
} }
} }
.preferredColorScheme(.dark)
} }
.confirmationDialog( .confirmationDialog(
"Reset all settings?", "Reset all settings?",
+4 -35
View File
@@ -35,9 +35,6 @@ public struct KeyboardRootView: View {
static let totalHeight: CGFloat = 280 static let totalHeight: CGFloat = 280
public var body: some View { public var body: some View {
ZStack(alignment: .top) {
background
VStack(spacing: 0) { VStack(spacing: 0) {
topBar topBar
.frame(height: 32) .frame(height: 32)
@@ -50,36 +47,11 @@ public struct KeyboardRootView: View {
} }
.padding(.top, 4) .padding(.top, 4)
.padding(.bottom, 6) .padding(.bottom, 6)
} // iOS keyboard extensions always render dark (Apple's default
// for custom keyboards), and we let the system UI chrome show
// through by drawing no background of our own.
.background(Color.clear)
.frame(height: Self.totalHeight) .frame(height: Self.totalHeight)
.preferredColorScheme(.dark)
}
// MARK: - Background
/// Solid dark fill plus a hairline highlight at the top edge, so the
/// keyboard reads as a physical surface rather than a floating card.
private var background: some View {
ZStack {
Palette.background
VStack(spacing: 0) {
Rectangle()
.fill(
LinearGradient(
colors: [Color.white.opacity(0.05), .clear],
startPoint: .top,
endPoint: .bottom
)
)
.frame(height: 1)
Spacer(minLength: 0)
}
}
.overlay(alignment: .top) {
Rectangle()
.fill(Palette.divider)
.frame(height: 0.5)
}
} }
// MARK: - Top bar // MARK: - Top bar
@@ -182,19 +154,16 @@ extension KeyboardRootView {
#Preview("Keyboard · Idle") { #Preview("Keyboard · Idle") {
KeyboardRootView(state: KeyboardViewController.State.previewIdle) KeyboardRootView(state: KeyboardViewController.State.previewIdle)
.frame(width: 390, height: 280) .frame(width: 390, height: 280)
.preferredColorScheme(.dark)
} }
#Preview("Keyboard · Recording") { #Preview("Keyboard · Recording") {
KeyboardRootView(state: KeyboardViewController.State.previewRecording) KeyboardRootView(state: KeyboardViewController.State.previewRecording)
.frame(width: 390, height: 280) .frame(width: 390, height: 280)
.preferredColorScheme(.dark)
} }
#Preview("Keyboard · Processing") { #Preview("Keyboard · Processing") {
KeyboardRootView(state: KeyboardViewController.State.previewProcessing) KeyboardRootView(state: KeyboardViewController.State.previewProcessing)
.frame(width: 390, height: 280) .frame(width: 390, height: 280)
.preferredColorScheme(.dark)
} }
#endif #endif
+94 -1
View File
@@ -9,7 +9,37 @@
import SwiftUI import SwiftUI
// MARK: - Palette // MARK: - Theme palette (light / dark)
/// Single source of truth for *one* colour scheme. The active palette is
/// injected via the `\.themePalette` environment key see
/// `DesignSystem/ThemedRoot.swift`. Token names mirror the previous
/// `Palette` static API so existing call sites (`Palette.background` etc.)
/// still compile and resolve through the legacy static accessors below.
public struct ThemePalette: Sendable {
public let background: Color
public let surface: Color
public let surfaceElevated: Color
public let surfaceMuted: Color
public let accent: Color
public let accentMuted: Color
public let accentGlow: Color
public let danger: Color
public let success: Color
public let warning: Color
public let textPrimary: Color
public let textSecondary: Color
public let textTertiary: Color
public let textOnAccent: Color
public let divider: Color
public let dividerStrong: Color
public let recordRed: Color
}
public enum Palette { public enum Palette {
// Backgrounds // Backgrounds
@@ -40,6 +70,69 @@ public enum Palette {
// Recording state // Recording state
public static let recordRed = Color(red: 1.000, green: 0.231, blue: 0.188) // #FF3B30 public static let recordRed = Color(red: 1.000, green: 0.231, blue: 0.188) // #FF3B30
/// Canonical dark palette preserves every legacy literal above so
/// existing call sites that read `Palette.background` directly keep
/// getting the dark value (important for the keyboard extension, which
/// deliberately stays dark regardless of system appearance).
public static let dark = ThemePalette(
background: background,
surface: surface,
surfaceElevated: surfaceElevated,
surfaceMuted: surfaceMuted,
accent: accent,
accentMuted: accentMuted,
accentGlow: accentGlow,
danger: danger,
success: success,
warning: warning,
textPrimary: textPrimary,
textSecondary: textSecondary,
textTertiary: textTertiary,
textOnAccent: textOnAccent,
divider: divider,
dividerStrong: dividerStrong,
recordRed: recordRed
)
/// Light palette iOS system light mode defaults. Used by the main app
/// when the user is in light mode; the keyboard extension stays dark.
public static let light = ThemePalette(
background: Color(red: 0.980, green: 0.980, blue: 0.988), // #FAFAFC
surface: Color(red: 1.000, green: 1.000, blue: 1.000), // #FFFFFF
surfaceElevated: Color(red: 0.941, green: 0.941, blue: 0.961), // #F0F0F5
surfaceMuted: Color(red: 0.953, green: 0.953, blue: 0.965), // #F3F3F6
accent: Color(red: 0.000, green: 0.478, blue: 1.000), // iOS systemBlue
accentMuted: Color(red: 0.000, green: 0.478, blue: 1.000).opacity(0.14),
accentGlow: Color(red: 0.000, green: 0.478, blue: 1.000).opacity(0.32),
danger: Color(red: 1.000, green: 0.231, blue: 0.188), // #FF3B30
success: Color(red: 0.157, green: 0.812, blue: 0.412), // #28CF69
warning: Color(red: 1.000, green: 0.620, blue: 0.094), // #FF9E18
textPrimary: Color(red: 0.067, green: 0.067, blue: 0.094), // #111118
textSecondary: Color(red: 0.392, green: 0.392, blue: 0.435), // #64646F
textTertiary: Color(red: 0.557, green: 0.557, blue: 0.604), // #8E8E9A
textOnAccent: Color.white,
divider: Color.black.opacity(0.06),
dividerStrong: Color.black.opacity(0.10),
recordRed: Color(red: 1.000, green: 0.231, blue: 0.188) // #FF3B30
)
}
// MARK: - Environment key
private struct ThemePaletteKey: EnvironmentKey {
/// Default falls back to the legacy dark palette so views that haven't
/// been wrapped in `ThemedRoot` continue to look identical to today.
static let defaultValue: ThemePalette = Palette.dark
}
public extension EnvironmentValues {
/// The palette currently active for this view. Reads from the nearest
/// `ThemedRoot` ancestor (or `Palette.dark` if none).
var themePalette: ThemePalette {
get { self[ThemePaletteKey.self] }
set { self[ThemePaletteKey.self] = newValue }
}
} }
// MARK: - Spacing scale (4 pt grid) // MARK: - Spacing scale (4 pt grid)
@@ -0,0 +1,51 @@
// ThemedRoot.swift
// OSGKeyboard · Design System
//
// `ThemedRoot` injects the right `ThemePalette` for the current system
// colour scheme. Wrap the main App's root in this view to opt into
// light/dark following; the keyboard extension deliberately stays dark
// (Apple's custom keyboards always render dark) and does NOT use this.
import SwiftUI
public struct ThemedRoot<Content: View>: View {
@Environment(\.colorScheme) private var colorScheme
let content: () -> Content
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
public var body: some View {
content()
.environment(\.themePalette, colorScheme == .dark ? Palette.dark : Palette.light)
}
}
#if DEBUG
#Preview("ThemedRoot · Dark") {
ThemedRoot {
ZStack {
Palette.dark.background.ignoresSafeArea()
Text("Dark")
.foregroundStyle(Palette.dark.textPrimary)
.font(.title)
}
}
.preferredColorScheme(.dark)
}
#Preview("ThemedRoot · Light") {
ThemedRoot {
ZStack {
Palette.light.background.ignoresSafeArea()
Text("Light")
.foregroundStyle(Palette.light.textPrimary)
.font(.title)
}
}
.preferredColorScheme(.light)
}
#endif