feat(shared-ui): add reusable settings components with glass styling

- Add SettingsPage, SettingsSection, SettingsCard components
- Add SettingsRow, SettingsToggle for interactive elements
- Add SettingsDangerZone, SettingsDangerButton for destructive actions
- Apply glass morphism styling matching PillNavigation
- Migrate settings pages in manacore, presi, zitare apps
- Migrate archived apps: maerchenzauber, memoro, nutriphi, uload

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-29 13:22:12 +01:00
parent 3cfa6a765a
commit 7deb5b9a1e
16 changed files with 2391 additions and 1222 deletions

View file

@ -4,6 +4,15 @@
import { goto } from '$app/navigation';
import { dataService } from '$lib/api';
import { toastStore } from '$lib/stores/toast.svelte';
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsToggle,
SettingsDangerZone,
SettingsDangerButton,
} from '@manacore/shared-ui';
// Stats
let storyCount = $state(0);
@ -76,22 +85,19 @@
toastStore.success('Bildmodell gespeichert');
}
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
</script>
<svelte:head>
<title>Einstellungen | Märchenzauber</title>
</svelte:head>
<div class="mx-auto max-w-2xl space-y-6">
<!-- Header -->
<div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-200">Einstellungen</h1>
<p class="text-sm text-gray-500 dark:text-gray-400">
Verwalte dein Konto und deine Einstellungen
</p>
</div>
<!-- Stats Section -->
<SettingsPage title="Einstellungen" subtitle="Verwalte dein Konto und deine Einstellungen">
<!-- Stats Section (Custom gradient) -->
<section
class="rounded-2xl bg-gradient-to-r from-pink-500 to-purple-600 p-6 text-white shadow-lg"
>
@ -136,65 +142,65 @@
</div>
</section>
<!-- Preferences Section -->
<section class="rounded-2xl bg-white p-6 shadow-sm dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">Darstellung</h2>
<!-- Appearance Section -->
<SettingsSection title="Darstellung">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{/snippet}
<div class="space-y-4">
<!-- Dark Mode Toggle -->
<div class="flex items-center justify-between rounded-xl bg-gray-50 p-4 dark:bg-gray-700/50">
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gray-200 text-gray-600 dark:bg-gray-600 dark:text-gray-300"
>
{#if isDarkMode}
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{:else}
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{/if}
</div>
<div>
<p class="font-medium text-gray-800 dark:text-gray-200">Dunkelmodus</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{isDarkMode ? 'Aktiviert' : 'Deaktiviert'}
</p>
</div>
</div>
<button
onclick={toggleTheme}
class="relative h-7 w-12 rounded-full transition-colors {isDarkMode
? 'bg-pink-500'
: 'bg-gray-300'}"
>
<span
class="absolute top-0.5 h-6 w-6 rounded-full bg-white shadow transition-all {isDarkMode
? 'left-5.5'
: 'left-0.5'}"
style="left: {isDarkMode ? '1.375rem' : '0.125rem'}"
></span>
</button>
</div>
</div>
</section>
<SettingsCard>
<SettingsToggle
label="Dunkelmodus"
description={isDarkMode ? 'Aktiviert' : 'Deaktiviert'}
isOn={isDarkMode}
onToggle={toggleTheme}
>
{#snippet icon()}
{#if isDarkMode}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{:else}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{/if}
{/snippet}
</SettingsToggle>
</SettingsCard>
</SettingsSection>
<!-- Image Model Section -->
<section class="rounded-2xl bg-white p-6 shadow-sm dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">Bildgenerierung</h2>
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
<!-- Image Model Section (Custom) -->
<SettingsSection title="Bildgenerierung">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{/snippet}
<p class="mb-4 text-sm text-[hsl(var(--muted-foreground))]">
Wähle das KI-Modell für die Illustration deiner Geschichten
</p>
@ -205,13 +211,13 @@
class="flex w-full items-center gap-3 rounded-xl p-4 text-left transition-all {selectedImageModel ===
model.id
? 'bg-pink-50 ring-2 ring-pink-500 dark:bg-pink-900/20'
: 'bg-gray-50 hover:bg-gray-100 dark:bg-gray-700/50 dark:hover:bg-gray-700'}"
: 'bg-[hsl(var(--muted))] hover:bg-[hsl(var(--muted)/0.8)]'}"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl {selectedImageModel ===
model.id
? 'bg-pink-500 text-white'
: 'bg-gray-200 text-gray-600 dark:bg-gray-600 dark:text-gray-300'}"
: 'bg-[hsl(var(--background))] text-[hsl(var(--muted-foreground))]'}"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@ -223,21 +229,16 @@
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200">{model.name}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">{model.description}</p>
<p class="font-medium text-[hsl(var(--foreground))]">{model.name}</p>
<p class="text-sm text-[hsl(var(--muted-foreground))]">{model.description}</p>
</div>
<span
class="rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-600 dark:text-gray-300"
class="rounded-full bg-[hsl(var(--background))] px-2 py-0.5 text-xs font-medium text-[hsl(var(--muted-foreground))]"
>
{model.speed}
</span>
{#if selectedImageModel === model.id}
<svg
class="h-5 w-5 text-pink-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<svg class="h-5 w-5 text-pink-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -249,22 +250,25 @@
</button>
{/each}
</div>
</section>
</SettingsSection>
<!-- Story Settings Section -->
<section class="rounded-2xl bg-white p-6 shadow-sm dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">Geschichten</h2>
<SettingsSection title="Geschichten">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
{/snippet}
<div class="space-y-2">
<!-- Creators -->
<a
href="/creators"
class="flex items-center gap-3 rounded-xl p-3 transition-all hover:bg-gray-50 dark:hover:bg-gray-700/50"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-purple-100 text-purple-500 dark:bg-purple-900/30 dark:text-purple-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<SettingsCard>
<SettingsRow label="Kreative wählen" description="Autoren & Illustratoren Stil" href="/creators">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -272,25 +276,15 @@
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200">Kreative wählen</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Autoren & Illustratoren Stil</p>
</div>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
<!-- Templates -->
<a
{/snippet}
</SettingsRow>
<SettingsRow
label="Story-Vorlagen"
description="Inspiration für neue Geschichten"
href="/templates"
class="flex items-center gap-3 rounded-xl p-3 transition-all hover:bg-gray-50 dark:hover:bg-gray-700/50"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-100 text-indigo-500 dark:bg-indigo-900/30 dark:text-indigo-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -298,25 +292,16 @@
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200">Story-Vorlagen</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Inspiration für neue Geschichten</p>
</div>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
<!-- Collections -->
<a
{/snippet}
</SettingsRow>
<SettingsRow
label="Sammlungen"
description="Geschichten organisieren"
href="/collections"
class="flex items-center gap-3 rounded-xl p-3 transition-all hover:bg-gray-50 dark:hover:bg-gray-700/50"
border={false}
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-green-100 text-green-500 dark:bg-green-900/30 dark:text-green-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -324,32 +309,33 @@
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200">Sammlungen</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Geschichten organisieren</p>
</div>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
</section>
{/snippet}
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Characters Section -->
<section class="rounded-2xl bg-white p-6 shadow-sm dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">Charaktere</h2>
<SettingsSection title="Charaktere">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{/snippet}
<div class="space-y-2">
<!-- Import Character -->
<a
<SettingsCard>
<SettingsRow
label="Charakter importieren"
description="Mit Teilen-Code importieren"
href="/characters/share"
class="flex items-center gap-3 rounded-xl p-3 transition-all hover:bg-gray-50 dark:hover:bg-gray-700/50"
border={false}
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-pink-100 text-pink-500 dark:bg-pink-900/30 dark:text-pink-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -357,46 +343,64 @@
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
/>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200">Charakter importieren</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Mit Teilen-Code importieren</p>
</div>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
</section>
{/snippet}
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Account Section -->
<section class="rounded-2xl bg-white p-6 shadow-sm dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">Konto</h2>
<SettingsSection title="Konto">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{/snippet}
<div class="space-y-4">
<!-- Email -->
<div class="flex items-center justify-between rounded-xl bg-gray-50 p-4 dark:bg-gray-700/50">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">E-Mail</p>
<p class="text-gray-800 dark:text-gray-200">{authStore.user?.email || '-'}</p>
</div>
</div>
<!-- User ID -->
<div class="flex items-center justify-between rounded-xl bg-gray-50 p-4 dark:bg-gray-700/50">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Benutzer-ID</p>
<p class="font-mono text-xs text-gray-600 dark:text-gray-400">
{authStore.user?.id || '-'}
</p>
</div>
</div>
</div>
</section>
<SettingsCard>
<SettingsRow label="E-Mail" description={authStore.user?.email || '-'}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
{/snippet}
</SettingsRow>
<SettingsRow label="Benutzer-ID" border={false}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"
/>
</svg>
{/snippet}
<span class="font-mono text-xs text-[hsl(var(--muted-foreground))]">
{authStore.user?.id || '-'}
</span>
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Subscription Section -->
<section class="rounded-2xl bg-white p-6 shadow-sm dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">Abonnement</h2>
<SettingsSection title="Abonnement">
{#snippet icon()}
<svg fill="currentColor" viewBox="0 0 24 24">
<path
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
/>
</svg>
{/snippet}
<a
href="/subscription"
@ -411,30 +415,38 @@
</svg>
</div>
<div>
<p class="font-medium text-gray-800 dark:text-gray-200">Mana verwalten</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Abonnement und Guthaben</p>
<p class="font-medium text-[hsl(var(--foreground))]">Mana verwalten</p>
<p class="text-sm text-[hsl(var(--muted-foreground))]">Abonnement und Guthaben</p>
</div>
</div>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg
class="h-5 w-5 text-[hsl(var(--muted-foreground))]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
</section>
</SettingsSection>
<!-- Actions Section -->
<section class="rounded-2xl bg-white p-6 shadow-sm dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">Mehr</h2>
<!-- More Section -->
<SettingsSection title="Mehr">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"
/>
</svg>
{/snippet}
<div class="space-y-2">
<!-- Feedback -->
<a
href="/feedback"
class="flex items-center gap-3 rounded-xl p-3 transition-all hover:bg-gray-50 dark:hover:bg-gray-700/50"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-rose-100 text-rose-500 dark:bg-rose-900/30 dark:text-rose-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<SettingsCard>
<SettingsRow label="Feedback & Ideen" description="Stimme für Features ab" href="/feedback">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -442,25 +454,15 @@
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200">Feedback & Ideen</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Stimme für Features ab</p>
</div>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
<!-- Archive -->
<a
{/snippet}
</SettingsRow>
<SettingsRow
label="Archiv"
description="Archivierte Geschichten und Charaktere"
href="/archive"
class="flex items-center gap-3 rounded-xl p-3 transition-all hover:bg-gray-50 dark:hover:bg-gray-700/50"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -468,27 +470,11 @@
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
/>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200">Archiv</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
Archivierte Geschichten und Charaktere
</p>
</div>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
<!-- Help -->
<a
href="/help"
class="flex items-center gap-3 rounded-xl p-3 transition-all hover:bg-gray-50 dark:hover:bg-gray-700/50"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-100 text-blue-500 dark:bg-blue-900/30 dark:text-blue-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{/snippet}
</SettingsRow>
<SettingsRow label="Hilfe" description="FAQ und Support" href="/help" border={false}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -496,41 +482,30 @@
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200">Hilfe</p>
<p class="text-sm text-gray-500 dark:text-gray-400">FAQ und Support</p>
</div>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{/snippet}
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Logout -->
<button
onclick={async () => {
await authStore.signOut();
goto('/login');
}}
class="flex w-full items-center gap-3 rounded-xl p-3 text-left transition-all hover:bg-red-50 dark:hover:bg-red-900/20"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-xl bg-red-100 text-red-500 dark:bg-red-900/30 dark:text-red-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-red-600 dark:text-red-400">Abmelden</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Von deinem Konto abmelden</p>
</div>
</button>
</div>
</section>
</div>
<!-- Logout -->
<SettingsDangerZone title="Abmelden">
<SettingsDangerButton
label="Abmelden"
description="Von deinem Konto abmelden"
buttonText="Abmelden"
onclick={handleLogout}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
{/snippet}
</SettingsDangerButton>
</SettingsDangerZone>
</SettingsPage>

View file

@ -4,9 +4,15 @@
import { theme } from '$lib/stores/theme';
import { settings } from '$lib/stores/settings';
import type { ThemeMode } from '$lib/stores/theme';
import SettingsToggle from '$lib/components/SettingsToggle.svelte';
import SectionHeader from '$lib/components/SectionHeader.svelte';
import { onMount } from 'svelte';
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsToggle,
SettingsDangerZone,
SettingsDangerButton,
} from '@manacore/shared-ui';
// theme is a Svelte 5 runes-based store, access properties directly
let currentMode = $derived(theme.mode);
@ -51,7 +57,7 @@
}
function handleRateApp() {
alert('Thank you for your interest! Rating feature coming soon for the web version. 🌟');
alert('Thank you for your interest! Rating feature coming soon for the web version.');
}
// Copy app info
@ -78,7 +84,7 @@ User: ${currentUser?.email || 'N/A'}`;
settings.setDeveloperMode(newMode);
alert(
newMode
? '🎉 Developer Mode activated!\nAdvanced settings are now visible.'
? 'Developer Mode activated! Advanced settings are now visible.'
: 'Developer Mode deactivated.'
);
}
@ -87,7 +93,7 @@ User: ${currentUser?.email || 'N/A'}`;
// Delete account confirmation
function handleDeleteAccount() {
const confirmed = confirm(
'⚠️ Are you sure you want to delete your account?\n\nThis action cannot be undone. All your data will be permanently deleted.'
'Are you sure you want to delete your account?\n\nThis action cannot be undone. All your data will be permanently deleted.'
);
if (confirmed) {
const doubleConfirm = confirm(
@ -98,270 +104,406 @@ User: ${currentUser?.email || 'N/A'}`;
}
}
}
function handleResetSettings() {
if (confirm('Reset all settings to defaults?')) {
settings.reset();
alert('Settings reset to defaults!');
}
}
</script>
<svelte:head>
<title>Settings - Memoro</title>
</svelte:head>
<div class="flex h-full flex-col">
<!-- Content Area -->
<div class="flex-1 overflow-y-auto">
<div class="mx-auto max-w-5xl pb-12">
<h1 class="mb-8 text-3xl font-bold">Settings</h1>
<!-- Appearance Section -->
<section class="mb-6">
<SectionHeader title="Appearance" isFirst={true} />
<div class="card">
<h3 class="mb-4 text-lg font-medium">Theme Mode</h3>
<div class="grid grid-cols-3 gap-3">
{#each themeModes as mode}
<button
onclick={() => {
setThemeMode(mode.value);
activeMode = mode.value;
}}
class="flex flex-col items-center gap-3 rounded-xl border-2 p-4 transition-all bg-content-hover {activeMode ===
mode.value
? 'border-primary shadow-md'
: 'border-theme'}"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#if mode.icon === 'sync'}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
{:else if mode.icon === 'sunny'}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
{/if}
</svg>
<span
class="text-sm font-medium {activeMode === mode.value
? 'text-primary'
: 'text-theme'}"
>
{mode.label}
</span>
</button>
{/each}
</div>
<p class="mt-4 text-sm text-theme-secondary">
Choose how Memoro looks. System automatically matches your device's theme.
</p>
</div>
</section>
<!-- User Interface Elements -->
<SectionHeader
title="User Interface"
collapsible={true}
isCollapsed={!showUIElements}
onPress={() => (showUIElements = !showUIElements)}
/>
{#if showUIElements}
<div class="mb-6 space-y-4">
<SettingsToggle
title="Show Language Button"
description="Display language selection button next to the recording button"
type="toggle"
isOn={currentSettings.showLanguageButton}
onToggle={settings.setShowLanguageButton}
/>
<SettingsToggle
title="Show Recording Instruction"
description="Show 'Start Recording' text with arrow near the recording button"
type="toggle"
isOn={currentSettings.showRecordingInstruction}
onToggle={settings.setShowRecordingInstruction}
/>
<SettingsToggle
title="Show Blueprints"
description="Display blueprint selection at the bottom of the screen"
type="toggle"
isOn={currentSettings.showBlueprints}
onToggle={settings.setShowBlueprints}
/>
<SettingsToggle
title="Show Mana Badge"
description="Display Mana counter in the header"
type="toggle"
isOn={currentSettings.showManaBadge}
onToggle={settings.setShowManaBadge}
/>
</div>
{/if}
<!-- Data & Privacy -->
<SectionHeader title="Data & Privacy" />
<div class="mb-6 space-y-4">
<SettingsToggle
title="Save Location"
description="Allow the app to save your location to enable location-based features"
type="toggle"
isOn={currentSettings.saveLocation}
onToggle={settings.setSaveLocation}
<SettingsPage title="Settings" subtitle="Manage your account and preferences" maxWidth="5xl">
<!-- Appearance Section -->
<SettingsSection title="Appearance">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
<SettingsToggle
title="Enable Analytics"
description="Help improve Memoro by sharing anonymous usage data"
type="toggle"
isOn={currentSettings.enableAnalytics}
onToggle={settings.setEnableAnalytics}
/>
</div>
</svg>
{/snippet}
<!-- Support -->
<SectionHeader title="Support" />
<div class="mb-6 space-y-4">
<SettingsToggle
title="Contact Support"
description="Need help? Get in touch with our support team"
type="button"
onPress={handleContactSupport}
icon="mail-outline"
/>
<SettingsToggle
title="Rate App"
description="Enjoying Memoro? Rate us in your browser's extension store"
type="button"
onPress={handleRateApp}
icon="star-outline"
/>
</div>
<!-- Advanced Settings -->
<div class="mb-6">
<button
onclick={() => (showMoreSettings = !showMoreSettings)}
class="btn-secondary w-full justify-center"
>
<svg
class="mr-2 inline-block h-5 w-5 transition-transform {showMoreSettings
? 'rotate-180'
: ''}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
{showMoreSettings ? 'Hide' : 'Show'} Advanced Settings
</button>
</div>
{#if showMoreSettings}
<div class="card mb-6 border-2 border-theme">
<h3 class="mb-4 text-lg font-semibold text-theme-secondary">Delete Account</h3>
<p class="mb-4 text-sm text-theme-secondary">
If you delete your account, all your data will be permanently deleted. This action
cannot be undone.
</p>
<SettingsCard>
<h3 class="mb-4 text-lg font-medium text-[hsl(var(--foreground))]">Theme Mode</h3>
<div class="grid grid-cols-3 gap-3">
{#each themeModes as mode}
<button
onclick={handleDeleteAccount}
class="btn-primary w-full bg-red-600 hover:bg-red-700"
onclick={() => {
setThemeMode(mode.value);
activeMode = mode.value;
}}
class="flex flex-col items-center gap-3 rounded-xl border-2 p-4 transition-all {activeMode ===
mode.value
? 'border-[hsl(var(--primary))] bg-[hsl(var(--primary)/0.1)] shadow-md'
: 'border-[hsl(var(--border))] bg-[hsl(var(--muted))]'}"
>
<svg
class="mr-2 inline-block h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#if mode.icon === 'sync'}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
{:else if mode.icon === 'sunny'}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
{/if}
</svg>
<span
class="text-sm font-medium {activeMode === mode.value
? 'text-[hsl(var(--primary))]'
: 'text-[hsl(var(--foreground))]'}"
>
{mode.label}
</span>
</button>
{/each}
</div>
<p class="mt-4 text-sm text-[hsl(var(--muted-foreground))]">
Choose how Memoro looks. System automatically matches your device's theme.
</p>
</SettingsCard>
</SettingsSection>
<!-- User Interface Section (Collapsible) -->
<SettingsSection title="User Interface">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"
/>
</svg>
{/snippet}
<button
onclick={() => (showUIElements = !showUIElements)}
class="mb-4 flex w-full items-center justify-between rounded-lg bg-[hsl(var(--muted))] p-3 text-left"
>
<span class="text-sm font-medium text-[hsl(var(--foreground))]">
{showUIElements ? 'Hide' : 'Show'} UI Element Options
</span>
<svg
class="h-5 w-5 transition-transform text-[hsl(var(--muted-foreground))] {showUIElements
? 'rotate-180'
: ''}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{#if showUIElements}
<SettingsCard>
<SettingsToggle
label="Show Language Button"
description="Display language selection button next to the recording button"
isOn={currentSettings.showLanguageButton}
onToggle={settings.setShowLanguageButton}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
/>
</svg>
Delete Account
</button>
</div>
{/if}
{/snippet}
</SettingsToggle>
<SettingsToggle
label="Show Recording Instruction"
description="Show 'Start Recording' text with arrow near the recording button"
isOn={currentSettings.showRecordingInstruction}
onToggle={settings.setShowRecordingInstruction}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/snippet}
</SettingsToggle>
<SettingsToggle
label="Show Blueprints"
description="Display blueprint selection at the bottom of the screen"
isOn={currentSettings.showBlueprints}
onToggle={settings.setShowBlueprints}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
{/snippet}
</SettingsToggle>
<SettingsToggle
label="Show Mana Badge"
description="Display Mana counter in the header"
isOn={currentSettings.showManaBadge}
onToggle={settings.setShowManaBadge}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
/>
</svg>
{/snippet}
</SettingsToggle>
</SettingsCard>
{/if}
</SettingsSection>
<!-- Developer Settings -->
{#if currentSettings.developerMode}
<SectionHeader title="Developer Settings" />
<div class="mb-6 space-y-4">
<SettingsToggle
title="Show Debug Borders"
description="Display borders around UI elements for development"
type="toggle"
isOn={currentSettings.showDebugBorders}
onToggle={settings.setShowDebugBorders}
/>
<SettingsToggle
title="Reset All Settings"
description="Reset all settings to default values"
type="button"
onPress={() => {
if (confirm('Reset all settings to defaults?')) {
settings.reset();
alert('Settings reset to defaults!');
}
}}
/>
</div>
{/if}
<!-- Data & Privacy Section -->
<SettingsSection title="Data & Privacy">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
{/snippet}
<!-- Account Section -->
<SectionHeader title="Account" />
<div class="card mb-6">
{#if currentUser}
<div class="mb-6">
<label class="mb-2 block text-sm font-medium text-theme-secondary">Email Address</label>
<p class="text-lg font-medium">{currentUser.email || 'No email available'}</p>
</div>
{/if}
<button onclick={handleLogout} class="btn-primary w-full">
<svg
class="mr-2 inline-block h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<SettingsCard>
<SettingsToggle
label="Save Location"
description="Allow the app to save your location to enable location-based features"
isOn={currentSettings.saveLocation}
onToggle={settings.setSaveLocation}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
Sign Out
</button>
</div>
{/snippet}
</SettingsToggle>
<SettingsToggle
label="Enable Analytics"
description="Help improve Memoro by sharing anonymous usage data"
isOn={currentSettings.enableAnalytics}
onToggle={settings.setEnableAnalytics}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
{/snippet}
</SettingsToggle>
</SettingsCard>
</SettingsSection>
<!-- App Information Section -->
<SectionHeader title="App Information" />
<div class="card mb-6 relative">
<!-- Support Section -->
<SettingsSection title="Support">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{/snippet}
<SettingsCard>
<SettingsRow
label="Contact Support"
description="Need help? Get in touch with our support team"
onclick={handleContactSupport}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
{/snippet}
</SettingsRow>
<SettingsRow
label="Rate App"
description="Enjoying Memoro? Rate us in your browser's extension store"
onclick={handleRateApp}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
/>
</svg>
{/snippet}
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Account Section -->
<SettingsSection title="Account">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{/snippet}
<SettingsCard>
<SettingsRow label="Email Address" description={currentUser?.email || 'No email available'}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
{/snippet}
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Developer Settings -->
{#if currentSettings.developerMode}
<SettingsSection title="Developer Settings">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
/>
</svg>
{/snippet}
<SettingsCard>
<SettingsToggle
label="Show Debug Borders"
description="Display borders around UI elements for development"
isOn={currentSettings.showDebugBorders}
onToggle={settings.setShowDebugBorders}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6z"
/>
</svg>
{/snippet}
</SettingsToggle>
<SettingsRow
label="Reset All Settings"
description="Reset all settings to default values"
onclick={handleResetSettings}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
{/snippet}
</SettingsRow>
</SettingsCard>
</SettingsSection>
{/if}
<!-- App Information Section -->
<SettingsSection title="App Information">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/snippet}
<SettingsCard>
<div class="relative">
<button
onclick={copyAppInfo}
class="absolute right-4 top-4 rounded-lg p-2 transition-colors bg-menu-hover"
class="absolute right-0 top-0 rounded-lg p-2 transition-colors bg-[hsl(var(--muted))] hover:bg-[hsl(var(--muted)/0.8)]"
title="Copy app information"
>
<svg
class="h-5 w-5 text-theme-muted"
class="h-5 w-5 text-[hsl(var(--muted-foreground))]"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -374,47 +516,102 @@ User: ${currentUser?.email || 'N/A'}`;
/>
</svg>
</button>
<div class="space-y-3 pr-12">
<div class="flex items-center justify-between border-b border-theme-light py-3">
<span class="text-sm text-theme-secondary">Version</span>
<span class="font-medium">0.1.0</span>
</div>
<div class="flex items-center justify-between border-b border-theme-light py-3">
<span class="text-sm text-theme-secondary">Platform</span>
<span class="font-medium">Web</span>
</div>
<div class="flex items-center justify-between border-b border-theme-light py-3">
<span class="text-sm text-theme-secondary">Build</span>
<span class="font-medium">Beta</span>
</div>
<div class="flex items-center justify-between py-3">
<span class="text-sm text-theme-secondary">Browser</span>
<span class="text-xs font-medium"
>{navigator.userAgent.split(' ').slice(-2).join(' ')}</span
>
</div>
</div>
</div>
<SettingsRow label="Version">
<span class="text-sm font-medium text-[hsl(var(--foreground))]">0.1.0</span>
</SettingsRow>
<SettingsRow label="Platform">
<span class="text-sm font-medium text-[hsl(var(--foreground))]">Web</span>
</SettingsRow>
<SettingsRow label="Build">
<span class="text-sm font-medium text-[hsl(var(--foreground))]">Beta</span>
</SettingsRow>
<SettingsRow label="Browser" border={false}>
<span class="text-xs font-medium text-[hsl(var(--foreground))]">
{navigator.userAgent.split(' ').slice(-2).join(' ')}
</span>
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Footer -->
<div class="pt-8 text-center">
<button
onclick={handleVersionClick}
class="text-sm text-theme-muted transition-colors hover:text-theme-secondary"
>
© 2025 Memoro GmbH
</button>
<p class="mt-1 text-xs text-theme-muted">Made with ❤️ in Germany</p>
{#if clickCount > 0 && clickCount < 7}
<p class="mt-2 text-xs text-theme-muted opacity-50">{7 - clickCount} more clicks...</p>
{/if}
</div>
</div>
<!-- Advanced Settings / Danger Zone -->
<button
onclick={() => (showMoreSettings = !showMoreSettings)}
class="mb-4 flex w-full items-center justify-center gap-2 rounded-lg bg-[hsl(var(--muted))] p-3"
>
<svg
class="h-5 w-5 transition-transform text-[hsl(var(--muted-foreground))] {showMoreSettings
? 'rotate-180'
: ''}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
<span class="text-sm font-medium text-[hsl(var(--foreground))]">
{showMoreSettings ? 'Hide' : 'Show'} Advanced Settings
</span>
</button>
{#if showMoreSettings}
<SettingsDangerZone title="Danger Zone">
<SettingsDangerButton
label="Delete Account"
description="Permanently delete your account and all data. This action cannot be undone."
buttonText="Delete Account"
onclick={handleDeleteAccount}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
{/snippet}
</SettingsDangerButton>
</SettingsDangerZone>
{/if}
<!-- Logout -->
<SettingsDangerZone title="Sign Out">
<SettingsDangerButton
label="Sign Out"
description="Sign out of your Memoro account"
buttonText="Sign Out"
onclick={handleLogout}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
{/snippet}
</SettingsDangerButton>
</SettingsDangerZone>
<!-- Footer -->
<div class="pt-8 text-center">
<button
onclick={handleVersionClick}
class="text-sm text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]"
>
2025 Memoro GmbH
</button>
<p class="mt-1 text-xs text-[hsl(var(--muted-foreground))]">Made with love in Germany</p>
{#if clickCount > 0 && clickCount < 7}
<p class="mt-2 text-xs text-[hsl(var(--muted-foreground))] opacity-50">
{7 - clickCount} more clicks...
</p>
{/if}
</div>
</div>
<style>
.grid {
display: grid;
}
</style>
</SettingsPage>

View file

@ -2,6 +2,15 @@
import { goto } from '$app/navigation';
import { auth, user } from '$lib/stores/auth';
import { theme } from '$lib/stores/theme';
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsToggle,
SettingsDangerZone,
SettingsDangerButton,
} from '@manacore/shared-ui';
let isDeleting = $state(false);
let showDeleteConfirm = $state(false);
@ -25,129 +34,215 @@
await auth.signOut();
goto('/login');
}
function toggleDarkMode(value: boolean) {
theme.toggleMode();
}
</script>
<div class="mx-auto max-w-2xl space-y-6">
<!-- Header -->
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Einstellungen</h1>
<p class="text-gray-600 dark:text-gray-400">Verwalte dein Konto und App-Einstellungen</p>
</div>
<SettingsPage title="Einstellungen" subtitle="Verwalte dein Konto und App-Einstellungen">
<!-- Account Section -->
<SettingsSection title="Konto">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{/snippet}
<!-- Account -->
<div class="rounded-2xl bg-white p-6 shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Konto</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">E-Mail</p>
<p class="text-gray-600 dark:text-gray-400">{$user?.email || 'Nicht angemeldet'}</p>
</div>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">Benutzer-ID</p>
<p class="font-mono text-sm text-gray-600 dark:text-gray-400">{$user?.id || '—'}</p>
</div>
</div>
</div>
</div>
<SettingsCard>
<SettingsRow label="E-Mail" description={$user?.email || 'Nicht angemeldet'}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
{/snippet}
</SettingsRow>
<SettingsRow label="Benutzer-ID" border={false}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"
/>
</svg>
{/snippet}
<span class="font-mono text-xs text-[hsl(var(--muted-foreground))]">{$user?.id || '—'}</span>
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Appearance -->
<div class="rounded-2xl bg-white p-6 shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Erscheinungsbild</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">Dunkles Design</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
Aktiviere den Dark Mode für eine augenfreundliche Ansicht
</p>
</div>
<button
onclick={() => theme.toggleMode()}
class="relative h-7 w-12 rounded-full transition-colors {effectiveMode === 'dark'
? 'bg-green-500'
: 'bg-gray-200 dark:bg-gray-700'}"
>
<span
class="absolute top-1 h-5 w-5 rounded-full bg-white shadow transition-transform {effectiveMode ===
'dark'
? 'translate-x-6'
: 'translate-x-1'}"
></span>
</button>
</div>
</div>
</div>
<!-- Appearance Section -->
<SettingsSection title="Erscheinungsbild">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{/snippet}
<!-- Data Management -->
<div class="rounded-2xl bg-white p-6 shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Datenverwaltung</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">Daten exportieren</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
Exportiere alle deine Mahlzeiten und Statistiken
</p>
</div>
<button
onclick={() => goto('/export')}
class="rounded-xl bg-gray-100 px-4 py-2 font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
>
<SettingsCard>
<SettingsToggle
label="Dunkles Design"
description="Aktiviere den Dark Mode für eine augenfreundliche Ansicht"
isOn={effectiveMode === 'dark'}
onToggle={toggleDarkMode}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{/snippet}
</SettingsToggle>
</SettingsCard>
</SettingsSection>
<!-- Data Management Section -->
<SettingsSection title="Datenverwaltung">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
/>
</svg>
{/snippet}
<SettingsCard>
<SettingsRow
label="Daten exportieren"
description="Exportiere alle deine Mahlzeiten und Statistiken"
onclick={() => goto('/export')}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
{/snippet}
<span class="px-4 py-2 text-sm font-medium bg-[hsl(var(--muted))] text-[hsl(var(--foreground))] rounded-lg">
Export
</button>
</div>
</span>
</SettingsRow>
</SettingsCard>
<hr class="border-gray-200 dark:border-gray-700" />
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-red-600 dark:text-red-400">Alle Daten löschen</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
Löscht alle deine Mahlzeiten und Statistiken unwiderruflich
</p>
</div>
{#if showDeleteConfirm}
<SettingsDangerZone title="Gefahrenbereich">
{#if showDeleteConfirm}
<div class="px-5 py-4 flex items-center justify-between gap-4">
<div>
<p class="font-medium text-[hsl(var(--foreground))]">Alle Daten löschen</p>
<p class="text-sm text-[hsl(var(--muted-foreground))]">
Löscht alle deine Mahlzeiten und Statistiken unwiderruflich
</p>
</div>
<div class="flex gap-2">
<button
onclick={() => (showDeleteConfirm = false)}
class="rounded-xl bg-gray-100 px-4 py-2 font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
class="px-4 py-2 text-sm font-medium bg-[hsl(var(--muted))] text-[hsl(var(--foreground))] rounded-lg hover:bg-[hsl(var(--muted)/0.8)]"
>
Abbrechen
</button>
<button
onclick={handleDeleteAllData}
disabled={isDeleting}
class="rounded-xl bg-red-500 px-4 py-2 font-medium text-white hover:bg-red-600 disabled:opacity-50"
class="px-4 py-2 text-sm font-medium bg-[hsl(var(--destructive))] text-white rounded-lg hover:bg-[hsl(var(--destructive)/0.9)] disabled:opacity-50"
>
{isDeleting ? 'Wird gelöscht...' : 'Bestätigen'}
</button>
</div>
{:else}
<button
onclick={handleDeleteAllData}
class="rounded-xl bg-red-100 px-4 py-2 font-medium text-red-600 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
>
Löschen
</button>
{/if}
</div>
</div>
</div>
</div>
{:else}
<SettingsDangerButton
label="Alle Daten löschen"
description="Löscht alle deine Mahlzeiten und Statistiken unwiderruflich"
buttonText="Löschen"
onclick={handleDeleteAllData}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
{/snippet}
</SettingsDangerButton>
{/if}
</SettingsDangerZone>
</SettingsSection>
<!-- Logout -->
<button
onclick={handleLogout}
class="w-full rounded-2xl border-2 border-red-500 bg-white py-4 font-semibold text-red-600 transition-colors hover:bg-red-50 dark:bg-gray-800 dark:hover:bg-red-900/20"
>
Abmelden
</button>
<!-- Logout Section -->
<SettingsDangerZone title="Abmelden">
<SettingsDangerButton
label="Abmelden"
description="Von deinem Konto abmelden"
buttonText="Abmelden"
onclick={handleLogout}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
{/snippet}
</SettingsDangerButton>
</SettingsDangerZone>
<!-- App Info -->
<div class="text-center text-sm text-gray-500 dark:text-gray-400">
<p>Nutriphi Web v0.1.0</p>
<p>Teil des Mana Core Ökosystems</p>
</div>
</div>
<SettingsSection title="Über">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/snippet}
<SettingsCard>
<SettingsRow label="Version" border={false}>
<span class="text-sm text-[hsl(var(--muted-foreground))]">0.1.0</span>
</SettingsRow>
</SettingsCard>
<p class="text-center text-sm text-[hsl(var(--muted-foreground))] mt-4">
Teil des Mana Core Ökosystems
</p>
</SettingsSection>
</SettingsPage>

View file

@ -4,6 +4,14 @@
import { page } from '$app/stores';
import { toastMessages, notify } from '$lib/services/toast';
import * as m from '$paraglide/messages';
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsDangerZone,
SettingsDangerButton,
} from '@manacore/shared-ui';
let { data, form }: { data: PageData; form: ActionData } = $props();
let isSubmitting = $state(false);
@ -28,14 +36,8 @@
}
</script>
<div class="min-h-screen bg-theme-background">
<div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-theme-text dark:text-white">Settings</h1>
<p class="mt-2 text-theme-text dark:text-theme-text">Manage your account and preferences</p>
</div>
<div class="space-y-6">
<SettingsPage title="Settings" subtitle="Manage your account and preferences" maxWidth="3xl">
<div class="space-y-6">
<!-- Profile Section -->
<div class="rounded-xl bg-white p-6 shadow-xl dark:bg-theme-surface">
<h2 class="mb-6 text-xl font-semibold text-theme-text dark:text-white">
@ -779,5 +781,4 @@
</div>
</div>
</div>
</div>
</div>
</SettingsPage>

