Compare commits
No commits in common. "main" and "v1.5.0" have entirely different histories.
412 changed files with 2685 additions and 1265 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,4 +5,3 @@
|
||||||
Package.resolved
|
Package.resolved
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
DerivedData/
|
DerivedData/
|
||||||
build/
|
|
||||||
|
|
|
||||||
125
CHANGELOG.md
125
CHANGELOG.md
|
|
@ -4,131 +4,6 @@ Alle Änderungen werden hier dokumentiert. Format orientiert an
|
||||||
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
|
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
|
||||||
[Semver](https://semver.org).
|
[Semver](https://semver.org).
|
||||||
|
|
||||||
## [1.7.0] — 2026-05-17
|
|
||||||
|
|
||||||
Minor — **`ManaAppLog`** + `ManaAppConfig.appGroup`/`logSubsystem`.
|
|
||||||
Audit 2026-05-17 V4. Ersetzt das hand-getippte `Log.swift`-Boilerplate
|
|
||||||
in jeder App durch einen Config-getriebenen Wrapper.
|
|
||||||
|
|
||||||
### Neu
|
|
||||||
|
|
||||||
- `ManaAppLog` (public struct, Sendable) — Factory für OSLog-Logger
|
|
||||||
gegen ein `ManaAppConfig`. Standard-Kategorien `app`/`auth`/`api`/
|
|
||||||
`db`/`web`, plus `category("…")` für app-spezifische Kategorien.
|
|
||||||
- `ManaAppConfig.appGroup: String?` (default `nil`) — Single-Source
|
|
||||||
für den App-Group-String, der heute in jeder App 3-4× hardcoded
|
|
||||||
steht. Apps ohne Widget/ShareExt setzen weiterhin nichts.
|
|
||||||
- `ManaAppConfig.logSubsystem: String` (default = `keychainService`)
|
|
||||||
— Subsystem für `ManaAppLog`. In allen Apps heute schon
|
|
||||||
`ev.mana.<app>`, deshalb default sinnvoll.
|
|
||||||
|
|
||||||
### Geändert
|
|
||||||
|
|
||||||
- Nichts breaking. Beide neuen Felder haben Default-Implementations
|
|
||||||
im Protocol-Extension, bestehende Konsumenten von `ManaAppConfig`
|
|
||||||
brauchen nichts anzupassen.
|
|
||||||
- `DefaultManaAppConfig.init` hat zwei zusätzliche optionale Parameter
|
|
||||||
(`appGroup`, `logSubsystem`), beide mit `nil`-Defaults — Quellkompatibel.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- 4 neue ManaAppConfig-Tests + 5 neue ManaAppLog-Tests. 85/85 grün
|
|
||||||
(vorher 76/76).
|
|
||||||
|
|
||||||
### Migrations-Hinweis
|
|
||||||
|
|
||||||
Apps können ihre lokale `Log.swift` von ~13 LOC auf ~5 LOC schrumpfen:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import ManaCore
|
|
||||||
|
|
||||||
enum Log {
|
|
||||||
private static let mana = ManaAppLog(AppConfig.manaAppConfig)
|
|
||||||
static let app = mana.app
|
|
||||||
static let auth = mana.auth
|
|
||||||
static let api = mana.api
|
|
||||||
static let study = mana.category("study") // app-spezifisch
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Plus `AppConfig.manaAppConfig` kann `appGroup: "group.ev.mana.<app>"`
|
|
||||||
ergänzen, damit der App-Group-String single-source ist.
|
|
||||||
|
|
||||||
## [1.6.0] — 2026-05-17
|
|
||||||
|
|
||||||
Minor — **`ManaTheme`-Variants** in ManaTokens. Acht Web-Themes aus
|
|
||||||
`@mana/themes` (mana, forest, paper, neutral, lume, twilight,
|
|
||||||
skylight, monochrome) sind jetzt als Swift verfügbar; generiert aus
|
|
||||||
den CSS-Quellen via `pnpm --filter @mana/themes gen:swift`.
|
|
||||||
|
|
||||||
Hintergrund: bisher haben Cards, Manaspur und Nutriphi je ~90 LOC
|
|
||||||
forest-HSL-Apparat lokal nachgebaut, weil ManaTokens nur die
|
|
||||||
mana-Variant kannte. Mit v1.6.0 sind diese App-lokalen Theme-Files
|
|
||||||
ablösbar (Audit 2026-05-17 Vorschlag V1).
|
|
||||||
|
|
||||||
### Neu
|
|
||||||
|
|
||||||
- `ManaTheme` (public enum) — `case mana | forest | paper | neutral
|
|
||||||
| lume | twilight | skylight | monochrome`. `String`-RawValue,
|
|
||||||
`CaseIterable`, `Sendable`.
|
|
||||||
- `ManaThemeColors` (public struct, Sendable) — die 12 Tokens als
|
|
||||||
`Color`-Properties. Per-Variant statisch verfügbar
|
|
||||||
(`ManaThemeColors.forest` usw.) aus der generierten Datei
|
|
||||||
`GeneratedThemes.swift`.
|
|
||||||
- `ManaTheme.colors` + Convenience-Accessoren (`.background`,
|
|
||||||
`.foreground`, …) — beide Schreibweisen funktionieren.
|
|
||||||
- `EnvironmentValues.manaTheme` + `View.manaTheme(_:)` —
|
|
||||||
SwiftUI-Environment, Default `.mana`. Apps mit User-Theme-Switching
|
|
||||||
setzen den Wert per `@AppStorage`-gestütztem Binding.
|
|
||||||
|
|
||||||
### Geändert
|
|
||||||
|
|
||||||
- Nichts breaking. `ManaColor.*` und `ManaBrand.*` bleiben unverändert
|
|
||||||
und liefern weiter die mana-Variant-Werte.
|
|
||||||
|
|
||||||
### Generator
|
|
||||||
|
|
||||||
- `mana/packages/themes/scripts/gen-swift-themes.mjs` liest die acht
|
|
||||||
CSS-Variant-Dateien und schreibt `GeneratedThemes.swift`. CI-Drift-
|
|
||||||
Check: nach Generator-Lauf `git diff --exit-code` in beiden Repos.
|
|
||||||
|
|
||||||
### Migrations-Hinweis
|
|
||||||
|
|
||||||
Apps können ihre lokalen `*Theme.swift`-Files (Cards, Manaspur,
|
|
||||||
Nutriphi) durch direkten Aufruf von `ManaTheme.<variant>` ersetzen.
|
|
||||||
Konvention: Apps mit fester Identität setzen `.manaTheme(.<variant>)`
|
|
||||||
an der App-Root; verschachtelte Views lesen via
|
|
||||||
`@Environment(\.manaTheme)`.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- 7 neue Tests (`ThemeTests.swift`). 12/12 ManaTokens grün auf macOS.
|
|
||||||
|
|
||||||
## [1.5.1] — 2026-05-17
|
|
||||||
|
|
||||||
Patch — KeychainStore-Migration-Fallback. Bisher haben die mana-Apps
|
|
||||||
`keychainAccessGroup: nil` gesetzt; Apple legt das Item dann im
|
|
||||||
default-bucket (`$(AppIdentifierPrefix).$(BundleId)`) ab. Beim
|
|
||||||
Wechsel auf eine explizite `accessGroup` (Apps werden mit v1.5.1
|
|
||||||
nachgezogen) hätten User sonst beim ersten Start einen Logout
|
|
||||||
gesehen, weil Apples Read-mit-Group das alte Item nicht immer
|
|
||||||
liefert.
|
|
||||||
|
|
||||||
### Verändert
|
|
||||||
|
|
||||||
- `KeychainStore.getString(for:)` — bei `accessGroup != nil` und
|
|
||||||
einem Miss wird einmalig ohne `kSecAttrAccessGroup` re-queryt.
|
|
||||||
Findet sich der alte Eintrag im default-bucket, wird er in den
|
|
||||||
expliziten Bucket migriert und der alte gelöscht. Transparent für
|
|
||||||
alle Caller — `getString` liefert wie gewohnt den Wert zurück.
|
|
||||||
|
|
||||||
### Migrations-Hinweis
|
|
||||||
|
|
||||||
- Apps, die `keychainAccessGroup` ab v1.5.1 explizit setzen, brauchen
|
|
||||||
keinen Logout zu erwarten. Apps, die weiterhin `nil` setzen, sind
|
|
||||||
von dem Fallback nicht betroffen (er greift nur bei `accessGroup
|
|
||||||
!= nil`).
|
|
||||||
|
|
||||||
## [1.5.0] — 2026-05-14
|
## [1.5.0] — 2026-05-14
|
||||||
|
|
||||||
Minor — `getProfile()` + `ProfileInfo`. Apps können den 2FA-Status
|
Minor — `getProfile()` + `ProfileInfo`. Apps können den 2FA-Status
|
||||||
|
|
|
||||||
|
|
@ -61,40 +61,10 @@ public final class KeychainStore: Sendable {
|
||||||
query[kSecMatchLimit] = kSecMatchLimitOne
|
query[kSecMatchLimit] = kSecMatchLimitOne
|
||||||
var result: AnyObject?
|
var result: AnyObject?
|
||||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
if status == errSecSuccess, let data = result as? Data {
|
guard status == errSecSuccess, let data = result as? Data else {
|
||||||
return String(data: data, encoding: .utf8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// One-shot migration: bis v1.2.0 haben die mana-Apps Keychain-
|
|
||||||
// Items ohne explizite `kSecAttrAccessGroup` geschrieben. Apple
|
|
||||||
// hat sie dann im default-bucket (= `$(AppIdentifierPrefix).$(BundleId)`)
|
|
||||||
// gelandet. Ab v1.2.1 setzen die Apps die accessGroup explizit
|
|
||||||
// auf denselben Identifier. In den meisten Fällen liefert Apple
|
|
||||||
// das alte Item beim Read-mit-Group transparent zurück — wenn
|
|
||||||
// nicht, hat das einen Logout zur Folge. Dieser Fallback liest
|
|
||||||
// den Item nochmal ohne accessGroup, schreibt ihn beim Erfolg
|
|
||||||
// mit accessGroup neu in den expliziten Bucket und löscht den
|
|
||||||
// alten Eintrag.
|
|
||||||
guard accessGroup != nil else { return nil }
|
|
||||||
var fallback: [CFString: Any] = [
|
|
||||||
kSecClass: kSecClassGenericPassword,
|
|
||||||
kSecAttrService: service,
|
|
||||||
kSecAttrAccount: key.rawValue,
|
|
||||||
kSecReturnData: true,
|
|
||||||
kSecMatchLimit: kSecMatchLimitOne,
|
|
||||||
]
|
|
||||||
var fallbackResult: AnyObject?
|
|
||||||
guard SecItemCopyMatching(fallback as CFDictionary, &fallbackResult) == errSecSuccess,
|
|
||||||
let data = fallbackResult as? Data,
|
|
||||||
let value = String(data: data, encoding: .utf8)
|
|
||||||
else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
try? setString(value, for: key)
|
return String(data: data, encoding: .utf8)
|
||||||
fallback.removeValue(forKey: kSecReturnData)
|
|
||||||
fallback.removeValue(forKey: kSecMatchLimit)
|
|
||||||
SecItemDelete(fallback as CFDictionary)
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func remove(for key: Key) {
|
public func remove(for key: Key) {
|
||||||
|
|
|
||||||
|
|
@ -21,31 +21,6 @@ public protocol ManaAppConfig: Sendable {
|
||||||
/// Apple-Developer-Team-ID provisioniert sein und das Entitlement
|
/// Apple-Developer-Team-ID provisioniert sein und das Entitlement
|
||||||
/// `keychain-access-groups` mit demselben Wert tragen.
|
/// `keychain-access-groups` mit demselben Wert tragen.
|
||||||
var keychainAccessGroup: String? { get }
|
var keychainAccessGroup: String? { get }
|
||||||
|
|
||||||
/// App-Group für Daten-Sharing zwischen App ↔ Widget ↔ ShareExt.
|
|
||||||
/// Üblich `group.ev.mana.<app>`. `nil` für Apps ohne Extensions.
|
|
||||||
///
|
|
||||||
/// Single-Source für den App-Group-String, der heute in jeder App
|
|
||||||
/// 3-4× hardcoded steht (AppConfig + App-Entitlement + Widget-
|
|
||||||
/// Entitlement + ShareExt-Entitlement). Die Entitlements bleiben
|
|
||||||
/// hardcoded (das verlangt iOS), aber im Swift-Code ist der Wert
|
|
||||||
/// damit single-source.
|
|
||||||
var appGroup: String? { get }
|
|
||||||
|
|
||||||
/// OSLog-Subsystem für App-Logger, üblich `ev.mana.<app>`. Default
|
|
||||||
/// ist `keychainService` (der schon der Konvention folgt).
|
|
||||||
var logSubsystem: String { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Default-Implementationen
|
|
||||||
|
|
||||||
public extension ManaAppConfig {
|
|
||||||
/// Default `nil` — Apps ohne Widget/ShareExt müssen nichts setzen.
|
|
||||||
var appGroup: String? { nil }
|
|
||||||
|
|
||||||
/// Default = `keychainService`. Beide folgen heute in allen Apps
|
|
||||||
/// derselben Konvention `ev.mana.<app>`.
|
|
||||||
var logSubsystem: String { keychainService }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Standard-Implementierung von ``ManaAppConfig``. Apps können diese
|
/// Standard-Implementierung von ``ManaAppConfig``. Apps können diese
|
||||||
|
|
@ -54,22 +29,14 @@ public struct DefaultManaAppConfig: ManaAppConfig {
|
||||||
public let authBaseURL: URL
|
public let authBaseURL: URL
|
||||||
public let keychainService: String
|
public let keychainService: String
|
||||||
public let keychainAccessGroup: String?
|
public let keychainAccessGroup: String?
|
||||||
public let appGroup: String?
|
|
||||||
public let logSubsystem: String
|
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
authBaseURL: URL,
|
authBaseURL: URL,
|
||||||
keychainService: String,
|
keychainService: String,
|
||||||
keychainAccessGroup: String? = nil,
|
keychainAccessGroup: String? = nil
|
||||||
appGroup: String? = nil,
|
|
||||||
logSubsystem: String? = nil
|
|
||||||
) {
|
) {
|
||||||
self.authBaseURL = authBaseURL
|
self.authBaseURL = authBaseURL
|
||||||
self.keychainService = keychainService
|
self.keychainService = keychainService
|
||||||
self.keychainAccessGroup = keychainAccessGroup
|
self.keychainAccessGroup = keychainAccessGroup
|
||||||
self.appGroup = appGroup
|
|
||||||
// Konvention: log-Subsystem = keychainService, falls nicht
|
|
||||||
// explizit anders gewünscht.
|
|
||||||
self.logSubsystem = logSubsystem ?? keychainService
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import Foundation
|
|
||||||
import OSLog
|
|
||||||
|
|
||||||
/// Convenience-Factory für OSLog-Logger gegen ein `ManaAppConfig`.
|
|
||||||
///
|
|
||||||
/// Hintergrund: Jede mana-App hatte vor v1.7.0 ein eigenes `Log.swift`
|
|
||||||
/// mit 4-6 hand-getippten `Logger(subsystem: "ev.mana.<slug>", …)`-
|
|
||||||
/// Aufrufen — der Subsystem-String und die Standard-Kategorien
|
|
||||||
/// (`app`/`auth`/`api`) wiederholten sich 8× identisch. `ManaAppLog`
|
|
||||||
/// kapselt den Subsystem-Lookup gegen die App-Config; Standard-
|
|
||||||
/// Kategorien sind Convenience-Accessoren, app-spezifische gehen über
|
|
||||||
/// ``ManaAppLog/category(_:)``.
|
|
||||||
///
|
|
||||||
/// **Verwendung** (in der App):
|
|
||||||
///
|
|
||||||
/// ```swift
|
|
||||||
/// import ManaCore
|
|
||||||
///
|
|
||||||
/// enum Log {
|
|
||||||
/// private static let mana = ManaAppLog(AppConfig.manaAppConfig)
|
|
||||||
/// static let app = mana.app
|
|
||||||
/// static let auth = mana.auth
|
|
||||||
/// static let api = mana.api
|
|
||||||
/// // App-spezifische Kategorien:
|
|
||||||
/// static let study = mana.category("study")
|
|
||||||
/// static let sync = mana.category("sync")
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Die Standard-Kategorien sind bewusst eine kleine Schublade
|
|
||||||
/// (app/auth/api/db/web). Cards/Memoro/Manaspur sollen ihre `study`-/
|
|
||||||
/// `audio`-/`tracking`-Kategorien weiterhin app-spezifisch deklarieren.
|
|
||||||
public struct ManaAppLog: Sendable {
|
|
||||||
public let subsystem: String
|
|
||||||
|
|
||||||
/// Direkter Constructor (für Tests oder andere Subsysteme).
|
|
||||||
public init(subsystem: String) {
|
|
||||||
self.subsystem = subsystem
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructor aus einer ``ManaAppConfig``. Nutzt `logSubsystem`
|
|
||||||
/// (Default = `keychainService`, beides üblich `ev.mana.<app>`).
|
|
||||||
public init(_ config: ManaAppConfig) {
|
|
||||||
self.subsystem = config.logSubsystem
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allgemeiner App-Logger.
|
|
||||||
public var app: Logger { Logger(subsystem: subsystem, category: "app") }
|
|
||||||
|
|
||||||
/// Auth-bezogene Events.
|
|
||||||
public var auth: Logger { Logger(subsystem: subsystem, category: "auth") }
|
|
||||||
|
|
||||||
/// API-/Netzwerk-Calls.
|
|
||||||
public var api: Logger { Logger(subsystem: subsystem, category: "api") }
|
|
||||||
|
|
||||||
/// Datenbank-/SwiftData-/Persistenz-Events.
|
|
||||||
public var db: Logger { Logger(subsystem: subsystem, category: "db") }
|
|
||||||
|
|
||||||
/// Web-/WKWebView-bezogene Events (für Hybrid-Apps).
|
|
||||||
public var web: Logger { Logger(subsystem: subsystem, category: "web") }
|
|
||||||
|
|
||||||
/// App-spezifische Kategorie. Beliebige Strings, weil Console.app
|
|
||||||
/// Kategorien als Free-Text filtert.
|
|
||||||
public func category(_ name: String) -> Logger {
|
|
||||||
Logger(subsystem: subsystem, category: name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
// ============================================================
|
|
||||||
// AUTOGENERIERT — NICHT MANUELL EDITIEREN.
|
|
||||||
//
|
|
||||||
// Quelle: mana/packages/themes/src/variants/*.css
|
|
||||||
// Generator: mana/packages/themes/scripts/gen-swift-themes.mjs
|
|
||||||
// Aufruf: pnpm --filter @mana/themes gen:swift
|
|
||||||
//
|
|
||||||
// Drift-Check: nach jedem Generator-Lauf `git diff --exit-code`
|
|
||||||
// in CI sicherstellen.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
public extension ManaThemeColors {
|
|
||||||
|
|
||||||
/// forest-Variant aus `mana/packages/themes/src/variants/forest.css`.
|
|
||||||
static let forest = ManaThemeColors(
|
|
||||||
background: Color.manaToken(light: (0, 0, 100), dark: (142, 30, 8)),
|
|
||||||
foreground: Color.manaToken(light: (142, 30, 12), dark: (142, 15, 95)),
|
|
||||||
surface: Color.manaToken(light: (142, 25, 98), dark: (142, 25, 12)),
|
|
||||||
surfaceHover: Color.manaToken(light: (142, 20, 95), dark: (142, 20, 16)),
|
|
||||||
muted: Color.manaToken(light: (142, 15, 93), dark: (142, 18, 18)),
|
|
||||||
mutedForeground: Color.manaToken(light: (142, 10, 42), dark: (142, 12, 65)),
|
|
||||||
border: Color.manaToken(light: (142, 15, 88), dark: (142, 18, 22)),
|
|
||||||
primary: Color.manaToken(light: (142, 76, 28), dark: (142, 71, 45)),
|
|
||||||
primaryForeground: Color.manaToken(light: (0, 0, 100), dark: (142, 30, 8)),
|
|
||||||
error: Color.manaToken(light: (0, 84, 60), dark: (0, 63, 55)),
|
|
||||||
success: Color.manaToken(light: (142, 71, 45), dark: (142, 71, 45)),
|
|
||||||
warning: Color.manaToken(light: (38, 92, 50), dark: (48, 96, 53))
|
|
||||||
)
|
|
||||||
|
|
||||||
/// lume-Variant aus `mana/packages/themes/src/variants/lume.css`.
|
|
||||||
static let lume = ManaThemeColors(
|
|
||||||
background: Color.manaToken(light: (0, 0, 100), dark: (0, 0, 6)),
|
|
||||||
foreground: Color.manaToken(light: (0, 0, 17), dark: (0, 0, 100)),
|
|
||||||
surface: Color.manaToken(light: (0, 0, 99), dark: (0, 0, 12)),
|
|
||||||
surfaceHover: Color.manaToken(light: (51, 40, 95), dark: (51, 30, 18)),
|
|
||||||
muted: Color.manaToken(light: (0, 0, 92), dark: (0, 0, 17)),
|
|
||||||
mutedForeground: Color.manaToken(light: (0, 0, 42), dark: (0, 0, 65)),
|
|
||||||
border: Color.manaToken(light: (0, 0, 86), dark: (0, 0, 26)),
|
|
||||||
primary: Color.manaToken(light: (51, 95, 58), dark: (51, 95, 58)),
|
|
||||||
primaryForeground: Color.manaToken(light: (0, 0, 13), dark: (0, 0, 6)),
|
|
||||||
error: Color.manaToken(light: (5, 78, 55), dark: (5, 78, 60)),
|
|
||||||
success: Color.manaToken(light: (142, 71, 45), dark: (142, 71, 50)),
|
|
||||||
warning: Color.manaToken(light: (38, 92, 50), dark: (48, 96, 55))
|
|
||||||
)
|
|
||||||
|
|
||||||
/// mana-Variant aus `mana/packages/themes/src/variants/mana.css`.
|
|
||||||
static let mana = ManaThemeColors(
|
|
||||||
background: Color.manaToken(light: (0, 0, 100), dark: (220, 20, 9)),
|
|
||||||
foreground: Color.manaToken(light: (220, 15, 12), dark: (25, 15, 95)),
|
|
||||||
surface: Color.manaToken(light: (0, 0, 99), dark: (220, 18, 13)),
|
|
||||||
surfaceHover: Color.manaToken(light: (25, 30, 95), dark: (25, 20, 18)),
|
|
||||||
muted: Color.manaToken(light: (25, 20, 93), dark: (220, 18, 18)),
|
|
||||||
mutedForeground: Color.manaToken(light: (220, 10, 42), dark: (220, 10, 65)),
|
|
||||||
border: Color.manaToken(light: (25, 15, 88), dark: (220, 15, 22)),
|
|
||||||
primary: Color.manaToken(light: (25, 100, 50), dark: (25, 100, 58)),
|
|
||||||
primaryForeground: Color.manaToken(light: (0, 0, 10), dark: (220, 20, 9)),
|
|
||||||
error: Color.manaToken(light: (0, 84, 60), dark: (0, 63, 55)),
|
|
||||||
success: Color.manaToken(light: (142, 71, 45), dark: (142, 71, 45)),
|
|
||||||
warning: Color.manaToken(light: (38, 92, 50), dark: (48, 96, 53))
|
|
||||||
)
|
|
||||||
|
|
||||||
/// monochrome-Variant aus `mana/packages/themes/src/variants/monochrome.css`.
|
|
||||||
static let monochrome = ManaThemeColors(
|
|
||||||
background: Color.manaToken(light: (0, 0, 100), dark: (0, 0, 5)),
|
|
||||||
foreground: Color.manaToken(light: (0, 0, 10), dark: (0, 0, 95)),
|
|
||||||
surface: Color.manaToken(light: (0, 0, 99), dark: (0, 0, 10)),
|
|
||||||
surfaceHover: Color.manaToken(light: (0, 0, 95), dark: (0, 0, 15)),
|
|
||||||
muted: Color.manaToken(light: (0, 0, 93), dark: (0, 0, 17)),
|
|
||||||
mutedForeground: Color.manaToken(light: (0, 0, 40), dark: (0, 0, 65)),
|
|
||||||
border: Color.manaToken(light: (0, 0, 85), dark: (0, 0, 22)),
|
|
||||||
primary: Color.manaToken(light: (0, 0, 25), dark: (0, 0, 80)),
|
|
||||||
primaryForeground: Color.manaToken(light: (0, 0, 100), dark: (0, 0, 5)),
|
|
||||||
error: Color.manaToken(light: (0, 70, 45), dark: (0, 65, 55)),
|
|
||||||
success: Color.manaToken(light: (142, 60, 35), dark: (142, 60, 50)),
|
|
||||||
warning: Color.manaToken(light: (38, 90, 45), dark: (48, 90, 55))
|
|
||||||
)
|
|
||||||
|
|
||||||
/// neutral-Variant aus `mana/packages/themes/src/variants/neutral.css`.
|
|
||||||
static let neutral = ManaThemeColors(
|
|
||||||
background: Color.manaToken(light: (0, 0, 99), dark: (0, 0, 8)),
|
|
||||||
foreground: Color.manaToken(light: (0, 0, 13), dark: (0, 0, 95)),
|
|
||||||
surface: Color.manaToken(light: (0, 0, 100), dark: (0, 0, 12)),
|
|
||||||
surfaceHover: Color.manaToken(light: (0, 0, 96), dark: (0, 0, 17)),
|
|
||||||
muted: Color.manaToken(light: (0, 0, 94), dark: (0, 0, 18)),
|
|
||||||
mutedForeground: Color.manaToken(light: (0, 0, 45), dark: (0, 0, 65)),
|
|
||||||
border: Color.manaToken(light: (0, 0, 88), dark: (0, 0, 22)),
|
|
||||||
primary: Color.manaToken(light: (215, 60, 40), dark: (215, 70, 60)),
|
|
||||||
primaryForeground: Color.manaToken(light: (0, 0, 100), dark: (0, 0, 8)),
|
|
||||||
error: Color.manaToken(light: (0, 65, 45), dark: (0, 63, 55)),
|
|
||||||
success: Color.manaToken(light: (135, 35, 35), dark: (135, 35, 55)),
|
|
||||||
warning: Color.manaToken(light: (38, 92, 45), dark: (48, 96, 53))
|
|
||||||
)
|
|
||||||
|
|
||||||
/// paper-Variant aus `mana/packages/themes/src/variants/paper.css`.
|
|
||||||
static let paper = ManaThemeColors(
|
|
||||||
background: Color.manaToken(light: (38, 28, 95), dark: (24, 14, 9)),
|
|
||||||
foreground: Color.manaToken(light: (20, 14, 16), dark: (38, 24, 88)),
|
|
||||||
surface: Color.manaToken(light: (0, 0, 100), dark: (24, 12, 13)),
|
|
||||||
surfaceHover: Color.manaToken(light: (38, 24, 92), dark: (24, 14, 17)),
|
|
||||||
muted: Color.manaToken(light: (38, 20, 90), dark: (24, 12, 18)),
|
|
||||||
mutedForeground: Color.manaToken(light: (20, 14, 50), dark: (38, 12, 60)),
|
|
||||||
border: Color.manaToken(light: (38, 18, 80), dark: (24, 10, 25)),
|
|
||||||
primary: Color.manaToken(light: (18, 50, 38), dark: (24, 60, 65)),
|
|
||||||
primaryForeground: Color.manaToken(light: (0, 0, 100), dark: (24, 14, 9)),
|
|
||||||
error: Color.manaToken(light: (0, 65, 45), dark: (0, 60, 55)),
|
|
||||||
success: Color.manaToken(light: (135, 35, 35), dark: (135, 35, 55)),
|
|
||||||
warning: Color.manaToken(light: (38, 80, 40), dark: (38, 70, 55))
|
|
||||||
)
|
|
||||||
|
|
||||||
/// skylight-Variant aus `mana/packages/themes/src/variants/skylight.css`.
|
|
||||||
static let skylight = ManaThemeColors(
|
|
||||||
background: Color.manaToken(light: (205, 50, 99), dark: (215, 40, 9)),
|
|
||||||
foreground: Color.manaToken(light: (215, 30, 15), dark: (205, 25, 95)),
|
|
||||||
surface: Color.manaToken(light: (0, 0, 100), dark: (215, 35, 13)),
|
|
||||||
surfaceHover: Color.manaToken(light: (205, 40, 95), dark: (215, 30, 18)),
|
|
||||||
muted: Color.manaToken(light: (205, 30, 93), dark: (215, 30, 19)),
|
|
||||||
mutedForeground: Color.manaToken(light: (215, 15, 45), dark: (205, 18, 65)),
|
|
||||||
border: Color.manaToken(light: (205, 25, 88), dark: (215, 25, 24)),
|
|
||||||
primary: Color.manaToken(light: (205, 90, 40), dark: (205, 90, 60)),
|
|
||||||
primaryForeground: Color.manaToken(light: (0, 0, 100), dark: (215, 40, 9)),
|
|
||||||
error: Color.manaToken(light: (0, 80, 50), dark: (0, 70, 60)),
|
|
||||||
success: Color.manaToken(light: (142, 65, 40), dark: (142, 65, 50)),
|
|
||||||
warning: Color.manaToken(light: (38, 92, 50), dark: (48, 95, 55))
|
|
||||||
)
|
|
||||||
|
|
||||||
/// twilight-Variant aus `mana/packages/themes/src/variants/twilight.css`.
|
|
||||||
static let twilight = ManaThemeColors(
|
|
||||||
background: Color.manaToken(light: (250, 30, 97), dark: (260, 35, 8)),
|
|
||||||
foreground: Color.manaToken(light: (260, 25, 15), dark: (250, 20, 92)),
|
|
||||||
surface: Color.manaToken(light: (250, 40, 99), dark: (260, 30, 12)),
|
|
||||||
surfaceHover: Color.manaToken(light: (260, 30, 94), dark: (260, 28, 17)),
|
|
||||||
muted: Color.manaToken(light: (260, 25, 92), dark: (260, 25, 19)),
|
|
||||||
mutedForeground: Color.manaToken(light: (260, 15, 45), dark: (250, 15, 65)),
|
|
||||||
border: Color.manaToken(light: (260, 20, 87), dark: (260, 20, 25)),
|
|
||||||
primary: Color.manaToken(light: (260, 70, 55), dark: (260, 75, 70)),
|
|
||||||
primaryForeground: Color.manaToken(light: (0, 0, 100), dark: (260, 35, 10)),
|
|
||||||
error: Color.manaToken(light: (0, 75, 55), dark: (0, 65, 60)),
|
|
||||||
success: Color.manaToken(light: (142, 60, 45), dark: (142, 60, 55)),
|
|
||||||
warning: Color.manaToken(light: (38, 90, 55), dark: (48, 90, 55))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// Eine der acht Verein-Theme-Variants aus `mana/packages/themes`.
|
|
||||||
///
|
|
||||||
/// Die Werte-Schicht (``ManaThemeColors``) wird per `colors`-Accessor
|
|
||||||
/// geliefert. Convenience-Properties (`background`, `foreground`, …)
|
|
||||||
/// erlauben Apps, ohne `.colors`-Indirektion zu schreiben:
|
|
||||||
///
|
|
||||||
/// ```swift
|
|
||||||
/// ManaTheme.forest.foreground // Convenience
|
|
||||||
/// ManaTheme.forest.colors.foreground // explizit
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Für User-Theme-Switching nutze ``SwiftUICore/View/manaTheme(_:)``
|
|
||||||
/// und lese im View per `@Environment(\.manaTheme)`.
|
|
||||||
public enum ManaTheme: String, CaseIterable, Sendable {
|
|
||||||
case mana
|
|
||||||
case forest
|
|
||||||
case paper
|
|
||||||
case neutral
|
|
||||||
case lume
|
|
||||||
case twilight
|
|
||||||
case skylight
|
|
||||||
case monochrome
|
|
||||||
|
|
||||||
/// Konkrete Token-Werte für diese Variant.
|
|
||||||
public var colors: ManaThemeColors {
|
|
||||||
switch self {
|
|
||||||
case .mana: return .mana
|
|
||||||
case .forest: return .forest
|
|
||||||
case .paper: return .paper
|
|
||||||
case .neutral: return .neutral
|
|
||||||
case .lume: return .lume
|
|
||||||
case .twilight: return .twilight
|
|
||||||
case .skylight: return .skylight
|
|
||||||
case .monochrome: return .monochrome
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Convenience-Accessors
|
|
||||||
|
|
||||||
public var background: Color { colors.background }
|
|
||||||
public var foreground: Color { colors.foreground }
|
|
||||||
public var surface: Color { colors.surface }
|
|
||||||
public var surfaceHover: Color { colors.surfaceHover }
|
|
||||||
public var muted: Color { colors.muted }
|
|
||||||
public var mutedForeground: Color { colors.mutedForeground }
|
|
||||||
public var border: Color { colors.border }
|
|
||||||
public var primary: Color { colors.primary }
|
|
||||||
public var primaryForeground: Color { colors.primaryForeground }
|
|
||||||
public var error: Color { colors.error }
|
|
||||||
public var success: Color { colors.success }
|
|
||||||
public var warning: Color { colors.warning }
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// Die 12 Vereins-Tokens als konkrete Color-Werte für eine bestimmte
|
|
||||||
/// Theme-Variant. Wird heute per-Variant von ``ManaTheme/colors``
|
|
||||||
/// geliefert; die statischen Variants (``ManaThemeColors/mana``,
|
|
||||||
/// ``ManaThemeColors/forest`` etc.) leben im generierten File
|
|
||||||
/// `GeneratedThemes.swift`.
|
|
||||||
///
|
|
||||||
/// **Verwendung:**
|
|
||||||
///
|
|
||||||
/// ```swift
|
|
||||||
/// // Direkt:
|
|
||||||
/// Text("Hi").foregroundColor(ManaTheme.forest.foreground)
|
|
||||||
///
|
|
||||||
/// // Via Environment (für Apps, die User-Theme-Switching anbieten):
|
|
||||||
/// @Environment(\.manaTheme) var theme
|
|
||||||
/// Text("Hi").foregroundColor(theme.foreground)
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Beide Werte (`light:`, `dark:`) sind CSS-HSL-Tripel `(hue 0–360,
|
|
||||||
/// saturation 0–100, lightness 0–100)`. Quelle:
|
|
||||||
/// `mana/packages/themes/src/variants/*.css`.
|
|
||||||
public struct ManaThemeColors: Sendable {
|
|
||||||
/// Token 1 — `--color-background`. Page-Hintergrund.
|
|
||||||
public let background: Color
|
|
||||||
/// Token 2 — `--color-foreground`. Standard-Text.
|
|
||||||
public let foreground: Color
|
|
||||||
/// Token 3 — `--color-surface`. Card, Panel, Modal, Popover.
|
|
||||||
public let surface: Color
|
|
||||||
/// Token 4 — `--color-surface-hover`. Hover-State auf Surface.
|
|
||||||
public let surfaceHover: Color
|
|
||||||
/// Token 5 — `--color-muted`. Disabled-Felder, Skeleton.
|
|
||||||
public let muted: Color
|
|
||||||
/// Token 6 — `--color-muted-foreground`. Sekundär-Text, Placeholder.
|
|
||||||
public let mutedForeground: Color
|
|
||||||
/// Token 7 — `--color-border`. Rahmen, Trennlinien.
|
|
||||||
public let border: Color
|
|
||||||
/// Token 8 — `--color-primary`. App-Akzent.
|
|
||||||
public let primary: Color
|
|
||||||
/// Token 9 — `--color-primary-foreground`. Text auf Primary-Flächen.
|
|
||||||
public let primaryForeground: Color
|
|
||||||
/// Token 10 — `--color-error`. Fehler, Lösch-Aktion.
|
|
||||||
public let error: Color
|
|
||||||
/// Token 11 — `--color-success`. Erfolg, Bestätigung.
|
|
||||||
public let success: Color
|
|
||||||
/// Token 12 — `--color-warning`. Warnung, Aufmerksamkeit.
|
|
||||||
public let warning: Color
|
|
||||||
|
|
||||||
public init(
|
|
||||||
background: Color,
|
|
||||||
foreground: Color,
|
|
||||||
surface: Color,
|
|
||||||
surfaceHover: Color,
|
|
||||||
muted: Color,
|
|
||||||
mutedForeground: Color,
|
|
||||||
border: Color,
|
|
||||||
primary: Color,
|
|
||||||
primaryForeground: Color,
|
|
||||||
error: Color,
|
|
||||||
success: Color,
|
|
||||||
warning: Color
|
|
||||||
) {
|
|
||||||
self.background = background
|
|
||||||
self.foreground = foreground
|
|
||||||
self.surface = surface
|
|
||||||
self.surfaceHover = surfaceHover
|
|
||||||
self.muted = muted
|
|
||||||
self.mutedForeground = mutedForeground
|
|
||||||
self.border = border
|
|
||||||
self.primary = primary
|
|
||||||
self.primaryForeground = primaryForeground
|
|
||||||
self.error = error
|
|
||||||
self.success = success
|
|
||||||
self.warning = warning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
private struct ManaThemeEnvironmentKey: EnvironmentKey {
|
|
||||||
static let defaultValue: ManaTheme = .mana
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension EnvironmentValues {
|
|
||||||
/// Aktuell aktive Theme-Variant. Default: ``ManaTheme/mana``.
|
|
||||||
var manaTheme: ManaTheme {
|
|
||||||
get { self[ManaThemeEnvironmentKey.self] }
|
|
||||||
set { self[ManaThemeEnvironmentKey.self] = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension View {
|
|
||||||
/// Setzt die aktive Theme-Variant für diesen View-Subtree.
|
|
||||||
///
|
|
||||||
/// Apps mit fixer Identität setzen das an der App-Root:
|
|
||||||
///
|
|
||||||
/// ```swift
|
|
||||||
/// @main
|
|
||||||
/// struct CardsApp: App {
|
|
||||||
/// var body: some Scene {
|
|
||||||
/// WindowGroup {
|
|
||||||
/// ContentView()
|
|
||||||
/// .manaTheme(.forest)
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Apps mit User-Theme-Switching binden den Wert an `@AppStorage`
|
|
||||||
/// oder eine Settings-Source und re-rendern bei Wechsel automatisch.
|
|
||||||
func manaTheme(_ theme: ManaTheme) -> some View {
|
|
||||||
environment(\.manaTheme, theme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -24,42 +24,4 @@ struct ManaAppConfigTests {
|
||||||
)
|
)
|
||||||
#expect(config.keychainAccessGroup == nil)
|
#expect(config.keychainAccessGroup == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("AppGroup ist optional, default nil")
|
|
||||||
func appGroupDefaultsNil() {
|
|
||||||
let config = DefaultManaAppConfig(
|
|
||||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
|
||||||
keychainService: "ev.mana.cards"
|
|
||||||
)
|
|
||||||
#expect(config.appGroup == nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("AppGroup wird durchgereicht wenn gesetzt")
|
|
||||||
func appGroupPassedThrough() {
|
|
||||||
let config = DefaultManaAppConfig(
|
|
||||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
|
||||||
keychainService: "ev.mana.cards",
|
|
||||||
appGroup: "group.ev.mana.cards"
|
|
||||||
)
|
|
||||||
#expect(config.appGroup == "group.ev.mana.cards")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("LogSubsystem default = keychainService")
|
|
||||||
func logSubsystemDefaultsToKeychainService() {
|
|
||||||
let config = DefaultManaAppConfig(
|
|
||||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
|
||||||
keychainService: "ev.mana.cards"
|
|
||||||
)
|
|
||||||
#expect(config.logSubsystem == "ev.mana.cards")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("LogSubsystem kann explizit überschrieben werden")
|
|
||||||
func logSubsystemOverride() {
|
|
||||||
let config = DefaultManaAppConfig(
|
|
||||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
|
||||||
keychainService: "ev.mana.cards",
|
|
||||||
logSubsystem: "ev.mana.cards.debug"
|
|
||||||
)
|
|
||||||
#expect(config.logSubsystem == "ev.mana.cards.debug")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import Foundation
|
|
||||||
import Testing
|
|
||||||
@testable import ManaCore
|
|
||||||
|
|
||||||
@Suite("ManaAppLog")
|
|
||||||
struct ManaAppLogTests {
|
|
||||||
@Test("Init aus Config nimmt logSubsystem")
|
|
||||||
func initFromConfig() {
|
|
||||||
let config = DefaultManaAppConfig(
|
|
||||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
|
||||||
keychainService: "ev.mana.cards"
|
|
||||||
)
|
|
||||||
let log = ManaAppLog(config)
|
|
||||||
#expect(log.subsystem == "ev.mana.cards")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Explizit gesetztes logSubsystem überschreibt keychainService")
|
|
||||||
func initFromConfigWithCustomSubsystem() {
|
|
||||||
let config = DefaultManaAppConfig(
|
|
||||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
|
||||||
keychainService: "ev.mana.cards",
|
|
||||||
logSubsystem: "ev.mana.cards.test"
|
|
||||||
)
|
|
||||||
let log = ManaAppLog(config)
|
|
||||||
#expect(log.subsystem == "ev.mana.cards.test")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Direkter String-Constructor")
|
|
||||||
func initFromString() {
|
|
||||||
let log = ManaAppLog(subsystem: "ev.mana.test")
|
|
||||||
#expect(log.subsystem == "ev.mana.test")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Standard-Kategorien sind erreichbar")
|
|
||||||
func standardCategoriesExist() {
|
|
||||||
let log = ManaAppLog(subsystem: "ev.mana.test")
|
|
||||||
// Logger ist nicht Equatable, aber wir können Existenz prüfen
|
|
||||||
// indem wir den Wert binden — Compile genügt.
|
|
||||||
let _ = log.app
|
|
||||||
let _ = log.auth
|
|
||||||
let _ = log.api
|
|
||||||
let _ = log.db
|
|
||||||
let _ = log.web
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("category() liefert beliebige Custom-Kategorie")
|
|
||||||
func customCategory() {
|
|
||||||
let log = ManaAppLog(subsystem: "ev.mana.test")
|
|
||||||
let _ = log.category("study")
|
|
||||||
let _ = log.category("tracking")
|
|
||||||
let _ = log.category("llm")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import SwiftUI
|
|
||||||
import Testing
|
|
||||||
@testable import ManaTokens
|
|
||||||
|
|
||||||
@Suite("ManaTheme Variants")
|
|
||||||
struct ThemeTests {
|
|
||||||
@Test("Alle 8 Variants sind im enum vorhanden")
|
|
||||||
func allCasesPresent() {
|
|
||||||
let cases = ManaTheme.allCases.map(\.rawValue).sorted()
|
|
||||||
#expect(cases == [
|
|
||||||
"forest",
|
|
||||||
"lume",
|
|
||||||
"mana",
|
|
||||||
"monochrome",
|
|
||||||
"neutral",
|
|
||||||
"paper",
|
|
||||||
"skylight",
|
|
||||||
"twilight",
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Jede Variant liefert nicht-leere Colors")
|
|
||||||
func everyVariantHasColors() {
|
|
||||||
for variant in ManaTheme.allCases {
|
|
||||||
let colors = variant.colors
|
|
||||||
// Existenz-Smoke — Color hat keinen Equatable-Color-Vergleich,
|
|
||||||
// aber wir können die Properties gegen Optional/Crash absichern.
|
|
||||||
let _: Color = colors.background
|
|
||||||
let _: Color = colors.foreground
|
|
||||||
let _: Color = colors.surface
|
|
||||||
let _: Color = colors.surfaceHover
|
|
||||||
let _: Color = colors.muted
|
|
||||||
let _: Color = colors.mutedForeground
|
|
||||||
let _: Color = colors.border
|
|
||||||
let _: Color = colors.primary
|
|
||||||
let _: Color = colors.primaryForeground
|
|
||||||
let _: Color = colors.error
|
|
||||||
let _: Color = colors.success
|
|
||||||
let _: Color = colors.warning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Convenience-Accessor matcht colors-Accessor")
|
|
||||||
func convenienceMatchesColors() {
|
|
||||||
// Identitäts-Smoke — beide Pfade müssen denselben Color-Wert liefern.
|
|
||||||
// Color ist nicht Equatable, also testen wir, dass es überhaupt
|
|
||||||
// möglich ist, beide Pfade typkorrekt zu nutzen.
|
|
||||||
let theme = ManaTheme.forest
|
|
||||||
let direct = theme.background
|
|
||||||
let viaColors = theme.colors.background
|
|
||||||
let _: (Color, Color) = (direct, viaColors)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("forest.primary entspricht forest.css (142, 76, 28)")
|
|
||||||
func forestPrimaryMatchesSpec() {
|
|
||||||
// Wir rekonstruieren den erwarteten Light-Wert aus HSL und
|
|
||||||
// vergleichen mit dem, was `Color.manaToken` für denselben Input
|
|
||||||
// liefert. Da `Color`-Vergleich nicht direkt möglich ist, prüfen
|
|
||||||
// wir indirekt über PlatformColor.fromHSL.
|
|
||||||
let expected = PlatformColor.fromHSL(142, 76, 28)
|
|
||||||
let components = rgbComponents(of: expected)
|
|
||||||
// Forest-Primary Light: HSL(142, 76, 28) → ~RGB(0.067, 0.493, 0.231)
|
|
||||||
#expect(approxEqual(components.r, 0.067, tolerance: 0.02))
|
|
||||||
#expect(approxEqual(components.g, 0.493, tolerance: 0.02))
|
|
||||||
#expect(approxEqual(components.b, 0.231, tolerance: 0.02))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("ManaTheme ist via rawValue rekonstruierbar")
|
|
||||||
func rawValueRoundtrip() {
|
|
||||||
for variant in ManaTheme.allCases {
|
|
||||||
let raw = variant.rawValue
|
|
||||||
let roundtrip = ManaTheme(rawValue: raw)
|
|
||||||
#expect(roundtrip == variant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Environment-Default ist mana")
|
|
||||||
func environmentDefault() {
|
|
||||||
// Wir können das Environment hier nicht direkt anzapfen ohne
|
|
||||||
// einen Host-View, aber wir spiegeln das Default per Konstante:
|
|
||||||
// ManaThemeEnvironmentKey.defaultValue ist auf .mana gesetzt.
|
|
||||||
// Smoke-Test, der die Konvention dokumentiert.
|
|
||||||
#expect(ManaTheme.allCases.contains(.mana))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
|
||||||
|
|
||||||
private func rgbComponents(of color: PlatformColor) -> (r: Double, g: Double, b: Double) {
|
|
||||||
#if canImport(UIKit)
|
|
||||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
|
||||||
color.getRed(&r, green: &g, blue: &b, alpha: &a)
|
|
||||||
return (Double(r), Double(g), Double(b))
|
|
||||||
#else
|
|
||||||
let space = color.usingColorSpace(.deviceRGB) ?? color
|
|
||||||
return (Double(space.redComponent), Double(space.greenComponent), Double(space.blueComponent))
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private func approxEqual(_ a: Double, _ b: Double, tolerance: Double = 0.001) -> Bool {
|
|
||||||
abs(a - b) < tolerance
|
|
||||||
}
|
|
||||||
4
build/GeneratedModuleMaps/ManaCore.modulemap
Normal file
4
build/GeneratedModuleMaps/ManaCore.modulemap
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
module ManaCore {
|
||||||
|
header "ManaCore-Swift.h"
|
||||||
|
export *
|
||||||
|
}
|
||||||
4
build/GeneratedModuleMaps/ManaTokens.modulemap
Normal file
4
build/GeneratedModuleMaps/ManaTokens.modulemap
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
module ManaTokens {
|
||||||
|
header "ManaTokens-Swift.h"
|
||||||
|
export *
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"ABIRoot": {
|
||||||
|
"kind": "Root",
|
||||||
|
"name": "NO_MODULE",
|
||||||
|
"printedName": "NO_MODULE",
|
||||||
|
"json_format_version": 8
|
||||||
|
},
|
||||||
|
"ConstValues": []
|
||||||
|
}
|
||||||
BIN
build/Release/ManaTokens.swiftmodule/x86_64-apple-macos.swiftdoc
Normal file
BIN
build/Release/ManaTokens.swiftmodule/x86_64-apple-macos.swiftdoc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue