Last active
July 17, 2023 23:55
-
-
Save waffleflopper/08ef7fe0795f7b54ad126f89f7fa5cd9 to your computer and use it in GitHub Desktop.
Melt-UI Navbar MVP Example (before pre-processor release)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- Derived from https://github.com/skeletonlabs/skeleton/blob/dev/packages/skeleton/src/lib/utilities/LightSwitch/LightSwitch.svelte --> | |
<script lang="ts"> | |
import { Moon, Sun } from 'lucide-svelte'; | |
import { onMount } from 'svelte'; | |
import { twc } from '$utils/class'; | |
let className = ''; | |
export { className as class }; | |
import { getModeOsPrefers, modeCurrent, setModeCurrent, setModeUserPrefers } from './darkSwitch'; | |
import type { OnKeyDownEvent } from '.'; | |
function toggleTheme(): void { | |
$modeCurrent = !$modeCurrent; | |
setModeUserPrefers($modeCurrent); | |
setModeCurrent($modeCurrent); | |
} | |
function onKeyDown(event: Event): void { | |
const keyboardEvent = event as OnKeyDownEvent; | |
if (['Enter', 'Space'].includes(keyboardEvent.code)) { | |
event.preventDefault(); | |
keyboardEvent.currentTarget.click(); | |
} | |
} | |
onMount(() => { | |
if (!('modeCurrent' in localStorage)) { | |
setModeCurrent(getModeOsPrefers()); | |
} | |
}); | |
</script> | |
<button | |
on:click={toggleTheme} | |
on:keydown={onKeyDown} | |
role="switch" | |
aria-label="Dark Switch" | |
aria-checked={$modeCurrent} | |
title="Toggle {$modeCurrent ? 'light' : 'dark'} mode" | |
class={twc('btn-ghost w-9 px-0', className)} | |
> | |
{#if $modeCurrent} | |
<Moon class="w-5 h-5" /> | |
<span class="sr-only">Dark Mode</span> | |
{:else} | |
<Sun class="w-5 h-5" /> | |
<span class="sr-only">Light Mode</span> | |
{/if} | |
</button> | |
<!-- | |
@component DarkSwitch | |
@selector dark-switch | |
@description A switch to toggle between light and dark mode. | |
@state $modeCurrent - The current mode. | |
@state $modeUserPrefers - The user's preferred mode. | |
@state $modeOsPrefers - The OS's preferred mode. | |
--> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- Shamelessly stolen from https://github.com/skeletonlabs/skeleton/blob/dev/packages/skeleton/src/lib/utilities/LightSwitch/lightswitch.ts --> | |
import { get } from 'svelte/store'; | |
import { localStorageStore } from '$lib/utils/store'; | |
//stores | |
export const modeOsPrefers = localStorageStore<boolean>('modeOsPrefers', false); | |
export const modeUserPrefers = localStorageStore<boolean | undefined>('modeUserPrefers', undefined); | |
export const modeCurrent = localStorageStore<boolean>('modeCurrent', false); | |
//get | |
export function getModeOsPrefers(): boolean { | |
const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches; | |
modeOsPrefers.set(prefersLight); | |
return get(modeOsPrefers); | |
} | |
export function getModeUserPrefers(): boolean | undefined { | |
return get(modeUserPrefers); | |
} | |
export function getModeAutoPrefers(): boolean { | |
const os = getModeOsPrefers(); | |
const user = getModeUserPrefers(); | |
const modeValue = user !== undefined ? user : os; | |
return modeValue; | |
} | |
//set | |
export function setModeUserPrefers(value: boolean): void { | |
modeUserPrefers.set(value); | |
} | |
export function setModeCurrent(value: boolean) { | |
const elemHtmlClasses = document.documentElement.classList; | |
const classDark = `dark`; | |
value == true ? elemHtmlClasses.remove(classDark) : elemHtmlClasses.add(classDark); | |
modeCurrent.set(value); | |
} | |
/** | |
* Set the initial class state of the html element based on the user's preferences or the OS preferences. If the user has not set a preference, the OS preference will be used. | |
* Shamelessly stolen from Skeleton-UI https://skeleton.dev | |
* @example | |
* <svelte:head> | |
* | |
* {@html `<\u{73}cript nonce="%sveltekit.nonce%">(${setInitialClassState.toString()})();</script>`} | |
* | |
* </svelte:head> | |
*/ | |
export function setInitialClassState() { | |
const elemHtmlClasses = document.documentElement.classList; | |
const localStorageUserPrefs = localStorage.getItem('modeUserPrefers') === 'false'; | |
if (localStorageUserPrefs) { | |
elemHtmlClasses.add('dark'); | |
} else { | |
const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches; | |
prefersLight ? elemHtmlClasses.add('dark') : elemHtmlClasses.remove('dark'); | |
} | |
} | |
export function autoModeWatcher(): void { | |
const mql = window.matchMedia('(prefers-color-scheme: dark)'); | |
function setMode(value: boolean) { | |
const elemHtmlClasses = document.documentElement.classList; | |
const classDark = `dark`; | |
value === true ? elemHtmlClasses.remove(classDark) : elemHtmlClasses.add(classDark); | |
} | |
setMode(mql.matches); | |
mql.onchange = () => { | |
setMode(mql.matches); | |
}; | |
} | |
export function toggleMode(): void { | |
const mode = get(modeCurrent); | |
modeUserPrefers.set(!mode); | |
setModeCurrent(!mode); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { setInitialClassState } from './darkSwitch'; | |
</script> | |
<svelte:head> | |
{@html `<\u{73}cript nonce="%sveltekit.nonce%">(${setInitialClassState.toString()})();</script>`} | |
</svelte:head> | |
<!-- | |
@component | |
This component is used to prevent the flash of unstyled content (FOUC) when the page is loaded. It should be placed in your main +layout.svelte file. Under the hood it injects the setInitialClassState function into the head of the document as a string. This function is then called immediately after the page is loaded and sets the initial class state of the document based on the user's preference. | |
Method originally discovered in Skeleton-UI source code in a layout file. | |
```html | |
<script> | |
import { FOUC } from '$lib/components/DarkSwitch'; | |
</script> | |
<FOUC /> | |
``` | |
--> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Source: https://github.com/joshnuss/svelte-local-storage-store | |
* License: MIT | |
* Modified by: @waffleflopper | |
* | |
* This file is used to create a writable store that is saved to local storage. | |
* | |
*/ | |
import type { Readable, Writable } from 'svelte/store'; | |
import { browser } from '$app/environment'; //or use esm-env | |
import { get, writable } from 'svelte/store'; | |
declare type Updater<T> = (value: T) => T; | |
declare type StoreDict<T> = { | |
[key: string]: Writable<T>; | |
}; | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
const stores: StoreDict<any> = {}; | |
interface Serializer<T> { | |
parse(text: string): T; | |
stringify(value: T): string; | |
} | |
type StorageType = 'local' | 'session'; | |
interface Options<T> { | |
serializer?: Serializer<T>; | |
storage?: StorageType; | |
} | |
function getStorage(type: StorageType): Storage { | |
return type === 'local' ? localStorage : sessionStorage; | |
} | |
export function localStorageStore<T>( | |
key: string, | |
initialValue: T, | |
options?: Options<T> | |
): Writable<T> { | |
const serializer = options?.serializer ?? JSON; | |
const storageType = options?.storage ?? 'local'; | |
const fullKey = `${key}`; | |
function updateStorage(key: string, value: T) { | |
if (!browser) return; | |
getStorage(storageType).setItem(key, serializer.stringify(value)); | |
} | |
if (!stores[fullKey]) { | |
const store = writable(initialValue, (set) => { | |
const json = browser ? getStorage(storageType).getItem(fullKey) : null; | |
if (json) { | |
set(<T>serializer.parse(json)); | |
} | |
if (browser) { | |
const handleStorage = (event: StorageEvent) => { | |
if (event.key === fullKey) { | |
set(event.newValue ? serializer.parse(event.newValue) : null); | |
} | |
}; | |
window.addEventListener('storage', handleStorage); | |
return () => window.removeEventListener('storage', handleStorage); | |
} | |
}); | |
const { subscribe, set } = store; | |
stores[fullKey] = { | |
set(value: T) { | |
updateStorage(fullKey, value); | |
set(value); | |
}, | |
update(updater: Updater<T>) { | |
const value = updater(get(store)); | |
updateStorage(fullKey, value); | |
set(value); | |
}, | |
subscribe | |
}; | |
} | |
return stores[fullKey]; | |
} | |
export function toReadable<T>(store: Writable<T>): Readable<T> { | |
return { | |
subscribe: store.subscribe | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I went ahead and added the dark/light implementation (including the localStorage store), both of which are sourced from OSS which is linked in the appropriate gist files.