feat: major update with network graphs, themes, todo extensions, and more

## New Features

### Network Graph Visualization (Contacts, Calendar, Todo)
- D3.js force simulation for physics-based layout
- Zoom & pan with mouse/touchpad
- Keyboard shortcuts: +/- zoom, 0 reset, Esc deselect, / search, F focus
- Filtering by tags, company/location/project, connection strength
- Shared components in @manacore/shared-ui

### Central Tags API (mana-core-auth)
- CRUD endpoints for tags
- Schema: tags table with userId, name, color, app
- Shared tag components in @manacore/shared-ui

### Custom Themes System
- Theme editor with live preview and color picker
- Community theme gallery
- Theme sharing (public, unlisted, private)
- Backend API in mana-core-auth

### Todo App Extensions
- Glass-pill design for task input and items
- Settings page with 20+ preferences
- Task edit modal with inline editing
- Statistics page with visualizations
- PWA support with offline capabilities
- Multiple kanban boards

### Contacts App Features
- Duplicate detection
- Photo upload
- Batch operations
- Enhanced favorites page with multiple view modes
- Alphabet view improvements
- Search modal

### Help System
- @manacore/shared-help-content
- @manacore/shared-help-ui
- @manacore/shared-help-types

### Other Features
- Themes page for all apps
- Referral system frontend
- CommandBar (global search)
- Skeleton loaders
- Settings page improvements

## Bug Fixes
- Network graph simulation initialization
- Database schema TEXT for user_id columns (Better Auth compatibility)
- Various styling fixes

## Documentation
- Daily report for 2025-12-10
- CI/CD deployment guide

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-10 02:37:46 +01:00
parent e84371aa94
commit ee42b6cc76
381 changed files with 39284 additions and 6275 deletions

View file

