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

@ -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>