From 733152f084e94e3e3a58664f3a9cc71564ecf39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20M=C3=A5nsson?= <31876997+ollema@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:22:42 +0100 Subject: [PATCH] feat: enable mode-watcher to manage the theme-color meta tag (#48) Co-authored-by: Hunter Johnston --- .changeset/witty-mangos-sip.md | 5 ++ .eslintrc.cjs | 14 +++--- .prettierrc | 2 +- README.md | 8 +++ playwright.config.ts | 4 +- postcss.config.cjs | 4 +- scripts/setupTest.ts | 26 +++++----- src/lib/index.ts | 6 +-- src/lib/mode-watcher.svelte | 45 ++++++++++------- src/lib/mode.ts | 33 +++++++++++-- src/lib/stores.ts | 90 +++++++++++++++++++++------------- src/lib/types.ts | 4 ++ src/routes/+layout.svelte | 2 +- src/routes/+page.svelte | 29 +++++++++++ src/tests/Mode.svelte | 2 +- src/tests/StealthMode.svelte | 2 +- src/tests/mode.spec.ts | 74 ++++++++++++++++++++++++++++ svelte.config.js | 4 +- tailwind.config.cjs | 28 +++++------ vite.config.ts | 6 +-- 20 files changed, 285 insertions(+), 103 deletions(-) create mode 100644 .changeset/witty-mangos-sip.md create mode 100644 src/lib/types.ts diff --git a/.changeset/witty-mangos-sip.md b/.changeset/witty-mangos-sip.md new file mode 100644 index 0000000..8cdd555 --- /dev/null +++ b/.changeset/witty-mangos-sip.md @@ -0,0 +1,5 @@ +--- +'mode-watcher': minor +--- + +Allow `mode-watcher` to manage the theme-color meta tag diff --git a/.eslintrc.cjs b/.eslintrc.cjs index ebc1958..19214a9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -4,27 +4,27 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:svelte/recommended', - 'prettier' + 'prettier', ], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], parserOptions: { sourceType: 'module', ecmaVersion: 2020, - extraFileExtensions: ['.svelte'] + extraFileExtensions: ['.svelte'], }, env: { browser: true, es2017: true, - node: true + node: true, }, overrides: [ { files: ['*.svelte'], parser: 'svelte-eslint-parser', parserOptions: { - parser: '@typescript-eslint/parser' - } - } - ] + parser: '@typescript-eslint/parser', + }, + }, + ], }; diff --git a/.prettierrc b/.prettierrc index a77fdde..da08664 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "useTabs": true, "singleQuote": true, - "trailingComma": "none", + "trailingComma": "es5", "printWidth": 100, "plugins": ["prettier-plugin-svelte"], "pluginSearchDirs": ["."], diff --git a/README.md b/README.md index 33f4fd8..eb52b2d 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,14 @@ To set a default mode, use the `defaultMode` prop: ``` +`ModeWatcher` can manage the `theme-color` meta tag for you. + +To enable this, set the `themeColor` prop to your preferred colors: + +```svelte + +``` + ## API ### toggleMode diff --git a/playwright.config.ts b/playwright.config.ts index 1c5d7a1..770c7ff 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,10 +3,10 @@ import type { PlaywrightTestConfig } from '@playwright/test'; const config: PlaywrightTestConfig = { webServer: { command: 'npm run build && npm run preview', - port: 4173 + port: 4173, }, testDir: 'tests', - testMatch: /(.+\.)?(test|spec)\.[jt]s/ + testMatch: /(.+\.)?(test|spec)\.[jt]s/, }; export default config; diff --git a/postcss.config.cjs b/postcss.config.cjs index fe10e55..045ced5 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -6,8 +6,8 @@ const config = { //Some plugins, like tailwindcss/nesting, need to run before Tailwind, tailwindcss(), //But others, like autoprefixer, need to run after, - autoprefixer - ] + autoprefixer, + ], }; module.exports = config; diff --git a/scripts/setupTest.ts b/scripts/setupTest.ts index ee636ba..72e2ed6 100644 --- a/scripts/setupTest.ts +++ b/scripts/setupTest.ts @@ -9,7 +9,7 @@ import type * as stores from '$app/stores'; import { configure } from '@testing-library/dom'; configure({ - asyncUtilTimeout: 1500 + asyncUtilTimeout: 1500, }); // Mock SvelteKit runtime module $app/environment @@ -17,7 +17,7 @@ vi.mock('$app/environment', (): typeof environment => ({ browser: false, dev: true, building: false, - version: 'any' + version: 'any', })); // Mock SvelteKit runtime module $app/navigation @@ -29,7 +29,7 @@ vi.mock('$app/navigation', (): typeof navigation => ({ invalidate: () => Promise.resolve(), invalidateAll: () => Promise.resolve(), preloadData: () => Promise.resolve(), - preloadCode: () => Promise.resolve() + preloadCode: () => Promise.resolve(), })); // Mock SvelteKit runtime module $app/stores @@ -40,12 +40,12 @@ vi.mock('$app/stores', (): typeof stores => { url: new URL('http://localhost'), params: {}, route: { - id: null + id: null, }, status: 200, error: null, data: {}, - form: undefined + form: undefined, }); const updated = { subscribe: readable(false).subscribe, check: async () => false }; @@ -55,30 +55,30 @@ vi.mock('$app/stores', (): typeof stores => { const page: typeof stores.page = { subscribe(fn) { return getStores().page.subscribe(fn); - } + }, }; const navigating: typeof stores.navigating = { subscribe(fn) { return getStores().navigating.subscribe(fn); - } + }, }; const updated: typeof stores.updated = { subscribe(fn) { return getStores().updated.subscribe(fn); }, - check: async () => false + check: async () => false, }; return { getStores, navigating, page, - updated + updated, }; }); export const mediaQueryState = { - matches: false + matches: false, }; const listeners: ((event: unknown) => void)[] = []; @@ -107,10 +107,10 @@ Object.defineProperty(window, 'matchMedia', { for (const callback of listeners) { callback({ matches: mediaQueryState.matches, - media: '(prefers-color-scheme: light)' + media: '(prefers-color-scheme: light)', }); } } - }) - })) + }), + })), }); diff --git a/src/lib/index.ts b/src/lib/index.ts index 2f717c0..56e4870 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -5,8 +5,8 @@ import { mode, setMode, toggleMode, - resetMode -} from './mode'; + resetMode, +} from './mode.js'; export { setMode, @@ -15,7 +15,7 @@ export { localStorageKey, userPrefersMode, systemPrefersMode, - mode + mode, }; export { default as ModeWatcher } from './mode-watcher.svelte'; diff --git a/src/lib/mode-watcher.svelte b/src/lib/mode-watcher.svelte index 0169b69..0d4a99e 100644 --- a/src/lib/mode-watcher.svelte +++ b/src/lib/mode-watcher.svelte @@ -1,37 +1,48 @@ + {#if themeColors} + + + + + {/if} - {@html ``} + {@html ``} diff --git a/src/lib/mode.ts b/src/lib/mode.ts index 7603291..5f161cf 100644 --- a/src/lib/mode.ts +++ b/src/lib/mode.ts @@ -1,5 +1,12 @@ import { get } from 'svelte/store'; -import { localStorageKey, userPrefersMode, systemPrefersMode, derivedMode } from './stores'; +import { + localStorageKey, + userPrefersMode, + systemPrefersMode, + derivedMode, + themeColors, +} from './stores.js'; +import type { Mode, ThemeColors } from './types.js'; /** Toggle between light and dark mode */ export function toggleMode(): void { @@ -7,7 +14,7 @@ export function toggleMode(): void { } /** Set the mode to light or dark */ -export function setMode(mode: 'dark' | 'light' | 'system'): void { +export function setMode(mode: Mode): void { userPrefersMode.set(mode); } @@ -16,4 +23,24 @@ export function resetMode(): void { userPrefersMode.set('system'); } -export { localStorageKey, userPrefersMode, systemPrefersMode, derivedMode as mode }; +export function setInitialMode(defaultMode: Mode, themeColors?: ThemeColors) { + const rootEl = document.documentElement; + const mode = localStorage.getItem('mode-watcher-mode') || defaultMode; + const light = + mode === 'light' || + (mode === 'system' && window.matchMedia('(prefers-color-scheme: light)').matches); + + rootEl.classList[light ? 'remove' : 'add']('dark'); + rootEl.style.colorScheme = light ? 'light' : 'dark'; + + if (themeColors) { + const themeMetaEl = document.querySelector('meta[name="theme-color"]'); + if (themeMetaEl) { + themeMetaEl.setAttribute('content', mode === 'light' ? themeColors.light : themeColors.dark); + } + } + + localStorage.setItem('mode', mode); +} + +export { localStorageKey, userPrefersMode, systemPrefersMode, derivedMode as mode, themeColors }; diff --git a/src/lib/stores.ts b/src/lib/stores.ts index 387a2a5..a0987df 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -1,21 +1,25 @@ import { writable, derived } from 'svelte/store'; -import { withoutTransition } from './without-transition'; +import { withoutTransition } from './without-transition.js'; +import type { Mode, ThemeColors } from './types.js'; // saves having to branch for server vs client const noopStorage = { // eslint-disable-next-line @typescript-eslint/no-unused-vars getItem: (_key: string) => null, // eslint-disable-next-line @typescript-eslint/no-unused-vars - setItem: (_key: string, _value: string) => {} + setItem: (_key: string, _value: string) => {}, }; // whether we are running on server vs client const isBrowser = typeof document !== 'undefined'; +// the modes that are supported, used for validation & type derivation +export const modes = ['dark', 'light', 'system'] as const; + /** * The key used to store the mode in localStorage. */ -export const localStorageKey = 'mode'; +export const localStorageKey = 'mode-watcher-mode'; /** * Writable store that represents the user's preferred mode (`"dark"`, `"light"` or `"system"`) */ @@ -24,6 +28,10 @@ export const userPrefersMode = createUserPrefersMode(); * Readable store that represents the system's preferred mode (`"dark"`, `"light"` or `undefined`) */ export const systemPrefersMode = createSystemMode(); +/** + * Theme colors for light and dark modes. + */ +export const themeColors = writable(undefined); /** * Derived store that represents the current mode (`"dark"`, `"light"` or `undefined`) */ @@ -34,28 +42,33 @@ function createUserPrefersMode() { const defaultValue = 'system'; const storage = isBrowser ? localStorage : noopStorage; - let value = (storage.getItem(localStorageKey) as 'dark' | 'light' | 'system') || defaultValue; + const initialValue = storage.getItem(localStorageKey); + + let value = isValidMode(initialValue) ? initialValue : defaultValue; const { subscribe, set: _set } = writable(value, () => { - if (isBrowser) { - const handler = (e: StorageEvent) => { - if (e.key === localStorageKey) { - _set((value = (e.newValue as 'dark' | 'light' | 'system') || defaultValue)); - } - }; - addEventListener('storage', handler); - return () => removeEventListener('storage', handler); - } + if (!isBrowser) return; + const handler = (e: StorageEvent) => { + if (e.key !== localStorageKey) return; + const newValue = e.newValue; + if (isValidMode(newValue)) { + _set((value = newValue)); + } else { + _set((value = defaultValue)); + } + }; + addEventListener('storage', handler); + return () => removeEventListener('storage', handler); }); - function set(v: 'dark' | 'light' | 'system') { + function set(v: Mode) { _set((value = v)); storage.setItem(localStorageKey, value); } return { subscribe, - set + set, }; } @@ -64,26 +77,25 @@ function createSystemMode() { let track = true; const { subscribe, set } = writable<'dark' | 'light' | undefined>(defaultValue, () => { - if (isBrowser) { - const handler = (e: MediaQueryListEvent) => { - if (track) { - set(e.matches ? 'light' : 'dark'); - } - }; - const mediaQueryState = window.matchMedia('(prefers-color-scheme: light)'); - mediaQueryState.addEventListener('change', handler); - return () => mediaQueryState.removeEventListener('change', handler); - } + if (!isBrowser) return; + + const handler = (e: MediaQueryListEvent) => { + if (!track) return; + set(e.matches ? 'light' : 'dark'); + }; + + const mediaQueryState = window.matchMedia('(prefers-color-scheme: light)'); + mediaQueryState.addEventListener('change', handler); + return () => mediaQueryState.removeEventListener('change', handler); }); /** * Query system preferences and update the store. */ function query() { - if (isBrowser) { - const mediaQueryState = window.matchMedia('(prefers-color-scheme: light)'); - set(mediaQueryState.matches ? 'light' : 'dark'); - } + if (!isBrowser) return; + const mediaQueryState = window.matchMedia('(prefers-color-scheme: light)'); + set(mediaQueryState.matches ? 'light' : 'dark'); } /** @@ -96,26 +108,33 @@ function createSystemMode() { return { subscribe, query, - tracking + tracking, }; } function createDerivedMode() { const { subscribe } = derived( - [userPrefersMode, systemPrefersMode], - ([$userPrefersMode, $systemPrefersMode]) => { + [userPrefersMode, systemPrefersMode, themeColors], + ([$userPrefersMode, $systemPrefersMode, $themeColors]) => { if (!isBrowser) return undefined; const derivedMode = $userPrefersMode === 'system' ? $systemPrefersMode : $userPrefersMode; withoutTransition(() => { const htmlEl = document.documentElement; + const themeColorEl = document.querySelector('meta[name="theme-color"]'); if (derivedMode === 'light') { htmlEl.classList.remove('dark'); htmlEl.style.colorScheme = 'light'; + if (themeColorEl && $themeColors) { + themeColorEl.setAttribute('content', $themeColors.light); + } } else { htmlEl.classList.add('dark'); htmlEl.style.colorScheme = 'dark'; + if (themeColorEl && $themeColors) { + themeColorEl.setAttribute('content', $themeColors.dark); + } } }); @@ -124,6 +143,11 @@ function createDerivedMode() { ); return { - subscribe + subscribe, }; } + +export function isValidMode(value: unknown): value is Mode { + if (typeof value !== 'string') return false; + return modes.includes(value as Mode); +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..a9d3674 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,4 @@ +import type { modes } from './stores'; + +export type Mode = typeof modes[number]; +export type ThemeColors = { dark: string; light: string } | undefined; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 762eb89..00da0a4 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,5 +3,5 @@ import ModeWatcher from '$lib/mode-watcher.svelte'; - + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2f80ac2..5a8f24f 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,11 +1,40 @@

User prefers mode: {$userPrefersMode}

System prefers mode: {$systemPrefersMode}

Current mode: {$mode}

+ + {#if $htmlElement !== undefined} +
{$htmlElement}
+ {/if} + {#if $themeColorElement !== undefined} +
{$themeColorElement}
+ {/if} + diff --git a/src/tests/Mode.svelte b/src/tests/Mode.svelte index 9b3371b..8a94510 100644 --- a/src/tests/Mode.svelte +++ b/src/tests/Mode.svelte @@ -4,7 +4,7 @@ export let track = true; - + {$mode} diff --git a/src/tests/StealthMode.svelte b/src/tests/StealthMode.svelte index a088a5b..00e22df 100644 --- a/src/tests/StealthMode.svelte +++ b/src/tests/StealthMode.svelte @@ -4,7 +4,7 @@ export let track = true; - + diff --git a/src/tests/mode.spec.ts b/src/tests/mode.spec.ts index 1d214bb..3666490 100644 --- a/src/tests/mode.spec.ts +++ b/src/tests/mode.spec.ts @@ -19,19 +19,25 @@ it('toggles the mode', async () => { const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); + const themeColor = getThemeColor(rootEl); expect(classes).toContain('dark'); expect(colorScheme).toBe('dark'); + expect(themeColor).toBe('black'); const toggle = getByTestId('toggle'); await userEvent.click(toggle); const classes2 = getClasses(rootEl); const colorScheme2 = getColorScheme(rootEl); + const themeColor2 = getThemeColor(rootEl); expect(classes2).not.toContain('dark'); expect(colorScheme2).toBe('light'); + expect(themeColor2).toBe('white'); await userEvent.click(toggle); const classes3 = getClasses(rootEl); const colorScheme3 = getColorScheme(rootEl); + const themeColor3 = getThemeColor(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); + expect(themeColor3).toBe('black'); }); it('allows the user to set the mode', async () => { @@ -39,21 +45,27 @@ it('allows the user to set the mode', async () => { const rootEl = container.parentElement; const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); + const themeColor = getThemeColor(rootEl); expect(classes).toContain('dark'); expect(colorScheme).toBe('dark'); + expect(themeColor).toBe('black'); const light = getByTestId('light'); await userEvent.click(light); const classes2 = getClasses(rootEl); const colorScheme2 = getColorScheme(rootEl); + const themeColor2 = getThemeColor(rootEl); expect(classes2).not.toContain('dark'); expect(colorScheme2).toBe('light'); + expect(themeColor2).toBe('white'); const dark = getByTestId('dark'); await userEvent.click(dark); const classes3 = getClasses(rootEl); const colorScheme3 = getColorScheme(rootEl); + const themeColor3 = getThemeColor(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); + expect(themeColor3).toBe('black'); }); it('keeps the mode store in sync with current mode', async () => { @@ -64,22 +76,28 @@ it('keeps the mode store in sync with current mode', async () => { const mode = getByTestId('mode'); const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); + const themeColor = getThemeColor(rootEl); expect(classes).toContain('dark'); expect(colorScheme).toBe('dark'); + expect(themeColor).toBe('black'); expect(mode.textContent).toBe('dark'); await userEvent.click(light); const classes2 = getClasses(rootEl); const colorScheme2 = getColorScheme(rootEl); + const themeColor2 = getThemeColor(rootEl); expect(classes2).not.toContain('dark'); expect(colorScheme2).toBe('light'); + expect(themeColor2).toBe('white'); expect(mode.textContent).toBe('light'); await userEvent.click(dark); const classes3 = getClasses(rootEl); const colorScheme3 = getColorScheme(rootEl); + const themeColor3 = getThemeColor(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); + expect(themeColor3).toBe('black'); expect(mode.textContent).toBe('dark'); }); @@ -91,22 +109,28 @@ it('resets the mode to system preferences', async () => { const mode = getByTestId('mode'); const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); + const themeColor = getThemeColor(rootEl); expect(classes).toContain('dark'); expect(colorScheme).toBe('dark'); + expect(themeColor).toBe('black'); expect(mode.textContent).toBe('dark'); await userEvent.click(light); const classes2 = getClasses(rootEl); const colorScheme2 = getColorScheme(rootEl); + const themeColor2 = getThemeColor(rootEl); expect(classes2).not.toContain('dark'); expect(colorScheme2).toBe('light'); + expect(themeColor2).toBe('white'); expect(mode.textContent).toBe('light'); await userEvent.click(reset); const classes3 = getClasses(rootEl); const colorScheme3 = getColorScheme(rootEl); + const themeColor3 = getThemeColor(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); + expect(themeColor3).toBe('black'); expect(mode.textContent).toBe('dark'); }); @@ -116,8 +140,10 @@ it('tracks changes to system preferences', async () => { const mode = getByTestId('mode'); const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); + const themeColor = getThemeColor(rootEl); expect(classes).toContain('dark'); expect(colorScheme).toBe('dark'); + expect(themeColor).toBe('black'); expect(mode.textContent).toBe('dark'); mediaQueryState.matches = true; @@ -126,8 +152,10 @@ it('tracks changes to system preferences', async () => { await tick(); const classes2 = getClasses(rootEl); const colorScheme2 = getColorScheme(rootEl); + const themeColor2 = getThemeColor(rootEl); expect(classes2).not.toContain('dark'); expect(colorScheme2).toBe('light'); + expect(themeColor2).toBe('white'); expect(mode.textContent).toBe('light'); mediaQueryState.matches = false; @@ -135,8 +163,10 @@ it('tracks changes to system preferences', async () => { await tick(); const classes3 = getClasses(rootEl); const colorScheme3 = getColorScheme(rootEl); + const themeColor3 = getThemeColor(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); + expect(themeColor3).toBe('black'); expect(mode.textContent).toBe('dark'); }); @@ -148,8 +178,10 @@ it('stops tracking changes to system preferences when user sets a mode', async ( const mode = getByTestId('mode'); const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); + const themeColor = getThemeColor(rootEl); expect(classes).toContain('dark'); expect(colorScheme).toBe('dark'); + expect(themeColor).toBe('black'); expect(mode.textContent).toBe('dark'); mediaQueryState.matches = true; @@ -158,8 +190,10 @@ it('stops tracking changes to system preferences when user sets a mode', async ( await tick(); const classes2 = getClasses(rootEl); const colorScheme2 = getColorScheme(rootEl); + const themeColor2 = getThemeColor(rootEl); expect(classes2).not.toContain('dark'); expect(colorScheme2).toBe('light'); + expect(themeColor2).toBe('white'); expect(mode.textContent).toBe('light'); mediaQueryState.matches = false; @@ -167,15 +201,19 @@ it('stops tracking changes to system preferences when user sets a mode', async ( await tick(); const classes3 = getClasses(rootEl); const colorScheme3 = getColorScheme(rootEl); + const themeColor3 = getThemeColor(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); + expect(themeColor3).toBe('black'); expect(mode.textContent).toBe('dark'); await userEvent.click(light); const classes4 = getClasses(rootEl); const colorScheme4 = getColorScheme(rootEl); + const themeColor4 = getThemeColor(rootEl); expect(classes4).not.toContain('dark'); expect(colorScheme4).toBe('light'); + expect(themeColor4).toBe('white'); expect(mode.textContent).toBe('light'); mediaQueryState.matches = true; @@ -183,8 +221,10 @@ it('stops tracking changes to system preferences when user sets a mode', async ( await tick(); const classes5 = getClasses(rootEl); const colorScheme5 = getColorScheme(rootEl); + const themeColor5 = getThemeColor(rootEl); expect(classes5).not.toContain('dark'); expect(colorScheme5).toBe('light'); + expect(themeColor5).toBe('white'); expect(mode.textContent).toBe('light'); mediaQueryState.matches = false; @@ -192,15 +232,19 @@ it('stops tracking changes to system preferences when user sets a mode', async ( await tick(); const classes6 = getClasses(rootEl); const colorScheme6 = getColorScheme(rootEl); + const themeColor6 = getThemeColor(rootEl); expect(classes6).not.toContain('dark'); expect(colorScheme6).toBe('light'); + expect(themeColor6).toBe('white'); expect(mode.textContent).toBe('light'); await userEvent.click(reset); const classes7 = getClasses(rootEl); const colorScheme7 = getColorScheme(rootEl); + const themeColor7 = getThemeColor(rootEl); expect(classes7).toContain('dark'); expect(colorScheme7).toBe('dark'); + expect(themeColor7).toBe('black'); expect(mode.textContent).toBe('dark'); }); @@ -210,8 +254,10 @@ it('does not track changes to system preference when track prop is set to false' const mode = getByTestId('mode'); const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); + const themeColor = getThemeColor(rootEl); expect(classes).toContain('dark'); expect(colorScheme).toBe('dark'); + expect(themeColor).toBe('black'); expect(mode.textContent).toBe('dark'); mediaQueryState.matches = true; @@ -220,8 +266,10 @@ it('does not track changes to system preference when track prop is set to false' await tick(); const classes2 = getClasses(rootEl); const colorScheme2 = getColorScheme(rootEl); + const themeColor2 = getThemeColor(rootEl); expect(classes2).toContain('dark'); expect(colorScheme2).toBe('dark'); + expect(themeColor2).toBe('black'); expect(mode.textContent).toBe('dark'); mediaQueryState.matches = false; @@ -229,8 +277,10 @@ it('does not track changes to system preference when track prop is set to false' await tick(); const classes3 = getClasses(rootEl); const colorScheme3 = getColorScheme(rootEl); + const themeColor3 = getThemeColor(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); + expect(themeColor3).toBe('black'); expect(mode.textContent).toBe('dark'); }); @@ -240,19 +290,25 @@ it('also works when $mode is not used in the current page', async () => { const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); + const themeColor = getThemeColor(rootEl); expect(classes).toContain('dark'); expect(colorScheme).toBe('dark'); + expect(themeColor).toBe('black'); const toggle = getByTestId('toggle'); await userEvent.click(toggle); const classes2 = getClasses(rootEl); const colorScheme2 = getColorScheme(rootEl); + const themeColor2 = getThemeColor(rootEl); expect(classes2).not.toContain('dark'); expect(colorScheme2).toBe('light'); + expect(themeColor2).toBe('white'); await userEvent.click(toggle); const classes3 = getClasses(rootEl); const colorScheme3 = getColorScheme(rootEl); + const themeColor3 = getThemeColor(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); + expect(themeColor3).toBe('black'); }); function getClasses(element: HTMLElement | null): string[] { @@ -269,3 +325,21 @@ function getColorScheme(element: HTMLElement | null) { } return element.style.colorScheme; } + +function getThemeColor(element: HTMLElement | null) { + if (element === null) { + return ''; + } + + const themeMetaEl = element.querySelector('meta[name="theme-color"]'); + if (themeMetaEl === null) { + return ''; + } + + const content = themeMetaEl.getAttribute('content'); + if (content === null) { + return ''; + } + + return content; +} diff --git a/svelte.config.js b/svelte.config.js index cf464a5..68ed9ff 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -11,8 +11,8 @@ const config = { // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // If your environment is not supported or you settled on a specific environment, switch out the adapter. // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter() - } + adapter: adapter(), + }, }; export default config; diff --git a/tailwind.config.cjs b/tailwind.config.cjs index e75c052..b32de6a 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -6,8 +6,8 @@ const config = { center: true, padding: '2rem', screens: { - '2xl': '1400px' - } + '2xl': '1400px', + }, }, extend: { colors: { @@ -18,42 +18,42 @@ const config = { foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' + foreground: 'hsl(var(--primary-foreground))', }, secondary: { DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' + foreground: 'hsl(var(--secondary-foreground))', }, destructive: { DEFAULT: 'hsl(var(--destructive) / )', - foreground: 'hsl(var(--destructive-foreground) / )' + foreground: 'hsl(var(--destructive-foreground) / )', }, muted: { DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' + foreground: 'hsl(var(--muted-foreground))', }, accent: { DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' + foreground: 'hsl(var(--accent-foreground))', }, popover: { DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' + foreground: 'hsl(var(--popover-foreground))', }, card: { DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - } + foreground: 'hsl(var(--card-foreground))', + }, }, borderRadius: { xl: `calc(var(--radius) + 4px)`, lg: `var(--radius)`, md: `calc(var(--radius) - 2px)`, - sm: 'calc(var(--radius) - 4px)' - } + sm: 'calc(var(--radius) - 4px)', + }, }, - plugins: [] - } + plugins: [], + }, }; module.exports = config; diff --git a/vite.config.ts b/vite.config.ts index d44282c..a301f81 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,8 +14,8 @@ export default defineConfig({ setupFiles: ['./scripts/setupTest.ts'], // Exclude files in v8 coverage: { - exclude: ['setupTest.ts'] + exclude: ['setupTest.ts'], }, - alias: [{ find: /^svelte$/, replacement: 'svelte/internal' }] - } + alias: [{ find: /^svelte$/, replacement: 'svelte/internal' }], + }, });