v0.1.0 — Phase β-0 Setup

Repo-Skelett für cards-native, native SwiftUI-Universal-App
für Cardecky (mana e.V.). Web-Parität zu cardecky.mana.how.

- project.yml mit Bundle ev.mana.cards, ManaSwiftCore-Dep via path
- AppConfig: auth.mana.how + cardecky-api.mana.how, Keychain ev.mana.cards
- CardsTheme: forest-Werte aus mana/packages/themes/.../forest.css
- LoginView (Email/PW gegen mana-auth via ManaCore.AuthClient)
- DashboardView als β-1-Placeholder mit cardecky-api-Reachability-Probe
- Log unter Subsystem ev.mana.cards
- 3 AppConfig-Tests
- iOS-Simulator-Build grün

Phasen-Plan: mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-12 19:29:45 +02:00
commit 28b20cd934
21 changed files with 896 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
.DS_Store
.build/
.swiftpm/
DerivedData/
Package.resolved
xcuserdata/
# XcodeGen output
*.xcodeproj
Sources/Resources/Info.plist
Sources/Resources/CardsNative.entitlements

10
.swiftformat Normal file
View file

@ -0,0 +1,10 @@
--swiftversion 6.0
--indent 4
--maxwidth 120
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--commas inline
--semicolons never
--self remove
--importgrouping testable-bottom

26
.swiftlint.yml Normal file
View file

@ -0,0 +1,26 @@
disabled_rules:
- todo
- trailing_comma
opt_in_rules:
- empty_count
- empty_string
- explicit_init
- first_where
- sorted_first_last
- toggle_bool
line_length:
warning: 120
error: 160
ignores_comments: true
identifier_name:
min_length: 2
excluded:
- id
- ok
included:
- Sources
- Tests

183
CLAUDE.md Normal file
View file

