feat(chat): add settings page and improve chat UI components

- Add settings page to chat web and mobile apps
- Add ChatLayout component for consistent chat structure
- Update ChatInput component styling
- Improve chat page layout and navigation
- Update profile page

🤖 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:24:02 +01:00
parent c1644039f3
commit b68e0152da
10 changed files with 1458 additions and 440 deletions

View file

@ -74,6 +74,15 @@ export default function DrawerLayout() {
),
}}
/>
<Drawer.Screen
name="settings"
options={{
title: 'Einstellungen',
drawerIcon: ({ color, size }) => (
<Ionicons name="settings-outline" size={size} color={color} />
),
}}
/>
</Drawer>
);
}

View file

@ -378,7 +378,23 @@ export default function ProfileScreen() {
</View>
<View style={styles.settingsSection}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Einstellungen</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Schnellzugriff</Text>
<TouchableOpacity
style={[styles.settingItem, { borderBottomColor: colors.border }]}
onPress={() => router.push('/settings')}
>
<View style={styles.settingIconContainer}>
<Ionicons name="settings-outline" size={24} color={colors.primary} />
</View>
<View style={styles.settingContent}>
<Text style={[styles.settingTitle, { color: colors.text }]}>Einstellungen</Text>
<Text style={[styles.settingValue, { color: colors.text + '80' }]}>
Erscheinungsbild, Benachrichtigungen & mehr
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.text + '60'} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.settingItem, { borderBottomColor: colors.border }]}
@ -395,17 +411,6 @@ export default function ProfileScreen() {
</View>
<Ionicons name="chevron-forward" size={20} color={colors.text + '60'} />
</TouchableOpacity>
<TouchableOpacity style={[styles.settingItem, { borderBottomColor: colors.border }]}>
<View style={styles.settingIconContainer}>
<Ionicons name="notifications" size={24} color={colors.primary} />
</View>
<View style={styles.settingContent}>
<Text style={[styles.settingTitle, { color: colors.text }]}>Benachrichtigungen</Text>
<Text style={[styles.settingValue, { color: colors.text + '80' }]}>Ein</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.text + '60'} />
</TouchableOpacity>
</View>
<View style={styles.accountSection}>

View file

@ -0,0 +1,512 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Switch,
Alert,
Platform,
Linking,
} from 'react-native';
import { useTheme } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAppTheme } from '../theme/ThemeProvider';
type ThemeVariant = 'default' | 'ocean' | 'forest' | 'sunset' | 'lavender';
interface ThemeOption {
id: ThemeVariant;
label: string;
icon: string;
color: string;
}
const themeOptions: ThemeOption[] = [
{ id: 'default', label: 'Standard', icon: 'color-palette', color: '#3b82f6' },
{ id: 'ocean', label: 'Ozean', icon: 'water', color: '#0ea5e9' },
{ id: 'forest', label: 'Wald', icon: 'leaf', color: '#22c55e' },
{ id: 'sunset', label: 'Sonnenuntergang', icon: 'sunny', color: '#f97316' },
{ id: 'lavender', label: 'Lavendel', icon: 'flower', color: '#a855f7' },
];
export default function SettingsScreen() {
const { colors } = useTheme();
const { isDarkMode, toggleTheme } = useAppTheme();
const router = useRouter();
// Local state for settings (would be persisted in a real app)
const [selectedTheme, setSelectedTheme] = useState<ThemeVariant>('default');
const [pushNotifications, setPushNotifications] = useState(false);
const [emailNotifications, setEmailNotifications] = useState(false);
const handleDeleteChatHistory = () => {
Alert.alert(
'Chat-Verlauf löschen',
'Bist du sicher, dass du alle Konversationen permanent löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.',
[
{
text: 'Abbrechen',
style: 'cancel',
},
{
text: 'Löschen',
style: 'destructive',
onPress: () => {
// TODO: Implement chat history deletion
Alert.alert('Erfolg', 'Dein Chat-Verlauf wurde gelöscht.');
},
},
]
);
};
const handleChangePassword = () => {
router.push('/profile');
};
const openLink = (url: string) => {
Linking.openURL(url);
};
return (
<ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text }]}>Einstellungen</Text>
<Text style={[styles.subtitle, { color: colors.text + '80' }]}>
Passe die App an deine Vorlieben an
</Text>
</View>
{/* Appearance Section */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="color-palette" size={20} color={colors.primary} />
<Text style={[styles.sectionTitle, { color: colors.text }]}>Erscheinungsbild</Text>
</View>
<View
style={[
styles.card,
{
backgroundColor: colors.card,
borderColor: colors.border,
shadowColor: isDarkMode ? undefined : '#000',
},
]}
>
{/* Dark Mode Toggle */}
<View style={[styles.settingRow, { borderBottomColor: colors.border }]}>
<View style={styles.settingInfo}>
<Ionicons
name={isDarkMode ? 'moon' : 'sunny'}
size={22}
color={colors.primary}
style={styles.settingIcon}
/>
<View>
<Text style={[styles.settingLabel, { color: colors.text }]}>Dunkler Modus</Text>
<Text style={[styles.settingDescription, { color: colors.text + '70' }]}>
{isDarkMode ? 'Aktiviert' : 'Deaktiviert'}
</Text>
</View>
</View>
<Switch
value={isDarkMode}
onValueChange={toggleTheme}
trackColor={{ false: colors.border, true: colors.primary + '60' }}
thumbColor={isDarkMode ? colors.primary : '#f4f3f4'}
ios_backgroundColor={colors.border}
/>
</View>
{/* Theme Selection */}
<View style={styles.themeSection}>
<Text style={[styles.settingLabel, { color: colors.text, marginBottom: 12 }]}>
Farbschema
</Text>
<View style={styles.themeGrid}>
{themeOptions.map((theme) => (
<TouchableOpacity
key={theme.id}
style={[
styles.themeOption,
{
borderColor:
selectedTheme === theme.id ? colors.primary : colors.border,
backgroundColor:
selectedTheme === theme.id ? colors.primary + '10' : 'transparent',
},
]}
onPress={() => setSelectedTheme(theme.id)}
>
<Ionicons
name={theme.icon as any}
size={24}
color={selectedTheme === theme.id ? colors.primary : theme.color}
/>
<Text
style={[
styles.themeLabel,
{
color: selectedTheme === theme.id ? colors.primary : colors.text,
},
]}
>
{theme.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</View>
</View>
{/* Notifications Section */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="notifications" size={20} color={colors.primary} />
<Text style={[styles.sectionTitle, { color: colors.text }]}>Benachrichtigungen</Text>
</View>
<View
style={[
styles.card,
{
backgroundColor: colors.card,
borderColor: colors.border,
shadowColor: isDarkMode ? undefined : '#000',
},
]}
>
<View style={[styles.settingRow, { borderBottomColor: colors.border }]}>
<View style={styles.settingInfo}>
<Ionicons
name="phone-portrait-outline"
size={22}
color={colors.text + '60'}
style={styles.settingIcon}
/>
<View>
<Text style={[styles.settingLabel, { color: colors.text }]}>
Push-Benachrichtigungen
</Text>
<Text style={[styles.settingDescription, { color: colors.text + '70' }]}>
Erhalte Benachrichtigungen über neue Nachrichten
</Text>
</View>
</View>
<Switch
value={pushNotifications}
onValueChange={setPushNotifications}
trackColor={{ false: colors.border, true: colors.primary + '60' }}
thumbColor={pushNotifications ? colors.primary : '#f4f3f4'}
ios_backgroundColor={colors.border}
disabled
/>
</View>
<View style={styles.settingRowLast}>
<View style={styles.settingInfo}>
<Ionicons
name="mail-outline"
size={22}
color={colors.text + '60'}
style={styles.settingIcon}
/>
<View>
<Text style={[styles.settingLabel, { color: colors.text }]}>
E-Mail-Benachrichtigungen
</Text>
<Text style={[styles.settingDescription, { color: colors.text + '70' }]}>
Erhalte wöchentliche Zusammenfassungen
</Text>
</View>
</View>
<Switch
value={emailNotifications}
onValueChange={setEmailNotifications}
trackColor={{ false: colors.border, true: colors.primary + '60' }}
thumbColor={emailNotifications ? colors.primary : '#f4f3f4'}
ios_backgroundColor={colors.border}
disabled
/>
</View>
<Text style={[styles.disabledNote, { color: colors.text + '50' }]}>
Benachrichtigungen werden in einer zukünftigen Version verfügbar sein.
</Text>
</View>
</View>
{/* Privacy & Security Section */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="shield-checkmark" size={20} color={colors.primary} />
<Text style={[styles.sectionTitle, { color: colors.text }]}>
Datenschutz & Sicherheit
</Text>
</View>
<View
style={[
styles.card,
{
backgroundColor: colors.card,
borderColor: colors.border,
shadowColor: isDarkMode ? undefined : '#000',
},
]}
>
<TouchableOpacity
style={[styles.actionRow, { borderBottomColor: colors.border }]}
onPress={handleChangePassword}
>
<View style={styles.settingInfo}>
<Ionicons
name="key-outline"
size={22}
color={colors.primary}
style={styles.settingIcon}
/>
<View>
<Text style={[styles.settingLabel, { color: colors.text }]}>
Passwort ändern
</Text>
<Text style={[styles.settingDescription, { color: colors.text + '70' }]}>
Aktualisiere dein Passwort regelmäßig
</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.text + '60'} />
</TouchableOpacity>
<TouchableOpacity style={styles.actionRowLast} onPress={handleDeleteChatHistory}>
<View style={styles.settingInfo}>
<Ionicons
name="trash-outline"
size={22}
color="#FF3B30"
style={styles.settingIcon}
/>
<View>
<Text style={[styles.settingLabel, { color: '#FF3B30' }]}>
Chat-Verlauf löschen
</Text>
<Text style={[styles.settingDescription, { color: colors.text + '70' }]}>
Lösche alle Konversationen permanent
</Text>
</View>
</View>
</TouchableOpacity>
</View>
</View>
{/* About Section */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="information-circle" size={20} color={colors.primary} />
<Text style={[styles.sectionTitle, { color: colors.text }]}>Über die App</Text>
</View>
<View
style={[
styles.card,
{
backgroundColor: colors.card,
borderColor: colors.border,
shadowColor: isDarkMode ? undefined : '#000',
},
]}
>
<View style={[styles.infoRow, { borderBottomColor: colors.border }]}>
<Text style={[styles.infoLabel, { color: colors.text + '80' }]}>Version</Text>
<Text style={[styles.infoValue, { color: colors.text }]}>1.0.0</Text>
</View>
<View style={styles.infoRowLast}>
<Text style={[styles.infoLabel, { color: colors.text + '80' }]}>Build</Text>
<Text style={[styles.infoValue, { color: colors.text }]}>2024.11.29</Text>
</View>
</View>
<View style={styles.linksContainer}>
<TouchableOpacity
style={styles.linkButton}
onPress={() => openLink('https://example.com/privacy')}
>
<Text style={[styles.linkText, { color: colors.primary }]}>Datenschutz</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={() => openLink('https://example.com/terms')}
>
<Text style={[styles.linkText, { color: colors.primary }]}>Nutzungsbedingungen</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={() => openLink('https://example.com/support')}
>
<Text style={[styles.linkText, { color: colors.primary }]}>Hilfe & Support</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.bottomSpacer} />
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
},
header: {
marginTop: 20,
marginBottom: 24,
},
title: {
fontSize: 28,
fontWeight: 'bold',
},
subtitle: {
fontSize: 14,
marginTop: 4,
},
section: {
marginBottom: 24,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
gap: 8,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
},
card: {
borderRadius: 12,
borderWidth: 1,
overflow: 'hidden',
...Platform.select({
ios: {
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
android: {
elevation: 2,
},
}),
},
settingRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
},
settingRowLast: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
},
actionRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
},
actionRowLast: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
},
settingInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
settingIcon: {
marginRight: 12,
},
settingLabel: {
fontSize: 15,
fontWeight: '500',
},
settingDescription: {
fontSize: 13,
marginTop: 2,
},
themeSection: {
padding: 16,
},
themeGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
themeOption: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 10,
borderWidth: 2,
gap: 8,
},
themeLabel: {
fontSize: 13,
fontWeight: '500',
},
disabledNote: {
fontSize: 12,
fontStyle: 'italic',
paddingHorizontal: 16,
paddingBottom: 16,
},
infoRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
},
infoRowLast: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
},
infoLabel: {
fontSize: 14,
},
infoValue: {
fontSize: 14,
fontWeight: '500',
},
linksContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 16,
gap: 16,
},
linkButton: {
paddingVertical: 4,
},
linkText: {
fontSize: 14,
fontWeight: '500',
},
bottomSpacer: {
height: 40,
},
});

View file

@ -40,8 +40,10 @@
}
</script>
<div class="border-t border-border bg-surface p-4">
<div class="flex items-end gap-3 max-w-4xl mx-auto">
<div class="relative">
<div
class="flex items-end gap-3 rounded-2xl border border-border bg-white/70 dark:bg-black/50 backdrop-blur-xl p-2 shadow-lg"
>
<div class="flex-1 relative">
<textarea
bind:this={textareaEl}
@ -51,10 +53,9 @@
{placeholder}
{disabled}
rows="1"
class="w-full resize-none rounded-xl border border-border
bg-muted text-foreground
px-4 py-3 text-sm
focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
class="w-full resize-none rounded-xl border-0 bg-transparent
px-4 py-3 text-sm text-foreground
focus:outline-none focus:ring-0
disabled:opacity-50 disabled:cursor-not-allowed
placeholder:text-muted-foreground"
></textarea>
@ -66,12 +67,12 @@
class="flex-shrink-0 p-3 rounded-xl bg-primary text-primary-foreground
hover:bg-primary/90 active:bg-primary/80
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary
transition-colors"
transition-all duration-200 shadow-md hover:shadow-lg"
>
<PaperPlaneTilt size={20} weight="bold" />
</button>
</div>
<p class="text-xs text-muted-foreground text-center mt-2">
<p class="text-xs text-muted-foreground text-center mt-2 opacity-70">
Enter zum Senden, Shift+Enter für neue Zeile
</p>
</div>

View file

