The preloader script was reading customTheme.background/primary directly,
but the app now stores colors under customTheme.colors.{background,primary}.
Also fixes the light theme builtin colors, adds support for configured
theme overrides (cfg.themes), and removes obsolete legacy preset handling
(legacy themes are migrated to 'custom' by AppProvider on load).
Replace the 19-token ThemeTokens interface with a 4-field CoreThemeColors
for config storage, Nostr events, and encrypted settings. All other
Tailwind CSS tokens (card, muted, border, etc.) are now derived
automatically via deriveTokensFromCore().
- Add CoreThemeColors interface: { background, text, primary, secondary }
- Keep ThemeTokens as internal type for CSS variable injection
- Update builtinThemes and themePresets to store CoreThemeColors
- Add backward-compatible parsing for legacy 19-token format in Nostr
events, encrypted settings, and localStorage
- Simplify ThemeBuilderPage: remove Surface/UI color sections, just 4
color pickers with auto-derivation always on
- Update all consumers: ThemeSelector, ScopedTheme, ThemeUpdateCard,
MobileDrawer, LeftSidebar, ProfilePage, InitialSyncGate, etc.
Restructure the theme architecture so 'light', 'dark', and 'system' are
builtin themes with build-time colors (customizable by self-hosters),
while 'black', 'pink', and future themes become presets that apply to a
single 'custom' theme slot.
Key changes:
- Theme type is now 'light' | 'dark' | 'system' | 'custom'
- builtinThemes (light/dark) and themePresets (black/pink/etc.) are
separate maps in themes.ts
- AppConfig gains optional customTheme (ThemeTokens) field
- useTheme exposes applyCustomTheme(tokens) for applying presets
- Custom theme tokens sync across devices via encrypted settings
- Backward compat: legacy 'black'/'pink' values in localStorage and
encrypted settings auto-migrate to custom + preset tokens
- All UI surfaces updated: ThemeSelector, LeftSidebar dropdown,
MobileDrawer cycle, and onboarding ThemeStep
Defer body inline style removal until CSS variables are injected by
useApplyTheme, preventing a one-frame flash of unstyled background.
Replace preloader logo <img> with CSS mask technique matching the React
DittoLogo component, so the logo uses the theme's primary color instead
of filter:invert() which turned purple into green.
Read the user's theme from localStorage before first paint so the
preloader background and spinner colors match dark/light/black/pink.
Uses a small blocking script (public/theme.js) that runs before
the preloader is visible.