@ -0,0 +1,183 @@
# CLAUDE.md — cards-native repo
Guidance für Claude Code in diesem Repository.
> **Wenn du gerade neu bist:** lies zuerst [`PLAN.md`](PLAN.md) und
> `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md` (übergeordneter
> Greenfield-Plan). Dieses CLAUDE.md ist die Konventions- und
> Cross-Repo-Referenz.
## Was dieses Repo ist
**Cards Native** — native SwiftUI-Universal-App (iOS / iPadOS / macOS)
für **Cardecky**, die Spaced-Repetition-Karten-App des Vereins
**mana e.V.** Web-Parität zu `cardecky.mana.how`, plus native iOS-
Affordances (Widgets, Notifications, Universal-Links, Pencil).
```
HTTPS/JWT ┌──────────────────┐
cards-api ◄───────────── │ cards-native │ SwiftUI
cardecky-api.mana.how │ (this repo) │ SwiftData (Cache)
│ ev.mana.cards │ WidgetKit (β-6)
└──────────────────┘
```
## Status
**Phase β-0 — Setup (2026-05-12).** Repo-Skelett, ManaCore + ManaTokens
als Package-Dependency, Login + Cardecky-API-Reachability-Probe.
Phasen β-1 bis β-7 in `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
## Leitprinzip: Web-Parität
Die Web-App auf `cardecky.mana.how` ist Funktions-Referenz. Bei
Konflikt zwischen Native und Web → **Web gewinnt**. Native ist
Re-Implementation, kein neues Produkt.
Datenmodell, FSRS-Verhalten, Marketplace-Slugs, Sharing-URLs:
identisch zu Web.
## Architektonische Invarianten
Beschlossen. Nicht ohne explizite Diskussion antasten.
1. **Server-authoritative FSRS.** Grading-Calls gehen *immer* an
`POST /api/v1/reviews/:cardId/:subIndex/grade`. Kein lokaler
ts-fsrs-Port.
2. **Offline-Read, Online-Write.** Decks + Due-Cards via SwiftData
gecacht (offline sichtbar). Grades werden bei Offline in einer
lokalen Queue persistiert und beim Reconnect der Reihe nach
abgesendet.
3. **mana-auth via ManaCore.** `import ManaCore`,
`AuthClient(config: AppConfig.manaAppConfig)`. Eigene
Auth-Implementierung ist verboten.
4. **Pure SwiftUI.** Keine externen UI-Libraries. AppKit/UIKit nur
als Bridge wenn zwingend (z.B. `PencilKit` für Image-Occlusion).
5. **Bundle-ID `ev.mana.cards`.** Reverse-Domain mana-ev.ch.
Universal-Link-Domain: `cardecky.mana.how`.
6. **Cards-Domain-Logik bleibt am Server.** SubIndex-Berechnung für
Cloze, Image-Occlusion-Mask-Validation, Content-Hash — alles
Server. Native zeigt nur, was vom Server kommt.
7. **`forest`-Theme.** Heute lokal in `CardsTheme.swift` nachgebaut
(Werte gespiegelt aus `mana/packages/themes/src/variants/forest.css`).
Migration auf ManaTokens-Theme-Switch ist Phase ε.
8. **Web gewinnt bei Konflikt.** Eleganteres Native-Verhalten geht
zuerst in die Web-App, dann nach hier.
## Konventionen
- **Swift 6.0**, Strict Concurrency komplett
- **iOS 18 / iPadOS 18 / macOS 15** Minimum
- **SwiftUI** als einziges UI-Framework
- **XcodeGen** als SOT: `project.yml` definiert Targets, Info.plist,
Entitlements. `.xcodeproj`, generierte Info.plist und Entitlements
sind **nicht** im Git
- **SwiftFormat** mit `.swiftformat` (4-space, 120-col, sorted imports)
- **SwiftLint** mit `.swiftlint.yml`
- **Logging:** App-Subsystem `ev.mana.cards` via
`Sources/Core/Telemetry/Log.swift`. ManaCore loggt parallel unter
`ev.mana.core`
- **Persistenz:** SwiftData für Deck/Card-Cache (ab β-1), JWT im
Keychain (über ManaCore)
- **Lokalisierung:** DE primary, EN fallback via `Localizable.xcstrings`
## Cardecky-API-Wire-Format
Wire-Format gegen `https://cardecky-api.mana.how/api/v1/*`. Quelle der
Wahrheit: `../cards/apps/api/src/routes/*.ts`. Bei neuem DTO
verifizieren:
1. Path + Method gegen den Hono-Handler prüfen
2. Response-Schema (`zod`) gegen `Codable`-Struct mappen
3. snake_case via `CodingKeys`, Optionale Felder explizit `Optional<T>`
4. Test-Fixture aus echtem Server-Response in `Tests/UnitTests/`
## Repo-Layout
```
cards-native/
├── project.yml XcodeGen-Manifest (SOT)
├── PLAN.md Phase-Tracking (gekürzt aus Greenfield-Plan)
├── CLAUDE.md dieses File
├── README.md
├── .swiftformat, .swiftlint.yml
├── Sources/
│ ├── App/ CardsNativeApp (@main), RootView
│ ├── Features/
│ │ ├── Account/ LoginView, AccountView (ab β-1)
│ │ ├── Decks/ DashboardView (Placeholder), DeckList (β-1)
│ │ ├── Study/ (β-2)
│ │ ├── Editor/ (β-3)
│ │ ├── Marketplace/ (β-5)
│ │ ├── Stats/ (β-1)
│ │ └── Imports/ (β-3)
│ ├── Core/
│ │ ├── Auth/ AppConfig (ManaAppConfig-Provider)
│ │ ├── API/ CardsAPI (AuthenticatedTransport-Wrapper)
│ │ ├── Domain/ (Card-Type-Enums, Rating-Enum — ab β-2)
│ │ ├── Storage/ (SwiftData-Models — ab β-1)
│ │ ├── Sync/ (ReviewQueue, MediaCache — ab β-2/β-4)
│ │ ├── Telemetry/ OSLog (Subsystem ev.mana.cards)
│ │ └── Theme/ CardsTheme (forest-Werte)
│ ├── Widgets/ (WidgetKit-Extension — ab β-6)
│ ├── ShareExtension/ (Save-as-Card — ab β-6)
│ └── Resources/
│ ├── Assets.xcassets
│ ├── Localizable.xcstrings
│ ├── Info.plist (generiert, gitignored)
│ └── CardsNative.entitlements (generiert, gitignored)
├── Tests/
│ ├── UnitTests/
│ └── UITests/
└── docs/
```
## Wichtige Cross-Repo-Doks
- `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md` — vollständiger
Phasen-Plan und Architektur-Entscheidungen
- `../mana/docs/MANA_SWIFT.md` — native-Plattform-SOT
- `../mana/docs/MANA_AUTH_FEDERATION.md` — Auth-Protokoll, das
ManaCore implementiert
- `../mana/docs/COMPLIANCE.md` — Telemetrie/Auth/Bezahl-Regeln, gilt
auch nativ
- `../cards/CLAUDE.md` — Cards-Repo, Web + API
- `../cards/STATUS.md` — Web-Phasenstand (Funktions-Referenz)
- `../mana-swift-core/CLAUDE.md` — geteilter Code, Konventionen
## Lokal entwickeln
**Pre-Requisites:**
- Xcode 16+
- `brew install xcodegen swiftformat swiftlint`
- `../mana-swift-core/` muss als Schwester-Verzeichnis existieren
(Package-Dependency via `path: ../mana-swift-core`)
**Workflow:**
```bash
xcodegen generate
open CardsNative.xcodeproj
```
**Vor jedem Commit:**
```bash
swiftformat Sources Tests
swiftlint --strict
```
## Phasen-Disziplin
Jede Phase aus dem Greenfield-Plan hat ein verifizierbares
Erfolgskriterium. Nicht in die nächste Phase reinarbeiten, bevor
die vorherige abgeschlossen ist:
- β-0: leerer Build + Login + API-Reachability-Probe (**JETZT**)
- β-1: Decks-Liste mit SwiftData-Cache
- β-2: Study-Loop + Offline-Grade-Queue + Endurance-Test auf realem Gerät
- β-3: Card-/Deck-Editor (basic, cloze, typing, multiple-choice)
- β-4: Media + image-occlusion + audio-front
- β-5: Marketplace + Universal-Links
- β-6: Native-Polish (Widgets, Notifications, Share-Extension)
- β-7: App-Store-Submission
Bei Phasen-Wechsel: PLAN.md aktualisieren + Greenfield-Plan abhaken.

