svelte-os-themes
Version:
[Svelte](https://svelte.dev/) 5 theme helper.
183 lines (177 loc) • 5.42 kB
JavaScript
import { parseTheme } from './parseTheme.js';
const defaultOptions = {
fallback: 'system',
attribute: 'class',
storageKey: 'theme',
system: false,
colorScheme: true,
};
export function createTheme(options) {
const config = $derived({
...defaultOptions,
...options,
});
let theme = $state(config.fallback);
function getTriggerProps(props) {
if (props.value !== 'auto') {
return {
type: 'button',
onclick() {
theme = props.value;
},
'aria-label': 'Enable %s mode'.replace('%s', props.value),
'data-state': theme === props.value ? 'on' : 'off',
'data-value': props.value,
};
}
const defaultSequence = [
/**/
'light',
'dark',
'system',
];
const sequence = props.sequence?.length ? props.sequence : defaultSequence;
const currIndex = sequence.indexOf(theme);
const nextIndex = currIndex + 1 < sequence.length ? currIndex + 1 : 0;
const nextTheme = sequence[nextIndex];
return {
type: 'button',
onclick() {
theme = nextTheme;
},
'aria-label': 'Enable %s mode'.replace('%s', nextTheme),
'data-value': props.value,
};
}
$effect(() => {
theme = parseTheme(window.localStorage.getItem(config.storageKey), config.fallback);
});
$effect(() => {
const html = document.documentElement;
html.classList.add('svelte-os-themes__no-transition');
const originalTheme = theme;
const resolvedTheme = originalTheme === 'system'
? window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
: originalTheme;
if (config.attribute === 'class') {
const removeClass = resolvedTheme === 'dark' ? 'light' : 'dark';
html.classList.remove(removeClass);
html.classList.add(resolvedTheme);
}
else {
html.setAttribute(config.attribute, resolvedTheme);
}
if (config.colorScheme)
html.style.colorScheme = resolvedTheme;
window.localStorage.setItem(config.storageKey, originalTheme);
setTimeout(() => {
html.classList.remove('svelte-os-themes__no-transition');
}, 1);
});
$effect(() => {
if (!config.system)
return function noop() { };
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
theme = e.matches ? 'dark' : 'light';
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
});
$effect(() => {
const handleChange = (e) => {
if (e.key === config.storageKey) {
theme = parseTheme(e.newValue, config.fallback);
}
};
window.addEventListener('storage', handleChange);
return () => {
window.removeEventListener('storage', handleChange);
};
});
return {
get current() {
return theme;
},
set current(newTheme) {
if (newTheme) {
theme = newTheme;
}
else {
theme = config.fallback;
}
},
getTriggerProps,
};
}
createTheme.style = function (props) {
const config = $derived({
...defaultOptions,
...props,
});
const value = $derived(`
<style ${assignNonce(config.styleNonce)}>
.svelte-os-themes__no-transition,
.svelte-os-themes__no-transition *,
.svelte-os-themes__no-transition *::after,
.svelte-os-themes__no-transition *::before {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}
</style>
`);
return {
get value() {
return value;
},
};
};
createTheme.script = function (props) {
const config = $derived({
...defaultOptions,
...props,
});
const value = $derived(`
<script ${assignNonce(config.scriptNonce)}>
(function(k, a, f, c) {
const h = document.documentElement;
const q = window.matchMedia('(prefers-color-scheme: dark)')
const s = window.localStorage.getItem(k)?.toLowerCase().trim();
const l = [
'dark',
'light',
'system'
];
const v = l.includes(s) ? s : f;
const t = v === 'system' ? q.matches ? 'dark' : 'light' : v;
if (a === 'class') {
h.classList.remove(t === 'dark' ? 'light' : 'dark');
h.classList.add(t);
} else {
h.setAttribute(a, t);
}
window.localStorage.setItem(k, v);
if (c) h.style.colorScheme = t;
})(
'${config.storageKey}',
'${config.attribute}',
'${config.fallback}',
${config.colorScheme},
);
</script>
`);
return {
get value() {
return value;
},
};
};
function assignNonce(nonce) {
return nonce ? `nonce="${nonce}"` : '';
}