svelte-os-themes 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/create-theme-context.svelte.d.ts +17 -0
- package/dist/create-theme-context.svelte.js +121 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/theme-provider.svelte +17 -0
- package/dist/theme-provider.svelte.d.ts +19 -0
- package/dist/use-theme.svelte.d.ts +4 -0
- package/dist/use-theme.svelte.js +14 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Svelte OS Themes
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install svelte-os-themes
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```svelte
|
|
12
|
+
<!-- +layout.svelte -->
|
|
13
|
+
<script>
|
|
14
|
+
import { ThemeProvider } from 'svelte-os-themes';
|
|
15
|
+
|
|
16
|
+
let { children } = $props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<ThemeProvider>
|
|
20
|
+
{@render children()}
|
|
21
|
+
</ThemeProvider>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```svelte
|
|
25
|
+
<!-- +page.svelte -->
|
|
26
|
+
<script>
|
|
27
|
+
import { useTheme } from 'svelte-os-themes';
|
|
28
|
+
|
|
29
|
+
let theme = useTheme();
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onclick={function () {
|
|
35
|
+
theme.value = 'light';
|
|
36
|
+
}}
|
|
37
|
+
data-selected={theme.value === 'light'}
|
|
38
|
+
>
|
|
39
|
+
Light
|
|
40
|
+
</button>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onclick={function () {
|
|
44
|
+
theme.value = 'dark';
|
|
45
|
+
}}
|
|
46
|
+
data-selected={theme.value === 'dark'}
|
|
47
|
+
>
|
|
48
|
+
Dark
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onclick={function () {
|
|
53
|
+
theme.value = 'system';
|
|
54
|
+
}}
|
|
55
|
+
data-selected={theme.value === 'system'}
|
|
56
|
+
>
|
|
57
|
+
System
|
|
58
|
+
</button>
|
|
59
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type Theme = 'dark' | 'light' | 'system';
|
|
2
|
+
export interface CreateThemeContextConfig {
|
|
3
|
+
attribute?: 'class' | `data-${string}`;
|
|
4
|
+
storageKey?: string;
|
|
5
|
+
systemPreference?: boolean;
|
|
6
|
+
nonce?: string;
|
|
7
|
+
}
|
|
8
|
+
export type CreateThemeContextReturn = ReturnType<typeof createThemeContext>;
|
|
9
|
+
export declare function createThemeContext(config?: CreateThemeContextConfig): {
|
|
10
|
+
theme: Theme;
|
|
11
|
+
readonly effects: {
|
|
12
|
+
setup(): void;
|
|
13
|
+
themeChanged(): void;
|
|
14
|
+
osThemeChanged(): (() => void) | undefined;
|
|
15
|
+
};
|
|
16
|
+
readonly script: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export function createThemeContext(config) {
|
|
2
|
+
const {
|
|
3
|
+
/**/
|
|
4
|
+
attribute, storageKey, systemPreference, nonce, } = $derived.by(() => {
|
|
5
|
+
const attribute = config?.attribute ?? 'class';
|
|
6
|
+
const storageKey = config?.storageKey ?? 'theme';
|
|
7
|
+
const systemPreference = config?.systemPreference ?? true;
|
|
8
|
+
const nonce = config?.nonce ?? '';
|
|
9
|
+
return {
|
|
10
|
+
attribute,
|
|
11
|
+
storageKey,
|
|
12
|
+
systemPreference,
|
|
13
|
+
nonce,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
let theme = $state('system');
|
|
17
|
+
const script = $derived(buildScript({
|
|
18
|
+
nonce,
|
|
19
|
+
attribute,
|
|
20
|
+
storageKey,
|
|
21
|
+
}));
|
|
22
|
+
const effects = $derived({
|
|
23
|
+
setup() {
|
|
24
|
+
theme = getLocalStorageTheme(storageKey);
|
|
25
|
+
},
|
|
26
|
+
themeChanged() {
|
|
27
|
+
const html = document.documentElement;
|
|
28
|
+
const head = document.head;
|
|
29
|
+
const style = document.createElement('style');
|
|
30
|
+
style.innerHTML = noTransitionStyle;
|
|
31
|
+
head.appendChild(style);
|
|
32
|
+
const originalTheme = theme;
|
|
33
|
+
const resolvedTheme = originalTheme === 'system'
|
|
34
|
+
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
35
|
+
? 'dark'
|
|
36
|
+
: 'light'
|
|
37
|
+
: originalTheme;
|
|
38
|
+
if (attribute === 'class') {
|
|
39
|
+
const removeClass = resolvedTheme === 'dark' ? 'light' : 'dark';
|
|
40
|
+
html.classList.remove(removeClass);
|
|
41
|
+
html.classList.add(resolvedTheme);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
html.setAttribute(attribute, resolvedTheme);
|
|
45
|
+
}
|
|
46
|
+
localStorage.setItem(storageKey, originalTheme);
|
|
47
|
+
html.style.colorScheme = resolvedTheme;
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
head.removeChild(style);
|
|
50
|
+
}, 1);
|
|
51
|
+
},
|
|
52
|
+
osThemeChanged() {
|
|
53
|
+
if (!systemPreference)
|
|
54
|
+
return;
|
|
55
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
56
|
+
function handler(e) {
|
|
57
|
+
theme = e.matches ? 'dark' : 'light';
|
|
58
|
+
}
|
|
59
|
+
mediaQuery.addEventListener('change', handler);
|
|
60
|
+
return () => {
|
|
61
|
+
mediaQuery.removeEventListener('change', handler);
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
get theme() {
|
|
67
|
+
return theme;
|
|
68
|
+
},
|
|
69
|
+
set theme(value) {
|
|
70
|
+
theme = value;
|
|
71
|
+
},
|
|
72
|
+
get effects() {
|
|
73
|
+
return effects;
|
|
74
|
+
},
|
|
75
|
+
get script() {
|
|
76
|
+
return script;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function getLocalStorageTheme(key) {
|
|
81
|
+
const value = localStorage.getItem(key);
|
|
82
|
+
if (value?.toLocaleLowerCase().trim() === 'dark')
|
|
83
|
+
return 'dark';
|
|
84
|
+
if (value?.toLocaleLowerCase().trim() === 'light')
|
|
85
|
+
return 'light';
|
|
86
|
+
return 'system';
|
|
87
|
+
}
|
|
88
|
+
function buildScript({ nonce, attribute, storageKey, }) {
|
|
89
|
+
return `
|
|
90
|
+
<script nonce="${nonce}">
|
|
91
|
+
(function(k, a) {
|
|
92
|
+
let h = document.documentElement;
|
|
93
|
+
let q = window.matchMedia('(prefers-color-scheme: dark)')
|
|
94
|
+
let s = localStorage.getItem(k)?.toLowerCase().trim();
|
|
95
|
+
|
|
96
|
+
let l = [
|
|
97
|
+
'dark',
|
|
98
|
+
'light',
|
|
99
|
+
'system'
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
let v = l.includes(s) ? s : 'system';
|
|
103
|
+
let t = v === 'system' ? q.matches ? 'dark' : 'light' : v;
|
|
104
|
+
|
|
105
|
+
if (a === 'class') {
|
|
106
|
+
h.classList.remove(t === 'dark' ? 'light' : 'dark');
|
|
107
|
+
h.classList.add(t);
|
|
108
|
+
} else {
|
|
109
|
+
h.setAttribute(a, t);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
localStorage.setItem(k, v);
|
|
113
|
+
h.style.colorScheme = t;
|
|
114
|
+
})(
|
|
115
|
+
'${storageKey}',
|
|
116
|
+
'${attribute}',
|
|
117
|
+
);
|
|
118
|
+
</script>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
const noTransitionStyle = '*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;transition:none!important;}';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">import { setContext } from "svelte";
|
|
2
|
+
import {
|
|
3
|
+
createThemeContext
|
|
4
|
+
} from "./create-theme-context.svelte.js";
|
|
5
|
+
let { children, ...props } = $props();
|
|
6
|
+
let context = createThemeContext(props);
|
|
7
|
+
$effect(context.effects.setup);
|
|
8
|
+
$effect(context.effects.themeChanged);
|
|
9
|
+
$effect(context.effects.osThemeChanged);
|
|
10
|
+
setContext("theme", context);
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
{@render children?.()}
|
|
14
|
+
|
|
15
|
+
<svelte:head>
|
|
16
|
+
{@html context.script}
|
|
17
|
+
</svelte:head>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
import { type CreateThemeContextConfig } from './create-theme-context.svelte.js';
|
|
3
|
+
declare const __propDef: {
|
|
4
|
+
props: CreateThemeContextConfig & {
|
|
5
|
+
children?: ((this: void) => typeof import("svelte").SnippetReturn & {
|
|
6
|
+
_: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
|
|
7
|
+
}) | undefined;
|
|
8
|
+
};
|
|
9
|
+
events: {
|
|
10
|
+
[evt: string]: CustomEvent<any>;
|
|
11
|
+
};
|
|
12
|
+
slots: {};
|
|
13
|
+
};
|
|
14
|
+
export type ThemeProviderProps = typeof __propDef.props;
|
|
15
|
+
export type ThemeProviderEvents = typeof __propDef.events;
|
|
16
|
+
export type ThemeProviderSlots = typeof __propDef.slots;
|
|
17
|
+
export default class ThemeProvider extends SvelteComponent<ThemeProviderProps, ThemeProviderEvents, ThemeProviderSlots> {
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getContext } from 'svelte';
|
|
2
|
+
export default function useTheme() {
|
|
3
|
+
const context = getContext('theme');
|
|
4
|
+
return {
|
|
5
|
+
get value() {
|
|
6
|
+
return context.theme;
|
|
7
|
+
},
|
|
8
|
+
set value(theme) {
|
|
9
|
+
if (context) {
|
|
10
|
+
context.theme = theme;
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "svelte-os-themes",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.2",
|
|
5
|
+
"svelte": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"svelte": "./dist/index.js",
|
|
11
|
+
"package.json": "./package.json"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"!dist/**/*.test.*",
|
|
17
|
+
"!dist/**/*.spec.*"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"Svelte",
|
|
21
|
+
"SvelteKit",
|
|
22
|
+
"theme provider",
|
|
23
|
+
"theme",
|
|
24
|
+
"themes",
|
|
25
|
+
"dark mode"
|
|
26
|
+
],
|
|
27
|
+
"repository": "https://github.com/calvo-jp/svelte-themes",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/calvo-jp/svelte-themes/issues"
|
|
30
|
+
},
|
|
31
|
+
"author": {
|
|
32
|
+
"name": "John Paul Calvo",
|
|
33
|
+
"email": "calvojp92@gmail.com"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"svelte": ">=5.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|