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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user