Liquid Glass theme with in-app appearance switcher (#1337)

* Centralize UI theme colors into semantic ThemePalette tokens

Introduce AppTheme/ThemePalette (Utils/Theme.swift) with an environment
key and @ThemedPalette property wrapper, persisted via @AppStorage.
Views now resolve background/primary/secondary/accentBlue/alertRed/
divider tokens from the environment instead of computing colors inline,
removing the backgroundColor/textColor/secondaryTextColor prop-drilling
through the header, composer, and people-sheet hierarchy.

Matrix theme output is pixel-identical; this is groundwork for a
user-selectable theme switcher.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add liquid glass theme with in-app appearance switcher

Add a .liquidGlass AppTheme alongside the matrix terminal theme,
selectable from a one-line APPEARANCE row in the app info sheet
(persisted via @AppStorage, applies live).

Liquid glass renders system fonts and colors over a subtle gradient
backdrop, with the header and composer floating as Liquid Glass panels
(real .glassEffect() on iOS/macOS 26, compiler-gated with an
ultraThinMaterial fallback for older SDKs). The message list scrolls
underneath the chrome via safe-area insets, and all sheets share the
same backdrop and surface language. The matrix theme is unchanged.

Details:
- Theme is threaded through ChatMessageFormatter so message
  AttributedStrings switch font design per theme; the per-message
  format cache gains a variant key so themes never serve each other's
  cached strings
- New palette tokens: accent (interactive tint) and locationAccent
  (geohash green), replacing scattered hardcoded greens/blues in the
  voice note, waveform, verification, and people-sheet views
- Header controls get full-height tap targets and the people count
  becomes a real Button (previously a tap gesture on the cluster)
- CommandSuggestionsView renders nothing when empty instead of a
  zero-height view that pushed the composer input off-center
- New appearance strings localized for all 29 catalog languages

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-06-12 01:13:04 +02:00
committed by GitHub
co-authored by jack Claude Fable 5
parent c8381737bb
commit af954b05ea
27 changed files with 1347 additions and 498 deletions
+13 -13
View File
@@ -21,7 +21,7 @@ struct MyQRView: View {
var body: some View {
VStack(spacing: 12) {
Text(Strings.title)
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.bitchatFont(size: 16, weight: .bold)
VStack(spacing: 10) {
QRCodeImage(data: qrString, size: 240)
@@ -29,7 +29,7 @@ struct MyQRView: View {
// Non-scrolling, fully visible URL (wraps across lines)
Text(qrString)
.font(.bitchatSystem(size: 11, design: .monospaced))
.bitchatFont(size: 11)
.textSelection(.enabled)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
@@ -69,7 +69,7 @@ struct QRCodeImage: View {
.frame(width: size, height: size)
.overlay(
Text(Strings.unavailable)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.gray)
)
}
@@ -150,7 +150,7 @@ struct QRScanView: View {
.clipShape(RoundedRectangle(cornerRadius: 8))
#else
Text(Strings.pastePrompt)
.font(.bitchatSystem(size: 14, weight: .medium, design: .monospaced))
.bitchatFont(size: 14, weight: .medium)
TextEditor(text: $input)
.frame(height: 100)
.border(Color.gray.opacity(0.4))
@@ -281,10 +281,10 @@ struct VerificationSheetView: View {
@EnvironmentObject private var verificationModel: VerificationModel
@Binding var isPresented: Bool
@State private var showingScanner = false
@Environment(\.colorScheme) var colorScheme
@ThemedPalette private var palette
private var backgroundColor: Color { colorScheme == .dark ? Color.black : Color.white }
private var accentColor: Color { colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) }
private var backgroundColor: Color { palette.background }
private var accentColor: Color { palette.accent }
private var boxColor: Color { Color.gray.opacity(0.1) }
var body: some View {
@@ -292,7 +292,7 @@ struct VerificationSheetView: View {
// Top header (always at top)
HStack {
Text("verification.sheet.title")
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.bitchatFont(size: 14, weight: .bold)
.foregroundColor(accentColor)
Spacer()
Button(action: {
@@ -316,7 +316,7 @@ struct VerificationSheetView: View {
if showingScanner {
VStack(alignment: .leading, spacing: 12) {
Text("verification.scan.prompt_friend")
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.bitchatFont(size: 16, weight: .bold)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
.foregroundColor(accentColor)
@@ -350,13 +350,13 @@ struct VerificationSheetView: View {
if showingScanner {
Button(action: { showingScanner = false }) {
Label("show my qr", systemImage: "qrcode")
.font(.bitchatSystem(size: 13, design: .monospaced))
.bitchatFont(size: 13)
}
.buttonStyle(.bordered)
} else {
Button(action: { showingScanner = true }) {
Label("scan someone else's qr", systemImage: "camera.viewfinder")
.font(.bitchatSystem(size: 13, weight: .medium, design: .monospaced))
.bitchatFont(size: 13, weight: .medium)
}
.buttonStyle(.bordered)
.tint(.gray)
@@ -367,7 +367,7 @@ struct VerificationSheetView: View {
verificationModel.isVerified(peerID: peerID) {
Button(action: { verificationModel.unverifyFingerprint(for: peerID) }) {
Label("remove verification", systemImage: "minus.circle")
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
}
.buttonStyle(.bordered)
.tint(.gray)
@@ -376,7 +376,7 @@ struct VerificationSheetView: View {
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.background(backgroundColor)
.themedSheetBackground()
.onDisappear { showingScanner = false }
}
}