mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:21:09 +02:00
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:
parent
ba6b953723
commit
9f66800945
6 changed files with 182 additions and 16 deletions
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue