From 881464409784ef5ab693516072025363089971e4 Mon Sep 17 00:00:00 2001 From: xKesvaL Date: Sun, 7 Sep 2025 19:58:03 +0200 Subject: [PATCH 1/3] feat: add "persist" option to setMode --- docs/src/content/utilities/set-mode.md | 26 ++++++++++++++++++- packages/mode-watcher/src/lib/mode.ts | 18 ++++++++++--- .../mode-watcher/src/lib/states.svelte.ts | 16 +++++++++--- packages/mode-watcher/src/routes/+page.svelte | 6 +++++ 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/docs/src/content/utilities/set-mode.md b/docs/src/content/utilities/set-mode.md index c044270..3509412 100644 --- a/docs/src/content/utilities/set-mode.md +++ b/docs/src/content/utilities/set-mode.md @@ -4,11 +4,15 @@ description: Sets the current mode to "light", "dark", or "system". section: Utilities --- + + `setMode` is a function that updates the user's preferred mode. It accepts one of three string values: `"light"`, `"dark"`, or `"system"`. -This updates both the visual mode and the persisted preference in `localStorage`. +This updates both the visual mode and the persisted preference in `localStorage`. You can also pass it options to customize the behavior, such as wether or not to persist it to `localStorage`. ## Usage @@ -20,3 +24,23 @@ This updates both the visual mode and the persisted preference in `localStorage` ``` + +You can also not persist the mode to `localStorage` by passing the `persist` option as `false`. This can be useful if you need to set the mode to a specific value for a specific part of your website. (ex: marketing part vs docs part, where in marketing part you want to set the mode to `light` and in docs part you want the user to choose the mode). + +```svelte + + + +``` + +## Options + +The `setMode` utility accepts the following options: + + + Whether to persist the theme to `localStorage`. + diff --git a/packages/mode-watcher/src/lib/mode.ts b/packages/mode-watcher/src/lib/mode.ts index 818b43f..bf1825d 100644 --- a/packages/mode-watcher/src/lib/mode.ts +++ b/packages/mode-watcher/src/lib/mode.ts @@ -1,20 +1,32 @@ import { userPrefersMode } from "./mode-states.svelte.js"; import { customTheme } from "./theme-state.svelte.js"; -import { derivedMode } from "./states.svelte.js"; +import { derivedMode, sessionUserPrefersMode } from "./states.svelte.js"; import type { Mode, ThemeColors } from "./types.js"; /** Toggle between light and dark mode */ export function toggleMode(): void { userPrefersMode.current = derivedMode.current === "dark" ? "light" : "dark"; + sessionUserPrefersMode.current = undefined; } +export type SetModeOptions = { + persist?: boolean; +}; + /** Set the mode to light or dark */ -export function setMode(mode: Mode): void { - userPrefersMode.current = mode; +export function setMode(mode: Mode, options?: SetModeOptions): void { + const persist = options?.persist ?? true; + if (persist) { + sessionUserPrefersMode.current = undefined; + userPrefersMode.current = mode; + } else { + sessionUserPrefersMode.current = mode; + } } /** Reset the mode to operating system preference */ export function resetMode(): void { + sessionUserPrefersMode.current = undefined; userPrefersMode.current = "system"; } diff --git a/packages/mode-watcher/src/lib/states.svelte.ts b/packages/mode-watcher/src/lib/states.svelte.ts index 5660c4c..0884378 100644 --- a/packages/mode-watcher/src/lib/states.svelte.ts +++ b/packages/mode-watcher/src/lib/states.svelte.ts @@ -1,6 +1,6 @@ import { box } from "svelte-toolbelt"; import { isBrowser, sanitizeClassNames } from "./utils.js"; -import type { ThemeColors } from "./types.js"; +import type { Mode, ThemeColors } from "./types.js"; import { withoutTransition } from "./without-transition.js"; import { systemPrefersMode, userPrefersMode } from "./mode-states.svelte.js"; import { customTheme } from "./theme-state.svelte.js"; @@ -33,13 +33,21 @@ export const darkClassNames = box([]); */ export const lightClassNames = box([]); +/** + * A non-persistent override for the user's preferred mode. + * When set, this value takes precedence over the persisted `userPrefersMode`. + */ +export const sessionUserPrefersMode = box(undefined); + function createDerivedMode() { const current = $derived.by(() => { if (!isBrowser) return undefined; - const derivedMode = - userPrefersMode.current === "system" - ? systemPrefersMode.current + const preferredMode = + sessionUserPrefersMode.current !== undefined + ? sessionUserPrefersMode.current : userPrefersMode.current; + const derivedMode = + preferredMode === "system" ? systemPrefersMode.current : preferredMode; const sanitizedDarkClassNames = sanitizeClassNames(darkClassNames.current); const sanitizedLightClassNames = sanitizeClassNames(lightClassNames.current); diff --git a/packages/mode-watcher/src/routes/+page.svelte b/packages/mode-watcher/src/routes/+page.svelte index 593722c..4526bbf 100644 --- a/packages/mode-watcher/src/routes/+page.svelte +++ b/packages/mode-watcher/src/routes/+page.svelte @@ -73,4 +73,10 @@ > Reset + From 88d47383c2a3585e361144a613623dc197656698 Mon Sep 17 00:00:00 2001 From: xKesvaL Date: Sun, 7 Sep 2025 23:02:25 +0200 Subject: [PATCH 2/3] feat: better docs w/ example, rename to nicer option, add tests and add clear utility --- .../content/utilities/clear-session-mode.md | 24 +++++++++++++ docs/src/content/utilities/set-mode.md | 24 ++++++++++--- packages/mode-watcher/src/lib/index.ts | 8 ++++- packages/mode-watcher/src/lib/mode.ts | 36 ++++++++++++------- packages/mode-watcher/src/routes/+page.svelte | 9 ++++- packages/mode-watcher/src/tests/mode.spec.ts | 31 ++++++++++++++++ 6 files changed, 114 insertions(+), 18 deletions(-) create mode 100644 docs/src/content/utilities/clear-session-mode.md diff --git a/docs/src/content/utilities/clear-session-mode.md b/docs/src/content/utilities/clear-session-mode.md new file mode 100644 index 0000000..98e24bb --- /dev/null +++ b/docs/src/content/utilities/clear-session-mode.md @@ -0,0 +1,24 @@ +--- +title: clearSessionMode +description: Clears the session-only override and reverts to the persisted or system mode. +section: Utilities +--- + +`clearSessionMode` removes any temporary, session-only mode set via `setMode(..., { sessionOnly: true })`. + +After calling this, the active mode falls back to the user's persisted preference (from `localStorage`) or to the system preference if none is set. + +## Usage + +```svelte + + + +``` + +## See also + +- [setMode](/docs/utilities/set-mode) +- [resetMode](/docs/utilities/reset-mode) diff --git a/docs/src/content/utilities/set-mode.md b/docs/src/content/utilities/set-mode.md index 3509412..fc308fb 100644 --- a/docs/src/content/utilities/set-mode.md +++ b/docs/src/content/utilities/set-mode.md @@ -27,20 +27,36 @@ This updates both the visual mode and the persisted preference in `localStorage` You can also not persist the mode to `localStorage` by passing the `persist` option as `false`. This can be useful if you need to set the mode to a specific value for a specific part of your website. (ex: marketing part vs docs part, where in marketing part you want to set the mode to `light` and in docs part you want the user to choose the mode). +`(marketing) group - layout.svelte` + ```svelte +``` - +`(docs) group - layout.svelte` + +```svelte + ``` +Then you get the marketing with light mode as you like, and the docs part will revert to the user's local storage preference! + ## Options The `setMode` utility accepts the following options: - + Whether to persist the theme to `localStorage`. diff --git a/packages/mode-watcher/src/lib/index.ts b/packages/mode-watcher/src/lib/index.ts index d226336..61f1f4c 100644 --- a/packages/mode-watcher/src/lib/index.ts +++ b/packages/mode-watcher/src/lib/index.ts @@ -2,6 +2,7 @@ import { generateSetInitialModeExpression, createInitialModeExpression, resetMode, + clearSessionMode, setMode, setTheme, toggleMode, @@ -16,6 +17,7 @@ export { setMode, toggleMode, resetMode, + clearSessionMode, modeStorageKey, userPrefersMode, systemPrefersMode, @@ -24,5 +26,9 @@ export { setTheme, themeStorageKey, }; -export type { SystemModeValue, UserPrefersMode, SystemPrefersMode } from "./mode-states.svelte.js"; +export type { + SystemModeValue, + UserPrefersMode, + SystemPrefersMode, +} from "./mode-states.svelte.js"; export { default as ModeWatcher } from "./components/mode-watcher.svelte"; diff --git a/packages/mode-watcher/src/lib/mode.ts b/packages/mode-watcher/src/lib/mode.ts index bf1825d..6bfc205 100644 --- a/packages/mode-watcher/src/lib/mode.ts +++ b/packages/mode-watcher/src/lib/mode.ts @@ -10,17 +10,17 @@ export function toggleMode(): void { } export type SetModeOptions = { - persist?: boolean; + /** Apply only for this session; do not persist to localStorage. */ + sessionOnly?: boolean; }; /** Set the mode to light or dark */ export function setMode(mode: Mode, options?: SetModeOptions): void { - const persist = options?.persist ?? true; - if (persist) { + if (options?.sessionOnly) { + sessionUserPrefersMode.current = mode; + } else { sessionUserPrefersMode.current = undefined; userPrefersMode.current = mode; - } else { - sessionUserPrefersMode.current = mode; } } @@ -30,6 +30,11 @@ export function resetMode(): void { userPrefersMode.current = "system"; } +/** Reset the current session-only override, falling back to persisted/user/system */ +export function clearSessionMode(): void { + sessionUserPrefersMode.current = undefined; +} + /** Set the theme to a custom value */ export function setTheme(newTheme: string): void { customTheme.current = newTheme; @@ -64,13 +69,18 @@ export function setInitialMode({ const theme = localStorage.getItem(themeStorageKey) ?? defaultTheme; const light = mode === "light" || - (mode === "system" && window.matchMedia("(prefers-color-scheme: light)").matches); + (mode === "system" && + window.matchMedia("(prefers-color-scheme: light)").matches); if (light) { - if (darkClassNames.length) rootEl.classList.remove(...darkClassNames.filter(Boolean)); - if (lightClassNames.length) rootEl.classList.add(...lightClassNames.filter(Boolean)); + if (darkClassNames.length) + rootEl.classList.remove(...darkClassNames.filter(Boolean)); + if (lightClassNames.length) + rootEl.classList.add(...lightClassNames.filter(Boolean)); } else { - if (lightClassNames.length) rootEl.classList.remove(...lightClassNames.filter(Boolean)); - if (darkClassNames.length) rootEl.classList.add(...darkClassNames.filter(Boolean)); + if (lightClassNames.length) + rootEl.classList.remove(...lightClassNames.filter(Boolean)); + if (darkClassNames.length) + rootEl.classList.add(...darkClassNames.filter(Boolean)); } rootEl.style.colorScheme = light ? "light" : "dark"; @@ -79,7 +89,7 @@ export function setInitialMode({ if (themeMetaEl) { themeMetaEl.setAttribute( "content", - mode === "light" ? themeColors.light : themeColors.dark + mode === "light" ? themeColors.light : themeColors.dark, ); } } @@ -95,7 +105,9 @@ export function setInitialMode({ /** * A type-safe way to generate the source expression used to set the initial mode and avoid FOUC. */ -export function createInitialModeExpression(config: SetInitialModeArgs = {}): string { +export function createInitialModeExpression( + config: SetInitialModeArgs = {}, +): string { return `(${setInitialMode.toString()})(${JSON.stringify(config)});`; } diff --git a/packages/mode-watcher/src/routes/+page.svelte b/packages/mode-watcher/src/routes/+page.svelte index 4526bbf..f71e154 100644 --- a/packages/mode-watcher/src/routes/+page.svelte +++ b/packages/mode-watcher/src/routes/+page.svelte @@ -2,6 +2,7 @@ import { mode, resetMode, + clearSessionMode, setMode, systemPrefersMode, theme, @@ -75,8 +76,14 @@ + diff --git a/packages/mode-watcher/src/tests/mode.spec.ts b/packages/mode-watcher/src/tests/mode.spec.ts index 2fbdc0c..ace0566 100644 --- a/packages/mode-watcher/src/tests/mode.spec.ts +++ b/packages/mode-watcher/src/tests/mode.spec.ts @@ -3,6 +3,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { userEvent } from "@testing-library/user-event"; import { tick } from "svelte"; import { mediaQueryState } from "../../scripts/setupTest.js"; +import { setMode, clearSessionMode, modeStorageKey } from "$lib/index.js"; import Mode from "./Mode.svelte"; import StealthMode from "./StealthMode.svelte"; import type { ModeWatcherProps } from "$lib/types.js"; @@ -353,6 +354,36 @@ describe("mode-watcher", () => { expect(classes2).toContain("custom-l-class"); }); + it("applies mode for session only without persisting to localStorage", async () => { + const { rootEl } = setup(); + expect(getClasses(rootEl)).toContain("dark"); + expect(localStorage.getItem(modeStorageKey.current)).toBe("system"); + + setMode("light", { sessionOnly: true }); + await tick(); + + expect(getClasses(rootEl)).not.toContain("dark"); + expect(getColorScheme(rootEl)).toBe("light"); + // localStorage unchanged + expect(localStorage.getItem(modeStorageKey.current)).toBe("system"); + }); + + it("clears session-only override and reverts to persisted value", async () => { + const { rootEl } = setup(); + // start with session-only light + setMode("light", { sessionOnly: true }); + await tick(); + expect(getColorScheme(rootEl)).toBe("light"); + // clear override + clearSessionMode(); + await tick(); + // should revert to system -> dark + expect(getClasses(rootEl)).toContain("dark"); + expect(getColorScheme(rootEl)).toBe("dark"); + // localStorage still system + expect(localStorage.getItem(modeStorageKey.current)).toBe("system"); + }); + it("allows the user to set a custom theme via the `defaultTheme` prop", async () => { const { theme, rootEl } = setup({ defaultTheme: "dracula", From b242e05312682d3e09eb336af134d469d921a3da Mon Sep 17 00:00:00 2001 From: xKesvaL Date: Sun, 7 Sep 2025 23:16:14 +0200 Subject: [PATCH 3/3] chore: rename to temporary mode --- .../content/utilities/clear-session-mode.md | 10 +++++----- docs/src/content/utilities/set-mode.md | 14 ++++++------- packages/mode-watcher/src/lib/index.ts | 4 ++-- packages/mode-watcher/src/lib/mode.ts | 20 +++++++++---------- .../mode-watcher/src/lib/states.svelte.ts | 6 +++--- packages/mode-watcher/src/routes/+page.svelte | 8 ++++---- packages/mode-watcher/src/tests/mode.spec.ts | 14 ++++++------- 7 files changed, 38 insertions(+), 38 deletions(-) diff --git a/docs/src/content/utilities/clear-session-mode.md b/docs/src/content/utilities/clear-session-mode.md index 98e24bb..9e73e0b 100644 --- a/docs/src/content/utilities/clear-session-mode.md +++ b/docs/src/content/utilities/clear-session-mode.md @@ -1,10 +1,10 @@ --- -title: clearSessionMode -description: Clears the session-only override and reverts to the persisted or system mode. +title: clearTemporaryMode +description: Clears the temporary override and reverts to the persisted or system mode. section: Utilities --- -`clearSessionMode` removes any temporary, session-only mode set via `setMode(..., { sessionOnly: true })`. +`clearTemporaryMode` removes any temporary mode set via `setMode(..., { temporaryOnly: true })`. After calling this, the active mode falls back to the user's persisted preference (from `localStorage`) or to the system preference if none is set. @@ -12,10 +12,10 @@ After calling this, the active mode falls back to the user's persisted preferenc ```svelte - + ``` ## See also diff --git a/docs/src/content/utilities/set-mode.md b/docs/src/content/utilities/set-mode.md index fc308fb..864b1e8 100644 --- a/docs/src/content/utilities/set-mode.md +++ b/docs/src/content/utilities/set-mode.md @@ -12,7 +12,7 @@ section: Utilities It accepts one of three string values: `"light"`, `"dark"`, or `"system"`. -This updates both the visual mode and the persisted preference in `localStorage`. You can also pass it options to customize the behavior, such as wether or not to persist it to `localStorage`. +This updates both the visual mode and the persisted preference in `localStorage`. You can also pass it options to customize the behavior, such as applying it temporarily without persisting it to `localStorage`. ## Usage @@ -25,7 +25,7 @@ This updates both the visual mode and the persisted preference in `localStorage` ``` -You can also not persist the mode to `localStorage` by passing the `persist` option as `false`. This can be useful if you need to set the mode to a specific value for a specific part of your website. (ex: marketing part vs docs part, where in marketing part you want to set the mode to `light` and in docs part you want the user to choose the mode). +You can also apply the mode temporarily (without persisting to `localStorage`) by passing the `temporaryOnly` option as `true`. This can be useful if you need to set the mode to a specific value for a specific part of your website. (ex: marketing part vs docs part, where in marketing part you want to set the mode to `light` and in docs part you want the user to choose the mode). `(marketing) group - layout.svelte` @@ -34,7 +34,7 @@ You can also not persist the mode to `localStorage` by passing the `persist` opt import { setMode } from "mode-watcher"; onMount(() => { - setMode("light", { sessionOnly: true }); + setMode("light", { temporaryOnly: true }); }); ``` @@ -43,10 +43,10 @@ You can also not persist the mode to `localStorage` by passing the `persist` opt ```svelte ``` @@ -57,6 +57,6 @@ Then you get the marketing with light mode as you like, and the docs part will r The `setMode` utility accepts the following options: - - Whether to persist the theme to `localStorage`. + + Whether to apply the mode only temporarily (do not persist to `localStorage`). diff --git a/packages/mode-watcher/src/lib/index.ts b/packages/mode-watcher/src/lib/index.ts index 61f1f4c..85cb2ec 100644 --- a/packages/mode-watcher/src/lib/index.ts +++ b/packages/mode-watcher/src/lib/index.ts @@ -2,7 +2,7 @@ import { generateSetInitialModeExpression, createInitialModeExpression, resetMode, - clearSessionMode, + clearTemporaryMode, setMode, setTheme, toggleMode, @@ -17,7 +17,7 @@ export { setMode, toggleMode, resetMode, - clearSessionMode, + clearTemporaryMode, modeStorageKey, userPrefersMode, systemPrefersMode, diff --git a/packages/mode-watcher/src/lib/mode.ts b/packages/mode-watcher/src/lib/mode.ts index 6bfc205..ab7838e 100644 --- a/packages/mode-watcher/src/lib/mode.ts +++ b/packages/mode-watcher/src/lib/mode.ts @@ -1,38 +1,38 @@ import { userPrefersMode } from "./mode-states.svelte.js"; import { customTheme } from "./theme-state.svelte.js"; -import { derivedMode, sessionUserPrefersMode } from "./states.svelte.js"; +import { derivedMode, temporaryUserPrefersMode } from "./states.svelte.js"; import type { Mode, ThemeColors } from "./types.js"; /** Toggle between light and dark mode */ export function toggleMode(): void { userPrefersMode.current = derivedMode.current === "dark" ? "light" : "dark"; - sessionUserPrefersMode.current = undefined; + temporaryUserPrefersMode.current = undefined; } export type SetModeOptions = { - /** Apply only for this session; do not persist to localStorage. */ - sessionOnly?: boolean; + /** Apply only temporarily; do not persist to localStorage. */ + temporaryOnly?: boolean; }; /** Set the mode to light or dark */ export function setMode(mode: Mode, options?: SetModeOptions): void { - if (options?.sessionOnly) { - sessionUserPrefersMode.current = mode; + if (options?.temporaryOnly) { + temporaryUserPrefersMode.current = mode; } else { - sessionUserPrefersMode.current = undefined; + temporaryUserPrefersMode.current = undefined; userPrefersMode.current = mode; } } /** Reset the mode to operating system preference */ export function resetMode(): void { - sessionUserPrefersMode.current = undefined; + temporaryUserPrefersMode.current = undefined; userPrefersMode.current = "system"; } /** Reset the current session-only override, falling back to persisted/user/system */ -export function clearSessionMode(): void { - sessionUserPrefersMode.current = undefined; +export function clearTemporaryMode(): void { + temporaryUserPrefersMode.current = undefined; } /** Set the theme to a custom value */ diff --git a/packages/mode-watcher/src/lib/states.svelte.ts b/packages/mode-watcher/src/lib/states.svelte.ts index 0884378..54ffeac 100644 --- a/packages/mode-watcher/src/lib/states.svelte.ts +++ b/packages/mode-watcher/src/lib/states.svelte.ts @@ -37,14 +37,14 @@ export const lightClassNames = box([]); * A non-persistent override for the user's preferred mode. * When set, this value takes precedence over the persisted `userPrefersMode`. */ -export const sessionUserPrefersMode = box(undefined); +export const temporaryUserPrefersMode = box(undefined); function createDerivedMode() { const current = $derived.by(() => { if (!isBrowser) return undefined; const preferredMode = - sessionUserPrefersMode.current !== undefined - ? sessionUserPrefersMode.current + temporaryUserPrefersMode.current !== undefined + ? temporaryUserPrefersMode.current : userPrefersMode.current; const derivedMode = preferredMode === "system" ? systemPrefersMode.current : preferredMode; diff --git a/packages/mode-watcher/src/routes/+page.svelte b/packages/mode-watcher/src/routes/+page.svelte index f71e154..6f378c9 100644 --- a/packages/mode-watcher/src/routes/+page.svelte +++ b/packages/mode-watcher/src/routes/+page.svelte @@ -2,7 +2,7 @@ import { mode, resetMode, - clearSessionMode, + clearTemporaryMode, setMode, systemPrefersMode, theme, @@ -76,14 +76,14 @@ diff --git a/packages/mode-watcher/src/tests/mode.spec.ts b/packages/mode-watcher/src/tests/mode.spec.ts index ace0566..8ab1f41 100644 --- a/packages/mode-watcher/src/tests/mode.spec.ts +++ b/packages/mode-watcher/src/tests/mode.spec.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { userEvent } from "@testing-library/user-event"; import { tick } from "svelte"; import { mediaQueryState } from "../../scripts/setupTest.js"; -import { setMode, clearSessionMode, modeStorageKey } from "$lib/index.js"; +import { setMode, clearTemporaryMode, modeStorageKey } from "$lib/index.js"; import Mode from "./Mode.svelte"; import StealthMode from "./StealthMode.svelte"; import type { ModeWatcherProps } from "$lib/types.js"; @@ -354,12 +354,12 @@ describe("mode-watcher", () => { expect(classes2).toContain("custom-l-class"); }); - it("applies mode for session only without persisting to localStorage", async () => { + it("applies mode temporarily without persisting to localStorage", async () => { const { rootEl } = setup(); expect(getClasses(rootEl)).toContain("dark"); expect(localStorage.getItem(modeStorageKey.current)).toBe("system"); - setMode("light", { sessionOnly: true }); + setMode("light", { temporaryOnly: true }); await tick(); expect(getClasses(rootEl)).not.toContain("dark"); @@ -368,14 +368,14 @@ describe("mode-watcher", () => { expect(localStorage.getItem(modeStorageKey.current)).toBe("system"); }); - it("clears session-only override and reverts to persisted value", async () => { + it("clears temporary override and reverts to persisted value", async () => { const { rootEl } = setup(); - // start with session-only light - setMode("light", { sessionOnly: true }); + // start with temporary-only light + setMode("light", { temporaryOnly: true }); await tick(); expect(getColorScheme(rootEl)).toBe("light"); // clear override - clearSessionMode(); + clearTemporaryMode(); await tick(); // should revert to system -> dark expect(getClasses(rootEl)).toContain("dark");