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
+11 -2
View File
@@ -395,7 +395,13 @@ async fn import_css(
) -> impl IntoResponse {
let css_dir = state.css_dir();
let mut settings = state.settings.lock().unwrap();
match settings.import_css(&state.settings_path, &css_dir, body.filename, &body.content) {
match settings.import_css(
&state.settings_path,
&css_dir,
body.filename,
&body.content,
body.is_theme,
) {
Ok(id) => {
let entry = settings
.css_entries
@@ -418,7 +424,7 @@ async fn link_css(
Json(body): Json<LinkCssRequest>,
) -> impl IntoResponse {
let mut settings = state.settings.lock().unwrap();
match settings.link_css(&state.settings_path, body.path_or_url.clone()) {
match settings.link_css(&state.settings_path, body.path_or_url.clone(), body.is_theme) {
Ok(()) => {
let entry = settings
.css_entries
@@ -564,6 +570,9 @@ async fn update_settings(
if let Some(max_logs) = body.max_logs {
settings.max_logs = max_logs;
}
if let Some(custom_theme_id) = body.custom_theme_id {
settings.custom_theme_id = custom_theme_id;
}
match settings.save(&state.settings_path) {
Ok(()) => (),
Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),