feat(ui): add skeleton loaders for calendar and clock apps

Calendar App:
- Add AppLoadingSkeleton for root layout initialization
- Add CalendarViewSkeleton for main week view
- Add AgendaSkeleton for agenda page event list
- Add EventDetailSkeleton for event modal
- Add RedirectSkeleton for event redirect page
- Fix TypeScript error in event/new page

Clock App:
- Add AppLoadingSkeleton for root layout initialization
- Add WorldClockSkeleton for world clock grid
- Add AlarmsSkeleton for alarms grid
- Add TimersSkeleton for active timers grid

All spinners replaced with contextual skeleton loaders using
@manacore/shared-ui SkeletonBox component.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-09 20:36:37 +01:00
parent a4846aea06
commit 7987fe009d
21 changed files with 558 additions and 102 deletions

View file

@ -0,0 +1,27 @@
<script lang="ts">
/**
* AlarmsSkeleton - Skeleton for alarms grid
* Shows placeholder tiles for alarm times
*/
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div role="status" aria-label="Wecker werden geladen...">
<!-- Alarm Preset Grid (matches DEFAULT_ALARM_PRESETS layout) -->
<div class="grid grid-cols-4 sm:grid-cols-6 gap-2">
{#each Array(12) as _, i}
<div
class="bg-card rounded-xl p-3 border border-border flex flex-col items-center justify-center"
style="opacity: {Math.max(0.4, 1 - i * 0.05)}"
>
<!-- Time -->
<SkeletonBox width="48px" height="24px" borderRadius="4px" />
<!-- Label -->
<div class="mt-1">
<SkeletonBox width="40px" height="10px" borderRadius="4px" />
</div>
</div>
{/each}
</div>
</div>

View file

@ -0,0 +1,47 @@
<script lang="ts">
/**
* AppLoadingSkeleton - Full-page loading skeleton for app initialization
* Replaces spinner with clock-themed skeleton layout
*/
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div
class="flex min-h-screen items-center justify-center bg-background"
role="status"
aria-label="App wird geladen..."
>
<div class="w-full max-w-sm px-6 space-y-8">
<!-- Clock Icon Area -->
<div class="flex flex-col items-center gap-4">
<SkeletonBox width="80px" height="80px" borderRadius="50%" />
<SkeletonBox width="120px" height="24px" borderRadius="8px" />
</div>
<!-- Time Display Skeleton -->
<div class="bg-card rounded-2xl p-6 space-y-4">
<!-- Large Time -->
<div class="flex justify-center">
<SkeletonBox width="200px" height="48px" borderRadius="8px" />
</div>
<!-- Date -->
<div class="flex justify-center">
<SkeletonBox width="140px" height="16px" borderRadius="4px" />
</div>
<!-- Quick Actions -->
<div class="flex justify-center gap-3 pt-2">
{#each Array(4) as _, i}
<SkeletonBox width="48px" height="48px" borderRadius="12px" />
{/each}
</div>
</div>
<!-- Loading Text -->
<div class="flex justify-center">
<SkeletonBox width="100px" height="14px" borderRadius="4px" />
</div>
</div>
</div>

View file

@ -0,0 +1,47 @@
<script lang="ts">
/**
* TimersSkeleton - Skeleton for active timers grid
* Shows placeholder tiles for timer displays
*/
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div role="status" aria-label="Timer werden geladen...">
<!-- Section Header -->
<div class="mb-2">
<SkeletonBox width="80px" height="12px" borderRadius="4px" />
</div>
<!-- Timer Grid -->
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
{#each Array(4) as _, i}
<div
class="bg-card rounded-xl p-3 border border-border"
style="opacity: {Math.max(0.5, 1 - i * 0.12)}"
>
<!-- Time + Delete Button Row -->
<div class="flex items-start justify-between mb-1">
<SkeletonBox width="70px" height="24px" borderRadius="4px" />
<SkeletonBox width="14px" height="14px" borderRadius="4px" />
</div>
<!-- Label -->
<div class="mb-2">
<SkeletonBox width="50px" height="10px" borderRadius="4px" />
</div>
<!-- Progress Bar -->
<div class="mb-2">
<SkeletonBox width="100%" height="4px" borderRadius="2px" />
</div>
<!-- Control Buttons -->
<div class="flex gap-1">
<SkeletonBox width="100%" height="28px" borderRadius="6px" />
<SkeletonBox width="48px" height="28px" borderRadius="6px" />
</div>
</div>
{/each}
</div>
</div>

View file

@ -0,0 +1,36 @@
<script lang="ts">
/**
* WorldClockSkeleton - Skeleton for world clock grid
* Shows placeholder cards for city times
*/
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div
class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
role="status"
aria-label="Weltuhren werden geladen..."
>
{#each Array(6) as _, i}
<div
class="bg-card rounded-xl p-4 border border-border"
style="opacity: {Math.max(0.4, 1 - i * 0.1)}"
>
<!-- Day/Night + City Name -->
<div class="mb-2 flex items-center gap-2">
<SkeletonBox width="32px" height="12px" borderRadius="4px" />
<SkeletonBox width="80px" height="14px" borderRadius="4px" />
</div>
<!-- Large Time Display -->
<SkeletonBox width="140px" height="40px" borderRadius="6px" />
<!-- Date and Offset -->
<div class="mt-2 flex items-center justify-between">
<SkeletonBox width="90px" height="13px" borderRadius="4px" />
<SkeletonBox width="36px" height="14px" borderRadius="4px" />
</div>
</div>
{/each}
</div>

View file

@ -0,0 +1,18 @@
/**
* Clock App Skeleton Components
*
* App-specific skeleton loaders that match the exact layout of clock components.
* Built on top of @manacore/shared-ui skeleton primitives.
*/
// App Loading Skeleton
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
// World Clock Skeleton
export { default as WorldClockSkeleton } from './WorldClockSkeleton.svelte';
// Alarms Skeleton
export { default as AlarmsSkeleton } from './AlarmsSkeleton.svelte';
// Timers Skeleton
export { default as TimersSkeleton } from './TimersSkeleton.svelte';

View file

@ -7,6 +7,7 @@
import { toast } from '$lib/stores/toast';
import type { CreateAlarmInput, Alarm } from '@clock/shared';
import { ALARM_SOUNDS, DEFAULT_ALARM_PRESETS } from '@clock/shared';
import { AlarmsSkeleton } from '$lib/components/skeletons';
// Quick create form (inline)
let newTime = $state('07:00');
@ -194,11 +195,7 @@
<!-- Loading State -->
{#if alarmsStore.loading}
<div class="flex justify-center py-12">
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"
></div>
</div>
<AlarmsSkeleton />
{:else}
<!-- Default Alarm Presets (Grid) -->
<div class="alarm-grid">

View file

@ -7,6 +7,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { QUICK_TIMER_PRESETS, formatDuration } from '@clock/shared';
import { TimersSkeleton } from '$lib/components/skeletons';
// Form state (inline on page)
let formMinutes = $state(5);
@ -219,11 +220,7 @@
<!-- Loading State -->
{#if timersStore.loading}
<div class="flex justify-center py-8">
<div
class="h-6 w-6 animate-spin rounded-full border-2 border-primary border-r-transparent"
></div>
</div>
<TimersSkeleton />
{:else if allTimers.length > 0}
<!-- Active Timers -->
<div>

View file

@ -7,6 +7,7 @@
import { toast } from '$lib/stores/toast';
import { POPULAR_TIMEZONES } from '@clock/shared';
import WorldMap from '$lib/components/WorldMap.svelte';
import { WorldClockSkeleton } from '$lib/components/skeletons';
// State
let showAddModal = $state(false);
@ -205,11 +206,7 @@
<!-- World Clock List -->
{#if worldClocksStore.loading}
<div class="flex justify-center py-12">
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"
></div>
</div>
<WorldClockSkeleton />
{:else if worldClocksStore.sortedWorldClocks.length === 0}
<div class="card py-12 text-center">
<p class="text-lg text-muted-foreground">{$_('worldClock.noClocks')}</p>

View file

@ -6,6 +6,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { waitLocale } from '$lib/i18n';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let { children } = $props();
@ -28,14 +29,7 @@
<ToastContainer />
{#if $isLocaleLoading || loading}
<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 border-r-transparent"
></div>
<p class="text-muted-foreground">Laden...</p>
</div>
</div>
<AppLoadingSkeleton />
{:else}
<div class="min-h-screen bg-background text-foreground">
{@render children()}