# Theme System This document describes the two separate but overlapping theme features in Ditto: the **App Theme** (which controls the local UI) and the **Profile Theme** (which is published to Nostr for others to see). Understanding the distinction is key to working with this codebase. ## Overview | Concept | Purpose | Scope | Persistence | |---|---|---|---| | **App Theme** | Controls colors, fonts, and background of the local UI | Local to the user's browser | localStorage + encrypted NIP-78 sync | | **Profile Theme** | A set of theme values published as a Nostr event | Public, visible to other users | Kind 16767 replaceable event | The App Theme and Profile Theme share the same underlying data structure (`ThemeConfig`), and there is an optional bridge between them (`autoShareTheme`), but they are fundamentally independent systems. --- ## Part 1: App Theme The App Theme controls what the user sees in their own browser. It has no inherent connection to Nostr. ### Core Concept: 3 Colors Define Everything The entire theme is derived from just 3 core colors, defined by the `CoreThemeColors` interface in `src/themes.ts:8`: ```typescript interface CoreThemeColors { background: string; // HSL string, e.g. "228 20% 10%" text: string; // Text/foreground color primary: string; // Primary accent (buttons, links, focus rings) } ``` From these 3 values, the system auto-derives 19 CSS tokens (the full `ThemeTokens` set) via `deriveTokensFromCore()` in `src/lib/colorUtils.ts:141`. The derivation algorithm: - Detects dark/light mode from background luminance (threshold: 0.2) - Derives `card` and `popover` surfaces by slightly lightening the background (dark mode) or using it directly (light mode) - Derives `secondary` and `muted` surfaces by adjusting background lightness - Derives `border` using the primary hue with reduced saturation - Computes `mutedForeground` as a dimmer version of the text color - Sets `accent = primary` and `ring = primary` - Auto-computes `primaryForeground` using WCAG contrast detection (white or dark) - Uses fixed red values for `destructive` / `destructiveForeground` ### Theme Modes The `Theme` type (`src/contexts/AppContext.ts:9`) has four values: | Mode | Behavior | |---|---| | `"light"` | Uses the builtin (or configured) light color set | | `"dark"` | Uses the builtin (or configured) dark color set | | `"system"` | Resolves to `"light"` or `"dark"` based on `prefers-color-scheme`, with a live media query listener | | `"custom"` | Uses user-defined colors stored in `config.customTheme` | **Builtin themes** are defined in `src/themes.ts:102`: ```typescript const builtinThemes = { light: { background: '270 50% 97%', text: '270 25% 12%', primary: '270 65% 55%' }, dark: { background: '228 20% 10%', text: '210 40% 98%', primary: '258 70% 60%' }, }; ``` Self-hosters can override these at build time via `ditto.json` (injected through `__DITTO_CONFIG__` in `vite.config.ts`), or at runtime via the `ThemesConfig` in `AppConfig.themes`. ### ThemeConfig The `ThemeConfig` type (`src/themes.ts:50`) wraps the 3 core colors with optional extras: ```typescript interface ThemeConfig { title?: string; colors: CoreThemeColors; font?: ThemeFont; // { family: string; url?: string } background?: ThemeBackground; // { url: string; mode?: 'cover' | 'tile'; ... } } ``` This is the canonical type used everywhere: in `AppConfig.customTheme`, in encrypted settings, and in Nostr theme events. ### Theme Presets Named presets are defined in `src/themes.ts:136` (e.g. `pink`, `toxic`, `sunset`). Each preset includes core colors and optionally a font and background image. Applying a preset sets the app theme to `"custom"` and stores the preset's config as `customTheme`. ### How Themes Apply to the DOM The theme pipeline has three stages designed to prevent any flash of wrong colors: #### Stage 1: Pre-React Blocking Script (`public/theme.js`) A synchronous `