fix(storage): improve SEO, accessibility, and best practices for higher audit score

- Add meta description, Open Graph tags, theme-color, preconnect/dns-prefetch to app.html
- Add per-page meta descriptions (files, favorites, search, trash, shared, settings)
- Add ARIA attributes to all loading states (role="status", aria-live="polite")
- Add aria-label, aria-expanded, aria-haspopup to all menu buttons (FileCard, FolderCard, FileRow, FolderRow)
- Add role="menu" and role="menuitem" to all dropdown menus
- Add semantic table roles to FileList (role="table", role="columnheader", role="rowgroup")
- Add aria-label and progressbar ARIA to UploadZone
- Add role="img" with aria-label to emoji icons in trash
- Add aria-label to icon-only delete button in shared page
- Add aria-hidden to decorative SVGs and spinners
- Use type="search" with aria-label on search input
- Remove console.log statements from files, favorites, and search pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-21 11:22:29 +01:00
parent bbc5919448
commit 78526f1d92
17 changed files with 182 additions and 73 deletions

View file

@ -4,7 +4,15 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Storage - Dein sicherer Cloud-Speicher für Dateien, Ordner und mehr. Hochladen, organisieren und teilen." />
<meta name="theme-color" content="#64748b" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Storage - Cloud Drive" />
<meta property="og:description" content="Dein sicherer Cloud-Speicher für Dateien, Ordner und mehr." />
<meta property="og:site_name" content="Storage" />
<title>Storage - Cloud Drive</title>
<link rel="preconnect" href="https://stats.mana.how" />
<link rel="dns-prefetch" href="https://stats.mana.how" />
%sveltekit.head%
<!-- Umami Analytics -->
<script defer src="https://stats.mana.how/script.js" data-website-id="392ff51d-11f1-4f0c-9d55-6af1402a3ee6"></script>

View file

