mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
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:
parent
a4846aea06
commit
7987fe009d
21 changed files with 558 additions and 102 deletions
|
|
@ -8,6 +8,7 @@
|
|||
import * as api from '$lib/api/events';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { EventDetailSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
interface Props {
|
||||
eventId: string;
|
||||
|
|
@ -147,10 +148,7 @@
|
|||
<div class="modal-backdrop" onclick={handleBackdropClick}>
|
||||
<div class="modal-container" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||
{#if loading}
|
||||
<div class="modal-loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
<EventDetailSkeleton />
|
||||
{:else if event}
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title" class="modal-title">
|
||||
|
|
@ -476,31 +474,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.modal-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
gap: 1rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid hsl(var(--color-border));
|
||||
border-top-color: hsl(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* AgendaSkeleton - Skeleton for agenda page event list
|
||||
* Shows date groups with event items
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="space-y-6" role="status" aria-label="Termine werden geladen...">
|
||||
<!-- Date Groups -->
|
||||
{#each Array(4) as _, groupIndex}
|
||||
<div class="space-y-2" style="opacity: {Math.max(0.4, 1 - groupIndex * 0.15)}">
|
||||
<!-- Date Header -->
|
||||
<div class="px-2">
|
||||
<SkeletonBox width="140px" height="14px" borderRadius="4px" />
|
||||
</div>
|
||||
|
||||
<!-- Event Items -->
|
||||
{#each Array(groupIndex === 0 ? 3 : 2) as _, eventIndex}
|
||||
<div
|
||||
class="bg-card rounded-xl p-4 flex gap-4"
|
||||
style="opacity: {Math.max(0.5, 1 - eventIndex * 0.15)}"
|
||||
>
|
||||
<!-- Color Bar -->
|
||||
<SkeletonBox width="4px" height="48px" borderRadius="2px" />
|
||||
|
||||
<!-- Event Content -->
|
||||
<div class="flex-1 space-y-2">
|
||||
<!-- Time -->
|
||||
<SkeletonBox width="90px" height="12px" borderRadius="4px" />
|
||||
<!-- Title -->
|
||||
<SkeletonBox width="70%" height="16px" borderRadius="4px" />
|
||||
<!-- Location (occasionally) -->
|
||||
{#if eventIndex === 0}
|
||||
<SkeletonBox width="50%" height="14px" borderRadius="4px" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* AppLoadingSkeleton - Full-page loading skeleton for app initialization
|
||||
* Replaces spinner with calendar-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="Kalender wird geladen..."
|
||||
>
|
||||
<div class="w-full max-w-md px-6 space-y-8">
|
||||
<!-- Logo/Header Area -->
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<SkeletonBox width="64px" height="64px" borderRadius="16px" />
|
||||
<SkeletonBox width="180px" height="24px" borderRadius="8px" />
|
||||
</div>
|
||||
|
||||
<!-- Calendar Preview Skeleton -->
|
||||
<div class="bg-card rounded-xl p-4 space-y-4">
|
||||
<!-- Mini Calendar Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<SkeletonBox width="120px" height="20px" />
|
||||
<div class="flex gap-2">
|
||||
<SkeletonBox width="32px" height="32px" borderRadius="8px" />
|
||||
<SkeletonBox width="32px" height="32px" borderRadius="8px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weekday Headers -->
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
{#each Array(7) as _}
|
||||
<SkeletonBox width="100%" height="24px" borderRadius="4px" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Calendar Days Grid -->
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
{#each Array(35) as _, i}
|
||||
<div style="opacity: {Math.max(0.3, 1 - (i % 7) * 0.08)}">
|
||||
<SkeletonBox width="100%" height="32px" borderRadius="8px" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<div class="flex justify-center">
|
||||
<SkeletonBox width="140px" height="16px" borderRadius="4px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* CalendarViewSkeleton - Skeleton for the main calendar week view
|
||||
* Shows a week grid with time slots and placeholder events
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
|
||||
// 7 days of the week
|
||||
const days = Array(7);
|
||||
// Show hours from 8 to 18 (working hours)
|
||||
const hours = Array(10);
|
||||
</script>
|
||||
|
||||
<div class="calendar-skeleton" role="status" aria-label="Kalender wird geladen...">
|
||||
<!-- Header with day names -->
|
||||
<div class="calendar-header">
|
||||
<!-- Time column spacer -->
|
||||
<div class="time-spacer"></div>
|
||||
|
||||
<!-- Day headers -->
|
||||
{#each days as _, dayIndex}
|
||||
<div class="day-header" style="opacity: {Math.max(0.5, 1 - dayIndex * 0.05)}">
|
||||
<SkeletonBox width="32px" height="14px" borderRadius="4px" />
|
||||
<SkeletonBox width="28px" height="28px" borderRadius="50%" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Calendar grid -->
|
||||
<div class="calendar-grid">
|
||||
<!-- Time column -->
|
||||
<div class="time-column">
|
||||
{#each hours as _, hourIndex}
|
||||
<div class="time-slot" style="opacity: {Math.max(0.4, 1 - hourIndex * 0.05)}">
|
||||
<SkeletonBox width="36px" height="12px" borderRadius="4px" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Day columns -->
|
||||
{#each days as _, dayIndex}
|
||||
<div class="day-column" style="opacity: {Math.max(0.6, 1 - dayIndex * 0.04)}">
|
||||
{#each hours as _, hourIndex}
|
||||
<div class="hour-cell"></div>
|
||||
{/each}
|
||||
|
||||
<!-- Placeholder events -->
|
||||
{#if dayIndex === 1}
|
||||
<div class="event-placeholder" style="top: 10%; height: 15%;">
|
||||
<SkeletonBox width="100%" height="100%" borderRadius="6px" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if dayIndex === 2}
|
||||
<div class="event-placeholder" style="top: 30%; height: 10%;">
|
||||
<SkeletonBox width="100%" height="100%" borderRadius="6px" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if dayIndex === 3}
|
||||
<div class="event-placeholder" style="top: 50%; height: 20%;">
|
||||
<SkeletonBox width="100%" height="100%" borderRadius="6px" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if dayIndex === 4}
|
||||
<div class="event-placeholder" style="top: 20%; height: 8%;">
|
||||
<SkeletonBox width="100%" height="100%" borderRadius="6px" />
|
||||
</div>
|
||||
<div class="event-placeholder" style="top: 60%; height: 12%;">
|
||||
<SkeletonBox width="100%" height="100%" borderRadius="6px" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if dayIndex === 5}
|
||||
<div class="event-placeholder" style="top: 40%; height: 15%;">
|
||||
<SkeletonBox width="100%" height="100%" borderRadius="6px" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.calendar-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid hsl(var(--color-border) / 0.5);
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.time-spacer {
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.time-column {
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.time-slot {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.day-column {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border-left: 1px solid hsl(var(--color-border) / 0.3);
|
||||
}
|
||||
|
||||
.hour-cell {
|
||||
height: 60px;
|
||||
border-bottom: 1px solid hsl(var(--color-border) / 0.2);
|
||||
}
|
||||
|
||||
.event-placeholder {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* EventDetailSkeleton - Skeleton for event detail modal
|
||||
* Matches the layout of EventDetailModal
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="p-6 space-y-5" role="status" aria-label="Termin wird geladen...">
|
||||
<!-- Header with Title and Actions -->
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<SkeletonBox width="60%" height="24px" borderRadius="6px" />
|
||||
<div class="flex gap-2 flex-shrink-0">
|
||||
<SkeletonBox width="32px" height="32px" borderRadius="8px" />
|
||||
<SkeletonBox width="32px" height="32px" borderRadius="8px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Row -->
|
||||
<div class="flex items-start gap-3">
|
||||
<SkeletonBox width="20px" height="20px" borderRadius="50%" />
|
||||
<div class="flex flex-col gap-1.5 flex-1">
|
||||
<SkeletonBox width="60px" height="12px" borderRadius="4px" />
|
||||
<SkeletonBox width="120px" height="16px" borderRadius="4px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Row -->
|
||||
<div class="flex items-start gap-3">
|
||||
<SkeletonBox width="20px" height="20px" borderRadius="4px" />
|
||||
<div class="flex flex-col gap-1.5 flex-1">
|
||||
<SkeletonBox width="40px" height="12px" borderRadius="4px" />
|
||||
<SkeletonBox width="200px" height="16px" borderRadius="4px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Row -->
|
||||
<div class="flex items-start gap-3">
|
||||
<SkeletonBox width="20px" height="20px" borderRadius="4px" />
|
||||
<div class="flex flex-col gap-1.5 flex-1">
|
||||
<SkeletonBox width="40px" height="12px" borderRadius="4px" />
|
||||
<SkeletonBox width="160px" height="16px" borderRadius="4px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Row -->
|
||||
<div class="flex items-start gap-3">
|
||||
<SkeletonBox width="20px" height="20px" borderRadius="4px" />
|
||||
<div class="flex flex-col gap-1.5 flex-1">
|
||||
<SkeletonBox width="80px" height="12px" borderRadius="4px" />
|
||||
<div class="space-y-2">
|
||||
<SkeletonBox width="100%" height="14px" borderRadius="4px" />
|
||||
<SkeletonBox width="90%" height="14px" borderRadius="4px" />
|
||||
<SkeletonBox width="70%" height="14px" borderRadius="4px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attendees Row -->
|
||||
<div class="flex items-start gap-3">
|
||||
<SkeletonBox width="20px" height="20px" borderRadius="4px" />
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<SkeletonBox width="100px" height="12px" borderRadius="4px" />
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{#each Array(3) as _, i}
|
||||
<div class="flex items-center gap-2" style="opacity: {Math.max(0.5, 1 - i * 0.2)}">
|
||||
<SkeletonBox width="140px" height="14px" borderRadius="4px" />
|
||||
<SkeletonBox width="24px" height="18px" borderRadius="4px" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* RedirectSkeleton - Simple centered skeleton for redirect pages
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center justify-center min-h-[50vh] gap-4"
|
||||
role="status"
|
||||
aria-label="Weiterleitung..."
|
||||
>
|
||||
<SkeletonBox width="48px" height="48px" borderRadius="50%" />
|
||||
<SkeletonBox width="100px" height="16px" borderRadius="4px" />
|
||||
</div>
|
||||
21
apps/calendar/apps/web/src/lib/components/skeletons/index.ts
Normal file
21
apps/calendar/apps/web/src/lib/components/skeletons/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Calendar App Skeleton Components
|
||||
*
|
||||
* App-specific skeleton loaders that match the exact layout of calendar components.
|
||||
* Built on top of @manacore/shared-ui skeleton primitives.
|
||||
*/
|
||||
|
||||
// App Loading Skeleton
|
||||
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
|
||||
|
||||
// Agenda Skeleton
|
||||
export { default as AgendaSkeleton } from './AgendaSkeleton.svelte';
|
||||
|
||||
// Event Detail Skeleton
|
||||
export { default as EventDetailSkeleton } from './EventDetailSkeleton.svelte';
|
||||
|
||||
// Redirect Skeleton
|
||||
export { default as RedirectSkeleton } from './RedirectSkeleton.svelte';
|
||||
|
||||
// Calendar View Skeleton
|
||||
export { default as CalendarViewSkeleton } from './CalendarViewSkeleton.svelte';
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
import CalendarSidebar from '$lib/components/calendar/CalendarSidebar.svelte';
|
||||
import QuickEventOverlay from '$lib/components/event/QuickEventOverlay.svelte';
|
||||
import EventDetailModal from '$lib/components/event/EventDetailModal.svelte';
|
||||
import { CalendarViewSkeleton } from '$lib/components/skeletons';
|
||||
import { format, addMinutes } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
||||
|
|
@ -166,7 +167,9 @@
|
|||
<CalendarHeader />
|
||||
|
||||
<div class="calendar-content">
|
||||
{#if viewStore.viewType === 'day'}
|
||||
{#if !initialized}
|
||||
<CalendarViewSkeleton />
|
||||
{:else if viewStore.viewType === 'day'}
|
||||
<DayView onQuickCreate={handleQuickCreate} />
|
||||
{:else if viewStore.viewType === '5day'}
|
||||
<MultiDayView dayCount={5} onQuickCreate={handleQuickCreate} />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { format, parseISO, isToday, isTomorrow, addDays, startOfDay, endOfDay } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { AgendaSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
let loading = $state(true);
|
||||
|
||||
|
|
@ -81,7 +82,7 @@
|
|||
</header>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Laden...</div>
|
||||
<AgendaSkeleton />
|
||||
{:else if groupedEvents.length === 0}
|
||||
<div class="empty-state card">
|
||||
<p>Keine Termine in den nächsten 30 Tagen</p>
|
||||
|
|
@ -153,12 +154,6 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { RedirectSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
// Redirect to main calendar page with event modal
|
||||
onMount(() => {
|
||||
|
|
@ -10,34 +11,4 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="redirect-loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.redirect-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 50vh;
|
||||
gap: 1rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid hsl(var(--color-border));
|
||||
border-top-color: hsl(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<RedirectSkeleton />
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import EventForm from '$lib/components/event/EventForm.svelte';
|
||||
import type { CreateEventInput } from '@calendar/shared';
|
||||
import type { CreateEventInput, UpdateEventInput } from '@calendar/shared';
|
||||
import { addHours, parseISO } from 'date-fns';
|
||||
|
||||
let initialStart = $state<Date | null>(null);
|
||||
|
|
@ -25,8 +25,9 @@
|
|||
}
|
||||
});
|
||||
|
||||
async function handleSave(data: CreateEventInput) {
|
||||
const result = await eventsStore.createEvent(data);
|
||||
async function handleSave(data: CreateEventInput | UpdateEventInput) {
|
||||
// In create mode, data is always CreateEventInput
|
||||
const result = await eventsStore.createEvent(data as CreateEventInput);
|
||||
|
||||
if (result.error) {
|
||||
toast.error(`Fehler beim Erstellen: ${result.error.message}`);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { theme } from '$lib/stores/theme';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
||||
import { AppLoadingSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
|
|
@ -23,14 +24,7 @@
|
|||
<ToastContainer />
|
||||
|
||||
{#if 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()}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
18
apps/clock/apps/web/src/lib/components/skeletons/index.ts
Normal file
18
apps/clock/apps/web/src/lib/components/skeletons/index.ts
Normal 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';
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue