From 24cb2e31480aa8ab6623120dcd919859bb559404 Mon Sep 17 00:00:00 2001 From: Max Rozdobudko Date: Sun, 22 Feb 2026 09:05:41 +0200 Subject: [PATCH 1/4] chore: updates .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 05762b3..07c9c6f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ default.profraw .github/pages/node_modules/ .github/pages/dist/ +.github/pages/.vite/ From d0036ca0f1368cf3515ce786d4b7c28be2d71fd4 Mon Sep 17 00:00:00 2001 From: Max Rozdobudko Date: Sun, 22 Feb 2026 19:11:50 +0200 Subject: [PATCH 2/4] Add custom resolver support to ThemeAdaptiveStyle ThemeAdaptiveStyle now supports pluggable resolution via a Resolver struct, enabling per-token resolution based on full EnvironmentValues (not just colorScheme). This allows accessibility-aware tokens, size-class-dependent styles, and other environment-driven adaptations. Key changes: - Add ThemeAdaptiveStyle.Resolver with id-based Equatable - light/dark become optional (nil for resolver-only tokens) - Data tokens auto-generate deterministic resolver IDs via JSON+Hasher - Custom resolvers get unique UUID-based IDs by default - Codable encode throws for resolver-only tokens (by design) - Generated code uses resolved(in:) for full environment access - Update tests to use EnvironmentValues-based resolution --- IMPLEMENTATION_DETAILS.md | 2 +- README.md | 2 +- .../ThemeAdaptiveStyle+ShapeStyle.swift | 2 +- Sources/ThemeKit/ThemeAdaptiveStyle.swift | 92 +++++++++++++- .../ThemeShadowedStyleGenerator.swift | 2 +- .../ThemeShapeStyleGenerator.swift | 2 +- .../ThemeFileGeneratorTests.swift | 2 +- .../ThemeAdaptiveStyleTests.swift | 115 +++++++++++++++--- 8 files changed, 187 insertions(+), 32 deletions(-) diff --git a/IMPLEMENTATION_DETAILS.md b/IMPLEMENTATION_DETAILS.md index 6320682..00ff41e 100644 --- a/IMPLEMENTATION_DETAILS.md +++ b/IMPLEMENTATION_DETAILS.md @@ -103,7 +103,7 @@ struct ThemeShapeStyle: ShapeStyle, Sendable { let keyPath: KeyPath> func resolve(in environment: EnvironmentValues) -> some ShapeStyle { - environment.theme[keyPath: keyPath].resolved(for: environment.colorScheme) + environment.theme[keyPath: keyPath].resolved(in: environment) } } ``` diff --git a/README.md b/README.md index 8c4dd65..67bd479 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ struct ThemeShapeStyle: ShapeStyle { let keyPath: KeyPath> func resolve(in environment: EnvironmentValues) -> some ShapeStyle { - environment.theme[keyPath: keyPath].resolved(for: environment.colorScheme) + environment.theme[keyPath: keyPath].resolved(in: environment) } } ``` diff --git a/Sources/ThemeKit/ThemeAdaptiveStyle+ShapeStyle.swift b/Sources/ThemeKit/ThemeAdaptiveStyle+ShapeStyle.swift index 71cf988..5509c1c 100644 --- a/Sources/ThemeKit/ThemeAdaptiveStyle+ShapeStyle.swift +++ b/Sources/ThemeKit/ThemeAdaptiveStyle+ShapeStyle.swift @@ -2,6 +2,6 @@ import SwiftUI extension ThemeAdaptiveStyle: ShapeStyle where Style: ShapeStyle { nonisolated public func resolve(in environment: EnvironmentValues) -> some ShapeStyle { - resolved(for: environment.colorScheme) + resolver.resolve(environment) } } diff --git a/Sources/ThemeKit/ThemeAdaptiveStyle.swift b/Sources/ThemeKit/ThemeAdaptiveStyle.swift index 8ed4d21..9326fa1 100644 --- a/Sources/ThemeKit/ThemeAdaptiveStyle.swift +++ b/Sources/ThemeKit/ThemeAdaptiveStyle.swift @@ -1,17 +1,97 @@ import SwiftUI -nonisolated public struct ThemeAdaptiveStyle: Sendable, Codable, Equatable { - public let light: Style - public let dark: Style +nonisolated public struct ThemeAdaptiveStyle: Sendable, Equatable { - nonisolated public init(light: Style, dark: Style) { + public let light: Style? + public let dark: Style? + + public let resolver: Resolver + + nonisolated public init( + resolver: Resolver, + ) { + self.light = nil + self.dark = nil + self.resolver = resolver + } + + nonisolated public init( + light: Style, + dark: Style, + ) { self.light = light self.dark = dark + self.resolver = Resolver(id: Self.resolverID(light: light, dark: dark)) { env in + env.colorScheme == .dark ? dark : light + } + } + + private static func resolverID(light: Style, dark: Style) -> String { + guard let data = try? JSONEncoder().encode([light, dark]) else { + return UUID().uuidString + } + var hasher = Hasher() + hasher.combine(data) + return String(hasher.finalize()) } } public extension ThemeAdaptiveStyle { - nonisolated func resolved(for colorScheme: ColorScheme) -> Style { - colorScheme == .dark ? dark : light + nonisolated func resolved(in environment: EnvironmentValues) -> Style { + resolver.resolve(environment) + } +} + +// MARK: - Resolver + +nonisolated public extension ThemeAdaptiveStyle { + + struct Resolver: Sendable { + public let id: String + public let resolve: @Sendable (EnvironmentValues) -> Style + + public init(id: String, resolve: @escaping @Sendable (EnvironmentValues) -> Style) { + self.id = id + self.resolve = resolve + } + + public init(resolve: @escaping @Sendable (EnvironmentValues) -> Style) { + self.init(id: UUID().uuidString, resolve: resolve) + } + } +} + +nonisolated extension ThemeAdaptiveStyle.Resolver: Equatable { + public static func == (lhs: ThemeAdaptiveStyle