mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-24 03:16:44 +02:00
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:
parent
d6a1c9fd8b
commit
c6c19dbc77
3 changed files with 403 additions and 172 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue