Theme pairs with dark mode toggle, gear active state, PALETTE_DEFAULT_DARK (#127)

- Collapse the 10 theme cards into 5 pairs: light swatches top row, dark bottom row
- Dark mode toggle in settings and topbar (sun/moon inline SVG)
- Toggling dark mode switches to the other variant of the selected pair
- Persist pair id in localStorage palette-theme, dark flag in palette-dark
- Head script resolves base+dark to variant; URL ?theme= accepts both ids and wins
- Settings gear highlights like nav tabs on /settings
- PALETTE_DEFAULT_DARK env var sets server default dark state (default on)
- Server-side resolution helpers + tests
This commit is contained in:
fen
2026-09-09 22:17:04 -05:00
parent 7ed50087a6
commit 821f49993f
7 changed files with 294 additions and 20 deletions
+41 -2
View File
@@ -24,6 +24,44 @@ import (
//go:embed templates/*.html
var tmplFS embed.FS
// DefaultDark is the server-configured default dark mode state for visitors
// without stored preferences (#127). Set via PALETTE_DEFAULT_DARK.
var DefaultDark = true
// SetDefaultDark applies the PALETTE_DEFAULT_DARK env var: unset = dark on,
// "false"/"0"/"off" = dark off.
func SetDefaultDark(v string) {
switch strings.ToLower(strings.TrimSpace(v)) {
case "false", "0", "off":
DefaultDark = false
default:
DefaultDark = true
}
}
// ResolvePreset maps a theme pair id (or explicit variant id) plus dark state
// to a concrete preset id. Returns the defaults when pair is empty or unknown.
// Precedence handled by callers: URL ?theme= > stored pair+dark > defaults.
func ResolvePreset(pair string, dark bool) string {
variants := map[string][2]string{ // pair -> {light, dark}
"midnight": {"midnight-light", "midnight"},
"smooth": {"smooth", "smooth-dark"},
"pastel-lavender": {"pastel-lavender", "pastel-lavender-dark"},
"pastel-peach": {"pastel-peach", "pastel-peach-dark"},
"pastel-cloud": {"pastel-cloud", "pastel-cloud-dark"},
}
if v, ok := variants[pair]; ok {
if dark {
return v[1]
}
return v[0]
}
if dark {
return "midnight"
}
return "midnight-light"
}
//go:embed static
var staticFS embed.FS
@@ -33,8 +71,9 @@ type UI struct {
func New() (*UI, error) {
funcs := template.FuncMap{
"humanSize": humanSize,
"version": func() string { return Version }, // #93: topbar version label
"humanSize": humanSize,
"version": func() string { return Version }, // #93: topbar version label
"defaultDark": func() bool { return DefaultDark }, // #127: server-configured default
}
t, err := template.New("").Funcs(funcs).ParseFS(tmplFS, "templates/*.html")
if err != nil {