diff --git a/OSGKeyboard/OSGKeyboardApp.swift b/OSGKeyboard/OSGKeyboardApp.swift index 3d4b5b1..72911f2 100644 --- a/OSGKeyboard/OSGKeyboardApp.swift +++ b/OSGKeyboard/OSGKeyboardApp.swift @@ -1,7 +1,5 @@ // OSGKeyboardApp.swift // OSGKeyboard · Main App -// -// DEBUG VERSION 2: restore real flow but instrument every step. import SwiftUI import OSGKeyboardShared @@ -11,21 +9,22 @@ struct OSGKeyboardApp: App { @StateObject private var config = ProviderConfig.shared init() { + #if DEBUG print("🔥 [OSGKeyboardApp] init()") + #endif } var body: some Scene { WindowGroup { - Group { - if config.isConfigured { - HomeView() - .onAppear { print("🔥 [OSGKeyboardApp] → HomeView appeared") } - } else { - OnboardingView(config: config) - .onAppear { print("🔥 [OSGKeyboardApp] → OnboardingView appeared") } + ThemedRoot { + Group { + if config.isConfigured { + HomeView() + } else { + OnboardingView(config: config) + } } } - .onAppear { print("🔥 [OSGKeyboardApp] body appeared") } } } -} +} \ No newline at end of file diff --git a/OSGKeyboard/Views/HomeView.swift b/OSGKeyboard/Views/HomeView.swift index 2af9f1b..2a9cde9 100644 --- a/OSGKeyboard/Views/HomeView.swift +++ b/OSGKeyboard/Views/HomeView.swift @@ -33,7 +33,6 @@ struct HomeView: View { .sheet(isPresented: $showKeyboardPreview) { KeyboardPreviewSheet() } - .preferredColorScheme(.dark) } // MARK: - Header diff --git a/OSGKeyboard/Views/KeyboardPreviewSheet.swift b/OSGKeyboard/Views/KeyboardPreviewSheet.swift index 8590544..7386e83 100644 --- a/OSGKeyboard/Views/KeyboardPreviewSheet.swift +++ b/OSGKeyboard/Views/KeyboardPreviewSheet.swift @@ -42,7 +42,6 @@ struct KeyboardPreviewSheet: View { keyboardBlock } } - .preferredColorScheme(.dark) } private var mockTextField: some View { diff --git a/OSGKeyboard/Views/KeyboardPreviewStub.swift b/OSGKeyboard/Views/KeyboardPreviewStub.swift index 600a02c..ee3a042 100644 --- a/OSGKeyboard/Views/KeyboardPreviewStub.swift +++ b/OSGKeyboard/Views/KeyboardPreviewStub.swift @@ -30,7 +30,6 @@ struct KeyboardPreviewStub: View { .padding(.bottom, 6) } .frame(height: 280) - .preferredColorScheme(.dark) } // MARK: - Top bar diff --git a/OSGKeyboard/Views/OnboardingView.swift b/OSGKeyboard/Views/OnboardingView.swift index 368c0ef..e3d3af4 100644 --- a/OSGKeyboard/Views/OnboardingView.swift +++ b/OSGKeyboard/Views/OnboardingView.swift @@ -37,7 +37,6 @@ struct OnboardingView: View { .padding(.bottom, Spacing.lg) } } - .preferredColorScheme(.dark) } private var pageDots: some View { diff --git a/OSGKeyboard/Views/SettingsView.swift b/OSGKeyboard/Views/SettingsView.swift index 3e3c131..3f4d8b7 100644 --- a/OSGKeyboard/Views/SettingsView.swift +++ b/OSGKeyboard/Views/SettingsView.swift @@ -37,7 +37,6 @@ struct SettingsView: View { .foregroundStyle(Palette.accent) } } - .preferredColorScheme(.dark) } .confirmationDialog( "Reset all settings?", diff --git a/OSGKeyboardExt/Views/KeyboardRootView.swift b/OSGKeyboardExt/Views/KeyboardRootView.swift index 78ab68f..cbba11d 100644 --- a/OSGKeyboardExt/Views/KeyboardRootView.swift +++ b/OSGKeyboardExt/Views/KeyboardRootView.swift @@ -35,51 +35,23 @@ public struct KeyboardRootView: View { static let totalHeight: CGFloat = 280 public var body: some View { - ZStack(alignment: .top) { - background + VStack(spacing: 0) { + topBar + .frame(height: 32) - VStack(spacing: 0) { - topBar - .frame(height: 32) + centreArea + .frame(maxWidth: .infinity, maxHeight: .infinity) - centreArea - .frame(maxWidth: .infinity, maxHeight: .infinity) - - bottomBar - .frame(height: 56) - } - .padding(.top, 4) - .padding(.bottom, 6) + bottomBar + .frame(height: 56) } + .padding(.top, 4) + .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) - .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 @@ -182,19 +154,16 @@ extension KeyboardRootView { #Preview("Keyboard · Idle") { KeyboardRootView(state: KeyboardViewController.State.previewIdle) .frame(width: 390, height: 280) - .preferredColorScheme(.dark) } #Preview("Keyboard · Recording") { KeyboardRootView(state: KeyboardViewController.State.previewRecording) .frame(width: 390, height: 280) - .preferredColorScheme(.dark) } #Preview("Keyboard · Processing") { KeyboardRootView(state: KeyboardViewController.State.previewProcessing) .frame(width: 390, height: 280) - .preferredColorScheme(.dark) } #endif diff --git a/OSGKeyboardShared/DesignSystem/Theme.swift b/OSGKeyboardShared/DesignSystem/Theme.swift index d26fd1b..c9cfbec 100644 --- a/OSGKeyboardShared/DesignSystem/Theme.swift +++ b/OSGKeyboardShared/DesignSystem/Theme.swift @@ -9,7 +9,37 @@ 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 { // Backgrounds @@ -40,6 +70,69 @@ public enum Palette { // Recording state 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) diff --git a/OSGKeyboardShared/DesignSystem/ThemedRoot.swift b/OSGKeyboardShared/DesignSystem/ThemedRoot.swift new file mode 100644 index 0000000..5a3875e --- /dev/null +++ b/OSGKeyboardShared/DesignSystem/ThemedRoot.swift @@ -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: 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 \ No newline at end of file