feat(storage): replace loading spinners with skeleton shimmer screens

Add FileSkeletonGrid and FileSkeletonList components that match the
real card/row layout. Applied to files, folders, favorites, and trash
pages with view-mode-aware skeleton selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-26 12:58:36 +01:00
parent ba6b953723
commit 9f66800945
6 changed files with 182 additions and 16 deletions

View file

@ -0,0 +1,74 @@
<script lang="ts">
interface Props {
count?: number;
}
let { count = 8 }: Props = $props();
</script>
<div class="skeleton-grid" role="status" aria-label="Dateien werden geladen">
{#each Array(count) as _, i}
<div class="skeleton-card" style="opacity: {1 - i * 0.08}">
<div class="skeleton-icon shimmer"></div>
<div class="skeleton-name shimmer"></div>
<div class="skeleton-size shimmer"></div>
</div>
{/each}
</div>
<style>
.skeleton-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.skeleton-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1.25rem 1rem;
background: rgb(var(--color-surface-elevated));
border: 1px solid rgb(var(--color-border));
border-radius: var(--radius-lg);
}
.skeleton-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
}
.skeleton-name {
width: 80%;
height: 14px;
border-radius: var(--radius-sm);
}
.skeleton-size {
width: 40%;
height: 11px;
border-radius: var(--radius-sm);
}
.shimmer {
background: linear-gradient(
90deg,
rgb(var(--color-border) / 0.3) 25%,
rgb(var(--color-border) / 0.6) 50%,
rgb(var(--color-border) / 0.3) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>

View file

@ -0,0 +1,85 @@
<script lang="ts">
interface Props {
count?: number;
}
let { count = 6 }: Props = $props();
</script>
<div class="skeleton-list" role="status" aria-label="Dateien werden geladen">
{#each Array(count) as _, i}
<div class="skeleton-row" style="opacity: {1 - i * 0.1}">
<div class="skeleton-icon shimmer"></div>
<div class="skeleton-name shimmer"></div>
<div class="skeleton-size shimmer"></div>
<div class="skeleton-date shimmer"></div>
</div>
{/each}
</div>
<style>
.skeleton-list {
display: flex;
flex-direction: column;
}
.skeleton-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid rgb(var(--color-border));
}
.skeleton-icon {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.skeleton-name {
flex: 1;
height: 14px;
max-width: 200px;
border-radius: var(--radius-sm);
}
.skeleton-size {
width: 60px;
height: 12px;
border-radius: var(--radius-sm);
}
.skeleton-date {
width: 100px;
height: 12px;
border-radius: var(--radius-sm);
}
.shimmer {
background: linear-gradient(
90deg,
rgb(var(--color-border) / 0.3) 25%,
rgb(var(--color-border) / 0.6) 50%,
rgb(var(--color-border) / 0.3) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@media (max-width: 640px) {
.skeleton-date {
display: none;
}
}
</style>

View file

@ -9,6 +9,8 @@
import FileGrid from '$lib/components/files/FileGrid.svelte';
import FileList from '$lib/components/files/FileList.svelte';
import FilePreviewModal from '$lib/components/files/FilePreviewModal.svelte';
import FileSkeletonGrid from '$lib/components/files/FileSkeletonGrid.svelte';
import FileSkeletonList from '$lib/components/files/FileSkeletonList.svelte';
let previewFile = $state<StorageFile | null>(null);
let files = $state<StorageFile[]>([]);
@ -98,10 +100,11 @@
</div>
{#if loading}
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
{#if filesStore.viewMode === 'grid'}
<FileSkeletonGrid />
{:else}
<FileSkeletonList />
{/if}
{:else if error}
<div class="error-state">
<p>Fehler: {error}</p>

View file

@ -11,6 +11,8 @@
import UploadZone from '$lib/components/files/UploadZone.svelte';
import NewFolderModal from '$lib/components/files/NewFolderModal.svelte';
import FilePreviewModal from '$lib/components/files/FilePreviewModal.svelte';
import FileSkeletonGrid from '$lib/components/files/FileSkeletonGrid.svelte';
import FileSkeletonList from '$lib/components/files/FileSkeletonList.svelte';
let previewFile = $state<StorageFile | null>(null);
let showUploadZone = $state(false);
@ -234,10 +236,11 @@
{/if}
{#if filesStore.loading}
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
{#if filesStore.viewMode === 'grid'}
<FileSkeletonGrid />
{:else}
<FileSkeletonList />
{/if}
{:else if filesStore.error}
<div class="error-state">
<p>Fehler: {filesStore.error}</p>

View file

@ -12,6 +12,8 @@
import UploadZone from '$lib/components/files/UploadZone.svelte';
import NewFolderModal from '$lib/components/files/NewFolderModal.svelte';
import FilePreviewModal from '$lib/components/files/FilePreviewModal.svelte';
import FileSkeletonGrid from '$lib/components/files/FileSkeletonGrid.svelte';
import FileSkeletonList from '$lib/components/files/FileSkeletonList.svelte';
let previewFile = $state<StorageFile | null>(null);
let showUploadZone = $state(false);
@ -250,10 +252,11 @@
{/if}
{#if filesStore.loading}
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
{#if filesStore.viewMode === 'grid'}
<FileSkeletonGrid />
{:else}
<FileSkeletonList />
{/if}
{:else if filesStore.error}
<div class="error-state">
<p>Fehler: {filesStore.error}</p>

View file

@ -5,6 +5,7 @@
import { StorageEvents } from '@manacore/shared-utils/analytics';
import type { StorageFile, StorageFolder } from '$lib/api/client';
import { toastStore } from '@manacore/shared-ui';
import FileSkeletonList from '$lib/components/files/FileSkeletonList.svelte';
let files = $state<StorageFile[]>([]);
let folders = $state<StorageFolder[]>([]);
@ -109,10 +110,7 @@
</div>
{#if loading}
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
<FileSkeletonList />
{:else if error}
<div class="error-state">
<p>Fehler: {error}</p>