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
+15 -29
View File
@@ -6,16 +6,14 @@ import UIKit
struct ContentComposerView: View {
@EnvironmentObject private var conversationUIModel: ConversationUIModel
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
@Environment(\.colorScheme) private var colorScheme
@Environment(\.appTheme) private var theme
@ThemedPalette private var palette
@Binding var messageText: String
var isTextFieldFocused: FocusState<Bool>.Binding
@ObservedObject var voiceRecordingVM: VoiceRecordingViewModel
@Binding var autocompleteDebounceTimer: Timer?
let backgroundColor: Color
let textColor: Color
let secondaryTextColor: Color
let onSendMessage: () -> Void
#if os(iOS)
@@ -35,8 +33,8 @@ struct ContentComposerView: View {
}) {
HStack {
Text(suggestion)
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 11)
.foregroundColor(palette.primary)
.fontWeight(.medium)
Spacer()
}
@@ -48,20 +46,11 @@ struct ContentComposerView: View {
.background(Color.gray.opacity(0.1))
}
}
.background(backgroundColor)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(secondaryTextColor.opacity(0.3), lineWidth: 1)
)
.themedOverlayPanel()
.padding(.horizontal, 12)
}
CommandSuggestionsView(
messageText: $messageText,
textColor: textColor,
backgroundColor: backgroundColor,
secondaryTextColor: secondaryTextColor
)
CommandSuggestionsView(messageText: $messageText)
if voiceRecordingVM.state.isActive {
recordingIndicator
@@ -74,11 +63,11 @@ struct ContentComposerView: View {
prompt: Text(
String(localized: "content.input.message_placeholder", comment: "Placeholder shown in the chat composer")
)
.foregroundColor(secondaryTextColor.opacity(0.6))
.foregroundColor(palette.secondary.opacity(0.6))
)
.textFieldStyle(.plain)
.font(.bitchatSystem(size: 15, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 15)
.foregroundColor(palette.primary)
.focused(isTextFieldFocused)
.autocorrectionDisabled(true)
#if os(iOS)
@@ -86,12 +75,9 @@ struct ContentComposerView: View {
#endif
.submitLabel(.send)
.onSubmit(onSendMessage)
.padding(.vertical, 4)
.padding(.vertical, theme.usesGlassChrome ? 8 : 4)
.padding(.horizontal, 6)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(colorScheme == .dark ? Color.black.opacity(0.35) : Color.white.opacity(0.7))
)
.themedInputBackground()
.modifier(FocusEffectDisabledModifier())
.frame(maxWidth: .infinity, alignment: .leading)
.onChange(of: messageText) { newValue in
@@ -114,9 +100,9 @@ struct ContentComposerView: View {
}
}
.padding(.horizontal, 6)
.padding(.top, 6)
.padding(.top, theme.usesGlassChrome ? 8 : 6)
.padding(.bottom, 8)
.background(backgroundColor.opacity(0.95))
.themedChromePanel(edge: .bottom)
.onDisappear {
autocompleteDebounceTimer?.invalidate()
}
@@ -134,7 +120,7 @@ private extension ContentComposerView {
"recording \(voiceRecordingVM.formattedDuration(for: context.date))",
comment: "Voice note recording duration indicator"
)
.font(.bitchatSystem(size: 13, design: .monospaced))
.bitchatFont(size: 13)
.foregroundColor(.red)
}
Spacer()
@@ -154,7 +140,7 @@ private extension ContentComposerView {
}
var composerAccentColor: Color {
privateConversationModel.selectedPeerID != nil ? Color.orange : textColor
privateConversationModel.selectedPeerID != nil ? Color.orange : palette.accent
}
var attachmentButton: some View {