Add theme selector
This commit is contained in:
parent
9cd717d5b3
commit
620f7593e7
7 changed files with 146 additions and 43 deletions
|
|
@ -15,10 +15,8 @@
|
||||||
import LogOut from "@app/components/LogOut.svelte"
|
import LogOut from "@app/components/LogOut.svelte"
|
||||||
import {user} from "@app/core"
|
import {user} from "@app/core"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {theme} from "@app/theme"
|
|
||||||
|
|
||||||
const logout = () => pushModal(LogOut)
|
const logout = () => pushModal(LogOut)
|
||||||
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
@ -74,7 +72,7 @@
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3 items-center opacity-75 text-sm">
|
<div class="flex gap-3 items-center opacity-75 text-sm">
|
||||||
<Button onclick={toggleTheme}>Theme</Button>
|
<Link replaceState href="/settings/theme">Theme</Link>
|
||||||
/
|
/
|
||||||
<Link replaceState href="/settings/about">About</Link>
|
<Link replaceState href="/settings/about">About</Link>
|
||||||
/
|
/
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $userSpaceUrls.includes(url)}
|
{#if $userSpaceUrls.includes(url)}
|
||||||
<div class="absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary text-primary-content flex justify-center items-center">
|
<div
|
||||||
|
class="absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary text-primary-content flex justify-center items-center">
|
||||||
<Icon icon={CheckCircle} class="scale-120" />
|
<Icon icon={CheckCircle} class="scale-120" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import twColors from "tailwindcss/colors"
|
import twColors from "tailwindcss/colors"
|
||||||
import {kv} from "@app/storage"
|
import {derived, readable} from "svelte/store"
|
||||||
import {synced} from "@welshman/store"
|
import {synced} from "@welshman/store"
|
||||||
|
import {FL_THEME} from "@app/env"
|
||||||
|
import {kv} from "@app/storage"
|
||||||
|
|
||||||
export const colors = [
|
export const colors = [
|
||||||
["amber", twColors.amber[600]],
|
["amber", twColors.amber[600]],
|
||||||
|
|
@ -24,15 +26,35 @@ export const colors = [
|
||||||
["zinc", twColors.zinc[600]],
|
["zinc", twColors.zinc[600]],
|
||||||
]
|
]
|
||||||
|
|
||||||
export const theme = synced({
|
// Every theme with token values in lib/components/theme.css
|
||||||
key: "theme",
|
export const flThemes = ["clay", "flat", "navy"]
|
||||||
defaultValue: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
|
||||||
|
export const flTheme = synced({
|
||||||
|
key: "flTheme",
|
||||||
|
defaultValue: FL_THEME,
|
||||||
storage: kv,
|
storage: kv,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook for development
|
export const theme = synced({
|
||||||
Object.assign(window, {
|
key: "theme",
|
||||||
setFlTheme: (t: string) => {
|
defaultValue: "system",
|
||||||
document.body.setAttribute("data-fl-theme", t)
|
storage: kv,
|
||||||
},
|
})
|
||||||
|
|
||||||
|
const prefersDark = readable(window.matchMedia("(prefers-color-scheme: dark)").matches, set => {
|
||||||
|
const query = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
const onChange = () => set(query.matches)
|
||||||
|
|
||||||
|
query.addEventListener("change", onChange)
|
||||||
|
|
||||||
|
return () => query.removeEventListener("change", onChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
// What actually gets stamped on the document, since `theme` may defer to the os
|
||||||
|
export const activeTheme = derived([theme, prefersDark], ([$theme, $prefersDark]) => {
|
||||||
|
if ($theme === "system") {
|
||||||
|
return $prefersDark ? "dark" : "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $theme
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
import {setupShareIntents, shareFromNative} from "@app/share"
|
import {setupShareIntents, shareFromNative} from "@app/share"
|
||||||
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
||||||
import * as env from "@app/env"
|
import * as env from "@app/env"
|
||||||
import {theme} from "@app/theme"
|
import {activeTheme, flTheme, theme} from "@app/theme"
|
||||||
import {toast, pushToast} from "@app/toast"
|
import {toast, pushToast} from "@app/toast"
|
||||||
import * as notifications from "@app/notifications"
|
import * as notifications from "@app/notifications"
|
||||||
import {notifications as notificationPaths, allNotifications} from "@app/notifications"
|
import {notifications as notificationPaths, allNotifications} from "@app/notifications"
|
||||||
|
|
@ -44,9 +44,17 @@
|
||||||
|
|
||||||
const {children} = $props()
|
const {children} = $props()
|
||||||
|
|
||||||
// Do this asap to avoid a font size flash
|
// Do this asap to avoid a flash of the wrong font size or theme. The stores these mirror live in
|
||||||
|
// indexeddb, which doesn't load until well after first paint.
|
||||||
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
document.documentElement.style["font-size"] = `${localStorage.getItem("font-size") || 1.1}rem`
|
document.documentElement.style["font-size"] = `${localStorage.getItem("font-size") || 1.1}rem`
|
||||||
|
document.body.setAttribute("data-fl-theme", localStorage.getItem("fl-theme") || env.FL_THEME)
|
||||||
|
document.body.setAttribute(
|
||||||
|
"data-theme",
|
||||||
|
localStorage.getItem("theme") || (prefersDark ? "dark" : "light"),
|
||||||
|
)
|
||||||
|
|
||||||
// Add stuff to window for convenience
|
// Add stuff to window for convenience
|
||||||
Object.assign(window, {get, nip19, theme, app, domain, Logger, ...lib, ...util})
|
Object.assign(window, {get, nip19, theme, app, domain, Logger, ...lib, ...util})
|
||||||
|
|
@ -231,9 +239,13 @@
|
||||||
|
|
||||||
// Sync theme and font size
|
// Sync theme and font size
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
theme.subscribe($theme => {
|
activeTheme.subscribe($activeTheme => {
|
||||||
document.body.setAttribute("data-theme", $theme)
|
localStorage.setItem("theme", $activeTheme)
|
||||||
document.body.setAttribute("data-fl-theme", env.FL_THEME)
|
document.body.setAttribute("data-theme", $activeTheme)
|
||||||
|
}),
|
||||||
|
flTheme.subscribe($flTheme => {
|
||||||
|
localStorage.setItem("fl-theme", $flTheme)
|
||||||
|
document.body.setAttribute("data-fl-theme", $flTheme)
|
||||||
}),
|
}),
|
||||||
userSettingsValues.subscribe(
|
userSettingsValues.subscribe(
|
||||||
debounce(100, $settings => {
|
debounce(100, $settings => {
|
||||||
|
|
@ -273,7 +285,7 @@
|
||||||
{#await unsubscribe}
|
{#await unsubscribe}
|
||||||
<!-- pass -->
|
<!-- pass -->
|
||||||
{:then}
|
{:then}
|
||||||
<div class={isMobile ? "fl mobile" : "fl"} data-fl-theme={env.FL_THEME}>
|
<div class={isMobile ? "fl mobile" : "fl"} data-fl-theme={$flTheme}>
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
import LogOut from "@app/components/LogOut.svelte"
|
import LogOut from "@app/components/LogOut.svelte"
|
||||||
import {HOSTING_ENABLED} from "@app/hosting"
|
import {HOSTING_ENABLED} from "@app/hosting"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {theme} from "@app/theme"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: Snippet
|
children?: Snippet
|
||||||
|
|
@ -28,8 +27,6 @@
|
||||||
const {children}: Props = $props()
|
const {children}: Props = $props()
|
||||||
|
|
||||||
const logout = () => pushModal(LogOut)
|
const logout = () => pushModal(LogOut)
|
||||||
|
|
||||||
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SecondaryNav>
|
<SecondaryNav>
|
||||||
|
|
@ -62,7 +59,7 @@
|
||||||
<SecondaryNavItem href="/settings/privacy">
|
<SecondaryNavItem href="/settings/privacy">
|
||||||
<Icon icon={Shield} /> Privacy
|
<Icon icon={Shield} /> Privacy
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
<SecondaryNavItem onclick={toggleTheme}>
|
<SecondaryNavItem href="/settings/theme">
|
||||||
<Icon icon={Moon} /> Theme
|
<Icon icon={Moon} /> Theme
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
<SecondaryNavItem href="/settings/about">
|
<SecondaryNavItem href="/settings/about">
|
||||||
|
|
|
||||||
|
|
@ -87,26 +87,6 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="flex flex-col gap-4">
|
|
||||||
<strong class="text-lg">Accessibility</strong>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Font size</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet secondary()}
|
|
||||||
<p>{Math.round(settings.font_size * 100)}%</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<input
|
|
||||||
class="range w-full"
|
|
||||||
type="range"
|
|
||||||
min="0.8"
|
|
||||||
max="1.3"
|
|
||||||
step="0.05"
|
|
||||||
bind:value={settings.font_size} />
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
</Card>
|
|
||||||
<Card class="flex flex-col gap-4 shadow-md">
|
<Card class="flex flex-col gap-4 shadow-md">
|
||||||
<strong class="text-lg">Editor Settings</strong>
|
<strong class="text-lg">Editor Settings</strong>
|
||||||
<Field>
|
<Field>
|
||||||
|
|
|
||||||
93
src/routes/settings/theme/+page.svelte
Normal file
93
src/routes/settings/theme/+page.svelte
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
|
import Accessibility from "@assets/icons/accessibility.svg?dataurl"
|
||||||
|
import Monitor from "@assets/icons/monitor.svg?dataurl"
|
||||||
|
import Moon from "@assets/icons/moon.svg?dataurl"
|
||||||
|
import Palette from "@assets/icons/palette.svg?dataurl"
|
||||||
|
import Sun from "@assets/icons/sun.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Card from "@lib/components/Card.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
|
import {publishSettings, userSettingsValues} from "@app/settings"
|
||||||
|
import {flTheme, flThemes, theme} from "@app/theme"
|
||||||
|
|
||||||
|
const saveFontSize = () => publishSettings({font_size: fontSize})
|
||||||
|
|
||||||
|
let fontSize = $state($userSettingsValues.font_size)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet colorScheme(value: string, label: string, icon: string)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
class={cx("join-item button button-xs", $theme === value ? "button-primary" : "button-neutral")}
|
||||||
|
onclick={() => theme.set(value)}>
|
||||||
|
<Icon {icon} size={4} />
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<PageContent>
|
||||||
|
<Card class="flex flex-col gap-4">
|
||||||
|
<strong class="flex items-center gap-3 text-lg">
|
||||||
|
<Icon icon={Palette} />
|
||||||
|
Theme Settings
|
||||||
|
</strong>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Color scheme</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="join">
|
||||||
|
{@render colorScheme("system", "System", Monitor)}
|
||||||
|
{@render colorScheme("light", "Light", Sun)}
|
||||||
|
{@render colorScheme("dark", "Dark", Moon)}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>System follows whatever your device is set to.</p>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>App theme</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select class="select input capitalize" bind:value={$flTheme} aria-label="Style">
|
||||||
|
{#each flThemes as option (option)}
|
||||||
|
<option value={option}>{option}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>Changes the shape and texture of everything, not just its colors.</p>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
</Card>
|
||||||
|
<Card class="flex flex-col gap-4">
|
||||||
|
<strong class="flex items-center gap-3 text-lg">
|
||||||
|
<Icon icon={Accessibility} />
|
||||||
|
Accessibility
|
||||||
|
</strong>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Font size</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet secondary()}
|
||||||
|
<p>{Math.round(fontSize * 100)}%</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<input
|
||||||
|
class="range w-full"
|
||||||
|
type="range"
|
||||||
|
min="0.8"
|
||||||
|
max="1.3"
|
||||||
|
step="0.05"
|
||||||
|
bind:value={fontSize}
|
||||||
|
onchange={saveFontSize} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
</Card>
|
||||||
|
</PageContent>
|
||||||
Loading…
Reference in a new issue