View file

@ -1,24 +1,34 @@
<script lang="ts">
import { Card, Button, Input } from '@manacore/shared-ui';
import { Button, Input } from '@manacore/shared-ui';
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsDangerZone,
SettingsDangerButton,
} from '@manacore/shared-ui';
import { enhance } from '$app/forms';
let { data, form } = $props();
let loading = $state(false);
</script>
<div>
<div class="mb-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Settings</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage your account settings and preferences
</p>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<!-- Profile Settings -->
<Card>
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Profile Information</h2>
<SettingsPage title="Settings" subtitle="Manage your account settings and preferences.">
<!-- Profile Section -->
<SettingsSection title="Profile Information">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{/snippet}
<SettingsCard>
<form
method="POST"
action="?/updateProfile"
@ -29,6 +39,7 @@
loading = false;
};
}}
class="p-5"
>
{#if form?.success}
<div
@ -50,7 +61,7 @@
<div>
<label
for="email"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
class="mb-2 block text-sm font-medium text-[hsl(var(--foreground))]"
>
Email
</label>
@ -59,15 +70,15 @@
id="email"
value={data.session?.user?.email || ''}
disabled
class="bg-gray-50 dark:bg-gray-900"
class="bg-[hsl(var(--muted))]"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Email cannot be changed</p>
<p class="mt-1 text-xs text-[hsl(var(--muted-foreground))]">Email cannot be changed</p>
</div>
<div>
<label
for="firstName"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
class="mb-2 block text-sm font-medium text-[hsl(var(--foreground))]"
>
First Name
</label>
@ -83,7 +94,7 @@
<div>
<label
for="lastName"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
class="mb-2 block text-sm font-medium text-[hsl(var(--foreground))]"
>
Last Name
</label>
@ -101,74 +112,116 @@
</Button>
</div>
</form>
</Card>
</SettingsCard>
</SettingsSection>
<!-- Account Stats -->
<Card>
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Account Information</h2>
<!-- Account Info Section -->
<SettingsSection title="Account Information">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
{/snippet}
<div class="space-y-4">
<div>
<div class="mb-1 flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400"
>Available Credits</span
>
<span class="text-2xl font-bold text-primary-600 dark:text-primary-400">
{data.profile?.credits || 0}
</span>
</div>
</div>
<SettingsCard>
<SettingsRow label="Available Credits">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/snippet}
<span class="text-2xl font-bold text-[hsl(var(--primary))]">
{data.profile?.credits || 0}
</span>
</SettingsRow>
<div class="border-t border-gray-200 pt-4 dark:border-gray-700">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400"
>Subscription Plan</span
>
<span
class="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
>
{data.profile?.subscription_plan_id || 'Free'}
</span>
</div>
</div>
<SettingsRow label="Subscription Plan">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
/>
</svg>
{/snippet}
<span class="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/20 dark:text-blue-400">
{data.profile?.subscription_plan_id || 'Free'}
</span>
</SettingsRow>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400"
>Subscription Status</span
>
<span
class="rounded-full {data.profile?.subscription_status === 'active'
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400'} px-3 py-1 text-xs font-medium"
>
{data.profile?.subscription_status || 'inactive'}
</span>
</div>
<SettingsRow label="Subscription Status">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/snippet}
<span
class="rounded-full px-3 py-1 text-xs font-medium
{data.profile?.subscription_status === 'active'
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400'}"
>
{data.profile?.subscription_status || 'inactive'}
</span>
</SettingsRow>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Member Since</span>
<span class="text-sm text-gray-900 dark:text-white">
{data.profile?.created_at
? new Date(data.profile.created_at).toLocaleDateString()
: 'N/A'}
</span>
</div>
</div>
</Card>
<SettingsRow label="Member Since" border={false}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{/snippet}
<span class="text-sm text-[hsl(var(--foreground))]">
{data.profile?.created_at
? new Date(data.profile.created_at).toLocaleDateString()
: 'N/A'}
</span>
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Danger Zone -->
<Card class="border-red-200 dark:border-red-800">
<h2 class="mb-4 text-lg font-semibold text-red-900 dark:text-red-400">Danger Zone</h2>
<div class="space-y-4">
<div>
<h3 class="mb-2 font-medium text-gray-900 dark:text-white">Delete Account</h3>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Once you delete your account, there is no going back. Please be certain.
</p>
<Button variant="danger" disabled>Delete Account</Button>
</div>
</div>
</Card>
</div>
</div>
<!-- Danger Zone -->
<SettingsDangerZone title="Danger Zone">
<SettingsDangerButton
label="Delete Account"
description="Once you delete your account, there is no going back. Please be certain."
buttonText="Delete Account"
onclick={() => {}}
disabled
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
{/snippet}
</SettingsDangerButton>
</SettingsDangerZone>
</SettingsPage>

View file

@ -2,125 +2,168 @@
import { goto } from '$app/navigation';
import { auth } from '$lib/stores/auth.svelte';
import { theme } from '$lib/stores/theme';
import { User, Mail, Shield, LogOut, Sun, Moon, Monitor } from 'lucide-svelte';
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsToggle,
SettingsDangerZone,
SettingsDangerButton,
} from '@manacore/shared-ui';
function handleLogout() {
auth.logout();
goto('/login');
}
function setThemeMode(mode: 'light' | 'dark' | 'system') {
theme.setMode(mode);
}
</script>
<svelte:head>
<title>Settings - Presi</title>
</svelte:head>
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl font-bold text-foreground mb-8">Settings</h1>
<SettingsPage title="Settings" subtitle="Manage your account and preferences.">
<!-- Account Section -->
<SettingsSection title="Account">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{/snippet}
<div class="space-y-6">
<!-- Account Section -->
<div
class="bg-surface rounded-xl shadow-sm border border-border overflow-hidden"
>
<div class="p-4 border-b border-border">
<h2 class="text-lg font-semibold text-foreground flex items-center gap-2">
<User class="w-5 h-5 text-muted-foreground" />
Account
</h2>
</div>
<div class="p-4 space-y-4">
<div class="flex items-center justify-between py-2">
<div class="flex items-center gap-3">
<Mail class="w-5 h-5 text-muted-foreground" />
<div>
<p class="text-sm font-medium text-foreground">Email</p>
<p class="text-sm text-muted-foreground">{auth.user?.email}</p>
</div>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div class="flex items-center gap-3">
<Shield class="w-5 h-5 text-muted-foreground" />
<div>
<p class="text-sm font-medium text-foreground">User ID</p>
<p class="text-sm text-muted-foreground font-mono">{auth.user?.id}</p>
</div>
</div>
</div>
</div>
</div>
<SettingsCard>
<SettingsRow label="Email" description={auth.user?.email || 'Not available'}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
{/snippet}
</SettingsRow>
<SettingsRow label="User ID" description={auth.user?.id || 'Not available'} border={false}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
{/snippet}
<span class="font-mono text-xs text-[hsl(var(--muted-foreground))]">{auth.user?.id || '-'}</span>
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Appearance Section -->
<div
class="bg-surface rounded-xl shadow-sm border border-border overflow-hidden"
>
<div class="p-4 border-b border-border">
<h2 class="text-lg font-semibold text-foreground flex items-center gap-2">
<Sun class="w-5 h-5 text-muted-foreground" />
Appearance
</h2>
</div>
<div class="p-4">
<p class="text-sm text-muted-foreground mb-4">Choose your preferred theme</p>
<!-- Appearance Section -->
<SettingsSection title="Appearance">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{/snippet}
<SettingsCard>
<div class="px-5 py-4">
<p class="font-medium text-[hsl(var(--foreground))] mb-2">Theme</p>
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">
Choose your preferred theme
</p>
<div class="grid grid-cols-3 gap-3">
<button
onclick={() => theme.setMode('light')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors {theme.mode ===
'light'
? 'border-primary bg-primary/10'
: 'border-border'}"
onclick={() => setThemeMode('light')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors
{theme.mode === 'light'
? 'border-[hsl(var(--primary))] bg-[hsl(var(--primary)/0.1)]'
: 'border-[hsl(var(--border))]'}"
>
<Sun class="w-6 h-6 text-amber-500" />
<span class="text-sm font-medium text-foreground">Light</span>
<svg class="w-6 h-6 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
<span class="text-sm font-medium text-[hsl(var(--foreground))]">Light</span>
</button>
<button
onclick={() => theme.setMode('dark')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors {theme.mode ===
'dark'
? 'border-primary bg-primary/10'
: 'border-border'}"
onclick={() => setThemeMode('dark')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors
{theme.mode === 'dark'
? 'border-[hsl(var(--primary))] bg-[hsl(var(--primary)/0.1)]'
: 'border-[hsl(var(--border))]'}"
>
<Moon class="w-6 h-6 text-indigo-500" />
<span class="text-sm font-medium text-foreground">Dark</span>
<svg class="w-6 h-6 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
<span class="text-sm font-medium text-[hsl(var(--foreground))]">Dark</span>
</button>
<button
onclick={() => theme.setMode('system')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors {theme.mode ===
'system'
? 'border-primary bg-primary/10'
: 'border-border'}"
onclick={() => setThemeMode('system')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors
{theme.mode === 'system'
? 'border-[hsl(var(--primary))] bg-[hsl(var(--primary)/0.1)]'
: 'border-[hsl(var(--border))]'}"
>
<Monitor class="w-6 h-6 text-muted-foreground" />
<span class="text-sm font-medium text-foreground">System</span>
<svg class="w-6 h-6 text-[hsl(var(--muted-foreground))]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
<span class="text-sm font-medium text-[hsl(var(--foreground))]">System</span>
</button>
</div>
</div>
</div>
</SettingsCard>
</SettingsSection>
<!-- Danger Zone -->
<div
class="bg-surface rounded-xl shadow-sm border border-red-300 dark:border-red-900/50 overflow-hidden"
<!-- Danger Zone -->
<SettingsDangerZone title="Danger Zone">
<SettingsDangerButton
label="Sign out"
description="Sign out of your account on this device"
buttonText="Sign out"
onclick={handleLogout}
border={false}
>
<div class="p-4 border-b border-red-300 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20">
<h2 class="text-lg font-semibold text-red-700 dark:text-red-400">Danger Zone</h2>
</div>
<div class="p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-foreground">Sign out</p>
<p class="text-sm text-muted-foreground">
Sign out of your account on this device
</p>
</div>
<button
onclick={handleLogout}
class="flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors"
>
<LogOut class="w-4 h-4" />
Sign out
</button>
</div>
</div>
</div>
</div>
</div>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
{/snippet}
</SettingsDangerButton>
</SettingsDangerZone>
</SettingsPage>

View file

@ -4,15 +4,15 @@
import { theme } from '$lib/stores/theme';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { ThemeColorPreview } from '@manacore/shared-theme-ui';
import { Sparkle, Leaf, Hexagon, Waves } from '@manacore/shared-icons';
// Theme icon mapping
const themeIcons = {
sparkle: Sparkle,
leaf: Leaf,
hexagon: Hexagon,
waves: Waves,
} as const;
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsToggle,
SettingsDangerZone,
SettingsDangerButton,
} from '@manacore/shared-ui';
// Settings state
let language = $state<'de' | 'en'>('de');
@ -32,7 +32,7 @@
localStorage.setItem(key, String(value));
}
function toggleDarkMode() {
function toggleDarkMode(value: boolean) {
theme.toggleMode();
}
@ -59,388 +59,191 @@
<title>Einstellungen - Zitare</title>
</svelte:head>
<div class="settings-page">
<div class="header-container">
<h1>Einstellungen</h1>
</div>
<SettingsPage title="Einstellungen" subtitle="Passe die App an deine Vorlieben an.">
<!-- Personal Section -->
<section class="settings-section">
<h2 class="section-title">Persönlich</h2>
<SettingsSection title="Persönlich">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{/snippet}
<div class="setting-card">
<div class="setting-header">
<h3>Dein Name</h3>
<SettingsCard>
<div class="px-5 py-4">
<label class="block">
<span class="font-medium text-[hsl(var(--foreground))] mb-2 block">Dein Name</span>
<input
type="text"
bind:value={userName}
onblur={saveUserName}
placeholder="Name eingeben..."
class="w-full px-3 py-2 rounded-lg border-2 border-[hsl(var(--border))] bg-[hsl(var(--background))] text-[hsl(var(--foreground))] focus:border-[hsl(var(--primary))] focus:outline-none transition-colors"
/>
</label>
<p class="text-sm text-[hsl(var(--muted-foreground))] mt-2">
Wird als Standard-Autor für eigene Zitate verwendet
</p>
</div>
<input
type="text"
bind:value={userName}
onblur={saveUserName}
placeholder="Name eingeben..."
class="text-input"
/>
<p class="setting-description">Wird als Standard-Autor für eigene Zitate verwendet</p>
</div>
</section>
</SettingsCard>
</SettingsSection>
<!-- Appearance Section -->
<section class="settings-section">
<h2 class="section-title">Aussehen</h2>
<!-- Dark Mode Toggle -->
<div class="setting-card">
<div class="setting-row">
<div class="setting-content">
<h3>Dark Mode</h3>
<p class="setting-description">Dunkles Farbschema verwenden</p>
</div>
<label class="toggle">
<input type="checkbox" checked={theme.isDark} onchange={toggleDarkMode} />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<!-- Current Theme -->
<div class="setting-card">
<div class="setting-row">
<div class="setting-content">
<h3>Aktuelles Theme</h3>
<p class="setting-description theme-label">
{#if THEME_DEFINITIONS[theme.variant].icon && themeIcons[THEME_DEFINITIONS[theme.variant].icon as keyof typeof themeIcons]}
<svelte:component
this={themeIcons[THEME_DEFINITIONS[theme.variant].icon as keyof typeof themeIcons]}
size={16}
weight="duotone"
class="theme-icon"
/>
{/if}
{THEME_DEFINITIONS[theme.variant].label}
</p>
</div>
<button class="theme-btn" onclick={() => goto('/themes')}>
Themes wählen
</button>
</div>
</div>
<!-- Theme Preview -->
<div class="setting-card">
<div class="setting-header">
<h3>Farbvorschau</h3>
<p class="setting-description">So sieht die App mit dem aktuellen Theme aus</p>
</div>
<div class="theme-preview">
<ThemeColorPreview
variant={theme.variant}
mode={theme.isDark ? 'dark' : 'light'}
size="lg"
<SettingsSection title="Aussehen">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
/>
</svg>
{/snippet}
<SettingsCard>
<SettingsToggle
label="Dark Mode"
description="Dunkles Farbschema verwenden"
isOn={theme.isDark}
onToggle={toggleDarkMode}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{/snippet}
</SettingsToggle>
<SettingsRow
label="Aktuelles Theme"
description={THEME_DEFINITIONS[theme.variant].label}
onclick={() => goto('/themes')}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
/>
</svg>
{/snippet}
<span class="px-3 py-1.5 text-sm font-medium bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] rounded-lg">
Themes wählen
</span>
</SettingsRow>
<div class="px-5 py-4 border-t border-[hsl(var(--border))]">
<p class="font-medium text-[hsl(var(--foreground))] mb-2">Farbvorschau</p>
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">
So sieht die App mit dem aktuellen Theme aus
</p>
<div class="flex justify-center">
<ThemeColorPreview
variant={theme.variant}
mode={theme.isDark ? 'dark' : 'light'}
size="lg"
/>
</div>
</div>
</div>
</section>
</SettingsCard>
</SettingsSection>
<!-- Language Section -->
<section class="settings-section">
<h2 class="section-title">Sprache</h2>
<SettingsSection title="Sprache">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
/>
</svg>
{/snippet}
<div class="setting-card">
<div class="setting-row">
<div class="setting-content">
<h3>Sprache</h3>
<p class="setting-description">Sprache der App und Zitate</p>
</div>
<div class="language-toggle">
<button
class="lang-btn"
class:active={language === 'de'}
onclick={() => setLanguageSetting('de')}
>
DE
</button>
<button
class="lang-btn"
class:active={language === 'en'}
onclick={() => setLanguageSetting('en')}
>
EN
</button>
<SettingsCard>
<div class="px-5 py-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-[hsl(var(--foreground))]">Sprache</p>
<p class="text-sm text-[hsl(var(--muted-foreground))]">Sprache der App und Zitate</p>
</div>
<div class="flex rounded-full overflow-hidden border border-[hsl(var(--border))]">
<button
class="px-4 py-2 text-sm font-medium transition-colors
{language === 'de'
? 'bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]'
: 'bg-transparent text-[hsl(var(--foreground))]'}"
onclick={() => setLanguageSetting('de')}
>
DE
</button>
<button
class="px-4 py-2 text-sm font-medium transition-colors
{language === 'en'
? 'bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]'
: 'bg-transparent text-[hsl(var(--foreground))]'}"
onclick={() => setLanguageSetting('en')}
>
EN
</button>
</div>
</div>
</div>
</div>
</section>
</SettingsCard>
</SettingsSection>
<!-- About Section -->
<section class="settings-section">
<h2 class="section-title">Über</h2>
<SettingsSection title="Über">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/snippet}
<div class="setting-card">
<div class="setting-row">
<span>Version</span>
<span class="setting-value">1.0.0</span>
</div>
</div>
</section>
<SettingsCard>
<SettingsRow label="Version" border={false}>
<span class="text-[hsl(var(--muted-foreground))]">1.0.0</span>
</SettingsRow>
</SettingsCard>
</SettingsSection>
<!-- Data Section -->
<section class="settings-section">
<h2 class="section-title">Daten</h2>
<div class="setting-card danger">
<button class="danger-btn" onclick={resetAllData}>
<div>
<h3 class="danger-title">Alle Daten zurücksetzen</h3>
<p class="setting-description">Löscht Favoriten, Playlists und Einstellungen</p>
</div>
<span class="danger-icon">🗑️</span>
</button>
</div>
</section>
</div>
<style>
.settings-page {
max-width: 1200px;
margin: 0 auto;
padding-bottom: var(--spacing-2xl);
}
.header-container {
max-width: 700px;
margin: 0 auto var(--spacing-2xl);
}
h1 {
font-size: 2rem;
margin: 0;
color: hsl(var(--foreground));
}
.settings-section {
max-width: 700px;
margin: 0 auto var(--spacing-2xl);
}
.section-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: hsl(var(--muted-foreground));
margin-bottom: var(--spacing-md);
}
.setting-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-md);
transition:
transform var(--transition-base),
box-shadow var(--transition-base);
}
.setting-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.setting-header {
margin-bottom: 1rem;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.setting-content {
flex: 1;
}
h3 {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0 0 var(--spacing-xs) 0;
}
.setting-description {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
margin: 0;
}
.setting-description.theme-label {
display: flex;
align-items: center;
gap: 0.375rem;
}
.setting-description.theme-label :global(.theme-icon) {
color: hsl(var(--primary));
}
.setting-value {
color: hsl(var(--muted-foreground));
font-size: 0.95rem;
}
/* Text Input */
.text-input {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
border: 2px solid hsl(var(--border));
background: hsl(var(--background));
color: hsl(var(--foreground));
font-size: 1rem;
margin-bottom: var(--spacing-sm);
transition: border-color var(--transition-fast);
}
.text-input:focus {
outline: none;
border-color: hsl(var(--primary));
}
/* Toggle Switch */
.toggle {
position: relative;
width: 51px;
height: 31px;
display: inline-block;
cursor: pointer;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: hsl(var(--border));
transition: var(--transition-base);
border-radius: 31px;
}
.toggle-slider:before {
position: absolute;
content: '';
height: 25px;
width: 25px;
left: 3px;
bottom: 3px;
background-color: white;
transition: var(--transition-base);
border-radius: 50%;
}
.toggle input:checked + .toggle-slider {
background-color: hsl(var(--primary));
}
.toggle input:checked + .toggle-slider:before {
transform: translateX(20px);
}
/* Theme Button */
.theme-btn {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
font-weight: 500;
border: none;
cursor: pointer;
transition: background var(--transition-fast);
}
.theme-btn:hover {
background: hsl(var(--primary) / 0.9);
}
/* Theme Preview */
.theme-preview {
margin-top: var(--spacing-md);
display: flex;
justify-content: center;
}
/* Language Toggle */
.language-toggle {
display: flex;
border-radius: var(--radius-full);
overflow: hidden;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
}
.lang-btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
background: transparent;
color: hsl(var(--foreground));
font-weight: 500;
cursor: pointer;
transition: background var(--transition-fast);
}
.lang-btn.active {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
/* Danger Zone */
.setting-card.danger {
background: hsl(var(--destructive) / 0.1);
border-color: hsl(var(--destructive) / 0.2);
}
.danger-btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
text-align: left;
}
.danger-title {
color: hsl(var(--destructive));
}
.danger-icon {
font-size: 1.25rem;
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.header-container,
.settings-section {
max-width: 100%;
}
h1 {
font-size: 1.5rem;
}
.section-title {
font-size: 0.7rem;
}
}
</style>
<SettingsDangerZone title="Daten">
<SettingsDangerButton
label="Alle Daten zurücksetzen"
description="Löscht Favoriten, Playlists und Einstellungen"
buttonText="Zurücksetzen"
onclick={resetAllData}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
{/snippet}
</SettingsDangerButton>
</SettingsDangerZone>
</SettingsPage>

View file

@ -47,3 +47,14 @@ export type {
PillNavElement,
PillNavigationProps,
} from './navigation';
// Settings
export {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsToggle,
SettingsDangerZone,
SettingsDangerButton,
} from './settings';

View file

@ -0,0 +1,107 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
/** Card title (optional) */
title?: string;
/** Card description (optional) */
description?: string;
/** Visual variant */
variant?: 'default' | 'danger';
/** Additional CSS classes */
class?: string;
/** Content (SettingsRow components) */
children: Snippet;
}
let {
title,
description,
variant = 'default',
class: className = '',
children,
}: Props = $props();
</script>
<div class="settings-card settings-card--{variant} {className}">
{#if title || description}
<header class="settings-card__header">
{#if title}
<h3 class="settings-card__title">{title}</h3>
{/if}
{#if description}
<p class="settings-card__description">{description}</p>
{/if}
</header>
{/if}
<div class="settings-card__content">
{@render children()}
</div>
</div>
<style>
.settings-card {
/* Glass effect */
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 1rem;
overflow: hidden;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:global(.dark) .settings-card {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.settings-card--danger {
border-color: hsl(var(--destructive) / 0.3);
background: rgba(239, 68, 68, 0.08);
}
:global(.dark) .settings-card--danger {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.25);
}
.settings-card__header {
padding: 1rem 1.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
:global(.dark) .settings-card__header {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.settings-card--danger .settings-card__header {
border-bottom-color: hsl(var(--destructive) / 0.2);
background: rgba(239, 68, 68, 0.1);
}
.settings-card__title {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0;
}
.settings-card--danger .settings-card__title {
color: hsl(var(--destructive));
}
.settings-card__description {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
margin: 0.25rem 0 0 0;
}
.settings-card__content {
display: flex;
flex-direction: column;
}
</style>

View file

@ -0,0 +1,180 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
/** Button label */
label: string;
/** Optional description */
description?: string;
/** Optional icon (Snippet for flexibility) */
icon?: Snippet;
/** Click handler */
onclick: () => void;
/** Button text (default: label) */
buttonText?: string;
/** Show border at bottom */
border?: boolean;
/** Disabled state */
disabled?: boolean;
/** Additional CSS classes */
class?: string;
}
let {
label,
description,
icon,
onclick,
buttonText,
border = true,
disabled = false,
class: className = '',
}: Props = $props();
</script>
<div
class="settings-danger-button {border ? 'settings-danger-button--border' : ''} {disabled ? 'settings-danger-button--disabled' : ''} {className}"
>
<div class="settings-danger-button__content">
{#if icon}
<span class="settings-danger-button__icon">
{@render icon()}
</span>
{/if}
<div class="settings-danger-button__text">
<span class="settings-danger-button__label">{label}</span>
{#if description}
<span class="settings-danger-button__description">{description}</span>
{/if}
</div>
</div>
<button
type="button"
{onclick}
class="settings-danger-button__button"
{disabled}
>
{buttonText || label}
</button>
</div>
<style>
.settings-danger-button {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
}
.settings-danger-button--border {
border-bottom: 1px solid rgba(239, 68, 68, 0.12);
}
:global(.dark) .settings-danger-button--border {
border-bottom-color: rgba(239, 68, 68, 0.18);
}
.settings-danger-button--border:last-child {
border-bottom: none;
}
.settings-danger-button--disabled {
opacity: 0.5;
cursor: not-allowed;
}
.settings-danger-button__content {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0;
}
.settings-danger-button__icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.625rem;
background: rgba(239, 68, 68, 0.1);
color: hsl(var(--destructive));
}
:global(.dark) .settings-danger-button__icon {
background: rgba(239, 68, 68, 0.15);
}
.settings-danger-button__icon :global(svg) {
width: 1.125rem;
height: 1.125rem;
}
.settings-danger-button__text {
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 0;
}
.settings-danger-button__label {
font-size: 0.9375rem;
font-weight: 500;
color: #374151;
}
:global(.dark) .settings-danger-button__label {
color: #f3f4f6;
}
.settings-danger-button__description {
font-size: 0.8125rem;
color: #6b7280;
line-height: 1.4;
}
:global(.dark) .settings-danger-button__description {
color: #9ca3af;
}
.settings-danger-button__button {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--destructive));
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 0.5rem;
cursor: pointer;
flex-shrink: 0;
transition: all 0.2s ease;
}
:global(.dark) .settings-danger-button__button {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.25);
}
.settings-danger-button__button:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.3);
}
:global(.dark) .settings-danger-button__button:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.25);
border-color: rgba(239, 68, 68, 0.35);
}
.settings-danger-button__button:disabled {
cursor: not-allowed;
}
.settings-danger-button__button:focus-visible {
outline: 2px solid rgba(239, 68, 68, 0.4);
outline-offset: 2px;
}
</style>

