feat: 5 dramatic extra themes (neon/terminal/blueprint/sunset) + board/review/layout polish

- Themes: add neon (cyber gradient), terminal (green CRT), blueprint (graph
  paper) and sunset (vaporwave) alongside the default Light (Y2K paper) and
  Dark. Theme toggle now cycles all; profile selector lists them; server
  validates against a shared whitelist.
- Bounty cards now show the project (customer) name.
- Remove the celebratory emoji from the empty Review Queue.
- Full-width h1 underline rule; My Tasks columns stack vertically for more
  room per task; card action buttons wrap/right-align so they never overflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
etalon
2026-06-13 10:44:07 +02:00
parent 05dc91410f
commit 0d28f69320
8 changed files with 153 additions and 8 deletions
+128 -1
View File
@@ -60,6 +60,113 @@
--dither-dense: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2'%3E%3Crect width='1' height='1' x='0' y='0' fill='%23d9a548'/%3E%3C/svg%3E");
}
/* ============================================================= */
/* Additional dramatic themes (user-requested). Each redefines the
full token set + an atmospheric override for a distinct look. */
/* ============================================================= */
/* ---- NEON — cyber, magenta/cyan gradients, glowing accents ---- */
:root[data-theme=neon] {
--bg:#080014; --surface:#160a32; --surface2:#221248; --border:#7a3bf0;
--text:#ece6ff; --muted:#a48fe0; --accent:#00e5ff; --accent-contrast:#06000f;
--ok:#39ff9e; --warn:#ffd23f; --err:#ff3b8b; --radius:8px;
--ink-shadow: 0 0 14px color-mix(in srgb, var(--accent) 55%, transparent);
--ink-shadow-lift: 0 0 26px color-mix(in srgb, var(--accent) 70%, transparent);
--dither: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Crect width='1' height='1' x='0' y='0' fill='%2300e5ff'/%3E%3Crect width='1' height='1' x='2' y='2' fill='%23ff3bd6'/%3E%3C/svg%3E");
--dither-dense: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2'%3E%3Crect width='1' height='1' x='0' y='0' fill='%2300e5ff'/%3E%3C/svg%3E");
}
:root[data-theme=neon] body {
background:
radial-gradient(900px 500px at 12% -8%, rgba(255,59,214,0.28), transparent 60%),
radial-gradient(900px 500px at 92% 4%, rgba(0,229,255,0.24), transparent 60%),
linear-gradient(170deg, #0c0020 0%, #080014 55%, #04000c 100%);
background-attachment: fixed;
}
:root[data-theme=neon] .card,
:root[data-theme=neon] .kanban-col {
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent),
0 0 22px rgba(122,59,240,0.18);
}
:root[data-theme=neon] .btn.primary {
background: linear-gradient(120deg, #00e5ff, #b34dff);
border: none; color: #06000f;
box-shadow: 0 0 16px rgba(0,229,255,0.5);
}
:root[data-theme=neon] h1, :root[data-theme=neon] .brand {
text-shadow: 0 0 18px rgba(0,229,255,0.45);
}
/* ---- TERMINAL — green phosphor CRT, mono everything, scanlines ---- */
:root[data-theme=terminal] {
--bg:#000a02; --surface:#04140a; --surface2:#06200f; --border:#1f8f3f;
--text:#41ff7a; --muted:#1fb04f; --accent:#7dff9f; --accent-contrast:#021006;
--ok:#7dff9f; --warn:#d7ff3f; --err:#ff5d5d; --radius:0px;
--ink-shadow: 0 0 10px rgba(65,255,122,0.4);
--ink-shadow-lift: 0 0 18px rgba(65,255,122,0.55);
--dither: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Crect width='1' height='1' x='0' y='0' fill='%237dff9f'/%3E%3Crect width='1' height='1' x='2' y='2' fill='%237dff9f'/%3E%3C/svg%3E");
--dither-dense: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2'%3E%3Crect width='1' height='1' x='0' y='0' fill='%237dff9f'/%3E%3C/svg%3E");
--font-display: "Spline Sans Mono", ui-monospace, monospace;
--font-body: "Spline Sans Mono", ui-monospace, monospace;
}
:root[data-theme=terminal] body {
background:
repeating-linear-gradient(0deg, rgba(0,0,0,0) 0 2px, rgba(0,40,15,0.55) 2px 3px),
radial-gradient(120% 90% at 50% 0%, #03240f 0%, #000a02 70%);
background-attachment: fixed;
text-shadow: 0 0 6px rgba(65,255,122,0.35);
}
:root[data-theme=terminal] h1 { text-transform: uppercase; letter-spacing: 0.1em; }
/* ---- BLUEPRINT — technical drawing, graph paper, cyan ink ---- */
:root[data-theme=blueprint] {
--bg:#082a4d; --surface:#0c3460; --surface2:#114576; --border:#6fb8ff;
--text:#eaf4ff; --muted:#9cc6f0; --accent:#ffd24a; --accent-contrast:#082a4d;
--ok:#7fffd4; --warn:#ffd24a; --err:#ff9a8a; --radius:0px;
--ink-shadow: 4px 4px 0 rgba(0,0,0,0.3);
--ink-shadow-lift: 6px 6px 0 rgba(0,0,0,0.4);
--dither: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Crect width='1' height='1' x='0' y='0' fill='%236fb8ff'/%3E%3Crect width='1' height='1' x='2' y='2' fill='%236fb8ff'/%3E%3C/svg%3E");
--dither-dense: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2'%3E%3Crect width='1' height='1' x='0' y='0' fill='%236fb8ff'/%3E%3C/svg%3E");
}
:root[data-theme=blueprint] body {
background:
repeating-linear-gradient(0deg, transparent 0 23px, rgba(111,184,255,0.16) 23px 24px),
repeating-linear-gradient(90deg, transparent 0 23px, rgba(111,184,255,0.16) 23px 24px),
#082a4d;
background-attachment: fixed;
}
:root[data-theme=blueprint] .card,
:root[data-theme=blueprint] .kanban-col {
background: rgba(12,52,96,0.82);
border: 1px solid var(--border);
}
/* ---- SUNSET — warm vaporwave gradient, soft pastel ink ---- */
:root[data-theme=sunset] {
--bg:#2a1138; --surface:#3a1a4e; --surface2:#4d2363; --border:#ff8ec7;
--text:#fff0fb; --muted:#e0a9d6; --accent:#ffb347; --accent-contrast:#2a1138;
--ok:#7ef0c4; --warn:#ffd76a; --err:#ff6b8e; --radius:12px;
--ink-shadow: 0 8px 24px rgba(255,107,142,0.25);
--ink-shadow-lift: 0 12px 34px rgba(255,142,199,0.35);
--dither: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Crect width='1' height='1' x='0' y='0' fill='%23ffb347'/%3E%3Crect width='1' height='1' x='2' y='2' fill='%23ff8ec7'/%3E%3C/svg%3E");
--dither-dense: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2'%3E%3Crect width='1' height='1' x='0' y='0' fill='%23ff8ec7'/%3E%3C/svg%3E");
}
:root[data-theme=sunset] body {
background:
linear-gradient(180deg, #ff6b8e 0%, #b14d9c 26%, #5e2a72 55%, #2a1138 100%);
background-attachment: fixed;
}
:root[data-theme=sunset] .card,
:root[data-theme=sunset] .kanban-col {
background: rgba(58,26,78,0.7);
backdrop-filter: blur(6px);
border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
:root[data-theme=sunset] .btn.primary {
background: linear-gradient(120deg, #ffb347, #ff6b8e);
border: none; color: #2a1138;
}
/* ---- base ---- */
* { box-sizing: border-box; }
/* author display rules (flex etc.) must never defeat the hidden attribute */
@@ -100,7 +207,7 @@ h1 { font-size: 2.1rem; }
h1::after {
content: "";
display: block;
width: 88px; height: 8px;
width: 100%; height: 8px;
margin-top: 10px;
background-image: var(--dither-dense);
border-bottom: 2px solid var(--accent);
@@ -527,6 +634,26 @@ table.list tbody tr:nth-child(even) { background: color-mix(in srgb, var(--surfa
letter-spacing: 0.16em;
color: var(--muted);
}
/* Project (customer) label on bounty cards */
.card-project {
font-family: var(--font-mono);
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--accent);
margin: 6px 0 0;
}
/* My Tasks: stack status columns vertically so each task gets full width */
.kanban.mt { grid-template-columns: 1fr; }
.kanban.mt .kanban-col { min-height: 0; }
/* Card action rows: wrap and right-align so buttons never overflow the card */
.kanban .card .spread { flex-wrap: wrap; }
.kanban .card [data-actions] {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
/* ---- notification bell ---- */
.bell-badge {
+3
View File
@@ -5,6 +5,7 @@ const grid = document.getElementById('board-grid');
const errorBox = document.getElementById('error');
let meId = '';
let tasks = [];
const customerNames = new Map();
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
@@ -25,6 +26,7 @@ async function load() {
const qs = new URLSearchParams(f).toString();
const res = await api('GET', '/api/v1/board?' + qs);
tasks = res.tasks;
(res.customers || []).forEach((c) => customerNames.set(c.id, c.name));
const sel = document.getElementById('f-customer');
if (sel.options.length === 1) {
res.customers.forEach((c) => sel.add(new Option(c.name, c.id)));
@@ -58,6 +60,7 @@ function renderCard(t) {
<span class="badge accent" style="font-size:1rem">◈ ${t.bounty}</span>
<span>${ageBadge(t)}</span>
</div>
${customerNames.has(t.customerId) ? `<p class="muted card-project">${esc(customerNames.get(t.customerId))}</p>` : ''}
<h3 class="mt"><a href="/tasks/${t.id}">${esc(t.title)}</a></h3>
<p class="muted">${esc((t.description || '').slice(0, 180))}</p>
<p class="muted">${(t.acceptanceCriteria || []).length} acceptance criteria</p>
+4 -1
View File
@@ -16,9 +16,12 @@ const themeBtn = document.getElementById('nav-theme');
if (themeBtn) {
themeBtn.addEventListener('click', async () => {
const el = document.documentElement;
const next = el.dataset.theme === 'dark' ? 'light' : 'dark';
const themes = (el.dataset.themes || 'light,dark').split(',');
const cur = themes.indexOf(el.dataset.theme);
const next = themes[(cur + 1) % themes.length];
el.dataset.theme = next;
try { localStorage.setItem('theme', next); } catch (e) { /* ignore */ }
toast('Theme: ' + next);
if (document.body.dataset.loggedIn === '1') {
try {
await api('PATCH', '/api/v1/profile', { settings: { theme: next } });
+1 -1
View File
@@ -26,7 +26,7 @@ async function load() {
return card;
}));
if (!res.tasks.length) {
host.innerHTML = '<p class="muted">Nothing waiting for review. 🎉</p>';
host.innerHTML = '<p class="muted">Nothing waiting for review.</p>';
}
} catch (e) { errorBox.textContent = e.message; }
}
+3 -1
View File
@@ -5,7 +5,9 @@
var el = document.documentElement;
var saved = null;
try { saved = localStorage.getItem('theme'); } catch (e) { /* private mode */ }
var THEMES = ['light', 'dark', 'neon', 'terminal', 'blueprint', 'sunset'];
var theme = saved || el.dataset.defaultTheme || 'light';
if (theme !== 'light' && theme !== 'dark') theme = 'light';
if (THEMES.indexOf(theme) === -1) theme = 'light';
el.dataset.theme = theme;
el.dataset.themes = THEMES.join(',');
})();
+5 -1
View File
@@ -58,7 +58,11 @@
<label for="p-theme">Theme</label>
<select id="p-theme">
<option value="light" {{if eq .User.Settings.Theme "light"}}selected{{end}}>Light (Y2K paper)</option>
<option value="dark" {{if eq .User.Settings.Theme "dark"}}selected{{end}}>Dark</option>
<option value="dark" {{if eq .User.Settings.Theme "dark"}}selected{{end}}>Dark (Y2K ink)</option>
<option value="neon" {{if eq .User.Settings.Theme "neon"}}selected{{end}}>Neon (cyber gradient)</option>
<option value="terminal" {{if eq .User.Settings.Theme "terminal"}}selected{{end}}>Terminal (green CRT)</option>
<option value="blueprint" {{if eq .User.Settings.Theme "blueprint"}}selected{{end}}>Blueprint (graph paper)</option>
<option value="sunset" {{if eq .User.Settings.Theme "sunset"}}selected{{end}}>Sunset (vaporwave)</option>
</select>
</div>