Skip to content

Commit 30648d0

Browse files
authored
respect system theme, without FOUC (#1087)
* respect system theme, without FOUC * fix * drive-by fix
1 parent ced06c3 commit 30648d0

File tree

13 files changed

+104
-68
lines changed

13 files changed

+104
-68
lines changed

Diff for: apps/svelte.dev/src/app.html

-5
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@
1919
/>
2020

2121
<script>
22-
document.documentElement.classList.add(
23-
JSON.parse(localStorage.getItem('svelte:theme'))?.current ??
24-
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
25-
);
26-
2722
document.documentElement.classList.add(
2823
`font-${localStorage.getItem('svelte:font') ?? 'elegant'}`
2924
);

Diff for: apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.svelte

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { afterNavigate, goto, replaceState } from '$app/navigation';
66
import type { Gist } from '$lib/db/types';
77
import { Repl } from '@sveltejs/repl';
8-
import { theme } from '@sveltejs/site-kit/stores';
8+
import { theme } from '@sveltejs/site-kit/state';
99
import { mapbox_setup } from '../../../../config.js';
1010
import AppControls from './AppControls.svelte';
1111
import { compress_and_encode_text, decode_and_decompress_text } from './gzip.js';
@@ -240,7 +240,7 @@
240240
injectedJS={mapbox_setup}
241241
{onchange}
242242
{download}
243-
previewTheme={$theme.current}
243+
previewTheme={theme.current}
244244
/>
245245
</div>
246246
{/if}

Diff for: apps/svelte.dev/src/routes/(authed)/playground/[id]/embed/+page.svelte

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { browser } from '$app/environment';
33
import { afterNavigate, replaceState } from '$app/navigation';
4-
import { theme } from '@sveltejs/site-kit/stores';
4+
import { theme } from '@sveltejs/site-kit/state';
55
import { Repl } from '@sveltejs/repl';
66
import { mapbox_setup } from '../../../../../config.js';
77
import { page } from '$app/state';
@@ -50,7 +50,7 @@
5050
{relaxed}
5151
can_escape
5252
injectedJS={mapbox_setup}
53-
previewTheme={$theme.current}
53+
previewTheme={theme.current}
5454
embedded
5555
/>
5656
{/if}

Diff for: apps/svelte.dev/src/routes/tutorial/[...slug]/Output.svelte

+12-15
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<script lang="ts">
66
import { browser, dev } from '$app/environment';
77
import { afterNavigate } from '$app/navigation';
8-
import { theme, type Theme } from '@sveltejs/site-kit/stores';
8+
import { theme } from '@sveltejs/site-kit/state';
99
import { onMount } from 'svelte';
1010
import Chrome from './Chrome.svelte';
1111
import Loading from './Loading.svelte';
@@ -43,18 +43,6 @@
4343
clearTimeout(timeout);
4444
});
4545
46-
function change_theme(theme: Theme) {
47-
if (!iframe) return;
48-
49-
try {
50-
const url = new URL(iframe.src);
51-
52-
url.searchParams.set('theme', theme.current);
53-
54-
iframe.src = url.href;
55-
} catch {}
56-
}
57-
5846
let timeout: any;
5947
6048
async function handle_message(e: MessageEvent) {
@@ -97,7 +85,7 @@
9785
parentNode?.removeChild(iframe);
9886
9987
const url = new URL(src);
100-
url.searchParams.set('theme', $theme.current);
88+
url.searchParams.set('theme', theme.current);
10189
10290
iframe.src = url.href;
10391
parentNode?.appendChild(iframe);
@@ -112,8 +100,17 @@
112100
$effect(() => {
113101
if (adapter_state.base) set_iframe_src(adapter_state.base + (path = exercise.path));
114102
});
103+
115104
$effect(() => {
116-
change_theme($theme);
105+
if (!iframe) return;
106+
107+
try {
108+
const url = new URL(iframe.src);
109+
110+
url.searchParams.set('theme', theme.current);
111+
112+
iframe.src = url.href;
113+
} catch {}
117114
});
118115
</script>
119116

Diff for: apps/svelte.dev/src/routes/tutorial/[...slug]/OutputRollup.svelte

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import Viewer from '@sveltejs/repl/viewer';
55
// @ts-expect-error TODO types
66
import Console, { type Log } from '@sveltejs/repl/console';
7-
import { theme } from '@sveltejs/site-kit/stores';
7+
import { theme } from '@sveltejs/site-kit/state';
88
import Chrome from './Chrome.svelte';
99
import Loading from './Loading.svelte';
1010
import { adapter_state, update } from './adapter.svelte';
@@ -37,7 +37,7 @@
3737
can_escape
3838
onLog={(l: Log[]) => (logs = l)}
3939
{bundle}
40-
theme={$theme.current}
40+
theme={theme.current}
4141
injectedCSS="@import '/tutorial/shared.css';"
4242
/>
4343
{/if}

Diff for: packages/repl/src/lib/Input/RunesInfo.svelte

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
let { runes }: { runes: boolean } = $props();
66
77
const { workspace, svelteVersion } = get_repl_context();
8-
const majorVersion = Number(svelteVersion.split(".")[0]);
8+
const majorVersion = Number(svelteVersion.split('.')[0]);
99
</script>
1010

1111
<Dropdown align="right">
@@ -19,8 +19,8 @@
1919
<div class="popup">
2020
{#if Number.isInteger(majorVersion) && majorVersion < 5}
2121
<p>
22-
<a href="/blog/runes">Runes</a> are available from Svelte 5 onwards, and this playground is
23-
using Svelte {svelteVersion}.
22+
<a href="/blog/runes">Runes</a> are available from Svelte 5 onwards, and this playground
23+
is using Svelte {svelteVersion}.
2424
</p>
2525
{:else if workspace.current.name.endsWith('.svelte.js')}
2626
<p>

Diff for: packages/site-kit/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
"./actions": {
7474
"default": "./src/lib/actions/index.ts"
7575
},
76+
"./state": {
77+
"default": "./src/lib/state/index.ts"
78+
},
7679
"./stores": {
7780
"default": "./src/lib/stores/index.ts"
7881
},

Diff for: packages/site-kit/src/lib/components/ThemeToggle.svelte

+21-15
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
<script lang="ts">
2-
import { on } from 'svelte/events';
3-
import { theme } from '../stores';
2+
import { theme } from '../state';
43
54
function toggle() {
6-
const next = $theme.current === 'light' ? 'dark' : 'light';
7-
const system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
8-
9-
$theme.preference = next === system ? 'system' : next;
10-
$theme.current = next;
5+
theme.current = theme.current === 'light' ? 'dark' : 'light';
116
}
127
138
$effect(() => {
14-
if ($theme.preference === 'system') {
15-
const query = window.matchMedia('(prefers-color-scheme: dark)');
16-
17-
return on(query, 'change', (e) => {
18-
$theme.current = e.matches ? 'dark' : 'light';
19-
});
20-
}
9+
document.documentElement.classList.remove('light', 'dark');
10+
document.documentElement.classList.add(theme.current);
2111
});
2212
</script>
2313

14+
<svelte:head>
15+
<script>
16+
{
17+
const theme = localStorage.getItem('sv:theme');
18+
19+
document.documentElement.classList.add(
20+
theme === 'system'
21+
? window.matchMedia('(prefers-color-scheme: dark)').matches
22+
? 'dark'
23+
: 'light'
24+
: theme
25+
);
26+
}
27+
</script>
28+
</svelte:head>
29+
2430
<button
2531
onclick={toggle}
2632
class="raised icon"
2733
type="button"
28-
aria-pressed={$theme.current === 'dark'}
34+
aria-pressed={theme.current === 'dark'}
2935
aria-label="Toggle dark mode"
3036
></button>
3137

Diff for: packages/site-kit/src/lib/state/Persisted.svelte.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { on } from 'svelte/events';
2+
import { createSubscriber } from 'svelte/reactivity';
3+
4+
export class Persisted<T extends string = string> {
5+
#key: string;
6+
#storage: Storage | undefined;
7+
#fallback: T;
8+
#version = $state(0);
9+
10+
#subscribe = createSubscriber((update) => {
11+
return on(window, 'storage', (e) => {
12+
if (e.key === this.#key) {
13+
update();
14+
}
15+
});
16+
});
17+
18+
constructor(
19+
key: string,
20+
fallback: T,
21+
storage = typeof localStorage === 'undefined' ? undefined : localStorage
22+
) {
23+
this.#key = key;
24+
this.#fallback = fallback;
25+
this.#storage = storage;
26+
}
27+
28+
get current() {
29+
this.#subscribe(); // handle cross-tab updates
30+
this.#version; // handle same-tab updates
31+
32+
return (this.#storage?.getItem(this.#key) as T) ?? this.#fallback;
33+
}
34+
35+
set current(v: T) {
36+
this.#storage?.setItem(this.#key, v);
37+
this.#version += 1;
38+
}
39+
}

Diff for: packages/site-kit/src/lib/state/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Persisted } from './Persisted.svelte';
2+
export { theme } from './theme.svelte';

Diff for: packages/site-kit/src/lib/state/theme.svelte.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MediaQuery } from 'svelte/reactivity';
2+
import { Persisted } from './Persisted.svelte';
3+
4+
class Theme {
5+
#preference = new Persisted<'system' | 'light' | 'dark'>('sv:theme', 'system');
6+
#query = new MediaQuery('prefers-color-scheme: dark');
7+
#system = $derived<'dark' | 'light'>(this.#query.current ? 'dark' : 'light');
8+
9+
get current() {
10+
return this.#preference.current === 'system' ? this.#system : this.#preference.current;
11+
}
12+
13+
set current(value: 'light' | 'dark') {
14+
this.#preference.current = value === this.#system ? 'system' : value;
15+
}
16+
}
17+
18+
export const theme = new Theme();

Diff for: packages/site-kit/src/lib/stores/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@ export { mql } from './mql';
22
export { nav_open, on_this_page_open, overlay_open, should_nav_autohide } from './nav';
33
export { reduced_motion } from './reduced-motion';
44
export { search_query, search_recent, searching } from './search';
5-
export { theme, type Theme } from './theme';

Diff for: packages/site-kit/src/lib/stores/theme.ts

-23
This file was deleted.

0 commit comments

Comments
 (0)