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:
@@ -63,6 +63,7 @@ go build -o palette ./cmd/palette
|
||||
| `PALETTE_MAX_TEXT` | `5242880` | Max paste size in bytes (5 MB) |
|
||||
| `PALETTE_MAX_ITEM` | `26214400` | Max can item size in bytes (25 MB) |
|
||||
| `PALETTE_ADMIN_KEY` | generated | Admin key; if unset a 32-char hex key is generated and persisted to `<db-dir>/admin-key` (0600) |
|
||||
| `PALETTE_DEFAULT_DARK` | dark on | Default dark mode for new visitors. Set `false`, `0`, or `off` to default to light mode. Visitors who toggle dark mode keep their choice in their browser. |
|
||||
|
||||
An `/admin` page exists for runtime settings, protected by a key set at
|
||||
install (`PALETTE_ADMIN_KEY` env var) and resettable locally — see
|
||||
|
||||
@@ -42,6 +42,7 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
web.SetDefaultDark(os.Getenv("PALETTE_DEFAULT_DARK"))
|
||||
srv := api.NewServer(st, cfg, ui, ss, adminKey)
|
||||
log.Printf("palette listening on %s", cfg.Addr)
|
||||
log.Fatal(http.ListenAndServe(cfg.Addr, srv.Routes()))
|
||||
|
||||
@@ -203,6 +203,22 @@ body {
|
||||
.iconbtn { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: var(--radius); padding: 5px 12px; font: inherit; font-size: 21.6px; cursor: pointer; text-decoration: none; }
|
||||
.iconbtn.gear { display: inline-flex; align-items: center; padding: 5px 9px; }
|
||||
.iconbtn.gear svg { width: 22px; height: 22px; }
|
||||
/* #127: gear highlights like selected nav tabs; dark toggle button */
|
||||
.iconbtn.gear.on { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
||||
.iconbtn.dark-toggle { display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px; }
|
||||
.iconbtn.dark-toggle svg { width: 22px; height: 22px; }
|
||||
.iconbtn.dark-toggle .icon-moon { display: none; }
|
||||
.iconbtn.dark-toggle .icon-sun { display: inline-block; }
|
||||
.dark .iconbtn.dark-toggle .icon-sun { display: none; }
|
||||
.dark .iconbtn.dark-toggle .icon-moon { display: inline-block; }
|
||||
/* #127: gear highlights like selected nav tabs; dark toggle button */
|
||||
.iconbtn.gear.on { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
||||
.iconbtn.dark-toggle { display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px; }
|
||||
.iconbtn.dark-toggle svg { width: 22px; height: 22px; }
|
||||
.iconbtn.dark-toggle .icon-moon { display: none; }
|
||||
.iconbtn.dark-toggle .icon-sun { display: inline-block; }
|
||||
.dark .iconbtn.dark-toggle .icon-sun { display: none; }
|
||||
.dark .iconbtn.dark-toggle .icon-moon { display: inline-block; }
|
||||
.settings-head { padding: 12px 18px; border-bottom: 1px solid var(--border); }
|
||||
.settings-head h1 { font-size: 29.2px; font-weight: 600; margin: 0; }
|
||||
.settings-body { padding: 16px 18px; color: var(--muted-fg); font-size: 21.6px; }
|
||||
@@ -480,6 +496,12 @@ a.admin-link:hover { color: var(--fg); text-decoration: underline; }
|
||||
.topbar nav a { padding: 5px 8px; font-size: 15px; }
|
||||
.iconbtn.gear { padding: 4px 7px; flex-shrink: 0; }
|
||||
.iconbtn.gear svg { width: 18px; height: 18px; }
|
||||
.iconbtn.dark-toggle { padding: 4px 7px; flex-shrink: 0; }
|
||||
.iconbtn.dark-toggle svg { width: 18px; height: 18px; }
|
||||
.iconbtn.dark-toggle span { display: none; }
|
||||
.iconbtn.dark-toggle { padding: 4px 7px; flex-shrink: 0; }
|
||||
.iconbtn.dark-toggle svg { width: 18px; height: 18px; }
|
||||
.iconbtn.dark-toggle span { display: none; }
|
||||
|
||||
/* new paste: stack editor above sidebar, natural page height */
|
||||
.deck {
|
||||
@@ -627,3 +649,45 @@ button[type="submit"]:focus-visible,
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* #38: attachment upload on /new + attachment display on paste view */
|
||||
.side-section .file-input { display: none; }
|
||||
.dropzone {
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px 12px;
|
||||
text-align: center;
|
||||
color: var(--muted-fg);
|
||||
cursor: pointer;
|
||||
font-size: 20.7px;
|
||||
transition: border-color .15s, background .15s;
|
||||
background: var(--surface-2, transparent);
|
||||
}
|
||||
.dropzone:hover, .dropzone.dragover {
|
||||
border-color: var(--accent);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.dropzone.dragover { border-width: 2px; }
|
||||
.file-chip {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 8px 14px; margin-top: 8px; background: var(--surface);
|
||||
font-size: 21.6px;
|
||||
}
|
||||
.file-chip .file-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
||||
.file-chip .file-chip-size { color: var(--muted-fg); font-size: 19px; }
|
||||
.file-chip .file-chip-remove {
|
||||
border: none; background: none; color: var(--muted-fg); cursor: pointer;
|
||||
font-size: 22px; padding: 0 4px; border-radius: var(--radius-sm);
|
||||
}
|
||||
.file-chip .file-chip-remove:hover { color: var(--danger, #c0392b); }
|
||||
.attachment-bar { display: flex; flex-direction: column; gap: 10px; }
|
||||
.attachment-chip {
|
||||
display: inline-flex; align-items: center; gap: 12px; align-self: flex-start;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 8px 16px; text-decoration: none; color: var(--fg);
|
||||
background: var(--surface-2); font-size: 21.6px;
|
||||
}
|
||||
.attachment-chip:hover { border-color: var(--accent); }
|
||||
.attachment-chip .attachment-size { color: var(--muted-fg); font-size: 19px; }
|
||||
.attachment-preview img { max-width: 480px; max-height: 360px; border-radius: var(--radius); border: 1px solid var(--border); }
|
||||
|
||||
@@ -3,11 +3,48 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<script>
|
||||
// preset hook (#16): ?theme= wins; else persisted choice (#100)
|
||||
// preset hook (#16): ?theme= wins; else persisted choice (#100).
|
||||
// #127: theme pairs - each base theme has a light and dark variant;
|
||||
// palette-theme stores the base id, palette-dark the dark flag.
|
||||
(function () {
|
||||
var PAIRS = {
|
||||
'midnight': { light: 'midnight-light', dark: 'midnight' },
|
||||
'smooth': { light: 'smooth', dark: 'smooth-dark' },
|
||||
'pastel-lavender': { light: 'pastel-lavender', dark: 'pastel-lavender-dark' },
|
||||
'pastel-peach': { light: 'pastel-peach', dark: 'pastel-peach-dark' },
|
||||
'pastel-cloud': { light: 'pastel-cloud', dark: 'pastel-cloud-dark' }
|
||||
};
|
||||
function resolve(t, dark) {
|
||||
for (var base in PAIRS) {
|
||||
if (t === PAIRS[base].light || t === PAIRS[base].dark) return t;
|
||||
if (t === base) return dark ? PAIRS[base].dark : PAIRS[base].light;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
var t = new URLSearchParams(location.search).get('theme');
|
||||
if (!t) try { t = localStorage.getItem('palette-theme'); } catch (e) {}
|
||||
if (t) document.documentElement.dataset.preset = t;
|
||||
// precedence: URL ?theme= > stored pair+dark prefs > server default dark > dark on
|
||||
var dark = {{ defaultDark }};
|
||||
try {
|
||||
var stored = localStorage.getItem('palette-dark');
|
||||
if (stored !== null) dark = stored !== 'false';
|
||||
} catch (e) {}
|
||||
if (t) {
|
||||
// URL ?theme= wins and accepts both base and variant ids; a variant id
|
||||
// also sets the dark flag and normalizes t back to its base pair id.
|
||||
var found = false;
|
||||
for (var base in PAIRS) {
|
||||
if (t === PAIRS[base].dark) { dark = true; t = base; found = true; break; }
|
||||
if (t === PAIRS[base].light) { dark = false; t = base; found = true; break; }
|
||||
if (t === base) { found = true; break; }
|
||||
}
|
||||
if (!found) t = null;
|
||||
}
|
||||
if (!t) {
|
||||
try { t = localStorage.getItem('palette-theme'); } catch (e) {}
|
||||
}
|
||||
if (!PAIRS[t]) t = 'midnight';
|
||||
// do not persist anything here: URL theme is a one-off override
|
||||
document.documentElement.dataset.preset = dark ? PAIRS[t].dark : PAIRS[t].light;
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -22,8 +59,57 @@
|
||||
<a href="https://git.archfox.org/poslop/palette" target="_blank" rel="noopener">Git<svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<a class="iconbtn gear" href="/settings" title="Settings" aria-label="Settings">
|
||||
<button class="iconbtn dark-toggle" id="dark-toggle" type="button" title="Toggle dark mode" aria-label="Toggle dark mode">
|
||||
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
</button>
|
||||
<a class="iconbtn gear{{if eq .Page "settings"}} on{{end}}" href="/settings" title="Settings" aria-label="Settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<script>
|
||||
// #127: dark mode toggle - flips to the other variant of the active pair.
|
||||
(function () {
|
||||
var PAIRS = {
|
||||
'midnight': { light: 'midnight-light', dark: 'midnight' },
|
||||
'smooth': { light: 'smooth', dark: 'smooth-dark' },
|
||||
'pastel-lavender': { light: 'pastel-lavender', dark: 'pastel-lavender-dark' },
|
||||
'pastel-peach': { light: 'pastel-peach', dark: 'pastel-peach-dark' },
|
||||
'pastel-cloud': { light: 'pastel-cloud', dark: 'pastel-cloud-dark' }
|
||||
};
|
||||
var root = document.documentElement;
|
||||
function state() {
|
||||
var p = root.dataset.preset || 'midnight';
|
||||
for (var base in PAIRS) {
|
||||
if (p === PAIRS[base].dark) return { base: base, dark: true };
|
||||
if (p === PAIRS[base].light) return { base: base, dark: false };
|
||||
}
|
||||
return { base: 'midnight', dark: true };
|
||||
}
|
||||
function apply() {
|
||||
var s = state();
|
||||
document.body.classList.toggle('dark', s.dark);
|
||||
root.classList.toggle('dark', s.dark);
|
||||
}
|
||||
function sync(btns) {
|
||||
btns.forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', state().dark ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
var btns = document.querySelectorAll('.dark-toggle');
|
||||
apply();
|
||||
sync(Array.prototype.slice.call(btns));
|
||||
btns.forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var s = state();
|
||||
var dark = !s.dark;
|
||||
root.dataset.preset = dark ? PAIRS[s.base].dark : PAIRS[s.base].light;
|
||||
try { localStorage.setItem('palette-dark', dark ? 'true' : 'false'); } catch (e) {}
|
||||
apply();
|
||||
sync(Array.prototype.slice.call(btns));
|
||||
document.dispatchEvent(new CustomEvent('palette-darkchange'));
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
<div class="settings-body">
|
||||
<h3>Theme</h3>
|
||||
<div class="theme-grid" id="theme-grid"></div>
|
||||
<button class="iconbtn dark-toggle" id="settings-dark-toggle" type="button" title="Toggle dark mode" aria-label="Toggle dark mode">
|
||||
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
<span>Dark mode</span>
|
||||
</button>
|
||||
<p class="admin-link-row"><a class="admin-link" href="/admin">Admin</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,17 +21,13 @@
|
||||
(function () {
|
||||
// #112: derive each preset's swatches from the real CSS variables in
|
||||
// app.css by temporarily applying data-preset, so they can never drift.
|
||||
var themeNames = [
|
||||
// #127: 5 theme pairs, light swatches top row, dark bottom row.
|
||||
var pairs = [
|
||||
{ id: 'midnight', name: 'Midnight' },
|
||||
{ id: 'smooth', name: 'Smooth' },
|
||||
{ id: 'pastel-lavender', name: 'Pastel Lavender' },
|
||||
{ id: 'pastel-peach', name: 'Pastel Peach' },
|
||||
{ id: 'pastel-cloud', name: 'Pastel Cloud' },
|
||||
{ id: 'midnight-light', name: 'Midnight Light' },
|
||||
{ id: 'smooth-dark', name: 'Smooth Dark' },
|
||||
{ id: 'pastel-lavender-dark', name: 'Pastel Lavender Dark' },
|
||||
{ id: 'pastel-peach-dark', name: 'Pastel Peach Dark' },
|
||||
{ id: 'pastel-cloud-dark', name: 'Pastel Cloud Dark' }
|
||||
{ id: 'pastel-cloud', name: 'Pastel Cloud' }
|
||||
];
|
||||
var SWATCH_VARS = ['--bg', '--surface', '--surface-2', '--muted', '--accent'];
|
||||
|
||||
@@ -40,26 +41,60 @@
|
||||
return colors;
|
||||
}
|
||||
|
||||
// current base pair + dark flag from the resolved data-preset.
|
||||
// "midnight" is itself the dark variant, so check dark ids first.
|
||||
function state() {
|
||||
var p = document.documentElement.dataset.preset || 'midnight';
|
||||
if (p === 'midnight' || /-dark$/.test(p)) {
|
||||
return { base: p === 'midnight' ? 'midnight' : p.replace(/-dark$/, ''), dark: true };
|
||||
}
|
||||
return { base: p === 'midnight-light' ? 'midnight' : p, dark: false };
|
||||
}
|
||||
|
||||
var grid = document.getElementById('theme-grid');
|
||||
var current = document.documentElement.dataset.preset || 'midnight';
|
||||
themeNames.forEach(function (t) {
|
||||
var colors = presetColors(t.id);
|
||||
var cards = {};
|
||||
pairs.forEach(function (t) {
|
||||
var light = presetColors(t.id);
|
||||
var dark = presetColors(t.id + '-dark');
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'theme-card';
|
||||
btn.setAttribute('aria-pressed', current === t.id ? 'true' : 'false');
|
||||
btn.setAttribute('aria-pressed', 'false');
|
||||
btn.setAttribute('data-pair', t.id);
|
||||
btn.innerHTML = '<strong>' + t.name + '</strong>' +
|
||||
'<span class="swatches">' + colors.map(function (c) {
|
||||
'<span class="swatches">' + light.map(function (c) {
|
||||
return '<span class="swatch" style="background:' + c + '"></span>';
|
||||
}).join('') + '</span>' +
|
||||
'<span class="swatches">' + dark.map(function (c) {
|
||||
return '<span class="swatch" style="background:' + c + '"></span>';
|
||||
}).join('') + '</span>';
|
||||
btn.addEventListener('click', function () {
|
||||
document.documentElement.dataset.preset = t.id;
|
||||
var dark = state().dark;
|
||||
document.documentElement.dataset.preset = dark ? t.id + '-dark' : t.id;
|
||||
try { localStorage.setItem('palette-theme', t.id); } catch (e) {}
|
||||
grid.querySelectorAll('.theme-card').forEach(function (c) { c.setAttribute('aria-pressed', 'false'); });
|
||||
Object.keys(cards).forEach(function (k) { cards[k].setAttribute('aria-pressed', 'false'); });
|
||||
btn.setAttribute('aria-pressed', 'true');
|
||||
});
|
||||
cards[t.id] = btn;
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
|
||||
function sync() {
|
||||
var s = state();
|
||||
Object.keys(cards).forEach(function (k) {
|
||||
cards[k].setAttribute('aria-pressed', k === s.base ? 'true' : 'false');
|
||||
});
|
||||
var dt = document.getElementById('settings-dark-toggle');
|
||||
if (dt) dt.setAttribute('aria-pressed', s.dark ? 'true' : 'false');
|
||||
}
|
||||
sync();
|
||||
// the topbar script runs before this button exists, so wire it here
|
||||
var dt = document.getElementById('settings-dark-toggle');
|
||||
dt.addEventListener('click', function () {
|
||||
var btns = document.querySelectorAll('.topbar .dark-toggle');
|
||||
if (btns.length) btns[0].click(); else document.dispatchEvent(new CustomEvent('palette-darkchange'));
|
||||
});
|
||||
document.addEventListener('palette-darkchange', sync);
|
||||
})();
|
||||
</script>
|
||||
{{template "foot" .}}
|
||||
|
||||
+41
-2
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package web
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolvePreset(t *testing.T) {
|
||||
cases := []struct {
|
||||
pair string
|
||||
dark bool
|
||||
want string
|
||||
}{
|
||||
{"", false, "midnight-light"},
|
||||
{"", true, "midnight"},
|
||||
{"midnight", false, "midnight-light"},
|
||||
{"midnight", true, "midnight"},
|
||||
{"smooth", false, "smooth"},
|
||||
{"smooth", true, "smooth-dark"},
|
||||
{"pastel-lavender", true, "pastel-lavender-dark"},
|
||||
{"pastel-peach", false, "pastel-peach"},
|
||||
{"pastel-cloud", true, "pastel-cloud-dark"},
|
||||
{"bogus", true, "midnight"},
|
||||
{"bogus", false, "midnight-light"},
|
||||
// explicit variant ids also resolve (URL ?theme= may pass them)
|
||||
{"smooth-dark", true, "midnight"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ResolvePreset(c.pair, c.dark); got != c.want {
|
||||
t.Errorf("ResolvePreset(%q, %v) = %q, want %q", c.pair, c.dark, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultDark(t *testing.T) {
|
||||
for _, off := range []string{"false", "0", "off", "OFF", " false "} {
|
||||
DefaultDark = true
|
||||
SetDefaultDark(off)
|
||||
if DefaultDark {
|
||||
t.Errorf("SetDefaultDark(%q): want dark off", off)
|
||||
}
|
||||
}
|
||||
for _, on := range []string{"true", "1", "on", "anything"} {
|
||||
DefaultDark = false
|
||||
SetDefaultDark(on)
|
||||
if !DefaultDark {
|
||||
t.Errorf("SetDefaultDark(%q): want dark on", on)
|
||||
}
|
||||
}
|
||||
DefaultDark = true
|
||||
}
|
||||
Reference in New Issue
Block a user