feat(theme): add ThemePage components and distinct background colors

- Add unique background colors for each theme variant:
  - Lume: warm cream/gold tint
  - Nature: green tint in dark mode
  - Stone: blue-gray tint in dark mode
  - Ocean: blue tint in dark mode

- Create shared-theme-ui components:
  - ThemeColorPreview: color circles preview component
  - ThemeCard: individual theme card with status support
  - ThemeGrid: responsive grid layout
  - ThemePage: full page component with mode selector

- Integrate theme page in Chat app:
  - Add /themes route with ThemePage component
  - Add "🎨 Alle Themes" link to PillNavigation dropdown
  - Add palette icon to shared-ui icon set

- Migrate Presi and Picture apps to shared-theme system
- Update semantic color usage across all apps

🤖 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 09:03:20 +01:00
parent 129692812b
commit 54383bf7c2
92 changed files with 1793 additions and 1936 deletions

View file

@ -40,7 +40,7 @@
}
</script>
<div class="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4">
<div class="border-t border-border bg-surface p-4">
<div class="flex items-end gap-3 max-w-4xl mx-auto">
<div class="flex-1 relative">
<textarea
@ -51,27 +51,27 @@
{placeholder}
{disabled}
rows="1"
class="w-full resize-none rounded-xl border border-gray-300 dark:border-gray-600
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100
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-blue-500 focus:border-transparent
focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed
placeholder:text-gray-500 dark:placeholder:text-gray-400"
placeholder:text-muted-foreground"
></textarea>
</div>
<button
onclick={handleSubmit}
disabled={disabled || !inputValue.trim()}
aria-label="Nachricht senden"
class="flex-shrink-0 p-3 rounded-xl bg-blue-600 text-white
hover:bg-blue-700 active:bg-blue-800
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600
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"
>
<PaperPlaneTilt size={20} weight="bold" />
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 text-center mt-2">
<p class="text-xs text-muted-foreground text-center mt-2">
Enter zum Senden, Shift+Enter für neue Zeile
</p>
</div>

View file

