Files
simplex-chat/apps/ios/spec/services/theme.md
T
Evgeny @ SimpleX ChatandEvgeny Poberezkin c3c733e0d4 ios, ui, ci: terminal chat layout in the iOS app (#7898)
* plan: UI improvements to improve LLM usage via SimpleX

* core, ui: types for chat layout

* core, ui: settings for alternative terminal-like chat layout for managing LLMs or other service bots

* plan: message priority

* fixes

* query plan

* update plans

* remove silent messages plan

* rename migrations

* implement layout

* amend

* fix

* fix

* fix

* improve

* time on the right, sender name in blue

* no avatar in direct chats

* better previews

* update, fixes

* update

* time gap

* changes

* ui: terminal layout line heights, gaps and time placement

* ui: simplify terminal meta layout, correct its docs and tests

* ui: right-to-left emoji edge in terminal rows, time placement tests

* android, desktop: raise Kotlin compiler memory to 4 GB

* ui: terminal captions as follow-up text, classic event separators

* ui: terminal media edge and overlay time, plan decisions

* ui: simplify terminal row separation, correct doc line references

* ui: keep the media time inside terminal rows, restore caption conditions

* ui: terminal heading capitals at large font scales, idle live message time

* ui: rename the classic voice-with-text flag, correct the meta copy docs

* ui: smaller terminal layout diff, sender gap and meta names

* ui: terminal row as tall as the media time

* docs: remove the chat layout section from the bots readme

* docs: shorten the terminal layout section of the chat view

* core: address settings API as on master, /auto_accept reads the stored layout

* ui: smaller terminal headings

* ui: classic sender rows and avatar rule as on master

* ui: classic reply quote text colour as on master

* ios: keep the chat layout setting with other UI settings

* ui: go-to button at the end of terminal rows, no gutter in direct chats

* ui: forwarded arrow on the header line of terminal rows, above the time

* ui: forwarded arrow centred above the time line of terminal rows

* ios: terminal chat layout, chosen per app, profile and chat

* ios: event time reserve of three spaces, app layout picker as Kotlin

* ios: spec and plan of the terminal chat layout

* ios: classic message text at 87% in dark themes, as terminal rows

* ios: terminal gaps scaled by the body size, 17/16 of Kotlin values

* ios: terminal quote sender at 13.5pt, thumbnail at the item end

* ios: product docs of the terminal layout, plans updated for iOS

* ios: chat scenes rendered to PNG by a unit test in the simulator CI

* ios: snapshots rendered by the smoke test script, workflow unchanged

* ios: terminal names at 13.5 x 17/16 pt, as the gaps

* ios: Markdown views at the 87% message colour in dark themes

* ios: terminal quote thumbnail 12pt after the quote text or the bar

* ios: terminal rule-only message widened by a wider quote or header

* ios: member avatars scaled with the text size, as in the chat list

* ios: snapshot rows at their own height, as list cells, so text wraps

* ios: member avatars back at 34pt, for the snapshot before scaling

* ios: member avatars scaled with the text size, as in the chat list

* ui: terminal quote thumbnail 12dp after the quote text or the bar

* ios: classic group bubble width follows the scaled member avatar

* ci: ios-sim README describes the snapshots rendered after the smoke test

* ios: scratch, padding variants for the I7 and I8 renders, to revert

* ios: revert the scratch padding variants after their renders

* ui: contrast of the voice message button in terminal rows

* ios: scratch, spacing variants and list margins to render, to revert

* ios: revert the scratch spacing variants after their renders

* ios: snapshots at a fixed date, in UTC and the en_US locale

* ios: snapshots drawn as 8-bit sRGB without alpha

* ci: compare iOS snapshots with the references in the repository

* ci: record and check iOS snapshot references on GitHub's runner

* ios: scratch, list margins on the remaining screens, to revert

* ios: revert the scratch list margins after their measurement

* ci: snapshot scripts executable, as the other CI scripts

* ios: scratch, voice circle variants in light themes, to revert

* ios: revert the scratch voice circle variants

* ios: one 12pt padding at the edges and after avatars in terminal rows

* ios: classic group sender name on the bubble edge

* ui: one 12dp padding at the edges and after avatars in terminal rows

* ios: one heading scale in both chat layouts

* ios: snapshot references of the chat scenes

* docs: leave the headings and classic lines of the plan to ep/classic-bubbles

* ios: declare terminal layout functions inside types

* ios: keep layout constants at file scope

* ios: keep file-scope functions, move only extension members into types

* ios: snapshot tests record row heights at first layout and the appearance layout switch

* ios: appearance preview follows the chat layout setting at once

* ios: snapshot test measures first-layout rows by SwiftUI and by cells

* ios: chat rows measured at the chat width when they first appear

* ios: snapshot test checks that chat rows keep their first height after a reload

* ios, ci: references of terminal and classic chat scenes, dark message text

* ios: message text at 95% in dark themes

* ios, ci: references with message text at 95% in dark themes

* ios: media in terminal rows end at the time's right inset

* ios, ci: references with terminal media at the time's right inset

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2026-10-29 12:45:34 +00:00

412 lines
19 KiB
Markdown

# SimpleX Chat iOS -- Theme Engine
> Technical specification for the theming system: ThemeManager, default themes, customization layers, wallpapers, and YAML export.
>
> Related specs: [State Management](../state.md) | [Architecture](../architecture.md) | [README](../README.md)
> Related product: [Product Overview](../../product/README.md)
**Source:** [`ThemeManager.swift`](../../Shared/Theme/ThemeManager.swift) | [`AppearanceSettings.swift`](../../Shared/Views/UserSettings/AppearanceSettings.swift) | [`ThemeTypes.swift`](../../SimpleXChat/Theme/ThemeTypes.swift) | [`ChatWallpaperTypes.swift`](../../SimpleXChat/Theme/ChatWallpaperTypes.swift) | [`Theme.swift`](../../Shared/Theme/Theme.swift)
---
## Table of Contents
1. [Overview](#1-overview)
2. [ThemeManager](#2-thememanager)
3. [Default Themes](#3-default-themes)
4. [Customization Layers](#4-customization-layers)
5. [Color System](#5-color-system)
6. [Wallpapers](#6-wallpapers)
7. [Chat Bubble Styling](#7-chat-bubble-styling)
8. [Color Scheme Mode](#8-color-scheme-mode)
9. [YAML Export/Import](#9-yaml-exportimport)
---
## 1. Overview
The theme engine provides a layered customization system where themes can be overridden at multiple levels: global defaults, per-user, and per-chat.
```
Theme Resolution Order (most specific wins):
┌─────────────────────┐
│ Per-chat override │ apiSetChatUIThemes(chatId:, themes:)
├─────────────────────┤
│ Per-user override │ apiSetUserUIThemes(userId:, themes:)
├─────────────────────┤
│ App settings theme │ themeOverridesDefault (UserDefaults)
├─────────────────────┤
│ Base theme │ Light / Dark / SimpleX / Black
└─────────────────────┘
```
The resolved theme is published as `AppTheme.shared` and consumed by all SwiftUI views via `@EnvironmentObject`.
---
## 2. [ThemeManager](../../Shared/Theme/ThemeManager.swift) (L15)
**File**: [`Shared/Theme/ThemeManager.swift`](../../Shared/Theme/ThemeManager.swift)
Static utility class that resolves the current theme by merging all customization layers.
### [ActiveTheme](../../Shared/Theme/ThemeManager.swift#L17)
The resolved theme output:
```swift
struct ActiveTheme: Equatable {
let name: String // Theme name (e.g., "light", "dark", "simplex", "black", "system")
let base: DefaultTheme // Base theme enum
let colors: Colors // Resolved color palette
let appColors: AppColors // App-specific colors (sent/received bubbles, etc.)
var wallpaper: AppWallpaper // Resolved wallpaper
}
```
### Key Static Methods
| Method | Purpose | Line |
|--------|---------|------|
| [`applyTheme(_:)`](../../Shared/Theme/ThemeManager.swift#L124) | Apply a theme by name, updates `AppTheme.shared` | [L124](../../Shared/Theme/ThemeManager.swift#L124) |
| [`currentColors(...)`](../../Shared/Theme/ThemeManager.swift#L64) | Resolve full theme from all layers | [L64](../../Shared/Theme/ThemeManager.swift#L64) |
| [`defaultActiveTheme(_:)`](../../Shared/Theme/ThemeManager.swift#L48) | Get default theme override from app settings | [L48](../../Shared/Theme/ThemeManager.swift#L48) |
| [`currentThemeOverridesForExport(...)`](../../Shared/Theme/ThemeManager.swift#L105) | Get current overrides for YAML export | [L105](../../Shared/Theme/ThemeManager.swift#L105) |
| [`adjustWindowStyle()`](../../Shared/Theme/ThemeManager.swift#L136) | Adjust window style after theme change | [L136](../../Shared/Theme/ThemeManager.swift#L136) |
| [`changeDarkTheme(_:)`](../../Shared/Theme/ThemeManager.swift#L166) | Change the dark theme variant | [L166](../../Shared/Theme/ThemeManager.swift#L166) |
| [`saveAndApplyThemeColor(...)`](../../Shared/Theme/ThemeManager.swift#L173) | Save and apply a theme color override | [L173](../../Shared/Theme/ThemeManager.swift#L173) |
| [`applyThemeColor(...)`](../../Shared/Theme/ThemeManager.swift#L186) | Apply a theme color to a binding | [L186](../../Shared/Theme/ThemeManager.swift#L186) |
| [`saveAndApplyWallpaper(...)`](../../Shared/Theme/ThemeManager.swift#L191) | Save and apply a wallpaper change | [L191](../../Shared/Theme/ThemeManager.swift#L191) |
| [`copyFromSameThemeOverrides(...)`](../../Shared/Theme/ThemeManager.swift#L213) | Copy overrides from matching theme | [L213](../../Shared/Theme/ThemeManager.swift#L213) |
| [`applyWallpaper(...)`](../../Shared/Theme/ThemeManager.swift#L256) | Apply wallpaper to a binding | [L256](../../Shared/Theme/ThemeManager.swift#L256) |
| [`saveAndApplyThemeOverrides(...)`](../../Shared/Theme/ThemeManager.swift#L267) | Save and apply full theme overrides | [L267](../../Shared/Theme/ThemeManager.swift#L267) |
| [`resetAllThemeColors(_:)`](../../Shared/Theme/ThemeManager.swift#L288) | Reset all color overrides (CodableDefault) | [L288](../../Shared/Theme/ThemeManager.swift#L288) |
| [`resetAllThemeColors(_:)`](../../Shared/Theme/ThemeManager.swift#L302) | Reset all color overrides (Binding) | [L302](../../Shared/Theme/ThemeManager.swift#L302) |
| [`removeTheme(_:)`](../../Shared/Theme/ThemeManager.swift#L311) | Remove a saved theme by ID | [L311](../../Shared/Theme/ThemeManager.swift#L311) |
### Theme Resolution Algorithm
[`currentColors()`](../../Shared/Theme/ThemeManager.swift#L64) in `ThemeManager.swift`:
1. Determine base theme from `currentThemeDefault`:
- If `"system"`: use light or dark based on [`systemInDarkThemeCurrently`](../../Shared/Theme/Theme.swift#L95)
- Dark mode maps to `systemDarkThemeDefault` (Dark, SimpleX, or Black)
2. Get base color palette ([`LightColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L656), [`DarkColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L635), [`SimplexColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L677), [`BlackColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L698))
3. Look up app settings theme override (`themeOverridesDefault`)
4. Look up per-user theme override (`User.uiThemes`)
5. Look up per-chat theme override (from ChatInfo)
6. Look up wallpaper preset colors (if wallpaper has preset color overrides)
7. Merge layers: base <- app override <- preset wallpaper colors <- per-user <- per-chat
8. Return `ActiveTheme` with resolved colors, app colors, and wallpaper
---
## 3. Default Themes
Four built-in themes with pre-defined color palettes:
| Theme | Enum | Key Characteristics |
|-------|------|---------------------|
| **Light** | `DefaultTheme.LIGHT` | White background, standard colors |
| **Dark** | `DefaultTheme.DARK` | Dark gray background, light text |
| **SimpleX** | `DefaultTheme.SIMPLEX` | Brand purple accents, dark background |
| **Black** | `DefaultTheme.BLACK` | Pure black background (OLED), high contrast |
### [DefaultTheme](../../SimpleXChat/Theme/ThemeTypes.swift#L13) Enum
```swift
enum DefaultTheme {
case LIGHT
case DARK
case SIMPLEX
case BLACK
static let SYSTEM_THEME_NAME = "SYSTEM"
var themeName: String { ... }
var mode: DefaultThemeMode { ... } // .light or .dark
}
```
### Color Palettes
Each base theme defines two palette types:
- [`Colors`](../../SimpleXChat/Theme/ThemeTypes.swift#L44): Standard UI colors (primary, background, surface, error, onBackground, onSurface)
- [`AppColors`](../../SimpleXChat/Theme/ThemeTypes.swift#L90): App-specific colors (sentMessage, receivedMessage, title, primaryVariant2)
Palette instances:
- [`LightColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L656) / [`LightColorPaletteApp`](../../SimpleXChat/Theme/ThemeTypes.swift#L668)
- [`DarkColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L635) / [`DarkColorPaletteApp`](../../SimpleXChat/Theme/ThemeTypes.swift#L647)
- [`SimplexColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L677) / [`SimplexColorPaletteApp`](../../SimpleXChat/Theme/ThemeTypes.swift#L689)
- [`BlackColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L698) / [`BlackColorPaletteApp`](../../SimpleXChat/Theme/ThemeTypes.swift#L710)
---
## 4. Customization Layers
### Layer 1: App Settings Theme
Stored in `themeOverridesDefault` (UserDefaults). Contains `[ThemeOverrides]` -- an array of theme overrides, one per base theme.
#### [`ThemeOverrides`](../../SimpleXChat/Theme/ThemeTypes.swift#L385)
```swift
struct ThemeOverrides: Codable {
var base: DefaultTheme
var colors: ThemeColors? // Color overrides
var wallpaper: ThemeWallpaper? // Wallpaper setting
}
```
### Layer 2: Per-User Theme
Stored on the `User` object (`User.uiThemes: ThemeModeOverrides?`), persisted in the Haskell database via `apiSetUserUIThemes(userId:, themes:)`.
#### [`ThemeModeOverrides`](../../SimpleXChat/Theme/ThemeTypes.swift#L570)
```swift
struct ThemeModeOverrides: Codable {
var light: ThemeModeOverride?
var dark: ThemeModeOverride?
var chatLayout: ChatLayout?
}
```
`chatLayout`: the chat layout chosen for the profile or the chat (Chat Layout below). `withChatLayout(_:)` sets it and returns nil for overrides left empty. Theme writes keep it: "Reset to app theme" and "Remove image" keep only the layout (`layoutOnly()`).
#### [`ThemeModeOverride`](../../SimpleXChat/Theme/ThemeTypes.swift#L591)
```swift
struct ThemeModeOverride: Codable {
var mode: DefaultThemeMode?
var colors: ThemeColors?
var wallpaper: ThemeWallpaper?
var type: WallpaperType? // Computed from wallpaper
}
```
### Layer 3: Per-Chat Theme
Stored per-chat via `apiSetChatUIThemes(chatId:, themes:)`. Same `ThemeModeOverrides` structure.
### Chat Layout
Values of `ChatLayout`: `.classic`, `.terminal`, `.unknown(String)`. Terminal rows: [chat view spec, section 9](../client/chat-view.md#9-terminal-layout).
```
app chatLayoutDefault (DEFAULT_CHAT_LAYOUT in UserDefaults); AppSettings.uiChatLayout
profile User.uiThemes.chatLayout
chat uiThemes.chatLayout of the contact or group
chat profile Contact.chatLayout, GroupProfile.chatLayout (ChatInfo.chatLayout)
```
Pickers, "Chat layout":
```
Appearance, app destination ChatLayoutPicker: Classic, Terminal → chatLayoutDefault
Appearance, profile destination ChatLayoutPicker: "<app layout> (default)" → nil; other layout
→ themeUserDestination.withChatLayout, saveThemeToDatabase
chat theme screen ChatLayoutSection, first section of ChatWallpaperEditor (layoutSection)
"<default> (chat)" when ChatInfo.chatLayout is the default, else "<default> (default)" → nil
other layout → chat uiThemes.withChatLayout, apiSetChatUIThemes, ChatModel.updateChatInfo
app destination selected ThemeDestinationPicker: currentUser.uiThemes = uiThemes.layoutOnly()
```
### Override Merging
Colors are merged field-by-field: if a more-specific layer defines a color, it overrides; if nil, falls through to the next layer.
---
## 5. Color System
**File**: [`SimpleXChat/Theme/ThemeTypes.swift`](../../SimpleXChat/Theme/ThemeTypes.swift)
### [ThemeColors](../../SimpleXChat/Theme/ThemeTypes.swift#L230)
Overridable color definitions:
```swift
struct ThemeColors: Codable {
var primary: String? // Primary brand color
var primaryVariant: String? // Primary variant
var secondary: String? // Secondary color
var secondaryVariant: String? // Secondary variant
var background: String? // Main background
var surface: String? // Card/surface background
var title: String? // Title text color
var primaryVariant2: String? // Additional variant
var sentMessage: String? // Sent message bubble
var sentQuote: String? // Sent quote background
var receivedMessage: String? // Received message bubble
var receivedQuote: String? // Received quote background
}
```
Colors are stored as hex strings (e.g., `"#FF6600"`) and converted to SwiftUI `Color` values at resolution time.
### [Colors](../../SimpleXChat/Theme/ThemeTypes.swift#L44) (Resolved Palette)
```swift
struct Colors {
var isLight: Bool
var primary: Color
var primaryVariant: Color
var secondary: Color
var secondaryVariant: Color
var background: Color
var surface: Color
var error: Color
var onBackground: Color
var onSurface: Color
// ... etc
}
```
### [AppColors](../../SimpleXChat/Theme/ThemeTypes.swift#L90) (Resolved App-Specific)
```swift
struct AppColors {
var title: Color
var primaryVariant2: Color
var sentMessage: Color
var sentQuote: Color
var receivedMessage: Color
var receivedQuote: Color
}
```
---
## 6. Wallpapers
**File**: [`SimpleXChat/Theme/ChatWallpaperTypes.swift`](../../SimpleXChat/Theme/ChatWallpaperTypes.swift)
### [Preset Wallpapers](../../SimpleXChat/Theme/ChatWallpaperTypes.swift#L13)
6 built-in wallpaper presets:
| Preset | ID | Description |
|--------|-----|-------------|
| Cats | `cats` | Cat-themed pattern |
| Flowers | `flowers` | Floral pattern |
| Hearts | `hearts` | Heart pattern |
| Kids | `kids` | Children's pattern |
| School | `school` | School/notebook pattern (default) |
| Travel | `travel` | Travel-themed pattern |
Each preset defines per-theme color tints (`PresetWallpaper.colors[DefaultTheme]`) that subtly adjust the color palette to complement the wallpaper.
### Custom Wallpapers
Users can set a custom image as wallpaper:
- Stored in `Documents/wallpapers/` directory
- Scaled and tiled to fill the chat background
- Custom wallpapers can be combined with color overrides
Chats in the terminal layout draw the wallpaper background colour without the image; their theme screens show presets as colours and hide the custom image tile, image scale and wallpaper tint ([chat view spec, section 9](../client/chat-view.md#9-terminal-layout)).
### [WallpaperType](../../SimpleXChat/Theme/ChatWallpaperTypes.swift#L311)
```swift
enum WallpaperType {
case preset(filename: String, scale: Float?) // Built-in wallpaper
case image(filename: String, scale: Float?) // Custom image
case empty // No wallpaper
}
```
### [AppWallpaper](../../SimpleXChat/Theme/ThemeTypes.swift#L142) (Resolved)
```swift
struct AppWallpaper {
var background: Color? // Background color override
var tint: Color? // Tint/overlay color
var type: WallpaperType
}
```
---
## 7. Chat Bubble Styling
Configurable bubble appearance properties:
| Property | Description | Stored In |
|----------|-------------|-----------|
| `chatItemRoundness` | Corner radius of message bubbles | App settings |
| `chatItemTail` | Whether bubbles have a tail/arrow | App settings |
| Avatar corner radius | Roundness of profile avatars | App settings |
These are configured in [`Shared/Views/UserSettings/AppearanceSettings.swift`](../../Shared/Views/UserSettings/AppearanceSettings.swift) ([L26](../../Shared/Views/UserSettings/AppearanceSettings.swift#L26)).
---
## 8. Color Scheme Mode
### System Follow
When theme is set to `"system"` (DefaultTheme.SYSTEM_THEME_NAME):
- Light mode: uses `DefaultTheme.LIGHT` palette
- Dark mode: uses the configured dark theme (`systemDarkThemeDefault`), which can be Dark, SimpleX, or Black
### Forced Mode
Users can force light or dark mode regardless of system setting by selecting a specific theme other than "system".
### Detection
[`systemInDarkThemeCurrently`](../../Shared/Theme/Theme.swift#L95):
```swift
var systemInDarkThemeCurrently: Bool {
return UITraitCollection.current.userInterfaceStyle == .dark
}
```
`ChatModel.currentUser` setter triggers [`ThemeManager.applyTheme()`](../../Shared/Theme/ThemeManager.swift#L124) to handle per-user theme overrides when switching users.
---
## 9. YAML Export/Import
Theme configurations can be exported as YAML for sharing:
### Export
[`ThemeManager.currentThemeOverridesForExport()`](../../Shared/Theme/ThemeManager.swift#L105) generates a `ThemeOverrides` representing the current resolved theme, which is then serialized to YAML using the Yams library.
### Import
YAML theme strings are parsed back into `ThemeOverrides` and applied as app settings theme overrides.
Key functions in [`AppearanceSettings.swift`](../../Shared/Views/UserSettings/AppearanceSettings.swift):
| Function | Purpose | Line |
|----------|---------|------|
| [`ImportExportThemeSection`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L603) | UI section for import/export controls | [L603](../../Shared/Views/UserSettings/AppearanceSettings.swift#L603) |
| [`ThemeImporter`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L640) | ViewModifier for YAML file import | [L640](../../Shared/Views/UserSettings/AppearanceSettings.swift#L640) |
| [`decodeYAML(_:)`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L1150) | Parse YAML string into Decodable type | [L1150](../../Shared/Views/UserSettings/AppearanceSettings.swift#L1150) |
| [`encodeThemeOverrides(_:)`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L1160) | Encode ThemeOverrides to YAML string | [L1160](../../Shared/Views/UserSettings/AppearanceSettings.swift#L1160) |
### Toolbar Material
[`ToolbarMaterial`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L319) controls the navigation bar appearance:
- Configurable opacity/material (translucent, opaque)
- Stored in app settings
---
## Source Files
| File | Path | Key Definitions |
|------|------|-----------------|
| Theme manager | [`Shared/Theme/ThemeManager.swift`](../../Shared/Theme/ThemeManager.swift) | `ThemeManager` (L15), `ActiveTheme` (L17) |
| Theme types & colors | [`SimpleXChat/Theme/ThemeTypes.swift`](../../SimpleXChat/Theme/ThemeTypes.swift) | `DefaultTheme` (L13), `Colors` (L44), `AppColors` (L90), `AppWallpaper` (L142), `ThemeColors` (L230), `ThemeWallpaper` (L302), `ThemeOverrides` (L385), `ThemeModeOverrides` (L570), `ThemeModeOverride` (L591) |
| Wallpaper types | [`SimpleXChat/Theme/ChatWallpaperTypes.swift`](../../SimpleXChat/Theme/ChatWallpaperTypes.swift) | `PresetWallpaper` (L13), `WallpaperType` (L311) |
| Color utilities | [`SimpleXChat/Theme/Color.swift`](../../SimpleXChat/Theme/Color.swift) | Hex color conversion |
| App theme observable | [`Shared/Theme/Theme.swift`](../../Shared/Theme/Theme.swift) | `AppTheme` (L22), `CurrentColors` (L14), `systemInDarkThemeCurrently` (L95) |
| Appearance settings UI | [`Shared/Views/UserSettings/AppearanceSettings.swift`](../../Shared/Views/UserSettings/AppearanceSettings.swift) | `AppearanceSettings` (L26), `ToolbarMaterial` (L319), `ImportExportThemeSection` (L603) |
| Theme mode editor | `Shared/Views/Helpers/ThemeModeEditor.swift` | Theme mode selection UI |
| Haskell theme types | `../../src/Simplex/Chat/Types/UITheme.hs` | Server-side theme persistence |