Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/lib/utilities/dark-mode/dark-mode.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<script lang="ts">
import type { Snippet } from 'svelte';

import type { ThemeOverride } from './dark-mode';
import { darkMode } from './dark-mode';

let { children }: { children?: Snippet } = $props();
let {
children,
overrideTheme,
}: { children?: Snippet; overrideTheme?: ThemeOverride } = $props();
</script>

<svelte:body use:darkMode />
<svelte:body use:darkMode={overrideTheme} />
{@render children?.()}
59 changes: 55 additions & 4 deletions src/lib/utilities/dark-mode/dark-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,73 @@ describe('dark-mode utilities', () => {
});

describe('darkMode', () => {
it('should set data-theme to "dark" when dark mode is enabled', async () => {
it('should set data-theme to "dark" when dark mode is enabled', () => {
const node = document.createElement('div');
useDarkModePreference.set(true);

darkMode(node);
await new Promise((resolve) => setTimeout(resolve, 0));

expect(node.dataset.theme).toBe('dark');
});

it('should set data-theme to "light" when dark mode is disabled', async () => {
it('should set data-theme to "light" when dark mode is disabled', () => {
const node = document.createElement('div');
useDarkModePreference.set(false);

darkMode(node);
await new Promise((resolve) => setTimeout(resolve, 0));

expect(node.dataset.theme).toBe('light');
});

it('should force "light" via overrideTheme even when dark mode is enabled', () => {
const node = document.createElement('div');
useDarkModePreference.set(true);

darkMode(node, 'light');

expect(node.dataset.theme).toBe('light');
});

it('should force "dark" via overrideTheme even when dark mode is disabled', () => {
const node = document.createElement('div');
useDarkModePreference.set(false);

darkMode(node, 'dark');

expect(node.dataset.theme).toBe('dark');
});

it('should re-apply the theme when the override changes via update', () => {
const node = document.createElement('div');
useDarkModePreference.set(true);

const action = darkMode(node, 'light');
expect(node.dataset.theme).toBe('light');

action.update('dark');
expect(node.dataset.theme).toBe('dark');
});

it('should fall back to the store theme when the override is cleared', () => {
const node = document.createElement('div');
useDarkModePreference.set(true);

const action = darkMode(node, 'light');
expect(node.dataset.theme).toBe('light');

action.update(undefined);
expect(node.dataset.theme).toBe('dark');
});

it('should stop updating data-theme after destroy', () => {
const node = document.createElement('div');
useDarkModePreference.set(false);

const action = darkMode(node);
expect(node.dataset.theme).toBe('light');

action.destroy();
useDarkModePreference.set(true);

expect(node.dataset.theme).toBe('light');
});
Expand Down
33 changes: 26 additions & 7 deletions src/lib/utilities/dark-mode/dark-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { derived } from 'svelte/store';
import { persistStore } from '$lib/stores/persist-store';

export type DarkModePreference = boolean | 'system';
export type ThemeOverride = 'light' | 'dark';

export const useDarkModePreference = persistStore<DarkModePreference>(
'dark mode',
Expand All @@ -25,12 +26,30 @@ export const useDarkMode = derived(useDarkModePreference, prefersDarkMode);
export const getNextDarkModePreference = (value: DarkModePreference) =>
value == 'system' ? true : value == true ? false : 'system';

export const darkMode = (node: HTMLElement) => {
useDarkMode.subscribe((value) => {
if (value) {
node.dataset.theme = 'dark';
} else {
node.dataset.theme = 'light';
}
const applyTheme = (
node: HTMLElement,
prefersDark: boolean,
overrideTheme?: ThemeOverride,
) => {
node.dataset.theme = overrideTheme ?? (prefersDark ? 'dark' : 'light');
};

export const darkMode = (node: HTMLElement, overrideTheme?: ThemeOverride) => {
let override = overrideTheme;
let prefersDark = false;

const unsubscribe = useDarkMode.subscribe((value) => {
prefersDark = value;
applyTheme(node, prefersDark, override);
});

return {
update(newOverride?: ThemeOverride) {
override = newOverride;
applyTheme(node, prefersDark, override);
},
destroy() {
unsubscribe();
},
};
};
2 changes: 1 addition & 1 deletion src/lib/utilities/dark-mode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ export {
getNextDarkModePreference,
} from './dark-mode';

export type { DarkModePreference } from './dark-mode';
export type { DarkModePreference, ThemeOverride } from './dark-mode';
3 changes: 3 additions & 0 deletions src/routes/(login)/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<script lang="ts">
import type { Snippet } from 'svelte';

import DarkMode from '$lib/utilities/dark-mode';

interface Props {
children: Snippet;
}

let { children }: Props = $props();
</script>

<DarkMode overrideTheme="light" />
{@render children()}
Loading