All checks were successful
Validate / Hassfest (push) Successful in 3s
New teal-accent color system, DM Sans + JetBrains Mono typography, glow effects, animated gradient login page, animated dashboard counters with gradient-border stat cards, event timeline, sidebar with active glow indicators, and polished components (modals, cards, snackbar). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
3.6 KiB
Svelte
150 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import { fly, fade } from 'svelte/transition';
|
|
import { getSnacks, removeSnack, type Snack } from '$lib/stores/snackbar.svelte';
|
|
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
|
|
|
const snacks = $derived(getSnacks());
|
|
|
|
let expandedIds = $state<Set<number>>(new Set());
|
|
|
|
function toggleDetail(id: number) {
|
|
const next = new Set(expandedIds);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
expandedIds = next;
|
|
}
|
|
|
|
const iconMap: Record<string, string> = {
|
|
success: 'mdiCheckCircle',
|
|
error: 'mdiAlertCircle',
|
|
info: 'mdiInformation',
|
|
warning: 'mdiAlert',
|
|
};
|
|
|
|
const accentMap: Record<string, string> = {
|
|
success: '#059669',
|
|
error: '#ef4444',
|
|
info: '#3b82f6',
|
|
warning: '#f59e0b',
|
|
};
|
|
</script>
|
|
|
|
{#if snacks.length > 0}
|
|
<div
|
|
style="position: fixed; left: 50%; transform: translateX(-50%); z-index: 9999; display: flex; flex-direction: column; gap: 0.5rem; width: 90%; max-width: 26rem; pointer-events: none;"
|
|
class="snackbar-container"
|
|
>
|
|
{#each snacks as snack (snack.id)}
|
|
<div
|
|
in:fly={{ y: 40, duration: 300 }}
|
|
out:fade={{ duration: 200 }}
|
|
class="snack-item"
|
|
style="--snack-accent: {accentMap[snack.type]};"
|
|
>
|
|
<span class="snack-icon" style="color: {accentMap[snack.type]};">
|
|
<MdiIcon name={iconMap[snack.type]} size={18} />
|
|
</span>
|
|
<div style="flex: 1; min-width: 0;">
|
|
<p class="snack-message">{snack.message}</p>
|
|
{#if snack.detail}
|
|
<button class="snack-detail-toggle" onclick={() => toggleDetail(snack.id)}>
|
|
{expandedIds.has(snack.id) ? 'Hide details' : 'Show details'}
|
|
</button>
|
|
{#if expandedIds.has(snack.id)}
|
|
<pre class="snack-detail">{snack.detail}</pre>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
<button class="snack-close" onclick={() => removeSnack(snack.id)} aria-label="Dismiss">
|
|
<MdiIcon name="mdiClose" size={14} />
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.snackbar-container {
|
|
bottom: 5rem;
|
|
}
|
|
@media (min-width: 768px) {
|
|
.snackbar-container {
|
|
bottom: 1.5rem;
|
|
}
|
|
}
|
|
|
|
.snack-item {
|
|
pointer-events: auto;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.625rem;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 0.75rem;
|
|
border-left: 3px solid var(--snack-accent);
|
|
background: var(--color-card);
|
|
border-top: 1px solid var(--color-border);
|
|
border-right: 1px solid var(--color-border);
|
|
border-bottom: 1px solid var(--color-border);
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.03) inset;
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
|
|
:global([data-theme="dark"]) .snack-item {
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 0 16px color-mix(in srgb, var(--snack-accent) 10%, transparent);
|
|
}
|
|
|
|
.snack-icon {
|
|
flex-shrink: 0;
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.snack-message {
|
|
font-size: 0.8rem;
|
|
line-height: 1.4;
|
|
margin: 0;
|
|
color: var(--color-foreground);
|
|
}
|
|
|
|
.snack-detail-toggle {
|
|
font-size: 0.7rem;
|
|
color: var(--color-muted-foreground);
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
margin-top: 0.25rem;
|
|
cursor: pointer;
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
|
|
.snack-detail-toggle:hover {
|
|
color: var(--color-foreground);
|
|
}
|
|
|
|
.snack-detail {
|
|
font-size: 0.7rem;
|
|
font-family: var(--font-mono);
|
|
color: var(--color-muted-foreground);
|
|
margin: 0.25rem 0 0;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.snack-close {
|
|
flex-shrink: 0;
|
|
background: none;
|
|
border: none;
|
|
color: var(--color-muted-foreground);
|
|
cursor: pointer;
|
|
padding: 0.125rem;
|
|
border-radius: 0.25rem;
|
|
transition: all 0.15s ease;
|
|
line-height: 1;
|
|
}
|
|
|
|
.snack-close:hover {
|
|
color: var(--color-foreground);
|
|
background: var(--color-muted);
|
|
}
|
|
</style>
|