diff --git a/apps/web/src/lib/components/ThemeChangeIcon.svelte b/apps/web/src/lib/components/ThemeChangeIcon.svelte deleted file mode 100644 index 2d0a3fb3..00000000 --- a/apps/web/src/lib/components/ThemeChangeIcon.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - theme.toggle()} - src={$theme === 'dark' ? SunIcon : MoonIcon} - class="{className} h-10 w-10 text-zinc-500 hover:text-indigo-800 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:text-indigo-600" -/> diff --git a/apps/web/src/lib/stores/theme.ts b/apps/web/src/lib/stores/theme.ts deleted file mode 100644 index ba7557db..00000000 --- a/apps/web/src/lib/stores/theme.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { browser } from '$app/environment'; -import { writable } from 'svelte/store'; - -const THEME_KEY = 'theme'; -const DEFAULT_THEME = 'light'; - -export function getInferredDefaultTheme(): 'dark' | 'light' { - if (!window.matchMedia) { - return DEFAULT_THEME; - } - if (window.matchMedia('(prefers-color-scheme: dark)').matches) { - return 'dark'; - } else { - return 'light'; - } -} - -function createThemeStore() { - const { subscribe, set, update } = writable<'light' | 'dark'>(DEFAULT_THEME); - - if (browser) { - let data = localStorage?.getItem(THEME_KEY) as 'light' | 'dark'; - - if (!data) { - data = getInferredDefaultTheme(); - } - set(data); - if (data === 'dark') { - document.querySelector('html')?.classList.add('dark'); - } - } - - subscribe((value) => { - if (browser) { - localStorage?.setItem(THEME_KEY, value); - } - }); - - function toggle() { - update((state) => { - state = state === 'dark' ? 'light' : 'dark'; - return state; - }); - document.querySelector('html')?.classList.toggle('dark'); - } - - return { - subscribe, - set, - toggle - }; -} - -export const theme = createThemeStore(); diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index 9fd7ee7e..d3f0cc72 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -9,14 +9,13 @@ import '../app.css'; import { onMount } from 'svelte'; import { page } from '$app/stores'; - import { Toasts, NavBar, Footer } from '@yd/ui'; + import { Toasts, NavBar, Footer, ThemeToggle } from '@yd/ui'; import Loading from '$lib/components/Loading.svelte'; import { session } from '$lib/stores/session'; import { downloads } from '$lib/stores/downloads'; import config from '$lib/config'; import { RoutePathConstants, links } from '$lib/utils/route'; import Logo from '$lib/components/Logo.svelte'; - import ThemeChangeIcon from '$lib/components/ThemeChangeIcon.svelte'; // Use session as token when making requests with client OpenAPI.TOKEN = async () => { @@ -38,7 +37,7 @@
- +
diff --git a/packages/ui/src/lib/components/theme/ThemeToggle.svelte b/packages/ui/src/lib/components/theme/ThemeToggle.svelte new file mode 100644 index 00000000..1809ef99 --- /dev/null +++ b/packages/ui/src/lib/components/theme/ThemeToggle.svelte @@ -0,0 +1,11 @@ + + + theme.toggle()} + src={$theme === 'dark' ? SunIcon : MoonIcon} + class="h-10 w-10 text-zinc-500 hover:text-indigo-800 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:text-indigo-600" +/> diff --git a/packages/ui/src/lib/components/theme/index.ts b/packages/ui/src/lib/components/theme/index.ts new file mode 100644 index 00000000..26ef9731 --- /dev/null +++ b/packages/ui/src/lib/components/theme/index.ts @@ -0,0 +1,2 @@ +export { theme } from './store'; +export { default as ThemeToggle } from './ThemeToggle.svelte'; diff --git a/packages/ui/src/lib/components/theme/store.ts b/packages/ui/src/lib/components/theme/store.ts new file mode 100644 index 00000000..366398b9 --- /dev/null +++ b/packages/ui/src/lib/components/theme/store.ts @@ -0,0 +1,61 @@ +import { writable } from 'svelte/store'; +import { browser } from '../../utilities'; + +type Theme = 'dark' | 'light'; + +const THEME_LOCALSTORAGE_KEY = 'theme'; +const DEFAULT_THEME: Theme = 'light'; + +// Get the default theme value inferred by the browser settings. +function getInferredDefaultTheme(): Theme { + if (browser()) { + // Cant match preferred color scheme, return default. + if (!window.matchMedia) { + return DEFAULT_THEME; + } + // Use dark theme if preferred by the user. + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark'; + } else { + return 'light'; + } + } else { + return DEFAULT_THEME; + } +} + +function createThemeStore() { + const { subscribe, set, update } = writable(getInferredDefaultTheme()); + + if (browser()) { + let data = localStorage?.getItem(THEME_LOCALSTORAGE_KEY) as Theme; + if (!data) { + data = getInferredDefaultTheme(); + } + set(data); + if (data === 'dark') { + document.querySelector('html')?.classList.add('dark'); + } + } + + subscribe((value) => { + if (browser()) { + localStorage?.setItem(THEME_LOCALSTORAGE_KEY, value); + } + }); + + function toggle() { + update((state) => { + state = state === 'dark' ? 'light' : 'dark'; + return state; + }); + document.querySelector('html')?.classList.toggle('dark'); + } + + return { + subscribe, + toggle + }; +} + +export const theme = createThemeStore(); diff --git a/packages/ui/src/lib/index.ts b/packages/ui/src/lib/index.ts index 18ba1484..65439701 100644 --- a/packages/ui/src/lib/index.ts +++ b/packages/ui/src/lib/index.ts @@ -1,5 +1,6 @@ // Component exports export { NavBar } from './components/nav'; +export { ThemeToggle, theme } from './components/theme'; export { Toast, Toasts, toast } from './components/toast'; export { Description, List, Title } from './components/typography'; export { default as Alert, type AlertVariants } from './components/Alert.svelte'; diff --git a/packages/ui/src/lib/utilities.ts b/packages/ui/src/lib/utilities.ts index 6494ac4a..e3fdd840 100644 --- a/packages/ui/src/lib/utilities.ts +++ b/packages/ui/src/lib/utilities.ts @@ -55,3 +55,9 @@ export function toIcon(value: unknown, extras?: { loading?: boolean }): IconSour return undefined; } } + +/** + * Helper function to check if running in browser + * @returns true when running in browser, false otherwise + */ +export const browser = () => typeof window !== 'undefined';