From d21c3edbfc30cb90aef9d757f137ef6c6b6d71a1 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 29 Aug 2026 01:41:16 +0000 Subject: [PATCH] Indicate if themes support light or dark modes, or both --- docs/themes-and-assets.md | 2 ++ frontend/src/SiteEditor.vue | 13 ++++++++--- pagerite/app.py | 2 +- pagerite/views.py | 44 ++++++++++++++++++++++++++++++++----- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/docs/themes-and-assets.md b/docs/themes-and-assets.md index f006a83..81bb704 100644 --- a/docs/themes-and-assets.md +++ b/docs/themes-and-assets.md @@ -16,6 +16,8 @@ Themes are folders in `pagerite/themes/{name}/` containing `theme.css` and/or `b `Data.theme` selects the active theme (empty = none/base only) and the site editor can switch it, choosing from the theme folders found on disk. Vue may add per-component styles on top where needed. +The site editor shows a light/dark-mode indicator in front of each theme name, read from the theme's `color-scheme` declaration in `theme.css`: ☀️ for light-only, 🌙 for dark-only, and 🌓 for themes that support both. The base theme (`none`) is light-only. + Current themes: - `purple` — dark dusk palette with Fraunces/Literata and a tilted oversized gradient brand. diff --git a/frontend/src/SiteEditor.vue b/frontend/src/SiteEditor.vue index 84bab4c..6e40d74 100644 --- a/frontend/src/SiteEditor.vue +++ b/frontend/src/SiteEditor.vue @@ -56,12 +56,19 @@ const brand = ref('') const theme = ref('') // Theme options come from the backend (theme folders on disk, see GET // /_api/settings), so added themes need no frontend changes. -const themeOptions = ref([{ value: '', label: 'none' }]) +const themeOptions = ref([{ value: '', label: '☀️ none' }]) // Page transition (cube, crossfade, ...): a design folder with // transition.css under pagerite/themes/, injected as #pagerite-transition. const transition = ref('cube') const transitionOptions = ref([]) +// Mode icons match the ones used in the Paskia auth frontend. +const MODE_ICONS = { light: '☀️', dark: '🌙', both: '🌓' } + +function themeLabel(t) { + return `${MODE_ICONS[t.mode] || MODE_ICONS.light} ${t.name}` +} + async function loadSettings() { try { const s = await (await fetch('/_api/settings')).json() @@ -73,8 +80,8 @@ async function loadSettings() { customCss.value = s.custom_css || '' favicon.value = s.favicon || '' themeOptions.value = [ - { value: '', label: 'none' }, - ...(s.themes || []).map((t) => ({ value: t, label: t })), + { value: '', label: `${MODE_ICONS.light} none` }, + ...(s.themes || []).map((t) => ({ value: t.name, label: themeLabel(t) })), ] transition.value = s.transition || 'cube' transitionOptions.value = s.transitions || [] diff --git a/pagerite/app.py b/pagerite/app.py index e5e1bf4..598c6bf 100644 --- a/pagerite/app.py +++ b/pagerite/app.py @@ -557,7 +557,7 @@ async def get_settings() -> dict: "theme": data.theme, "custom_css": data.custom_css, "favicon": f"/_f/{data.favicon}" if data.favicon else "", - "themes": views._theme_names(), + "themes": views._theme_info(), "banner_designs": views._banner_design_names(), "transition": data.transition, "transitions": views._transition_names(), diff --git a/pagerite/views.py b/pagerite/views.py index 31979f2..c462e4d 100644 --- a/pagerite/views.py +++ b/pagerite/views.py @@ -47,11 +47,45 @@ def _manifest() -> dict: return _manifest_cache -def _theme_names() -> list[str]: - """Theme folders on disk (a folder is a theme when it has theme.css).""" - return sorted( - d.name for d in THEMES.iterdir() if d.is_dir() and (d / "theme.css").exists() - ) +def _theme_color_schemes(theme: str) -> set[str]: + """Return the color-scheme keywords (``light``/``dark``) from theme.css. + + Reads the first ``color-scheme:`` declaration in the file. An empty set + means the theme did not declare one. + """ + path = THEMES / theme / "theme.css" + try: + css = path.read_text() + except (OSError, ValueError): + return set() + css = re.sub(r"/\*.*?\*/", "", css, flags=re.DOTALL) + m = re.search(r"color-scheme\s*:\s*([^;]+);", css, re.IGNORECASE) + if not m: + return set() + return {tok.lower() for tok in m.group(1).split() if tok.lower() in {"light", "dark"}} + + +def _theme_mode(theme: str) -> str: + """Light/dark mode support of a theme, derived from its CSS. + + Returns one of ``"light"``, ``"dark"``, or ``"both"``. Themes without a + ``color-scheme`` declaration are treated as light-only. + """ + schemes = _theme_color_schemes(theme) + if "light" in schemes and "dark" in schemes: + return "both" + if "dark" in schemes: + return "dark" + return "light" + + +def _theme_info() -> list[dict[str, str]]: + """Theme folders on disk, each with its name and supported color mode.""" + return [ + {"name": d.name, "mode": _theme_mode(d.name)} + for d in sorted(THEMES.iterdir(), key=lambda d: d.name) + if d.is_dir() and (d / "theme.css").exists() + ] def _banner_design_names() -> list[str]: