Add dark mode with system/light/dark theme selector in Appearance

This commit is contained in:
Ilia Sharin
2026-05-07 17:13:31 -04:00
parent c2e9eaaa7b
commit 37563ffe6e
4 changed files with 183 additions and 2 deletions
+19
View File
@@ -406,11 +406,30 @@ const ICON_STYLES = [
{ value: 'ascii', label: 'ASCII', hint: 'Plain text glyphs: [ ] [/] [!] …' },
];
const THEMES = [
{ value: 'system', label: 'System', hint: 'Follow OS light / dark setting' },
{ value: 'light', label: 'Light', hint: 'Always light' },
{ value: 'dark', label: 'Dark', hint: 'Always dark' },
];
function AppearanceCard({ tweaks, setTweak }) {
const current = tweaks?.status_style || 'color';
const theme = tweaks?.theme || 'system';
return (
<section className="card">
<div className="card-head"><h3>Appearance</h3></div>
<div style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.04em', margin: '4px 0 6px' }}>Theme</div>
<div className="seg" style={{ marginBottom: 14 }}>
{THEMES.map(t => (
<button key={t.value}
className={`seg-btn ${theme === t.value ? 'active' : ''}`}
title={t.hint}
onClick={() => setTweak('theme', t.value)}>
{t.label}
</button>
))}
</div>
<div style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.04em', margin: '4px 0 6px' }}>Status icons</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{ICON_STYLES.map(opt => (
<label key={opt.value}
+15 -1
View File
@@ -57,7 +57,7 @@ function App() {
const [confirm, setConfirm] = useStateMain(null);
const [needsConnect, setNeedsConnect] = useStateMain(false);
const TWEAK_DEFAULTS = { accent_hue: 35, density: 'comfortable', show_ids: false, paper_texture: true, status_style: 'color', sort_mode: 'priority' };
const TWEAK_DEFAULTS = { accent_hue: 35, density: 'comfortable', show_ids: false, paper_texture: true, status_style: 'color', sort_mode: 'priority', theme: 'system' };
const [tweaks, setTweak] = useTweaks({ ...TWEAK_DEFAULTS, ...(loadLocalState()?.tweaks ?? {}) });
// Refs for async callbacks that need latest state without stale closures.
@@ -76,6 +76,20 @@ function App() {
document.documentElement.dataset.paper = tweaks.paper_texture ? 'true' : 'false';
}, [tweaks]);
useEffectMain(() => {
const apply = () => {
const t = tweaks.theme === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: tweaks.theme;
document.documentElement.dataset.theme = t;
};
apply();
if (tweaks.theme !== 'system') return;
const mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener('change', apply);
return () => mq.removeEventListener('change', apply);
}, [tweaks.theme]);
// ---- Storage initialisation (runs once on mount) ----
useEffectMain(() => {
let cancelled = false;
+47
View File
@@ -43,6 +43,53 @@
}
html[data-density="compact"] { --row-h: 28px; --row-pad-y: 4px; }
/* ---------- Dark theme ---------- */
html[data-theme="dark"] {
--bg: oklch(0.19 0.008 60);
--bg-2: oklch(0.23 0.009 60);
--paper: oklch(0.215 0.008 60);
--ink: oklch(0.93 0.006 70);
--ink-2: oklch(0.74 0.006 70);
--ink-3: oklch(0.56 0.008 70);
--rule: oklch(0.32 0.012 70);
--rule-2: oklch(0.27 0.010 70);
--status-open: oklch(0.72 0.008 60);
--status-progress: oklch(0.68 0.13 220);
--status-blocked: oklch(0.66 0.16 25);
--status-postponed: oklch(0.74 0.14 60);
--status-done: oklch(0.66 0.12 145);
--status-cancelled: oklch(0.55 0.012 60);
--warn: oklch(0.78 0.13 80);
--warn-bg: oklch(0.30 0.06 80);
--ok: oklch(0.7 0.12 145);
--danger: oklch(0.66 0.16 25);
--danger-soft: oklch(0.66 0.16 25 / 0.14);
--shadow-1: 0 1px 0 oklch(0 0 0 / 0.4);
--shadow-2: 0 1px 2px oklch(0 0 0 / 0.4), 0 4px 14px oklch(0 0 0 / 0.35);
--shadow-pop: 0 8px 24px oklch(0 0 0 / 0.55), 0 2px 6px oklch(0 0 0 / 0.45);
}
html[data-theme="dark"] .header { background: oklch(0.19 0.008 60 / 0.86); }
html[data-theme="dark"] .row:hover { background: oklch(0.27 0.010 60 / 0.6); }
html[data-theme="dark"] .btn-primary:hover:not(:disabled),
html[data-theme="dark"] .row-btn.primary:hover { background: oklch(0.85 0.008 70); color: var(--bg); }
html[data-theme="dark"] .banner.info { background: oklch(0.30 0.06 220); color: oklch(0.85 0.04 220); }
html[data-theme="dark"][data-paper="true"] body {
background-image: radial-gradient(oklch(0.55 0.008 70 / 0.18) 1px, transparent 1px);
}
html[data-theme="dark"] .restore-warn {
background: oklch(0.28 0.06 80);
border-color: oklch(0.45 0.10 80 / 0.6);
color: oklch(0.85 0.06 75);
}
html[data-theme="dark"] .restore-warn svg { color: oklch(0.78 0.13 70); }
html[data-theme="dark"] .restore-warn-title { color: oklch(0.92 0.05 75); }
html[data-theme="dark"] .restore-warn-body { color: oklch(0.78 0.04 70); }
html[data-theme="dark"] .restore-warn-body code { background: oklch(0 0 0 / 0.30); }
html[data-theme="dark"] .drag-handle { color: oklch(0.50 0.005 60); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
body {
+102 -1
View File
@@ -55,6 +55,53 @@
}
html[data-density="compact"] { --row-h: 28px; --row-pad-y: 4px; }
/* ---------- Dark theme ---------- */
html[data-theme="dark"] {
--bg: oklch(0.19 0.008 60);
--bg-2: oklch(0.23 0.009 60);
--paper: oklch(0.215 0.008 60);
--ink: oklch(0.93 0.006 70);
--ink-2: oklch(0.74 0.006 70);
--ink-3: oklch(0.56 0.008 70);
--rule: oklch(0.32 0.012 70);
--rule-2: oklch(0.27 0.010 70);
--status-open: oklch(0.72 0.008 60);
--status-progress: oklch(0.68 0.13 220);
--status-blocked: oklch(0.66 0.16 25);
--status-postponed: oklch(0.74 0.14 60);
--status-done: oklch(0.66 0.12 145);
--status-cancelled: oklch(0.55 0.012 60);
--warn: oklch(0.78 0.13 80);
--warn-bg: oklch(0.30 0.06 80);
--ok: oklch(0.7 0.12 145);
--danger: oklch(0.66 0.16 25);
--danger-soft: oklch(0.66 0.16 25 / 0.14);
--shadow-1: 0 1px 0 oklch(0 0 0 / 0.4);
--shadow-2: 0 1px 2px oklch(0 0 0 / 0.4), 0 4px 14px oklch(0 0 0 / 0.35);
--shadow-pop: 0 8px 24px oklch(0 0 0 / 0.55), 0 2px 6px oklch(0 0 0 / 0.45);
}
html[data-theme="dark"] .header { background: oklch(0.19 0.008 60 / 0.86); }
html[data-theme="dark"] .row:hover { background: oklch(0.27 0.010 60 / 0.6); }
html[data-theme="dark"] .btn-primary:hover:not(:disabled),
html[data-theme="dark"] .row-btn.primary:hover { background: oklch(0.85 0.008 70); color: var(--bg); }
html[data-theme="dark"] .banner.info { background: oklch(0.30 0.06 220); color: oklch(0.85 0.04 220); }
html[data-theme="dark"][data-paper="true"] body {
background-image: radial-gradient(oklch(0.55 0.008 70 / 0.18) 1px, transparent 1px);
}
html[data-theme="dark"] .restore-warn {
background: oklch(0.28 0.06 80);
border-color: oklch(0.45 0.10 80 / 0.6);
color: oklch(0.85 0.06 75);
}
html[data-theme="dark"] .restore-warn svg { color: oklch(0.78 0.13 70); }
html[data-theme="dark"] .restore-warn-title { color: oklch(0.92 0.05 75); }
html[data-theme="dark"] .restore-warn-body { color: oklch(0.78 0.04 70); }
html[data-theme="dark"] .restore-warn-body code { background: oklch(0 0 0 / 0.30); }
html[data-theme="dark"] .drag-handle { color: oklch(0.50 0.005 60); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
body {
@@ -5162,16 +5209,58 @@ const ICON_STYLES = [{
label: 'ASCII',
hint: 'Plain text glyphs: [ ] [/] [!] …'
}];
const THEMES = [{
value: 'system',
label: 'System',
hint: 'Follow OS light / dark setting'
}, {
value: 'light',
label: 'Light',
hint: 'Always light'
}, {
value: 'dark',
label: 'Dark',
hint: 'Always dark'
}];
function AppearanceCard({
tweaks,
setTweak
}) {
const current = tweaks?.status_style || 'color';
const theme = tweaks?.theme || 'system';
return /*#__PURE__*/React.createElement("section", {
className: "card"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Appearance")), /*#__PURE__*/React.createElement("div", {
style: {
fontSize: 11.5,
fontWeight: 600,
color: 'var(--ink-3)',
textTransform: 'uppercase',
letterSpacing: '0.04em',
margin: '4px 0 6px'
}
}, "Theme"), /*#__PURE__*/React.createElement("div", {
className: "seg",
style: {
marginBottom: 14
}
}, THEMES.map(t => /*#__PURE__*/React.createElement("button", {
key: t.value,
className: `seg-btn ${theme === t.value ? 'active' : ''}`,
title: t.hint,
onClick: () => setTweak('theme', t.value)
}, t.label))), /*#__PURE__*/React.createElement("div", {
style: {
fontSize: 11.5,
fontWeight: 600,
color: 'var(--ink-3)',
textTransform: 'uppercase',
letterSpacing: '0.04em',
margin: '4px 0 6px'
}
}, "Status icons"), /*#__PURE__*/React.createElement("div", {
style: {
display: 'flex',
flexDirection: 'column',
@@ -5558,7 +5647,8 @@ function App() {
show_ids: false,
paper_texture: true,
status_style: 'color',
sort_mode: 'priority'
sort_mode: 'priority',
theme: 'system'
};
const [tweaks, setTweak] = useTweaks({
...TWEAK_DEFAULTS,
@@ -5582,6 +5672,17 @@ function App() {
document.documentElement.dataset.showIds = tweaks.show_ids ? 'true' : 'false';
document.documentElement.dataset.paper = tweaks.paper_texture ? 'true' : 'false';
}, [tweaks]);
useEffectMain(() => {
const apply = () => {
const t = tweaks.theme === 'system' ? window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' : tweaks.theme;
document.documentElement.dataset.theme = t;
};
apply();
if (tweaks.theme !== 'system') return;
const mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener('change', apply);
return () => mq.removeEventListener('change', apply);
}, [tweaks.theme]);
// ---- Storage initialisation (runs once on mount) ----
useEffectMain(() => {