diff --git a/src/store/settings.js b/src/store/settings.js index cdc6eb0..ac7e1bf 100644 --- a/src/store/settings.js +++ b/src/store/settings.js @@ -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', + ); } } diff --git a/src/styles/layout.css b/src/styles/layout.css index 0bef994..b764a98 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -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 { diff --git a/src/styles/tokens.css b/src/styles/tokens.css index c8e4efe..f0dafa9 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -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; } diff --git a/src/views/DexGrid.js b/src/views/DexGrid.js index c841710..7eee641 100644 --- a/src/views/DexGrid.js +++ b/src/views/DexGrid.js @@ -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(); diff --git a/src/views/SettingsView.js b/src/views/SettingsView.js index e96b3a2..47af97b 100644 --- a/src/views/SettingsView.js +++ b/src/views/SettingsView.js @@ -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,