η-0: De-Hybrid — WKWebView raus, native Tabs mit Platzhaltern
Lift von Hybrid (WKWebView für Lesen/Erkunden) auf fully-native ist beschlossen. Diese Phase entfernt die WebShell-Infrastruktur; das volle native Read-Surface folgt in η-2..η-5 nach docs/NATIVE_LIFT_PLAN.md. - ManaWebShell-Dep raus aus project.yml - Sources/Core/WebShell/CookieBridge.swift gelöscht - RootView auf vier native Tabs (Lesen + Erkunden = Platzhalter, Submit + Konto unverändert nativ) - DocComments in DeepLinkRouter / AppConfig / Account / Settings von WebView-Verweisen befreit - CLAUDE.md Invarianten von Hybrid auf η umgestellt (13 Invarianten, pure SwiftUI + Offline-first + SafariView-Ausnahme für Legal) - PLAN.md auf η-0 + Phasenübersicht η-0..η-10 - AppConfigTests.test_keychainService_matchesSharedGroup auf ManaSharedKeychainGroup aktualisiert (war drift seit Cross-App-SSO) Verifikation: - xcodebuild iOS-Simulator iPhone 16e: BUILD SUCCEEDED - nm ZitareNative | grep WKWebView: 0 Referenzen - otool -L: kein WebKit-Framework-Link - 20/20 Tests grün Cross-Repo-Follow-up (η-1 Blocker): - zitare/apps/zitare/ muss index-full.json + 7 Stammdaten-JSONs liefern - zitare/apps/api/ Volltext-Search-Endpoint bestätigen/ergänzen Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
58eb2807c7
commit
1d770123f5
11 changed files with 619 additions and 365 deletions
|
|
@ -1,11 +1,16 @@
|
|||
import Foundation
|
||||
|
||||
/// Routet sowohl Custom-Scheme- (`zitare://`) als auch Universal-Link-URLs
|
||||
/// (`zitare.com/...`) auf eine konkrete `WebTarget` + Ziel-Tab.
|
||||
/// (`zitare.com/...`) auf eine normalisierte URL + Ziel-Tab.
|
||||
///
|
||||
/// **η-0 (de-Hybrid):** Die URL ist nicht mehr WebView-Target, sondern
|
||||
/// Eingabe für das native `NavigationPath`-Routing in η-2. Path-Struktur
|
||||
/// (`/q/<slug>`, `/a/<slug>`, `/c/<slug>`, `/thema/<slug>` etc.) bleibt
|
||||
/// 1:1 wie im Web-Repo — die Native-Views matchen die gleichen Slugs.
|
||||
///
|
||||
/// Pure-Logic, kein State — easy testbar.
|
||||
enum DeepLinkRouter {
|
||||
/// Mapt eine externe URL auf eine WebShell-URL.
|
||||
/// Normalisiert eine externe URL auf den kanonischen `https://`-Pfad.
|
||||
/// `zitare://quote/x` → `https://zitare.com/q/x`,
|
||||
/// `zitare://author/x` → `https://zitare.com/a/x`,
|
||||
/// `zitare://collection/x` → `https://zitare.com/c/x`.
|
||||
|
|
|
|||
|
|
@ -1,67 +1,29 @@
|
|||
import ManaAuthUI
|
||||
import ManaCore
|
||||
import ManaWebShell
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
private func makeWebShellConfig() -> WebShellConfig {
|
||||
WebShellConfig(
|
||||
allowedHosts: [
|
||||
"zitare.com",
|
||||
"www.zitare.com",
|
||||
"*.mana.how",
|
||||
],
|
||||
userAgent: AppConfig.userAgent,
|
||||
backgroundColor: ZitareTheme.background,
|
||||
progressTint: ZitareTheme.primary,
|
||||
errorBackgroundColor: ZitareTheme.muted,
|
||||
errorForegroundColor: ZitareTheme.foreground,
|
||||
errorIconColor: ZitareTheme.warning,
|
||||
userScripts: [
|
||||
// Syncs System-Dark-Mode in den WebView; zitare-web liest
|
||||
// `localStorage['zitare-mode']` beim First Paint und toggelt
|
||||
// dann `.dark` auf <html>.
|
||||
WebShellScripts.syncDarkMode(localStorageKey: "zitare-mode"),
|
||||
// Versteckt den zitare-web-Header (Brand-Logo + Nav), weil
|
||||
// die native TabBar bereits global navigiert.
|
||||
WebShellScripts.hideElements(
|
||||
selectors: [
|
||||
"header[data-app-nav]",
|
||||
"body header:has(a.brand)",
|
||||
"body > header:first-of-type",
|
||||
"body > div > header:first-of-type",
|
||||
],
|
||||
tagName: "hide-web-header"
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Top-Level-View: TabView mit drei Tabs.
|
||||
/// Top-Level-View: TabView mit vier nativen Tabs.
|
||||
///
|
||||
/// **Phase ζ-1:** Lesen + Erkunden laden `zitare.com` via `WebShellView`.
|
||||
/// Universal-Links auf `zitare.com/q/<slug>` / `/a/<slug>` etc. öffnen
|
||||
/// die App und routen in den passenden Tab.
|
||||
/// **η-0 (de-Hybrid 2026-05-22):** `WebShellView` ist raus. Lesen +
|
||||
/// Erkunden zeigen jetzt Platzhalter-Views; das volle native Read-
|
||||
/// Surface kommt in η-2 (Heute / Quote-Detail / Author-Detail) bzw.
|
||||
/// η-4 (Explore + Filter). Universal-Links werden hier vorerst nur
|
||||
/// geloggt und in den Lesen-Tab gepinnt, bis `NavigationStack`-Routing
|
||||
/// in η-2 steht. Submit + Konto sind schon nativ.
|
||||
struct RootView: View {
|
||||
@Environment(AuthClient.self) private var auth
|
||||
@Environment(ManaAuthGate.self) private var authGate
|
||||
@State private var selectedTab: AppTab = .read
|
||||
@State private var readTarget = WebTarget(url: AppConfig.webBaseURL)
|
||||
@State private var exploreTarget = WebTarget(
|
||||
url: AppConfig.webBaseURL.appendingPathComponent("explore")
|
||||
)
|
||||
@State private var reloadCounter: Int = 0
|
||||
@State private var pendingDeepLink: URL?
|
||||
@State private var healthStatus: HealthStatus = .unknown
|
||||
|
||||
private var webShellConfig: WebShellConfig { makeWebShellConfig() }
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
WebShellView(target: readTarget, config: webShellConfig)
|
||||
ReadPlaceholderView(pendingDeepLink: pendingDeepLink)
|
||||
.tabItem { Label("Lesen", systemImage: "book") }
|
||||
.tag(AppTab.read)
|
||||
|
||||
WebShellView(target: exploreTarget, config: webShellConfig)
|
||||
ExplorePlaceholderView()
|
||||
.tabItem { Label("Erkunden", systemImage: "sparkle.magnifyingglass") }
|
||||
.tag(AppTab.explore)
|
||||
|
||||
|
|
@ -73,35 +35,31 @@ struct RootView: View {
|
|||
.tabItem { Label("Konto", systemImage: "person.circle") }
|
||||
.tag(AppTab.account)
|
||||
}
|
||||
// Mac-Window-Hintergrund auf Paper-Theme setzen, damit der
|
||||
// TabBar-/Title-Bar-Bereich oben nicht mit dem System-Grau
|
||||
// gegen das Paper-Theme ausreißt. `windowToolbar`-Placement
|
||||
// ist macOS-only.
|
||||
.background(ZitareTheme.background.ignoresSafeArea())
|
||||
#if os(macOS)
|
||||
.toolbarBackground(ZitareTheme.background, for: .windowToolbar)
|
||||
.toolbarBackground(.visible, for: .windowToolbar)
|
||||
#endif
|
||||
.manaBrand(ZitareBrand.manaBrand)
|
||||
.manaAuthGate(authGate) {
|
||||
NavigationStack {
|
||||
ManaLoginView(
|
||||
auth: auth,
|
||||
onSignUpTapped: {},
|
||||
onForgotTapped: {}
|
||||
)
|
||||
.manaBrand(ZitareBrand.manaBrand)
|
||||
.manaBrand(ZitareBrand.manaBrand)
|
||||
.manaAuthGate(authGate) {
|
||||
NavigationStack {
|
||||
ManaLoginView(
|
||||
auth: auth,
|
||||
onSignUpTapped: {},
|
||||
onForgotTapped: {}
|
||||
)
|
||||
.manaBrand(ZitareBrand.manaBrand)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await probeHealth()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
handle(url: url)
|
||||
}
|
||||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
||||
if let url = activity.webpageURL { handle(url: url) }
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await probeHealth()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
handle(url: url)
|
||||
}
|
||||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
||||
if let url = activity.webpageURL { handle(url: url) }
|
||||
}
|
||||
}
|
||||
|
||||
private func probeHealth() async {
|
||||
|
|
@ -118,31 +76,18 @@ struct RootView: View {
|
|||
}
|
||||
}
|
||||
|
||||
/// Universal-Link- und Custom-URL-Routing. Wird sowohl von
|
||||
/// `onOpenURL` (Custom-Scheme `zitare://...`) als auch von
|
||||
/// `onContinueUserActivity` (Universal-Links auf `zitare.com/...`)
|
||||
/// aufgerufen.
|
||||
/// Deep-Link- + Universal-Link-Routing.
|
||||
///
|
||||
/// Routing-Regeln (gespiegelt zu `app-manifest.json#link_patterns`):
|
||||
/// - `/q/<slug>`, `/a/<slug>`, `/c/<slug>` → Lesen-Tab
|
||||
/// - `/heute`, `/random`, `/feed.rss` → Lesen-Tab
|
||||
/// - `/explore`, `/region/...`, `/thema/...`, `/rolle/...`,
|
||||
/// `/epoche/...`, `/sprache/...`, `/search`, `/t/...` → Erkunden-Tab
|
||||
/// - alles andere unter `zitare.com` → Lesen-Tab, Root-Pfad
|
||||
///
|
||||
/// Custom-Scheme `zitare://quote/<slug>` wird auf
|
||||
/// `https://zitare.com/q/<slug>` umgemappt.
|
||||
/// η-0: Routing-Logik (URL-Normalisierung + Tab-Auswahl) bleibt
|
||||
/// erhalten, das eigentliche Ansteuern einer Quote/Author/Source-
|
||||
/// Detail-View kommt in η-2 mit `NavigationPath`. Solange parken
|
||||
/// wir die URL in `pendingDeepLink`, die Placeholder-View zeigt
|
||||
/// sie zur Diagnose an.
|
||||
private func handle(url: URL) {
|
||||
Log.app.info("Deep-Link empfangen: \(url.absoluteString, privacy: .public)")
|
||||
let routed = DeepLinkRouter.route(url, base: AppConfig.webBaseURL)
|
||||
reloadCounter += 1
|
||||
if routed.isExplore {
|
||||
exploreTarget = WebTarget(url: routed.url, reloadToken: reloadCounter)
|
||||
selectedTab = .explore
|
||||
} else {
|
||||
readTarget = WebTarget(url: routed.url, reloadToken: reloadCounter)
|
||||
selectedTab = .read
|
||||
}
|
||||
pendingDeepLink = routed.url
|
||||
selectedTab = routed.isExplore ? .explore : .read
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,3 +103,55 @@ enum HealthStatus {
|
|||
case ok
|
||||
case down
|
||||
}
|
||||
|
||||
/// η-0 Platzhalter — wird in η-2 durch HeuteView + QuoteFeedView ersetzt.
|
||||
private struct ReadPlaceholderView: View {
|
||||
let pendingDeepLink: URL?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "book")
|
||||
.font(.system(size: 56))
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
Text("Lesen")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
Text("Native Read-Surface kommt in η-2 (Heute, Quote-Detail, Author-Detail).")
|
||||
.font(.callout)
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
if let url = pendingDeepLink {
|
||||
Text("Deep-Link wartet auf η-2:\n\(url.absoluteString)")
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(ZitareTheme.background)
|
||||
}
|
||||
}
|
||||
|
||||
/// η-0 Platzhalter — wird in η-4 durch ExploreView (Facet-Filter) ersetzt.
|
||||
private struct ExplorePlaceholderView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "sparkle.magnifyingglass")
|
||||
.font(.system(size: 56))
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
Text("Erkunden")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
Text("Filter (Sprache, Länge, Thema, Region, Epoche, Rolle) + lokale Suche kommen in η-4 und η-5.")
|
||||
.font(.callout)
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(ZitareTheme.background)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import ManaCore
|
|||
/// App-spezifische Konfiguration für Zitare. Implementiert
|
||||
/// `ManaAppConfig` aus ManaCore und ergänzt die Zitare-eigene
|
||||
/// `apiBaseURL` (api.zitare.com, getrennt von mana-auth) sowie
|
||||
/// `webBaseURL` (zitare.com, für WKWebView und Universal-Links)
|
||||
/// und `appBaseURL` (app.zitare.com, für eingeloggte Pfade).
|
||||
/// `webBaseURL` (zitare.com, für Universal-Links + Snapshot-Pull +
|
||||
/// SafariView auf Legal-Seiten) und `appBaseURL` (app.zitare.com,
|
||||
/// historisch SPA-Surface — η-0+ nicht mehr vom Native-Client geladen).
|
||||
///
|
||||
/// Cutover zu .zitare.com-Subdomains am 2026-05-20. zitare.mana.how
|
||||
/// ist abgeschaltet; zitare-api.mana.how bleibt als Back-Compat-
|
||||
|
|
@ -36,18 +37,19 @@ enum AppConfig {
|
|||
/// Edit, Admin, Me).
|
||||
static let appBaseURL = URL(string: "https://app.zitare.com")!
|
||||
|
||||
/// Default-URL für den WebView (öffentliches Lese-Surface).
|
||||
/// Default-Web-URL (öffentliches Lese-Surface, Snapshot, AASA).
|
||||
/// η-0+: kein WebView mehr, aber bleibt Universal-Link-Domain und
|
||||
/// SafariView-Ziel für Legal-Seiten.
|
||||
static let webBaseURL = publicWebURL
|
||||
|
||||
/// Endpoint für den Korpus-Snapshot (Phase ζ-2). Heute noch nicht
|
||||
/// als statische HTTP-Datei publiziert — Aufgabe im Web-Repo:
|
||||
/// `apps/zitare/static/index-min.json` aus dem Snapshot-Job
|
||||
/// zusätzlich rauskopieren. Bis dahin schlägt der Pull mit 404
|
||||
/// fehl und `SnapshotSync.tryRefresh()` macht fail-soft no-op.
|
||||
/// Endpoint für den Korpus-Snapshot (heute Phase ζ-2: `index-min.json`,
|
||||
/// in η-1 ersetzt durch `index-full.json` + Stammdaten-Collections,
|
||||
/// siehe `docs/NATIVE_LIFT_PLAN.md`). Bis das im Web-Repo gebaut ist,
|
||||
/// schlägt der Pull mit 404 fehl und `SnapshotSync.tryRefresh()` macht
|
||||
/// fail-soft no-op.
|
||||
static let snapshotURL = webBaseURL.appendingPathComponent("index-min.json")
|
||||
|
||||
/// User-Agent-Suffix für WKWebView (ManaWebShell). WKWebView hängt
|
||||
/// das an seinen Standard-UA an, ersetzt ihn nicht.
|
||||
// User-Agent-Suffix für URLSession-API-Calls.
|
||||
#if os(macOS)
|
||||
static let userAgent = "ZitareNative/0.1 (macOS)"
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
import WebKit
|
||||
|
||||
/// Reicht den mana-auth-JWT als Cookie an den `WKWebView` weiter, sodass
|
||||
/// eingeloggte `(app)`-Routen auf `app.zitare.com` ohne zweiten Login
|
||||
/// erreichbar sind.
|
||||
///
|
||||
/// **Phase ζ-1: Skeleton.** Methoden existieren, werden aber heute
|
||||
/// nicht aufgerufen — bevor sie scharfgeschaltet werden, muss der
|
||||
/// Cookie-SSO-Pfad auf der Web-Seite (`zitare/apps/api/src/auth/`
|
||||
/// und `apps/zitare/src/lib/auth/token-helper.ts`) gegen einen
|
||||
/// *echten* mana-auth-Token End-to-End getestet sein (Verifikations-
|
||||
/// Lücke in `zitare/STATUS.md`).
|
||||
///
|
||||
/// **Cross-Domain-Flow (Cutover 2026-05-20):** Der WKWebView lädt
|
||||
/// `app.zitare.com` (Brand-Domain). Die SvelteKit-App ruft beim
|
||||
/// Boot Cross-Origin `POST auth.mana.how/api/v1/auth/refresh` mit
|
||||
/// `credentials: 'include'` und erwartet einen Refresh-Cookie auf
|
||||
/// `.mana.how`. Der Cookie-Domain-Wert hier (`.mana.how`) ist
|
||||
/// genau richtig — der Browser sendet ihn an das XHR-Ziel
|
||||
/// (auth.mana.how), unabhängig vom Source-Page-Host. Identisches
|
||||
/// Pattern wie im Web-Client (`apps/zitare/src/lib/auth.ts`).
|
||||
///
|
||||
/// **Cookie-Schema** (gespiegelt zu mana-auth `better-auth.config.ts`):
|
||||
/// - Name: `mana.access` (JWT) und optional `mana.refresh` (Opaque)
|
||||
/// - Domain: `.mana.how` (Cookie wird an auth.mana.how-XHR mitgesendet)
|
||||
/// - Path: `/`
|
||||
/// - Secure: true, HTTPOnly: false (WebView muss lesen können)
|
||||
/// - SameSite: **None** — mana-auth setzt für Cross-Subdomain-SSO
|
||||
/// `sameSite: 'none'` (better-auth.config.ts), ohne das wird der
|
||||
/// Cookie bei Cross-Origin-POST nicht mitgesendet. Foundation
|
||||
/// `HTTPCookieStringPolicy` hat dafür keinen Konstanten-Wert →
|
||||
/// `cookieAttributesNone` als Roh-String über die initWithProperties-
|
||||
/// `String`-Variante.
|
||||
enum CookieBridge {
|
||||
/// Setzt den `mana.access`-Cookie im geteilten `WKHTTPCookieStore`,
|
||||
/// wenn der `AuthClient` einen gültigen JWT hält. No-op sonst.
|
||||
@MainActor
|
||||
static func installManaAccess(from auth: AuthClient) async {
|
||||
guard case .signedIn = auth.status, let token = currentAccessToken(from: auth) else {
|
||||
Log.web.debug("CookieBridge: kein signedIn-Token, no-op")
|
||||
return
|
||||
}
|
||||
guard let cookie = makeAccessCookie(token: token) else {
|
||||
Log.web.warning("CookieBridge: konnte Cookie-Properties nicht bauen")
|
||||
return
|
||||
}
|
||||
let store = WKWebsiteDataStore.default().httpCookieStore
|
||||
await store.setCookie(cookie)
|
||||
Log.web.info("CookieBridge: mana.access für .mana.how gesetzt")
|
||||
}
|
||||
|
||||
/// Entfernt den `mana.access`-Cookie wieder — etwa nach Logout.
|
||||
@MainActor
|
||||
static func removeManaAccess() async {
|
||||
let store = WKWebsiteDataStore.default().httpCookieStore
|
||||
let cookies = await store.allCookies()
|
||||
for cookie in cookies where cookie.name == "mana.access" {
|
||||
await store.deleteCookie(cookie)
|
||||
}
|
||||
Log.web.info("CookieBridge: mana.access entfernt")
|
||||
}
|
||||
|
||||
private static func currentAccessToken(from auth: AuthClient) -> String? {
|
||||
// ManaCore hält den JWT im Keychain. In ζ-3 ersetzt durch die
|
||||
// tatsächliche `auth.currentAccessToken()`-API; heute nur
|
||||
// Skelett-Hook, damit Cookie-Setup und API in Reichweite sind.
|
||||
// Linter beruhigen ohne unused warning:
|
||||
_ = auth
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func makeAccessCookie(token: String) -> HTTPCookie? {
|
||||
// SameSite=None: Foundation hat keinen Konstanten-Wert für
|
||||
// "None", aber HTTPCookie akzeptiert beliebige Strings für
|
||||
// .sameSitePolicy. Cross-Origin-POST von app.zitare.com →
|
||||
// auth.mana.how braucht None, sonst kein Cookie-Versand.
|
||||
HTTPCookie(properties: [
|
||||
.name: "mana.access",
|
||||
.value: token,
|
||||
.domain: ".mana.how",
|
||||
.path: "/",
|
||||
.secure: true,
|
||||
.sameSitePolicy: "None"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@ import ManaCore
|
|||
import SwiftUI
|
||||
|
||||
/// Phase ζ-0 minimal: zeigt Auth-Status und Healthz-Probe-Ergebnis.
|
||||
/// Phase ζ-3 erweitert um Submission-History-Link (via WebShell auf
|
||||
/// `zitare.mana.how/me`). Login-Sheet schon hier, damit Guests einen
|
||||
/// Anmelden-Button finden.
|
||||
/// Phase η-6 erweitert um native Submission-History (eigene
|
||||
/// Submissions-Liste via `GET /api/v1/me/submissions`) + Link auf
|
||||
/// Web-Konto-Verwaltung via SafariView. Login-Sheet schon hier, damit
|
||||
/// Guests einen Anmelden-Button finden.
|
||||
struct AccountView: View {
|
||||
@Environment(AuthClient.self) private var auth
|
||||
@Environment(ManaAuthGate.self) private var authGate
|
||||
|
|
@ -185,13 +186,12 @@ struct AccountView: View {
|
|||
|
||||
private var aboutCard: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Phase ζ-0 — Setup")
|
||||
Text("Phase η-0 — De-Hybrid")
|
||||
.font(.caption)
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
Text(
|
||||
"Diese App ist noch im Aufbau. Web-App live auf "
|
||||
+ "zitare.com und zitare.mana.how. "
|
||||
+ "Plan in mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md."
|
||||
"Diese App wird nativ ausgebaut. Web-App weiter live auf "
|
||||
+ "zitare.com. Plan in zitare-native/docs/NATIVE_LIFT_PLAN.md."
|
||||
)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(ZitareTheme.foreground)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import SwiftUI
|
||||
|
||||
/// Phase ζ-5 Placeholder.
|
||||
/// Phase η-9 Placeholder.
|
||||
///
|
||||
/// Aufgabenliste in ζ-5:
|
||||
/// Aufgabenliste in η-9:
|
||||
///
|
||||
/// - Theme-Toggle (System / Light / Dark) — propagiert per
|
||||
/// `localStorage['zitare-mode']` an den WebView.
|
||||
/// - Reader-Schriftgröße (S/M/L/XL) — per JS-Bridge an die Web-CSS-
|
||||
/// Variable `--zit-reader-size`.
|
||||
/// - DSGVO-Daten-Export (öffnet `zitare.mana.how/me` Export-Page im
|
||||
/// WebView).
|
||||
/// - About / Impressum / Lizenz (CC-BY-SA-4.0).
|
||||
/// - Theme-Toggle (System / Light / Dark) — direkt nativ via
|
||||
/// `@AppStorage("zitare-mode")` → `ZitareTheme.applyMode`.
|
||||
/// - Reader-Schriftgröße (S/M/L/XL) — `@AppStorage("zitare-reader-size")`,
|
||||
/// propagiert an `QuoteDetailView` (η-2).
|
||||
/// - DSGVO-Daten-Export — SafariView auf `https://app.zitare.com/me/export`.
|
||||
/// - About / Impressum / Lizenz (CC-BY-SA-4.0) — SafariView-Sheets (η-7).
|
||||
struct SettingsView: View {
|
||||
var body: some View {
|
||||
Text("Einstellungen — ζ-5 TODO")
|
||||
Text("Einstellungen — η-9 TODO")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue