feat(moodlit): fullscreen mood on click with visual card redesign

Clicking a mood now opens it immediately in fullscreen (browser
Fullscreen API, z-index above all UI). Preview step removed.
Cards redesigned with full gradient backgrounds, live animations,
gradient overlays, and hover border highlight.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-12 18:45:31 +02:00
parent d6a1c9fd8b
commit c6c19dbc77
3 changed files with 403 additions and 172 deletions

View file

@ -6,12 +6,13 @@
import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database'; import { db } from '$lib/data/database';
import { BaseListView } from '@mana/shared-ui'; import { BaseListView } from '@mana/shared-ui';
import type { LocalMood, AnimationType } from './types'; import type { LocalMood, AnimationType, Mood } from './types';
import { ANIMATIONS } from './types'; import { ANIMATIONS } from './types';
import { moodsStore } from './stores/moods.svelte'; import { moodsStore } from './stores/moods.svelte';
import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui'; import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui';
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte'; import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
import { Trash, Power } from '@mana/shared-icons'; import { Trash, Power } from '@mana/shared-icons';
import MoodFullscreen from './components/mood/MoodFullscreen.svelte';
const moodsQuery = useLiveQueryWithDefault(async () => { const moodsQuery = useLiveQueryWithDefault(async () => {
const all = await db.table<LocalMood>('moods').toArray(); const all = await db.table<LocalMood>('moods').toArray();
@ -20,8 +21,16 @@
const moods = $derived(moodsQuery.value); const moods = $derived(moodsQuery.value);
let activeMoodId = $state<string | null>(null); let fullscreenMood = $state<LocalMood | null>(null);
const activeMood = $derived(moods.find((m) => m.id === activeMoodId));
function toMood(local: LocalMood): Mood {
return {
id: local.id,
name: local.name,
colors: local.colors,
animationType: local.animation as AnimationType,
};
}
function gradientStyle(colors: string[]): string { function gradientStyle(colors: string[]): string {
if (colors.length === 0) return 'background: #333'; if (colors.length === 0) return 'background: #333';
@ -29,6 +38,25 @@
return `background: linear-gradient(135deg, ${colors.join(', ')})`; return `background: linear-gradient(135deg, ${colors.join(', ')})`;
} }
function getAnimClass(animation: string): string {
switch (animation) {
case 'pulse':
case 'breath':
return 'anim-breath';
case 'wave':
case 'ocean':
return 'anim-wave';
case 'candle':
case 'fire':
return 'anim-candle';
case 'disco':
case 'rave':
return 'anim-disco';
default:
return 'anim-gradient';
}
}
// ── Inline create ────────────────────────────────────── // ── Inline create ──────────────────────────────────────
let creating = $state(false); let creating = $state(false);
let newName = $state(''); let newName = $state('');
@ -69,11 +97,11 @@
? [ ? [
{ {
id: 'activate', id: 'activate',
label: activeMoodId === ctxMenu.state.target.id ? 'Deaktivieren' : 'Aktivieren', label: 'Aktivieren',
icon: Power, icon: Power,
action: () => { action: () => {
const target = ctxMenu.state.target; const target = ctxMenu.state.target;
if (target) activeMoodId = activeMoodId === target.id ? null : target.id; if (target) fullscreenMood = target;
}, },
}, },
{ id: 'div', label: '', type: 'divider' as const }, { id: 'div', label: '', type: 'divider' as const },
@ -84,10 +112,7 @@
variant: 'danger' as const, variant: 'danger' as const,
action: () => { action: () => {
const target = ctxMenu.state.target; const target = ctxMenu.state.target;
if (target) { if (target) moodsStore.deleteMood(target.id);
if (activeMoodId === target.id) activeMoodId = null;
moodsStore.deleteMood(target.id);
}
}, },
}, },
] ]
@ -103,22 +128,6 @@
listClass="grid grid-cols-2 sm:grid-cols-3 gap-2 content-start" listClass="grid grid-cols-2 sm:grid-cols-3 gap-2 content-start"
> >
{#snippet toolbar()} {#snippet toolbar()}
<!-- Active mood preview -->
{#if activeMood}
<div
class="flex h-24 items-center justify-center rounded-lg"
style={gradientStyle(activeMood.colors)}
>
<p class="text-sm font-medium text-white drop-shadow">{activeMood.name}</p>
</div>
{:else}
<div
class="flex h-24 items-center justify-center rounded-lg bg-[hsl(var(--color-foreground)/0.05)]"
>
<p class="text-sm text-[hsl(var(--color-muted-foreground))]">Kein Mood aktiv</p>
</div>
{/if}
<!-- Create toggle --> <!-- Create toggle -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-xs text-[hsl(var(--color-muted-foreground))]">{moods.length} Moods</span> <span class="text-xs text-[hsl(var(--color-muted-foreground))]">{moods.length} Moods</span>
@ -135,10 +144,15 @@
<div class="flex flex-col gap-2 rounded-lg bg-[hsl(var(--color-foreground)/0.05)] p-3"> <div class="flex flex-col gap-2 rounded-lg bg-[hsl(var(--color-foreground)/0.05)] p-3">
<!-- Preview --> <!-- Preview -->
<div <div
class="flex h-12 items-center justify-center rounded-md" class="relative flex h-12 items-center justify-center overflow-hidden rounded-lg"
style={gradientStyle(newColors)} style={gradientStyle(newColors)}
> >
<span class="text-xs font-medium text-white drop-shadow">{newName || 'Vorschau'}</span> <div
class="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-transparent"
></div>
<span class="relative text-xs font-medium text-white drop-shadow-md"
>{newName || 'Vorschau'}</span
>
</div> </div>
<!-- Name --> <!-- Name -->
@ -204,20 +218,31 @@
{#snippet item(mood)} {#snippet item(mood)}
<button <button
onclick={() => (activeMoodId = activeMoodId === mood.id ? null : mood.id)} onclick={() => (fullscreenMood = mood)}
oncontextmenu={(e) => ctxMenu.open(e, mood)} oncontextmenu={(e) => ctxMenu.open(e, mood)}
class="group flex flex-col items-center gap-1.5 rounded-lg p-2 transition-colors hover:bg-[hsl(var(--color-foreground)/0.05)] class="mood-card group relative aspect-[4/3] w-full overflow-hidden rounded-xl border-2 border-transparent transition-all duration-200 hover:border-white/40 focus:outline-none"
{activeMoodId === mood.id ? 'ring-1 ring-[hsl(var(--color-border))]' : ''}" style="--mood-color: {mood.colors[0]}"
> >
<div class="h-10 w-10 rounded-full" style={gradientStyle(mood.colors)}></div> <div
class="absolute inset-0 {getAnimClass(mood.animation)}"
style="{gradientStyle(mood.colors)}; background-size: 400% 400%;"
></div>
<div
class="absolute inset-0 bg-gradient-to-t from-black/45 via-transparent to-transparent"
></div>
<span <span
class="text-[10px] text-[hsl(var(--color-muted-foreground))] group-hover:text-[hsl(var(--color-foreground))]" class="absolute inset-x-0 bottom-0 px-2 pb-1.5 text-[10px] font-medium text-white drop-shadow-md"
>{mood.name}</span
> >
{mood.name}
</span>
</button> </button>
{/snippet} {/snippet}
</BaseListView> </BaseListView>
{#if fullscreenMood}
<MoodFullscreen mood={toMood(fullscreenMood)} minimal onClose={() => (fullscreenMood = null)} />
{/if}
<ContextMenu <ContextMenu
visible={ctxMenu.state.visible} visible={ctxMenu.state.visible}
x={ctxMenu.state.x} x={ctxMenu.state.x}
@ -225,3 +250,78 @@
items={ctxMenuItems} items={ctxMenuItems}
onClose={ctxMenu.close} onClose={ctxMenu.close}
/> />
<style>
.anim-gradient {
animation: gradient-shift 8s ease infinite;
}
.anim-breath {
animation: breath 4s ease-in-out infinite;
}
.anim-wave {
animation: wave 3s ease-in-out infinite;
}
.anim-candle {
animation: candle 0.8s ease-in-out infinite;
}
.anim-disco {
animation: disco 2s linear infinite;
}
@keyframes gradient-shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes breath {
0%,
100% {
opacity: 0.85;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.02);
}
}
@keyframes wave {
0%,
100% {
background-position: 0% 50%;
opacity: 1;
}
50% {
background-position: 100% 50%;
opacity: 0.85;
}
}
@keyframes candle {
0%,
100% {
filter: brightness(1);
}
25% {
filter: brightness(0.92);
}
50% {
filter: brightness(1.08);
}
75% {
filter: brightness(0.95);
}
}
@keyframes disco {
0% {
filter: hue-rotate(0deg) saturate(1.2);
}
100% {
filter: hue-rotate(360deg) saturate(1.2);
}
}
</style>

View file

@ -5,12 +5,13 @@
interface Props { interface Props {
mood: Mood; mood: Mood;
minimal?: boolean;
isFavorite?: boolean; isFavorite?: boolean;
onClose: () => void; onClose: () => void;
onFavoriteToggle?: () => void; onFavoriteToggle?: () => void;
} }
let { mood, isFavorite = false, onClose, onFavoriteToggle }: Props = $props(); let { mood, minimal = false, isFavorite = false, onClose, onFavoriteToggle }: Props = $props();
let isPlaying = $state(true); let isPlaying = $state(true);
let showControls = $state(true); let showControls = $state(true);
@ -87,7 +88,7 @@
timerRemaining--; timerRemaining--;
if (timerRemaining <= 0) { if (timerRemaining <= 0) {
stopTimer(); stopTimer();
onClose(); handleClose();
} }
}, 1000); }, 1000);
} }
@ -106,9 +107,16 @@
return `${mins}:${secs.toString().padStart(2, '0')}`; return `${mins}:${secs.toString().padStart(2, '0')}`;
} }
async function handleClose() {
if (document.fullscreenElement) {
await document.exitFullscreen().catch(() => {});
}
onClose();
}
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') { if (e.key === 'Escape') {
onClose(); handleClose();
} else if (e.key === ' ') { } else if (e.key === ' ') {
e.preventDefault(); e.preventDefault();
togglePlay(); togglePlay();
@ -116,10 +124,26 @@
} }
$effect(() => { $effect(() => {
showControlsTemporarily(); if (minimal) {
document.documentElement.requestFullscreen?.().catch(() => {});
} else {
showControlsTemporarily();
}
const onFsChange = () => {
if (minimal && !document.fullscreenElement) {
onClose();
}
};
document.addEventListener('fullscreenchange', onFsChange);
return () => { return () => {
if (controlsTimeout) clearTimeout(controlsTimeout); if (controlsTimeout) clearTimeout(controlsTimeout);
if (timerInterval) clearInterval(timerInterval); if (timerInterval) clearInterval(timerInterval);
document.removeEventListener('fullscreenchange', onFsChange);
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
}
}; };
}); });
</script> </script>
@ -127,9 +151,10 @@
<svelte:window on:keydown={handleKeydown} /> <svelte:window on:keydown={handleKeydown} />
<div <div
class="fixed inset-0 z-50 flex items-center justify-center cursor-pointer select-none" class="fixed inset-0 flex items-center justify-center cursor-pointer select-none"
onclick={showControlsTemporarily} style="z-index: 1000001;"
onmousemove={showControlsTemporarily} onclick={minimal ? handleClose : showControlsTemporarily}
onmousemove={minimal ? undefined : showControlsTemporarily}
role="presentation" role="presentation"
> >
<!-- Animated Background --> <!-- Animated Background -->
@ -153,121 +178,125 @@
{/if} {/if}
<!-- Controls Overlay --> <!-- Controls Overlay -->
<div {#if !minimal}
class="absolute inset-0 flex flex-col transition-opacity duration-300 pointer-events-none"
class:opacity-0={!showControls}
class:opacity-100={showControls}
>
<!-- Top Bar -->
<div <div
class="flex items-center justify-between p-4 bg-gradient-to-b from-black/40 to-transparent pointer-events-auto" class="absolute inset-0 flex flex-col transition-opacity duration-300 pointer-events-none"
class:opacity-0={!showControls}
class:opacity-100={showControls}
> >
<div class="flex items-center gap-3"> <!-- Top Bar -->
<button <div
type="button" class="flex items-center justify-between p-4 bg-gradient-to-b from-black/40 to-transparent pointer-events-auto"
class="p-2 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-colors" >
onclick={(e) => { <div class="flex items-center gap-3">
e.stopPropagation(); <button
onClose(); type="button"
}} class="p-2 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-colors"
aria-label="Close" onclick={(e) => {
> e.stopPropagation();
<X size={24} class="text-white" /> handleClose();
</button> }}
<div> aria-label="Close"
<h1 class="text-xl font-bold text-white drop-shadow-lg">{mood.name}</h1> >
<p class="text-sm text-white/70 capitalize">{mood.animationType}</p> <X size={24} class="text-white" />
</button>
<div>
<h1 class="text-xl font-bold text-white drop-shadow-lg">{mood.name}</h1>
<p class="text-sm text-white/70 capitalize">{mood.animationType}</p>
</div>
</div>
<div class="flex items-center gap-2">
{#if timerActive}
<div class="px-3 py-1.5 rounded-full bg-white/20 backdrop-blur-sm text-white font-mono">
{formatTime(timerRemaining)}
</div>
{/if}
<button
type="button"
class="p-2 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-colors"
onclick={(e) => {
e.stopPropagation();
onFavoriteToggle?.();
}}
aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
size={20}
weight={isFavorite ? 'fill' : 'regular'}
class={isFavorite ? 'text-red-500' : 'text-white'}
/>
</button>
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <!-- Center Play/Pause -->
{#if timerActive} <div class="flex-1 flex items-center justify-center pointer-events-auto">
<div class="px-3 py-1.5 rounded-full bg-white/20 backdrop-blur-sm text-white font-mono">
{formatTime(timerRemaining)}
</div>
{/if}
<button <button
type="button" type="button"
class="p-2 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-colors" class="p-6 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-all hover:scale-110"
onclick={(e) => { onclick={(e) => {
e.stopPropagation(); e.stopPropagation();
onFavoriteToggle?.(); togglePlay();
}} }}
aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'} aria-label={isPlaying ? 'Pause' : 'Play'}
> >
<Heart {#if isPlaying}
size={20} <Pause size={48} class="text-white" />
weight={isFavorite ? 'fill' : 'regular'} {:else}
class={isFavorite ? 'text-red-500' : 'text-white'} <Play size={48} class="text-white" />
/> {/if}
</button> </button>
</div> </div>
</div>
<!-- Center Play/Pause --> <!-- Bottom Bar -->
<div class="flex-1 flex items-center justify-center pointer-events-auto"> <div class="p-4 bg-gradient-to-t from-black/40 to-transparent pointer-events-auto">
<button <div class="flex items-center justify-center gap-4">
type="button" {#if !timerActive}
class="p-6 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-all hover:scale-110" <div
onclick={(e) => { class="flex items-center gap-2 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2"
e.stopPropagation();
togglePlay();
}}
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{#if isPlaying}
<Pause size={48} class="text-white" />
{:else}
<Play size={48} class="text-white" />
{/if}
</button>
</div>
<!-- Bottom Bar -->
<div class="p-4 bg-gradient-to-t from-black/40 to-transparent pointer-events-auto">
<div class="flex items-center justify-center gap-4">
{#if !timerActive}
<div class="flex items-center gap-2 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2">
<Timer size={20} class="text-white" />
<select
class="bg-transparent text-white border-none outline-none cursor-pointer"
bind:value={timerMinutes}
onclick={(e) => e.stopPropagation()}
> >
<option value={1}>1 min</option> <Timer size={20} class="text-white" />
<option value={5}>5 min</option> <select
<option value={10}>10 min</option> class="bg-transparent text-white border-none outline-none cursor-pointer"
<option value={15}>15 min</option> bind:value={timerMinutes}
<option value={30}>30 min</option> onclick={(e) => e.stopPropagation()}
<option value={60}>60 min</option> >
</select> <option value={1}>1 min</option>
<option value={5}>5 min</option>
<option value={10}>10 min</option>
<option value={15}>15 min</option>
<option value={30}>30 min</option>
<option value={60}>60 min</option>
</select>
<button
type="button"
class="px-3 py-1 bg-white/20 hover:bg-white/30 rounded-full text-sm text-white transition-colors"
onclick={(e) => {
e.stopPropagation();
startTimer();
}}
>
Start Timer
</button>
</div>
{:else}
<button <button
type="button" type="button"
class="px-3 py-1 bg-white/20 hover:bg-white/30 rounded-full text-sm text-white transition-colors" class="px-4 py-2 bg-white/20 hover:bg-white/30 backdrop-blur-sm rounded-full text-white transition-colors"
onclick={(e) => { onclick={(e) => {
e.stopPropagation(); e.stopPropagation();
startTimer(); stopTimer();
}} }}
> >
Start Timer Stop Timer
</button> </button>
</div> {/if}
{:else} </div>
<button
type="button"
class="px-4 py-2 bg-white/20 hover:bg-white/30 backdrop-blur-sm rounded-full text-white transition-colors"
onclick={(e) => {
e.stopPropagation();
stopTimer();
}}
>
Stop Timer
</button>
{/if}
</div> </div>
</div> </div>
</div> {/if}
</div> </div>
<style> <style>

View file

@ -3,10 +3,11 @@
import { useLiveQuery } from '@mana/local-store/svelte'; import { useLiveQuery } from '@mana/local-store/svelte';
import { moodTable } from '$lib/modules/moodlit/collections'; import { moodTable } from '$lib/modules/moodlit/collections';
import { moodsStore } from '$lib/modules/moodlit/stores/moods.svelte'; import { moodsStore } from '$lib/modules/moodlit/stores/moods.svelte';
import type { LocalMood } from '$lib/modules/moodlit/types'; import type { LocalMood, Mood, AnimationType } from '$lib/modules/moodlit/types';
import { db } from '$lib/data/database'; import { db } from '$lib/data/database';
import { toast } from '$lib/stores/toast.svelte'; import { toast } from '$lib/stores/toast.svelte';
import { X } from '@mana/shared-icons'; import { X } from '@mana/shared-icons';
import MoodFullscreen from '$lib/modules/moodlit/components/mood/MoodFullscreen.svelte';
const moods = useLiveQuery(() => const moods = useLiveQuery(() =>
db db
@ -19,7 +20,16 @@
let newName = $state(''); let newName = $state('');
let newColors = $state(['#7c3aed', '#a78bfa', '#c4b5fd']); let newColors = $state(['#7c3aed', '#a78bfa', '#c4b5fd']);
let newAnimation = $state('gradient'); let newAnimation = $state('gradient');
let activeMood = $state<LocalMood | null>(null); let fullscreenMood = $state<LocalMood | null>(null);
function toMood(local: LocalMood): Mood {
return {
id: local.id,
name: local.name,
colors: local.colors,
animationType: local.animation as AnimationType,
};
}
async function createMood() { async function createMood() {
if (!newName) return; if (!newName) return;
@ -33,19 +43,33 @@
showCreate = false; showCreate = false;
} }
function getAnimClass(animation: string): string {
switch (animation) {
case 'pulse':
case 'breath':
return 'anim-breath';
case 'wave':
case 'ocean':
return 'anim-wave';
case 'candle':
case 'fire':
return 'anim-candle';
case 'disco':
case 'rave':
return 'anim-disco';
default:
return 'anim-gradient';
}
}
async function deleteMood(mood: LocalMood) { async function deleteMood(mood: LocalMood) {
if (mood.isDefault) { if (mood.isDefault) {
toast.error('Standard-Moods konnen nicht geloscht werden'); toast.error('Standard-Moods konnen nicht geloscht werden');
return; return;
} }
await moodsStore.deleteMood(mood.id); await moodsStore.deleteMood(mood.id);
if (activeMood?.id === mood.id) activeMood = null;
toast.success('Geloscht'); toast.success('Geloscht');
} }
function activateMood(mood: LocalMood) {
activeMood = activeMood?.id === mood.id ? null : mood;
}
</script> </script>
<svelte:head> <svelte:head>
@ -63,22 +87,6 @@
</button> </button>
</div> </div>
<!-- Active Mood Display -->
{#if activeMood}
<div
class="mb-6 overflow-hidden rounded-2xl p-8 text-center transition-all duration-1000"
style="background: linear-gradient(135deg, {activeMood.colors.join(', ')})"
>
<h2 class="text-4xl font-bold text-white drop-shadow-lg">{activeMood.name}</h2>
<p class="mt-2 text-white/70">{activeMood.animation}</p>
<button
onclick={() => (activeMood = null)}
class="mt-4 rounded-lg bg-white/20 px-4 py-2 text-sm text-white backdrop-blur hover:bg-white/30"
>Stoppen</button
>
</div>
{/if}
{#if showCreate} {#if showCreate}
<div class="mb-6 rounded-xl border border-border bg-card p-6"> <div class="mb-6 rounded-xl border border-border bg-card p-6">
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
@ -145,42 +153,136 @@
{#if moods.loading} {#if moods.loading}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each Array(6) as _} {#each Array(6) as _}
<div class="h-32 animate-pulse rounded-xl bg-muted"></div> <div class="aspect-[16/10] animate-pulse rounded-2xl bg-muted"></div>
{/each} {/each}
</div> </div>
{:else} {:else}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each moods.value ?? [] as mood (mood.id)} {#each moods.value ?? [] as mood (mood.id)}
{@const gradient =
mood.colors.length === 1
? mood.colors[0]
: `linear-gradient(135deg, ${mood.colors.join(', ')})`}
<button <button
onclick={() => activateMood(mood)} onclick={() => (fullscreenMood = mood)}
class="group relative overflow-hidden rounded-xl border-2 p-6 text-left transition-all hover:scale-[1.02] {activeMood?.id === class="mood-card group relative aspect-[16/10] w-full overflow-hidden rounded-2xl border-[3px] border-transparent transition-all duration-200 hover:border-white/40 focus:outline-none focus:ring-2 focus:ring-primary/50"
mood.id style="--mood-color: {mood.colors[0]}"
? 'border-primary shadow-lg shadow-purple-500/20'
: 'border-border hover:border-muted-foreground/30'}"
style="background: linear-gradient(135deg, {mood.colors.map((c) => c + '40').join(', ')})"
> >
<h3 class="text-lg font-bold text-foreground">{mood.name}</h3> <div
<p class="mt-1 text-xs text-muted-foreground">{mood.animation}</p> class="absolute inset-0 {getAnimClass(mood.animation)}"
<div class="mt-3 flex gap-1"> style="background: {gradient}; background-size: 400% 400%;"
{#each mood.colors as color} ></div>
<div class="h-4 w-4 rounded-full" style="background: {color}"></div> <div
// svelte-ignore node_invalid_placement_ssr class="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent"
{/each} ></div>
<div class="absolute inset-x-0 bottom-0 p-4 text-left">
<h3 class="text-lg font-semibold text-white drop-shadow-md">{mood.name}</h3>
<span
class="mt-1 inline-block rounded-full bg-white/20 px-2 py-0.5 text-[10px] font-medium text-white/80 backdrop-blur-sm capitalize"
>{mood.animation}</span
>
</div> </div>
{#if !mood.isDefault} {#if !mood.isDefault}
<!-- svelte-ignore node_invalid_placement_ssr --> <!-- svelte-ignore node_invalid_placement_ssr -->
<button <div
role="button"
tabindex="-1"
onclick={(e) => { onclick={(e) => {
e.stopPropagation(); e.stopPropagation();
deleteMood(mood); deleteMood(mood);
}} }}
class="absolute right-2 top-2 rounded-full p-1 text-muted-foreground opacity-0 hover:bg-muted hover:text-red-400 group-hover:opacity-100" onkeydown={(e) => {
if (e.key === 'Enter') {
e.stopPropagation();
deleteMood(mood);
}
}}
class="absolute right-2 top-2 rounded-full bg-black/20 p-1.5 text-white/70 opacity-0 backdrop-blur-sm transition-all hover:bg-black/40 hover:text-white group-hover:opacity-100 cursor-pointer"
> >
<X size={16} /> <X size={14} />
</button> </div>
{/if} {/if}
</button> </button>
{/each} {/each}
</div> </div>
{/if} {/if}
</div> </div>
{#if fullscreenMood}
<MoodFullscreen mood={toMood(fullscreenMood)} minimal onClose={() => (fullscreenMood = null)} />
{/if}
<style>
.anim-gradient {
animation: gradient-shift 8s ease infinite;
}
.anim-breath {
animation: breath 4s ease-in-out infinite;
}
.anim-wave {
animation: wave 3s ease-in-out infinite;
}
.anim-candle {
animation: candle 0.8s ease-in-out infinite;
}
.anim-disco {
animation: disco 2s linear infinite;
}
@keyframes gradient-shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes breath {
0%,
100% {
opacity: 0.85;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.02);
}
}
@keyframes wave {
0%,
100% {
background-position: 0% 50%;
opacity: 1;
}
50% {
background-position: 100% 50%;
opacity: 0.85;
}
}
@keyframes candle {
0%,
100% {
filter: brightness(1);
}
25% {
filter: brightness(0.92);
}
50% {
filter: brightness(1.08);
}
75% {
filter: brightness(0.95);
}
}
@keyframes disco {
0% {
filter: hue-rotate(0deg) saturate(1.2);
}
100% {
filter: hue-rotate(360deg) saturate(1.2);
}
}
</style>