The UI was hardcoded to one Catppuccin Mocha palette (color literals in
view() + theme() pinned to Dark). It now sources colours from a chosen
theme's palette, selectable in Settings.
- theme.rs (NEW): Palette (13 semantic colour roles) + AppTheme enum
(Catppuccin Mocha/Macchiato/Frappe/Latte, Dracula, Nord, Tokyo Night,
Gruvbox Dark, Solarized Light, Gruvbox Light). Pure palette()/label()/
ALL/base_theme()/is_dark() + WCAG relative_luminance()/contrast_ratio().
- config: theme: AppTheme field (serde-default Mocha) + backward-compat.
- app: theme() returns config.theme.base_theme() (iced widget chrome);
view() + with_layout_picker() source colours from the palette; new
ThemeSwatch canvas widget; a Theme section of clickable swatches in
Settings; SelectTheme applies live + persists. Canvas widgets already
take colours as data, so they re-theme for free.
Tests written alongside (+7 theme, +2 config; 135 -> 144 lib): every
palette clears WCAG AA text-on-base contrast (4.5:1) with subtext/accent
>= 3:1, is_dark matches luminance direction, variants distinct/labeled,
serde round-trips, default = Mocha.
Verified live: the Settings swatch grid renders all 10 palettes and the
whole UI re-themes (screenshot-checked Latte light + Dracula dark).
clippy --all-targets clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>