,
+) {
+ function OptionToView(option: [string, T]) {
+ const [text, value] = option;
+ return OptionButton(text, value, selection);
+ }
+
+ return (
+
+ );
+}
diff --git a/src/View/Modals/settingsModal.tsx b/src/View/Modals/settingsModal.tsx
index 03f1c4cf..e2de34d3 100644
--- a/src/View/Modals/settingsModal.tsx
+++ b/src/View/Modals/settingsModal.tsx
@@ -8,7 +8,9 @@ import { SplitModal } from "../Components/splitModal";
import { InfoTile } from "../Components/infoTile";
import { Option } from "../Components/option";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
-import { Languages } from "../../Model/Global/settingsModel";
+import { Languages, ThemeSettings } from "../../Model/Global/settingsModel";
+import { OptionButtonList } from "../Components/optionButtonList";
+import { stringify } from "../../Model/Utility/utility";
export function SettingsModal(
coreViewModel: CoreViewModel,
@@ -19,7 +21,10 @@ export function SettingsModal(
() => {
switch (settingsViewModel.selectedModalPage.value) {
case SettingsModalPages.Appearance:
- return Appearance
;
+ return SettingsAppearancePane(
+ coreViewModel,
+ settingsViewModel,
+ );
case SettingsModalPages.Regional:
return SettingsRegionalPane(
coreViewModel,
@@ -161,8 +166,7 @@ function SettingsRegionalPane(
Option(
languageNames[language],
language,
- language ==
- settingsViewModel.language.value,
+ language == settingsViewModel.language.value,
),
)}
@@ -172,3 +176,32 @@ function SettingsRegionalPane(
);
}
+
+function SettingsAppearancePane(
+ coreViewModel: CoreViewModel,
+ settingsViewModel: SettingsViewModel,
+) {
+ return (
+
+
{coreViewModel.translations.settings.pages.appearance}
+
+ {OptionButtonList(
+ new React.ListState<[string, ThemeSettings]>([
+ [
+ coreViewModel.translations.settings.themes.dark,
+ ThemeSettings.Dark,
+ ],
+ [
+ coreViewModel.translations.settings.themes.light,
+ ThemeSettings.Light,
+ ],
+ [
+ coreViewModel.translations.settings.themes.system,
+ ThemeSettings.System,
+ ],
+ ]),
+ settingsViewModel.theme,
+ )}
+
+ );
+}
diff --git a/src/View/translations.ts b/src/View/translations.ts
index 59946ca3..9331c9b4 100644
--- a/src/View/translations.ts
+++ b/src/View/translations.ts
@@ -80,6 +80,11 @@ const englishTranslations = {
regional: "Language & Region",
info: "About Comms",
},
+ themes: {
+ dark: "Dark",
+ light: "Light",
+ system: "Device theme",
+ },
version: "Version",
language: "Language",
@@ -375,6 +380,11 @@ export const allTranslations: {
regional: "Sprache & Region",
info: "Über Comms",
},
+ themes: {
+ dark: "Dunkel",
+ light: "Hell",
+ system: "Geräteeinstellung",
+ },
version: "Version",
language: "Sprache",
@@ -639,6 +649,11 @@ export const allTranslations: {
regional: "Idioma y Región",
info: "Sobre Comms",
},
+ themes: {
+ dark: "Oscuro",
+ light: "Claro",
+ system: "Según dispositivo",
+ },
version: "Versión",
language: "Idioma",
@@ -832,6 +847,6 @@ export const languageNames = {
en: "English",
de: "Deutsch",
es: "Español",
-}
+};
-export type Translations = typeof englishTranslations;
\ No newline at end of file
+export type Translations = typeof englishTranslations;
diff --git a/src/ViewModel/Global/settingsViewModel.ts b/src/ViewModel/Global/settingsViewModel.ts
index 4b4f2b53..488a684f 100644
--- a/src/ViewModel/Global/settingsViewModel.ts
+++ b/src/ViewModel/Global/settingsViewModel.ts
@@ -1,8 +1,7 @@
import * as React from "bloatless-react";
import CoreViewModel from "./coreViewModel";
-import SettingsModel from "../../Model/Global/settingsModel";
-import { Languages } from "../../View/translations";
+import { Languages, ThemeSettings } from "../../Model/Global/settingsModel";
export default class SettingsViewModel {
// state
@@ -15,7 +14,12 @@ export default class SettingsViewModel {
requiresReload = new React.State(false);
firstDayOfWeek: React.State = new React.State("0");
- language: React.State = new React.State(Languages.English);
+ language: React.State = new React.State(
+ Languages.English,
+ );
+ theme: React.State = new React.State(
+ ThemeSettings.System,
+ );
// guards
cannotSetName: React.State = React.createProxyState(
@@ -54,19 +58,49 @@ export default class SettingsViewModel {
this.selectedModalPage.value = page;
};
+ // view
+ applyTheme = (): void => {
+ let theme = this.theme.value;
+ if (theme == ThemeSettings.System) {
+ theme = SettingsViewModel.getSystemTheme();
+ }
+ document.body.setAttribute("theme", theme);
+ };
+
// init
constructor(public coreViewModel: CoreViewModel) {
this.username.value = coreViewModel.settingsModel.username;
this.usernameInput.value = coreViewModel.settingsModel.username;
this.firstDayOfWeek.value = coreViewModel.settingsModel.firstDayOfWeek;
this.language.value = coreViewModel.settingsModel.language;
+ this.theme.value = coreViewModel.settingsModel.theme;
// subscriptions
this.firstDayOfWeek.subscribe(this.setFirstDayofWeek);
this.language.subscribeSilent((newValue) => {
this.coreViewModel.settingsModel.setLanguage(newValue);
this.requiresReload.value = true;
- })
+ });
+ this.theme.subscribeSilent((newValue) => {
+ this.coreViewModel.settingsModel.setTheme(newValue);
+ });
+
+ // set theme
+ this.theme.subscribe(() => {
+ this.applyTheme();
+ });
+ SettingsViewModel.generateThemeMedia().addEventListener("change", () =>
+ this.applyTheme(),
+ );
+ }
+
+ static generateThemeMedia(): MediaQueryList {
+ return window.matchMedia("(prefers-color-scheme: dark)");
+ }
+
+ static getSystemTheme(): string {
+ const media = SettingsViewModel.generateThemeMedia();
+ return media.matches == true ? ThemeSettings.Dark : ThemeSettings.Light;
}
}
diff --git a/src/base.css b/src/base.css
index e20d6a98..d6089b0f 100644
--- a/src/base.css
+++ b/src/base.css
@@ -76,23 +76,21 @@ main {
);
}
-@media (prefers-color-scheme: dark) {
- #background-wrapper {
- opacity: 0.5;
- }
-
- #background-wrapper #sky {
- --color-1: hsl(188, 100%, 30%);
- --color-2: hsl(194, 72%, 24%);
- }
-
- #background-wrapper #grass-1 {
- --color-1: hsl(120, 30%, 30%);
- --color-2: hsl(120, 30%, 24%);
- }
-
- #background-wrapper #grass-2 {
- --color-1: hsl(110, 60%, 30%);
- --color-2: hsl(110, 50%, 24%);
- }
+body[theme="dark"] #background-wrapper {
+ opacity: 0.5;
+}
+
+body[theme="dark"] #background-wrapper #sky {
+ --color-1: hsl(188, 100%, 30%);
+ --color-2: hsl(194, 72%, 24%);
+}
+
+body[theme="dark"] #background-wrapper #grass-1 {
+ --color-1: hsl(120, 30%, 30%);
+ --color-2: hsl(120, 30%, 24%);
+}
+
+body[theme="dark"] #background-wrapper #grass-2 {
+ --color-1: hsl(110, 60%, 30%);
+ --color-2: hsl(110, 50%, 24%);
}
diff --git a/src/colors.css b/src/colors.css
index 0aac46ed..8b93e054 100644
--- a/src/colors.css
+++ b/src/colors.css
@@ -12,15 +12,13 @@
--surface-active: hsla(var(--hue), 100%, 5%, 15%);
}
-@media (prefers-color-scheme: dark) {
- [color]:not([color="standard"]) {
- --accent-luma: var(--accent-luma-dark);
- --accent-text-luma: var(--accent-text-luma-dark);
+body[theme="dark"] [color]:not([color="standard"]) {
+ --accent-luma: var(--accent-luma-dark);
+ --accent-text-luma: var(--accent-text-luma-dark);
- --surface: hsla(var(--hue), 100%, 95%, 5%);
- --surface-hover: hsla(var(--hue), 100%, 95%, 10%);
- --surface-active: hsla(var(--hue), 100%, 95%, 15%);
- }
+ --surface: hsla(var(--hue), 100%, 95%, 5%);
+ --surface-hover: hsla(var(--hue), 100%, 95%, 10%);
+ --surface-active: hsla(var(--hue), 100%, 95%, 15%);
}
/* tile */
@@ -44,16 +42,14 @@
--text-normal: hsl(var(--hue), 100%, var(--text-luma));
}
-@media (prefers-color-scheme: dark) {
- [color]:not([color="standard"]).tile {
- --gradient-luma-1: 20%;
- --gradient-luma-2: 15%;
+body[theme="dark"] [color]:not([color="standard"]).tile {
+ --gradient-luma-1: 20%;
+ --gradient-luma-2: 15%;
- --hover-luma: 25%;
- --active-luma: 30%;
+ --hover-luma: 25%;
+ --active-luma: 30%;
- --text-luma: 90%;
- }
+ --text-luma: 90%;
}
/* fill */
@@ -74,11 +70,9 @@
background: hsl(var(--hue), var(--sat), var(--luma), 40%);
}
- @media (prefers-color-scheme: dark) {
- [color]:not([color="standard"]).background {
- --luma: var(--background-luma-dark);
- --sat: 100%;
- }
+ body[theme="dark"] [color]:not([color="standard"]).background {
+ --luma: var(--background-luma-dark);
+ --sat: 100%;
}
[color="standard"].background {
@@ -92,15 +86,13 @@
background: hsl(var(--hue), 100%, var(--luma), 50%);
}
-@media (prefers-color-scheme: dark) {
- [color]:not([color="standard"]).subtle-background {
- --luma: var(--subtle-luma-dark);
- }
+body[theme="dark"] [color]:not([color="standard"]).subtle-background {
+ --luma: var(--subtle-luma-dark);
+}
- @media (display-mode: standalone) {
- [color]:not([color="standard"]).subtle-background {
- --luma: 0%;
- }
+@media (display-mode: standalone) {
+ [color]:not([color="standard"]).subtle-background {
+ --luma: 0%;
}
}