diff --git a/frontend/css/style.css b/frontend/css/style.css index 5aac33bd4..a37c7d236 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -1,4 +1,7 @@ +/* Default (light) theme if no system override is detected, or system is light */ :root { + color-scheme: light; + /* Fonts */ --font-family: "Fira Sans", sans-serif; --font-family-monospaced: "Fira Mono", monospace; @@ -107,11 +110,12 @@ --help-sidebar-border: #eaeaea; } +/* If user has no manual selection (no data-theme) AND system is dark, use these */ @media (prefers-color-scheme: dark) { - :root { + :root:not([data-theme]) { color-scheme: dark; - /* Base colors */ + /* Base colors (dark) */ --heading-color: #d7dce2; --text-color: hsl(0deg 0% 75%); --text-color-darker: hsl(0deg 0% 25%); @@ -167,7 +171,7 @@ /* Query Editor */ --bql-keywords: #c678dd; --bql-values: #98c379; - --bql-string: #ee5e5e; /* #e5c07b; */ + --bql-string: #ee5e5e; --bql-errors: var(--text-color-lighter); /* Misc */ @@ -186,6 +190,164 @@ } } +/* Explicit Light Mode (when data-theme="light") */ +:root[data-theme="light"] { + color-scheme: light; + + /* Base colors */ + --heading-color: #333; + --text-color: hsl(0deg 0% 27%); + --text-color-lighter: hsl(0deg 0% 33%); + --text-color-lightest: hsl(0deg 0% 47%); + --link-color: hsl(203deg 100% 32%); + --link-hover-color: hsl(0deg 0% 33%); + --code-background: hsl(0deg 0% 97%); + --background: #fff; + --background-darker: hsl(0deg 0% 85%); + --border: hsl(0deg 0% 85%); + --border-darker: hsl(0deg 0% 80%); + + /* Box shadows */ + --box-shadow-button: 0 0 5px hsl(0deg 0% 50% / 50%); + --box-shadow-dropdown: 3px 3px 3px hsl(0deg 0% 50% / 50%); + --box-shadow-kbd: inset 0 -1px 0 var(--border-darker); + --box-shadow-overlay: 0 0 20px var(--overlay-wrapper-background); + + /* Sidebar */ + --sidebar-color: #444; + --sidebar-hover-color: var(--link-color); + --sidebar-background: hsl(0deg 0% 96%); + --sidebar-border: hsl(0deg 0% 87%); + + /* Details */ + --summary-background: hsl(0deg 0% 95%); + --summary-background-darker: hsl(0deg 0% 90%); + + /* Header */ + --header-color: #fff; + --header-background: hsl(203deg 100% 32%); + --header-placeholder-color: hsl(203deg 47% 66%); + --header-placeholder-background: hsl(203deg 56% 45%); + + /* Tables */ + --table-header-text: hsl(0deg 0% 40%); + --table-header-background: hsl(0deg 0% 90%); + --table-border: hsl(0deg 0% 90%); + --table-background-even: hsl(0deg 0% 95%); + + /* Editor elements. */ + --editor-activeline: #cef4; + --editor-selectionmatch: hsl(105deg 100% 73% / 50%); + --editor-account: var(--link-color); + --editor-class: #b84; + --editor-comment: #998; + --editor-constant: #008080; + --editor-currencies: #708; + --editor-date: #099; + --editor-directive: #333; + --editor-invalid-background: rgb(255 199 199 / 50%); + --editor-invalid: #333; + --editor-label-name: #221198; + --editor-number: #116543; + --editor-string: #a91111; + + /* Query Editor */ + --bql-keywords: #708; + --bql-values: #085; + --bql-string: #a11; + --bql-errors: #000; + + /* Misc */ + --placeholder-color: var(--text-color-lightest); + --placeholder-background: var(--background); + --mobile-button-text: #000; + --overlay-wrapper-background: rgb(0 0 0 / 50%); + + /* Help pages */ + --help-sidebar-background: #f8f8f8; + --help-sidebar-border: #eaeaea; +} + +/* Explicit Dark Mode (when data-theme="dark") */ +:root[data-theme="dark"] { + color-scheme: dark; + + /* Base colors */ + --heading-color: #d7dce2; + --text-color: hsl(0deg 0% 75%); + --text-color-darker: hsl(0deg 0% 25%); + --text-color-lighter: hsl(0deg 0% 85%); + --link-color: hsl(203deg 100% 70%); + --link-hover-color: hsl(0deg 0% 45%); + --code-background: hsl(0deg 0% 25%); + --background: hsl(200deg 6% 15%); + --background-darker: hsl(200deg 5% 30%); + --border: hsl(0deg 0% 35%); + --border-darker: hsl(0deg 0% 30%); + + /* Box shadows */ + --box-shadow-dropdown: 3px 3px 3px hsl(0deg 0% 25% / 50%); + + /* Sidebar */ + --sidebar-background: hsl(200deg 5% 18%); + --sidebar-color: hsl(0deg 0% 73%); + --sidebar-border: hsl(200deg 5% 25%); + + /* Details */ + --summary-background: hsl(0deg 0% 25%); + --summary-background-darker: hsl(0deg 0% 20%); + + /* Header */ + --header-color: #fff; + --header-background: hsl(203deg 100% 25%); + + /* Tables */ + --table-header-text: hsl(0deg 0% 80%); + --table-header-background: var(--sidebar-background); + --table-border: var(--sidebar-border); + --table-background-even: hsl(0deg 0% 18%); + + /* Editor elements. */ + --editor-activeline: #44535b44; + --editor-selectionmatch: hsl(105deg 100% 30% / 50%); + --editor-account: var(--link-color); + --editor-class: #e1a759; + --editor-comment: #998; + --editor-constant: #02a0a0; + --editor-currencies: #cd00e8; + --editor-date: #0ad3d3; + --editor-directive: #c6c6c6; + --editor-invalid-background: rgb(176 82 82 / 50%); + --editor-invalid: #d5c5c5; + --editor-label-name: #9c90f6; + --editor-number: #00b672; + --editor-string: #e87f7f; + + /* Query Editor */ + --bql-keywords: #c678dd; + --bql-values: #98c379; + --bql-string: #ee5e5e; + --bql-errors: var(--text-color-lighter); + + /* Misc */ + --placeholder-color: var(--text-color-lighter); + --placeholder-background: hsl(222deg 7% 29%); + --mobile-button-text: #cacaca; + --overlay-wrapper-background: rgb(0 0 0 / 50%); + + /* Help pages */ + --help-sidebar-background: #3b3b3b; + --help-sidebar-border: #2a2a2a; + + input, + textarea { + background: var(--placeholder-background); + } +} + +/* ---------------------------------------------------------------------------- + Journal classes for default or light mode + -------------------------------------------------------------------------- */ .journal .balance { --entry-background: hsl(120deg 100% 90%); } @@ -240,55 +402,104 @@ --journal-hover-highlight: hsl(0deg 0% 90% / 60%); } +/* Fallback for system dark (no data-theme) - override journal colors */ @media (prefers-color-scheme: dark) { - :root { - .journal .balance { - --entry-background: hsl(120deg 50% 15%); - } + :root:not([data-theme]) .journal .balance { + --entry-background: hsl(120deg 50% 15%); + } - .journal .close { - --entry-background: hsl(0deg 0% 15%); - } + :root:not([data-theme]) .journal .close { + --entry-background: hsl(0deg 0% 15%); + } - .journal .custom { - --entry-background: hsl(52deg 100% 15%); - } + :root:not([data-theme]) .journal .custom { + --entry-background: hsl(52deg 100% 15%); + } - .journal .document { - --entry-background: hsl(300deg 45% 25%); - } + :root:not([data-theme]) .journal .document { + --entry-background: hsl(300deg 45% 25%); + } - .journal .note { - --entry-background: hsl(212deg 43% 25%); - } + :root:not([data-theme]) .journal .note { + --entry-background: hsl(212deg 43% 25%); + } - .journal .open { - --entry-background: hsl(0deg 0% 20%); - } + :root:not([data-theme]) .journal .open { + --entry-background: hsl(0deg 0% 20%); + } - .journal .other { - --entry-background: hsl(180deg 100% 25%); - } + :root:not([data-theme]) .journal .other { + --entry-background: hsl(180deg 100% 25%); + } - .journal .pad { - --entry-background: hsl(180deg 100% 15%); - } + :root:not([data-theme]) .journal .pad { + --entry-background: hsl(180deg 100% 15%); + } - .journal .pending { - --entry-background: hsl(343deg 60% 20%); - } + :root:not([data-theme]) .journal .pending { + --entry-background: hsl(343deg 60% 20%); + } - .journal .query { - --entry-background: hsl(213deg 100% 25%); - } + :root:not([data-theme]) .journal .query { + --entry-background: hsl(213deg 100% 25%); + } - .journal .budget { - --entry-background: hsl(35deg 100% 20%); - } + :root:not([data-theme]) .journal .budget { + --entry-background: hsl(35deg 100% 20%); + } - .journal { - --journal-postings: hsl(0deg 0% 10%); - --journal-hover-highlight: hsl(0deg 0% 20% / 60%); - } + :root:not([data-theme]) .journal { + --journal-postings: hsl(0deg 0% 10%); + --journal-hover-highlight: hsl(0deg 0% 20% / 60%); } } + +/* Explicit dark mode overrides for journal classes */ +:root[data-theme="dark"] .journal .balance { + --entry-background: hsl(120deg 50% 15%); +} + +:root[data-theme="dark"] .journal .close { + --entry-background: hsl(0deg 0% 15%); +} + +:root[data-theme="dark"] .journal .custom { + --entry-background: hsl(52deg 100% 15%); +} + +:root[data-theme="dark"] .journal .document { + --entry-background: hsl(300deg 45% 25%); +} + +:root[data-theme="dark"] .journal .note { + --entry-background: hsl(212deg 43% 25%); +} + +:root[data-theme="dark"] .journal .open { + --entry-background: hsl(0deg 0% 20%); +} + +:root[data-theme="dark"] .journal .other { + --entry-background: hsl(180deg 100% 25%); +} + +:root[data-theme="dark"] .journal .pad { + --entry-background: hsl(180deg 100% 15%); +} + +:root[data-theme="dark"] .journal .pending { + --entry-background: hsl(343deg 60% 20%); +} + +:root[data-theme="dark"] .journal .query { + --entry-background: hsl(213deg 100% 25%); +} + +:root[data-theme="dark"] .journal .budget { + --entry-background: hsl(35deg 100% 20%); +} + +:root[data-theme="dark"] .journal { + --journal-postings: hsl(0deg 0% 10%); + --journal-hover-highlight: hsl(0deg 0% 20% / 60%); +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 0344f5fff..fe25b5365 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -135,6 +135,14 @@ function init(): void { }); router.trigger("page-loaded"); + + + const storedTheme = localStorage.getItem("theme") ?? "auto"; + if (storedTheme === "auto") { + document.documentElement.removeAttribute("data-theme"); + } else { + document.documentElement.setAttribute("data-theme", storedTheme); + } } init(); diff --git a/frontend/src/reports/options/Options.svelte b/frontend/src/reports/options/Options.svelte index 876576e1b..e0efb05fa 100644 --- a/frontend/src/reports/options/Options.svelte +++ b/frontend/src/reports/options/Options.svelte @@ -2,11 +2,17 @@ import { urlFor } from "../../helpers"; import { _ } from "../../i18n"; import OptionsTable from "./OptionsTable.svelte"; + import ThemeToggle from "./ThemeToggle.svelte"; export let fava_options: Record; export let beancount_options: Record; +

+ {_("Theme style")} +

+ +

{_("Fava options")} ({_("help")}) diff --git a/frontend/src/reports/options/ThemeToggle.svelte b/frontend/src/reports/options/ThemeToggle.svelte new file mode 100644 index 000000000..cb665d029 --- /dev/null +++ b/frontend/src/reports/options/ThemeToggle.svelte @@ -0,0 +1,89 @@ + + +
+ + + + + +
+ + diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts new file mode 100644 index 000000000..d0bce9e7d --- /dev/null +++ b/frontend/src/stores/theme.ts @@ -0,0 +1,28 @@ +import { writable } from 'svelte/store'; + +function getInitialTheme() { + const savedTheme = localStorage.getItem('theme'); + if (savedTheme !== null) { + return savedTheme; + } + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +export const theme = writable(getInitialTheme()); + +// Subscribe to theme changes +theme.subscribe((value) => { + if (typeof document !== 'undefined') { + document.documentElement.setAttribute('data-theme', value); + } +}); + +// Listen for system theme changes +if (typeof window !== 'undefined') { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (localStorage.getItem('theme') === null) { + // Only update if user hasn't manually set a theme + theme.set(e.matches ? 'dark' : 'light'); + } + }); +}