@ -0,0 +1,312 @@
<script lang="ts">
import type {
CommunityTheme,
CommunityThemeQuery,
PaginatedCommunityThemes,
CustomThemesStore,
} from '@manacore/shared-theme';
import { ArrowLeft, Heart, DownloadSimple } from '@manacore/shared-icons';
import CommunityThemeGallery from '../components/community/CommunityThemeGallery.svelte';
type TabType = 'browse' | 'favorites' | 'downloaded';
interface Props {
/** Custom themes store */
store: CustomThemesStore;
/** Current effective mode */
effectiveMode?: 'light' | 'dark';
/** Callback to navigate back */
onBack?: () => void;
/** Callback when a theme is selected for details */
onSelectTheme?: (theme: CommunityTheme) => void;
/** Page title */
title?: string;
/** Initial tab */
initialTab?: TabType;
}
let {
store,
effectiveMode = 'light',
onBack,
onSelectTheme,
title = 'Community Themes',
initialTab = 'browse',
}: Props = $props();
// Active tab
let activeTab = $state<TabType>(initialTab);
// Current query for browsing
let currentQuery = $state<CommunityThemeQuery>({
page: 1,
sort: 'popular',
});
// Load data based on active tab
$effect(() => {
if (activeTab === 'browse') {
store.browseCommunity(currentQuery);
} else if (activeTab === 'favorites') {
store.loadFavorites();
} else if (activeTab === 'downloaded') {
store.loadDownloaded();
}
});
function handleQueryChange(query: CommunityThemeQuery) {
currentQuery = query;
store.browseCommunity(query);
}
async function handleDownload(theme: CommunityTheme) {
await store.downloadTheme(theme.id);
}
async function handleToggleFavorite(theme: CommunityTheme) {
await store.toggleFavorite(theme.id);
}
async function handleRate(theme: CommunityTheme, rating: number) {
await store.rateTheme(theme.id, rating);
}
function handleApplyTheme(theme: CommunityTheme) {
store.applyCustomTheme(theme);
}
// Tab definitions
const tabs: { id: TabType; label: string; icon: typeof Heart }[] = [
{ id: 'browse', label: 'Entdecken', icon: DownloadSimple },
{ id: 'favorites', label: 'Favoriten', icon: Heart },
{ id: 'downloaded', label: 'Installiert', icon: DownloadSimple },
];
// Get themes for current tab
let displayThemes = $derived(() => {
switch (activeTab) {
case 'browse':
return store.communityThemes;
case 'favorites':
return store.favorites;
case 'downloaded':
return store.downloaded;
default:
return [];
}
});
</script>
<div class="min-h-screen bg-background">
<!-- Header -->
<header class="flex items-start gap-4 p-6 border-b border-border bg-surface">
{#if onBack}
<button
type="button"
class="flex items-center justify-center w-10 h-10 bg-muted rounded-lg text-foreground hover:bg-muted/80 transition-colors flex-shrink-0"
onclick={onBack}
aria-label="Zurück"
>
<ArrowLeft size={20} weight="bold" />
</button>
{/if}
<div class="flex-1">
<h1 class="text-2xl font-bold text-foreground">{title}</h1>
<p class="text-sm text-muted-foreground mt-1">Entdecke von der Community erstellte Themes</p>
</div>
</header>
<!-- Tabs -->
<div class="bg-surface border-b border-border px-6">
<nav class="flex gap-2 overflow-x-auto" role="tablist">
{#each tabs as tab}
<button
type="button"
class="flex items-center gap-2 px-5 py-4 text-sm font-medium border-b-2 whitespace-nowrap transition-colors
{activeTab === tab.id
? 'text-primary border-primary'
: 'text-muted-foreground border-transparent hover:text-foreground'}"
onclick={() => (activeTab = tab.id)}
role="tab"
aria-selected={activeTab === tab.id}
>
<svelte:component
this={tab.icon}
size={16}
weight={activeTab === tab.id ? 'fill' : 'regular'}
/>
{tab.label}
{#if tab.id === 'favorites' && store.favorites.length > 0}
<span
class="px-2 py-0.5 text-xs font-semibold rounded-full
{activeTab === tab.id ? 'bg-primary/10 text-primary' : 'bg-muted'}"
>
{store.favorites.length}
</span>
{:else if tab.id === 'downloaded' && store.downloaded.length > 0}
<span
class="px-2 py-0.5 text-xs font-semibold rounded-full
{activeTab === tab.id ? 'bg-primary/10 text-primary' : 'bg-muted'}"
>
{store.downloaded.length}
</span>
{/if}
</button>
{/each}
</nav>
</div>
<!-- Content -->
<div class="p-6 max-w-6xl mx-auto">
{#if activeTab === 'browse'}
<CommunityThemeGallery
themes={store.communityThemes}
pagination={store.pagination}
{currentQuery}
loading={store.loading}
{effectiveMode}
onQueryChange={handleQueryChange}
{onSelectTheme}
onDownloadTheme={handleDownload}
onToggleFavorite={handleToggleFavorite}
onRateTheme={handleRate}
/>
{:else if activeTab === 'favorites'}
{#if store.favorites.length === 0 && !store.loading}
<div class="text-center py-16 text-muted-foreground">
<Heart size={48} weight="light" class="mx-auto mb-4" />
<h3 class="text-xl font-semibold text-foreground mb-2">Keine Favoriten</h3>
<p class="text-sm mb-6">Themes, die du favorisierst, werden hier angezeigt.</p>
<button
type="button"
class="px-5 py-2.5 text-sm font-medium bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
onclick={() => (activeTab = 'browse')}
>
Themes entdecken
</button>
</div>
{:else}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each store.favorites as theme (theme.id)}
{@const colors = effectiveMode === 'dark' ? theme.darkColors : theme.lightColors}
<div
class="cursor-pointer outline-none group"
onclick={() => onSelectTheme?.(theme)}
onkeypress={(e) => e.key === 'Enter' && onSelectTheme?.(theme)}
role="button"
tabindex="0"
>
<div
class="bg-surface border border-border rounded-xl p-4 flex items-center gap-4 transition-all hover:border-border-strong hover:shadow-sm group-focus-visible:ring-2 group-focus-visible:ring-primary group-focus-visible:ring-offset-2"
>
<div class="flex gap-1">
<div
class="w-5 h-5 rounded-full border border-border"
style="background-color: hsl({colors.primary})"
></div>
<div
class="w-5 h-5 rounded-full border border-border"
style="background-color: hsl({colors.background})"
></div>
<div
class="w-5 h-5 rounded-full border border-border"
style="background-color: hsl({colors.surface})"
></div>
</div>
<div class="flex-1 flex items-center gap-2 min-w-0">
<span class="text-xl">{theme.emoji}</span>
<span class="font-semibold text-foreground truncate">{theme.name}</span>
</div>
<div class="flex gap-2">
<button
type="button"
class="px-3 py-2 text-xs font-medium bg-muted border border-border rounded-md text-foreground hover:bg-muted/80 transition-colors"
onclick={(e) => {
e.stopPropagation();
handleApplyTheme(theme);
}}
>
Anwenden
</button>
<button
type="button"
class="p-2 text-red-500 bg-muted border border-border rounded-md hover:bg-red-500/10 transition-colors"
onclick={(e) => {
e.stopPropagation();
handleToggleFavorite(theme);
}}
>
<Heart size={18} weight="fill" />
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
{:else if activeTab === 'downloaded'}
{#if store.downloaded.length === 0 && !store.loading}
<div class="text-center py-16 text-muted-foreground">
<DownloadSimple size={48} weight="light" class="mx-auto mb-4" />
<h3 class="text-xl font-semibold text-foreground mb-2">Keine installierten Themes</h3>
<p class="text-sm mb-6">Themes, die du installierst, werden hier angezeigt.</p>
<button
type="button"
class="px-5 py-2.5 text-sm font-medium bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
onclick={() => (activeTab = 'browse')}
>
Themes entdecken
</button>
</div>
{:else}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each store.downloaded as theme (theme.id)}
{@const colors = effectiveMode === 'dark' ? theme.darkColors : theme.lightColors}
<div
class="cursor-pointer outline-none group"
onclick={() => onSelectTheme?.(theme)}
onkeypress={(e) => e.key === 'Enter' && onSelectTheme?.(theme)}
role="button"
tabindex="0"
>
<div
class="bg-surface border border-border rounded-xl p-4 flex items-center gap-4 transition-all hover:border-border-strong hover:shadow-sm group-focus-visible:ring-2 group-focus-visible:ring-primary group-focus-visible:ring-offset-2"
>
<div class="flex gap-1">
<div
class="w-5 h-5 rounded-full border border-border"
style="background-color: hsl({colors.primary})"
></div>
<div
class="w-5 h-5 rounded-full border border-border"
style="background-color: hsl({colors.background})"
></div>
<div
class="w-5 h-5 rounded-full border border-border"
style="background-color: hsl({colors.surface})"
></div>
</div>
<div class="flex-1 flex items-center gap-2 min-w-0">
<span class="text-xl">{theme.emoji}</span>
<span class="font-semibold text-foreground truncate">{theme.name}</span>
</div>
<div class="flex gap-2">
<button
type="button"
class="px-3 py-2 text-xs font-medium bg-primary text-primary-foreground border border-primary rounded-md hover:bg-primary/90 transition-colors"
onclick={(e) => {
e.stopPropagation();
handleApplyTheme(theme);
}}
>
Anwenden
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
{/if}
</div>
</div>

View file

@ -0,0 +1,276 @@
<script lang="ts">
import type {
ThemeColors,
EffectiveMode,
CreateCustomThemeInput,
CustomTheme,
} from '@manacore/shared-theme';
import { ArrowLeft } from '@manacore/shared-icons';
import ThemeEditor from '../components/editor/ThemeEditor.svelte';
import ThemeLivePreview from '../components/editor/ThemeLivePreview.svelte';
interface Props {
/** Theme to edit (for update mode) */
editTheme?: CustomTheme;
/** Current effective mode */
effectiveMode?: EffectiveMode;
/** Callback when theme is saved */
onSave?: (theme: CreateCustomThemeInput) => Promise<void>;
/** Callback to navigate back */
onBack?: () => void;
/** Is saving in progress */
isSaving?: boolean;
/** Page title */
title?: string;
}
let {
editTheme,
effectiveMode = 'light',
onSave,
onBack,
isSaving = false,
title,
}: Props = $props();
// Track current theme state for preview
let currentTheme = $state<Partial<CreateCustomThemeInput>>(
editTheme
? {
name: editTheme.name,
description: editTheme.description,
emoji: editTheme.emoji,
icon: editTheme.icon,
lightColors: editTheme.lightColors,
darkColors: editTheme.darkColors,
baseVariant: editTheme.baseVariant,
}
: {}
);
// Preview mode state - syncs with editor mode
let previewMode = $state<EffectiveMode>(effectiveMode);
// Get colors for preview - always show current theme colors
let displayColors = $derived<ThemeColors | null>(
currentTheme.lightColors && currentTheme.darkColors
? previewMode === 'dark'
? (currentTheme.darkColors as ThemeColors)
: (currentTheme.lightColors as ThemeColors)
: null
);
function handleThemeChange(theme: Partial<CreateCustomThemeInput>) {
currentTheme = theme;
}
function handlePreview(colors: ThemeColors, mode: EffectiveMode) {
// Update preview mode when user clicks preview in editor
previewMode = mode;
}
function handleStopPreview() {
// No-op - preview always shows current colors
}
async function handleSave(theme: CreateCustomThemeInput) {
await onSave?.(theme);
}
let pageTitle = $derived(
title ?? (editTheme ? `"${editTheme.name}" bearbeiten` : 'Neues Theme erstellen')
);
</script>
<div class="editor-page">
<!-- Header -->
<header class="page-header">
{#if onBack}
<button type="button" class="back-btn" onclick={onBack} aria-label="Zurück">
<ArrowLeft size={20} weight="bold" />
</button>
{/if}
<div class="header-content">
<h1 class="page-title">{pageTitle}</h1>
<p class="page-subtitle">Gestalte dein eigenes Theme mit individuellen Farben</p>
</div>
</header>
<!-- Split Layout: Editor + Preview -->
<div class="editor-layout">
<!-- Editor Panel -->
<div class="editor-panel">
<ThemeEditor
initialTheme={editTheme
? {
name: editTheme.name,
description: editTheme.description,
emoji: editTheme.emoji,
icon: editTheme.icon,
lightColors: editTheme.lightColors,
darkColors: editTheme.darkColors,
baseVariant: editTheme.baseVariant,
}
: undefined}
{effectiveMode}
onThemeChange={handleThemeChange}
onSave={handleSave}
onPreview={handlePreview}
onStopPreview={handleStopPreview}
{isSaving}
/>
</div>
<!-- Preview Panel -->
<div class="preview-panel">
<div class="preview-sticky">
{#if displayColors}
<ThemeLivePreview
colors={displayColors}
mode={previewMode}
onModeChange={(m) => (previewMode = m)}
/>
{:else}
<div class="preview-placeholder">
<p>Wähle eine Basis-Variante oder passe Farben an, um eine Vorschau zu sehen</p>
</div>
{/if}
</div>
</div>
</div>
</div>
<style>
.editor-page {
min-height: 100vh;
background: hsl(var(--background));
}
/* Header */
.page-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--surface));
}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
background: hsl(var(--muted));
border: none;
border-radius: 0.375rem;
color: hsl(var(--foreground));
cursor: pointer;
transition: all 0.15s;
flex-shrink: 0;
}
.back-btn:hover {
background: hsl(var(--muted) / 0.8);
}
.header-content {
flex: 1;
}
.page-title {
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0;
}
.page-subtitle {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin: 0.125rem 0 0;
}
/* Layout - Full width on desktop */
.editor-layout {
display: grid;
grid-template-columns: 1fr 400px;
gap: 1.5rem;
padding: 1.5rem;
}
@media (min-width: 1400px) {
.editor-layout {
grid-template-columns: 1fr 440px;
padding: 1.5rem 2.5rem;
}
}
@media (min-width: 1600px) {
.editor-layout {
grid-template-columns: 1fr 480px;
padding: 1.5rem 3rem;
}
}
@media (min-width: 1920px) {
.editor-layout {
grid-template-columns: 1fr 520px;
padding: 2rem 4rem;
}
}
.editor-panel {
min-width: 0;
}
.preview-panel {
min-width: 0;
}
.preview-sticky {
position: sticky;
top: 1.5rem;
}
.preview-placeholder {
background: hsl(var(--surface));
border: 1px dashed hsl(var(--border));
border-radius: 0.5rem;
padding: 2rem 1rem;
text-align: center;
}
.preview-placeholder p {
color: hsl(var(--muted-foreground));
font-size: 0.8125rem;
margin: 0;
}
/* Responsive */
@media (max-width: 1024px) {
.editor-layout {
grid-template-columns: 1fr;
}
.preview-sticky {
position: static;
}
}
@media (max-width: 640px) {
.page-header {
padding: 1rem;
}
.page-title {
font-size: 1.25rem;
}
.editor-layout {
padding: 1rem;
gap: 1rem;
}
}
</style>

View file

@ -1,11 +1,29 @@
<script lang="ts">
import type { ThemeVariant, ThemeMode, A11yStore } from '@manacore/shared-theme';
import { ArrowLeft, Sun, Moon, Desktop } from '@manacore/shared-icons';
import type {
ThemeVariant,
ThemeMode,
A11yStore,
CustomThemesStore,
CustomTheme,
UserSettingsStore,
} from '@manacore/shared-theme';
import {
ArrowLeft,
Sun,
Moon,
Desktop,
Plus,
PaintBrush,
Users,
Palette,
} from '@manacore/shared-icons';
import type { ThemeCardData, ThemePageTranslations, A11yTranslations } from '../types';
import { defaultTranslations, defaultA11yTranslations } from '../types';
import ThemeGrid from '../components/ThemeGrid.svelte';
import A11ySettings from '../components/A11ySettings.svelte';
type TabType = 'themes' | 'custom' | 'community';
interface Props {
// Theme Store Integration
currentVariant: ThemeVariant;
@ -36,6 +54,18 @@
a11yStore?: A11yStore;
showA11ySettings?: boolean;
a11yTranslations?: Partial<A11yTranslations>;
// Custom Themes (new)
customThemesStore?: CustomThemesStore;
showCustomThemes?: boolean;
onCreateTheme?: () => void;
onEditTheme?: (theme: CustomTheme) => void;
onCommunityThemes?: () => void;
// Theme Pinning (user settings)
userSettingsStore?: UserSettingsStore;
pinnedThemes?: ThemeVariant[];
onTogglePin?: (variant: ThemeVariant) => void;
}
let {
@ -55,8 +85,26 @@
a11yStore,
showA11ySettings = false,
a11yTranslations = {},
customThemesStore,
showCustomThemes = false,
onCreateTheme,
onEditTheme,
onCommunityThemes,
userSettingsStore,
pinnedThemes = [],
onTogglePin,
}: Props = $props();
// Active tab state
let activeTab = $state<TabType>('themes');
// Load custom themes when tab becomes active
$effect(() => {
if (activeTab === 'custom' && customThemesStore) {
customThemesStore.loadCustomThemes();
}
});
// Merge translations with defaults
const t = $derived({ ...defaultTranslations, ...translations });
const a11yT = $derived({ ...defaultA11yTranslations, ...a11yTranslations });
@ -66,12 +114,29 @@
{ mode: 'dark', icon: Moon, label: t.darkMode },
{ mode: 'system', icon: Desktop, label: t.systemMode },
]);
// Tab definitions
const tabs: { id: TabType; label: string; icon: typeof Palette }[] = [
{ id: 'themes', label: 'Themes', icon: Palette },
{ id: 'custom', label: 'Meine Themes', icon: PaintBrush },
{ id: 'community', label: 'Community', icon: Users },
];
function handleApplyCustomTheme(theme: CustomTheme) {
customThemesStore?.applyCustomTheme(theme);
}
async function handleDeleteTheme(theme: CustomTheme) {
if (confirm(`Theme "${theme.name}" wirklich löschen?`)) {
await customThemesStore?.deleteTheme(theme.id);
}
}
</script>
<div class="min-h-screen bg-background">
<div class="max-w-4xl mx-auto px-4 py-8">
<!-- Header -->
<header class="mb-8">
<header class="mb-6">
<div class="flex items-center gap-3 mb-2">
{#if showBackButton && onBack}
<button
@ -93,9 +158,34 @@
</p>
</header>
<!-- Tabs (if custom themes enabled) -->
{#if showCustomThemes && customThemesStore}
<nav class="flex gap-1 mb-6 p-1 bg-muted rounded-lg" role="tablist">
{#each tabs as tab}
<button
type="button"
class="flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors flex-1 justify-center
{activeTab === tab.id
? 'bg-surface text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'}"
onclick={() => (activeTab = tab.id)}
role="tab"
aria-selected={activeTab === tab.id}
>
<svelte:component
this={tab.icon}
size={16}
weight={activeTab === tab.id ? 'fill' : 'regular'}
/>
{tab.label}
</button>
{/each}
</nav>
{/if}
<!-- Mode Selector -->
{#if showModeSelector && onModeChange}
<section class="mb-8">
<section class="mb-6">
<h2 class="text-sm font-medium text-muted-foreground mb-3">
{t.modeLabel}
</h2>
@ -117,20 +207,154 @@
</section>
{/if}
<!-- Theme Grid -->
<section>
<h2 class="text-sm font-medium text-muted-foreground mb-4">
{t.currentTheme}
</h2>
<ThemeGrid
{currentVariant}
onSelect={onSelectTheme}
{themes}
onUnlock={onUnlockTheme}
{showLockedThemes}
{translations}
/>
</section>
<!-- Tab Content -->
{#if !showCustomThemes || activeTab === 'themes'}
<!-- Theme Grid -->
<section>
<h2 class="text-sm font-medium text-muted-foreground mb-4">
{t.currentTheme}
</h2>
<ThemeGrid
{currentVariant}
onSelect={onSelectTheme}
{themes}
onUnlock={onUnlockTheme}
{showLockedThemes}
{translations}
{pinnedThemes}
{onTogglePin}
/>
</section>
{:else if activeTab === 'custom' && customThemesStore}
<!-- Custom Themes -->
<section>
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-medium text-muted-foreground">Meine Themes</h2>
{#if onCreateTheme}
<button
type="button"
onclick={onCreateTheme}
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:bg-primary/90 transition-colors"
>
<Plus size={16} />
Neues Theme
</button>
{/if}
</div>
{#if customThemesStore.loading}
<div class="text-center py-8 text-muted-foreground">Lade...</div>
{:else if customThemesStore.customThemes.length === 0}
<div class="text-center py-12 border border-dashed border-border rounded-xl">
<PaintBrush size={48} class="mx-auto mb-4 text-muted-foreground" weight="light" />
<h3 class="text-lg font-semibold text-foreground mb-2">Noch keine eigenen Themes</h3>
<p class="text-muted-foreground mb-4">
Erstelle dein erstes eigenes Theme mit individuellen Farben.
</p>
{#if onCreateTheme}
<button
type="button"
onclick={onCreateTheme}
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:bg-primary/90 transition-colors"
>
<Plus size={16} />
Theme erstellen
</button>
{/if}
</div>
{:else}
<div class="grid gap-4 sm:grid-cols-2">
{#each customThemesStore.customThemes as theme (theme.id)}
<div class="bg-surface border border-border rounded-xl overflow-hidden">
<!-- Color preview bar -->
<div class="h-8 flex">
<div
class="flex-1"
style="background-color: hsl({theme.lightColors.primary})"
></div>
<div
class="flex-1"
style="background-color: hsl({theme.lightColors.background})"
></div>
<div
class="flex-1"
style="background-color: hsl({theme.lightColors.surface})"
></div>
<div
class="flex-1"
style="background-color: hsl({theme.lightColors.foreground})"
></div>
</div>
<div class="p-4">
<div class="flex items-center gap-3 mb-2">
<span class="text-xl">{theme.emoji}</span>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-foreground truncate">{theme.name}</h3>
{#if theme.description}
<p class="text-sm text-muted-foreground truncate">{theme.description}</p>
{/if}
</div>
{#if theme.isPublished}
<span
class="px-2 py-0.5 text-xs font-medium bg-success/10 text-success rounded"
>
Veröffentlicht
</span>
{/if}
</div>
<div class="flex gap-2 mt-3">
<button
type="button"
onclick={() => handleApplyCustomTheme(theme)}
class="flex-1 px-3 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:bg-primary/90 transition-colors"
>
Anwenden
</button>
{#if onEditTheme}
<button
type="button"
onclick={() => onEditTheme(theme)}
class="px-3 py-2 bg-muted text-foreground rounded-lg text-sm font-medium hover:bg-muted/80 transition-colors"
>
Bearbeiten
</button>
{/if}
<button
type="button"
onclick={() => handleDeleteTheme(theme)}
class="px-3 py-2 bg-muted text-error rounded-lg text-sm font-medium hover:bg-error/10 transition-colors"
>
Löschen
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</section>
{:else if activeTab === 'community'}
<!-- Community Themes -->
<section>
<div class="text-center py-12 border border-dashed border-border rounded-xl">
<Users size={48} class="mx-auto mb-4 text-muted-foreground" weight="light" />
<h3 class="text-lg font-semibold text-foreground mb-2">Community Themes</h3>
<p class="text-muted-foreground mb-4">
Entdecke Themes, die von anderen Nutzern erstellt wurden.
</p>
{#if onCommunityThemes}
<button
type="button"
onclick={onCommunityThemes}
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:bg-primary/90 transition-colors"
>
<Users size={16} />
Community durchsuchen
</button>
{/if}
</div>
</section>
{/if}
<!-- A11y Settings -->
{#if showA11ySettings && a11yStore}