a11y: viewport-fit, reduced-motion, like aria-pressed, labels, contrast (M13-M18)

M13: add viewport-fit=cover so env(safe-area-inset-*) resolves on notched phones.
M14: like buttons (list card, grid overlay, lightbox) get aria-pressed + a
descriptive aria-label so AT announces self-state, not just the shared count.
M15: a prefers-reduced-motion media query neutralizes the decorative keyframes
(Ken Burns, crossfade, HeartBurst) and snaps transitions.
M16: join/recover name + PIN inputs and the lightbox comment input get aria-labels.
M17: FeedGrid overlay like/comment buttons get aria-labels and ≥44px hit areas.
M18: bump light-mode secondary text from gray-400 to gray-500 (keeping
dark:text-gray-400) across the app for WCAG AA contrast.

Also raises the PIN inputs to 6 digits (placeholder, maxlength, slice, and the
auto-submit threshold) to match the new 6-digit generated PINs; legacy 4-digit
PINs still submit via the button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-27 16:26:21 +02:00
parent 5bd008591b
commit 0737288ed9
13 changed files with 69 additions and 40 deletions

View File

@@ -1 +1,18 @@
@import "./tailwind-theme.css"; @import "./tailwind-theme.css";
/*
* Respect the OS "reduce motion" setting (WCAG 2.3.3 / 2.2.2). Neutralizes the
* decorative animations — Ken Burns zoom, slideshow crossfade, HeartBurst — and
* snaps transitions, which is a vestibular/migraine safeguard especially for the
* big-screen diashow. Auto-advance timing is additionally slowed in JS.
*/
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="text-scale" content="scale" /> <meta name="text-scale" content="scale" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<!-- <!--

View File

@@ -67,7 +67,9 @@
<p class="truncate text-xs font-medium text-white">{upload.uploader_name}</p> <p class="truncate text-xs font-medium text-white">{upload.uploader_name}</p>
<div class="mt-0.5 flex items-center gap-3 text-xs text-white/80"> <div class="mt-0.5 flex items-center gap-3 text-xs text-white/80">
<button <button
class="pointer-events-auto flex items-center gap-0.5" class="pointer-events-auto -m-2 flex min-h-11 min-w-11 items-center justify-center gap-0.5 p-2"
aria-pressed={upload.liked_by_me}
aria-label="Gefällt mir ({upload.like_count})"
onclick={(e) => { e.stopPropagation(); onlike(upload.id); }} onclick={(e) => { e.stopPropagation(); onlike(upload.id); }}
> >
<svg class="h-3.5 w-3.5 {upload.liked_by_me ? 'fill-red-400 text-red-400' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-3.5 w-3.5 {upload.liked_by_me ? 'fill-red-400 text-red-400' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -76,7 +78,8 @@
{upload.like_count} {upload.like_count}
</button> </button>
<button <button
class="pointer-events-auto flex items-center gap-0.5" class="pointer-events-auto -m-2 flex min-h-11 min-w-11 items-center justify-center gap-0.5 p-2"
aria-label="Kommentare ({upload.comment_count})"
onclick={(e) => { e.stopPropagation(); oncomment(upload.id); }} onclick={(e) => { e.stopPropagation(); oncomment(upload.id); }}
> >
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">

View File