View file

@ -0,0 +1,71 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
/** Section title */
title?: string;
/** Additional CSS classes */
class?: string;
/** Content (danger actions) */
children: Snippet;
}
let {
title = 'Danger Zone',
class: className = '',
children,
}: Props = $props();
</script>
<section class="settings-danger-zone {className}">
<header class="settings-danger-zone__header">
<h2 class="settings-danger-zone__title">{title}</h2>
</header>
<div class="settings-danger-zone__content">
{@render children()}
</div>
</section>
<style>
.settings-danger-zone {
/* Glass effect with danger tint */
background: rgba(239, 68, 68, 0.08);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 1rem;
overflow: hidden;
box-shadow:
0 4px 6px -1px rgba(239, 68, 68, 0.1),
0 2px 4px -1px rgba(239, 68, 68, 0.06);
}
:global(.dark) .settings-danger-zone {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.25);
}
.settings-danger-zone__header {
padding: 1rem 1.25rem;
border-bottom: 1px solid rgba(239, 68, 68, 0.15);
background: rgba(239, 68, 68, 0.1);
}
:global(.dark) .settings-danger-zone__header {
border-bottom-color: rgba(239, 68, 68, 0.2);
background: rgba(239, 68, 68, 0.15);
}
.settings-danger-zone__title {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--destructive));
margin: 0;
}
.settings-danger-zone__content {
display: flex;
flex-direction: column;
}
</style>

