Skip to content

Commit

Permalink
feat: add light/dark mode switch (beancount#1874)
Browse files Browse the repository at this point in the history
  • Loading branch information
nerd4me committed Oct 27, 2024
1 parent bb91cd4 commit a113ed0
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 125 deletions.
208 changes: 83 additions & 125 deletions frontend/css/style.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
:root {
:root,
.light-theme {
/* Fonts */
--font-family: "Fira Sans", sans-serif;
--font-family-monospaced: "Fira Mono", monospace;
Expand Down Expand Up @@ -107,82 +108,86 @@
--help-sidebar-border: #eaeaea;
}

.dark-theme {
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 */
--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; /* #e5c07b; */
--bql-errors: var(--text-color-lighter);

/* Misc */
--placeholder-color: var(--text-color-lighter);
--placeholder-background: hsl(222deg 7% 29%);
--mobile-button-text: #cacaca;

/* Help pages */
--help-sidebar-background: #3b3b3b;
--help-sidebar-border: #2a2a2a;

input,
textarea {
background: var(--placeholder-background);
}
}

@media (prefers-color-scheme: dark) {
:root {
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 */
--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; /* #e5c07b; */
--bql-errors: var(--text-color-lighter);

/* Misc */
--placeholder-color: var(--text-color-lighter);
--placeholder-background: hsl(222deg 7% 29%);
--mobile-button-text: #cacaca;

/* Help pages */
--help-sidebar-background: #3b3b3b;
--help-sidebar-border: #2a2a2a;

input,
textarea {
background: var(--placeholder-background);
}
:root:not(.light-theme):not(.dark-theme) {
/* ... existing dark theme variables ... */
}
}

Expand Down Expand Up @@ -241,54 +246,7 @@
}

@media (prefers-color-scheme: dark) {
:root {
.journal .balance {
--entry-background: hsl(120deg 50% 15%);
}

.journal .close {
--entry-background: hsl(0deg 0% 15%);
}

.journal .custom {
--entry-background: hsl(52deg 100% 15%);
}

.journal .document {
--entry-background: hsl(300deg 45% 25%);
}

.journal .note {
--entry-background: hsl(212deg 43% 25%);
}

.journal .open {
--entry-background: hsl(0deg 0% 20%);
}

.journal .other {
--entry-background: hsl(180deg 100% 25%);
}

.journal .pad {
--entry-background: hsl(180deg 100% 15%);
}

.journal .pending {
--entry-background: hsl(343deg 60% 20%);
}

.journal .query {
--entry-background: hsl(213deg 100% 25%);
}

.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(.light-theme):not(.dark-theme) {
/* ... existing dark theme variables ... */
}
}
79 changes: 79 additions & 0 deletions frontend/src/components/ThemeSwitch.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script lang="ts">
import { themeStore } from "../stores/theme";
const icons = {
light: `<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
</svg>`,
dark: `<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M9.37 5.51A7.35 7.35 0 0 0 9.1 7.5c0 4.08 3.32 7.4 7.4 7.4.68 0 1.35-.09 1.99-.27A7.014 7.014 0 0 1 12 19c-3.86 0-7-3.14-7-7 0-2.93 1.81-5.45 4.37-6.49zM12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/>
</svg>`
};
</script>

<div class="theme-switch">
{#each [...themeStore.values()] as [option, name]}
<button
class="theme-button"
class:active={$themeStore === option}
on:click={() => themeStore.set(option)}
title={name}
>
{@html icons[option]}
</button>
{/each}
</div>

<style>
.theme-switch {
display: flex;
gap: 2px;
padding: 3px;
margin-right: 0.5em;
background: var(--background-darker);
border-radius: 8px;
}
.theme-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
color: var(--text-color-lighter);
cursor: pointer;
background: transparent;
border: none;
border-radius: 6px;
transition: all 0.2s ease;
}
.theme-button:hover {
color: var(--text-color);
background: var(--background);
}
.theme-button.active {
color: var(--header-color);
background: var(--header-background);
}
/* Ensure SVG icons inherit color properly */
.theme-button :global(svg) {
width: 18px;
height: 18px;
}
@media (width <= 768px) {
.theme-button {
width: 28px;
height: 28px;
}
.theme-button :global(svg) {
width: 16px;
height: 16px;
}
}
</style>
2 changes: 2 additions & 0 deletions frontend/src/sidebar/FilterForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { _ } from "../i18n";
import { accounts, links, payees, tags, years } from "../stores";
import { account_filter, fql_filter, time_filter } from "../stores/filters";
import ThemeSwitch from "../components/ThemeSwitch.svelte";
$: fql_filter_suggestions = [
...$tags.map((tag) => `#${tag}`),
Expand Down Expand Up @@ -57,6 +58,7 @@
</script>

<form on:submit|preventDefault={submit}>
<ThemeSwitch />
<AutocompleteInput
bind:value={time_filter_value}
placeholder={_("Time")}
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/stores/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { constants } from "../lib/validation";
import { localStorageSyncedStore } from "../lib/store";
import type { ValidationT } from "../lib/validation";

const theme_validator = constants("light", "dark");
type Theme = ValidationT<typeof theme_validator>;

export const themeStore = localStorageSyncedStore<Theme>(
"theme",
theme_validator,
() => "dark", // Set dark as default
() => [
["light", "Light"],
["dark", "Dark"],
],
);

// Apply theme class to document
themeStore.subscribe((theme: Theme) => {
if (theme === "light") {
document.documentElement.classList.remove("dark-theme");
document.documentElement.classList.add("light-theme");
} else {
document.documentElement.classList.remove("light-theme");
document.documentElement.classList.add("dark-theme");
}
});

0 comments on commit a113ed0

Please sign in to comment.