@@ -100,7 +100,7 @@
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">{upload.uploader_name}</p> <p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">{upload.uploader_name}</p>
<p class="text-xs text-gray-400 dark:text-gray-500">{relativeTime(upload.created_at)}</p> <p class="text-xs text-gray-500 dark:text-gray-400">{relativeTime(upload.created_at)}</p>
</div> </div>
</div> </div>
@@ -164,6 +164,8 @@
<div class="flex items-center gap-4 px-4 py-2"> <div class="flex items-center gap-4 px-4 py-2">
<button <button
onclick={() => { vibrate(10); onlike(upload.id); }} onclick={() => { vibrate(10); onlike(upload.id); }}
aria-pressed={upload.liked_by_me}
aria-label="Gefällt mir ({upload.like_count})"
class="flex items-center gap-1.5 text-sm font-medium transition-colors class="flex items-center gap-1.5 text-sm font-medium transition-colors
{upload.liked_by_me ? 'text-red-500 dark:text-red-400' : 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}" {upload.liked_by_me ? 'text-red-500 dark:text-red-400' : 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
> >
@@ -188,7 +190,7 @@
{upload.comment_count} {upload.comment_count}
</button> </button>
{#if isOwn} {#if isOwn}
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span> <span class="ml-auto text-xs text-gray-500 dark:text-gray-400">Eigener Beitrag</span>
{/if} {/if}
</div> </div>

View File

@@ -141,10 +141,12 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<span id="lightbox-title" class="font-medium text-gray-900 dark:text-gray-100">{upload.uploader_name}</span> <span id="lightbox-title" class="font-medium text-gray-900 dark:text-gray-100">{upload.uploader_name}</span>
<span class="ml-2 text-xs text-gray-400 dark:text-gray-500">{formatTime(upload.created_at)}</span> <span class="ml-2 text-xs text-gray-500 dark:text-gray-400">{formatTime(upload.created_at)}</span>
</div> </div>
<button <button
onclick={() => onlike(upload.id)} onclick={() => onlike(upload.id)}
aria-pressed={upload.liked_by_me}
aria-label="Gefällt mir ({upload.like_count})"
class="flex items-center gap-1 rounded-full px-2.5 py-1 text-sm transition { class="flex items-center gap-1 rounded-full px-2.5 py-1 text-sm transition {
upload.liked_by_me upload.liked_by_me
? 'bg-red-50 text-red-600 dark:bg-red-950/40 dark:text-red-300' ? 'bg-red-50 text-red-600 dark:bg-red-950/40 dark:text-red-300'
@@ -165,7 +167,7 @@
<!-- Comments list --> <!-- Comments list -->
<div class="flex-1 overflow-y-auto p-3"> <div class="flex-1 overflow-y-auto p-3">
{#if comments.length === 0} {#if comments.length === 0}
<p class="text-center text-sm text-gray-400 dark:text-gray-500">Noch keine Kommentare.</p> <p class="text-center text-sm text-gray-500 dark:text-gray-400">Noch keine Kommentare.</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each comments as comment (comment.id)} {#each comments as comment (comment.id)}
@@ -173,7 +175,7 @@
<div class="flex-1"> <div class="flex-1">
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{comment.uploader_name}</span> <span class="text-sm font-medium text-gray-900 dark:text-gray-100">{comment.uploader_name}</span>
<span class="ml-1 text-sm text-gray-700 dark:text-gray-300">{comment.body}</span> <span class="ml-1 text-sm text-gray-700 dark:text-gray-300">{comment.body}</span>
<div class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">{formatTime(comment.created_at)}</div> <div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{formatTime(comment.created_at)}</div>
</div> </div>
{#if comment.user_id === userId} {#if comment.user_id === userId}
<button <button
@@ -201,6 +203,7 @@
<input <input
type="text" type="text"
bind:value={newComment} bind:value={newComment}
aria-label="Kommentar schreiben"
placeholder="Kommentar schreiben..." placeholder="Kommentar schreiben..."
maxlength={COMMENT_MAX} maxlength={COMMENT_MAX}
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500" class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"

View File

@@ -161,7 +161,7 @@
</div> </div>
</div> </div>
{#if expiry} {#if expiry}
<p class="mt-3 text-xs text-gray-400 dark:text-gray-500">Sitzung gültig bis {formatDate(expiry)}</p> <p class="mt-3 text-xs text-gray-500 dark:text-gray-400">Sitzung gültig bis {formatDate(expiry)}</p>
{/if} {/if}
</div> </div>
@@ -334,7 +334,7 @@
style="width: {quotaPercent}%" style="width: {quotaPercent}%"
></div> ></div>
</div> </div>
<p class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Geschätzt für {$quotaStore.active_uploaders} aktive Beitragende. Geschätzt für {$quotaStore.active_uploaders} aktive Beitragende.
</p> </p>
</div> </div>
@@ -361,7 +361,7 @@
href="/recover" href="/recover"
class="flex items-center gap-3 px-5 py-4 transition hover:bg-gray-50 dark:hover:bg-gray-700/50" class="flex items-center gap-3 px-5 py-4 transition hover:bg-gray-50 dark:hover:bg-gray-700/50"
> >
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <svg class="h-5 w-5 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 8.25h3m-3 3h3m-3 3h3" /> <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 8.25h3m-3 3h3m-3 3h3" />
</svg> </svg>
<span class="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300">Gerät wechseln / PIN nutzen</span> <span class="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300">Gerät wechseln / PIN nutzen</span>

View File

@@ -412,7 +412,7 @@
<div class="mx-auto max-w-3xl p-4"> <div class="mx-auto max-w-3xl p-4">
{#if loading} {#if loading}
<div class="py-16 text-center text-gray-400 dark:text-gray-500">Laden…</div> <div class="py-16 text-center text-gray-500 dark:text-gray-400">Laden…</div>
{:else if error} {:else if error}
<div class="rounded-lg bg-red-50 p-4 text-sm text-red-700">{error}</div> <div class="rounded-lg bg-red-50 p-4 text-sm text-red-700">{error}</div>
{:else} {:else}
@@ -451,7 +451,7 @@
style="width: {diskPct(stats)}%" style="width: {diskPct(stats)}%"
></div> ></div>
</div> </div>
<p class="mt-1.5 text-xs text-gray-400 dark:text-gray-500">{formatBytes(stats.disk_free_bytes)} frei</p> <p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">{formatBytes(stats.disk_free_bytes)} frei</p>
</div> </div>
{/if} {/if}
</div> </div>
@@ -552,7 +552,7 @@
</button> </button>
</div> </div>
{#if exportJobs.length === 0} {#if exportJobs.length === 0}
<p class="text-sm text-gray-400 dark:text-gray-500">Noch keine Export-Jobs.</p> <p class="text-sm text-gray-500 dark:text-gray-400">Noch keine Export-Jobs.</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each exportJobs as job} {#each exportJobs as job}
@@ -589,7 +589,7 @@
<!-- Search --> <!-- Search -->
<div class="p-4"> <div class="p-4">
<div class="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-700 dark:bg-gray-900"> <div class="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-700 dark:bg-gray-900">
<svg class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg class="h-4 w-4 shrink-0 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg> </svg>
<input <input
@@ -601,7 +601,7 @@
</div> </div>
</div> </div>
{#if filteredUsers.length === 0} {#if filteredUsers.length === 0}
<p class="px-5 py-8 text-center text-sm text-gray-400 dark:text-gray-500">Keine Treffer.</p> <p class="px-5 py-8 text-center text-sm text-gray-500 dark:text-gray-400">Keine Treffer.</p>
{:else} {:else}
<div class="divide-y divide-gray-100 dark:divide-gray-700"> <div class="divide-y divide-gray-100 dark:divide-gray-700">
{#each filteredUsers as user} {#each filteredUsers as user}
@@ -618,7 +618,7 @@
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/40 dark:text-red-200">Gesperrt</span> <span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/40 dark:text-red-200">Gesperrt</span>
{/if} {/if}
</div> </div>
<p class="text-xs text-gray-400 dark:text-gray-500"> <p class="text-xs text-gray-500 dark:text-gray-400">
{user.upload_count} Upload{user.upload_count !== 1 ? 's' : ''} · {formatBytes(user.total_upload_bytes)} {user.upload_count} Upload{user.upload_count !== 1 ? 's' : ''} · {formatBytes(user.total_upload_bytes)}
</p> </p>
</div> </div>

View File

@@ -177,7 +177,7 @@
<div class="mx-auto max-w-lg space-y-4 p-4"> <div class="mx-auto max-w-lg space-y-4 p-4">
{#if loading} {#if loading}
<div class="py-16 text-center text-gray-400 dark:text-gray-500">Laden…</div> <div class="py-16 text-center text-gray-500 dark:text-gray-400">Laden…</div>
{:else if loadError} {:else if loadError}
<div class="rounded-xl border border-gray-200 bg-white p-6 text-center dark:border-gray-700 dark:bg-gray-800"> <div class="rounded-xl border border-gray-200 bg-white p-6 text-center dark:border-gray-700 dark:bg-gray-800">
<p class="font-medium text-gray-700 dark:text-gray-300">Status konnte nicht geladen werden.</p> <p class="font-medium text-gray-700 dark:text-gray-300">Status konnte nicht geladen werden.</p>
@@ -205,7 +205,7 @@
<div class="min-w-0"> <div class="min-w-0">
<h2 class="font-semibold text-gray-900 dark:text-gray-100">ZIP-Archiv</h2> <h2 class="font-semibold text-gray-900 dark:text-gray-100">ZIP-Archiv</h2>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">Alle Original-Fotos und Videos in strukturierten Ordnern.</p> <p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">Alle Original-Fotos und Videos in strukturierten Ordnern.</p>
<p class="mt-1 text-xs {status.zip.status === 'done' ? 'text-green-600 dark:text-green-400' : status.zip.status === 'failed' ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'}"> <p class="mt-1 text-xs {status.zip.status === 'done' ? 'text-green-600 dark:text-green-400' : status.zip.status === 'failed' ? 'text-red-500 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'}">
{statusText(status.zip)} {statusText(status.zip)}
</p> </p>
</div> </div>
@@ -232,7 +232,7 @@
<div class="min-w-0"> <div class="min-w-0">
<h2 class="font-semibold text-gray-900 dark:text-gray-100">HTML-Viewer</h2> <h2 class="font-semibold text-gray-900 dark:text-gray-100">HTML-Viewer</h2>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">Schöne Offline-Galerie mit Filterung, Kommentaren und Likes — kein Internet nötig.</p> <p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">Schöne Offline-Galerie mit Filterung, Kommentaren und Likes — kein Internet nötig.</p>
<p class="mt-1 text-xs {status.html.status === 'done' ? 'text-green-600 dark:text-green-400' : status.html.status === 'failed' ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'}"> <p class="mt-1 text-xs {status.html.status === 'done' ? 'text-green-600 dark:text-green-400' : status.html.status === 'failed' ? 'text-red-500 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'}">
{statusText(status.html)} {statusText(status.html)}
</p> </p>
</div> </div>

View File

@@ -456,7 +456,7 @@
<div class="mx-auto max-w-2xl px-4 pb-3"> <div class="mx-auto max-w-2xl px-4 pb-3">
<div class="relative"> <div class="relative">
<div class="flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 focus-within:border-blue-400 focus-within:bg-white focus-within:ring-1 focus-within:ring-blue-200 dark:border-gray-700 dark:bg-gray-800 dark:focus-within:border-blue-500 dark:focus-within:bg-gray-800"> <div class="flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 focus-within:border-blue-400 focus-within:bg-white focus-within:ring-1 focus-within:ring-blue-200 dark:border-gray-700 dark:bg-gray-800 dark:focus-within:border-blue-500 dark:focus-within:bg-gray-800">
<svg class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg class="h-4 w-4 shrink-0 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg> </svg>
<input <input
@@ -493,7 +493,7 @@
onmousedown={() => selectSuggestion(item)} onmousedown={() => selectSuggestion(item)}
> >
{#if item.type === 'user'} {#if item.type === 'user'}
<svg class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <svg class="h-4 w-4 shrink-0 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg> </svg>
<span class="font-medium text-gray-900 dark:text-gray-100">{item.value}</span> <span class="font-medium text-gray-900 dark:text-gray-100">{item.value}</span>
@@ -590,7 +590,7 @@
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-2xl">
{#if displayUploads.length === 0} {#if displayUploads.length === 0}
<div class="py-16 text-center"> <div class="py-16 text-center">
<p class="text-sm text-gray-400 dark:text-gray-500">Keine Treffer für die gewählten Filter.</p> <p class="text-sm text-gray-500 dark:text-gray-400">Keine Treffer für die gewählten Filter.</p>
<button onclick={clearFilters} class="mt-2 text-sm text-blue-600 hover:underline dark:text-blue-400">Filter zurücksetzen</button> <button onclick={clearFilters} class="mt-2 text-sm text-blue-600 hover:underline dark:text-blue-400">Filter zurücksetzen</button>
</div> </div>
{:else} {:else}

View File

@@ -288,7 +288,7 @@
<div class="mx-auto max-w-3xl space-y-3 p-4"> <div class="mx-auto max-w-3xl space-y-3 p-4">
{#if loading} {#if loading}
<div class="py-16 text-center text-gray-400 dark:text-gray-500">Laden…</div> <div class="py-16 text-center text-gray-500 dark:text-gray-400">Laden…</div>
{:else if error} {:else if error}
<div class="rounded-lg bg-red-50 p-4 text-sm text-red-700 dark:bg-red-950/30 dark:text-red-300">{error}</div> <div class="rounded-lg bg-red-50 p-4 text-sm text-red-700 dark:bg-red-950/30 dark:text-red-300">{error}</div>
{:else if event} {:else if event}
@@ -301,7 +301,7 @@
> >
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Statistiken</h2> <h2 class="font-semibold text-gray-900 dark:text-gray-100">Statistiken</h2>
<svg <svg
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {statsOpen ? 'rotate-180' : ''}" class="h-5 w-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 {statsOpen ? 'rotate-180' : ''}"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
> >
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
@@ -324,7 +324,7 @@
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p> <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
</div> </div>
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60"> <div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
<p class="text-2xl font-bold {event.export_released ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-gray-500'}"> <p class="text-2xl font-bold {event.export_released ? 'text-blue-600 dark:text-blue-400' : 'text-gray-500 dark:text-gray-400'}">
{event.export_released ? 'Ja' : 'Nein'} {event.export_released ? 'Ja' : 'Nein'}
</p> </p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Freigegeben</p> <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Freigegeben</p>
@@ -341,7 +341,7 @@
> >
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Event-Einstellungen</h2> <h2 class="font-semibold text-gray-900 dark:text-gray-100">Event-Einstellungen</h2>
<svg <svg
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {settingsOpen ? 'rotate-180' : ''}" class="h-5 w-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 {settingsOpen ? 'rotate-180' : ''}"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
> >
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
@@ -376,7 +376,7 @@
> >
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Nutzerverwaltung</h2> <h2 class="font-semibold text-gray-900 dark:text-gray-100">Nutzerverwaltung</h2>
<svg <svg
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {usersOpen ? 'rotate-180' : ''}" class="h-5 w-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 {usersOpen ? 'rotate-180' : ''}"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
> >
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
@@ -387,7 +387,7 @@
<!-- Search --> <!-- Search -->
<div class="px-4 py-3"> <div class="px-4 py-3">
<div class="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-700 dark:bg-gray-900"> <div class="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-700 dark:bg-gray-900">
<svg class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg class="h-4 w-4 shrink-0 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg> </svg>
<input <input
@@ -399,7 +399,7 @@
</div> </div>
</div> </div>
{#if filteredUsers.length === 0} {#if filteredUsers.length === 0}
<p class="px-5 py-8 text-center text-sm text-gray-400 dark:text-gray-500">Keine Treffer.</p> <p class="px-5 py-8 text-center text-sm text-gray-500 dark:text-gray-400">Keine Treffer.</p>
{:else} {:else}
<div class="divide-y divide-gray-100 dark:divide-gray-700"> <div class="divide-y divide-gray-100 dark:divide-gray-700">
{#each filteredUsers as user} {#each filteredUsers as user}
@@ -416,7 +416,7 @@
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/40 dark:text-red-200">Gesperrt</span> <span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/40 dark:text-red-200">Gesperrt</span>
{/if} {/if}
</div> </div>
<p class="text-xs text-gray-400 dark:text-gray-500"> <p class="text-xs text-gray-500 dark:text-gray-400">
{user.upload_count} Upload{user.upload_count !== 1 ? 's' : ''} · {formatBytes(user.total_upload_bytes)} {user.upload_count} Upload{user.upload_count !== 1 ? 's' : ''} · {formatBytes(user.total_upload_bytes)}
</p> </p>
</div> </div>

View File

@@ -100,10 +100,10 @@
// user doesn't have to chase the (now cosmetic) Anmelden button. // user doesn't have to chase the (now cosmetic) Anmelden button.
function onRecoveryPinInput(e: Event) { function onRecoveryPinInput(e: Event) {
const el = e.currentTarget as HTMLInputElement; const el = e.currentTarget as HTMLInputElement;
const cleaned = el.value.replace(/\D/g, '').slice(0, 4); const cleaned = el.value.replace(/\D/g, '').slice(0, 6);
if (cleaned !== el.value) el.value = cleaned; if (cleaned !== el.value) el.value = cleaned;
recoveryPin = cleaned; recoveryPin = cleaned;
if (recoveryPin.length === 4 && !recoveryLoading) { if (recoveryPin.length === 6 && !recoveryLoading) {
handleInlineRecover(); handleInlineRecover();
} }
} }
@@ -131,8 +131,9 @@
type="text" type="text"
value={recoveryPin} value={recoveryPin}
oninput={onRecoveryPinInput} oninput={onRecoveryPinInput}
placeholder="4-stelliger PIN" aria-label="Wiederherstellungs-PIN"
maxlength={4} placeholder="6-stelliger PIN"
maxlength={6}
inputmode="numeric" inputmode="numeric"
pattern="[0-9]*" pattern="[0-9]*"
data-testid="recovery-pin-input" data-testid="recovery-pin-input"
@@ -170,6 +171,7 @@
<input <input
type="text" type="text"
bind:value={displayName} bind:value={displayName}
aria-label="Dein Name"
placeholder="Dein Name" placeholder="Dein Name"
maxlength={50} maxlength={50}
data-testid="join-name-input" data-testid="join-name-input"

View File

@@ -58,10 +58,10 @@
// invalid intermediate state. // invalid intermediate state.
function onPinInput(e: Event) { function onPinInput(e: Event) {
const el = e.currentTarget as HTMLInputElement; const el = e.currentTarget as HTMLInputElement;
const cleaned = el.value.replace(/\D/g, '').slice(0, 4); const cleaned = el.value.replace(/\D/g, '').slice(0, 6);
if (cleaned !== el.value) el.value = cleaned; if (cleaned !== el.value) el.value = cleaned;
pin = cleaned; pin = cleaned;
if (pin.length === 4 && displayName.trim() && !loading) { if (pin.length === 6 && displayName.trim() && !loading) {
handleRecover(); handleRecover();
} }
} }
@@ -89,6 +89,7 @@
<input <input
type="text" type="text"
bind:value={displayName} bind:value={displayName}
aria-label="Dein Name"
placeholder="Dein Name" placeholder="Dein Name"
maxlength={50} maxlength={50}
data-testid="recover-name-input" data-testid="recover-name-input"
@@ -98,8 +99,9 @@
type="text" type="text"
value={pin} value={pin}
oninput={onPinInput} oninput={onPinInput}
placeholder="4-stelliger PIN" aria-label="Wiederherstellungs-PIN"
maxlength={4} placeholder="6-stelliger PIN"
maxlength={6}
inputmode="numeric" inputmode="numeric"
pattern="[0-9]*" pattern="[0-9]*"
data-testid="recover-pin-input" data-testid="recover-pin-input"

View File

@@ -177,7 +177,7 @@
</svg> </svg>
<div> <div>
<p class="font-medium text-gray-500 dark:text-gray-400">Keine Dateien ausgewählt</p> <p class="font-medium text-gray-500 dark:text-gray-400">Keine Dateien ausgewählt</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Geh zurück und tippe auf den Plus-Button.</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Geh zurück und tippe auf den Plus-Button.</p>
</div> </div>
<button <button
onclick={cancel} onclick={cancel}