feat: add theme editor in settings
This commit is contained in:
@@ -7,6 +7,10 @@
|
|||||||
title: 'Account',
|
title: 'Account',
|
||||||
url: '/settings/account'
|
url: '/settings/account'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Appearance',
|
||||||
|
url: '/settings/appearance'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Libraries',
|
title: 'Libraries',
|
||||||
url: '/settings/libraries'
|
url: '/settings/libraries'
|
||||||
@@ -19,7 +23,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-[calc(100vh-var(--header-height)-2rem)] flex-col">
|
<div class="flex h-[calc(100vh-var(--header-height)-2rem)] flex-col">
|
||||||
<h1 class="text-xl font-bold">Settings</h1>
|
<h1 class="font-serif text-xl font-medium tracking-tight">Settings</h1>
|
||||||
<div class="mt-6 flex flex-1 gap-8 overflow-hidden">
|
<div class="mt-6 flex flex-1 gap-8 overflow-hidden">
|
||||||
<nav class="flex w-48 shrink-0 flex-col gap-1">
|
<nav class="flex w-48 shrink-0 flex-col gap-1">
|
||||||
{#each items as item}
|
{#each items as item}
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
import * as Field from '$lib/components/ui/field/index.js';
|
||||||
|
import { NativeSelect } from '$lib/components/ui/native-select/index.js';
|
||||||
|
import { Progress } from '$lib/components/ui/progress/index.js';
|
||||||
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
|
import ThemeToggle from '$lib/components/layout/theme-toggle.svelte';
|
||||||
|
import { getThemeState } from '$lib/theme/theme.svelte';
|
||||||
|
import { COLOR_TOKENS, FONT_TOKENS, FONT_STACKS, PRESETS } from '$lib/theme/presets';
|
||||||
|
import { RotateCcw, Funnel, Star } from '@lucide/svelte';
|
||||||
|
|
||||||
|
const theme = getThemeState();
|
||||||
|
|
||||||
|
const RADII = [
|
||||||
|
{ label: 'Square', value: '0rem' },
|
||||||
|
{ label: 'Slight', value: '0.25rem' },
|
||||||
|
{ label: 'Rounded', value: '0.625rem' },
|
||||||
|
{ label: 'Soft', value: '1rem' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex max-w-3xl flex-col gap-8 pb-10">
|
||||||
|
<header class="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="font-serif text-lg font-medium">Appearance</h2>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Changes apply immediately and are remembered on this browser. Light and dark are edited
|
||||||
|
separately — you are editing <strong>{theme.mode}</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
{#if theme.isCustomised}
|
||||||
|
<Button variant="outline" size="sm" onclick={() => theme.reset()}>
|
||||||
|
<RotateCcw class="size-3.5" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Presets -->
|
||||||
|
<section class="flex flex-col gap-3">
|
||||||
|
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Theme</Label>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
|
{#each PRESETS as preset (preset.id)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => theme.setPreset(preset.id)}
|
||||||
|
aria-pressed={theme.config.preset === preset.id}
|
||||||
|
class="flex flex-col gap-3 rounded-lg border p-3 text-left transition-colors hover:bg-muted aria-pressed:border-primary aria-pressed:bg-accent"
|
||||||
|
>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
{#each ['background', 'card', 'primary', 'muted-foreground', 'flag'] as key (key)}
|
||||||
|
<span
|
||||||
|
class="size-5 rounded border"
|
||||||
|
style="background:{preset[theme.mode][key]}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium">{preset.name}</span>
|
||||||
|
<p class="text-xs text-muted-foreground">{preset.description}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Colours -->
|
||||||
|
<section class="flex flex-col gap-3">
|
||||||
|
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Colours</Label>
|
||||||
|
<div class="grid gap-x-6 gap-y-1 sm:grid-cols-2">
|
||||||
|
{#each COLOR_TOKENS as token (token.key)}
|
||||||
|
<div class="flex items-center gap-3 rounded-md px-2 py-2 hover:bg-muted">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
id="color-{token.key}"
|
||||||
|
value={theme.palette[token.key]}
|
||||||
|
oninput={(e) => theme.setColor(token.key, e.currentTarget.value)}
|
||||||
|
class="size-7 shrink-0 cursor-pointer rounded border bg-transparent"
|
||||||
|
/>
|
||||||
|
<label for="color-{token.key}" class="min-w-0 flex-1 cursor-pointer">
|
||||||
|
<span class="block text-sm">{token.label}</span>
|
||||||
|
<span class="block truncate text-xs text-muted-foreground">{token.hint}</span>
|
||||||
|
</label>
|
||||||
|
<code class="font-mono text-xs text-muted-foreground">{theme.palette[token.key]}</code>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Radius -->
|
||||||
|
<section class="flex flex-col gap-3">
|
||||||
|
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Corners</Label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each RADII as option (option.value)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => theme.setRadius(option.value)}
|
||||||
|
aria-pressed={theme.radius === option.value}
|
||||||
|
class="flex items-center gap-2 border px-3 py-2 text-sm transition-colors hover:bg-muted aria-pressed:border-primary aria-pressed:bg-accent"
|
||||||
|
style="border-radius:{option.value}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="size-4 border bg-primary"
|
||||||
|
style="border-radius:{option.value}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<section class="flex flex-col gap-3">
|
||||||
|
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Fonts</Label>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
|
{#each FONT_TOKENS as token (token.key)}
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="font-{token.key}">{token.label}</Field.Label>
|
||||||
|
<NativeSelect
|
||||||
|
id="font-{token.key}"
|
||||||
|
value={theme.fonts[token.key]}
|
||||||
|
onchange={(e) => theme.setFont(token.key, e.currentTarget.value)}
|
||||||
|
>
|
||||||
|
{#each FONT_STACKS as stack (stack.label)}
|
||||||
|
<option value={stack.value}>{stack.label}</option>
|
||||||
|
{/each}
|
||||||
|
</NativeSelect>
|
||||||
|
</Field.Field>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<section class="flex flex-col gap-3">
|
||||||
|
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Preview</Label>
|
||||||
|
<div class="rounded-lg border bg-sidebar p-4">
|
||||||
|
<div class="flex flex-col gap-4 rounded-lg border bg-card p-4">
|
||||||
|
<div class="flex items-baseline justify-between gap-4">
|
||||||
|
<h3 class="font-serif text-base">Frankenstein</h3>
|
||||||
|
<span class="font-mono text-xs text-muted-foreground tabular-nums">280 pp · 1818</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Progress value={0.62} max={1} class="h-1 rounded [&>div]:bg-flag" />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge>Fiction</Badge>
|
||||||
|
<Badge variant="outline">Gothic</Badge>
|
||||||
|
<span class="font-mono text-xs font-semibold text-success">Finished</span>
|
||||||
|
<Star class="size-4 fill-star text-star" />
|
||||||
|
<span class="relative inline-flex">
|
||||||
|
<Funnel class="size-4 text-muted-foreground" />
|
||||||
|
<span class="absolute -top-0.5 -right-0.5 size-1.5 rounded-full bg-flag"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Button size="sm">Read</Button>
|
||||||
|
<Button size="sm" variant="outline">Download</Button>
|
||||||
|
<Button size="sm" variant="destructive">Delete</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user