63
PLAN.md Normal file
View file

@ -0,0 +1,63 @@
# Plan — cards-native (SwiftUI Universal)
**Stand: 2026-05-12 — Phase β-0 abgeschlossen.** Repo lebt lokal,
ManaCore + ManaTokens als Package-Dependency, Login funktioniert,
Cardecky-API-Reachability-Probe.
> **SOT:** `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
> Dieses File ist die App-lokale Status-Spur, das Greenfield-Doc
> hat die ganze Architektur-Begründung.
## Aktueller Stand
✅ **β-0 — Setup**
- Repo-Skelett unter `git.mana.how/till/cards-native`
- `project.yml` mit Bundle-ID `ev.mana.cards`, ManaSwiftCore via
`path: ../mana-swift-core`
- `AppConfig` als `ManaAppConfig`-Provider:
- Auth: `https://auth.mana.how`
- API: `https://cardecky-api.mana.how`
- Keychain-Service: `ev.mana.cards`
- `CardsTheme.swift` mit forest-Werten (lokal nachgebaut aus
`mana/packages/themes/src/variants/forest.css`)
- `LoginView` (Email/PW gegen mana-auth)
- `DashboardView` als β-1-Placeholder mit API-Reachability-Indikator
- 3 Unit-Tests (AppConfig)
- iOS-Simulator-Build grün
## Phasen (Detail in Greenfield-Plan)
| Phase | Status | Inhalt |
|---|---|---|
| β-0 | ✅ 2026-05-12 | Setup, Login, API-Probe |
| β-1 | ⏳ | Decks lesen, SwiftData-Cache |
| β-2 | — | Study-Loop, Offline-Grade-Queue, Endurance-Test |
| β-3 | — | Card-/Deck-Editor (basic, cloze, typing, multiple-choice) |
| β-4 | — | Media, image-occlusion (PencilKit), audio-front |
| β-5 | — | Marketplace, Universal-Links |
| β-6 | — | Native-Polish (Widgets, Notifications, Share-Extension) |
| β-7 | — | App-Store-Submission |
## Nächste Schritte für β-1
Aus Greenfield-Plan-Sektion "Phase β-1 — Decks lesen":
1. `Deck`-`Codable`-Struct nach Wire-Format aus
`../cards/apps/api/src/routes/decks.ts` + `cards/packages/cards-domain/src/schemas/`
2. `CardsAPI.decks() -> [Deck]` mit `GET /api/v1/decks`
3. `DeckListView` mit Pull-to-Refresh, Card/Due-Counts
4. `CachedDeck` als SwiftData-Model mit `lastFetchedAt`
5. Offline-Display bei fehlendem Netz
6. Inbox-Banner aus `?forked_from_marketplace=true`-Query
**Erfolgskriterium:** Web-Account-Decks vollständig in identischer
Reihenfolge sichtbar, Pull-to-Refresh aktualisiert Counts.
## Cross-Refs
- `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md` — Greenfield-Plan SOT
- `../mana/docs/MANA_SWIFT.md` — Plattform-SOT
- `../cards/CLAUDE.md` — Cards-Repo
- `../cards/STATUS.md` — Web-Phasenstand (Referenz)
- `../mana-swift-core/CLAUDE.md` — ManaCore-Konventionen
- `CLAUDE.md` — Repo-Konventionen

