From d5fa6cc56bb3ca68444c272b4f67541b77d43dbe Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Sun, 17 May 2026 13:41:40 -0400 Subject: [PATCH] feat(themes): replace cycle button with popover theme picker Click theme button opens panel grouped by dark/light. Hover previews theme live, click confirms. Dismiss on outside-click or Escape. --- web/app.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++----- web/style.css | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/web/app.js b/web/app.js index 843e2af..9a24b48 100644 --- a/web/app.js +++ b/web/app.js @@ -1892,20 +1892,88 @@ // ========== Theme ========== - const THEMES = ['dark', 'paper', 'tinycard', 'catppuccin', 'catppuccin-latte', 'nord', 'dracula', 'gruvbox', 'rosepine', 'rosepine-dawn', 'tokyonight', 'solarized', 'solarized-light']; - const THEME_ICONS = { dark: '◑', paper: '◐', tinycard: '◈', catppuccin: '◕', 'catppuccin-latte': '◔', nord: '◓', dracula: '◒', gruvbox: '◔', rosepine: '◍', 'rosepine-dawn': '◌', tokyonight: '◗', solarized: '◖', 'solarized-light': '◑' }; + const THEMES_DARK = [ + { id: 'dark', label: 'Noir', swatch: '#c8942a' }, + { id: 'tinycard', label: 'Tinycard', swatch: '#ad8ee6' }, + { id: 'catppuccin', label: 'Catppuccin', swatch: '#cba6f7' }, + { id: 'nord', label: 'Nord', swatch: '#88c0d0' }, + { id: 'dracula', label: 'Dracula', swatch: '#bd93f9' }, + { id: 'gruvbox', label: 'Gruvbox', swatch: '#fabd2f' }, + { id: 'rosepine', label: 'Rosé Pine', swatch: '#c4a7e7' }, + { id: 'tokyonight', label: 'Tokyo Night', swatch: '#7aa2f7' }, + { id: 'solarized', label: 'Solarized', swatch: '#268bd2' }, + ]; + const THEMES_LIGHT = [ + { id: 'paper', label: 'Paper', swatch: '#8a6018' }, + { id: 'catppuccin-latte', label: 'Catppuccin Latte', swatch: '#8839ef' }, + { id: 'rosepine-dawn', label: 'Rosé Pine Dawn', swatch: '#907aa9' }, + { id: 'solarized-light', label: 'Solarized Light', swatch: '#268bd2' }, + ]; + const ALL_THEME_IDS = [...THEMES_DARK, ...THEMES_LIGHT].map(t => t.id); const themeToggle = $('#theme-toggle'); let nibTheme = localStorage.getItem('nib:theme') || 'dark'; - if (!THEMES.includes(nibTheme)) nibTheme = 'dark'; + if (!ALL_THEME_IDS.includes(nibTheme)) nibTheme = 'dark'; document.documentElement.setAttribute('data-theme', nibTheme); - themeToggle.textContent = THEME_ICONS[nibTheme]; + themeToggle.textContent = '◑'; - themeToggle.addEventListener('click', () => { - nibTheme = THEMES[(THEMES.indexOf(nibTheme) + 1) % THEMES.length]; + const popover = document.createElement('div'); + popover.className = 'theme-popover'; + function buildPopover() { + popover.innerHTML = ''; + const addSection = (label, themes) => { + const lbl = document.createElement('div'); + lbl.className = 'theme-popover-label'; + lbl.textContent = label; + popover.appendChild(lbl); + themes.forEach(t => { + const item = document.createElement('div'); + item.className = 'theme-popover-item' + (t.id === nibTheme ? ' active' : ''); + item.dataset.theme = t.id; + item.innerHTML = `${t.label}`; + popover.appendChild(item); + }); + }; + addSection('Dark', THEMES_DARK); + addSection('Light', THEMES_LIGHT); + } + buildPopover(); + themeToggle.appendChild(popover); + + let previewTheme = null; + themeToggle.addEventListener('click', (e) => { + if (e.target.closest('.theme-popover-item')) return; + popover.classList.toggle('open'); + }); + + popover.addEventListener('mouseover', (e) => { + const item = e.target.closest('.theme-popover-item'); + if (!item) return; + previewTheme = item.dataset.theme; + document.documentElement.setAttribute('data-theme', previewTheme); + }); + + popover.addEventListener('mouseleave', () => { + previewTheme = null; + document.documentElement.setAttribute('data-theme', nibTheme); + }); + + popover.addEventListener('click', (e) => { + const item = e.target.closest('.theme-popover-item'); + if (!item) return; + nibTheme = item.dataset.theme; + previewTheme = null; document.documentElement.setAttribute('data-theme', nibTheme); localStorage.setItem('nib:theme', nibTheme); - themeToggle.textContent = THEME_ICONS[nibTheme]; + popover.classList.remove('open'); + buildPopover(); + }); + + document.addEventListener('click', (e) => { + if (!themeToggle.contains(e.target)) popover.classList.remove('open'); + }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') popover.classList.remove('open'); }); // ========== Init ========== diff --git a/web/style.css b/web/style.css index da9216a..6406254 100644 --- a/web/style.css +++ b/web/style.css @@ -382,6 +382,7 @@ nav { display: flex; gap: 2px; } #search-input::placeholder { color: var(--dim); } .theme-toggle { + position: relative; margin-left: auto; border: 1px solid var(--border); border-radius: var(--r1); @@ -394,6 +395,60 @@ nav { display: flex; gap: 2px; } .theme-toggle:hover { color: var(--accent); border-color: var(--accent); } +.theme-popover { + position: absolute; + top: calc(100% + 6px); + right: 0; + z-index: 900; + background: var(--surf); + border: 1px solid var(--border); + border-radius: var(--r3); + padding: 8px 0; + min-width: 180px; + box-shadow: 0 8px 24px rgba(0,0,0,.3); + display: none; +} + +.theme-popover.open { display: block; } + +.theme-popover-label { + padding: 4px 12px; + font-size: 10px; + font-family: var(--mono); + color: var(--dim); + text-transform: uppercase; + letter-spacing: .5px; +} + +.theme-popover-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + cursor: pointer; + font-size: 13px; + font-family: var(--sans); + color: var(--muted); + transition: background var(--t-fast), color var(--t-fast); +} + +.theme-popover-item:hover { + background: var(--a-bg); + color: var(--text); +} + +.theme-popover-item.active { + color: var(--accent); +} + +.theme-popover-swatch { + width: 12px; + height: 12px; + border-radius: 50%; + border: 1px solid var(--border); + flex-shrink: 0; +} + /* ── MAIN LAYOUT ────────────────────────────────────── */ main { display: grid;