diff --git a/src/settings.rs b/src/settings.rs index b607019..e492379 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -18,12 +18,17 @@ use crate::alerts::AlertRule; pub type SettingsHandle = Arc>; /// User's preferred colour scheme for the webapp. +/// +/// `Dark` accepts the legacy `"grey"` serde value (ticket 004: "grey" was a +/// near-duplicate of "dark" and has been removed as a selectable option) so a +/// settings file saved before that change still loads cleanly instead of +/// falling back to `System`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum ColorScheme { Light, + #[serde(alias = "grey")] Dark, - Grey, Red, Green, Blue, @@ -316,6 +321,15 @@ mod tests { assert_eq!(reloaded, settings); } + #[test] + fn legacy_grey_color_scheme_deserializes_as_dark() { + let settings: Settings = serde_json::from_str( + r#"{"color_scheme":"grey","css_entries":[],"custom_theme_id":null,"profiles":{"names":[],"active_profile":null,"profiles_dir":"profiles"},"max_logs":null,"alert_rules":[],"ntfy_topic":null,"ntfy_server":null}"#, + ) + .unwrap(); + assert_eq!(settings.color_scheme, ColorScheme::Dark); + } + #[test] fn round_trips_through_save_and_load() { let dir = tempfile::tempdir().unwrap(); diff --git a/tickets/004-remove-grey-theme-keep-dark.md b/tickets/004-remove-grey-theme-keep-dark.md deleted file mode 100644 index 3ab6137..0000000 --- a/tickets/004-remove-grey-theme-keep-dark.md +++ /dev/null @@ -1,8 +0,0 @@ -# Remove duplicate "grey" theme, keep "dark" - -Dark and grey are the same thing. Keep dark, remove grey. - -## Acceptance criteria -- The "grey" theme option is removed from theme selection UI and settings. -- Any references/config values for "grey" are migrated or aliased to "dark" so existing saved settings don't break. -- "dark" remains as the single equivalent theme. diff --git a/tickets/012-merge-custom-themes-and-imported-css.md b/tickets/012-merge-custom-themes-and-imported-css.md deleted file mode 100644 index 07ea314..0000000 --- a/tickets/012-merge-custom-themes-and-imported-css.md +++ /dev/null @@ -1,14 +0,0 @@ -# Merge "custom themes" and "configured/imported CSS" into one concept - -Custom themes and configured/imported CSS are the same thing. - -## Current behavior (notes) -- Imported CSS (as pasted content or file upload) is base64-encoded and stored in the settings file. -- The "name" field is not a file name — it's a human-readable name, labeled just "name" in the UI. It gets displayed alongside the other (built-in) themes. -- The built-in themes are also auto-inserted into the settings JSON. -- Imported CSS entries can be "deleted", which removes them from the settings JSON. - -## Acceptance criteria -- The "custom themes" and "configured/imported CSS" features are consolidated into a single mechanism. -- Duplicate UI, settings, and code paths for the two are removed. -- Existing user configuration for either is migrated or aliased so it keeps working under the unified mechanism. diff --git a/webapp/src/components/NavMenu.tsx b/webapp/src/components/NavMenu.tsx index 7618cad..318f93e 100644 --- a/webapp/src/components/NavMenu.tsx +++ b/webapp/src/components/NavMenu.tsx @@ -21,7 +21,6 @@ const COLOR_SCHEMES: ColorScheme[] = [ 'system', 'light', 'dark', - 'grey', 'red', 'green', 'blue', @@ -185,13 +184,13 @@ function PresetsDropdown({ panel }: { panel: ReturnType } function SettingsPanel({ panel }: { panel: ReturnType }) { - const [filename, setFilename] = useState('') - const [content, setContent] = useState('') const [maxLogsInput, setMaxLogsInput] = useState('') - const [themeUploadError, setThemeUploadError] = useState(null) - const [themeFilename, setThemeFilename] = useState('') - const [themeContent, setThemeContent] = useState('') - const [themeLinkUrl, setThemeLinkUrl] = useState('') + const [cssFilename, setCssFilename] = useState('') + const [cssContent, setCssContent] = useState('') + const [cssIsTheme, setCssIsTheme] = useState(false) + const [cssUploadError, setCssUploadError] = useState(null) + const [cssLinkUrl, setCssLinkUrl] = useState('') + const [cssLinkIsTheme, setCssLinkIsTheme] = useState(false) const [ntfyTopicInput, setNtfyTopicInput] = useState('') const [ntfyServerInput, setNtfyServerInput] = useState('') const [ntfyTestError, setNtfyTestError] = useState(null) @@ -247,19 +246,41 @@ function SettingsPanel({ panel }: { panel: ReturnType } panel.setMaxLogs(Math.floor(parsed)) } - async function handleFileChange(e: React.ChangeEvent) { + async function handleCssFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return - setFilename(file.name) - setContent(await file.text()) + setCssFilename(file.name) + setCssContent(await file.text()) + setCssUploadError(null) } - async function handleImport(e: React.FormEvent) { + // Single import path for both "configured/imported CSS" and "custom + // themes" (ticket 012) — `cssIsTheme` is the only thing that distinguishes + // them, so a theme upload is only additionally validated against the + // canonical `--c-*` variable set before it's sent. + async function handleCssImport(e: React.FormEvent) { e.preventDefault() - if (!filename.trim() || !content.trim()) return - await panel.importCssEntry(filename.trim(), content) - setFilename('') - setContent('') + setCssUploadError(null) + if (!cssFilename.trim() || !cssContent.trim()) return + if (cssIsTheme) { + const { missing } = parseThemeCss(cssContent) + if (missing.length > 0) { + setCssUploadError(`Missing variable${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`) + return + } + } + await panel.importCssEntry(cssFilename.trim(), cssContent, cssIsTheme) + setCssFilename('') + setCssContent('') + setCssIsTheme(false) + } + + async function handleCssLink(e: React.FormEvent) { + e.preventDefault() + if (!cssLinkUrl.trim()) return + await panel.linkCssEntry(cssLinkUrl.trim(), cssLinkIsTheme) + setCssLinkUrl('') + setCssLinkIsTheme(false) } function downloadBuiltinTheme(name: string) { @@ -268,7 +289,7 @@ function SettingsPanel({ panel }: { panel: ReturnType } downloadThemeFile(`${name}.css`, generateThemeCss(vars)) } - async function downloadThemeEntry(entry: CssEntry) { + async function downloadCssEntry(entry: CssEntry) { if (entry.kind === 'imported') { const res = await fetch(cssContentUrl(entry.id)) const text = await res.text() @@ -278,38 +299,6 @@ function SettingsPanel({ panel }: { panel: ReturnType } } } - async function handleThemeFileChange(e: React.ChangeEvent) { - const file = e.target.files?.[0] - if (!file) return - setThemeFilename(file.name) - setThemeContent(await file.text()) - setThemeUploadError(null) - } - - async function handleThemeUpload(e: React.FormEvent) { - e.preventDefault() - setThemeUploadError(null) - if (!themeFilename.trim() || !themeContent.trim()) return - const { missing } = parseThemeCss(themeContent) - if (missing.length > 0) { - setThemeUploadError(`Missing variable${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`) - return - } - await panel.importThemeEntry(themeFilename.trim(), themeContent) - setThemeFilename('') - setThemeContent('') - } - - async function handleThemeLink(e: React.FormEvent) { - e.preventDefault() - if (!themeLinkUrl.trim()) return - await panel.linkThemeEntry(themeLinkUrl.trim()) - setThemeLinkUrl('') - } - - const themeEntries = panel.cssEntries.filter((entry) => entry.is_theme) - const arbitraryCssEntries = panel.cssEntries.filter((entry) => !entry.is_theme) - return (
@@ -344,96 +333,6 @@ function SettingsPanel({ panel }: { panel: ReturnType }
-
-

Custom themes

- {themeEntries.length === 0 ? ( -
No custom themes yet.
- ) : ( -
    - {themeEntries.map((entry) => ( -
  • - -
    - - {entry.kind === 'imported' ? ( - - ) : ( - - — - - )} -
    -
  • - ))} -
- )} - -
- - setThemeFilename(e.target.value)} - className="rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none" - /> - {themeUploadError &&
{themeUploadError}
} - -
- -
- setThemeLinkUrl(e.target.value)} - className="min-w-0 flex-1 rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none" - /> - -
-
-

Log retention

@@ -490,28 +389,97 @@ function SettingsPanel({ panel }: { panel: ReturnType }
-

Import CSS

-
+
+

CSS

+ + imported/linked stylesheets — check "Use as theme" to make one a selectable alternative to the built-in + colour schemes above instead of an always-on overlay + +
+ {panel.cssEntries.length === 0 ? ( +
No CSS configured.
+ ) : ( +
    + {panel.cssEntries.map((entry) => ( +
  • +
    + {entry.is_theme ? ( + + ) : ( +
    {entry.filename ?? entry.path_or_url}
    + )} +
    + {entry.kind} + {entry.is_theme ? ' · theme' : ''} +
    +
    +
    + + {entry.kind === 'imported' ? ( + + ) : ( + + — + + )} +
    +
  • + ))} +
+ )} + + setFilename(e.target.value)} + placeholder="name.css" + value={cssFilename} + onChange={(e) => setCssFilename(e.target.value)} className="rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none" />