View file

@ -0,0 +1,92 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
/** Page title */
title: string;
/** Optional subtitle/description */
subtitle?: string;
/** Maximum width of the content */
maxWidth?: 'sm' | 'md' | 'lg' | 'xl';
/** Additional CSS classes */
class?: string;
/** Main content */
children: Snippet;
}
let {
title,
subtitle,
maxWidth = 'md',
class: className = '',
children,
}: Props = $props();
const maxWidthClasses = {
sm: 'max-w-lg',
md: 'max-w-2xl',
lg: 'max-w-3xl',
xl: 'max-w-4xl',
};
</script>
<div class="settings-page {className}">
<div class="settings-page__container {maxWidthClasses[maxWidth]}">
<header class="settings-page__header">
<h1 class="settings-page__title">{title}</h1>
{#if subtitle}
<p class="settings-page__subtitle">{subtitle}</p>
{/if}
</header>
<div class="settings-page__content">
{@render children()}
</div>
</div>
</div>
<style>
.settings-page {
min-height: calc(100vh - 4rem);
padding: 2rem 1rem;
background-color: hsl(var(--background));
}
.settings-page__container {
margin-left: auto;
margin-right: auto;
}
.settings-page__header {
margin-bottom: 2rem;
}
.settings-page__title {
font-size: 1.5rem;
font-weight: 700;
color: hsl(var(--foreground));
margin: 0;
}
.settings-page__subtitle {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
margin: 0.25rem 0 0 0;
}
.settings-page__content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
@media (min-width: 640px) {
.settings-page {
padding: 2rem 1.5rem;
}
.settings-page__title {
font-size: 1.75rem;
}
}
</style>

View file

@ -0,0 +1,240 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
/** Row label */
label: string;
/** Optional description */
description?: string;
/** Optional icon (Snippet for flexibility) */
icon?: Snippet;
/** Make the entire row clickable */
href?: string;
/** Click handler (alternative to href) */
onclick?: () => void;
/** Show border at bottom */
border?: boolean;
/** Disabled state */
disabled?: boolean;
/** Additional CSS classes */
class?: string;
/** Control element (Toggle, Button, etc.) */
children?: Snippet;
}
let {
label,
description,
icon,
href,
onclick,
border = true,
disabled = false,
class: className = '',
children,
}: Props = $props();
const isClickable = $derived(!!href || !!onclick);
</script>
{#if href}
<a
{href}
class="settings-row {border ? 'settings-row--border' : ''} settings-row--clickable {disabled ? 'settings-row--disabled' : ''} {className}"
>
<div class="settings-row__content">
{#if icon}
<span class="settings-row__icon">
{@render icon()}
</span>
{/if}
<div class="settings-row__text">
<span class="settings-row__label">{label}</span>
{#if description}
<span class="settings-row__description">{description}</span>
{/if}
</div>
</div>
<div class="settings-row__control">
{#if children}
{@render children()}
{:else}
<svg class="settings-row__chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
{/if}
</div>
</a>
{:else if onclick}
<button
type="button"
{onclick}
class="settings-row {border ? 'settings-row--border' : ''} settings-row--clickable {disabled ? 'settings-row--disabled' : ''} {className}"
{disabled}
>
<div class="settings-row__content">
{#if icon}
<span class="settings-row__icon">
{@render icon()}
</span>
{/if}
<div class="settings-row__text">
<span class="settings-row__label">{label}</span>
{#if description}
<span class="settings-row__description">{description}</span>
{/if}
</div>
</div>
<div class="settings-row__control">
{#if children}
{@render children()}
{:else}
<svg class="settings-row__chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
{/if}
</div>
</button>
{:else}
<div
class="settings-row {border ? 'settings-row--border' : ''} {disabled ? 'settings-row--disabled' : ''} {className}"
>
<div class="settings-row__content">
{#if icon}
<span class="settings-row__icon">
{@render icon()}
</span>
{/if}
<div class="settings-row__text">
<span class="settings-row__label">{label}</span>
{#if description}
<span class="settings-row__description">{description}</span>
{/if}
</div>
</div>
{#if children}
<div class="settings-row__control">
{@render children()}
</div>
{/if}
</div>
{/if}
<style>
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
background: transparent;
border: none;
width: 100%;
text-align: left;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
}
.settings-row--border {
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
:global(.dark) .settings-row--border {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.settings-row--border:last-child {
border-bottom: none;
}
.settings-row--clickable {
cursor: pointer;
}
.settings-row--clickable:hover {
background: rgba(0, 0, 0, 0.04);
}
:global(.dark) .settings-row--clickable:hover {
background: rgba(255, 255, 255, 0.06);
}
.settings-row--disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.settings-row__content {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0;
}
.settings-row__icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.625rem;
background: rgba(0, 0, 0, 0.04);
color: hsl(var(--primary));
}
:global(.dark) .settings-row__icon {
background: rgba(255, 255, 255, 0.08);
}
.settings-row__icon :global(svg) {
width: 1.125rem;
height: 1.125rem;
}
.settings-row__text {
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 0;
}
.settings-row__label {
font-size: 0.9375rem;
font-weight: 500;
color: #374151;
}
:global(.dark) .settings-row__label {
color: #f3f4f6;
}
.settings-row__description {
font-size: 0.8125rem;
color: #6b7280;
line-height: 1.4;
}
:global(.dark) .settings-row__description {
color: #9ca3af;
}
.settings-row__control {
display: flex;
align-items: center;
flex-shrink: 0;
}
.settings-row__chevron {
width: 1.25rem;
height: 1.25rem;
color: #9ca3af;
}
:global(.dark) .settings-row__chevron {
color: #6b7280;
}
</style>

View file

@ -0,0 +1,91 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
/** Section title */
title?: string;
/** Optional icon (Snippet for flexibility) */
icon?: Snippet;
/** Additional CSS classes */
class?: string;
/** Content (SettingsCard components) */
children: Snippet;
}
let {
title,
icon,
class: className = '',
children,
}: Props = $props();
</script>
<section class="settings-section {className}">
{#if title}
<header class="settings-section__header">
{#if icon}
<span class="settings-section__icon">
{@render icon()}
</span>
{/if}
<h2 class="settings-section__title">{title}</h2>
</header>
{/if}
<div class="settings-section__content">
{@render children()}
</div>
</section>
<style>
.settings-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.settings-section__header {
display: flex;
align-items: center;
gap: 0.5rem;
padding-left: 0.25rem;
}
.settings-section__icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border-radius: 0.5rem;
background: rgba(0, 0, 0, 0.04);
color: hsl(var(--primary));
}
:global(.dark) .settings-section__icon {
background: rgba(255, 255, 255, 0.08);
}
.settings-section__icon :global(svg) {
width: 1rem;
height: 1rem;
}
.settings-section__title {
font-size: 0.9375rem;
font-weight: 600;
color: #374151;
margin: 0;
letter-spacing: -0.01em;
}
:global(.dark) .settings-section__title {
color: #f3f4f6;
}
.settings-section__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
</style>

View file

@ -0,0 +1,202 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
/** Row label */
label: string;
/** Optional description */
description?: string;
/** Optional icon (Snippet for flexibility) */
icon?: Snippet;
/** Toggle state */
isOn: boolean;
/** Toggle handler */
onToggle: (value: boolean) => void;
/** Show border at bottom */
border?: boolean;
/** Disabled state */
disabled?: boolean;
/** Additional CSS classes */
class?: string;
}
let {
label,
description,
icon,
isOn = false,
onToggle,
border = true,
disabled = false,
class: className = '',
}: Props = $props();
function handleToggle() {
if (!disabled) {
onToggle(!isOn);
}
}
</script>
<div
class="settings-toggle {border ? 'settings-toggle--border' : ''} {disabled ? 'settings-toggle--disabled' : ''} {className}"
>
<div class="settings-toggle__content">
{#if icon}
<span class="settings-toggle__icon">
{@render icon()}
</span>
{/if}
<div class="settings-toggle__text">
<span class="settings-toggle__label">{label}</span>
{#if description}
<span class="settings-toggle__description">{description}</span>
{/if}
</div>
</div>
<button
type="button"
onclick={handleToggle}
class="settings-toggle__switch {isOn ? 'settings-toggle__switch--on' : ''}"
role="switch"
aria-checked={isOn}
aria-label={label}
{disabled}
>
<span class="settings-toggle__thumb"></span>
</button>
</div>
<style>
.settings-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
}
.settings-toggle--border {
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
:global(.dark) .settings-toggle--border {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.settings-toggle--border:last-child {
border-bottom: none;
}
.settings-toggle--disabled {
opacity: 0.5;
cursor: not-allowed;
}
.settings-toggle__content {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0;
}
.settings-toggle__icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.625rem;
background: rgba(0, 0, 0, 0.04);
color: hsl(var(--primary));
}
:global(.dark) .settings-toggle__icon {
background: rgba(255, 255, 255, 0.08);
}
.settings-toggle__icon :global(svg) {
width: 1.125rem;
height: 1.125rem;
}
.settings-toggle__text {
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 0;
}
.settings-toggle__label {
font-size: 0.9375rem;
font-weight: 500;
color: #374151;
}
:global(.dark) .settings-toggle__label {
color: #f3f4f6;
}
.settings-toggle__description {
font-size: 0.8125rem;
color: #6b7280;
line-height: 1.4;
}
:global(.dark) .settings-toggle__description {
color: #9ca3af;
}
/* Toggle Switch - Glass style */
.settings-toggle__switch {
position: relative;
width: 3rem;
height: 1.75rem;
border-radius: 9999px;
border: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.08);
cursor: pointer;
flex-shrink: 0;
transition: all 0.2s ease;
}
:global(.dark) .settings-toggle__switch {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.15);
}
.settings-toggle__switch:disabled {
cursor: not-allowed;
}
.settings-toggle__switch--on {
background-color: hsl(var(--primary));
border-color: hsl(var(--primary));
}
.settings-toggle__thumb {
position: absolute;
top: 0.0625rem;
left: 0.0625rem;
width: 1.5rem;
height: 1.5rem;
border-radius: 9999px;
background-color: white;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.15),
0 1px 2px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.settings-toggle__switch--on .settings-toggle__thumb {
transform: translateX(1.25rem);
}
.settings-toggle__switch:focus-visible {
outline: 2px solid hsl(var(--primary) / 0.4);
outline-offset: 2px;
}
</style>

View file

@ -0,0 +1,8 @@
// Settings Components
export { default as SettingsPage } from './SettingsPage.svelte';
export { default as SettingsSection } from './SettingsSection.svelte';
export { default as SettingsCard } from './SettingsCard.svelte';
export { default as SettingsRow } from './SettingsRow.svelte';
export { default as SettingsToggle } from './SettingsToggle.svelte';
export { default as SettingsDangerZone } from './SettingsDangerZone.svelte';
export { default as SettingsDangerButton } from './SettingsDangerButton.svelte';