Add downloadable/re-uploadable custom themes (ticket 033)

Themes download as a small `:root { --c-*: ...; }` CSS file (full
canonical variable set); uploading validates that set is complete and
registers the file as a CssEntry the theme picker can select. A new
`is_theme` flag on CssEntry distinguishes theme entries from arbitrary
imported/linked CSS so they don't leak into the page unconditionally
like the existing "Configured CSS" list does — a selected custom
theme's CSS is injected only while active, with built-in html.<scheme>
classes removed so its :root override isn't outranked in the cascade.
Linked (URL) custom themes reuse the existing CSS link mechanism and
inherit its non-deletable semantics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 16:41:39 +01:00
co-authored by Claude Sonnet 5
parent 7bf323dd50
commit 41c9a3660b
8 changed files with 660 additions and 89 deletions
@@ -1,42 +0,0 @@
# 033 — Downloadable and re-uploadable custom themes
## Summary
Let a user export the active theme's `--c-*` variable values as a file, edit
it, and load it back in as a custom theme — reusing the CSS import/link
machinery `Settings` already has (`CssEntry`, `import_css`/`link_css` in
`src/settings.rs`) rather than inventing a parallel storage mechanism.
## Scope
- **Download**: given a theme name (one of the built-ins from
[032](032-greyscale-theme-rework.md), or a previously-uploaded custom one),
produce a small CSS (or JSON) file containing just its `--c-*` variable
values, served for download from the webapp's theme picker.
- **Upload**: accept such a file back, validate it defines the expected set
of `--c-*` variables, and register it as a selectable custom theme —
persisted via the existing `import_css`-style flow (a new CSS entry scoped
under a `html.<custom-name>` selector, or a dedicated theme entry if that
shape doesn't fit `CssEntry` cleanly — decide during implementation which
reuses more of the existing settings model).
- **Linked** custom themes: since `CssEntry::Linked` already models
"reference a URL, don't own the content," a custom theme can also be
registered by URL instead of upload, consistent with how linked CSS works
today.
- Theme picker UI: list built-in themes alongside any uploaded/linked custom
ones, each with a download action.
## Acceptance criteria
- Downloading a theme produces a file that, re-uploaded unmodified, recreates
the same theme.
- Editing a downloaded theme's variable values and re-uploading it registers
a distinct selectable theme without touching the built-ins.
- A linked (URL) custom theme behaves like linked CSS today: not deletable
via the "remove" action that applies to uploaded ones, per
`CssError::NotImported`'s existing semantics.
## Depends on
- [032 — Greyscale theme rework](032-greyscale-theme-rework.md)
- [008 — Settings model](008-settings-model.md) (`CssEntry` import/link flow)