@ -64,21 +64,28 @@
<span class="file-name" title={file.name}>{file.name}</span>
<span class="file-size">{formatFileSize(file.size)}</span>
</div>
<button class="menu-button" onclick={handleMenuClick} type="button">
<button
class="menu-button"
onclick={handleMenuClick}
type="button"
aria-label="Aktionen für {file.name}"
aria-expanded={showMenu}
aria-haspopup="menu"
>
<DotsThreeVertical size={16} />
</button>
{#if showMenu}
<div class="menu-dropdown">
<button onclick={() => handleAction('download')}>Herunterladen</button>
<button onclick={() => handleAction('rename')}>Umbenennen</button>
<button onclick={() => handleAction('share')}>Teilen</button>
<button onclick={() => handleAction('favorite')}>
<div class="menu-dropdown" role="menu" aria-label="Dateiaktionen">
<button role="menuitem" onclick={() => handleAction('download')}>Herunterladen</button>
<button role="menuitem" onclick={() => handleAction('rename')}>Umbenennen</button>
<button role="menuitem" onclick={() => handleAction('share')}>Teilen</button>
<button role="menuitem" onclick={() => handleAction('favorite')}>
{file.isFavorite ? 'Favorit entfernen' : 'Als Favorit'}
</button>
<button onclick={() => handleAction('move')}>Verschieben</button>
<button role="menuitem" onclick={() => handleAction('move')}>Verschieben</button>
<hr />
<button class="danger" onclick={() => handleAction('delete')}>Löschen</button>
<button role="menuitem" class="danger" onclick={() => handleAction('delete')}>Löschen</button>
</div>
{/if}
</div>

View file

@ -32,14 +32,14 @@
}
</script>
<div class="file-list">
<div class="list-header">
<span class="col-name">Name</span>
<span class="col-size">Größe</span>
<span class="col-date">Geändert</span>
<span class="col-actions"></span>
<div class="file-list" role="table" aria-label="Dateien und Ordner">
<div class="list-header" role="row">
<span class="col-name" role="columnheader">Name</span>
<span class="col-size" role="columnheader">Größe</span>
<span class="col-date" role="columnheader">Geändert</span>
<span class="col-actions" role="columnheader"><span class="sr-only">Aktionen</span></span>
</div>
<div class="list-body">
<div class="list-body" role="rowgroup">
{#each folders as folder (folder.id)}
<FolderRow
{folder}
@ -84,6 +84,18 @@
background: rgb(var(--color-surface-elevated));
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
@media (max-width: 640px) {
.list-header {
grid-template-columns: 1fr 50px;

View file

@ -58,20 +58,29 @@
<span class="col-size">{formatFileSize(file.size)}</span>
<span class="col-date">{formatDate(file.updatedAt)}</span>
<span class="col-actions">
<button class="menu-button" onclick={handleMenuClick} type="button">
<button
class="menu-button"
onclick={handleMenuClick}
type="button"
aria-label="Aktionen für {file.name}"
aria-expanded={showMenu}
aria-haspopup="menu"
>
<DotsThreeVertical size={16} />
</button>
{#if showMenu}
<div class="menu-dropdown">
<button onclick={() => handleAction('download')}>Herunterladen</button>
<button onclick={() => handleAction('rename')}>Umbenennen</button>
<button onclick={() => handleAction('share')}>Teilen</button>
<button onclick={() => handleAction('favorite')}>
<div class="menu-dropdown" role="menu" aria-label="Dateiaktionen">
<button role="menuitem" onclick={() => handleAction('download')}>Herunterladen</button>
<button role="menuitem" onclick={() => handleAction('rename')}>Umbenennen</button>
<button role="menuitem" onclick={() => handleAction('share')}>Teilen</button>
<button role="menuitem" onclick={() => handleAction('favorite')}>
{file.isFavorite ? 'Favorit entfernen' : 'Als Favorit'}
</button>
<button onclick={() => handleAction('move')}>Verschieben</button>
<button role="menuitem" onclick={() => handleAction('move')}>Verschieben</button>
<hr />
<button class="danger" onclick={() => handleAction('delete')}>Löschen</button>
<button role="menuitem" class="danger" onclick={() => handleAction('delete')}
>Löschen</button
>
</div>
{/if}
</span>

View file

@ -49,20 +49,27 @@
<div class="folder-info">
<span class="folder-name" title={folder.name}>{folder.name}</span>
</div>
<button class="menu-button" onclick={handleMenuClick} type="button">
<button
class="menu-button"
onclick={handleMenuClick}
type="button"
aria-label="Aktionen für {folder.name}"
aria-expanded={showMenu}
aria-haspopup="menu"
>
<DotsThreeVertical size={16} />
</button>
{#if showMenu}
<div class="menu-dropdown">
<button onclick={() => handleAction('rename')}>Umbenennen</button>
<button onclick={() => handleAction('share')}>Teilen</button>
<button onclick={() => handleAction('favorite')}>
<div class="menu-dropdown" role="menu" aria-label="Ordneraktionen">
<button role="menuitem" onclick={() => handleAction('rename')}>Umbenennen</button>
<button role="menuitem" onclick={() => handleAction('share')}>Teilen</button>
<button role="menuitem" onclick={() => handleAction('favorite')}>
{folder.isFavorite ? 'Favorit entfernen' : 'Als Favorit'}
</button>
<button onclick={() => handleAction('move')}>Verschieben</button>
<button role="menuitem" onclick={() => handleAction('move')}>Verschieben</button>
<hr />
<button class="danger" onclick={() => handleAction('delete')}>Löschen</button>
<button role="menuitem" class="danger" onclick={() => handleAction('delete')}>Löschen</button>
</div>
{/if}
</div>

View file

@ -51,19 +51,28 @@
<span class="col-size"></span>
<span class="col-date">{formatDate(folder.updatedAt)}</span>
<span class="col-actions">
<button class="menu-button" onclick={handleMenuClick} type="button">
<button
class="menu-button"
onclick={handleMenuClick}
type="button"
aria-label="Aktionen für {folder.name}"
aria-expanded={showMenu}
aria-haspopup="menu"
>
<DotsThreeVertical size={16} />
</button>
{#if showMenu}
<div class="menu-dropdown">
<button onclick={() => handleAction('rename')}>Umbenennen</button>
<button onclick={() => handleAction('share')}>Teilen</button>
<button onclick={() => handleAction('favorite')}>
<div class="menu-dropdown" role="menu" aria-label="Ordneraktionen">
<button role="menuitem" onclick={() => handleAction('rename')}>Umbenennen</button>
<button role="menuitem" onclick={() => handleAction('share')}>Teilen</button>
<button role="menuitem" onclick={() => handleAction('favorite')}>
{folder.isFavorite ? 'Favorit entfernen' : 'Als Favorit'}
</button>
<button onclick={() => handleAction('move')}>Verschieben</button>
<button role="menuitem" onclick={() => handleAction('move')}>Verschieben</button>
<hr />
<button class="danger" onclick={() => handleAction('delete')}>Löschen</button>
<button role="menuitem" class="danger" onclick={() => handleAction('delete')}
>Löschen</button
>
</div>
{/if}
</span>

View file

@ -53,6 +53,7 @@
ondrop={handleDrop}
role="button"
tabindex="0"
aria-label="Dateien zum Hochladen hierher ziehen oder klicken"
onclick={openFileDialog}
onkeydown={(e) => e.key === 'Enter' && openFileDialog()}
>
@ -66,8 +67,15 @@
/>
{#if uploading}
<div class="upload-progress">
<div class="progress-bar">
<div class="upload-progress" role="status" aria-live="polite">
<div
class="progress-bar"
role="progressbar"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Upload-Fortschritt"
>
<div class="progress-fill" style="width: {progress}%"></div>
</div>
<span class="progress-text">Hochladen... {progress}%</span>

View file

@ -163,10 +163,16 @@
<ToastContainer />
{#if loading}
<div class="flex min-h-screen items-center justify-center bg-background">
<div
class="flex min-h-screen items-center justify-center bg-background"
role="status"
aria-live="polite"
aria-busy="true"
>
<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"
aria-hidden="true"
></div>
<p class="text-muted-foreground">Laden...</p>
</div>

View file

@ -8,6 +8,6 @@
});
</script>
<div class="flex min-h-screen items-center justify-center">
<div class="flex min-h-screen items-center justify-center" role="status" aria-live="polite">
<p class="text-muted-foreground">Weiterleitung...</p>
</div>

View file

@ -39,7 +39,7 @@
}
function handleFileClick(file: StorageFile) {
console.log('File clicked:', file);
// TODO: Open file preview
}
async function handleFileAction(action: string, file: StorageFile) {
@ -65,6 +65,7 @@
<svelte:head>
<title>Favoriten - Storage</title>
<meta name="description" content="Deine favorisierten Dateien und Ordner auf einen Blick." />
</svelte:head>
<div class="favorites-page">
@ -95,8 +96,8 @@
</div>
{#if loading}
<div class="loading-state">
<div class="spinner"></div>
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
{:else if error}

View file

@ -34,7 +34,6 @@
function handleFileClick(file: StorageFile) {
// TODO: Open file preview
console.log('File clicked:', file);
}
async function handleFileAction(action: string, file: StorageFile) {
@ -161,6 +160,10 @@
<svelte:head>
<title>Meine Dateien - Storage</title>
<meta
name="description"
content="Verwalte deine Dateien und Ordner in der Cloud. Hochladen, organisieren und teilen."
/>
</svelte:head>
<div class="files-page">
@ -207,8 +210,8 @@
{/if}
{#if filesStore.loading}
<div class="loading-state">
<div class="spinner"></div>
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
{:else if filesStore.error}

View file

@ -41,7 +41,7 @@
}
function handleFileClick(file: StorageFile) {
console.log('File clicked:', file);
// TODO: Open file preview
}
async function handleFileAction(action: string, file: StorageFile) {
@ -226,8 +226,8 @@
{/if}
{#if filesStore.loading}
<div class="loading-state">
<div class="spinner"></div>
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
{:else if filesStore.error}

View file

@ -34,8 +34,19 @@
<div class="min-h-screen bg-slate-900 flex items-center justify-center px-4">
<div class="text-center max-w-md">
<div class="mb-8">
<svg class="w-24 h-24 mx-auto text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a5 5 0 01-.354-7.072L8.95 7.636m1.414 5.657L7.535 16.12m8.485 0a5 5 0 01-7.07 0" />
<svg
class="w-24 h-24 mx-auto text-slate-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a5 5 0 01-.354-7.072L8.95 7.636m1.414 5.657L7.535 16.12m8.485 0a5 5 0 01-7.07 0"
/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3l18 18" />
</svg>
</div>
@ -54,22 +65,38 @@
{#if !isOnline}
<div class="space-y-4">
<a href="/" class="inline-flex items-center justify-center px-6 py-3 bg-slate-600 hover:bg-slate-700 text-white font-medium rounded-lg transition-colors">
<a
href="/"
class="inline-flex items-center justify-center px-6 py-3 bg-slate-600 hover:bg-slate-700 text-white font-medium rounded-lg transition-colors"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Zur Startseite
</a>
<button onclick={() => window.location.reload()} class="block w-full px-6 py-3 text-slate-400 hover:text-white transition-colors">
<button
onclick={() => window.location.reload()}
class="block w-full px-6 py-3 text-slate-400 hover:text-white transition-colors"
>
Erneut versuchen
</button>
</div>
{:else}
<div class="flex items-center justify-center text-green-400">
<svg class="w-5 h-5 mr-2 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<div class="flex items-center justify-center text-green-400" role="status" aria-live="polite">
<svg class="w-5 h-5 mr-2 animate-spin" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Weiterleitung...
</div>

View file

@ -57,12 +57,13 @@
}
function handleFileClick(file: StorageFile) {
console.log('File clicked:', file);
// TODO: Open file preview
}
</script>
<svelte:head>
<title>Suche - Storage</title>
<meta name="description" content="Durchsuche deine Dateien und Ordner in der Cloud." />
</svelte:head>
<div class="search-page">
@ -95,10 +96,11 @@
<div class="search-bar">
<MagnifyingGlass size={20} />
<input
type="text"
type="search"
bind:value={query}
onkeydown={handleKeydown}
placeholder="Dateien und Ordner durchsuchen..."
aria-label="Dateien und Ordner durchsuchen"
autofocus
/>
<button onclick={handleSearch} disabled={!query.trim() || loading}>
@ -107,8 +109,8 @@
</div>
{#if loading}
<div class="loading-state">
<div class="spinner"></div>
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Suche läuft...</p>
</div>
{:else if searched && files.length === 0 && folders.length === 0}

View file

@ -19,13 +19,14 @@
<svelte:head>
<title>Einstellungen - Storage</title>
<meta name="description" content="Passe Darstellung, Speicher und weitere Einstellungen an." />
</svelte:head>
<SettingsPage title="Einstellungen" subtitle="Passe die App an deine Vorlieben an.">
<!-- Appearance Section -->
<SettingsSection title="Darstellung">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -78,7 +79,7 @@
<!-- Storage Section -->
<SettingsSection title="Speicher">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -106,7 +107,7 @@
<!-- About Section -->
<SettingsSection title="Über">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"

View file

@ -70,6 +70,7 @@
<svelte:head>
<title>Geteilt - Storage</title>
<meta name="description" content="Verwalte deine geteilten Links für Dateien und Ordner." />
</svelte:head>
<div class="shared-page">
@ -81,8 +82,8 @@
</div>
{#if loading}
<div class="loading-state">
<div class="spinner"></div>
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
{:else if error}
@ -127,7 +128,11 @@
<Copy size={16} />
Link kopieren
</button>
<button class="delete-btn" onclick={() => deleteShare(share.id)}>
<button
class="delete-btn"
onclick={() => deleteShare(share.id)}
aria-label="Share-Link löschen"
>
<Trash size={16} />
</button>
</div>

View file

@ -84,6 +84,10 @@
<svelte:head>
<title>Papierkorb - Storage</title>
<meta
name="description"
content="Gelöschte Dateien und Ordner wiederherstellen oder endgültig löschen."
/>
</svelte:head>
<div class="trash-page">
@ -102,8 +106,8 @@
</div>
{#if loading}
<div class="loading-state">
<div class="spinner"></div>
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Laden...</p>
</div>
{:else if error}
@ -122,7 +126,7 @@
{#each folders as folder (folder.id)}
<div class="trash-item">
<div class="item-info">
<span class="item-icon folder">📁</span>
<span class="item-icon folder" role="img" aria-label="Ordner">📁</span>
<div class="item-details">
<span class="item-name">{folder.name}</span>
<span class="item-meta">Gelöscht am {formatDate(folder.deletedAt)}</span>
@ -142,7 +146,7 @@
{#each files as file (file.id)}
<div class="trash-item">
<div class="item-info">
<span class="item-icon file">📄</span>
<span class="item-icon file" role="img" aria-label="Datei">📄</span>
<div class="item-details">
<span class="item-name">{file.name}</span>
<span class="item-meta">Gelöscht am {formatDate(file.deletedAt)}</span>