Declutter feed header; add theme + accent options

- Feed header: the game pill is gone (the nav Games button opens the sheet);
  the active game is now a quiet text label under the title
- Settings > Theme: add Black (OLED) and Sepia alongside System/Light/Dark
- Settings > Accent colour: six swatches (red default, blue, green, amber,
  violet, rose) layered over any theme via [data-accent]; persisted
- applyTheme() takes the settings object; system theme now keys off the
  absence of [data-theme] so explicit light/sepia don't fight the media query

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
chris 2026-08-27 13:24:48 -04:00
parent fa6df9d60b
commit 61ceb6abf3
5 changed files with 119 additions and 38 deletions

View File

@ -13,23 +13,36 @@ import { createStore } from './createStore.js';
export const settings = createStore('pdx.settings', {
versionGroup: 'scarlet-violet',
pokedex: null,
theme: 'system',
theme: 'system', // system | light | dark | black | sepia
accent: 'red', // red | blue | green | amber | violet | rose
spriteStyle: 'official',
showShiny: false,
locale: 'en',
});
export function applyTheme(theme = settings.get().theme) {
const DARKISH = new Set(['dark', 'black']);
export function applyTheme(state = settings.get()) {
const root = document.documentElement;
if (theme === 'system') root.removeAttribute('data-theme');
const { theme, accent } = state;
if (!theme || theme === 'system') root.removeAttribute('data-theme');
else root.setAttribute('data-theme', theme);
if (!accent || accent === 'red') root.removeAttribute('data-accent');
else root.setAttribute('data-accent', accent);
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) {
const dark =
theme === 'dark' ||
(theme === 'system' &&
DARKISH.has(theme) ||
((!theme || theme === 'system') &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
meta.setAttribute('content', dark ? '#0b0b0c' : '#b3161a');
meta.setAttribute(
'content',
dark
? '#0b0b0c'
: getComputedStyle(root).getPropertyValue('--accent').trim() || '#b3161a',
);
}
}

View File

@ -219,29 +219,13 @@
line-height: 1.1;
text-transform: capitalize;
}
.game-pill {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 8px;
padding: 5px 10px 5px 12px;
border: none;
border-radius: 999px;
background: var(--surface-2);
color: var(--text-dim);
font: inherit;
.feed-head__game {
display: block;
margin-top: 4px;
font-size: 0.82rem;
font-weight: 600;
text-decoration: none;
color: var(--text-dim);
text-transform: capitalize;
cursor: pointer;
}
.game-pill:hover {
color: var(--text);
}
.game-pill__chev {
font-size: 0.9rem;
line-height: 1;
}
.ring {
@ -1379,6 +1363,28 @@
color: var(--text);
font: inherit;
}
.swatches {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.swatch {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid var(--border);
background: var(--sw);
cursor: pointer;
padding: 0;
transition: transform 0.12s var(--ease-spring);
}
.swatch:hover {
transform: scale(1.1);
}
.swatch.is-on {
border-color: var(--text);
box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--sw);
}
/* ---------- Toast ------------------------------------------- */
.toast {

View File

@ -47,8 +47,9 @@
--type-fairy: #ec8fe6;
}
/* System default: follow the OS only when no explicit theme is chosen. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
:root:not([data-theme]) {
--bg: #0b0b0c;
--surface: #17171a;
--surface-2: #202024;
@ -73,6 +74,39 @@
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
}
/* Pure black — for OLED screens. */
:root[data-theme="black"] {
--bg: #000000;
--surface: #0b0b0c;
--surface-2: #17171a;
--text: #f4f4f6;
--text-dim: #9a9aa4;
--border: #222226;
--accent: #ff5a5f;
--accent-text: #16161a;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.6), 0 10px 30px rgba(0, 0, 0, 0.55);
--shadow-lg: 0 20px 50px -12px rgba(0, 0, 0, 0.65);
}
/* Sepia — a warm, low-glare light theme. */
:root[data-theme="sepia"] {
--bg: #f2e8d5;
--surface: #fbf4e4;
--surface-2: #e9dbbe;
--text: #3b3022;
--text-dim: #7c6c52;
--border: #dccbaa;
--accent: #a8410f;
--accent-text: #ffffff;
}
/* Accent overrides — layered on top of any theme. Red is the default. */
:root[data-accent="blue"] { --accent: #2f6fed; --accent-text: #ffffff; }
:root[data-accent="green"] { --accent: #1f9d57; --accent-text: #ffffff; }
:root[data-accent="amber"] { --accent: #d98a00; --accent-text: #1a1200; }
:root[data-accent="violet"] { --accent: #7b52e0; --accent-text: #ffffff; }
:root[data-accent="rose"] { --accent: #e0417a; --accent-text: #ffffff; }
* {
box-sizing: border-box;
}

View File

@ -6,7 +6,6 @@ import { ui } from '../store/ui.js';
import { selection, stats } from '../store/selection.js';
import { Card } from '../components/Card.js';
import { ProgressRing } from '../components/ProgressRing.js';
import { openGameSheet } from '../components/GameSheet.js';
const FILTERS = [
{ key: 'all', label: 'All', test: () => true },
@ -82,12 +81,9 @@ export async function DexGrid() {
const ring = ProgressRing();
const title = el('h1', {});
const gamePill = el(
'button',
{ class: 'game-pill', type: 'button', onclick: () => openGameSheet() },
el('span', {}),
el('span', { class: 'game-pill__chev', 'aria-hidden': 'true' }, '⌄'),
);
// Which game is active — a quiet label, not a control. Change it from the
// Games button in the nav.
const gameLabel = el('span', { class: 'feed-head__game' });
const search = el('input', {
class: 'field-search__input',
@ -159,7 +155,7 @@ export async function DexGrid() {
el(
'div',
{ class: 'feed-head__top' },
el('div', { class: 'feed-head__id' }, title, gamePill),
el('div', { class: 'feed-head__id' }, title, gameLabel),
ring.node,
),
el(
@ -260,7 +256,7 @@ export async function DexGrid() {
ids = rows.map((r) => r.species.id);
title.textContent = prettify(dex.name || dex.key);
gamePill.firstChild.textContent =
gameLabel.textContent =
st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup);
refreshMeta();
paintGrid();

View File

@ -10,11 +10,43 @@ export async function SettingsView() {
['system', 'System'],
['light', 'Light'],
['dark', 'Dark'],
['black', 'Black (OLED)'],
['sepia', 'Sepia'],
], (v) => {
settings.set({ theme: v });
applyTheme(v);
applyTheme();
});
const ACCENTS = [
['red', '#b3161a'],
['blue', '#2f6fed'],
['green', '#1f9d57'],
['amber', '#d98a00'],
['violet', '#7b52e0'],
['rose', '#e0417a'],
];
const accentRow = el(
'div',
{ class: 'swatches' },
...ACCENTS.map(([key, hex]) =>
el('button', {
class: `swatch${st.accent === key ? ' is-on' : ''}`,
type: 'button',
style: `--sw:${hex}`,
title: key,
'aria-label': `${key} accent`,
onclick: () => {
settings.set({ accent: key });
applyTheme();
[...accentRow.children].forEach((b, i) =>
b.classList.toggle('is-on', ACCENTS[i][0] === key),
);
},
}),
),
);
const accentField = el('div', { class: 'field' }, el('span', {}, 'Accent colour'), accentRow);
const spriteField = selectField('Sprite style', st.spriteStyle, [
['default', 'Pixel (modern)'],
['game', 'Game era (pixel art from the selected game)'],
@ -61,7 +93,7 @@ export async function SettingsView() {
view.append(
el('header', { class: 'view__header' }, el('h1', {}, 'Settings')),
el('div', { class: 'settings__group' }, themeField, spriteField, shinyField),
el('div', { class: 'settings__group' }, themeField, accentField, spriteField, shinyField),
el('h2', {}, 'Your data'),
storageNote,