@ -36,11 +36,11 @@
<div class="flex flex-col h-full">
<!-- New Chat Button -->
<div class="p-3 border-b border-gray-200 dark:border-gray-700">
<div class="p-3 border-b border-border">
<a
href="/chat"
class="flex items-center justify-center gap-2 w-full px-4 py-2.5
bg-blue-600 hover:bg-blue-700 text-white rounded-lg
bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg
font-medium transition-colors"
>
<Plus size={20} weight="bold" />
@ -53,11 +53,11 @@
{#if isLoading}
<div class="flex items-center justify-center py-8">
<div
class="animate-spin w-6 h-6 border-2 border-blue-500 border-r-transparent rounded-full"
class="animate-spin w-6 h-6 border-2 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if conversations.length === 0}
<div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<div class="px-4 py-8 text-center text-muted-foreground">
<p class="text-sm">Keine Konversationen</p>
<p class="text-xs mt-1">Starte einen neuen Chat</p>
</div>
@ -69,14 +69,14 @@
href="/chat/{conv.id}"
class="block px-3 py-2 mx-2 rounded-lg transition-colors
{isActive
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300'}"
? 'bg-primary/10 text-primary'
: 'hover:bg-muted text-foreground'}"
>
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-medium truncate">
{truncateTitle(conv.title || 'Neue Konversation')}
</span>
<span class="text-xs text-gray-500 dark:text-gray-500 flex-shrink-0">
<span class="text-xs text-muted-foreground flex-shrink-0">
{formatDate(conv.updated_at || conv.created_at)}
</span>
</div>

View file

@ -29,8 +29,8 @@
<div class="flex {isUser ? 'justify-end' : 'justify-start'} mb-4">
<div
class="max-w-[80%] rounded-2xl px-4 py-3 {isUser
? 'bg-blue-600 text-white rounded-br-md'
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-md'}"
? 'bg-primary text-primary-foreground rounded-br-md'
: 'bg-muted text-foreground rounded-bl-md'}"
>
{#if isUser}
<p class="whitespace-pre-wrap">{message.message_text}</p>
@ -39,7 +39,7 @@
{@html htmlContent}
</div>
{/if}
<div class="text-xs mt-1 {isUser ? 'text-blue-200' : 'text-gray-500 dark:text-gray-400'}">
<div class="text-xs mt-1 {isUser ? 'text-primary-foreground/70' : 'text-muted-foreground'}">
{formattedTime}
</div>
</div>

View file

@ -34,7 +34,7 @@
<div bind:this={containerEl} class="flex-1 overflow-y-auto px-4 py-6">
{#if messages.length === 0}
<div class="flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center justify-center h-full text-muted-foreground">
<ChatCircleDots size={64} weight="light" class="mb-4 opacity-50" />
<p class="text-lg font-medium">Keine Nachrichten</p>
<p class="text-sm">Starte eine Konversation!</p>

View file

@ -22,9 +22,9 @@
value={selectedModelId}
onchange={handleChange}
{disabled}
class="appearance-none bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100
text-sm rounded-lg px-3 py-2 pr-8 border border-gray-200 dark:border-gray-700
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
class="appearance-none bg-muted text-foreground
text-sm rounded-lg px-3 py-2 pr-8 border border-border
focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed
cursor-pointer min-w-[160px]"
>
@ -37,6 +37,6 @@
{/if}
</select>
<div class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<CaretDown size={16} weight="bold" class="text-gray-500" />
<CaretDown size={16} weight="bold" class="text-muted-foreground" />
</div>
</div>

View file

@ -3,18 +3,18 @@
</script>
<div class="flex justify-start mb-4">
<div class="bg-gray-100 dark:bg-gray-800 rounded-2xl rounded-bl-md px-4 py-3">
<div class="bg-muted rounded-2xl rounded-bl-md px-4 py-3">
<div class="flex items-center gap-1">
<div
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
class="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce"
style="animation-delay: 0ms"
></div>
<div
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
class="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce"
style="animation-delay: 150ms"
></div>
<div
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
class="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce"
style="animation-delay: 300ms"
></div>
</div>

View file

@ -36,7 +36,7 @@
<svelte:window onclick={() => (showMenu = false)} />
<div
class="group relative bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
class="group relative bg-surface rounded-xl border border-border
shadow-sm hover:shadow-md transition-all cursor-pointer"
onclick={() => onSelect(space.id)}
onkeydown={(e) => e.key === 'Enter' && onSelect(space.id)}
@ -47,13 +47,13 @@
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<UsersThree size={20} weight="bold" class="text-blue-500 flex-shrink-0" />
<h3 class="text-base font-semibold text-gray-900 dark:text-white truncate">
<UsersThree size={20} weight="bold" class="text-primary flex-shrink-0" />
<h3 class="text-base font-semibold text-foreground truncate">
{space.name}
</h3>
{#if isOwner}
<span
class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded"
class="px-2 py-0.5 text-xs font-medium bg-primary/10 text-primary rounded"
>
Besitzer
</span>
@ -61,12 +61,12 @@
</div>
{#if space.description}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">
<p class="text-sm text-muted-foreground line-clamp-2 mb-2">
{space.description}
</p>
{/if}
<p class="text-xs text-gray-500 dark:text-gray-500">
<p class="text-xs text-muted-foreground">
Erstellt: {formatDate(space.created_at)}
</p>
</div>
@ -75,8 +75,8 @@
<div class="relative">
<button
onclick={handleMenuClick}
class="p-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300
hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
class="p-2 text-muted-foreground hover:text-foreground
hover:bg-muted rounded-lg transition-colors"
aria-label="Optionen"
>
<DotsThreeVertical size={20} weight="bold" />
@ -84,8 +84,8 @@
{#if showMenu}
<div
class="absolute right-0 top-full mt-1 py-1 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg
border border-gray-200 dark:border-gray-700 z-10"
class="absolute right-0 top-full mt-1 py-1 w-40 bg-surface rounded-lg shadow-lg
border border-border z-10"
onclick={(e) => e.stopPropagation()}
onkeydown={() => {}}
role="menu"
@ -94,8 +94,8 @@
{#if isOwner}
<button
onclick={() => handleAction(() => onEdit(space.id))}
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-300
hover:bg-gray-100 dark:hover:bg-gray-700"
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground
hover:bg-muted"
role="menuitem"
>
<Gear size={16} weight="bold" />
@ -103,8 +103,8 @@
</button>
<button
onclick={() => handleAction(() => onDelete(space.id))}
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 dark:text-red-400
hover:bg-red-50 dark:hover:bg-red-900/20"
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-destructive
hover:bg-destructive/10"
role="menuitem"
>
<Trash size={16} weight="bold" />
@ -113,8 +113,8 @@
{:else}
<button
onclick={() => handleAction(() => onLeave(space.id))}
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 dark:text-red-400
hover:bg-red-50 dark:hover:bg-red-900/20"
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-destructive
hover:bg-destructive/10"
role="menuitem"
>
<SignOut size={16} weight="bold" />

View file

@ -36,8 +36,8 @@
}
</script>
<div class="bg-white dark:bg-gray-900 p-6 rounded-xl max-w-lg mx-auto">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">
<div class="bg-surface p-6 rounded-xl max-w-lg mx-auto">
<h2 class="text-xl font-bold text-foreground mb-6">
{isEditMode ? 'Space bearbeiten' : 'Neuen Space erstellen'}
</h2>
@ -50,7 +50,7 @@
>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label for="name" class="block text-sm font-medium text-foreground mb-1">
Name *
</label>
<input
@ -59,13 +59,13 @@
bind:value={name}
maxlength={100}
placeholder="Name des Spaces"
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
text-gray-900 dark:text-white placeholder-gray-500
{errors.name ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full px-3 py-2 border rounded-lg bg-muted
text-foreground placeholder-muted-foreground
{errors.name ? 'border-destructive' : 'border-border'}
focus:ring-2 focus:ring-primary focus:border-transparent"
/>
{#if errors.name}
<p class="mt-1 text-sm text-red-500">{errors.name}</p>
<p class="mt-1 text-sm text-destructive">{errors.name}</p>
{/if}
</div>
@ -73,7 +73,7 @@
<div>
<label
for="description"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
class="block text-sm font-medium text-foreground mb-1"
>
Beschreibung (optional)
</label>
@ -83,9 +83,9 @@
maxlength={500}
rows={3}
placeholder="Worum geht es in diesem Space?"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500
focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
class="w-full px-3 py-2 border border-border rounded-lg
bg-muted text-foreground placeholder-muted-foreground
focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
></textarea>
</div>
@ -94,15 +94,15 @@
<button
type="button"
onclick={onCancel}
class="flex-1 px-4 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300
rounded-lg font-medium hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
class="flex-1 px-4 py-2.5 border border-border text-foreground
rounded-lg font-medium hover:bg-muted transition-colors"
>
Abbrechen
</button>
<button
type="submit"
class="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-medium
hover:bg-blue-700 transition-colors"
class="flex-1 px-4 py-2.5 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
{isEditMode ? 'Speichern' : 'Erstellen'}
</button>

View file

@ -19,10 +19,10 @@
</script>
<div
class="group relative flex rounded-xl overflow-hidden bg-white dark:bg-gray-800 shadow-sm hover:shadow-md transition-all
class="group relative flex rounded-xl overflow-hidden bg-surface shadow-sm hover:shadow-md transition-all
{template.is_default
? 'ring-2 ring-blue-500'
: 'border border-gray-200 dark:border-gray-700'}"
? 'ring-2 ring-primary'
: 'border border-border'}"
>
<!-- Color Indicator -->
<div class="w-2 flex-shrink-0" style="background-color: {template.color}"></div>
@ -32,23 +32,23 @@
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<h3 class="text-base font-semibold text-gray-900 dark:text-white truncate">
<h3 class="text-base font-semibold text-foreground truncate">
{template.name}
</h3>
{#if template.is_default}
<span class="px-2 py-0.5 text-xs font-medium bg-blue-500 text-white rounded">
<span class="px-2 py-0.5 text-xs font-medium bg-primary text-primary-foreground rounded">
Standard
</span>
{/if}
</div>
{#if template.description}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">
<p class="text-sm text-muted-foreground line-clamp-2 mb-2">
{template.description}
</p>
{/if}
<p class="text-xs text-gray-500 dark:text-gray-500 italic line-clamp-2">
<p class="text-xs text-muted-foreground italic line-clamp-2">
{truncatePrompt(template.system_prompt)}
</p>
</div>
@ -58,7 +58,7 @@
{#if !template.is_default}
<button
onclick={() => onSetDefault(template.id)}
class="p-1.5 text-gray-500 hover:text-yellow-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
class="p-1.5 text-muted-foreground hover:text-yellow-500 hover:bg-muted rounded-lg transition-colors"
title="Als Standard setzen"
aria-label="Als Standard setzen"
>
@ -67,7 +67,7 @@
{/if}
<button
onclick={() => onEdit(template.id)}
class="p-1.5 text-gray-500 hover:text-blue-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
class="p-1.5 text-muted-foreground hover:text-primary hover:bg-muted rounded-lg transition-colors"
title="Bearbeiten"
aria-label="Bearbeiten"
>
@ -75,7 +75,7 @@
</button>
<button
onclick={() => onDelete(template.id)}
class="p-1.5 text-gray-500 hover:text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
class="p-1.5 text-muted-foreground hover:text-destructive hover:bg-muted rounded-lg transition-colors"
title="Löschen"
aria-label="Löschen"
>

View file

@ -76,8 +76,8 @@
}
</script>
<div class="bg-white dark:bg-gray-900 p-6 rounded-xl max-w-2xl mx-auto">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">
<div class="bg-surface p-6 rounded-xl max-w-2xl mx-auto">
<h2 class="text-xl font-bold text-foreground mb-6">
{isEditMode ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}
</h2>
@ -90,7 +90,7 @@
>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label for="name" class="block text-sm font-medium text-foreground mb-1">
Name *
</label>
<input
@ -99,13 +99,13 @@
bind:value={name}
maxlength={50}
placeholder="Name der Vorlage"
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
text-gray-900 dark:text-white placeholder-gray-500
{errors.name ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full px-3 py-2 border rounded-lg bg-muted
text-foreground placeholder-muted-foreground
{errors.name ? 'border-destructive' : 'border-border'}
focus:ring-2 focus:ring-primary focus:border-transparent"
/>
{#if errors.name}
<p class="mt-1 text-sm text-red-500">{errors.name}</p>
<p class="mt-1 text-sm text-destructive">{errors.name}</p>
{/if}
</div>
@ -113,7 +113,7 @@
<div>
<label
for="description"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
class="block text-sm font-medium text-foreground mb-1"
>
Beschreibung (optional)
</label>
@ -123,9 +123,9 @@
maxlength={200}
rows={2}
placeholder="Kurze Beschreibung dieser Vorlage"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500
focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
class="w-full px-3 py-2 border border-border rounded-lg
bg-muted text-foreground placeholder-muted-foreground
focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
></textarea>
</div>
@ -133,7 +133,7 @@
<div>
<label
for="systemPrompt"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
class="block text-sm font-medium text-foreground mb-1"
>
System-Prompt *
</label>
@ -142,15 +142,15 @@
bind:value={systemPrompt}
rows={5}
placeholder="System-Prompt für die KI"
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
text-gray-900 dark:text-white placeholder-gray-500
{errors.systemPrompt ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
class="w-full px-3 py-2 border rounded-lg bg-muted
text-foreground placeholder-muted-foreground
{errors.systemPrompt ? 'border-destructive' : 'border-border'}
focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
></textarea>
{#if errors.systemPrompt}
<p class="mt-1 text-sm text-red-500">{errors.systemPrompt}</p>
<p class="mt-1 text-sm text-destructive">{errors.systemPrompt}</p>
{:else}
<p class="mt-1 text-xs text-gray-500">
<p class="mt-1 text-xs text-muted-foreground">
Der System-Prompt definiert die Rolle und das Verhalten der KI.
</p>
{/if}
@ -160,7 +160,7 @@
<div>
<label
for="initialQuestion"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
class="block text-sm font-medium text-foreground mb-1"
>
Beispielfrage (optional)
</label>
@ -169,18 +169,18 @@
bind:value={initialQuestion}
rows={2}
placeholder="Beispiel für eine passende Frage oder Anweisung"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500
focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
class="w-full px-3 py-2 border border-border rounded-lg
bg-muted text-foreground placeholder-muted-foreground
focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
></textarea>
<p class="mt-1 text-xs text-gray-500">
<p class="mt-1 text-xs text-muted-foreground">
Diese Frage wird als Vorschlag angezeigt, wenn die Vorlage ausgewählt wird.
</p>
</div>
<!-- Color -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> Farbe </label>
<label class="block text-sm font-medium text-foreground mb-2"> Farbe </label>
<div class="flex flex-wrap gap-2">
{#each TEMPLATE_COLORS as color}
<button
@ -210,22 +210,22 @@
<!-- Model -->
<div>
<label for="model" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label for="model" class="block text-sm font-medium text-foreground mb-1">
Bevorzugtes Modell (optional)
</label>
<select
id="model"
bind:value={selectedModelId}
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full px-3 py-2 border border-border rounded-lg
bg-muted text-foreground
focus:ring-2 focus:ring-primary focus:border-transparent"
>
<option value="">Kein Modell ausgewählt</option>
{#each models as model}
<option value={model.id}>{model.name}</option>
{/each}
</select>
<p class="mt-1 text-xs text-gray-500">
<p class="mt-1 text-xs text-muted-foreground">
Falls ausgewählt, wird dieses Modell automatisch mit der Vorlage verwendet.
</p>
</div>
@ -237,18 +237,18 @@
onclick={() => (documentMode = !documentMode)}
class="w-full flex items-center justify-between p-4 border rounded-lg transition-colors
{documentMode
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800'}"
? 'border-primary bg-primary/10'
: 'border-border bg-muted'}"
>
<div class="text-left">
<p class="font-medium text-gray-900 dark:text-white">Dokumentmodus aktivieren</p>
<p class="text-xs text-gray-500 mt-0.5">
<p class="font-medium text-foreground">Dokumentmodus aktivieren</p>
<p class="text-xs text-muted-foreground mt-0.5">
Ermöglicht die Bearbeitung eines Dokuments während der Konversation
</p>
</div>
<div
class="w-6 h-6 rounded-full flex items-center justify-center
{documentMode ? 'bg-blue-500' : 'bg-gray-400'}"
{documentMode ? 'bg-primary' : 'bg-muted-foreground'}"
>
{#if documentMode}
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -278,15 +278,15 @@
<button
type="button"
onclick={onCancel}
class="flex-1 px-4 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300
rounded-lg font-medium hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
class="flex-1 px-4 py-2.5 border border-border text-foreground
rounded-lg font-medium hover:bg-muted transition-colors"
>
Abbrechen
</button>
<button
type="submit"
class="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-medium
hover:bg-blue-700 transition-colors"
class="flex-1 px-4 py-2.5 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
{isEditMode ? 'Speichern' : 'Erstellen'}
</button>

View file

@ -4,12 +4,13 @@
import { page } from '$app/stores';
import { authStore } from '$lib/stores/auth.svelte';
import { theme } from '$lib/stores/theme';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import {
isSidebarMode as sidebarModeStore,
isNavCollapsed as collapsedStore,
} from '$lib/stores/navigation';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import type { LayoutData } from './$types';
let { children, data }: { children: any; data: LayoutData } = $props();
@ -21,6 +22,29 @@
// Use theme store's isDark directly
let isDark = $derived(theme.isDark);
// Theme variant dropdown items
let themeVariantItems = $derived<PillDropdownItem[]>([
// Theme variants
...theme.variants.map((variant) => ({
id: variant,
label: `${THEME_DEFINITIONS[variant].emoji} ${THEME_DEFINITIONS[variant].label}`,
onClick: () => theme.setVariant(variant),
active: theme.variant === variant,
})),
// Separator and link to full themes page
{
id: 'all-themes',
label: '🎨 Alle Themes',
onClick: () => goto('/themes'),
active: false,
},
]);
// Current theme variant label
let currentThemeVariantLabel = $derived(
`${THEME_DEFINITIONS[theme.variant].emoji} ${THEME_DEFINITIONS[theme.variant].label}`
);
// Navigation items for Chat
const navItems: PillNavItem[] = [
{ href: '/', label: 'Chat', icon: 'home' },
@ -112,12 +136,12 @@
{#if isChecking}
<!-- Loading state while checking auth -->
<div class="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
<div class="flex min-h-screen items-center justify-center bg-background">
<div class="text-center">
<div
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-500 border-r-transparent"
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
></div>
<p class="text-gray-500 dark:text-gray-400">Laden...</p>
<p class="text-muted-foreground">Laden...</p>
</div>
</div>
{:else}
@ -136,6 +160,9 @@
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
showLanguageSwitcher={false}
showLogout={true}
onLogout={handleLogout}
@ -144,7 +171,7 @@
<!-- Main Content with dynamic padding based on nav mode -->
<main
class="main-content bg-gray-50 dark:bg-gray-900"
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
>

View file

@ -53,12 +53,12 @@
<title>Archiv | ManaChat</title>
</svelte:head>
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-4xl mx-auto px-4">
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Archiv</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
<h1 class="text-2xl font-bold text-foreground">Archiv</h1>
<p class="text-sm text-muted-foreground mt-1">
Deine archivierten Konversationen.
</p>
</div>
@ -67,14 +67,14 @@
{#if isLoading}
<div class="flex items-center justify-center py-16">
<div
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
class="animate-spin w-8 h-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if conversations.length === 0}
<!-- Empty State -->
<div class="text-center py-16">
<svg
class="w-16 h-16 text-gray-400 mx-auto mb-4"
class="w-16 h-16 text-muted-foreground mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -86,24 +86,24 @@
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
/>
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">
<h3 class="text-lg font-medium text-foreground mb-1">
Keine archivierten Konversationen
</h3>
<p class="text-gray-500 dark:text-gray-400">Archivierte Gespräche erscheinen hier.</p>
<p class="text-muted-foreground">Archivierte Gespräche erscheinen hier.</p>
</div>
{:else}
<!-- Conversation List -->
<div class="space-y-3">
{#each conversations as conv (conv.id)}
<div
class="group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
class="group bg-surface rounded-xl border border-border
shadow-sm hover:shadow-md transition-all overflow-hidden"
>
<button onclick={() => handleConversationClick(conv.id)} class="w-full p-4 text-left">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<svg
class="w-5 h-5 text-gray-400"
class="w-5 h-5 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -115,14 +115,14 @@
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
/>
</svg>
<h3 class="font-semibold text-gray-900 dark:text-white">
<h3 class="font-semibold text-foreground">
{conv.title || 'Unbenannte Konversation'}
</h3>
</div>
<span class="text-xs text-gray-500">{formatDate(conv.updated_at)}</span>
<span class="text-xs text-muted-foreground">{formatDate(conv.updated_at)}</span>
</div>
<div class="flex items-center gap-2 text-xs text-gray-500">
<span class="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">
<div class="flex items-center gap-2 text-xs text-muted-foreground">
<span class="px-2 py-0.5 bg-muted rounded">
{conv.conversation_mode === 'free'
? 'Freier Modus'
: conv.conversation_mode === 'guided'
@ -134,12 +134,12 @@
<!-- Actions -->
<div
class="flex justify-end gap-2 px-4 py-2 border-t border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50"
class="flex justify-end gap-2 px-4 py-2 border-t border-border bg-muted/50"
>
<button
onclick={() => handleUnarchive(conv.id)}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400
hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground
hover:text-primary hover:bg-primary/10
rounded-lg transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -154,8 +154,8 @@
</button>
<button
onclick={() => handleDelete(conv.id)}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400
hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground
hover:text-destructive hover:bg-destructive/10
rounded-lg transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View file

@ -23,8 +23,8 @@
<!-- Sidebar Toggle (mobile) -->
<button
onclick={toggleSidebar}
class="fixed bottom-4 left-4 z-50 p-3 bg-blue-600 text-white rounded-full shadow-lg
sm:hidden hover:bg-blue-700 transition-colors"
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}
@ -36,7 +36,7 @@
<!-- Sidebar -->
<aside
class="w-72 flex-shrink-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700
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

View file

@ -138,11 +138,11 @@
<div class="flex flex-col h-full">
<!-- Chat Header -->
<header
class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3"
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-gray-900 dark:text-white">Neuer Chat</h2>
<h2 class="text-lg font-semibold text-foreground">Neuer Chat</h2>
<!-- Model Selector -->
<ModelSelector
@ -158,9 +158,9 @@
onchange={handleTemplateSelect}
value={selectedTemplateId}
disabled={isSending}
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
bg-white dark:bg-gray-800 text-gray-900 dark:text-white
focus:ring-2 focus:ring-blue-500 focus:border-transparent
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>
@ -179,8 +179,8 @@
disabled={isSending}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg transition-colors
{documentMode
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-300 dark:border-blue-700'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600'}
? '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"
>
@ -192,9 +192,9 @@
<div class="flex items-center gap-2">
<button
onclick={toggleTheme}
class="p-2 text-gray-700 dark:text-gray-300
bg-gray-100 dark:bg-gray-800 rounded-lg
hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
class="p-2 text-muted-foreground
bg-muted rounded-lg
hover:bg-muted/80 transition-colors"
aria-label="Theme wechseln"
>
<Moon size={20} weight="bold" />
@ -204,7 +204,7 @@
</header>
<!-- Messages Area -->
<main class="flex-1 overflow-hidden bg-white dark:bg-gray-900">
<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>

View file

@ -179,23 +179,23 @@
{#if isLoading}
<div class="flex items-center justify-center h-full">
<div
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
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-red-500 mb-4">{error}</p>
<a href="/chat" class="text-blue-600 hover:underline">Zurück zum Chat</a>
<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-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3"
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-gray-900 dark:text-white truncate max-w-xs">
<h2 class="text-lg font-semibold text-foreground truncate max-w-xs">
{conversation?.title || 'Chat'}
</h2>
<ModelSelector
@ -211,8 +211,8 @@
onclick={toggleDocumentPanel}
class="p-2 transition-colors rounded-lg
{showDocumentPanel
? 'text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30'
: 'text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700'}"
? 'text-primary bg-primary/10'
: 'text-foreground bg-muted hover:bg-muted/80'}"
aria-label="Dokument-Panel"
title="Dokument-Panel ein/ausblenden"
>
@ -228,9 +228,7 @@
{/if}
<button
onclick={handleArchive}
class="p-2 text-gray-700 dark:text-gray-300
bg-gray-100 dark:bg-gray-800 rounded-lg
hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
class="p-2 text-foreground bg-muted rounded-lg hover:bg-muted/80 transition-colors"
aria-label="Archivieren"
title="Archivieren"
>
@ -245,9 +243,7 @@
</button>
<button
onclick={handleDelete}
class="p-2 text-red-600 dark:text-red-400
bg-gray-100 dark:bg-gray-800 rounded-lg
hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors"
class="p-2 text-destructive bg-muted rounded-lg hover:bg-destructive/10 transition-colors"
aria-label="Löschen"
title="Löschen"
>
@ -262,9 +258,7 @@
</button>
<button
onclick={toggleTheme}
class="p-2 text-gray-700 dark:text-gray-300
bg-gray-100 dark:bg-gray-800 rounded-lg
hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
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">
@ -289,7 +283,7 @@
: 'w-full'}"
>
<!-- Messages Area -->
<main class="flex-1 overflow-hidden bg-white dark:bg-gray-900">
<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>
@ -302,15 +296,15 @@
<!-- Document Panel -->
{#if isDocumentMode && showDocumentPanel}
<div
class="hidden lg:flex lg:w-1/2 flex-col border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900"
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-gray-200 dark:border-gray-700"
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-blue-600 dark:text-blue-400"
class="w-5 h-5 text-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -322,10 +316,10 @@
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-gray-900 dark:text-white">Dokument</span>
<span class="font-medium text-foreground">Dokument</span>
{#if document}
<span
class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded"
class="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded"
>
v{document.version}
</span>
@ -334,8 +328,8 @@
<div class="flex items-center gap-2">
<button
onclick={loadVersions}
class="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white
hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors"
class="p-1.5 text-muted-foreground hover:text-foreground
hover:bg-muted rounded transition-colors"
title="Versionen anzeigen"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -350,8 +344,8 @@
<button
onclick={saveDocument}
disabled={isSavingDocument || !documentContent.trim()}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white
bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400
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}
@ -385,9 +379,9 @@ Du kannst Markdown verwenden:
- Aufzählung
**Fett** und *Kursiv*"
class="w-full h-full min-h-[300px] p-4 text-sm font-mono
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white
border border-gray-200 dark:border-gray-700 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
bg-muted text-foreground
border border-border rounded-lg
focus:ring-2 focus:ring-primary focus:border-transparent
resize-none"
></textarea>
</div>
@ -398,7 +392,7 @@ Du kannst Markdown verwenden:
<!-- 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"
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>
@ -409,15 +403,15 @@ Du kannst Markdown verwenden:
{#if showVersionsModal}
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col"
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-gray-200 dark:border-gray-700"
class="flex items-center justify-between p-4 border-b border-border"
>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Dokumentversionen</h3>
<h3 class="text-lg font-semibold text-foreground">Dokumentversionen</h3>
<button
onclick={() => (showVersionsModal = false)}
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
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">
@ -432,7 +426,7 @@ Du kannst Markdown verwenden:
</div>
<div class="flex-1 overflow-auto p-4">
{#if documentVersions.length === 0}
<p class="text-center text-gray-500 dark:text-gray-400 py-8">
<p class="text-center text-muted-foreground py-8">
Keine Versionen vorhanden
</p>
{:else}
@ -440,16 +434,16 @@ Du kannst Markdown verwenden:
{#each documentVersions as version (version.id)}
<button
onclick={() => restoreVersion(version)}
class="w-full p-3 text-left rounded-lg border border-gray-200 dark:border-gray-700
hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors
{version.id === document?.id ? 'ring-2 ring-blue-500' : ''}"
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-gray-900 dark:text-white">
<span class="font-medium text-foreground">
Version {version.version}
{version.id === document?.id ? ' (aktuell)' : ''}
</span>
<span class="text-xs text-gray-500">
<span class="text-xs text-muted-foreground">
{new Date(version.created_at).toLocaleDateString('de-DE', {
day: '2-digit',
month: 'short',
@ -458,7 +452,7 @@ Du kannst Markdown verwenden:
})}
</span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
<p class="text-sm text-muted-foreground line-clamp-2">
{version.content.substring(0, 100)}...
</p>
</button>

View file

@ -77,20 +77,20 @@
<title>Dokumente | ManaChat</title>
</svelte:head>
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-6xl mx-auto px-4">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Dokumente</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
<h1 class="text-2xl font-bold text-foreground">Dokumente</h1>
<p class="text-sm text-muted-foreground mt-1">
Alle Dokumente aus deinen Konversationen im Dokumentmodus.
</p>
</div>
<button
onclick={loadDocuments}
class="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200
hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
class="p-2 text-muted-foreground hover:text-foreground
hover:bg-muted rounded-lg transition-colors"
aria-label="Aktualisieren"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -108,14 +108,14 @@
{#if isLoading}
<div class="flex items-center justify-center py-16">
<div
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
class="animate-spin w-8 h-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if documents.length === 0}
<!-- Empty State -->
<div class="text-center py-16">
<svg
class="w-16 h-16 text-gray-400 mx-auto mb-4"
class="w-16 h-16 text-muted-foreground mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -127,10 +127,10 @@
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>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">
<h3 class="text-lg font-medium text-foreground mb-1">
Keine Dokumente gefunden
</h3>
<p class="text-gray-500 dark:text-gray-400 max-w-sm mx-auto">
<p class="text-muted-foreground max-w-sm mx-auto">
Erstelle ein neues Dokument in einer Konversation mit aktiviertem Dokumentmodus.
</p>
</div>
@ -140,19 +140,19 @@
{#each documents as doc (doc.id)}
<button
onclick={() => navigateToConversation(doc.conversation_id)}
class="text-left p-0 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
shadow-sm hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all overflow-hidden"
class="text-left p-0 bg-surface rounded-xl border border-border
shadow-sm hover:shadow-md hover:border-primary/50 transition-all overflow-hidden"
>
<!-- Header -->
<div class="p-4 border-b border-gray-100 dark:border-gray-700">
<h3 class="font-semibold text-gray-900 dark:text-white line-clamp-2 mb-2">
<div class="p-4 border-b border-border">
<h3 class="font-semibold text-foreground line-clamp-2 mb-2">
{extractTitle(doc.content)}
</h3>
<div class="flex items-center justify-between text-xs text-gray-500">
<div class="flex items-center justify-between text-xs text-muted-foreground">
<span class="truncate">{doc.conversation_title}</span>
<div class="flex items-center gap-2 flex-shrink-0">
<span>{formatDate(doc.updated_at)}</span>
<span class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded font-medium">
<span class="px-1.5 py-0.5 bg-muted rounded font-medium">
v{doc.version}
</span>
</div>
@ -161,7 +161,7 @@
<!-- Preview -->
<div class="p-4 h-32 overflow-hidden">
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-5">
<p class="text-sm text-muted-foreground line-clamp-5">
{getPreview(doc.content)}
</p>
</div>

View file

@ -26,27 +26,27 @@
<title>Profil | ManaChat</title>
</svelte:head>
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-2xl mx-auto px-4">
<!-- Header -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Profil</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
<h1 class="text-2xl font-bold text-foreground">Profil</h1>
<p class="text-sm text-muted-foreground mt-1">
Verwalte dein Konto und deine Einstellungen.
</p>
</div>
<!-- Profile Card -->
<div
class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden mb-6"
class="bg-surface rounded-xl border border-border shadow-sm overflow-hidden mb-6"
>
<div class="p-6">
<div class="flex items-center gap-4 mb-6">
<div
class="w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center"
class="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center"
>
<svg
class="w-8 h-8 text-blue-600 dark:text-blue-400"
class="w-8 h-8 text-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -60,10 +60,10 @@
</svg>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
<h2 class="text-lg font-semibold text-foreground">
{authStore.user?.email || 'Benutzer'}
</h2>
<p class="text-sm text-gray-500">
<p class="text-sm text-muted-foreground">
Mitglied seit {formatDate(authStore.user?.created_at)}
</p>
</div>
@ -71,19 +71,19 @@
<div class="space-y-4">
<div
class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-700"
class="flex items-center justify-between py-3 border-b border-border"
>
<div>
<p class="font-medium text-gray-900 dark:text-white">E-Mail</p>
<p class="text-sm text-gray-500">{authStore.user?.email || '-'}</p>
<p class="font-medium text-foreground">E-Mail</p>
<p class="text-sm text-muted-foreground">{authStore.user?.email || '-'}</p>
</div>
</div>
<div
class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-700"
class="flex items-center justify-between py-3 border-b border-border"
>
<div>
<p class="font-medium text-gray-900 dark:text-white">Benutzer-ID</p>
<p class="text-sm text-gray-500 font-mono">{authStore.user?.id || '-'}</p>
<p class="font-medium text-foreground">Benutzer-ID</p>
<p class="text-sm text-muted-foreground font-mono">{authStore.user?.id || '-'}</p>
</div>
</div>
</div>
@ -92,22 +92,22 @@
<!-- Settings Card -->
<div
class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden mb-6"
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-gray-900 dark:text-white mb-4">Einstellungen</h3>
<h3 class="text-lg font-semibold text-foreground mb-4">Einstellungen</h3>
<div class="space-y-4">
<!-- Theme Toggle -->
<div class="flex items-center justify-between py-3">
<div>
<p class="font-medium text-gray-900 dark:text-white">Dunkler Modus</p>
<p class="text-sm text-gray-500">Aktiviere den dunklen Modus für die App</p>
<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>
<button
onclick={toggleTheme}
class="relative w-12 h-6 rounded-full transition-colors
{theme.mode === 'dark' ? 'bg-blue-600' : 'bg-gray-300'}"
{theme.mode === 'dark' ? 'bg-primary' : 'bg-muted'}"
role="switch"
aria-checked={theme.mode === 'dark'}
aria-label="Dunkler Modus umschalten"
@ -124,14 +124,14 @@
<!-- Sign Out -->
<div
class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden"
class="bg-surface rounded-xl border border-border shadow-sm overflow-hidden"
>
<div class="p-6">
<button
onclick={handleSignOut}
class="w-full flex items-center justify-center gap-2 px-4 py-3 bg-red-50 dark:bg-red-900/20
text-red-600 dark:text-red-400 rounded-lg font-medium
hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors"
class="w-full flex items-center justify-center gap-2 px-4 py-3 bg-destructive/10
text-destructive rounded-lg font-medium
hover:bg-destructive/20 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path

View file

@ -86,20 +86,20 @@
<title>Spaces | ManaChat</title>
</svelte:head>
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-4xl mx-auto px-4">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Spaces</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
<h1 class="text-2xl font-bold text-foreground">Spaces</h1>
<p class="text-sm text-muted-foreground mt-1">
Organisiere deine Konversationen in kollaborativen Arbeitsbereichen.
</p>
</div>
<button
onclick={handleCreateNew}
class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
hover:bg-blue-700 transition-colors"
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@ -117,14 +117,14 @@
{#if spacesStore.isLoading}
<div class="flex items-center justify-center py-16">
<div
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
class="animate-spin w-8 h-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if spacesStore.spaces.length === 0}
<!-- Empty State -->
<div class="text-center py-16">
<svg
class="w-16 h-16 text-gray-400 mx-auto mb-4"
class="w-16 h-16 text-muted-foreground mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -136,16 +136,16 @@
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">
<h3 class="text-lg font-medium text-foreground mb-1">
Keine Spaces gefunden
</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">
<p class="text-muted-foreground mb-4">
Erstelle einen neuen Space oder frage nach einer Einladung
</p>
<button
onclick={handleCreateNew}
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
hover:bg-blue-700 transition-colors"
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@ -176,7 +176,7 @@
<!-- Error Message -->
{#if spacesStore.error}
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
<div class="mt-4 p-4 bg-destructive/10 text-destructive rounded-lg">
{spacesStore.error}
</div>
{/if}

View file

@ -87,23 +87,23 @@
{#if isLoading}
<div class="flex items-center justify-center h-[calc(100vh-4rem)]">
<div
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
class="animate-spin w-8 h-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if error}
<div class="flex flex-col items-center justify-center h-[calc(100vh-4rem)] text-center p-4">
<p class="text-red-500 mb-4">{error}</p>
<a href="/spaces" class="text-blue-600 hover:underline">Zurück zu Spaces</a>
<p class="text-destructive mb-4">{error}</p>
<a href="/spaces" class="text-primary hover:underline">Zurück zu Spaces</a>
</div>
{:else if space}
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-4xl mx-auto px-4">
<!-- Header -->
<div class="mb-6">
<div class="flex items-center gap-2 mb-2">
<a
href="/spaces"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
class="p-1 text-muted-foreground hover:text-foreground rounded-lg hover:bg-muted transition-colors"
aria-label="Zurück zu Spaces"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -115,24 +115,24 @@
/>
</svg>
</a>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{space.name}</h1>
<h1 class="text-2xl font-bold text-foreground">{space.name}</h1>
</div>
{#if space.description}
<p class="text-sm text-gray-600 dark:text-gray-400">{space.description}</p>
<p class="text-sm text-muted-foreground">{space.description}</p>
{/if}
</div>
<!-- New Chat Section -->
<div
class="mb-8 p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700"
class="mb-8 p-4 bg-surface rounded-xl border border-border"
>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">Neuen Chat starten</h2>
<h2 class="text-lg font-semibold text-foreground mb-3">Neuen Chat starten</h2>
<div class="flex items-center gap-3">
<select
bind:value={selectedModelId}
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="flex-1 px-3 py-2 border border-border rounded-lg
bg-muted text-foreground
focus:ring-2 focus:ring-primary focus:border-transparent"
>
{#each models as model}
<option value={model.id}>{model.name}</option>
@ -140,8 +140,8 @@
</select>
<button
onclick={handleNewChat}
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
hover:bg-blue-700 transition-colors whitespace-nowrap"
class="px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors whitespace-nowrap"
>
Chat starten
</button>
@ -150,16 +150,16 @@
<!-- Conversations List -->
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<h2 class="text-lg font-semibold text-foreground mb-4">
Konversationen in diesem Space
</h2>
{#if conversations.length === 0}
<div
class="text-center py-12 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700"
class="text-center py-12 bg-surface rounded-xl border border-border"
>
<svg
class="w-12 h-12 text-gray-400 mx-auto mb-3"
class="w-12 h-12 text-muted-foreground mx-auto mb-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -171,7 +171,7 @@
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
<p class="text-gray-500 dark:text-gray-400">
<p class="text-muted-foreground">
Noch keine Konversationen in diesem Space.
</p>
</div>
@ -180,14 +180,14 @@
{#each conversations as conv (conv.id)}
<a
href="/chat/{conv.id}"
class="block p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
hover:border-blue-300 dark:hover:border-blue-600 transition-colors"
class="block p-4 bg-surface rounded-xl border border-border
hover:border-primary/50 transition-colors"
>
<div class="flex items-center justify-between">
<h3 class="font-medium text-gray-900 dark:text-white">
<h3 class="font-medium text-foreground">
{conv.title || 'Neue Konversation'}
</h3>
<span class="text-xs text-gray-500">{formatDate(conv.updated_at)}</span>
<span class="text-xs text-muted-foreground">{formatDate(conv.updated_at)}</span>
</div>
</a>
{/each}

View file

@ -105,21 +105,21 @@
<title>Vorlagen | ManaChat</title>
</svelte:head>
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-4xl mx-auto px-4">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Vorlagen</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
<h1 class="text-2xl font-bold text-foreground">Vorlagen</h1>
<p class="text-sm text-muted-foreground mt-1">
Erstelle Vorlagen mit benutzerdefinierten System-Prompts für verschiedene
KI-Verhaltensweisen.
</p>
</div>
<button
onclick={handleCreateNew}
class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
hover:bg-blue-700 transition-colors"
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@ -137,14 +137,14 @@
{#if templatesStore.isLoading}
<div class="flex items-center justify-center py-16">
<div
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
class="animate-spin w-8 h-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if templatesStore.templates.length === 0}
<!-- Empty State -->
<div class="text-center py-16">
<svg
class="w-16 h-16 text-gray-400 mx-auto mb-4"
class="w-16 h-16 text-muted-foreground mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -156,16 +156,16 @@
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">
<h3 class="text-lg font-medium text-foreground mb-1">
Keine Vorlagen vorhanden
</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">
<p class="text-muted-foreground mb-4">
Erstelle deine erste Vorlage, um loszulegen
</p>
<button
onclick={handleCreateNew}
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
hover:bg-blue-700 transition-colors"
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@ -195,7 +195,7 @@
<!-- Error Message -->
{#if templatesStore.error}
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
<div class="mt-4 p-4 bg-destructive/10 text-destructive rounded-lg">
{templatesStore.error}
</div>
{/if}

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ThemePage } from '@manacore/shared-theme-ui';
import { theme } from '$lib/stores/theme';
</script>
<svelte:head>
<title>Themes | ManaChat</title>
</svelte:head>
<ThemePage
currentVariant={theme.variant}
onSelectTheme={(v) => theme.setVariant(v)}
showModeSelector={true}
currentMode={theme.mode}
onModeChange={(m) => theme.setMode(m)}
showBackButton={true}
onBack={() => goto('/chat')}
/>

View file

@ -11,6 +11,6 @@
});
</script>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<div class="min-h-screen bg-background text-foreground">
{@render children()}
</div>

View file

@ -22,8 +22,8 @@
<div class="flex items-center justify-center min-h-screen">
<div class="text-center">
<div
class="animate-spin w-10 h-10 border-4 border-blue-500 border-r-transparent rounded-full mx-auto"
class="animate-spin w-10 h-10 border-4 border-primary border-r-transparent rounded-full mx-auto"
></div>
<p class="mt-4 text-gray-600 dark:text-gray-400">Wird geladen...</p>
<p class="mt-4 text-muted-foreground">Wird geladen...</p>
</div>
</div>

View file

@ -20,9 +20,11 @@
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-i18n": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-subscription-types": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"@picture/design-tokens": "workspace:*",

View file

@ -1,11 +1,9 @@
<script lang="ts">
import { AppSlider, type AppItem } from '@manacore/shared-ui';
import { APP_ICONS } from '@manacore/shared-branding';
import { actualMode } from '$lib/stores/theme';
import { theme } from '$lib/stores/theme';
import { t } from 'svelte-i18n';
let isDark = $derived($actualMode === 'dark');
let apps = $derived<AppItem[]>([
{
name: 'Picture',
@ -78,7 +76,7 @@
<AppSlider
{apps}
title={$t('app_slider.title')}
{isDark}
isDark={theme.isDark}
{statusLabels}
comingSoonLabel={$t('app_slider.coming_soon')}
openAppLabel={$t('app_slider.download')}

View file

@ -2,9 +2,8 @@
import { locale } from 'svelte-i18n';
import { LanguageSelector } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { actualMode } from '$lib/stores/theme';
import { theme } from '$lib/stores/theme';
let isDark = $derived($actualMode === 'dark');
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
@ -16,6 +15,6 @@
{currentLocale}
{supportedLocales}
onLocaleChange={handleLocaleChange}
{isDark}
isDark={theme.isDark}
primaryColor="#3b82f6"
/>

View file

@ -1,9 +1,7 @@
<script lang="ts">
import type { Database } from '@picture/shared/types';
import type { Image } from '$lib/api/images';
import { showContextMenu } from '$lib/stores/contextMenu';
type Image = Database['public']['Tables']['images']['Row'];
interface Props {
image: Image;
onclick: () => void;
@ -38,7 +36,7 @@
type="button"
>
<img
src={image.public_url}
src={image.publicUrl}
alt={image.prompt}
class="h-full w-full object-cover transition-opacity duration-300 {imageLoaded
? 'opacity-100'
@ -52,7 +50,7 @@
class="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/80 via-black/40 to-transparent p-4 opacity-0 transition-opacity group-hover:opacity-100"
>
<p class="text-base font-medium text-white">{image.prompt}</p>
<p class="mt-1 text-sm text-gray-300">{formatDate(image.created_at)}</p>
<p class="mt-1 text-sm text-gray-300">{formatDate(image.createdAt)}</p>
</div>
<!-- Archived badge - always visible -->

View file

@ -1,11 +1,10 @@
<script lang="ts">
import type { Database } from '@picture/shared/types';
import type { Image } from '$lib/api/images';
import Modal from '../ui/Modal.svelte';
import Button from '../ui/Button.svelte';
import { unarchiveImage, deleteImage, downloadImage } from '$lib/api/images';
import { archivedImages } from '$lib/stores/archive';
type Image = Database['public']['Tables']['images']['Row'];
import { DownloadSimple, ArrowCounterClockwise, Trash } from '@manacore/shared-icons';
interface Props {
image: Image | null;
@ -54,9 +53,9 @@
}
function handleDownload() {
if (!image) return;
if (!image || !image.publicUrl) return;
const filename = `picture-${image.id}.png`;
downloadImage(image.public_url, filename);
downloadImage(image.publicUrl, filename);
}
function formatDate(dateString: string) {
@ -77,7 +76,7 @@
<!-- Image -->
<div class="flex-1">
<img
src={image.public_url}
src={image.publicUrl}
alt={image.prompt}
class="h-auto w-full rounded-lg object-contain"
/>
@ -101,26 +100,19 @@
<!-- Model -->
<div>
<h3 class="mb-2 text-sm font-medium text-gray-500">Model</h3>
<p class="text-gray-900">{image.model_id || 'Unknown'}</p>
<p class="text-gray-900">{image.model || 'Unknown'}</p>
</div>
<!-- Created At -->
<div>
<h3 class="mb-2 text-sm font-medium text-gray-500">Created</h3>
<p class="text-gray-900">{formatDate(image.created_at)}</p>
<p class="text-gray-900">{formatDate(image.createdAt)}</p>
</div>
<!-- Actions -->
<div class="space-y-2">
<Button variant="primary" class="w-full" onclick={handleDownload}>
<svg class="mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<DownloadSimple size={20} class="mr-2" />
Download
</Button>
@ -131,14 +123,7 @@
loading={isUnarchiving}
disabled={isUnarchiving || isDeleting}
>
<svg class="mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
/>
</svg>
<ArrowCounterClockwise size={20} class="mr-2" />
Restore to Gallery
</Button>
@ -149,14 +134,7 @@
loading={isDeleting}
disabled={isUnarchiving || isDeleting}
>
<svg class="mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Trash size={20} class="mr-2" />
Delete Permanently
</Button>
</div>

View file

@ -226,13 +226,13 @@
const konvaImage = new Konva.Image({
id: item.id,
name: item.id,
x: item.position_x,
y: item.position_y,
x: item.positionX,
y: item.positionY,
image: imageObj,
width: item.width || imageObj.width,
height: item.height || imageObj.height,
scaleX: item.scale_x,
scaleY: item.scale_y,
scaleX: item.scaleX,
scaleY: item.scaleY,
rotation: item.rotation,
draggable: true,
opacity: item.opacity,
@ -273,9 +273,9 @@
const konvaText = new Konva.Text({
id: item.id,
name: item.id,
x: item.position_x,
y: item.position_y,
text: item.text_content,
x: item.positionX,
y: item.positionY,
text: item.textContent,
fontSize: item.font_size,
fontFamily: item.properties?.fontFamily || 'Arial',
fontStyle: `${item.properties?.fontStyle || 'normal'} ${item.properties?.fontWeight || 'normal'}`,
@ -283,8 +283,8 @@
width: item.width || 300,
align: item.properties?.textAlign || 'left',
rotation: item.rotation,
scaleX: item.scale_x,
scaleY: item.scale_y,
scaleX: item.scaleX,
scaleY: item.scaleY,
opacity: item.opacity,
draggable: true,
lineHeight: item.properties?.lineHeight || 1.2,
@ -364,12 +364,12 @@
// Update store
updateCanvasItem(itemId, {
position_x: x,
position_y: y,
positionX: x,
positionY: y,
});
// Save to database
await saveBoardItem(itemId, { position_x: x, position_y: y });
await saveBoardItem(itemId, { positionX: x, positionY: y });
}
async function handleTransformEnd(node: Konva.Image, itemId: string) {
@ -379,15 +379,15 @@
// Update store
updateCanvasItem(itemId, {
scale_x: scaleX,
scale_y: scaleY,
scaleX: scaleX,
scaleY: scaleY,
rotation: rotation,
});
// Save to database
await saveBoardItem(itemId, {
scale_x: scaleX,
scale_y: scaleY,
scaleX: scaleX,
scaleY: scaleY,
rotation: rotation,
});
}
@ -405,16 +405,16 @@
// Update store
updateCanvasItem(itemId, {
width: Math.round(width),
scale_x: 1,
scale_y: scaleY,
scaleX: 1,
scaleY: scaleY,
rotation: rotation,
});
// Save to database
await saveBoardItem(itemId, {
width: Math.round(width),
scale_x: 1,
scale_y: scaleY,
scaleX: 1,
scaleY: scaleY,
rotation: rotation,
});
}
@ -459,8 +459,8 @@
layer.batchDraw();
// Update store and database
updateCanvasItem(item.id, { text_content: newText });
await saveBoardItem(item.id, { text_content: newText });
updateCanvasItem(item.id, { textContent: newText });
await saveBoardItem(item.id, { textContent: newText });
stopEditingText();
};

View file

@ -19,6 +19,19 @@
import { boardSettings } from '$lib/stores/boards';
import Button from '$lib/components/ui/Button.svelte';
import Konva from 'konva';
import {
CaretLeft,
ArrowCounterClockwise,
ArrowClockwise,
Minus,
Plus,
GridFour,
Table,
Trash,
TextT,
Image,
DownloadSimple,
} from '@manacore/shared-icons';
interface Props {
boardName: string;
@ -80,14 +93,7 @@
class="flex h-10 w-10 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
title="Zurück"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
<CaretLeft size={20} weight="bold" />
</button>
<div>
@ -107,14 +113,7 @@
class="flex h-10 w-10 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 disabled:opacity-30 dark:text-gray-400 dark:hover:bg-gray-800"
title="Rückgängig (⌘Z)"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
/>
</svg>
<ArrowCounterClockwise size={20} weight="bold" />
</button>
<button
@ -123,14 +122,7 @@
class="flex h-10 w-10 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 disabled:opacity-30 dark:text-gray-400 dark:hover:bg-gray-800"
title="Wiederherstellen (⌘⇧Z)"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 10h-10a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6"
/>
</svg>
<ArrowClockwise size={20} weight="bold" />
</button>
<div class="mx-2 h-6 w-px bg-gray-300 dark:bg-gray-600"></div>
@ -141,9 +133,7 @@
class="flex h-10 w-10 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
title="Verkleinern"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
</svg>
<Minus size={20} weight="bold" />
</button>
<div
@ -157,14 +147,7 @@
class="flex h-10 w-10 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
title="Vergrößern"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
<Plus size={20} weight="bold" />
</button>
<button
@ -193,14 +176,7 @@
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'}"
title="Raster {$showGrid ? 'ausblenden' : 'einblenden'}"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"
/>
</svg>
<GridFour size={20} weight="bold" />
</button>
<!-- Snap Toggle -->
@ -211,14 +187,7 @@
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'}"
title="Am Raster einrasten"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
<Table size={20} weight="bold" />
</button>
{#if hasSelection}
@ -230,14 +199,7 @@
class="flex h-10 w-10 items-center justify-center rounded-lg text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950"
title="Löschen (Entf)"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Trash size={20} weight="bold" />
</button>
{/if}
</div>
@ -245,38 +207,17 @@
<!-- Right: Actions -->
<div class="flex items-center gap-2">
<Button variant="outline" onclick={onAddText}>
<svg class="mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h7"
/>
</svg>
<TextT size={20} class="mr-2" />
Text
</Button>
<Button variant="outline" onclick={onAddImages}>
<svg class="mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<Image size={20} class="mr-2" />
Bilder
</Button>
<Button variant="secondary" onclick={handleExport}>
<svg class="mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<DownloadSimple size={20} class="mr-2" />
Exportieren
</Button>
</div>

View file

@ -1,15 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { authStore } from '$lib/stores/auth.svelte';
import { images, isLoading as isLoadingImages } from '$lib/stores/images';
import { canvasItems, addCanvasItem } from '$lib/stores/canvas';
import { getImages } from '$lib/api/images';
import { addBoardItem, isImageOnBoard } from '$lib/api/boardItems';
import { addBoardItem } from '$lib/api/boardItems';
import { showToast } from '$lib/stores/toast';
import Modal from '$lib/components/ui/Modal.svelte';
import Button from '$lib/components/ui/Button.svelte';
import type { Database } from '@picture/shared/types';
import { MagnifyingGlass, Image as ImageIcon, Check } from '@manacore/shared-icons';
interface Props {
open: boolean;
@ -18,8 +17,6 @@
let { open, onClose }: Props = $props();
type Image = Database['public']['Tables']['images']['Row'];
let selectedImages = $state<Set<string>>(new Set());
let isAdding = $state(false);
let searchQuery = $state('');
@ -41,7 +38,6 @@
isLoadingImages.set(true);
try {
const data = await getImages({
userId: authStore.user.id,
page: 1,
limit: 50,
archived: false,
@ -72,7 +68,7 @@
}
function isImageAlreadyOnBoard(imageId: string): boolean {
return $canvasItems.some((item) => item.image_id === imageId);
return $canvasItems.some((item) => item.imageId === imageId);
}
async function handleAddImages() {
@ -94,17 +90,11 @@
// Add to board
const boardItem = await addBoardItem({
board_id: boardId,
image_id: imageId,
position_x: 100 + addedCount * 20, // Offset each image slightly
position_y: 100 + addedCount * 20,
scale_x: 1,
scale_y: 1,
rotation: 0,
z_index: addedCount,
opacity: 1,
width: image.width || 400,
height: image.height || 300,
boardId: boardId,
itemType: 'image',
imageId: imageId,
positionX: 100 + addedCount * 20, // Offset each image slightly
positionY: 100 + addedCount * 20,
});
// Add to canvas
@ -160,19 +150,7 @@
placeholder="Bilder suchen..."
class="w-full rounded-lg border border-gray-300 bg-white px-4 py-2 pl-10 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
<svg
class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<MagnifyingGlass size={20} class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
</div>
<Button
@ -204,19 +182,7 @@
</div>
{:else if filteredImages.length === 0}
<div class="flex h-full flex-col items-center justify-center py-12">
<svg
class="h-16 w-16 text-gray-300 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<ImageIcon size={64} weight="thin" class="text-gray-300 dark:text-gray-600" />
<p class="mt-4 text-gray-600 dark:text-gray-400">
{searchQuery ? 'Keine Bilder gefunden' : 'Keine Bilder in deiner Galerie'}
</p>
@ -236,7 +202,7 @@
: 'hover:ring-2 hover:ring-gray-300'}"
>
<img
src={image.public_url}
src={image.publicUrl}
alt={image.prompt || 'Image'}
class="h-full w-full object-cover"
/>
@ -246,14 +212,7 @@
<div
class="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-blue-500 text-white"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="M5 13l4 4L19 7"
/>
</svg>
<Check size={16} weight="bold" />
</div>
{/if}

View file

@ -1,11 +1,21 @@
<script lang="ts">
import { selectedItems, updateCanvasItem, removeSelectedItems } from '$lib/stores/canvas';
import { updateBoardItem, changeBoardItemZIndex } from '$lib/api/boardItems';
import { updateBoardItem, changeBoardItemZIndex, isImageItem } from '$lib/api/boardItems';
import Button from '$lib/components/ui/Button.svelte';
import { showToast } from '$lib/stores/toast';
import {
Image,
CaretDoubleUp,
CaretDoubleDown,
ArrowUp,
ArrowDown,
Trash,
CursorClick,
} from '@manacore/shared-icons';
const selectedItem = $derived($selectedItems[0] || null);
const hasMultipleSelected = $derived($selectedItems.length > 1);
const selectedImageItem = $derived(selectedItem && isImageItem(selectedItem) ? selectedItem : null);
// Local state for inputs (synced with selected item)
let positionX = $state(0);
@ -19,10 +29,10 @@
// Update local state when selection changes
$effect(() => {
if (selectedItem) {
positionX = Math.round(selectedItem.position_x);
positionY = Math.round(selectedItem.position_y);
scaleX = Math.round(selectedItem.scale_x * 100);
scaleY = Math.round(selectedItem.scale_y * 100);
positionX = Math.round(selectedItem.positionX);
positionY = Math.round(selectedItem.positionY);
scaleX = Math.round(selectedItem.scaleX * 100);
scaleY = Math.round(selectedItem.scaleY * 100);
rotation = Math.round(selectedItem.rotation);
opacity = Math.round(selectedItem.opacity * 100);
}
@ -31,7 +41,7 @@
async function handlePositionChange(axis: 'x' | 'y', value: number) {
if (!selectedItem) return;
const updates = axis === 'x' ? { position_x: value } : { position_y: value };
const updates = axis === 'x' ? { positionX: value } : { positionY: value };
updateCanvasItem(selectedItem.id, updates);
@ -50,11 +60,11 @@
let updates: any = {};
if (lockAspectRatio) {
updates = { scale_x: scale, scale_y: scale };
updates = { scaleX: scale, scaleY: scale };
scaleX = percent;
scaleY = percent;
} else {
updates = axis === 'x' ? { scale_x: scale } : { scale_y: scale };
updates = axis === 'x' ? { scaleX: scale } : { scaleY: scale };
}
updateCanvasItem(selectedItem.id, updates);
@ -137,19 +147,7 @@
class="h-full overflow-y-auto border-l border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900"
>
<div class="text-center">
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<Image size={48} weight="regular" class="mx-auto text-gray-400" />
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">
{$selectedItems.length} Bilder ausgewählt
</h3>
@ -170,22 +168,24 @@
</h3>
<!-- Image Preview -->
<div class="mb-6 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<img src={selectedItem.image.public_url} alt="Preview" class="w-full" />
</div>
<!-- Prompt Info -->
{#if selectedItem.image.prompt}
<div class="mb-6">
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
Prompt
</label>
<p
class="rounded-lg bg-gray-50 p-3 text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{selectedItem.image.prompt}
</p>
{#if selectedImageItem?.image}
<div class="mb-6 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<img src={selectedImageItem.image.publicUrl} alt="Preview" class="w-full" />
</div>
<!-- Prompt Info -->
{#if selectedImageItem.image.prompt}
<div class="mb-6">
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
Prompt
</label>
<p
class="rounded-lg bg-gray-50 p-3 text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{selectedImageItem.image.prompt}
</p>
</div>
{/if}
{/if}
<!-- Position -->
@ -339,56 +339,28 @@
onclick={() => handleLayerChange('top')}
class="flex items-center justify-center gap-2 rounded-lg border border-gray-300 px-3 py-2 text-sm hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 15l7-7 7 7"
/>
</svg>
<CaretDoubleUp size={16} />
Nach vorne
</button>
<button
onclick={() => handleLayerChange('bottom')}
class="flex items-center justify-center gap-2 rounded-lg border border-gray-300 px-3 py-2 text-sm hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
<CaretDoubleDown size={16} />
Nach hinten
</button>
<button
onclick={() => handleLayerChange('up')}
class="flex items-center justify-center gap-2 rounded-lg border border-gray-300 px-3 py-2 text-sm hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
</svg>
<ArrowUp size={16} />
Eine Ebene
</button>
<button
onclick={() => handleLayerChange('down')}
class="flex items-center justify-center gap-2 rounded-lg border border-gray-300 px-3 py-2 text-sm hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
<ArrowDown size={16} />
Eine Ebene
</button>
</div>
@ -397,22 +369,24 @@
<!-- Dimensions Info -->
<div class="mb-6 rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
<div class="text-xs text-gray-500 dark:text-gray-400">
<div class="flex justify-between py-1">
<span>Original:</span>
<span class="font-medium"
>{selectedItem.image.width} × {selectedItem.image.height}px</span
>
</div>
<div class="flex justify-between py-1">
<span>Aktuell:</span>
<span class="font-medium">
{Math.round((selectedItem.image.width || 0) * selectedItem.scale_x)} ×
{Math.round((selectedItem.image.height || 0) * selectedItem.scale_y)}px
</span>
</div>
{#if selectedImageItem?.image}
<div class="flex justify-between py-1">
<span>Original:</span>
<span class="font-medium"
>{selectedImageItem.image.width} × {selectedImageItem.image.height}px</span
>
</div>
<div class="flex justify-between py-1">
<span>Aktuell:</span>
<span class="font-medium">
{Math.round((selectedImageItem.image.width || 0) * selectedItem.scaleX)} ×
{Math.round((selectedImageItem.image.height || 0) * selectedItem.scaleY)}px
</span>
</div>
{/if}
<div class="flex justify-between py-1">
<span>Z-Index:</span>
<span class="font-medium">{selectedItem.z_index}</span>
<span class="font-medium">{selectedItem.zIndex}</span>
</div>
</div>
</div>
@ -423,14 +397,7 @@
Transform zurücksetzen
</Button>
<Button variant="danger" class="w-full" onclick={handleDelete}>
<svg class="mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Trash size={16} class="mr-2" />
Bild entfernen
</Button>
</div>
@ -442,19 +409,7 @@
class="flex h-full items-center justify-center border-l border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900"
>
<div class="text-center">
<svg
class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
<CursorClick size={48} weight="thin" class="mx-auto text-gray-300 dark:text-gray-600" />
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
Wähle ein Bild aus, um<br />seine Eigenschaften zu bearbeiten
</p>

View file

@ -1,10 +1,9 @@
<script lang="ts">
import type { Database } from '@picture/shared/types';
import type { Image } from '$lib/api/images';
import ImageCard from './ImageCard.svelte';
import { selectedImage } from '$lib/stores/images';
import { viewMode, type ViewMode } from '$lib/stores/view';
type Image = Database['public']['Tables']['images']['Row'];
import { Image as ImageIcon } from '@manacore/shared-icons';
interface Props {
images: Image[];
@ -31,19 +30,7 @@
{#if images.length === 0}
<div class="flex min-h-[400px] items-center justify-center rounded-lg bg-white p-8 shadow">
<div class="text-center">
<svg
class="mx-auto h-24 w-24 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<ImageIcon size={96} weight="thin" class="mx-auto text-gray-400" />
<h2 class="mt-4 text-xl font-semibold text-gray-900">No images yet</h2>
<p class="mt-2 text-gray-600">Start generating AI images to see them here.</p>
<a

View file

@ -1,10 +1,8 @@
<script lang="ts">
import type { Database } from '@picture/shared/types';
import type { Image } from '$lib/api/images';
import type { ViewMode } from '$lib/stores/view';
import { showContextMenu } from '$lib/stores/contextMenu';
type Image = Database['public']['Tables']['images']['Row'];
interface Props {
image: Image;
onclick?: () => void;
@ -49,7 +47,7 @@
>
<div class="w-full {aspectClass}">
<img
src={image.public_url}
src={image.publicUrl}
alt={image.prompt}
class="h-full w-full object-cover transition-opacity duration-300 {imageLoaded
? 'opacity-100'
@ -68,13 +66,13 @@
</p>
{#if viewMode !== 'grid5'}
<p class="text-sm text-white/80">
{formatDate(image.created_at)}
{formatDate(image.createdAt)}
</p>
{/if}
</div>
</div>
{#if image.archived_at}
{#if image.archivedAt}
<div class="absolute right-2 top-2 rounded-full bg-black/50 px-2 py-1 text-xs text-white">
Archived
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import type { Database } from '@picture/shared/types';
import type { Image } from '$lib/api/images';
import type { Tag } from '$lib/api/tags';
import {
archiveImage,
deleteImage,
@ -11,9 +12,18 @@
import { showToast } from '$lib/stores/toast';
import { fade, fly } from 'svelte/transition';
import { getImageTags, getAllTags, addTagToImage, removeTagFromImage } from '$lib/api/tags';
type Image = Database['public']['Tables']['images']['Row'];
type Tag = Database['public']['Tables']['tags']['Row'];
import {
X,
Info,
Tag as TagIcon,
DownloadSimple,
Globe,
CaretLeft,
CaretRight,
Archive,
Trash,
Check,
} from '@manacore/shared-icons';
interface Props {
image: Image | null;
@ -33,7 +43,7 @@
let isPublishing = $state(false);
// Get current image index
const currentIndex = $derived(image ? $images.findIndex((img) => img.id === image.id) : -1);
const currentIndex = $derived(image ? $images.findIndex((img) => img.id === image?.id) : -1);
const hasPrevious = $derived(currentIndex > 0);
const hasNext = $derived(currentIndex >= 0 && currentIndex < $images.length - 1);
@ -87,12 +97,13 @@
async function handleArchive() {
if (!image) return;
const imageId = image.id;
isArchiving = true;
try {
await archiveImage(image.id);
await archiveImage(imageId);
// Update store
images.update((current) => current.filter((img) => img.id !== image.id));
images.update((current) => current.filter((img) => img.id !== imageId));
showToast('Bild erfolgreich archiviert', 'success');
onClose();
} catch (error) {
@ -105,6 +116,7 @@
async function handleDelete() {
if (!image) return;
const imageId = image.id;
if (
!confirm(
'Bist du sicher, dass du dieses Bild löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'
@ -114,9 +126,9 @@
isDeleting = true;
try {
await deleteImage(image.id);
await deleteImage(imageId);
// Update store
images.update((current) => current.filter((img) => img.id !== image.id));
images.update((current) => current.filter((img) => img.id !== imageId));
showToast('Bild erfolgreich gelöscht', 'success');
onClose();
} catch (error) {
@ -128,9 +140,9 @@
}
function handleDownload() {
if (!image) return;
if (!image || !image.publicUrl) return;
const filename = `picture-${image.id}.png`;
downloadImage(image.public_url, filename);
downloadImage(image.publicUrl, filename);
showToast('Download gestartet', 'success');
}
@ -199,7 +211,7 @@
await publishImage(image.id);
// Update local image state
if (image) {
image = { ...image, is_public: true };
image = { ...image, isPublic: true };
}
showToast('Bild erfolgreich veröffentlicht!', 'success');
closePublishModal();
@ -219,7 +231,7 @@
await unpublishImage(image.id);
// Update local image state
if (image) {
image = { ...image, is_public: false };
image = { ...image, isPublic: false };
}
showToast('Bild nicht mehr öffentlich', 'success');
closePublishModal();
@ -249,14 +261,7 @@
class="fixed right-4 top-4 z-[60] flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white backdrop-blur-xl transition-all hover:bg-white/20"
aria-label="Schließen"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={24} weight="bold" />
</button>
<!-- Info Toggle -->
@ -270,14 +275,7 @@
: ''}"
aria-label="Info anzeigen"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Info size={24} />
</button>
<!-- Tags Button -->
@ -289,14 +287,7 @@
class="fixed right-4 top-36 z-[60] flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white backdrop-blur-xl transition-all hover:bg-white/20"
aria-label="Tags verwalten"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
/>
</svg>
<TagIcon size={24} />
</button>
<!-- Download Button -->
@ -308,14 +299,7 @@
class="fixed right-4 top-52 z-[60] flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white backdrop-blur-xl transition-all hover:bg-white/20"
aria-label="Download"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<DownloadSimple size={24} />
</button>
<!-- Publish Button -->
@ -324,19 +308,12 @@
e.stopPropagation();
openPublishModal();
}}
class="fixed right-4 top-[17rem] z-[60] flex h-12 w-12 items-center justify-center rounded-full transition-all {image?.is_public
class="fixed right-4 top-[17rem] z-[60] flex h-12 w-12 items-center justify-center rounded-full transition-all {image?.isPublic
? 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
: 'bg-white/10 text-white hover:bg-white/20'} backdrop-blur-xl"
aria-label="Veröffentlichen"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Globe size={24} />
</button>
<!-- Main Image Container -->
@ -351,20 +328,13 @@
class="absolute left-4 top-1/2 z-[60] flex h-14 w-14 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white backdrop-blur-xl transition-all hover:bg-white/20"
aria-label="Vorheriges Bild"
>
<svg class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
<CaretLeft size={28} weight="bold" />
</button>
{/if}
<!-- Image -->
<img
src={image.public_url}
src={image.publicUrl}
alt={image.prompt}
class="max-h-full max-w-full object-contain"
onclick={(e) => e.stopPropagation()}
@ -380,14 +350,7 @@
class="absolute right-4 top-1/2 z-[60] flex h-14 w-14 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white backdrop-blur-xl transition-all hover:bg-white/20"
aria-label="Nächstes Bild"
>
<svg class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<CaretRight size={28} weight="bold" />
</button>
{/if}
</div>
@ -423,7 +386,7 @@
<h3 class="mb-1 text-xs font-semibold uppercase tracking-wide text-white/60">
Model
</h3>
<p class="text-sm text-white">{image.model_id || 'Unknown'}</p>
<p class="text-sm text-white">{image.model || 'Unknown'}</p>
</div>
{#if imageTags.length > 0}
@ -456,7 +419,7 @@
<h3 class="mb-1 text-xs font-semibold uppercase tracking-wide text-white/60">
Erstellt
</h3>
<p class="text-sm text-white">{formatDate(image.created_at)}</p>
<p class="text-sm text-white">{formatDate(image.createdAt)}</p>
</div>
<!-- Actions -->
@ -465,14 +428,7 @@
onclick={handleDownload}
class="flex flex-1 items-center justify-center gap-2 rounded-lg bg-white/20 px-4 py-2.5 text-sm font-medium text-white backdrop-blur-xl transition-all hover:bg-white/30"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<DownloadSimple size={16} />
Download
</button>
@ -481,14 +437,7 @@
disabled={isArchiving || isDeleting}
class="flex items-center justify-center gap-2 rounded-lg bg-white/20 px-4 py-2.5 text-sm font-medium text-white backdrop-blur-xl transition-all hover:bg-white/30 disabled:opacity-50"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Archive size={16} />
</button>
<button
@ -496,14 +445,7 @@
disabled={isArchiving || isDeleting}
class="flex items-center justify-center gap-2 rounded-lg bg-red-500/20 px-4 py-2.5 text-sm font-medium text-white backdrop-blur-xl transition-all hover:bg-red-500/30 disabled:opacity-50"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Trash size={16} />
</button>
</div>
</div>
@ -535,14 +477,7 @@
class="flex h-8 w-8 items-center justify-center rounded-full text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
aria-label="Schließen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={20} weight="bold" />
</button>
</div>
@ -578,19 +513,7 @@
</span>
</div>
{#if isSelected}
<svg
class="h-5 w-5 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<Check size={20} weight="bold" class="text-blue-600 dark:text-blue-400" />
{/if}
</button>
{/each}
@ -625,25 +548,18 @@
>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{image.is_public ? 'Veröffentlichung entfernen' : 'Bild veröffentlichen'}
{image.isPublic ? 'Veröffentlichung entfernen' : 'Bild veröffentlichen'}
</h2>
<button
onclick={closePublishModal}
class="flex h-8 w-8 items-center justify-center rounded-full text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
aria-label="Schließen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={20} weight="bold" />
</button>
</div>
{#if image.is_public}
{#if image.isPublic}
<div class="mb-6">
<p class="text-gray-600 dark:text-gray-400">
Dieses Bild ist derzeit öffentlich und kann von anderen Nutzern im Explore-Bereich

View file

@ -12,6 +12,7 @@
type AdvancedSettings,
type AspectRatio,
} from '$lib/components/generate/AdvancedSettingsModal.svelte';
import { Gear, Lightning, X } from '@manacore/shared-icons';
interface Props {
onGenerated?: () => void;
@ -36,8 +37,8 @@
$effect(() => {
if ($selectedModel) {
// Update defaults from model
advancedSettings.steps = $selectedModel.default_steps || 50;
advancedSettings.guidanceScale = parseFloat($selectedModel.default_guidance_scale) || 7.5;
advancedSettings.steps = $selectedModel.defaultSteps || 50;
advancedSettings.guidanceScale = $selectedModel.defaultGuidanceScale || 7.5;
}
});
@ -51,7 +52,7 @@
const data = await getActiveModels();
models.set(data);
// Select default model
const defaultModel = data.find((m) => m.is_default) || data[0];
const defaultModel = data.find((m) => m.isDefault) || data[0];
if (defaultModel) {
selectedModelId = defaultModel.id;
selectedModel.set(defaultModel);
@ -87,11 +88,11 @@
// Start generation with new async API
const { generationId } = await generateImageAsync({
prompt: prompt.trim(),
model_id: selectedModelId,
modelId: selectedModelId,
width: advancedSettings.aspectRatio.width,
height: advancedSettings.aspectRatio.height,
num_inference_steps: advancedSettings.steps,
guidance_scale: advancedSettings.guidanceScale,
numInferenceSteps: advancedSettings.steps,
guidanceScale: advancedSettings.guidanceScale,
});
// Wait for completion using realtime subscription
@ -222,7 +223,7 @@
{#each $models as model}
<option value={model.id}>
{model.name}
{model.is_default ? '(Standard)' : ''}
{model.isDefault ? '(Standard)' : ''}
</option>
{/each}
{/if}
@ -260,20 +261,7 @@
class="relative flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-gray-100/80 text-gray-600 backdrop-blur-xl transition-all hover:bg-gray-200/80 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800/80 dark:text-gray-400 dark:hover:bg-gray-700/80"
aria-label="Einstellungen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Gear size={20} />
{#if hasCustomSettings}
<span class="absolute right-0 top-0 flex h-3 w-3">
<span
@ -300,19 +288,7 @@
class="h-5 w-5 animate-spin rounded-full border-2 border-solid border-white border-r-transparent"
></div>
{:else}
<svg
class="h-5 w-5 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<Lightning size={20} weight="fill" class="text-white" />
<span class="text-sm font-medium text-white">Generieren</span>
{/if}
</button>
@ -324,14 +300,7 @@
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-gray-100/80 text-gray-600 backdrop-blur-xl transition-all hover:bg-gray-200/80 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800/80 dark:text-gray-400 dark:hover:bg-gray-700/80"
aria-label="Schließen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={20} weight="bold" />
</button>
</div>
</div>
@ -346,14 +315,7 @@
class="flex h-14 items-center justify-center gap-2 rounded-full bg-blue-600/90 px-6 text-white shadow-2xl backdrop-blur-xl transition-all hover:scale-105 hover:bg-blue-700/90 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-500/90 dark:hover:bg-blue-600/90"
aria-label="Generieren"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<Lightning size={24} weight="fill" />
<span class="text-sm font-medium">Bild generieren</span>
</button>
</div>
@ -370,14 +332,7 @@
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-600/90 text-white backdrop-blur-xl transition-all hover:bg-blue-700/90 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-500/90 dark:hover:bg-blue-600/90"
aria-label="Erweitern"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<Lightning size={24} weight="fill" />
</button>
<div class="flex-1 text-sm text-gray-600 dark:text-gray-400">Bild generieren...</div>
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { fly, fade } from 'svelte/transition';
import { X } from '@manacore/shared-icons';
export interface AspectRatio {
label: string;
@ -80,14 +81,7 @@
class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 text-gray-600 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
aria-label="Schließen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={20} weight="bold" />
</button>
</div>

View file

@ -8,6 +8,7 @@
import { generateImageAsync, subscribeToGenerationUpdates } from '$lib/api/generate-async';
import Button from '../ui/Button.svelte';
import Card from '../ui/Card.svelte';
import { XCircle, Lightning } from '@manacore/shared-icons';
let prompt = $state('');
let negativePrompt = $state('');
@ -26,7 +27,7 @@
const data = await getActiveModels();
models.set(data);
// Select default model
const defaultModel = data.find((m) => m.is_default) || data[0];
const defaultModel = data.find((m) => m.isDefault) || data[0];
if (defaultModel) {
selectedModelId = defaultModel.id;
selectedModel.set(defaultModel);
@ -55,8 +56,8 @@
// Start generation with new async API
const { generationId } = await generateImageAsync({
prompt: prompt.trim(),
model_id: selectedModelId,
negative_prompt: negativePrompt.trim() || undefined,
modelId: selectedModelId,
negativePrompt: negativePrompt.trim() || undefined,
});
// Wait for completion using realtime subscription
@ -113,13 +114,7 @@
<div class="rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
<XCircle size={20} weight="fill" class="text-red-400" />
</div>
<div class="ml-3">
<p class="text-sm text-red-800">{$generationError}</p>
@ -159,7 +154,7 @@
{#each $models as model}
<option value={model.id}>
{model.name}
{model.is_default ? '(Default)' : ''}
{model.isDefault ? '(Default)' : ''}
</option>
{/each}
{/if}
@ -223,14 +218,7 @@
disabled={!canGenerate}
loading={$isGenerating}
>
<svg class="mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<Lightning size={20} class="mr-2" />
{$isGenerating ? 'Generating...' : 'Generate Image'}
</Button>
</div>

View file

@ -2,6 +2,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { List, X, CaretDown } from '@manacore/shared-icons';
let showUserMenu = $state(false);
let showMobileMenu = $state(false);
@ -30,14 +31,11 @@
class="flex items-center justify-center rounded-lg p-2 text-gray-700 hover:bg-gray-100 md:hidden dark:text-gray-300 dark:hover:bg-gray-800"
aria-label="Menu"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={showMobileMenu ? 'M6 18L18 6M6 6l12 12' : 'M4 6h16M4 12h16M4 18h16'}
/>
</svg>
{#if showMobileMenu}
<X size={24} weight="bold" />
{:else}
<List size={24} weight="bold" />
{/if}
</button>
<!-- Desktop Navigation -->
@ -87,14 +85,7 @@
>
{authStore.user?.email?.charAt(0).toUpperCase()}
</div>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
<CaretDown size={16} />
</button>
{#if showUserMenu}

View file

@ -2,7 +2,6 @@
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { currentTheme } from '$lib/stores/theme';
import { viewMode, cycleViewMode, type ViewMode } from '$lib/stores/view';
import { isSidebarCollapsed, setSidebarCollapsed } from '$lib/stores/sidebar';
import {
@ -19,6 +18,24 @@
import { searchPublicImages, getPublicImages } from '$lib/api/explore';
import { showKeyboardShortcuts } from '$lib/stores/ui';
import TagPills from '$lib/components/tags/TagPills.svelte';
import {
List,
Image,
SquaresFour,
Square,
MagnifyingGlass,
Lightning,
CloudArrowUp,
Tag,
Archive,
CurrencyCircleDollar,
Question,
CaretLeft,
CaretDown,
User,
SignOut,
Heart,
} from '@manacore/shared-icons';
let showUserMenu = $state(false);
let searchInput = $state('');
@ -33,16 +50,15 @@
return $page.url.pathname === path;
}
function getViewModeIcon(mode: ViewMode) {
switch (mode) {
case 'single':
return 'M4 6h16M4 12h16M4 18h16';
case 'grid3':
return 'M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z';
case 'grid5':
return 'M4 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM10 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM16 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5z';
}
}
type IconName =
| 'gallery'
| 'board'
| 'explore'
| 'generate'
| 'upload'
| 'tags'
| 'archive'
| 'subscription';
function handleSearchInput(e: Event) {
const target = e.target as HTMLInputElement;
@ -103,50 +119,18 @@
interface NavItem {
path: string;
label: string;
icon: string;
iconName: IconName;
}
const navItems: NavItem[] = [
{
path: '/app/gallery',
label: 'Galerie',
icon: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z',
},
{
path: '/app/board',
label: 'Moodboards',
icon: 'M4 5a1 1 0 011-1h4a1 1 0 011 1v7a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v7a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 16a1 1 0 011-1h4a1 1 0 011 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-3zM14 16a1 1 0 011-1h4a1 1 0 011 1v3a1 1 0 01-1 1h-4a1 1 0 01-1-1v-3z',
},
{
path: '/app/explore',
label: 'Entdecken',
icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
},
{
path: '/app/generate',
label: 'Generieren',
icon: 'M13 10V3L4 14h7v7l9-11h-7z',
},
{
path: '/app/upload',
label: 'Upload',
icon: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12',
},
{
path: '/app/tags',
label: 'Tags',
icon: 'M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z',
},
{
path: '/app/archive',
label: 'Archiv',
icon: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4',
},
{
path: '/app/subscription',
label: 'Abonnement',
icon: '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',
},
{ path: '/app/gallery', label: 'Galerie', iconName: 'gallery' },
{ path: '/app/board', label: 'Moodboards', iconName: 'board' },
{ path: '/app/explore', label: 'Entdecken', iconName: 'explore' },
{ path: '/app/generate', label: 'Generieren', iconName: 'generate' },
{ path: '/app/upload', label: 'Upload', iconName: 'upload' },
{ path: '/app/tags', label: 'Tags', iconName: 'tags' },
{ path: '/app/archive', label: 'Archiv', iconName: 'archive' },
{ path: '/app/subscription', label: 'Abonnement', iconName: 'subscription' },
];
</script>
@ -157,14 +141,7 @@
class:-translate-x-[calc(100%+2rem)]={!$isSidebarCollapsed}
aria-label="Sidebar öffnen"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<List size={24} weight="bold" />
</button>
<!-- Sidebar for Desktop -->
@ -182,9 +159,7 @@
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-400 backdrop-blur-xl transition-colors hover:bg-gray-100/80 hover:text-gray-600 dark:hover:bg-gray-800/80 dark:hover:text-gray-300"
aria-label="Sidebar schließen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<CaretLeft size={20} weight="bold" />
</button>
</div>
@ -198,16 +173,29 @@
? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}"
>
<svg
class="h-5 w-5 {active
<span
class="{active
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-400 group-hover:text-gray-600 dark:text-gray-500 dark:group-hover:text-gray-300'}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={item.icon} />
</svg>
{#if item.iconName === 'gallery'}
<Image size={20} />
{:else if item.iconName === 'board'}
<SquaresFour size={20} />
{:else if item.iconName === 'explore'}
<MagnifyingGlass size={20} />
{:else if item.iconName === 'generate'}
<Lightning size={20} />
{:else if item.iconName === 'upload'}
<CloudArrowUp size={20} />
{:else if item.iconName === 'tags'}
<Tag size={20} />
{:else if item.iconName === 'archive'}
<Archive size={20} />
{:else if item.iconName === 'subscription'}
<CurrencyCircleDollar size={20} />
{/if}
</span>
<span>{item.label}</span>
</a>
{/each}
@ -220,19 +208,9 @@
onclick={() => showKeyboardShortcuts.set(true)}
class="group flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 transition-all hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
>
<svg
class="h-5 w-5 text-gray-400 group-hover:text-gray-600 dark:text-gray-500 dark:group-hover:text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span class="text-gray-400 group-hover:text-gray-600 dark:text-gray-500 dark:group-hover:text-gray-300">
<Question size={20} />
</span>
<span>Tastaturkürzel</span>
</button>
@ -255,9 +233,7 @@
: 'text-gray-400 hover:bg-gray-100/80 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-800/80 dark:hover:text-gray-300'}"
title="Liste"
>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M4 6h16M4 12h16M4 18h16" />
</svg>
<List size={20} weight="bold" />
</button>
<button
onclick={() => viewMode.set('grid3')}
@ -267,11 +243,7 @@
: 'text-gray-400 hover:bg-gray-100/80 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-800/80 dark:hover:text-gray-300'}"
title="Mittel"
>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
<path
d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"
/>
</svg>
<SquaresFour size={20} weight="bold" />
</button>
<button
onclick={() => viewMode.set('grid5')}
@ -281,11 +253,7 @@
: 'text-gray-400 hover:bg-gray-100/80 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-800/80 dark:hover:text-gray-300'}"
title="Klein"
>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
<path
d="M4 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM10 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM16 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM4 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2zM10 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2zM16 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2z"
/>
</svg>
<Square size={20} weight="bold" />
</button>
</div>
</div>
@ -319,19 +287,7 @@
? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'}"
>
<svg
class="h-4 w-4"
fill={$showExploreFavoritesOnly ? 'currentColor' : 'none'}
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
<Heart size={16} weight={$showExploreFavoritesOnly ? 'fill' : 'regular'} />
<span>Favoriten</span>
</button>
</div>
@ -350,19 +306,9 @@
placeholder="Prompts..."
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pl-9 text-sm text-gray-900 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400"
/>
<svg
class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500">
<MagnifyingGlass size={16} />
</span>
</div>
</div>
@ -417,19 +363,7 @@
? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'}"
>
<svg
class="h-4 w-4"
fill={$showFavoritesOnly ? 'currentColor' : 'none'}
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
<Heart size={16} weight={$showFavoritesOnly ? 'fill' : 'regular'} />
<span>Favoriten</span>
</button>
@ -466,7 +400,7 @@
>
<div
class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full text-sm font-semibold text-white"
style="background-color: {$currentTheme.primary.default};"
style="background-color: hsl(var(--color-primary));"
>
{authStore.user?.email?.charAt(0).toUpperCase()}
</div>
@ -476,19 +410,9 @@
</p>
<p class="truncate text-xs text-gray-500 dark:text-gray-400">Account</p>
</div>
<svg
class="h-4 w-4 text-gray-400 transition-transform {showUserMenu ? 'rotate-180' : ''}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
<span class="text-gray-400 transition-transform {showUserMenu ? 'rotate-180' : ''}">
<CaretDown size={16} />
</span>
</button>
{#if showUserMenu}
@ -500,28 +424,14 @@
onclick={() => (showUserMenu = false)}
class="flex items-center gap-3 px-4 py-3 text-sm text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<User size={16} />
Profil & Einstellungen
</a>
<button
onclick={handleLogout}
class="flex w-full items-center gap-3 border-t border-gray-200 px-4 py-3 text-left text-sm text-red-600 transition-colors hover:bg-gray-100 dark:border-gray-700 dark:text-red-400 dark:hover:bg-gray-700"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<SignOut size={16} />
Abmelden
</button>
</div>
@ -542,7 +452,7 @@
<button
onclick={() => (showUserMenu = !showUserMenu)}
class="flex h-9 w-9 items-center justify-center rounded-full text-sm font-semibold text-white"
style="background-color: {$currentTheme.primary.default};"
style="background-color: hsl(var(--color-primary));"
>
{authStore.user?.email?.charAt(0).toUpperCase()}
</button>
@ -561,14 +471,25 @@
? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400'
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800'}"
>
<svg
class="h-5 w-5 {active ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400'}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={item.icon} />
</svg>
<span class="{active ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400'}">
{#if item.iconName === 'gallery'}
<Image size={20} />
{:else if item.iconName === 'board'}
<SquaresFour size={20} />
{:else if item.iconName === 'explore'}
<MagnifyingGlass size={20} />
{:else if item.iconName === 'generate'}
<Lightning size={20} />
{:else if item.iconName === 'upload'}
<CloudArrowUp size={20} />
{:else if item.iconName === 'tags'}
<Tag size={20} />
{:else if item.iconName === 'archive'}
<Archive size={20} />
{:else if item.iconName === 'subscription'}
<CurrencyCircleDollar size={20} />
{/if}
</span>
{item.label}
</a>
{/each}
@ -577,28 +498,16 @@
onclick={() => (showUserMenu = false)}
class="flex items-center gap-3 border-b border-gray-100 px-4 py-3 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
>
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<span class="text-gray-400">
<User size={20} />
</span>
Profil & Einstellungen
</a>
<button
onclick={handleLogout}
class="flex w-full items-center gap-3 px-4 py-3 text-left text-sm font-medium text-red-600 transition-colors hover:bg-gray-50 dark:text-red-400 dark:hover:bg-gray-800"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<SignOut size={20} />
Abmelden
</button>
</nav>
@ -619,9 +528,23 @@
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'}"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={item.icon} />
</svg>
{#if item.iconName === 'gallery'}
<Image size={24} />
{:else if item.iconName === 'board'}
<SquaresFour size={24} />
{:else if item.iconName === 'explore'}
<MagnifyingGlass size={24} />
{:else if item.iconName === 'generate'}
<Lightning size={24} />
{:else if item.iconName === 'upload'}
<CloudArrowUp size={24} />
{:else if item.iconName === 'tags'}
<Tag size={24} />
{:else if item.iconName === 'archive'}
<Archive size={24} />
{:else if item.iconName === 'subscription'}
<CurrencyCircleDollar size={24} />
{/if}
<span class="text-xs font-medium">{item.label}</span>
</a>
{/each}

View file

@ -1,13 +1,8 @@
<script lang="ts">
import {
themeVariant,
themeMode,
setThemeVariant,
setThemeMode,
currentTheme,
} from '$lib/stores/theme';
import { themes, type ThemeVariant } from '@picture/design-tokens';
import type { ThemeMode } from '$lib/stores/theme';
import { theme } from '$lib/stores/theme';
import type { ThemeVariant, ThemeMode } from '@manacore/shared-theme';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { CheckCircle, Info } from '@manacore/shared-icons';
interface ThemeOption {
value: ThemeVariant;
@ -22,8 +17,9 @@
}
const themeOptions: ThemeOption[] = [
{ value: 'default', label: 'Indigo', icon: '🔵' },
{ value: 'sunset', label: 'Sunset', icon: '🌅' },
{ value: 'lume', label: 'Lume', icon: '✨' },
{ value: 'nature', label: 'Nature', icon: '🌿' },
{ value: 'stone', label: 'Stone', icon: '🪨' },
{ value: 'ocean', label: 'Ocean', icon: '🌊' },
];
@ -32,18 +28,26 @@
{ value: 'light', label: 'Hell', icon: '☀️' },
{ value: 'dark', label: 'Dunkel', icon: '🌙' },
];
/**
* Get the primary color for a variant based on current effective mode
*/
function getVariantColor(variant: ThemeVariant): string {
const definition = THEME_DEFINITIONS[variant];
const colors = theme.effectiveMode === 'dark' ? definition.dark : definition.light;
return `hsl(${colors.primary})`;
}
</script>
<div class="rounded-2xl bg-white p-6 shadow-sm dark:bg-gray-900">
<!-- Theme Variant Selection -->
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100">Theme</h3>
<div class="grid grid-cols-3 gap-3">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
{#each themeOptions as option}
{@const isSelected = $themeVariant === option.value}
{@const themePreview = themes[option.value]}
{@const isSelected = theme.variant === option.value}
<button
onclick={() => setThemeVariant(option.value)}
onclick={() => theme.setVariant(option.value)}
class="relative rounded-xl border-2 p-4 transition-all hover:scale-105 {isSelected
? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-950'
: 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800'}"
@ -61,31 +65,17 @@
</div>
<!-- Color Preview -->
<div class="flex justify-center gap-1">
<div class="flex justify-center">
<div
class="h-5 w-5 rounded-full"
style="background-color: {themePreview.colors.dark.primary.default}"
></div>
<div
class="h-5 w-5 rounded-full"
style="background-color: {themePreview.colors.dark.secondary.default}"
style="background-color: {getVariantColor(option.value)}"
></div>
</div>
<!-- Checkmark -->
{#if isSelected}
<div class="absolute right-2 top-2">
<svg
class="h-6 w-6 text-blue-600 dark:text-blue-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
<CheckCircle size={24} weight="fill" class="text-blue-600 dark:text-blue-400" />
</div>
{/if}
</button>
@ -98,9 +88,9 @@
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100">Modus</h3>
<div class="grid grid-cols-3 gap-3">
{#each modeOptions as option}
{@const isSelected = $themeMode === option.value}
{@const isSelected = theme.mode === option.value}
<button
onclick={() => setThemeMode(option.value)}
onclick={() => theme.setMode(option.value)}
class="flex flex-col items-center justify-center gap-2 rounded-xl border px-4 py-3 transition-all {isSelected
? 'border-blue-500 bg-blue-500 text-white'
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200'}"
@ -112,23 +102,11 @@
</div>
<!-- System Mode Info -->
{#if $themeMode === 'system'}
{#if theme.mode === 'system'}
<div
class="mt-4 flex items-start gap-2 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-900 dark:bg-blue-950"
>
<svg
class="mt-0.5 h-5 w-5 flex-shrink-0 text-blue-500 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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>
<Info size={20} class="mt-0.5 flex-shrink-0 text-blue-500 dark:text-blue-400" />
<p class="text-sm text-blue-700 dark:text-blue-300">
Das Theme folgt den Systemeinstellungen deines Geräts
</p>

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { tags, selectedTags } from '$lib/stores/tags';
import type { Database } from '@picture/shared/types';
import { Check } from '@manacore/shared-icons';
type Tag = Database['public']['Tables']['tags']['Row'];
@ -38,14 +39,7 @@
{/if}
<span>{tag.name}</span>
{#if selected}
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="M5 13l4 4L19 7"
/>
</svg>
<Check size={14} weight="bold" />
{/if}
</button>
{/each}

View file

@ -1,5 +1,6 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { CircleNotch } from '@manacore/shared-icons';
interface Props {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
@ -45,20 +46,7 @@
<button {type} class={buttonClass} disabled={disabled || loading} {onclick}>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<CircleNotch size={16} weight="bold" class="mr-2 animate-spin" />
{/if}
{@render children()}
</button>

View file

@ -14,11 +14,22 @@
import { archivedImages } from '$lib/stores/archive';
import { showToast } from '$lib/stores/toast';
import type { Database } from '@picture/shared/types';
import {
DownloadSimple,
Link,
Heart,
Tag as TagIcon,
Archive,
ArrowCounterClockwise,
Trash,
CaretRight,
Check,
} from '@manacore/shared-icons';
type Tag = Database['public']['Tables']['tags']['Row'];
type TagType = Database['public']['Tables']['tags']['Row'];
let tagSubmenuElement = $state<HTMLElement | null>(null);
let imageTags = $state<Tag[]>([]);
let imageTags = $state<TagType[]>([]);
// Check if current image is archived
const isArchived = $derived(
@ -31,9 +42,11 @@
// Check if current image belongs to current user
const isOwnImage = $derived($contextMenu.image?.user_id === authStore.user?.id);
type IconName = 'download' | 'link' | 'heart' | 'tag' | 'archive' | 'restore' | 'trash';
interface MenuItem {
label: string;
icon: string;
iconName: IconName;
action: () => void;
submenu?: boolean;
divider?: boolean;
@ -59,7 +72,7 @@
showTagSubmenu(rect.right, rect.top);
}
async function handleAddTag(tag: Tag) {
async function handleAddTag(tag: TagType) {
if (!$contextMenu.image) return;
try {
@ -72,7 +85,7 @@
}
}
async function handleRemoveTag(tag: Tag) {
async function handleRemoveTag(tag: TagType) {
if (!$contextMenu.image) return;
try {
@ -181,23 +194,23 @@
const menuItems = $derived([
{
label: 'Herunterladen',
icon: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4',
iconName: 'download' as IconName,
action: handleDownload,
},
{
label: 'Link kopieren',
icon: 'M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3',
iconName: 'link' as IconName,
action: handleCopyLink,
},
{
label: isFavorite ? 'Aus Favoriten entfernen' : 'Zu Favoriten',
icon: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z',
iconName: 'heart' as IconName,
action: handleToggleFavorite,
filled: isFavorite,
},
{
label: 'Tags',
icon: 'M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z',
iconName: 'tag' as IconName,
action: () => {},
submenu: true,
divider: true,
@ -207,9 +220,7 @@
? [
{
label: isArchived ? 'Wiederherstellen' : 'Archivieren',
icon: isArchived
? 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15'
: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4',
iconName: (isArchived ? 'restore' : 'archive') as IconName,
action: handleArchive,
divider: true,
},
@ -220,7 +231,7 @@
? [
{
label: 'Löschen',
icon: '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',
iconName: 'trash' as IconName,
action: handleDelete,
},
]
@ -278,24 +289,26 @@
: ''}"
role="menuitem"
>
<svg
class="h-4 w-4 flex-shrink-0"
fill={item.filled ? 'currentColor' : 'none'}
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={item.icon} />
</svg>
<span class="flex-shrink-0">
{#if item.iconName === 'download'}
<DownloadSimple size={16} weight={item.filled ? 'fill' : 'regular'} />
{:else if item.iconName === 'link'}
<Link size={16} weight={item.filled ? 'fill' : 'regular'} />
{:else if item.iconName === 'heart'}
<Heart size={16} weight={item.filled ? 'fill' : 'regular'} />
{:else if item.iconName === 'tag'}
<TagIcon size={16} weight={item.filled ? 'fill' : 'regular'} />
{:else if item.iconName === 'archive'}
<Archive size={16} weight={item.filled ? 'fill' : 'regular'} />
{:else if item.iconName === 'restore'}
<ArrowCounterClockwise size={16} weight={item.filled ? 'fill' : 'regular'} />
{:else if item.iconName === 'trash'}
<Trash size={16} weight={item.filled ? 'fill' : 'regular'} />
{/if}
</span>
<span class="flex-1">{item.label}</span>
{#if item.submenu}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<CaretRight size={16} />
{/if}
</button>
{/each}
@ -339,19 +352,7 @@
></div>
<span class="flex-1 text-gray-700 dark:text-gray-300">{tag.name}</span>
{#if hasTag}
<svg
class="h-4 w-4 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="M5 13l4 4L19 7"
/>
</svg>
<Check size={16} weight="bold" class="text-blue-600 dark:text-blue-400" />
{/if}
</button>
{/each}

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { showKeyboardShortcuts } from '$lib/stores/ui';
import { X } from '@manacore/shared-icons';
interface Shortcut {
key: string;
@ -56,14 +57,7 @@
class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-100/80 text-gray-600 backdrop-blur-xl transition-all hover:bg-gray-200/80 dark:bg-gray-800/80 dark:text-gray-400 dark:hover:bg-gray-700/80"
aria-label="Schließen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={20} weight="bold" />
</button>
</div>

View file

@ -1,6 +1,7 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { onMount } from 'svelte';
import { X } from '@manacore/shared-icons';
interface Props {
open: boolean;
@ -47,6 +48,12 @@
onClose();
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
</script>
{#if open}
@ -54,7 +61,7 @@
bind:this={modalElement}
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 dark:bg-black/70"
onclick={handleBackdropClick}
onkeydown={(e) => e.key === 'Enter' && handleBackdropClick(e)}
onkeydown={handleKeydown}
role="dialog"
aria-modal="true"
tabindex="-1"
@ -69,14 +76,7 @@
class="absolute right-4 top-4 z-10 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black/50 dark:bg-gray-800 dark:hover:bg-gray-700"
aria-label="Close"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={24} weight="bold" />
</button>
{@render children()}
</div>

View file

@ -1,32 +1,7 @@
<script lang="ts">
import { toasts, dismissToast, type Toast } from '$lib/stores/toast';
import { fly, fade } from 'svelte/transition';
function getToastIcon(type: Toast['type']) {
switch (type) {
case 'success':
return {
path: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
color: 'text-green-500',
};
case 'error':
return {
path: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z',
color: 'text-red-500',
};
case 'warning':
return {
path: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
color: 'text-yellow-500',
};
case 'info':
default:
return {
path: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
color: 'text-blue-500',
};
}
}
import { CheckCircle, XCircle, Warning, Info, X } from '@manacore/shared-icons';
function getToastBgColor(type: Toast['type']) {
switch (type) {
@ -45,21 +20,23 @@
<div class="pointer-events-none fixed bottom-4 right-4 z-[100] flex flex-col gap-2">
{#each $toasts as toast (toast.id)}
{@const icon = getToastIcon(toast.type)}
{@const bgColor = getToastBgColor(toast.type)}
<div
transition:fly={{ y: 50, duration: 300 }}
class="pointer-events-auto flex min-w-[320px] items-start gap-3 rounded-lg border p-4 shadow-lg {bgColor}"
role="alert"
>
<svg
class="h-6 w-6 flex-shrink-0 {icon.color}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={icon.path} />
</svg>
<span class="flex-shrink-0">
{#if toast.type === 'success'}
<CheckCircle size={24} weight="regular" class="text-green-500" />
{:else if toast.type === 'error'}
<XCircle size={24} weight="regular" class="text-red-500" />
{:else if toast.type === 'warning'}
<Warning size={24} weight="regular" class="text-yellow-500" />
{:else}
<Info size={24} weight="regular" class="text-blue-500" />
{/if}
</span>
<p class="flex-1 text-sm font-medium text-gray-900">{toast.message}</p>
@ -68,14 +45,7 @@
class="flex-shrink-0 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
aria-label="Schließen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={20} weight="bold" />
</button>
</div>
{/each}

View file

@ -1,16 +1,6 @@
<script lang="ts">
import { viewMode, cycleViewMode, type ViewMode } from '$lib/stores/view';
function getIcon(mode: ViewMode) {
switch (mode) {
case 'single':
return 'M4 6h16M4 12h16M4 18h16'; // List icon
case 'grid3':
return 'M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z'; // 2x2 grid
case 'grid5':
return 'M4 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM10 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM16 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM4 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2zM10 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2zM16 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2z'; // 3x2 grid
}
}
import { List, SquaresFour, Squares } from '@manacore/shared-icons';
function getLabel(mode: ViewMode) {
switch (mode) {
@ -29,8 +19,12 @@
class="flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
title="Ansicht wechseln ({getLabel($viewMode)})"
>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
<path d={getIcon($viewMode)} />
</svg>
{#if $viewMode === 'single'}
<List size={20} />
{:else if $viewMode === 'grid3'}
<SquaresFour size={20} />
{:else}
<Squares size={20} />
{/if}
<span class="hidden sm:inline">{getLabel($viewMode)}</span>
</button>

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { validateImage } from '$lib/api/upload';
import type { UploadProgress } from '$lib/api/upload';
import { CloudArrowUp, X } from '@manacore/shared-icons';
interface Props {
onFilesSelected: (files: File[]) => void;
@ -104,19 +105,11 @@
role="button"
tabindex="0"
>
<svg
class="mb-4 h-16 w-16 {isDragging ? 'text-blue-500' : 'text-gray-400 dark:text-gray-600'}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<CloudArrowUp
size={64}
weight="regular"
class="mb-4 {isDragging ? 'text-blue-500' : 'text-gray-400 dark:text-gray-600'}"
/>
<h3 class="mb-2 text-xl font-semibold text-gray-900 dark:text-white">
{isDragging ? 'Loslassen zum Hochladen' : 'Bilder hochladen'}
@ -178,14 +171,7 @@
onclick={() => removeFile(index)}
class="absolute right-2 top-2 flex h-8 w-8 items-center justify-center rounded-full bg-white/90 text-gray-900 opacity-0 transition-opacity hover:bg-white group-hover:opacity-100"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={16} weight="bold" />
</button>
{/if}
@ -255,14 +241,7 @@
onclick={handleUpload}
class="flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 text-base font-medium text-white transition-all hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<CloudArrowUp size={20} weight="bold" />
{selectedFiles.length}
{selectedFiles.length === 1 ? 'Bild' : 'Bilder'} hochladen
</button>

View file

@ -1,7 +1,5 @@
import { writable } from 'svelte/store';
import type { Database } from '@picture/shared/types';
type Image = Database['public']['Tables']['images']['Row'];
import type { Image } from '$lib/api/images';
export const archivedImages = writable<Image[]>([]);
export const isLoadingArchive = writable(false);

View file

@ -1,8 +1,5 @@
import { writable, derived } from 'svelte/store';
import type { Database } from '@picture/shared/types';
import type { BoardWithCount } from '$lib/api/boards';
type Board = Database['public']['Tables']['boards']['Row'];
import type { Board, BoardWithCount } from '$lib/api/boards';
// Current boards list
export const boards = writable<BoardWithCount[]>([]);
@ -30,9 +27,9 @@ export const shareBoardId = writable<string | null>(null);
// Board settings (for canvas)
export const boardSettings = derived(currentBoard, ($currentBoard) => ({
width: $currentBoard?.canvas_width || 2000,
height: $currentBoard?.canvas_height || 1500,
backgroundColor: $currentBoard?.background_color || '#ffffff',
width: $currentBoard?.canvasWidth || 2000,
height: $currentBoard?.canvasHeight || 1500,
backgroundColor: $currentBoard?.backgroundColor || '#ffffff',
}));
// Helper functions for board management
@ -59,7 +56,7 @@ export function removeBoardFromList(boardId: string) {
export function incrementBoardItemCount(boardId: string) {
boards.update((current) =>
current.map((board) =>
board.id === boardId ? { ...board, item_count: board.item_count + 1 } : board
board.id === boardId ? { ...board, itemCount: board.itemCount + 1 } : board
)
);
}
@ -67,7 +64,7 @@ export function incrementBoardItemCount(boardId: string) {
export function decrementBoardItemCount(boardId: string) {
boards.update((current) =>
current.map((board) =>
board.id === boardId ? { ...board, item_count: Math.max(0, board.item_count - 1) } : board
board.id === boardId ? { ...board, itemCount: Math.max(0, board.itemCount - 1) } : board
)
);
}

View file

@ -155,7 +155,9 @@ export const canvasStore = {
},
updateItem(id: string, updates: Partial<BoardItem>) {
canvasItems = canvasItems.map((item) => (item.id === id ? { ...item, ...updates } : item));
canvasItems = canvasItems.map((item) =>
item.id === id ? ({ ...item, ...updates } as BoardItem) : item
);
saveToHistory();
},

View file

@ -86,7 +86,7 @@ export function addCanvasItem(item: BoardItem) {
export function updateCanvasItem(id: string, updates: Partial<BoardItem>) {
canvasItems.update((items) =>
items.map((item) => (item.id === id ? { ...item, ...updates } : item))
items.map((item) => (item.id === id ? ({ ...item, ...updates } as BoardItem) : item))
);
saveToHistory();
}
@ -137,14 +137,14 @@ export function deselectAll() {
export function bringToFront(id: string) {
const items = get(canvasItems);
const maxZIndex = Math.max(...items.map((item) => item.z_index));
updateCanvasItem(id, { z_index: maxZIndex + 1 });
const maxZIndex = Math.max(...items.map((item) => item.zIndex));
updateCanvasItem(id, { zIndex: maxZIndex + 1 });
}
export function sendToBack(id: string) {
const items = get(canvasItems);
const minZIndex = Math.min(...items.map((item) => item.z_index));
updateCanvasItem(id, { z_index: minZIndex - 1 });
const minZIndex = Math.min(...items.map((item) => item.zIndex));
updateCanvasItem(id, { zIndex: minZIndex - 1 });
}
export function moveForward(id: string) {
@ -152,11 +152,11 @@ export function moveForward(id: string) {
const item = items.find((i) => i.id === id);
if (!item) return;
const itemsAbove = items.filter((i) => i.z_index > item.z_index);
const itemsAbove = items.filter((i) => i.zIndex > item.zIndex);
if (itemsAbove.length === 0) return;
const nextZIndex = Math.min(...itemsAbove.map((i) => i.z_index));
updateCanvasItem(id, { z_index: nextZIndex + 0.5 });
const nextZIndex = Math.min(...itemsAbove.map((i) => i.zIndex));
updateCanvasItem(id, { zIndex: nextZIndex + 0.5 });
}
export function moveBackward(id: string) {
@ -164,11 +164,11 @@ export function moveBackward(id: string) {
const item = items.find((i) => i.id === id);
if (!item) return;
const itemsBelow = items.filter((i) => i.z_index < item.z_index);
const itemsBelow = items.filter((i) => i.zIndex < item.zIndex);
if (itemsBelow.length === 0) return;
const prevZIndex = Math.max(...itemsBelow.map((i) => i.z_index));
updateCanvasItem(id, { z_index: prevZIndex - 0.5 });
const prevZIndex = Math.max(...itemsBelow.map((i) => i.zIndex));
updateCanvasItem(id, { zIndex: prevZIndex - 0.5 });
}
// Zoom functions

View file

@ -68,7 +68,7 @@ export const contextMenuStore = {
contextMenuState = { ...initialState };
},
showTagSubmenu(x: number, y: number) {
openTagSubmenu(x: number, y: number) {
contextMenuState = {
...contextMenuState,
showTagSubmenu: true,
@ -95,7 +95,7 @@ export function hideContextMenu() {
}
export function showTagSubmenu(x: number, y: number) {
contextMenuStore.showTagSubmenu(x, y);
contextMenuStore.openTagSubmenu(x, y);
}
export function hideTagSubmenu() {

View file

@ -1,7 +1,5 @@
import { writable } from 'svelte/store';
import type { Database } from '@picture/shared/types';
type Image = Database['public']['Tables']['images']['Row'];
import type { Image } from '$lib/api/images';
interface ContextMenuState {
visible: boolean;

View file

@ -1,7 +1,5 @@
import { writable } from 'svelte/store';
import type { Database } from '@picture/shared/types';
type Image = Database['public']['Tables']['images']['Row'];
import type { Image } from '$lib/api/images';
export const exploreImages = writable<Image[]>([]);
export const isLoadingExplore = writable(false);

View file

@ -1,7 +1,5 @@
import { writable } from 'svelte/store';
import type { Database } from '@picture/shared/types';
type Image = Database['public']['Tables']['images']['Row'];
import type { Image } from '$lib/api/images';
export const images = writable<Image[]>([]);
export const selectedImage = writable<Image | null>(null);

View file

@ -1,7 +1,5 @@
import { writable } from 'svelte/store';
import type { Database } from '@picture/shared/types';
type Model = Database['public']['Tables']['models']['Row'];
import type { Model } from '$lib/api/models';
export const models = writable<Model[]>([]);
export const selectedModel = writable<Model | null>(null);

View file

@ -1,7 +1,5 @@
import { writable } from 'svelte/store';
import type { Database } from '@picture/shared/types';
type Tag = Database['public']['Tables']['tags']['Row'];
import type { Tag } from '$lib/api/tags';
export const tags = writable<Tag[]>([]);
export const selectedTags = writable<string[]>([]);

View file

@ -1,122 +1,23 @@
import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';
import { themes, type ThemeVariant } from '@picture/design-tokens';
import { createThemeStore, APP_THEME_CONFIGS } from '@manacore/shared-theme';
export type ThemeMode = 'light' | 'dark' | 'system';
/**
* Picture theme store using the global shared theme system
*
* Usage:
* - theme.mode: 'light' | 'dark' | 'system'
* - theme.variant: 'lume' | 'nature' | 'stone' | 'ocean'
* - theme.effectiveMode: 'light' | 'dark' (resolved from system if needed)
* - theme.isDark: boolean
* - theme.setMode(mode): Set theme mode
* - theme.setVariant(variant): Set theme variant
* - theme.toggleMode(): Toggle between light/dark
* - theme.cycleMode(): Cycle through light dark system
* - theme.initialize(): Initialize and apply theme (call in onMount)
*
* CSS Variables applied automatically:
* --color-primary, --color-background, --color-foreground, etc.
*/
export const theme = createThemeStore(APP_THEME_CONFIGS.picture);
interface ThemeState {
variant: ThemeVariant;
mode: ThemeMode;
}
const THEME_VARIANT_KEY = 'picture_theme_variant';
const THEME_MODE_KEY = 'picture_theme_mode';
// Load initial values from localStorage
function loadInitialTheme(): ThemeState {
if (!browser) {
return { variant: 'default', mode: 'system' };
}
const savedVariant = localStorage.getItem(THEME_VARIANT_KEY) as ThemeVariant | null;
const savedMode = localStorage.getItem(THEME_MODE_KEY) as ThemeMode | null;
return {
variant: savedVariant || 'default',
mode: savedMode || 'system',
};
}
// Create stores with initial values
const initialTheme = loadInitialTheme();
export const themeVariant = writable<ThemeVariant>(initialTheme.variant);
export const themeMode = writable<ThemeMode>(initialTheme.mode);
// Derive the actual mode (resolve 'system' to 'light' or 'dark')
export const actualMode = derived(themeMode, ($mode) => {
if ($mode === 'system' && browser) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return $mode === 'system' ? 'dark' : $mode;
});
// Derive the current theme object
export const currentTheme = derived([themeVariant, actualMode], ([$variant, $actualMode]) => {
const theme = themes[$variant];
return theme.colors[$actualMode];
});
// Actions
export function setThemeVariant(variant: ThemeVariant) {
themeVariant.set(variant);
if (browser) {
localStorage.setItem(THEME_VARIANT_KEY, variant);
}
}
export function setThemeMode(mode: ThemeMode) {
themeMode.set(mode);
if (browser) {
localStorage.setItem(THEME_MODE_KEY, mode);
}
}
export function toggleThemeMode() {
themeMode.update((current) => {
const newMode = current === 'dark' ? 'light' : 'dark';
if (browser) {
localStorage.setItem(THEME_MODE_KEY, newMode);
}
return newMode;
});
}
// Listen to system theme changes and apply theme to DOM
if (browser) {
// Listen to system color scheme changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => {
// Force re-evaluation of actualMode when system preference changes
themeMode.update((mode) => mode);
});
// Apply CSS custom properties and background colors
currentTheme.subscribe((theme) => {
const root = document.documentElement;
// Primary colors
root.style.setProperty('--color-primary', theme.primary.default);
root.style.setProperty('--color-primary-hover', theme.primary.hover);
root.style.setProperty('--color-primary-active', theme.primary.active);
// Background colors
root.style.setProperty('--color-background', theme.background);
root.style.setProperty('--color-surface', theme.surface);
root.style.setProperty('--color-elevated', theme.elevated);
// Text colors
root.style.setProperty('--color-text-primary', theme.text.primary);
root.style.setProperty('--color-text-secondary', theme.text.secondary);
root.style.setProperty('--color-text-tertiary', theme.text.tertiary);
// Border colors
root.style.setProperty('--color-border', theme.border);
root.style.setProperty('--color-divider', theme.divider);
// Status colors
root.style.setProperty('--color-success', theme.success);
root.style.setProperty('--color-error', theme.error);
root.style.setProperty('--color-warning', theme.warning);
root.style.setProperty('--color-info', theme.info);
// Apply background color to body
document.body.style.backgroundColor = theme.background;
document.body.style.color = theme.text.primary;
});
// Apply dark/light mode class to document element
actualMode.subscribe((mode) => {
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(mode);
});
}
// Export theme types for convenience
export type { ThemeMode, ThemeVariant } from '@manacore/shared-theme';

View file

@ -6,8 +6,8 @@
import { onMount } from 'svelte';
import { initPostHog, analytics } from '$lib/analytics/posthog';
// Import theme stores to initialize them
import '$lib/stores/theme';
// Import and initialize theme
import { theme } from '$lib/stores/theme';
// Initialize i18n
import '$lib/i18n';
@ -15,6 +15,9 @@
let { children, data } = $props();
onMount(async () => {
// Initialize theme (applies CSS variables and loads from localStorage)
const cleanupTheme = theme.initialize();
// Initialize PostHog
initPostHog();
@ -27,6 +30,10 @@
email: authStore.user.email,
});
}
return () => {
cleanupTheme();
};
});
</script>

View file

@ -5,7 +5,7 @@
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillNavElement } from '@manacore/shared-ui';
import KeyboardShortcutsModal from '$lib/components/ui/KeyboardShortcutsModal.svelte';
import { currentTheme, actualMode, toggleThemeMode } from '$lib/stores/theme';
import { theme } from '$lib/stores/theme';
import { isUIVisible, toggleUI, showKeyboardShortcuts } from '$lib/stores/ui';
import { viewMode, setViewMode, type ViewMode } from '$lib/stores/view';
import { browser } from '$app/environment';
@ -42,7 +42,7 @@
}
function handleToggleTheme() {
toggleThemeMode();
theme.toggleMode();
}
// Client-side auth check
@ -157,7 +157,7 @@
</div>
</div>
{:else if authStore.user}
<div class="min-h-screen" style="background-color: {$currentTheme.background};">
<div class="min-h-screen" style="background-color: hsl(var(--color-background));">
<!-- PillNavigation (conditionally visible) -->
{#if $isUIVisible}
<PillNavigation
@ -168,7 +168,7 @@
homeRoute="/app/gallery"
onLogout={handleLogout}
onToggleTheme={handleToggleTheme}
isDark={$actualMode === 'dark'}
isDark={theme.isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}

View file

@ -11,6 +11,7 @@
import ArchivedImageModal from '$lib/components/archive/ArchivedImageModal.svelte';
import ImageSkeleton from '$lib/components/ui/ImageSkeleton.svelte';
import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
import { Archive } from '@manacore/shared-icons';
import { onMount } from 'svelte';
import type { Database } from '@picture/shared/types';
@ -100,19 +101,7 @@
{:else if $archivedImages.length === 0}
<div class="flex min-h-[400px] items-center justify-center px-4 py-8">
<div class="text-center">
<svg
class="mx-auto h-16 w-16 text-gray-400 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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>
<Archive size={64} weight="thin" class="mx-auto text-gray-400 dark:text-gray-600" />
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">Kein Archiv</h3>
<p class="mt-2 text-gray-600 dark:text-gray-400">
Archiviere Bilder aus deiner Galerie, um sie organisiert zu halten ohne sie zu löschen

View file

@ -17,6 +17,7 @@
import Button from '$lib/components/ui/Button.svelte';
import Modal from '$lib/components/ui/Modal.svelte';
import { showToast } from '$lib/stores/toast';
import { Plus, SquaresFour, Image, Trash } from '@manacore/shared-icons';
let loadingMore = $state(false);
let observer: IntersectionObserver | null = null;
@ -172,9 +173,7 @@
</p>
</div>
<Button onclick={() => showCreateBoardModal.set(true)}>
<svg class="mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<Plus size={20} class="mr-2" />
Neues Board
</Button>
</div>
@ -193,19 +192,7 @@
{:else if $boards.length === 0}
<!-- Empty State -->
<div class="flex flex-col items-center justify-center py-20">
<svg
class="h-24 w-24 text-gray-300 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M4 5a1 1 0 011-1h4a1 1 0 011 1v7a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v7a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 16a1 1 0 011-1h4a1 1 0 011 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-3zM14 16a1 1 0 011-1h4a1 1 0 011 1v3a1 1 0 01-1 1h-4a1 1 0 01-1-1v-3z"
/>
</svg>
<SquaresFour size={96} weight="thin" class="text-gray-300 dark:text-gray-600" />
<h3 class="mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100">
Keine Boards vorhanden
</h3>
@ -237,19 +224,7 @@
/>
{:else}
<div class="flex h-full items-center justify-center">
<svg
class="h-16 w-16 text-gray-300 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<Image size={64} weight="thin" class="text-gray-300 dark:text-gray-600" />
</div>
{/if}
</button>
@ -285,14 +260,7 @@
Duplizieren
</Button>
<Button size="sm" variant="danger" onclick={() => confirmDelete(board.id)}>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Trash size={16} />
</Button>
</div>
</div>

View file

@ -19,6 +19,7 @@
import CanvasToolbar from '$lib/components/board/CanvasToolbar.svelte';
import ImagePickerModal from '$lib/components/board/ImagePickerModal.svelte';
import ImagePropertiesPanel from '$lib/components/board/ImagePropertiesPanel.svelte';
import { X, SlidersHorizontal } from '@manacore/shared-icons';
const boardId = $derived($page.params.id);
@ -152,25 +153,9 @@
title={$showPropertiesPanel ? 'Eigenschaften ausblenden' : 'Eigenschaften anzeigen'}
>
{#if $showPropertiesPanel}
<!-- Close Icon -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<X size={24} />
{:else}
<!-- Settings/Sliders Icon -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
/>
</svg>
<SlidersHorizontal size={24} />
{/if}
</button>

View file

@ -16,6 +16,7 @@
import ImageSkeleton from '$lib/components/ui/ImageSkeleton.svelte';
import ViewModeSwitcher from '$lib/components/ui/ViewModeSwitcher.svelte';
import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
import { MagnifyingGlass, X, Heart } from '@manacore/shared-icons';
import { onMount } from 'svelte';
import type { Database } from '@picture/shared/types';
import type { ViewMode } from '$lib/stores/view';
@ -179,19 +180,7 @@
placeholder="Bilder suchen..."
class="w-full rounded-full border border-gray-300/50 bg-white/80 px-4 py-2 pl-10 text-sm text-gray-900 placeholder-gray-500 backdrop-blur-xl transition-all focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600/50 dark:bg-gray-800/80 dark:text-gray-100 dark:placeholder-gray-400"
/>
<svg
class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<MagnifyingGlass size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" />
{#if searchInput}
<button
onclick={() => {
@ -201,9 +190,7 @@
}}
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<X size={16} />
</button>
{/if}
</div>
@ -226,19 +213,7 @@
? 'bg-blue-600 text-white'
: 'bg-gray-100/80 text-gray-700 backdrop-blur-xl hover:bg-gray-200/80 dark:bg-gray-800/80 dark:text-gray-300 dark:hover:bg-gray-700/80'}"
>
<svg
class="h-4 w-4"
fill={$showExploreFavoritesOnly ? 'currentColor' : 'none'}
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
<Heart size={16} weight={$showExploreFavoritesOnly ? 'fill' : 'regular'} />
<span>Favoriten</span>
</button>
@ -267,19 +242,7 @@
{:else if $exploreImages.length === 0}
<div class="flex min-h-[400px] items-center justify-center px-4">
<div class="text-center">
<svg
class="mx-auto h-16 w-16 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<MagnifyingGlass size={64} weight="thin" class="mx-auto text-gray-400 dark:text-gray-500" />
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">
Keine Bilder gefunden
</h3>

View file

@ -19,6 +19,7 @@
import ImageSkeleton from '$lib/components/ui/ImageSkeleton.svelte';
import ViewModeSwitcher from '$lib/components/ui/ViewModeSwitcher.svelte';
import TagPills from '$lib/components/tags/TagPills.svelte';
import { Heart } from '@manacore/shared-icons';
import { onMount } from 'svelte';
let loadingMore = $state(false);
@ -130,19 +131,7 @@
? 'bg-blue-600 text-white'
: 'bg-gray-100/80 text-gray-700 backdrop-blur-xl hover:bg-gray-200/80 dark:bg-gray-800/80 dark:text-gray-300 dark:hover:bg-gray-700/80'}"
>
<svg
class="h-4 w-4"
fill={$showFavoritesOnly ? 'currentColor' : 'none'}
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
<Heart size={16} weight={$showFavoritesOnly ? 'fill' : 'regular'} />
<span>Favoriten</span>
</button>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import GenerateForm from '$lib/components/generate/GenerateForm.svelte';
import { CheckCircle } from '@manacore/shared-icons';
</script>
<svelte:head>
@ -24,74 +25,26 @@
<h3 class="mb-3 text-lg font-semibold text-gray-900">Tips for better results:</h3>
<ul class="space-y-2 text-sm text-gray-700">
<li class="flex items-start">
<svg
class="mr-2 mt-0.5 h-5 w-5 flex-shrink-0 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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>
<CheckCircle size={20} class="mr-2 mt-0.5 flex-shrink-0 text-blue-600" />
<span
><strong>Be specific:</strong> Include details about style, mood, colors, and composition</span
>
</li>
<li class="flex items-start">
<svg
class="mr-2 mt-0.5 h-5 w-5 flex-shrink-0 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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>
<CheckCircle size={20} class="mr-2 mt-0.5 flex-shrink-0 text-blue-600" />
<span
><strong>Use descriptive words:</strong> "Vibrant sunset over mountains" is better than "sunset"</span
>
</li>
<li class="flex items-start">
<svg
class="mr-2 mt-0.5 h-5 w-5 flex-shrink-0 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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>
<CheckCircle size={20} class="mr-2 mt-0.5 flex-shrink-0 text-blue-600" />
<span
><strong>Negative prompts:</strong> Use to exclude unwanted elements (e.g., "blurry, distorted,
low quality")</span
>
</li>
<li class="flex items-start">
<svg
class="mr-2 mt-0.5 h-5 w-5 flex-shrink-0 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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>
<CheckCircle size={20} class="mr-2 mt-0.5 flex-shrink-0 text-blue-600" />
<span
><strong>Try different models:</strong> Each model has unique strengths and artistic styles</span
>

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { currentTheme } from '$lib/stores/theme';
import { showToast } from '$lib/stores/toast';
function handleSubscribe(planId: string) {
@ -18,7 +17,7 @@
<title>Abonnement - Picture</title>
</svelte:head>
<div class="min-h-screen p-4 md:p-8" style="background-color: {$currentTheme.background};">
<div class="min-h-screen p-4 md:p-8" style="background-color: hsl(var(--color-background));">
<div class="mx-auto max-w-6xl">
<SubscriptionPage
appName="Picture"

View file

@ -3,13 +3,14 @@
import { tags, isLoadingTags } from '$lib/stores/tags';
import { getAllTags, createTag, updateTag, deleteTag } from '$lib/api/tags';
import { showToast } from '$lib/stores/toast';
import { Plus, Tag as TagIcon, PencilSimple, Trash } from '@manacore/shared-icons';
import type { Database } from '@picture/shared/types';
type Tag = Database['public']['Tables']['tags']['Row'];
type TagType = Database['public']['Tables']['tags']['Row'];
let showCreateModal = $state(false);
let showEditModal = $state(false);
let editingTag = $state<Tag | null>(null);
let editingTag = $state<TagType | null>(null);
let newTagName = $state('');
let newTagColor = $state('#3B82F6');
let editTagName = $state('');
@ -62,7 +63,7 @@
}
}
function openEditModal(tag: Tag) {
function openEditModal(tag: TagType) {
editingTag = tag;
editTagName = tag.name;
editTagColor = tag.color || '#3B82F6';
@ -119,14 +120,7 @@
onclick={() => (showCreateModal = true)}
class="flex items-center gap-2 rounded-2xl bg-blue-600/90 px-6 py-3 text-sm font-medium text-white backdrop-blur-xl transition-all hover:bg-blue-700/90 dark:bg-blue-500/90 dark:hover:bg-blue-600/90"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
<Plus size={20} />
Neuer Tag
</button>
</div>
@ -142,19 +136,7 @@
<div
class="rounded-3xl border border-gray-200/50 bg-white/80 p-12 text-center backdrop-blur-xl dark:border-gray-700/50 dark:bg-gray-900/80"
>
<svg
class="mx-auto h-16 w-16 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
/>
</svg>
<TagIcon size={64} weight="thin" class="mx-auto text-gray-400 dark:text-gray-500" />
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">
Keine Tags vorhanden
</h3>
@ -187,28 +169,14 @@
class="rounded-lg bg-gray-100/80 p-2 text-gray-600 backdrop-blur-xl transition-all hover:bg-gray-200/80 dark:bg-gray-800/80 dark:text-gray-400 dark:hover:bg-gray-700/80"
aria-label="Bearbeiten"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
<PencilSimple size={16} />
</button>
<button
onclick={() => handleDeleteTag(tag.id)}
class="rounded-lg bg-red-100/80 p-2 text-red-600 backdrop-blur-xl transition-all hover:bg-red-200/80 dark:bg-red-900/20 dark:text-red-400 dark:hover:bg-red-900/30"
aria-label="Löschen"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<Trash size={16} />
</button>
</div>
</div>

View file

@ -5,6 +5,7 @@
import { showToast } from '$lib/stores/toast';
import DropZone from '$lib/components/upload/DropZone.svelte';
import { images } from '$lib/stores/images';
import { Check, Image, CloudArrowUp, CheckCircle } from '@manacore/shared-icons';
let uploading = $state(false);
let uploadProgress = $state<UploadProgress[]>([]);
@ -20,7 +21,7 @@
successCount = 0;
try {
const uploadedImages = await uploadMultipleImages(files, authStore.user.id, (progress) => {
const uploadedImages = await uploadMultipleImages(files, (progress) => {
uploadProgress = progress;
});
@ -70,19 +71,7 @@
<div
class="mb-6 flex items-center gap-3 rounded-2xl border border-green-200 bg-green-50 p-4 dark:border-green-900 dark:bg-green-950/20"
>
<svg
class="h-6 w-6 flex-shrink-0 text-green-600 dark:text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<Check size={24} class="flex-shrink-0 text-green-600 dark:text-green-400" />
<div>
<p class="font-medium text-green-900 dark:text-green-100">Upload erfolgreich!</p>
<p class="text-sm text-green-700 dark:text-green-300">
@ -105,19 +94,7 @@
<div
class="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-950"
>
<svg
class="h-5 w-5 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<Image size={20} class="text-blue-600 dark:text-blue-400" />
</div>
<h3 class="mb-1 font-semibold text-gray-900 dark:text-white">Unterstützte Formate</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
@ -131,19 +108,7 @@
<div
class="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-purple-100 dark:bg-purple-950"
>
<svg
class="h-5 w-5 text-purple-600 dark:text-purple-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<CloudArrowUp size={20} class="text-purple-600 dark:text-purple-400" />
</div>
<h3 class="mb-1 font-semibold text-gray-900 dark:text-white">Maximale Größe</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Bis zu 10MB pro Bild</p>
@ -155,19 +120,7 @@
<div
class="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-green-100 dark:bg-green-950"
>
<svg
class="h-5 w-5 text-green-600 dark:text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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>
<CheckCircle size={20} class="text-green-600 dark:text-green-400" />
</div>
<h3 class="mb-1 font-semibold text-gray-900 dark:text-white">Batch Upload</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">

View file

@ -1,34 +1,16 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { RegisterPage, setGoogleClientId } from '@manacore/shared-auth-ui';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { getRegisterTranslations } from '@manacore/shared-i18n';
import PictureLogo from '$lib/components/branding/PictureLogo.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { onMount } from 'svelte';
import { PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_APPLE_CLIENT_ID } from '$env/static/public';
// Default to German
const translations = getRegisterTranslations('de');
onMount(() => {
if (PUBLIC_GOOGLE_CLIENT_ID) {
setGoogleClientId(PUBLIC_GOOGLE_CLIENT_ID);
}
});
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleSignUpWithGoogle() {
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Google Sign-Up not yet implemented' };
}
async function handleSignUpWithApple() {
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Apple Sign-Up not yet implemented' };
}
</script>
<svelte:head>
@ -40,11 +22,7 @@
logo={PictureLogo}
primaryColor="#3b82f6"
onSignUp={handleSignUp}
onSignUpWithGoogle={PUBLIC_GOOGLE_CLIENT_ID ? handleSignUpWithGoogle : undefined}
onSignUpWithApple={PUBLIC_APPLE_CLIENT_ID ? handleSignUpWithApple : undefined}
{goto}
enableGoogle={!!PUBLIC_GOOGLE_CLIENT_ID}
enableApple={!!PUBLIC_APPLE_CLIENT_ID}
successRedirect="/app/gallery"
loginPath="/auth/login"
lightBackground="#f0f9ff"

View file

@ -16,14 +16,14 @@
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/postcss": "^4.1.17",
"@types/node": "^20.0.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.0",
"tailwindcss": "^4.1.17",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^6.0.0"
@ -33,6 +33,8 @@
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"lucide-svelte": "^0.460.0"
},

View file

@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
};

View file

@ -1,44 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@import "@manacore/shared-tailwind/themes.css";
:root {
--color-bg: 250 250 250;
--color-bg-secondary: 255 255 255;
--color-text: 15 23 42;
--color-text-secondary: 100 116 139;
--color-border: 226 232 240;
--color-primary: 14 165 233;
}
/* Scan shared packages for Tailwind classes */
@source "../../../../packages/shared-ui/src";
@source "../../../../packages/shared-auth-ui/src";
@source "../../../../packages/shared-branding/src";
.dark {
--color-bg: 15 23 42;
--color-bg-secondary: 30 41 59;
--color-text: 248 250 252;
--color-text-secondary: 148 163 184;
--color-border: 51 65 85;
--color-primary: 56 189 248;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: rgb(var(--color-bg));
color: rgb(var(--color-text));
min-height: 100vh;
}
/* Slide aspect ratio container */
/* Presi-specific styles */
.slide-container {
aspect-ratio: 16 / 9;
max-width: 100%;
}
/* Presentation mode fullscreen */
.presentation-fullscreen {
position: fixed;
inset: 0;
z-index: 50;
background-color: rgb(var(--color-bg));
background-color: hsl(var(--color-background));
}
/* Custom scrollbar */
@ -48,14 +26,14 @@ body {
}
::-webkit-scrollbar-track {
background: rgb(var(--color-bg-secondary));
background: hsl(var(--color-surface));
}
::-webkit-scrollbar-thumb {
background: rgb(var(--color-border));
background: hsl(var(--color-border));
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgb(var(--color-text-secondary));
background: hsl(var(--color-muted-foreground));
}

View file

@ -0,0 +1,10 @@
import { createThemeStore } from '@manacore/shared-theme';
export const theme = createThemeStore({
appId: 'presi',
defaultVariant: 'stone',
primaryColor: {
light: '220 9% 46%',
dark: '220 9% 56%',
},
});

View file

@ -3,8 +3,10 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { auth } from '$lib/stores/auth.svelte';
import { theme } from '$lib/stores/theme';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import {
isSidebarMode as sidebarModeStore,
isNavCollapsed as collapsedStore,
@ -16,7 +18,21 @@
let loading = $state(true);
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
let isDark = $state(false);
// Theme variant dropdown items
let themeVariantItems = $derived<PillDropdownItem[]>(
theme.variants.map((variant) => ({
id: variant,
label: `${THEME_DEFINITIONS[variant].emoji} ${THEME_DEFINITIONS[variant].label}`,
onClick: () => theme.setVariant(variant),
active: theme.variant === variant,
}))
);
// Current theme variant label
let currentThemeVariantLabel = $derived(
`${THEME_DEFINITIONS[theme.variant].emoji} ${THEME_DEFINITIONS[theme.variant].label}`
);
// Navigation items for Presi
const navItems: PillNavItem[] = [
@ -52,9 +68,7 @@
}
function handleToggleTheme() {
isDark = !isDark;
localStorage.setItem('theme', isDark ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', isDark);
theme.toggleMode();
}
function handleLogout() {
@ -74,10 +88,7 @@
auth.init();
// Initialize theme
isDark =
localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);
const cleanup = theme.initialize();
// Initialize sidebar mode from localStorage
const savedSidebar = localStorage.getItem('presi-nav-sidebar');
@ -94,6 +105,8 @@
}
loading = false;
return cleanup;
});
</script>
@ -102,12 +115,12 @@
</svelte:head>
{#if loading || auth.isLoading}
<div class="flex min-h-screen items-center justify-center bg-slate-50 dark:bg-slate-900">
<div class="flex min-h-screen items-center justify-center bg-background">
<div class="text-center">
<div
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary-500 border-r-transparent"
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
></div>
<p class="text-slate-500 dark:text-slate-400">Laden...</p>
<p class="text-muted-foreground">Laden...</p>
</div>
</div>
{:else if auth.isAuthenticated || publicRoutes.includes($page.url.pathname)}
@ -121,16 +134,19 @@
appName="Presi"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
isDark={theme.isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
showLanguageSwitcher={false}
showLogout={true}
onLogout={handleLogout}
primaryColor="#0ea5e9"
primaryColor="#64748b"
/>
<!-- Main Content with dynamic padding based on nav mode -->
@ -146,7 +162,7 @@
</div>
{:else}
<!-- Public/Presentation routes without nav -->
<main class="min-h-screen bg-slate-50 dark:bg-slate-900">
<main class="min-h-screen bg-background">
{@render children()}
</main>
{/if}

View file

@ -1,32 +1,8 @@
<script lang="ts">
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 { onMount } from 'svelte';
type ThemeMode = 'light' | 'dark' | 'system';
let themeMode = $state<ThemeMode>('system');
onMount(() => {
const saved = localStorage.getItem('theme') as ThemeMode | null;
if (saved === 'light' || saved === 'dark') {
themeMode = saved;
} else {
themeMode = 'system';
}
});
function setTheme(mode: ThemeMode) {
themeMode = mode;
if (mode === 'system') {
localStorage.removeItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.toggle('dark', prefersDark);
} else {
localStorage.setItem('theme', mode);
document.documentElement.classList.toggle('dark', mode === 'dark');
}
}
function handleLogout() {
auth.logout();
@ -39,35 +15,35 @@
</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-slate-900 dark:text-white mb-8">Settings</h1>
<h1 class="text-2xl font-bold text-foreground mb-8">Settings</h1>
<div class="space-y-6">
<!-- Account Section -->
<div
class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden"
class="bg-surface rounded-xl shadow-sm border border-border overflow-hidden"
>
<div class="p-4 border-b border-slate-200 dark:border-slate-700">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center gap-2">
<User class="w-5 h-5 text-slate-400" />
<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-slate-400" />
<Mail class="w-5 h-5 text-muted-foreground" />
<div>
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Email</p>
<p class="text-sm text-slate-500 dark:text-slate-400">{auth.user?.email}</p>
<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-slate-400" />
<Shield class="w-5 h-5 text-muted-foreground" />
<div>
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">User ID</p>
<p class="text-sm text-slate-500 dark:text-slate-400 font-mono">{auth.user?.id}</p>
<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>
@ -76,46 +52,46 @@
<!-- Appearance Section -->
<div
class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden"
class="bg-surface rounded-xl shadow-sm border border-border overflow-hidden"
>
<div class="p-4 border-b border-slate-200 dark:border-slate-700">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center gap-2">
<Sun class="w-5 h-5 text-slate-400" />
<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-slate-600 dark:text-slate-400 mb-4">Choose your preferred theme</p>
<p class="text-sm text-muted-foreground mb-4">Choose your preferred theme</p>
<div class="grid grid-cols-3 gap-3">
<button
onclick={() => setTheme('light')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors {themeMode ===
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-500 bg-primary-50 dark:bg-primary-900/30'
: 'border-slate-200 dark:border-slate-600'}"
? 'border-primary bg-primary/10'
: 'border-border'}"
>
<Sun class="w-6 h-6 text-amber-500" />
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">Light</span>
<span class="text-sm font-medium text-foreground">Light</span>
</button>
<button
onclick={() => setTheme('dark')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors {themeMode ===
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-500 bg-primary-50 dark:bg-primary-900/30'
: 'border-slate-200 dark:border-slate-600'}"
? 'border-primary bg-primary/10'
: 'border-border'}"
>
<Moon class="w-6 h-6 text-indigo-500" />
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">Dark</span>
<span class="text-sm font-medium text-foreground">Dark</span>
</button>
<button
onclick={() => setTheme('system')}
class="flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors {themeMode ===
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-500 bg-primary-50 dark:bg-primary-900/30'
: 'border-slate-200 dark:border-slate-600'}"
? 'border-primary bg-primary/10'
: 'border-border'}"
>
<Monitor class="w-6 h-6 text-slate-500" />
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">System</span>
<Monitor class="w-6 h-6 text-muted-foreground" />
<span class="text-sm font-medium text-foreground">System</span>
</button>
</div>
</div>
@ -123,16 +99,16 @@
<!-- Danger Zone -->
<div
class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-red-200 dark:border-red-900/50 overflow-hidden"
class="bg-surface rounded-xl shadow-sm border border-red-300 dark:border-red-900/50 overflow-hidden"
>
<div class="p-4 border-b border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20">
<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-slate-700 dark:text-slate-300">Sign out</p>
<p class="text-sm text-slate-500 dark:text-slate-400">
<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>

View file

@ -1,32 +0,0 @@
import type { Config } from 'tailwindcss';
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
'../../../packages/shared-ui/src/**/*.{html,js,svelte,ts}',
],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
},
aspectRatio: {
'16/9': '16 / 9',
},
},
},
plugins: [],
} satisfies Config;

View file

@ -20,9 +20,19 @@ export default defineConfig({
},
},
ssr: {
noExternal: ['@manacore/shared-ui'],
noExternal: [
'@manacore/shared-ui',
'@manacore/shared-theme',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
],
},
optimizeDeps: {
exclude: ['@manacore/shared-ui'],
exclude: [
'@manacore/shared-ui',
'@manacore/shared-theme',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
],
},
});

View file

@ -23,6 +23,22 @@
"./ThemeModeSelector.svelte": {
"svelte": "./src/ThemeModeSelector.svelte",
"default": "./src/ThemeModeSelector.svelte"
},
"./ThemeColorPreview.svelte": {
"svelte": "./src/components/ThemeColorPreview.svelte",
"default": "./src/components/ThemeColorPreview.svelte"
},
"./ThemeCard.svelte": {
"svelte": "./src/components/ThemeCard.svelte",
"default": "./src/components/ThemeCard.svelte"
},
"./ThemeGrid.svelte": {
"svelte": "./src/components/ThemeGrid.svelte",
"default": "./src/components/ThemeGrid.svelte"
},
"./ThemePage.svelte": {
"svelte": "./src/pages/ThemePage.svelte",
"default": "./src/pages/ThemePage.svelte"
}
},
"peerDependencies": {

View file

@ -0,0 +1,136 @@
<script lang="ts">
import type { ThemeVariant } from '@manacore/shared-theme';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { Check, Lock, Clock, Star } from '@manacore/shared-icons';
import type { ThemeStatus } from '../types';
import ThemeColorPreview from './ThemeColorPreview.svelte';
interface Props {
variant: ThemeVariant;
isActive: boolean;
status?: ThemeStatus;
onClick?: () => void;
onUnlock?: () => void;
translations?: {
locked?: string;
comingSoon?: string;
premium?: string;
unlock?: string;
lightPreview?: string;
darkPreview?: string;
};
}
let {
variant,
isActive,
status = 'available',
onClick,
onUnlock,
translations = {},
}: Props = $props();
const t = {
locked: translations.locked ?? 'Gesperrt',
comingSoon: translations.comingSoon ?? 'Bald verfügbar',
premium: translations.premium ?? 'Premium',
unlock: translations.unlock ?? 'Freischalten',
lightPreview: translations.lightPreview ?? 'Hell',
darkPreview: translations.darkPreview ?? 'Dunkel',
};
const definition = $derived(THEME_DEFINITIONS[variant]);
const isAvailable = $derived(status === 'available');
const isLocked = $derived(status === 'locked');
const isComingSoon = $derived(status === 'coming_soon');
const isPremium = $derived(status === 'premium');
function handleClick() {
if (isAvailable && onClick) {
onClick();
}
}
function handleUnlock(e: MouseEvent) {
e.stopPropagation();
if (onUnlock) {
onUnlock();
}
}
</script>
<button
type="button"
onclick={handleClick}
disabled={!isAvailable}
class="relative w-full p-4 rounded-xl border-2 transition-all text-left
{isActive
? 'border-primary bg-primary/5 ring-2 ring-primary/20'
: 'border-border bg-surface hover:border-primary/50'}
{!isAvailable ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'}
{isPremium ? 'border-yellow-500/50' : ''}"
>
<!-- Premium badge -->
{#if isPremium}
<div
class="absolute -top-2 -right-2 flex items-center gap-1 px-2 py-0.5
bg-yellow-500 text-yellow-950 text-xs font-medium rounded-full"
>
<Star size={12} weight="fill" />
{t.premium}
</div>
{/if}
<!-- Active checkmark -->
{#if isActive && isAvailable}
<div
class="absolute top-3 right-3 w-6 h-6 flex items-center justify-center
bg-primary text-primary-foreground rounded-full"
>
<Check size={14} weight="bold" />
</div>
{/if}
<!-- Header -->
<div class="flex items-center gap-2 mb-3">
<span class="text-xl">{definition.emoji}</span>
<span class="font-semibold text-foreground">{definition.label}</span>
</div>
<!-- Color previews -->
<div class="space-y-2 mb-3">
<div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground w-10">{t.lightPreview}</span>
<ThemeColorPreview {variant} mode="light" size="sm" />
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground w-10">{t.darkPreview}</span>
<ThemeColorPreview {variant} mode="dark" size="sm" />
</div>
</div>
<!-- Status badges -->
{#if isLocked}
<div class="flex items-center justify-between">
<div class="flex items-center gap-1 text-muted-foreground text-sm">
<Lock size={14} weight="bold" />
{t.locked}
</div>
{#if onUnlock}
<button
type="button"
onclick={handleUnlock}
class="px-2 py-1 text-xs font-medium text-primary bg-primary/10
rounded hover:bg-primary/20 transition-colors"
>
{t.unlock}
</button>
{/if}
</div>
{:else if isComingSoon}
<div class="flex items-center gap-1 text-muted-foreground text-sm">
<Clock size={14} weight="bold" />
{t.comingSoon}
</div>
{/if}
</button>

View file

@ -0,0 +1,48 @@
<script lang="ts">
import type { ThemeVariant } from '@manacore/shared-theme';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
interface Props {
variant: ThemeVariant;
mode: 'light' | 'dark';
size?: 'sm' | 'md' | 'lg';
}
let { variant, mode, size = 'md' }: Props = $props();
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
};
const gapClasses = {
sm: '-ml-1.5',
md: '-ml-2',
lg: '-ml-2.5',
};
const colors = $derived(() => {
const def = THEME_DEFINITIONS[variant];
const themeColors = mode === 'dark' ? def.dark : def.light;
return [
{ name: 'primary', value: themeColors.primary },
{ name: 'secondary', value: themeColors.secondary },
{ name: 'background', value: themeColors.background },
{ name: 'surface', value: themeColors.surface },
{ name: 'muted', value: themeColors.muted },
];
});
</script>
<div class="flex items-center">
{#each colors() as color, i}
<div
class="{sizeClasses[size]} rounded-full border border-white/20 shadow-sm {i > 0
? gapClasses[size]
: ''}"
style="background-color: hsl({color.value}); z-index: {5 - i};"
title={color.name}
></div>
{/each}
</div>

View file

@ -0,0 +1,58 @@
<script lang="ts">
import type { ThemeVariant } from '@manacore/shared-theme';
import { THEME_VARIANTS } from '@manacore/shared-theme';
import type { ThemeCardData, ThemePageTranslations } from '../types';
import ThemeCard from './ThemeCard.svelte';
interface Props {
currentVariant: ThemeVariant;
onSelect: (variant: ThemeVariant) => void;
themes?: ThemeCardData[];
onUnlock?: (variant: ThemeVariant) => void;
showLockedThemes?: boolean;
translations?: Partial<ThemePageTranslations>;
}
let {
currentVariant,
onSelect,
themes,
onUnlock,
showLockedThemes = true,
translations = {},
}: Props = $props();
// Build theme data - use provided themes or create defaults from THEME_VARIANTS
const themeData = $derived(() => {
if (themes) {
return showLockedThemes ? themes : themes.filter((t) => t.status === 'available');
}
// Default: all variants are available
return THEME_VARIANTS.map((variant) => ({
variant,
status: 'available' as const,
}));
});
const cardTranslations = $derived({
locked: translations.locked,
comingSoon: translations.comingSoon,
premium: translations.premium,
unlock: translations.unlock,
lightPreview: translations.lightPreview,
darkPreview: translations.darkPreview,
});
</script>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{#each themeData() as theme (theme.variant)}
<ThemeCard
variant={theme.variant}
isActive={currentVariant === theme.variant}
status={theme.status}
onClick={() => onSelect(theme.variant)}
onUnlock={onUnlock ? () => onUnlock(theme.variant) : undefined}
translations={cardTranslations}
/>
{/each}
</div>

View file

@ -1,4 +1,21 @@
// Theme UI Components
// Theme UI Components (existing)
export { default as ThemeToggle } from './ThemeToggle.svelte';
export { default as ThemeSelector } from './ThemeSelector.svelte';
export { default as ThemeModeSelector } from './ThemeModeSelector.svelte';
// New Components
export { default as ThemeColorPreview } from './components/ThemeColorPreview.svelte';
export { default as ThemeCard } from './components/ThemeCard.svelte';
export { default as ThemeGrid } from './components/ThemeGrid.svelte';
// Pages
export { default as ThemePage } from './pages/ThemePage.svelte';
// Types
export type {
ThemeStatus,
ThemeCardData,
ThemePageProps,
ThemePageTranslations,
} from './types';
export { defaultTranslations } from './types';

View file

@ -0,0 +1,125 @@
<script lang="ts">
import type { ThemeVariant, ThemeMode } from '@manacore/shared-theme';
import { ArrowLeft, Sun, Moon, Desktop } from '@manacore/shared-icons';
import type { ThemeCardData, ThemePageTranslations } from '../types';
import { defaultTranslations } from '../types';
import ThemeGrid from '../components/ThemeGrid.svelte';
interface Props {
// Theme Store Integration
currentVariant: ThemeVariant;
onSelectTheme: (variant: ThemeVariant) => void;
// Theme Data (for store extension)
themes?: ThemeCardData[];
// UI Customization
title?: string;
subtitle?: string;
showModeSelector?: boolean;
currentMode?: ThemeMode;
onModeChange?: (mode: ThemeMode) => void;
// Back navigation
showBackButton?: boolean;
onBack?: () => void;
// Store Features (preparation)
showLockedThemes?: boolean;
onUnlockTheme?: (variant: ThemeVariant) => void;
// Translations
translations?: Partial<ThemePageTranslations>;
}
let {
currentVariant,
onSelectTheme,
themes,
title,
subtitle,
showModeSelector = false,
currentMode = 'system',
onModeChange,
showBackButton = false,
onBack,
showLockedThemes = true,
onUnlockTheme,
translations = {},
}: Props = $props();
// Merge translations with defaults
const t = $derived({ ...defaultTranslations, ...translations });
const modes: { mode: ThemeMode; icon: typeof Sun; label: string }[] = $derived([
{ mode: 'light', icon: Sun, label: t.lightMode },
{ mode: 'dark', icon: Moon, label: t.darkMode },
{ mode: 'system', icon: Desktop, label: t.systemMode },
]);
</script>
<div class="min-h-screen bg-background">
<div class="max-w-4xl mx-auto px-4 py-8">
<!-- Header -->
<header class="mb-8">
<div class="flex items-center gap-3 mb-2">
{#if showBackButton && onBack}
<button
type="button"
onclick={onBack}
class="p-2 -ml-2 text-muted-foreground hover:text-foreground
hover:bg-muted rounded-lg transition-colors"
aria-label="Zurück"
>
<ArrowLeft size={20} weight="bold" />
</button>
{/if}
<h1 class="text-2xl font-bold text-foreground">
{title ?? t.title}
</h1>
</div>
<p class="text-muted-foreground">
{subtitle ?? t.subtitle}
</p>
</header>
<!-- Mode Selector -->
{#if showModeSelector && onModeChange}
<section class="mb-8">
<h2 class="text-sm font-medium text-muted-foreground mb-3">
{t.modeLabel}
</h2>
<div class="inline-flex rounded-lg bg-muted p-1">
{#each modes as { mode, icon: Icon, label }}
<button
type="button"
onclick={() => onModeChange(mode)}
class="flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors
{currentMode === mode
? 'bg-surface text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'}"
>
<Icon size={16} weight={currentMode === mode ? 'fill' : 'regular'} />
{label}
</button>
{/each}
</div>
</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>
</div>
</div>

View file

@ -0,0 +1,87 @@
import type { ThemeVariant, ThemeMode } from '@manacore/shared-theme';
/**
* Theme availability status for store integration
*/
export type ThemeStatus = 'available' | 'locked' | 'coming_soon' | 'premium';
/**
* Theme card data for displaying in grid/list
*/
export interface ThemeCardData {
variant: ThemeVariant;
status: ThemeStatus;
isPremium?: boolean;
price?: number;
releaseDate?: string;
}
/**
* Props for ThemePage component
*/
export interface ThemePageProps {
// Theme Store Integration
currentVariant: ThemeVariant;
onSelectTheme: (variant: ThemeVariant) => void;
// Theme Data (for store extension)
themes?: ThemeCardData[];
// UI Customization
title?: string;
subtitle?: string;
showModeSelector?: boolean;
currentMode?: ThemeMode;
onModeChange?: (mode: ThemeMode) => void;
// Back navigation
showBackButton?: boolean;
onBack?: () => void;
// Store Features (preparation)
showLockedThemes?: boolean;
onUnlockTheme?: (variant: ThemeVariant) => void;
// Translations
translations?: Partial<ThemePageTranslations>;
}
/**
* Translations for ThemePage
*/
export interface ThemePageTranslations {
title: string;
subtitle: string;
modeLabel: string;
lightMode: string;
darkMode: string;
systemMode: string;
currentTheme: string;
selectTheme: string;
locked: string;
comingSoon: string;
premium: string;
unlock: string;
lightPreview: string;
darkPreview: string;
}
/**
* Default German translations
*/
export const defaultTranslations: ThemePageTranslations = {
title: 'Theme-Einstellungen',
subtitle: 'Wähle dein bevorzugtes Farbschema',
modeLabel: 'Modus',
lightMode: 'Hell',
darkMode: 'Dunkel',
systemMode: 'System',
currentTheme: 'Aktuelles Theme',
selectTheme: 'Auswählen',
locked: 'Gesperrt',
comingSoon: 'Bald verfügbar',
premium: 'Premium',
unlock: 'Freischalten',
lightPreview: 'Hell',
darkPreview: 'Dunkel',
};

View file

@ -31,7 +31,7 @@ const lumeLight: ThemeColors = {
primaryForeground: '0 0% 0%', // Black text on gold
secondary: '47 100% 41%', // #D4B200 - Darker gold
secondaryForeground: '0 0% 0%',
background: '0 0% 87%', // #dddddd - Light gray
background: '45 30% 96%', // Warm cream with gold tint
foreground: '0 0% 17%', // #2c2c2c - Dark text
surface: '0 0% 100%', // #ffffff - White
surfaceHover: '0 0% 96%', // #f5f5f5
@ -52,15 +52,15 @@ const lumeDark: ThemeColors = {
primaryForeground: '0 0% 0%', // Black text on gold
secondary: '47 70% 29%', // #7C6B16 - Muted gold
secondaryForeground: '0 0% 100%',
background: '0 0% 6%', // #101010 - Very dark
background: '40 10% 7%', // Very dark with warm tint
foreground: '0 0% 100%', // #ffffff - White text
surface: '0 0% 12%', // #1E1E1E - Dark surface
surfaceHover: '0 0% 16%', // #292929
surfaceElevated: '0 0% 14%', // #242424
muted: '0 0% 20%', // #333333
mutedForeground: '0 0% 60%', // #999999
border: '0 0% 26%', // #424242
borderStrong: '0 0% 35%', // #595959
surface: '40 8% 12%', // Dark surface with warm tint
surfaceHover: '40 8% 16%',
surfaceElevated: '40 8% 14%',
muted: '40 6% 20%',
mutedForeground: '40 5% 60%',
border: '40 6% 26%',
borderStrong: '40 5% 35%',
error: '6 78% 57%', // #e74c3c
success: '145 63% 49%', // #2ecc71
warning: '48 100% 50%', // #f1c40f
@ -94,15 +94,15 @@ const natureDark: ThemeColors = {
primaryForeground: '0 0% 100%',
secondary: '122 30% 35%', // Muted green
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
background: '120 12% 6%', // Very dark with green tint
foreground: '0 0% 100%', // White
surface: '120 10% 12%', // Slight green tint
surfaceHover: '120 10% 16%',
surfaceElevated: '120 10% 14%',
muted: '120 10% 20%',
surface: '120 10% 11%', // Slight green tint
surfaceHover: '120 10% 15%',
surfaceElevated: '120 10% 13%',
muted: '120 10% 19%',
mutedForeground: '120 10% 60%',
border: '120 10% 25%',
borderStrong: '120 10% 35%',
border: '120 10% 24%',
borderStrong: '120 10% 34%',
error: '0 65% 57%',
success: '122 50% 55%',
warning: '48 100% 50%',
@ -136,11 +136,11 @@ const stoneDark: ThemeColors = {
primaryForeground: '0 0% 0%',
secondary: '200 12% 35%',
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
background: '210 15% 8%', // Very dark with blue-gray tint
foreground: '0 0% 100%',
surface: '200 10% 12%',
surfaceHover: '200 10% 16%',
surfaceElevated: '200 10% 14%',
surface: '200 12% 12%',
surfaceHover: '200 12% 16%',
surfaceElevated: '200 12% 14%',
muted: '200 10% 20%',
mutedForeground: '200 10% 60%',
border: '200 10% 25%',
@ -178,15 +178,15 @@ const oceanDark: ThemeColors = {
primaryForeground: '0 0% 0%',
secondary: '199 60% 35%',
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
background: '200 25% 7%', // Very dark with blue tint
foreground: '0 0% 100%',
surface: '199 30% 12%', // Slight blue tint
surfaceHover: '199 30% 16%',
surfaceElevated: '199 30% 14%',
muted: '199 20% 20%',
mutedForeground: '199 20% 60%',
border: '199 20% 25%',
borderStrong: '199 20% 35%',
surface: '199 20% 11%', // Slight blue tint
surfaceHover: '199 20% 15%',
surfaceElevated: '199 20% 13%',
muted: '199 15% 19%',
mutedForeground: '199 15% 60%',
border: '199 15% 24%',
borderStrong: '199 15% 34%',
error: '4 90% 58%',
success: '145 63% 49%',
warning: '48 100% 50%',

View file

@ -219,4 +219,12 @@ export const APP_THEME_CONFIGS = {
dark: '280 60% 60%' as HSLValue,
},
},
picture: {
appId: 'picture',
defaultVariant: 'ocean' as ThemeVariant,
primaryColor: {
light: '217 91% 60%' as HSLValue, // Blue #3b82f6
dark: '217 91% 60%' as HSLValue,
},
},
} as const;

View file

@ -61,6 +61,8 @@
chevronDown: 'M19 9l-7 7-7-7',
globe:
'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9',
palette:
'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',
};
function getIcon(iconName: string) {

View file

@ -43,6 +43,12 @@
elements?: PillNavElement[];
/** Show logout button */
showLogout?: boolean;
/** Theme variant dropdown items */
themeVariantItems?: PillDropdownItem[];
/** Current theme variant label */
currentThemeVariantLabel?: string;
/** Show theme variant selector */
showThemeVariants?: boolean;
}
let {
@ -65,6 +71,9 @@
primaryColor,
elements = [],
showLogout = true,
themeVariantItems = [],
currentThemeVariantLabel = 'Theme',
showThemeVariants = false,
}: Props = $props();
// Type guards for elements
@ -161,6 +170,8 @@
grid: 'M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z',
gridSmall:
'M4 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM10 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM16 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM4 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2zM10 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2zM16 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2z',
palette:
'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',
};
function getIconPath(name: string): string {
@ -297,6 +308,16 @@
/>
{/if}
<!-- Theme Variant Selector -->
{#if showThemeVariants && themeVariantItems.length > 0}
<PillDropdown
items={themeVariantItems}
direction="down"
label={currentThemeVariantLabel}
icon="palette"
/>
{/if}
<!-- Theme Toggle -->
{#if showThemeToggle && onToggleTheme}
<button

342
pnpm-lock.yaml generated
View file

@ -73,7 +73,7 @@ importers:
devDependencies:
'@nestjs/cli':
specifier: ^10.4.9
version: 10.4.9(esbuild@0.27.0)
version: 10.4.9(esbuild@0.19.12)
'@nestjs/schematics':
specifier: ^10.2.3
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
@ -106,7 +106,7 @@ importers:
version: 0.5.21
ts-loader:
specifier: ^9.5.1
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0))
version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12))
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
@ -133,14 +133,14 @@ importers:
version: link:../../../../packages/shared-landing-ui
astro:
specifier: ^5.16.0
version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)
version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)
typescript:
specifier: ^5.0.0
version: 5.9.3
devDependencies:
'@astrojs/tailwind':
specifier: ^6.0.0
version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))
@ -1386,6 +1386,9 @@ importers:
'@manacore/shared-i18n':
specifier: workspace:*
version: link:../../../../packages/shared-i18n
'@manacore/shared-icons':
specifier: workspace:*
version: link:../../../../packages/shared-icons
'@manacore/shared-subscription-types':
specifier: workspace:*
version: link:../../../../packages/shared-subscription-types
@ -1395,6 +1398,9 @@ importers:
'@manacore/shared-tailwind':
specifier: workspace:*
version: link:../../../../packages/shared-tailwind
'@manacore/shared-theme':
specifier: workspace:*
version: link:../../../../packages/shared-theme
'@manacore/shared-theme-ui':
specifier: workspace:*
version: link:../../../../packages/shared-theme-ui
@ -1722,6 +1728,12 @@ importers:
'@manacore/shared-branding':
specifier: workspace:*
version: link:../../../../packages/shared-branding
'@manacore/shared-tailwind':
specifier: workspace:*
version: link:../../../../packages/shared-tailwind
'@manacore/shared-theme':
specifier: workspace:*
version: link:../../../../packages/shared-theme
'@manacore/shared-ui':
specifier: workspace:*
version: link:../../../../packages/shared-ui
@ -1741,12 +1753,12 @@ importers:
'@sveltejs/vite-plugin-svelte':
specifier: ^5.0.0
version: 5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
'@tailwindcss/postcss':
specifier: ^4.1.17
version: 4.1.17
'@types/node':
specifier: ^20.0.0
version: 20.19.25
autoprefixer:
specifier: ^10.4.16
version: 10.4.22(postcss@8.5.6)
postcss:
specifier: ^8.4.32
version: 8.5.6
@ -1763,8 +1775,8 @@ importers:
specifier: ^4.0.0
version: 4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3)
tailwindcss:
specifier: ^3.4.0
version: 3.4.18(tsx@4.20.6)(yaml@2.8.1)
specifier: ^4.1.17
version: 4.1.17
tslib:
specifier: ^2.4.1
version: 2.8.1
@ -1831,7 +1843,7 @@ importers:
devDependencies:
'@nestjs/cli':
specifier: ^10.4.9
version: 10.4.9(esbuild@0.19.12)
version: 10.4.9(esbuild@0.27.0)
'@nestjs/schematics':
specifier: ^10.2.3
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
@ -1864,7 +1876,7 @@ importers:
version: 0.5.21
ts-loader:
specifier: ^9.5.1
version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12))
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0))
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
@ -2545,6 +2557,9 @@ importers:
'@manacore/shared-auth':
specifier: workspace:*
version: link:../shared-auth
'@manacore/shared-icons':
specifier: workspace:*
version: link:../shared-icons
devDependencies:
svelte:
specifier: ^5.16.0
@ -4771,7 +4786,7 @@ packages:
'@expo/bunyan@4.0.1':
resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==}
engines: {node: '>=0.10.0'}
engines: {'0': node >=0.10.0}
'@expo/cli@0.22.26':
resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==}
@ -17096,6 +17111,16 @@ snapshots:
transitivePeerDependencies:
- ts-node
'@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))':
dependencies:
astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)
autoprefixer: 10.4.22(postcss@8.5.6)
postcss: 8.5.6
postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1)
transitivePeerDependencies:
- ts-node
'@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))':
dependencies:
astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)
@ -18997,7 +19022,7 @@ snapshots:
wrap-ansi: 7.0.0
ws: 8.18.3
optionalDependencies:
expo-router: 6.0.15(jiucxy5ca3jdtbnulaxuc46jdq)
expo-router: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq)
react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
@ -19074,7 +19099,7 @@ snapshots:
wrap-ansi: 7.0.0
ws: 8.18.3
optionalDependencies:
expo-router: 6.0.15(dux2nvtiztnejw7mxzfaajqvh4)
expo-router: 6.0.15(nttrd3tw67nnyhowcwgdzipb5e)
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
@ -20382,6 +20407,43 @@ snapshots:
- supports-color
- ts-node
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))':
dependencies:
'@jest/console': 30.2.0
'@jest/pattern': 30.0.1
'@jest/reporters': 30.2.0
'@jest/test-result': 30.2.0
'@jest/transform': 30.2.0
'@jest/types': 30.2.0
'@types/node': 22.19.1
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 4.3.1
exit-x: 0.2.2
graceful-fs: 4.2.11
jest-changed-files: 30.2.0
jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
jest-haste-map: 30.2.0
jest-message-util: 30.2.0
jest-regex-util: 30.0.1
jest-resolve: 30.2.0
jest-resolve-dependencies: 30.2.0
jest-runner: 30.2.0
jest-runtime: 30.2.0
jest-snapshot: 30.2.0
jest-util: 30.2.0
jest-validate: 30.2.0
jest-watcher: 30.2.0
micromatch: 4.0.8
pretty-format: 30.2.0
slash: 3.0.0
transitivePeerDependencies:
- babel-plugin-macros
- esbuild-register
- supports-color
- ts-node
optional: true
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))':
dependencies:
'@jest/console': 30.2.0
@ -23305,17 +23367,17 @@ snapshots:
react-test-renderer: 19.1.0(react@19.1.0)
redent: 3.0.0
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
jest-matcher-utils: 30.2.0
picocolors: 1.1.1
pretty-format: 30.2.0
react: 19.1.0
react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
react-test-renderer: 19.1.0(react@19.1.0)
redent: 3.0.0
optionalDependencies:
jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))
jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
optional: true
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
@ -24277,11 +24339,11 @@ snapshots:
- vite
optional: true
'@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)':
'@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)':
dependencies:
'@testing-library/dom': 10.4.1
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
'@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
'@vitest/utils': 3.2.4
magic-string: 0.30.21
sirv: 3.0.2
@ -24321,6 +24383,15 @@ snapshots:
optionalDependencies:
vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
'@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
optional: true
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
@ -24350,7 +24421,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
'@vitest/utils@3.2.4':
dependencies:
@ -24944,6 +25015,108 @@ snapshots:
- uploadthing
- yaml
astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1):
dependencies:
'@astrojs/compiler': 2.13.0
'@astrojs/internal-helpers': 0.7.5
'@astrojs/markdown-remark': 6.3.9
'@astrojs/telemetry': 3.3.0
'@capsizecss/unpack': 3.0.1
'@oslojs/encoding': 1.1.0
'@rollup/pluginutils': 5.3.0(rollup@4.53.3)
acorn: 8.15.0
aria-query: 5.3.2
axobject-query: 4.1.0
boxen: 8.0.1
ci-info: 4.3.1
clsx: 2.1.1
common-ancestor-path: 1.0.1
cookie: 1.1.0
cssesc: 3.0.0
debug: 4.4.3
deterministic-object-hash: 2.0.2
devalue: 5.5.0
diff: 5.2.0
dlv: 1.1.3
dset: 3.1.4
es-module-lexer: 1.7.0
esbuild: 0.25.12
estree-walker: 3.0.3
flattie: 1.1.1
fontace: 0.3.1
github-slugger: 2.0.0
html-escaper: 3.0.3
http-cache-semantics: 4.2.0
import-meta-resolve: 4.2.0
js-yaml: 4.1.1
magic-string: 0.30.21
magicast: 0.5.1
mrmime: 2.0.1
neotraverse: 0.6.18
p-limit: 6.2.0
p-queue: 8.1.1
package-manager-detector: 1.5.0
piccolore: 0.1.3
picomatch: 4.0.3
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.3
shiki: 3.15.0
smol-toml: 1.5.2
svgo: 4.0.0
tinyexec: 1.0.2
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@5.9.3)
ultrahtml: 1.6.0
unifont: 0.6.0
unist-util-visit: 5.0.0
unstorage: 1.17.3(@netlify/blobs@10.4.1)(ioredis@5.8.2)
vfile: 6.0.3
vite: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
vitefu: 1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
zod: 3.25.76
zod-to-json-schema: 3.25.0(zod@3.25.76)
zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76)
optionalDependencies:
sharp: 0.34.5
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@types/node'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- db0
- idb-keyval
- ioredis
- jiti
- less
- lightningcss
- rollup
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- typescript
- uploadthing
- yaml
astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1):
dependencies:
'@astrojs/compiler': 2.13.0
@ -26903,9 +27076,9 @@ snapshots:
'@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1))
globals: 16.5.0
@ -26920,9 +27093,9 @@ snapshots:
'@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1))
globals: 16.5.0
@ -26992,7 +27165,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@ -27003,7 +27176,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
eslint: 9.39.1(jiti@2.6.1)
get-tsconfig: 4.13.0
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@ -27027,25 +27215,25 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@ -27145,7 +27333,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -27156,7 +27344,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -27174,7 +27362,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -27185,7 +27373,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -28326,21 +28514,21 @@ snapshots:
- '@types/react-dom'
- supports-color
expo-router@6.0.15(jiucxy5ca3jdtbnulaxuc46jdq):
expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e):
dependencies:
'@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@expo/schema-utils': 0.1.7
'@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0)
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
client-only: 0.0.1
debug: 4.4.3
escape-string-regexp: 4.0.0
expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))
expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))
expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
expo-server: 1.0.4
fast-deep-equal: 3.1.3
invariant: 2.2.4
@ -28348,10 +28536,10 @@ snapshots:
query-string: 7.1.3
react: 19.1.0
react-fast-compare: 3.2.2
react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
semver: 7.6.3
server-only: 0.0.1
sf-symbols-typescript: 2.1.0
@ -28359,13 +28547,13 @@ snapshots:
use-latest-callback: 0.2.6(react@19.1.0)
vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
optionalDependencies:
'@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)
'@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
'@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)
react-dom: 19.1.0(react@19.1.0)
react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0))
react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12))
transitivePeerDependencies:
- '@react-native-masked-view/masked-view'
- '@types/react'
@ -30409,15 +30597,15 @@ snapshots:
- supports-color
- ts-node
jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)):
jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
dependencies:
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
'@jest/test-result': 30.2.0
'@jest/types': 30.2.0
chalk: 4.1.2
exit-x: 0.2.2
import-local: 3.2.0
jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))
jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
jest-util: 30.2.0
jest-validate: 30.2.0
yargs: 17.7.2
@ -30580,7 +30768,7 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)):
jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.28.5
'@jest/get-type': 30.1.0
@ -30607,8 +30795,9 @@ snapshots:
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 20.19.25
esbuild-register: 3.6.0(esbuild@0.27.0)
'@types/node': 22.19.1
esbuild-register: 3.6.0(esbuild@0.19.12)
ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@ -31269,12 +31458,12 @@ snapshots:
- supports-color
- ts-node
jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)):
jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
dependencies:
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
'@jest/types': 30.2.0
import-local: 3.2.0
jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))
jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -34897,6 +35086,16 @@ snapshots:
webpack: 5.100.2(esbuild@0.27.0)
webpack-sources: 3.3.3
react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)):
dependencies:
acorn-loose: 8.5.2
neo-async: 2.6.2
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
webpack: 5.97.1(esbuild@0.19.12)
webpack-sources: 3.3.3
optional: true
react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1):
dependencies:
get-nonce: 1.0.1
@ -37001,6 +37200,23 @@ snapshots:
tsx: 4.20.6
yaml: 2.8.1
vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.53.3
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.1
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.30.2
terser: 5.44.1
tsx: 4.20.6
yaml: 2.8.1
vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1):
dependencies:
esbuild: 0.25.12
@ -37060,6 +37276,10 @@ snapshots:
optionalDependencies:
vite: 6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
vitefu@1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)):
optionalDependencies:
vite: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
vitefu@1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)):
optionalDependencies:
vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
@ -37145,7 +37365,7 @@ snapshots:
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 24.10.1
'@vitest/browser': 3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)
'@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)
'@vitest/ui': 3.2.4(vitest@3.2.4)
jsdom: 27.2.0
transitivePeerDependencies: