From 37c8fe0bf4aa0d29cb0da5aa6f34b3e0e7640b59 Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Fri, 17 Oct 2025 23:36:04 +0530 Subject: [PATCH] feat: Add themes and allow cycling thru them --- README.md | 2 +- blank.json | 2 +- src/config.ts | 4 +-- src/editor/commands/cycleTheme.test.ts | 35 +++++++++++++++++++++++++ src/editor/commands/cycleTheme.ts | 10 +++++++ src/editor/commands/index.ts | 2 +- src/editor/commands/toggleTheme.test.ts | 35 ------------------------- src/editor/commands/toggleTheme.ts | 8 ------ src/editor/plugins/keymap.ts | 4 +-- src/scss/themes/_black.scss | 8 ++++++ src/scss/themes/_blue.scss | 8 ++++++ src/scss/themes/_green.scss | 8 ++++++ src/scss/themes/_index.scss | 4 +++ src/scss/themes/_red.scss | 8 ++++++ src/state.ts | 13 +++++++-- src/storage.ts | 6 +++-- 16 files changed, 103 insertions(+), 54 deletions(-) create mode 100644 src/editor/commands/cycleTheme.test.ts create mode 100644 src/editor/commands/cycleTheme.ts delete mode 100644 src/editor/commands/toggleTheme.test.ts delete mode 100644 src/editor/commands/toggleTheme.ts create mode 100644 src/scss/themes/_black.scss create mode 100644 src/scss/themes/_blue.scss create mode 100644 src/scss/themes/_green.scss create mode 100644 src/scss/themes/_red.scss diff --git a/README.md b/README.md index 3409d68..67a977a 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ In order to change the keyboard bindings copy your modified version of the [defa | Save as | Mod + Shift + S | | Open File | Mod + O | | Export as PDF | Mod + Alt + P | -| Toggle Theme (Dark / Light) | Mod + Alt + T | +| Cycle through themes | Mod + Alt + T | | Undo | Mod + Z | | Redo | Mod + Shift + Z | | Insert line break | Mod + Enter / Shift + Enter | diff --git a/blank.json b/blank.json index 6e56b42..c569297 100644 --- a/blank.json +++ b/blank.json @@ -23,6 +23,6 @@ "file.save_as": "Mod-Shift-s", "file.open": "Mod-o", "export.pdf": "Mod-Alt-p", - "theme.toggle": "Mod-Alt-t" + "theme.cycle": "Mod-Alt-t" } } diff --git a/src/config.ts b/src/config.ts index 2fc49be..b1173d7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,7 +26,7 @@ export enum CommandIdentifier { FILE_SAVE_AS = "file.save_as", FILE_OPEN = "file.open", EXPORT_PDF = "export.pdf", - THEME_TOGGLE = "theme.toggle", + THEME_CYCLE = "theme.cycle", } export interface Config { @@ -58,7 +58,7 @@ const defaultConfig: Config = { [CommandIdentifier.FILE_SAVE_AS]: "Mod-Shift-s", [CommandIdentifier.FILE_OPEN]: "Mod-o", [CommandIdentifier.EXPORT_PDF]: "Mod-Alt-p", - [CommandIdentifier.THEME_TOGGLE]: "Mod-Alt-t", + [CommandIdentifier.THEME_CYCLE]: "Mod-Alt-t", }, }; diff --git a/src/editor/commands/cycleTheme.test.ts b/src/editor/commands/cycleTheme.test.ts new file mode 100644 index 0000000..ca7b971 --- /dev/null +++ b/src/editor/commands/cycleTheme.test.ts @@ -0,0 +1,35 @@ +import { afterEach, assert, beforeEach, describe, expect, it } from "vitest"; +import { EditorState } from "prosemirror-state"; + +import { clearMocks, mockWindows } from "@tauri-apps/api/mocks"; + +import { cycleTheme } from "."; +import { theme, themes } from "../../state"; + +describe("command.cycleTheme", () => { + beforeEach(() => { + mockWindows("main"); + theme.value = themes[0]; + }); + + afterEach(() => { + clearMocks(); + }); + + it("cycles", () => { + expect(window).toBeDefined(); + + const bodyTheme = () => document.body.dataset.theme; + + themes.forEach((currentTheme, index) => { + assert.equal(theme.value, currentTheme); + assert.equal(bodyTheme(), currentTheme); + + cycleTheme()(new EditorState()); + + const nextTheme = themes[(index + 1) % themes.length]; + assert.equal(theme.value, nextTheme); + assert.equal(bodyTheme(), nextTheme); + }); + }); +}); diff --git a/src/editor/commands/cycleTheme.ts b/src/editor/commands/cycleTheme.ts new file mode 100644 index 0000000..5006222 --- /dev/null +++ b/src/editor/commands/cycleTheme.ts @@ -0,0 +1,10 @@ +import type { Command } from "prosemirror-state"; + +import { theme, themes } from "../../state"; + +export default (): Command => () => { + const currentIndex = themes.indexOf(theme.value); + const nextIndex = (currentIndex + 1) % themes.length; + theme.value = themes[nextIndex]; + return true; +}; diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index fac9ffd..42390f2 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -3,4 +3,4 @@ export { default as insertNode } from "./insertNode"; export { default as newFile } from "./newFile"; export { default as openFile } from "./openFile"; export { default as saveFile } from "./saveFile"; -export { default as toggleTheme } from "./toggleTheme"; +export { default as cycleTheme } from "./cycleTheme"; diff --git a/src/editor/commands/toggleTheme.test.ts b/src/editor/commands/toggleTheme.test.ts deleted file mode 100644 index bf60653..0000000 --- a/src/editor/commands/toggleTheme.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { afterEach, assert, beforeEach, describe, expect, it } from "vitest"; -import { EditorState } from "prosemirror-state"; - -import { clearMocks, mockWindows } from "@tauri-apps/api/mocks"; - -import { toggleTheme } from "."; -import { theme } from "../../state"; - -describe("command.toggleTheme", () => { - beforeEach(() => { - mockWindows("main"); - }); - - afterEach(() => { - clearMocks(); - }); - - it("toggles", () => { - expect(window).toBeDefined(); - - const bodyTheme = () => document.body.dataset.theme; - assert.equal(theme.value, "dark"); - assert.equal(bodyTheme(), undefined); - - toggleTheme()(new EditorState()); - - assert.equal(theme.value, "light"); - assert.equal(bodyTheme(), "light"); - - toggleTheme()(new EditorState()); - - assert.equal(theme.value, "dark"); - assert.equal(bodyTheme(), "dark"); - }); -}); diff --git a/src/editor/commands/toggleTheme.ts b/src/editor/commands/toggleTheme.ts deleted file mode 100644 index 84d2791..0000000 --- a/src/editor/commands/toggleTheme.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Command } from "prosemirror-state"; - -import { theme } from "../../state"; - -export default (): Command => () => { - theme.value = theme.value === "dark" ? "light" : "dark"; - return true; -}; diff --git a/src/editor/plugins/keymap.ts b/src/editor/plugins/keymap.ts index 73cbf24..4cb8cb9 100644 --- a/src/editor/plugins/keymap.ts +++ b/src/editor/plugins/keymap.ts @@ -20,7 +20,7 @@ import { openFile, saveFile, exportAs, - toggleTheme, + cycleTheme, insertNode, } from "../commands"; @@ -72,7 +72,7 @@ const commandMap: { [key in CommandIdentifier]: Command } = { [CommandIdentifier.EXPORT_PDF]: exportAs("PDF-Export", exporters.toPDF, [ { name: "PDF-File", extensions: ["pdf"] }, ]), - [CommandIdentifier.THEME_TOGGLE]: toggleTheme(), + [CommandIdentifier.THEME_CYCLE]: cycleTheme(), }; export const keymap = () => diff --git a/src/scss/themes/_black.scss b/src/scss/themes/_black.scss new file mode 100644 index 0000000..2a642b2 --- /dev/null +++ b/src/scss/themes/_black.scss @@ -0,0 +1,8 @@ +[data-theme='black'] { + --color: #f4f4f2; + --background-color: #0a0a0f; + --color-inverted: #0a0a0f; + --background-inverted: #f4f4f2; + --font-family: 'DejaVu Sans', system-ui, -apple-system, 'Segoe UI', 'Roboto', + 'Ubuntu', 'Cantarell', 'Noto Sans', sans-serif; +} \ No newline at end of file diff --git a/src/scss/themes/_blue.scss b/src/scss/themes/_blue.scss new file mode 100644 index 0000000..0e37d63 --- /dev/null +++ b/src/scss/themes/_blue.scss @@ -0,0 +1,8 @@ +[data-theme='blue'] { + --color: #fbc572; + --background-color: #25324c; + --color-inverted: #25324c; + --background-inverted: #fbc572; + --font-family: 'DejaVu Sans', system-ui, -apple-system, 'Segoe UI', 'Roboto', + 'Ubuntu', 'Cantarell', 'Noto Sans', sans-serif; +} \ No newline at end of file diff --git a/src/scss/themes/_green.scss b/src/scss/themes/_green.scss new file mode 100644 index 0000000..adbd8ac --- /dev/null +++ b/src/scss/themes/_green.scss @@ -0,0 +1,8 @@ +[data-theme='green'] { + --color: #c6c7b3; + --background-color: #323f37; + --color-inverted: #323f37; + --background-inverted: #c6c7b3; + --font-family: 'DejaVu Sans', system-ui, -apple-system, 'Segoe UI', 'Roboto', + 'Ubuntu', 'Cantarell', 'Noto Sans', sans-serif; +} \ No newline at end of file diff --git a/src/scss/themes/_index.scss b/src/scss/themes/_index.scss index 586ac03..c0ad1be 100644 --- a/src/scss/themes/_index.scss +++ b/src/scss/themes/_index.scss @@ -1,5 +1,9 @@ @use 'light'; @use 'dark'; +@use 'black'; +@use 'red'; +@use 'green'; +@use 'blue'; body { color: var(--color); diff --git a/src/scss/themes/_red.scss b/src/scss/themes/_red.scss new file mode 100644 index 0000000..fc21594 --- /dev/null +++ b/src/scss/themes/_red.scss @@ -0,0 +1,8 @@ +[data-theme='red'] { + --color: #ede1c5; + --background-color: #532728; + --color-inverted: #daa9aa; + --background-inverted: #2f1414; + --font-family: 'DejaVu Sans', system-ui, -apple-system, 'Segoe UI', 'Roboto', + 'Ubuntu', 'Cantarell', 'Noto Sans', sans-serif; +} \ No newline at end of file diff --git a/src/state.ts b/src/state.ts index f8810f9..fbef2af 100644 --- a/src/state.ts +++ b/src/state.ts @@ -29,8 +29,17 @@ transaction.subscribe( }, 50), ); -export type themeType = "light" | "dark"; -export const theme = new Observable("dark"); +export const themes: string[] = [ + "light", + "dark", + "black", + "red", + "green", + "blue", +]; + +export type themeType = (typeof themes)[number]; +export const theme = new Observable("light"); theme.subscribe((value: themeType) => { document.body.dataset.theme = value; }); diff --git a/src/storage.ts b/src/storage.ts index c5aa910..30e5541 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -3,7 +3,7 @@ import { Transaction } from "prosemirror-state"; import { Node } from "prosemirror-model"; import { debounce } from "observable.ts"; -import { path, transaction, theme, themeType } from "./state"; +import { path, transaction, theme, themeType, themes } from "./state"; import { schema } from "prosemirror-markdown"; localforage.config({ @@ -19,7 +19,9 @@ export const bootStorage = async () => { }); const _theme = await localforage.getItem("theme"); - theme.value = _theme === "dark" ? "dark" : "light"; + theme.value = themes.includes(_theme as string) + ? (_theme as themeType) + : themes[0]; theme.subscribe((value: themeType) => { localforage.setItem("theme", value).catch(console.warn);