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.
This commit is contained in:
2026-05-17 13:41:40 -04:00
parent 8555d0da19
commit d5fa6cc56b
2 changed files with 130 additions and 7 deletions
+75 -7
View File
@@ -1892,20 +1892,88 @@
// ========== Theme ========== // ========== Theme ==========
const THEMES = ['dark', 'paper', 'tinycard', 'catppuccin', 'catppuccin-latte', 'nord', 'dracula', 'gruvbox', 'rosepine', 'rosepine-dawn', 'tokyonight', 'solarized', 'solarized-light']; const THEMES_DARK = [
const THEME_ICONS = { dark: '◑', paper: '◐', tinycard: '◈', catppuccin: '◕', 'catppuccin-latte': '◔', nord: '◓', dracula: '◒', gruvbox: '◔', rosepine: '◍', 'rosepine-dawn': '◌', tokyonight: '◗', solarized: '◖', 'solarized-light': '◑' }; { 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'); const themeToggle = $('#theme-toggle');
let nibTheme = localStorage.getItem('nib:theme') || 'dark'; 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); document.documentElement.setAttribute('data-theme', nibTheme);
themeToggle.textContent = THEME_ICONS[nibTheme]; themeToggle.textContent = '◑';
themeToggle.addEventListener('click', () => { const popover = document.createElement('div');
nibTheme = THEMES[(THEMES.indexOf(nibTheme) + 1) % THEMES.length]; 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 = `<span class="theme-popover-swatch" style="background:${t.swatch}"></span>${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); document.documentElement.setAttribute('data-theme', nibTheme);
localStorage.setItem('nib: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 ========== // ========== Init ==========
+55
View File
@@ -382,6 +382,7 @@ nav { display: flex; gap: 2px; }
#search-input::placeholder { color: var(--dim); } #search-input::placeholder { color: var(--dim); }
.theme-toggle { .theme-toggle {
position: relative;
margin-left: auto; margin-left: auto;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--r1); border-radius: var(--r1);
@@ -394,6 +395,60 @@ nav { display: flex; gap: 2px; }
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); } .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 LAYOUT ────────────────────────────────────── */
main { main {
display: grid; display: grid;