fix: use CSS media query as authoritative dark mode source for system preference
This commit is contained in:
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.20.28] - 2026-04-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Dark mode "System" setting now reliably follows the OS preference on every page load, even in browsers where JavaScript `matchMedia` is restricted (e.g. Brave with fingerprint protection); CSS `@media (prefers-color-scheme: dark)` now serves as the authoritative source for system preference detection instead of JS
|
||||||
|
|
||||||
## [0.20.27] - 2026-04-20
|
## [0.20.27] - 2026-04-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.20.27",
|
"version": "0.20.28",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.20.27",
|
"version": "0.20.28",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.20.27",
|
"version": "0.20.28",
|
||||||
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+8
-14
@@ -40,24 +40,18 @@
|
|||||||
<link rel="stylesheet" href="/styles/login.css" />
|
<link rel="stylesheet" href="/styles/login.css" />
|
||||||
<link rel="stylesheet" href="/styles/reminders.css" />
|
<link rel="stylesheet" href="/styles/reminders.css" />
|
||||||
|
|
||||||
<!-- Theme: Vor CSS-Rendering anwenden (Flash-Prevention + System-Sync).
|
<!-- Theme: explizite Nutzer-Overrides vor CSS-Rendering anwenden (Flash-Prevention).
|
||||||
Setzt data-theme einmalig beim Laden, damit tokens.css nur [data-theme="dark"]
|
System-Präferenz wird durch @media (prefers-color-scheme: dark) in tokens.css
|
||||||
braucht — kein duplizierter @media-Block mehr nötig. -->
|
direkt per CSS behandelt — kein JS-matchMedia erforderlich. -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var stored = localStorage.getItem('oikos-theme');
|
var stored = localStorage.getItem('oikos-theme');
|
||||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
if (stored === 'dark') {
|
||||||
if (stored === 'dark' || stored === 'light') {
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
document.documentElement.setAttribute('data-theme', stored);
|
} else if (stored === 'light') {
|
||||||
} else {
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
|
||||||
}
|
}
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
// System/null: tokens.css @media (prefers-color-scheme: dark) übernimmt
|
||||||
var current = localStorage.getItem('oikos-theme');
|
|
||||||
if (!current || current === 'system') {
|
|
||||||
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -851,11 +851,13 @@ function currentTheme() {
|
|||||||
|
|
||||||
function applyTheme(value) {
|
function applyTheme(value) {
|
||||||
localStorage.setItem('oikos-theme', value);
|
localStorage.setItem('oikos-theme', value);
|
||||||
if (value === 'light' || value === 'dark') {
|
if (value === 'dark') {
|
||||||
document.documentElement.setAttribute('data-theme', value);
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
} else if (value === 'light') {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
} else {
|
} else {
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
document.documentElement.removeAttribute('data-theme');
|
||||||
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
// tokens.css @media (prefers-color-scheme: dark) übernimmt sofort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -415,10 +415,105 @@
|
|||||||
/* ================================================================
|
/* ================================================================
|
||||||
* Dark Mode — private Tokens überschreiben, öffentliche API bleibt stabil.
|
* Dark Mode — private Tokens überschreiben, öffentliche API bleibt stabil.
|
||||||
*
|
*
|
||||||
* data-theme="dark" wird durch das Head-Script in index.html gesetzt —
|
* Zwei Selektoren aktivieren Dark Mode:
|
||||||
* sowohl für manuelle Overrides als auch für System-Präferenz (via
|
* 1. @media (prefers-color-scheme: dark) — System-Präferenz (CSS-native,
|
||||||
* matchMedia-Listener). Ein einziger Selektor reicht, kein @media-Duplikat.
|
* kein JS erforderlich, funktioniert auch wenn matchMedia blockiert ist)
|
||||||
|
* 2. [data-theme="dark"] — expliziter Override durch den Nutzer
|
||||||
|
*
|
||||||
|
* Explizites Light ([data-theme="light"]) blockiert den @media-Block via :not().
|
||||||
|
* Explizites Dark ([data-theme="dark"]) überschreibt den @media-Block, falls OS
|
||||||
|
* auf Light steht.
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
/* Neutral-Skala invertiert (warm-dunkel) */
|
||||||
|
--_neutral-50: #1A1A18;
|
||||||
|
--_neutral-100: #222220;
|
||||||
|
--_neutral-150: #2A2A28;
|
||||||
|
--_neutral-200: #333331;
|
||||||
|
--_neutral-250: #3D3D3A;
|
||||||
|
--_neutral-300: #48484A;
|
||||||
|
--_neutral-400: #636360;
|
||||||
|
--_neutral-500: #8E8D89;
|
||||||
|
--_neutral-600: #AEADB0;
|
||||||
|
--_neutral-700: #C8C7C3;
|
||||||
|
--_neutral-800: #E2E1DC;
|
||||||
|
--_neutral-900: #F5F4F1;
|
||||||
|
--_neutral-950: #FAFAF8;
|
||||||
|
|
||||||
|
--_color-surface: #2A2A28;
|
||||||
|
--_color-surface-3: #333331;
|
||||||
|
|
||||||
|
--_sidebar-bg: #1A1A18;
|
||||||
|
--_sidebar-shadow-light: rgba(255, 255, 255, 0.04);
|
||||||
|
--_sidebar-shadow-dark: rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
--_color-accent: #818CF8;
|
||||||
|
--_color-accent-hover: #6366F1;
|
||||||
|
--_color-accent-active: #4F46E5;
|
||||||
|
--_color-accent-light: #2E2D5B;
|
||||||
|
--_color-accent-subtle: #252255;
|
||||||
|
--_color-btn-primary: #6366F1;
|
||||||
|
--_color-btn-primary-hover: #4F46E5;
|
||||||
|
--_color-accent-secondary: #A78BFA;
|
||||||
|
|
||||||
|
--_color-success: #4ADE80;
|
||||||
|
--_color-warning: #F59E0B;
|
||||||
|
--_color-danger: #FCA5A5;
|
||||||
|
--_color-text-tertiary: #A3A3A0;
|
||||||
|
--_color-success-light: #1A3325;
|
||||||
|
--_color-warning-light: #332400;
|
||||||
|
--_color-danger-light: #3D1C1A;
|
||||||
|
--_color-info-light: #1A2D40;
|
||||||
|
|
||||||
|
--_module-dashboard: #818CF8;
|
||||||
|
--_module-tasks: #4ADE80;
|
||||||
|
--_module-calendar: #A78BFA;
|
||||||
|
--_module-meals: #FB923C;
|
||||||
|
--_module-shopping: #F472B6;
|
||||||
|
--_module-notes: #FCD34D;
|
||||||
|
--_module-contacts: #60A5FA;
|
||||||
|
--_module-budget: #2DD4BF;
|
||||||
|
--_module-settings: #94A3B8;
|
||||||
|
|
||||||
|
--_meal-breakfast: #F59E0B;
|
||||||
|
--_meal-breakfast-light: #332400;
|
||||||
|
--_meal-lunch-light: #1A3325;
|
||||||
|
--_meal-dinner: #818CF8;
|
||||||
|
--_meal-dinner-light: #2E2D5B;
|
||||||
|
--_meal-snack-light: #3D2010;
|
||||||
|
|
||||||
|
--_color-priority-none-bg: rgba(142, 141, 137, 0.12);
|
||||||
|
--_color-priority-low-bg: rgba(142, 141, 137, 0.18);
|
||||||
|
--_color-priority-medium-bg: rgba(230, 147, 10, 0.18);
|
||||||
|
--_color-priority-high-bg: rgba(212, 81, 30, 0.18);
|
||||||
|
--_color-priority-urgent-bg: rgba(229, 83, 75, 0.18);
|
||||||
|
|
||||||
|
--_color-overlay: rgba(0, 0, 0, 0.6);
|
||||||
|
--_color-overlay-light: rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
|
--_shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||||
|
--_shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
|
||||||
|
--_shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
|
||||||
|
--_glass-bg: rgba(40, 40, 38, 0.75);
|
||||||
|
--_glass-bg-hover: rgba(50, 50, 48, 0.82);
|
||||||
|
--_glass-bg-elevated: rgba(58, 58, 55, 0.90);
|
||||||
|
--_glass-border: rgba(255, 255, 255, 0.12);
|
||||||
|
--_glass-border-subtle: rgba(255, 255, 255, 0.07);
|
||||||
|
--_glass-highlight: rgba(255, 255, 255, 0.10);
|
||||||
|
--_glass-highlight-subtle: rgba(255, 255, 255, 0.06);
|
||||||
|
--_glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.30), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||||
|
--_glass-shadow-md: 0 4px 20px rgba(0, 0, 0, 0.40), 0 0 0 1px rgba(255, 255, 255, 0.07);
|
||||||
|
--_glass-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||||
|
--_glass-bg-card: rgba(38, 38, 36, 0.50);
|
||||||
|
--_glass-bg-card-hover: rgba(48, 48, 46, 0.62);
|
||||||
|
--_glass-bg-input: rgba(34, 34, 32, 0.45);
|
||||||
|
--_glass-bg-toolbar: rgba(40, 40, 38, 0.55);
|
||||||
|
--_glass-tint-strength: 8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
/* Neutral-Skala invertiert (warm-dunkel) */
|
/* Neutral-Skala invertiert (warm-dunkel) */
|
||||||
--_neutral-50: #1A1A18;
|
--_neutral-50: #1A1A18;
|
||||||
|
|||||||
Reference in New Issue
Block a user