36
README.md Normal file
View file

@ -0,0 +1,36 @@
# cards-native
Native SwiftUI-Universal-App (iOS / iPadOS / macOS) für Cardecky —
die Spaced-Repetition-Karten-App des Vereins **mana e.V.**
> **Web-App-Parität.** Die existierende Web-App auf
> `cardecky.mana.how` ist Funktions- und Verhaltens-Referenz.
> Native bringt kein neues Produkt, sondern die App in einer Form,
> die iOS-Hardware besser nutzt.
## Status
**Phase β-0 — Setup.** Leerer Build, Login funktioniert, Cardecky-API-
Reachability-Check. Vollständiger Phasen-Plan in
`../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
```
HTTPS/JWT ┌──────────────────┐
cards-api ◄───────────── │ cards-native │ SwiftUI
cardecky-api.mana.how │ ev.mana.cards │ WidgetKit (β-6)
└──────────────────┘
┌─────────────────────────────────────────┐
▼ ▼
ManaCore (Auth, Transport) ManaTokens (Designwerte)
git.mana.how/till/mana-swift-core v1.0.0+
```
## Lokal entwickeln
```bash
xcodegen generate
open CardsNative.xcodeproj # iPhone-17-Simulator
```
Konventionen, Invarianten, Phasen-Disziplin: [`CLAUDE.md`](CLAUDE.md).

View file

@ -0,0 +1,22 @@
import ManaCore
import SwiftUI
@main
struct CardsNativeApp: App {
@State private var auth: AuthClient
init() {
let auth = AuthClient(config: AppConfig.manaAppConfig)
auth.bootstrap()
_auth = State(initialValue: auth)
Log.app.info("Cards starting — auth status: \(String(describing: auth.status), privacy: .public)")
}
var body: some Scene {
WindowGroup {
RootView()
.environment(auth)
.tint(CardsTheme.primary)
}
}
}

View file

@ -0,0 +1,18 @@
import ManaCore
import SwiftUI
/// Top-Level-Switch: Login vs Dashboard.
/// Ab Phase β-1 wird Dashboard durch eine echte Tab-Bar (Decks / Study /
/// Stats / Account) ersetzt.
struct RootView: View {
@Environment(AuthClient.self) private var auth
var body: some View {
switch auth.status {
case .signedIn:
DashboardView()
case .unknown, .signedOut, .signingIn, .error:
LoginView()
}
}
}

View file

@ -0,0 +1,22 @@
import Foundation
import ManaCore
/// Cards-spezifischer API-Client. Wrapper um `AuthenticatedTransport`
/// aus ManaCore, der die Cardecky-Endpoints kennt.
///
/// In Phase β-0 ist die API leer Endpoints kommen ab β-1 (Decks),
/// β-2 (Reviews), β-3 (Editor), β-4 (Media), β-5 (Marketplace).
actor CardsAPI {
private let transport: AuthenticatedTransport
init(auth: AuthClient) {
transport = AuthenticatedTransport(baseURL: AppConfig.apiBaseURL, auth: auth)
}
/// Health-Probe für β-0 verifiziert dass cardecky-api erreichbar
/// ist und der eigene JWT akzeptiert wird.
func healthCheck() async throws -> Bool {
let (_, http) = try await transport.request(path: "/healthz")
return http.statusCode == 200
}
}

View file

@ -0,0 +1,15 @@
import Foundation
import ManaCore
/// App-spezifische Konfiguration für Cards. Implementiert `ManaAppConfig`
/// aus ManaCore und ergänzt die Cards-eigene `apiBaseURL` (cardecky-api,
/// getrennt von mana-auth).
enum AppConfig {
static let manaAppConfig: ManaAppConfig = DefaultManaAppConfig(
authBaseURL: URL(string: "https://auth.mana.how")!,
keychainService: "ev.mana.cards",
keychainAccessGroup: nil
)
static let apiBaseURL = URL(string: "https://cardecky-api.mana.how")!
}

View file

@ -0,0 +1,13 @@
import Foundation
import OSLog
/// App-eigene OSLog-Logger unter Subsystem `ev.mana.cards`.
/// ManaCore loggt unter `ev.mana.core` parallel siehe
/// `mana-swift-core/Sources/ManaCore/Telemetry/CoreLog.swift`.
enum Log {
static let app = Logger(subsystem: "ev.mana.cards", category: "app")
static let auth = Logger(subsystem: "ev.mana.cards", category: "auth")
static let api = Logger(subsystem: "ev.mana.cards", category: "api")
static let study = Logger(subsystem: "ev.mana.cards", category: "study")
static let sync = Logger(subsystem: "ev.mana.cards", category: "sync")
}

View file

@ -0,0 +1,99 @@
import SwiftUI
#if canImport(UIKit)
import UIKit
private typealias PlatformColorType = UIColor
#elseif canImport(AppKit)
import AppKit
private typealias PlatformColorType = NSColor
#endif
/// Forest-Theme aus `mana/packages/themes/src/variants/forest.css`.
/// Lokal in cards-native nachgebaut, weil ManaTokens v1.0.0 nur den
/// Default-Theme (mana-Variant) liefert.
///
/// Migration auf einen Theme-Switch in ManaTokens ist Phase ε aus
/// `mana/docs/MANA_SWIFT.md` bis dahin lebt forest hier.
enum CardsTheme {
/// Page-Hintergrund
static let background = dynamic(light: (0, 0, 100), dark: (142, 30, 8))
/// Standard-Text
static let foreground = dynamic(light: (142, 30, 12), dark: (142, 15, 95))
/// Card, Panel, Modal
static let surface = dynamic(light: (142, 25, 98), dark: (142, 25, 12))
/// Hover-State auf Surface
static let surfaceHover = dynamic(light: (142, 20, 95), dark: (142, 20, 16))
/// Disabled-Felder, Skeleton
static let muted = dynamic(light: (142, 15, 93), dark: (142, 18, 18))
/// Sekundär-Text, Placeholder
static let mutedForeground = dynamic(light: (142, 10, 42), dark: (142, 12, 65))
/// Rahmen, Trennlinien
static let border = dynamic(light: (142, 15, 88), dark: (142, 18, 22))
/// Cards-Brand-Grün Tiefgrün im Light, leuchtender im Dark
static let primary = dynamic(light: (142, 76, 28), dark: (142, 71, 45))
/// Text auf Primary
static let primaryForeground = dynamic(light: (0, 0, 100), dark: (142, 30, 8))
static let error = dynamic(light: (0, 84, 60), dark: (0, 63, 55))
static let success = dynamic(light: (142, 71, 45), dark: (142, 71, 45))
static let warning = dynamic(light: (38, 92, 50), dark: (48, 96, 53))
// MARK: - HSL Helper
private static func dynamic(
light: (Double, Double, Double),
dark: (Double, Double, Double)
) -> Color {
let lightColor = fromHSL(light.0, light.1, light.2)
let darkColor = fromHSL(dark.0, dark.1, dark.2)
#if canImport(UIKit)
return Color(uiColor: UIColor { trait in
trait.userInterfaceStyle == .dark ? darkColor : lightColor
})
#elseif canImport(AppKit)
return Color(nsColor: NSColor(name: nil) { appearance in
let isDark = appearance.bestMatch(from: [.darkAqua, .vibrantDark]) != nil
return isDark ? darkColor : lightColor
})
#else
return Color(red: 0, green: 0, blue: 0)
#endif
}
private static func fromHSL(_ hue: Double, _ saturation: Double, _ lightness: Double) -> PlatformColorType {
let h = hue / 360
let s = saturation / 100
let l = lightness / 100
if s == 0 {
return PlatformColorType(red: l, green: l, blue: l, alpha: 1)
}
let q = l < 0.5 ? l * (1 + s) : l + s - l * s
let p = 2 * l - q
let r = hueToRGB(p, q, h + 1.0 / 3.0)
let g = hueToRGB(p, q, h)
let b = hueToRGB(p, q, h - 1.0 / 3.0)
return PlatformColorType(red: r, green: g, blue: b, alpha: 1)
}
private static func hueToRGB(_ p: Double, _ q: Double, _ rawT: Double) -> Double {
var t = rawT
if t < 0 { t += 1 }
if t > 1 { t -= 1 }
if t < 1.0 / 6.0 { return p + (q - p) * 6 * t }
if t < 1.0 / 2.0 { return q }
if t < 2.0 / 3.0 { return p + (q - p) * (2.0 / 3.0 - t) * 6 }
return p
}
}

View file

@ -0,0 +1,78 @@
import ManaCore
import SwiftUI
struct LoginView: View {
@Environment(AuthClient.self) private var auth
@State private var email = ""
@State private var password = ""
var body: some View {
ZStack {
CardsTheme.background.ignoresSafeArea()
VStack(spacing: 24) {
Text("Cards")
.font(.system(size: 48, weight: .bold))
.foregroundStyle(CardsTheme.primary)
Text("Karteikarten des Vereins mana e.V.")
.font(.subheadline)
.foregroundStyle(CardsTheme.mutedForeground)
VStack(spacing: 12) {
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.padding(.vertical, 12)
.padding(.horizontal, 16)
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 8))
SecureField("Passwort", text: $password)
.textContentType(.password)
.padding(.vertical, 12)
.padding(.horizontal, 16)
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 8))
}
.padding(.horizontal, 32)
Button {
Task { await auth.signIn(email: email, password: password) }
} label: {
HStack {
if case .signingIn = auth.status {
ProgressView()
.controlSize(.small)
.tint(CardsTheme.primaryForeground)
}
Text("Anmelden")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 8))
.foregroundStyle(CardsTheme.primaryForeground)
}
.padding(.horizontal, 32)
.disabled(isSigningIn || email.isEmpty || password.isEmpty)
if case let .error(message) = auth.status {
Text(message)
.font(.footnote)
.foregroundStyle(CardsTheme.error)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
}
}
}
}
private var isSigningIn: Bool {
if case .signingIn = auth.status { return true }
return false
}
}
#Preview {
LoginView()
.environment(AuthClient(config: AppConfig.manaAppConfig))
}

View file

@ -0,0 +1,58 @@
import ManaCore
import SwiftUI
/// Phase β-0-Placeholder. Wird in β-1 durch eine echte Tab-Bar mit
/// Decks / Study / Stats / Account ersetzt.
struct DashboardView: View {
@Environment(AuthClient.self) private var auth
@State private var apiReachable: Bool?
var body: some View {
ZStack {
CardsTheme.background.ignoresSafeArea()
VStack(spacing: 24) {
Text("Cards")
.font(.largeTitle.bold())
.foregroundStyle(CardsTheme.primary)
if let email = auth.currentEmail {
Text("Angemeldet als \(email)")
.font(.subheadline)
.foregroundStyle(CardsTheme.mutedForeground)
}
ContentUnavailableView {
Label("β-1 in Vorbereitung", systemImage: "rectangle.stack")
.foregroundStyle(CardsTheme.foreground)
} description: {
Text("Decks- und Study-Views kommen in der nächsten Phase.")
.foregroundStyle(CardsTheme.mutedForeground)
}
if let reachable = apiReachable {
Label(
reachable ? "cardecky-api erreichbar" : "cardecky-api nicht erreichbar",
systemImage: reachable ? "checkmark.circle.fill" : "xmark.circle.fill"
)
.foregroundStyle(reachable ? CardsTheme.success : CardsTheme.error)
.font(.footnote)
}
Button("Abmelden", role: .destructive) {
Task { await auth.signOut() }
}
.padding(.top, 24)
}
.padding(32)
}
.task {
let api = CardsAPI(auth: auth)
apiReachable = (try? await api.healthCheck()) ?? false
}
}
}
#Preview {
DashboardView()
.environment(AuthClient(config: AppConfig.manaAppConfig))
}

View file

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.231",
"green" : "0.502",
"red" : "0.106"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,85 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,5 @@
{
"sourceLanguage" : "de",
"strings" : { },
"version" : "1.0"
}

View file

@ -0,0 +1,9 @@
import XCTest
final class CardsNativeUITests: XCTestCase {
func testAppLaunches() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.staticTexts["Cards"].waitForExistence(timeout: 5))
}
}

View file

@ -0,0 +1,20 @@
import Testing
@testable import CardsNative
@Suite("AppConfig")
struct AppConfigTests {
@Test("Cards-API zeigt auf cardecky-api.mana.how")
func apiBaseURLPointsToCardecky() {
#expect(AppConfig.apiBaseURL.absoluteString == "https://cardecky-api.mana.how")
}
@Test("Auth zeigt auf auth.mana.how")
func authBaseURLPointsToManaAuth() {
#expect(AppConfig.manaAppConfig.authBaseURL.absoluteString == "https://auth.mana.how")
}
@Test("Keychain-Service ist ev.mana.cards")
func keychainServiceIsAppSpecific() {
#expect(AppConfig.manaAppConfig.keychainService == "ev.mana.cards")
}
}

97
project.yml Normal file
View file

@ -0,0 +1,97 @@
name: CardsNative
options:
bundleIdPrefix: ev.mana
createIntermediateGroups: true
deploymentTarget:
iOS: "18.0"
macOS: "15.0"
developmentLanguage: de
groupSortPosition: top
generateEmptyDirectories: true
packages:
ManaSwiftCore:
path: ../mana-swift-core
settings:
base:
SWIFT_VERSION: "6.0"
SWIFT_STRICT_CONCURRENCY: complete
CURRENT_PROJECT_VERSION: "1"
MARKETING_VERSION: "0.1.0"
GENERATE_INFOPLIST_FILE: "NO"
ENABLE_USER_SCRIPT_SANDBOXING: "YES"
DEAD_CODE_STRIPPING: "YES"
CLANG_ENABLE_MODULES: "YES"
targets:
CardsNative:
type: application
supportedDestinations: [iOS, macOS]
dependencies:
- package: ManaSwiftCore
product: ManaCore
- package: ManaSwiftCore
product: ManaTokens
sources:
- path: Sources/App
- path: Sources/Features
- path: Sources/Core
- path: Sources/Resources
excludes:
- "Info.plist"
- "CardsNative.entitlements"
info:
path: Sources/Resources/Info.plist
properties:
CFBundleShortVersionString: "0.1.0"
CFBundleVersion: "1"
CFBundleDevelopmentRegion: de
CFBundleDisplayName: Cards
LSApplicationCategoryType: "public.app-category.education"
UILaunchScreen: {}
CFBundleURLTypes:
- CFBundleURLName: ev.mana.cards
CFBundleURLSchemes:
- cards
ITSAppUsesNonExemptEncryption: false
entitlements:
path: Sources/Resources/CardsNative.entitlements
properties:
com.apple.security.app-sandbox: true
com.apple.security.network.client: true
com.apple.security.files.user-selected.read-write: true
keychain-access-groups:
- $(AppIdentifierPrefix)ev.mana.cards
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cards
CODE_SIGN_STYLE: Automatic
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
ENABLE_PREVIEWS: "YES"
CardsNativeTests:
type: bundle.unit-test
supportedDestinations: [iOS, macOS]
sources:
- Tests/UnitTests
dependencies:
- target: CardsNative
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cards.tests
GENERATE_INFOPLIST_FILE: "YES"
CardsNativeUITests:
type: bundle.ui-testing
supportedDestinations: [iOS, macOS]
sources:
- Tests/UITests
dependencies:
- target: CardsNative
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cards.uitests
GENERATE_INFOPLIST_FILE: "YES"