@ -0,0 +1,282 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { page } from '$app/stores';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { isSidebarMode, isNavCollapsed } from '$lib/stores/navigation';
import { MagnifyingGlass, X, Plus, ChatCircle } from '@manacore/shared-icons';
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
}
let { children }: Props = $props();
// Resizer state
let leftColumnWidth = $state(320);
let isResizing = $state(false);
const MIN_WIDTH = 260;
const MAX_WIDTH = 450;
// Search state
let searchQuery = $state('');
// Get conversations from store
let conversations = $derived(conversationsStore.conversations);
let isLoading = $derived(conversationsStore.isLoading);
// Filtered conversations based on search
let filteredConversations = $derived(
searchQuery.trim()
? conversations.filter((conv) =>
conv.title?.toLowerCase().includes(searchQuery.toLowerCase())
)
: conversations
);
// Resizer handlers
function startResize(e: MouseEvent) {
e.preventDefault();
isResizing = true;
}
function handleMouseMove(e: MouseEvent) {
if (!isResizing) return;
const newWidth = e.clientX;
if (newWidth >= MIN_WIDTH && newWidth <= MAX_WIDTH) {
leftColumnWidth = newWidth;
}
}
function stopResize() {
isResizing = false;
}
// Load conversations on mount
onMount(() => {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', stopResize);
if (authStore.user) {
conversationsStore.loadConversations(authStore.user.id);
}
});
onDestroy(() => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', stopResize);
});
function formatDate(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
} else if (diffDays === 1) {
return 'Gestern';
} else if (diffDays < 7) {
return date.toLocaleDateString('de-DE', { weekday: 'short' });
} else {
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
}
}
function getPreview(title: string | undefined): string {
if (!title) return 'Neue Konversation';
return title.length > 60 ? title.substring(0, 60) + '...' : title;
}
// Check if current conversation is active
function isActive(convId: string): boolean {
return $page.params.id === convId;
}
</script>
<div
class="flex w-full gap-0 overflow-hidden"
class:h-screen={$isNavCollapsed || $isSidebarMode}
class:h-[calc(100vh-5rem)]={!$isNavCollapsed && !$isSidebarMode}
>
<!-- Left Column: Conversation List -->
<div
class="relative flex flex-shrink-0 flex-col bg-muted/30 border-r border-border"
style="width: {leftColumnWidth}px;"
>
<!-- Search Bar -->
<div class="px-3 py-3 border-b border-border">
<div class="relative">
<input
type="text"
bind:value={searchQuery}
placeholder="Konversationen durchsuchen..."
class="w-full rounded-xl border border-border bg-white/70 dark:bg-black/50 backdrop-blur-xl px-4 py-2.5 pl-10 text-sm font-medium text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary shadow-sm"
/>
<MagnifyingGlass
size={16}
weight="bold"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
{#if searchQuery}
<button
onclick={() => (searchQuery = '')}
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
title="Suche leeren"
>
<X size={14} weight="bold" />
</button>
{/if}
</div>
</div>
<!-- Conversation List (Scrollable) -->
<div class="flex-1 overflow-hidden flex flex-col">
<div class="flex-1 overflow-y-auto py-3 px-3 scrollbar-hide">
<!-- New Chat Button -->
<a
href="/chat"
class="flex items-center gap-2 w-full mb-3 px-4 py-2.5 rounded-xl
bg-primary text-primary-foreground font-medium text-sm
shadow-md hover:bg-primary/90 transition-colors"
>
<Plus size={18} weight="bold" />
Neuer Chat
</a>
{#if isLoading}
<div class="flex items-center justify-center py-8">
<div
class="animate-spin w-6 h-6 border-2 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if filteredConversations.length === 0}
<div class="flex flex-col items-center justify-center py-8 text-center">
{#if searchQuery}
<div class="text-4xl mb-3">🔍</div>
<h3 class="text-base font-semibold mb-1 text-foreground">Keine Ergebnisse</h3>
<p class="text-sm text-muted-foreground mb-3">
Keine Konversationen für "{searchQuery}"
</p>
<button
onclick={() => (searchQuery = '')}
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Suche leeren
</button>
{:else}
<div class="text-4xl mb-3">💬</div>
<h3 class="text-base font-semibold mb-1 text-foreground">Keine Konversationen</h3>
<p class="text-sm text-muted-foreground">Starte einen neuen Chat</p>
{/if}
</div>
{:else}
{#each filteredConversations as conv (conv.id)}
<a
href="/chat/{conv.id}"
class="block w-full rounded-xl border bg-surface p-4 text-left transition-all mb-3 hover:shadow-md
{isActive(conv.id)
? 'border-primary bg-primary/5 shadow-md'
: 'border-border hover:bg-muted/50'}"
>
<!-- Title Row -->
<div class="mb-1.5 flex items-center gap-2">
<ChatCircle
size={16}
weight={isActive(conv.id) ? 'fill' : 'regular'}
class="flex-shrink-0 {isActive(conv.id)
? 'text-primary'
: 'text-muted-foreground'}"
/>
<h3 class="text-sm font-semibold line-clamp-1 text-foreground flex-1">
{conv.title || 'Neue Konversation'}
</h3>
</div>
<!-- Preview -->
<p class="mb-2 text-sm text-muted-foreground line-clamp-2">
{getPreview(conv.title)}
</p>
<!-- Footer -->
<div class="flex items-center justify-between">
<span class="text-xs text-muted-foreground">
{formatDate(conv.updated_at || conv.created_at)}
</span>
{#if conv.document_mode}
<span
class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full"
>
Dokument
</span>
{/if}
</div>
</a>
{/each}
{/if}
</div>
</div>
</div>
<!-- Resizer -->
<button
type="button"
aria-label="Sidebar-Breite anpassen"
class="resizer"
class:resizing={isResizing}
onmousedown={startResize}
></button>
<!-- Right Column: Chat Content -->
<div class="flex flex-1 flex-col bg-surface overflow-hidden">
{@render children()}
</div>
</div>
<style>
/* Hide scrollbar completely */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Resizer */
.resizer {
width: 4px;
background-color: transparent;
cursor: col-resize;
user-select: none;
transition: background-color 0.2s;
position: relative;
}
.resizer:hover,
.resizer.resizing {
background-color: var(--color-primary, #3b82f6);
}
.resizer::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -4px;
right: -4px;
}
.resizer:active {
background-color: var(--color-primary, #2563eb);
}
/* Prevent text selection while resizing */
:global(body:has(.resizer.resizing)) {
user-select: none;
cursor: col-resize;
}
</style>

View file

@ -1,65 +1,6 @@
<script lang="ts">
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import ConversationList from '$lib/components/chat/ConversationList.svelte';
import { X, List } from '@manacore/shared-icons';
let { children }: { children: any } = $props();
let showSidebar = $state(true);
// Wait for auth to be initialized before loading conversations
$effect(() => {
if (authStore.initialized && authStore.user) {
conversationsStore.loadConversations(authStore.user.id);
}
});
function toggleSidebar() {
showSidebar = !showSidebar;
}
</script>
<div class="flex h-[calc(100vh-4rem)]">
<!-- Sidebar Toggle (mobile) -->
<button
onclick={toggleSidebar}
class="fixed bottom-4 left-4 z-50 p-3 bg-primary text-primary-foreground rounded-full shadow-lg
sm:hidden hover:bg-primary/90 transition-colors"
aria-label={showSidebar ? 'Seitenleiste schließen' : 'Seitenleiste öffnen'}
>
{#if showSidebar}
<X size={24} weight="bold" />
{:else}
<List size={24} weight="bold" />
{/if}
</button>
<!-- Sidebar -->
<aside
class="w-72 flex-shrink-0 bg-surface border-r border-border
transition-transform duration-200 ease-in-out
fixed sm:static inset-y-0 left-0 z-40 top-16
{showSidebar
? 'translate-x-0'
: '-translate-x-full sm:translate-x-0 sm:w-0 sm:border-0'}"
>
<ConversationList
conversations={conversationsStore.conversations}
isLoading={conversationsStore.isLoading}
/>
</aside>
<!-- Mobile Overlay -->
{#if showSidebar}
<button
class="fixed inset-0 bg-black/50 z-30 sm:hidden"
onclick={toggleSidebar}
aria-label="Seitenleiste schließen"
></button>
{/if}
<!-- Main Content -->
<main class="flex-1 overflow-hidden">
{@render children()}
</main>
</div>
<!-- Chat pages handle their own layout via ChatLayout component -->
{@render children()}

View file

@ -8,9 +8,9 @@
import MessageList from '$lib/components/chat/MessageList.svelte';
import ChatInput from '$lib/components/chat/ChatInput.svelte';
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import { theme } from '$lib/stores/theme';
import ChatLayout from '$lib/components/chat/ChatLayout.svelte';
import type { AIModel, Message, Template } from '@chat/types';
import { FileText, Moon } from '@manacore/shared-icons';
import { FileText, Sparkle } from '@manacore/shared-icons';
let models = $state<AIModel[]>([]);
let templates = $state<Template[]>([]);
@ -125,100 +125,137 @@
function toggleDocumentMode() {
documentMode = !documentMode;
}
function toggleTheme() {
theme.toggleMode();
}
</script>
<svelte:head>
<title>Chat | ManaChat</title>
</svelte:head>
<div class="flex flex-col h-full">
<!-- Chat Header -->
<header
class="flex-shrink-0 border-b border-border bg-surface px-4 py-3"
>
<div class="flex items-center justify-between max-w-4xl mx-auto">
<div class="flex items-center gap-3 flex-wrap">
<h2 class="text-lg font-semibold text-foreground">Neuer Chat</h2>
<ChatLayout>
{#snippet children()}
<div class="flex flex-col h-full">
<!-- Welcome Header -->
<div class="flex-shrink-0 border-b border-border bg-surface/50 backdrop-blur-sm px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center"
>
<Sparkle size={22} weight="fill" class="text-primary-foreground" />
</div>
<div>
<h2 class="text-lg font-semibold text-foreground">Neuer Chat</h2>
<p class="text-sm text-muted-foreground">Starte eine neue Unterhaltung</p>
</div>
</div>
<div class="flex items-center gap-3 flex-wrap">
<!-- Model Selector -->
<ModelSelector
{models}
{selectedModelId}
onSelect={handleModelSelect}
disabled={isSending}
/>
<!-- Model Selector -->
<ModelSelector
{models}
{selectedModelId}
onSelect={handleModelSelect}
disabled={isSending}
/>
<!-- Template Selector -->
{#if templates.length > 0}
<select
onchange={handleTemplateSelect}
value={selectedTemplateId}
disabled={isSending}
class="px-3 py-1.5 text-sm border border-border rounded-lg
bg-surface text-foreground
focus:ring-2 focus:ring-primary focus:border-transparent
disabled:opacity-50"
>
<option value="">Ohne Vorlage</option>
{#each templates as template}
<option value={template.id}>
{template.name}
{template.is_default ? ' (Standard)' : ''}
</option>
{/each}
</select>
{/if}
<!-- Template Selector -->
{#if templates.length > 0}
<select
onchange={handleTemplateSelect}
value={selectedTemplateId}
disabled={isSending}
class="px-3 py-1.5 text-sm border border-border rounded-lg
bg-surface text-foreground
focus:ring-2 focus:ring-primary focus:border-transparent
disabled:opacity-50"
>
<option value="">Ohne Vorlage</option>
{#each templates as template}
<option value={template.id}>
{template.name}
{template.is_default ? ' (Standard)' : ''}
</option>
{/each}
</select>
<!-- Document Mode Toggle -->
<button
onclick={toggleDocumentMode}
disabled={isSending}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg transition-colors
{documentMode
? 'bg-primary/10 text-primary border border-primary/30'
: 'bg-muted text-muted-foreground border border-border'}
hover:bg-opacity-80 disabled:opacity-50"
title="Dokumentmodus aktivieren"
>
<FileText size={16} weight="bold" />
Dokument
</button>
</div>
</div>
</div>
<!-- Messages Area -->
<main class="flex-1 overflow-hidden">
{#if messages.length === 0 && !isSending}
<!-- Empty State -->
<div class="h-full flex flex-col items-center justify-center px-6">
<div class="text-center max-w-md">
<div
class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center"
>
<Sparkle size={32} weight="duotone" class="text-primary" />
</div>
<h3 class="text-xl font-semibold text-foreground mb-2">Worüber möchtest du reden?</h3>
<p class="text-muted-foreground mb-6">
Stelle eine Frage, bitte um Hilfe bei einem Projekt oder starte einfach eine
Unterhaltung.
</p>
<div class="flex flex-wrap justify-center gap-2">
<button
onclick={() => handleSend('Erkläre mir, wie KI funktioniert')}
class="px-4 py-2 text-sm rounded-xl bg-muted hover:bg-muted/80 text-foreground transition-colors"
>
Erkläre mir KI
</button>
<button
onclick={() => handleSend('Hilf mir beim Schreiben eines Textes')}
class="px-4 py-2 text-sm rounded-xl bg-muted hover:bg-muted/80 text-foreground transition-colors"
>
Beim Schreiben helfen
</button>
<button
onclick={() => handleSend('Was sind aktuelle Technologie-Trends?')}
class="px-4 py-2 text-sm rounded-xl bg-muted hover:bg-muted/80 text-foreground transition-colors"
>
Tech-Trends
</button>
</div>
</div>
</div>
{:else}
<div class="h-full flex flex-col">
<MessageList {messages} isTyping={isSending} />
</div>
{/if}
</main>
<!-- Document Mode Toggle -->
<button
onclick={toggleDocumentMode}
disabled={isSending}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg transition-colors
{documentMode
? 'bg-primary/10 text-primary border border-primary/30'
: 'bg-muted text-muted-foreground border border-border'}
hover:bg-opacity-80 disabled:opacity-50"
title="Dokumentmodus aktivieren"
>
<FileText size={16} weight="bold" />
Dokument
</button>
<!-- Floating Chat Input -->
<div class="flex-shrink-0 p-4 bg-gradient-to-t from-surface via-surface to-transparent">
<div class="max-w-3xl mx-auto">
<ChatInput onSend={handleSend} disabled={isSending || isLoading} />
</div>
</div>
<div class="flex items-center gap-2">
<button
onclick={toggleTheme}
class="p-2 text-muted-foreground
bg-muted rounded-lg
hover:bg-muted/80 transition-colors"
aria-label="Theme wechseln"
<!-- Error Message -->
{#if error}
<div
class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-destructive text-destructive-foreground rounded-lg shadow-lg z-50"
>
<Moon size={20} weight="bold" />
</button>
</div>
{error}
</div>
{/if}
</div>
</header>
<!-- Messages Area -->
<main class="flex-1 overflow-hidden bg-surface">
<div class="h-full max-w-4xl mx-auto flex flex-col">
<MessageList {messages} isTyping={isSending} />
</div>
</main>
<!-- Input Area -->
<ChatInput onSend={handleSend} disabled={isSending || isLoading} />
<!-- Error Message -->
{#if error}
<div
class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-red-500 text-white rounded-lg shadow-lg"
>
{error}
</div>
{/if}
</div>
{/snippet}
</ChatLayout>

View file

@ -7,11 +7,20 @@
import { documentService } from '$lib/services/document';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { theme } from '$lib/stores/theme';
import MessageList from '$lib/components/chat/MessageList.svelte';
import ChatInput from '$lib/components/chat/ChatInput.svelte';
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import ChatLayout from '$lib/components/chat/ChatLayout.svelte';
import type { Conversation, Message, AIModel, Document } from '@chat/types';
import {
Archive,
Trash,
FileText,
ClockCounterClockwise,
X,
FloppyDisk,
ChatCircle,
} from '@manacore/shared-icons';
let conversation = $state<Conversation | null>(null);
let messages = $state<Message[]>([]);
@ -166,301 +175,249 @@
}
}
}
function toggleTheme() {
theme.toggleMode();
}
</script>
<svelte:head>
<title>{conversation?.title || 'Chat'} | ManaChat</title>
</svelte:head>
{#if isLoading}
<div class="flex items-center justify-center h-full">
<div
class="animate-spin w-8 h-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if error && !conversation}
<div class="flex flex-col items-center justify-center h-full text-center p-4">
<p class="text-destructive mb-4">{error}</p>
<a href="/chat" class="text-primary hover:underline">Zurück zum Chat</a>
</div>
{:else}
<div class="flex flex-col h-full">
<!-- Chat Header -->
<header
class="flex-shrink-0 border-b border-border bg-surface px-4 py-3"
>
<div class="flex items-center justify-between max-w-4xl mx-auto">
<div class="flex items-center gap-4">
<h2 class="text-lg font-semibold text-foreground truncate max-w-xs">
{conversation?.title || 'Chat'}
</h2>
<ModelSelector
{models}
{selectedModelId}
onSelect={handleModelSelect}
disabled={isSending}
/>
</div>
<div class="flex items-center gap-2">
{#if isDocumentMode}
<button
onclick={toggleDocumentPanel}
class="p-2 transition-colors rounded-lg
{showDocumentPanel
? 'text-primary bg-primary/10'
: 'text-foreground bg-muted hover:bg-muted/80'}"
aria-label="Dokument-Panel"
title="Dokument-Panel ein/ausblenden"
>
<svg class="w-5 h-5" 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>
</button>
{/if}
<button
onclick={handleArchive}
class="p-2 text-foreground bg-muted rounded-lg hover:bg-muted/80 transition-colors"
aria-label="Archivieren"
title="Archivieren"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
/>
</svg>
</button>
<button
onclick={handleDelete}
class="p-2 text-destructive bg-muted rounded-lg hover:bg-destructive/10 transition-colors"
aria-label="Löschen"
title="Löschen"
>
<svg class="w-5 h-5" 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>
</button>
<button
onclick={toggleTheme}
class="p-2 text-foreground bg-muted rounded-lg hover:bg-muted/80 transition-colors"
aria-label="Theme wechseln"
>
<svg class="w-5 h-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>
</button>
</div>
</div>
</header>
<!-- Main Content Area -->
<div class="flex-1 flex overflow-hidden">
<!-- Chat Area -->
<div
class="flex-1 flex flex-col overflow-hidden {isDocumentMode && showDocumentPanel
? 'lg:w-1/2'
: 'w-full'}"
>
<!-- Messages Area -->
<main class="flex-1 overflow-hidden bg-surface">
<div class="h-full max-w-4xl mx-auto flex flex-col">
<MessageList {messages} isTyping={isSending} />
</div>
</main>
<!-- Input Area -->
<ChatInput onSend={handleSend} disabled={isSending} />
</div>
<!-- Document Panel -->
{#if isDocumentMode && showDocumentPanel}
<ChatLayout>
{#snippet children()}
{#if isLoading}
<div class="flex items-center justify-center h-full">
<div
class="hidden lg:flex lg:w-1/2 flex-col border-l border-border bg-surface"
class="animate-spin w-8 h-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if error && !conversation}
<div class="flex flex-col items-center justify-center h-full text-center p-4">
<p class="text-destructive mb-4">{error}</p>
<a href="/chat" class="text-primary hover:underline">Zurück zum Chat</a>
</div>
{:else}
<div class="flex flex-col h-full">
<!-- Chat Header -->
<header
class="flex-shrink-0 border-b border-border bg-surface/50 backdrop-blur-sm px-6 py-4"
>
<!-- Document Header -->
<div
class="flex items-center justify-between px-4 py-3 border-b border-border"
>
<div class="flex items-center gap-2">
<svg
class="w-5 h-5 text-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center"
>
<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>
<span class="font-medium text-foreground">Dokument</span>
{#if document}
<span
class="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded"
>
v{document.version}
</span>
{/if}
<ChatCircle size={22} weight="fill" class="text-primary-foreground" />
</div>
<div>
<h2 class="text-lg font-semibold text-foreground truncate max-w-xs">
{conversation?.title || 'Chat'}
</h2>
<p class="text-sm text-muted-foreground">
{messages.length} Nachricht{messages.length !== 1 ? 'en' : ''}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<!-- Model Selector -->
<ModelSelector
{models}
{selectedModelId}
onSelect={handleModelSelect}
disabled={isSending}
/>
{#if isDocumentMode}
<button
onclick={toggleDocumentPanel}
class="p-2.5 transition-colors rounded-xl
{showDocumentPanel
? 'text-primary bg-primary/10 border border-primary/30'
: 'text-muted-foreground bg-muted border border-border hover:bg-muted/80'}"
aria-label="Dokument-Panel"
title="Dokument-Panel ein/ausblenden"
>
<FileText size={18} weight="bold" />
</button>
{/if}
<button
onclick={loadVersions}
class="p-1.5 text-muted-foreground hover:text-foreground
hover:bg-muted rounded transition-colors"
title="Versionen anzeigen"
onclick={handleArchive}
class="p-2.5 text-muted-foreground bg-muted border border-border rounded-xl hover:bg-muted/80 transition-colors"
aria-label="Archivieren"
title="Archivieren"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Archive size={18} weight="bold" />
</button>
<button
onclick={saveDocument}
disabled={isSavingDocument || !documentContent.trim()}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-primary-foreground
bg-primary hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground
rounded-lg transition-colors"
onclick={handleDelete}
class="p-2.5 text-destructive bg-muted border border-border rounded-xl hover:bg-destructive/10 transition-colors"
aria-label="Löschen"
title="Löschen"
>
{#if isSavingDocument}
<div
class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"
></div>
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
/>
</svg>
{/if}
Speichern
<Trash size={18} weight="bold" />
</button>
</div>
</div>
</header>
<!-- Document Editor -->
<div class="flex-1 p-4 overflow-auto">
<textarea
bind:value={documentContent}
placeholder="Beginne hier mit dem Schreiben deines Dokuments...
<!-- Main Content Area -->
<div class="flex-1 flex overflow-hidden">
<!-- Chat Area -->
<div
class="flex-1 flex flex-col overflow-hidden {isDocumentMode && showDocumentPanel
? 'lg:w-1/2'
: 'w-full'}"
>
<!-- Messages Area -->
<main class="flex-1 overflow-hidden">
<div class="h-full flex flex-col">
<MessageList {messages} isTyping={isSending} />
</div>
</main>
<!-- Floating Chat Input -->
<div
class="flex-shrink-0 p-4 bg-gradient-to-t from-surface via-surface to-transparent"
>
<div class="max-w-3xl mx-auto">
<ChatInput onSend={handleSend} disabled={isSending} />
</div>
</div>
</div>
<!-- Document Panel -->
{#if isDocumentMode && showDocumentPanel}
<div class="hidden lg:flex lg:w-1/2 flex-col border-l border-border bg-surface">
<!-- Document Header -->
<div
class="flex items-center justify-between px-4 py-3 border-b border-border bg-surface/50 backdrop-blur-sm"
>
<div class="flex items-center gap-2">
<FileText size={18} weight="bold" class="text-primary" />
<span class="font-medium text-foreground">Dokument</span>
{#if document}
<span
class="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-lg"
>
v{document.version}
</span>
{/if}
</div>
<div class="flex items-center gap-2">
<button
onclick={loadVersions}
class="p-2 text-muted-foreground hover:text-foreground
hover:bg-muted rounded-lg transition-colors"
title="Versionen anzeigen"
>
<ClockCounterClockwise size={16} weight="bold" />
</button>
<button
onclick={saveDocument}
disabled={isSavingDocument || !documentContent.trim()}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-primary-foreground
bg-primary hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground
rounded-lg transition-colors"
>
{#if isSavingDocument}
<div
class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"
></div>
{:else}
<FloppyDisk size={16} weight="bold" />
{/if}
Speichern
</button>
</div>
</div>
<!-- Document Editor -->
<div class="flex-1 p-4 overflow-auto">
<textarea
bind:value={documentContent}
placeholder="Beginne hier mit dem Schreiben deines Dokuments...
Du kannst Markdown verwenden:
# Überschrift
## Unterüberschrift
- Aufzählung
**Fett** und *Kursiv*"
class="w-full h-full min-h-[300px] p-4 text-sm font-mono
bg-muted text-foreground
border border-border rounded-lg
focus:ring-2 focus:ring-primary focus:border-transparent
resize-none"
></textarea>
</div>
</div>
{/if}
</div>
<!-- Error Message -->
{#if error}
<div
class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-destructive text-destructive-foreground rounded-lg shadow-lg"
>
{error}
</div>
{/if}
</div>
<!-- Versions Modal -->
{#if showVersionsModal}
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div
class="bg-surface rounded-xl shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col"
>
<div
class="flex items-center justify-between p-4 border-b border-border"
>
<h3 class="text-lg font-semibold text-foreground">Dokumentversionen</h3>
<button
onclick={() => (showVersionsModal = false)}
class="p-1 text-muted-foreground hover:text-foreground"
aria-label="Schließen"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="flex-1 overflow-auto p-4">
{#if documentVersions.length === 0}
<p class="text-center text-muted-foreground py-8">
Keine Versionen vorhanden
</p>
{:else}
<div class="space-y-2">
{#each documentVersions as version (version.id)}
<button
onclick={() => restoreVersion(version)}
class="w-full p-3 text-left rounded-lg border border-border
hover:bg-muted transition-colors
{version.id === document?.id ? 'ring-2 ring-primary' : ''}"
>
<div class="flex items-center justify-between mb-1">
<span class="font-medium text-foreground">
Version {version.version}
{version.id === document?.id ? ' (aktuell)' : ''}
</span>
<span class="text-xs text-muted-foreground">
{new Date(version.created_at).toLocaleDateString('de-DE', {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
<p class="text-sm text-muted-foreground line-clamp-2">
{version.content.substring(0, 100)}...
</p>
</button>
{/each}
class="w-full h-full min-h-[300px] p-4 text-sm font-mono
bg-muted/50 text-foreground
border border-border rounded-xl
focus:ring-2 focus:ring-primary focus:border-transparent
resize-none"
></textarea>
</div>
</div>
{/if}
</div>
<!-- Error Message -->
{#if error}
<div
class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-destructive text-destructive-foreground rounded-lg shadow-lg z-50"
>
{error}
</div>
{/if}
</div>
</div>
{/if}
{/if}
<!-- Versions Modal -->
{#if showVersionsModal}
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div
class="bg-surface rounded-2xl shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col border border-border"
>
<div class="flex items-center justify-between p-4 border-b border-border">
<h3 class="text-lg font-semibold text-foreground">Dokumentversionen</h3>
<button
onclick={() => (showVersionsModal = false)}
class="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
aria-label="Schließen"
>
<X size={18} weight="bold" />
</button>
</div>
<div class="flex-1 overflow-auto p-4">
{#if documentVersions.length === 0}
<p class="text-center text-muted-foreground py-8">Keine Versionen vorhanden</p>
{:else}
<div class="space-y-2">
{#each documentVersions as version (version.id)}
<button
onclick={() => restoreVersion(version)}
class="w-full p-4 text-left rounded-xl border
hover:bg-muted/50 transition-colors
{version.id === document?.id
? 'border-primary bg-primary/5 ring-2 ring-primary'
: 'border-border'}"
>
<div class="flex items-center justify-between mb-1">
<span class="font-medium text-foreground">
Version {version.version}
{version.id === document?.id ? ' (aktuell)' : ''}
</span>
<span class="text-xs text-muted-foreground">
{new Date(version.created_at).toLocaleDateString('de-DE', {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
<p class="text-sm text-muted-foreground line-clamp-2">
{version.content.substring(0, 100)}...
</p>
</button>
{/each}
</div>
{/if}
</div>
</div>
</div>
{/if}
{/if}
{/snippet}
</ChatLayout>

View file

@ -90,19 +90,54 @@
</div>
</div>
<!-- Settings Card -->
<!-- Quick Actions Card -->
<div
class="bg-surface rounded-xl border border-border shadow-sm overflow-hidden mb-6"
>
<div class="p-6">
<h3 class="text-lg font-semibold text-foreground mb-4">Einstellungen</h3>
<h3 class="text-lg font-semibold text-foreground mb-4">Schnellzugriff</h3>
<div class="space-y-4">
<!-- Settings Link -->
<a
href="/settings"
class="flex items-center justify-between py-3 border-b border-border hover:bg-muted/50 -mx-6 px-6 transition-colors"
>
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<div>
<p class="font-medium text-foreground">Einstellungen</p>
<p class="text-sm text-muted-foreground">Erscheinungsbild, Benachrichtigungen & mehr</p>
</div>
</div>
<svg class="w-5 h-5 text-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>
<!-- Theme Toggle -->
<div class="flex items-center justify-between py-3">
<div>
<p class="font-medium text-foreground">Dunkler Modus</p>
<p class="text-sm text-muted-foreground">Aktiviere den dunklen Modus für die App</p>
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-primary" 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>
<div>
<p class="font-medium text-foreground">Dunkler Modus</p>
<p class="text-sm text-muted-foreground">Schnell umschalten</p>
</div>
</div>
<button
onclick={toggleTheme}

View file

@ -0,0 +1,239 @@
<script lang="ts">
import { theme } from '$lib/stores/theme';
import { THEME_DEFINITIONS, type ThemeVariant } from '@manacore/shared-theme';
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsToggle,
SettingsDangerZone,
SettingsDangerButton,
} from '@manacore/shared-ui';
// Available theme variants
const themeVariants = theme.variants;
function setThemeVariant(variant: ThemeVariant) {
theme.setVariant(variant);
}
function toggleDarkMode(value: boolean) {
theme.toggleMode();
}
function handleDeleteChatHistory() {
// TODO: Implement chat history deletion
alert('Diese Funktion wird bald verfügbar sein.');
}
</script>
<svelte:head>
<title>Einstellungen | ManaChat</title>
</svelte:head>
<SettingsPage title="Einstellungen" subtitle="Passe die App an deine Vorlieben an.">
<!-- 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="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="Dunkler Modus"
description="Aktiviere den dunklen Modus für die App"
isOn={theme.mode === '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>
<div class="px-5 py-4">
<p class="font-medium text-[hsl(var(--foreground))] mb-2">Farbschema</p>
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">
Wähle ein Farbschema für die App
</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
{#each themeVariants as variant}
{@const def = THEME_DEFINITIONS[variant]}
<button
onclick={() => setThemeVariant(variant)}
class="flex items-center gap-3 p-3 rounded-lg border-2 transition-all
{theme.variant === variant
? 'border-[hsl(var(--primary))] bg-[hsl(var(--primary)/0.05)]'
: 'border-[hsl(var(--border))] hover:border-[hsl(var(--primary)/0.5)]'}"
>
<span class="text-xl">{def.icon}</span>
<span
class="text-sm font-medium {theme.variant === variant
? 'text-[hsl(var(--primary))]'
: 'text-[hsl(var(--foreground))]'}"
>
{def.label}
</span>
</button>
{/each}
</div>
</div>
</SettingsCard>
</SettingsSection>
<!-- Notifications Section -->
<SettingsSection title="Benachrichtigungen">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
{/snippet}
<SettingsCard>
<SettingsToggle
label="Push-Benachrichtigungen"
description="Erhalte Benachrichtigungen über neue Nachrichten"
isOn={false}
onToggle={() => {}}
disabled
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="opacity-50">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
{/snippet}
</SettingsToggle>
<SettingsToggle
label="E-Mail-Benachrichtigungen"
description="Erhalte wöchentliche Zusammenfassungen per E-Mail"
isOn={false}
onToggle={() => {}}
disabled
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="opacity-50">
<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}
</SettingsToggle>
<p class="text-xs text-[hsl(var(--muted-foreground))] px-5 pb-4 italic">
Benachrichtigungen werden in einer zukünftigen Version verfügbar sein.
</p>
</SettingsCard>
</SettingsSection>
<!-- Privacy & Security Section -->
<SettingsSection title="Datenschutz & Sicherheit">
{#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}
<SettingsCard>
<SettingsRow
label="Passwort ändern"
description="Aktualisiere dein Passwort regelmäßig"
href="/profile"
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
/>
</svg>
{/snippet}
</SettingsRow>
</SettingsCard>
<SettingsDangerZone title="Gefahrenbereich">
<SettingsDangerButton
label="Chat-Verlauf löschen"
description="Lösche alle deine Konversationen permanent"
buttonText="Löschen"
onclick={handleDeleteChatHistory}
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>
</SettingsSection>
<!-- About Section -->
<SettingsSection title="Über die App">
{#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={true}>
<span class="font-medium text-[hsl(var(--foreground))]">1.0.0</span>
</SettingsRow>
<SettingsRow label="Build" border={false}>
<span class="font-mono text-sm text-[hsl(var(--foreground))]">2024.11.29</span>
</SettingsRow>
</SettingsCard>
<div class="flex flex-wrap gap-4 text-sm mt-2">
<a href="#" class="text-[hsl(var(--primary))] hover:underline">Datenschutz</a>
<a href="#" class="text-[hsl(var(--primary))] hover:underline">Nutzungsbedingungen</a>
<a href="#" class="text-[hsl(var(--primary))] hover:underline">Hilfe & Support</a>
</div>
</SettingsSection>
</SettingsPage>