feat: add theme editor in settings

This commit is contained in:
2026-08-11 20:08:53 -04:00
parent d71b53b3c5
commit f36aa14463
2 changed files with 175 additions and 1 deletions
@@ -7,6 +7,10 @@
title: 'Account',
url: '/settings/account'
},
{
title: 'Appearance',
url: '/settings/appearance'
},
{
title: 'Libraries',
url: '/settings/libraries'
@@ -19,7 +23,7 @@
</script>
<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">
<nav class="flex w-48 shrink-0 flex-col gap-1">
{#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 &mdash; 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>