[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:
@@ -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") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(Palette.accent)
|
.foregroundStyle(Palette.accent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
}
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
"Reset all settings?",
|
"Reset all settings?",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user