feat(eventsnap): UX review batch-3 — feed virtualization, a11y/UX fixes, shared primitives

Frontend
- Feed: DOM-windowing via @tanstack/svelte-virtual (new VirtualFeed). The window
  virtualizer keeps the sticky header, pull-to-refresh, infinite-scroll sentinel and
  bottom nav working. List = dynamic measured heights keyed by upload id +
  anchorTo:start so an SSE prepend doesn't jump a scrolled reader; grid = measured
  square rows. Drops the content-visibility band-aid and the old FeedGrid.
  measureElement(null) on row unmount prevents ResizeObserver retention; stable
  option callbacks + guarded setOptions avoid O(n) re-measure on like/comment patches.
- Global: viewport-fit=cover (activates safe-area insets), lang=de, PWA manifest +
  maskable icon + apple-touch metas, prefers-reduced-motion, safe-area-top headers.
- Feed reactivity: like/comment SSE patch the single card in place; upload-processed
  debounced in-place merge; IntersectionObserver leak fixed; shared 60s clock.
- CLS: list cards reserve the skeleton aspect box.
- Destructive actions (promote/demote/unban, gallery release) routed through
  ConfirmSheet (host + admin).
- Export OOM: streamed download via single-use ticket instead of res.blob().
- Shared primitives: IconButton (44px hit area), scrollLock action; UploadSheet a11y.
- Polish: api timeout + non-JSON guard, focus-trap offsetParent fix, pull-to-refresh
  passive:false + guards, diashow keyboard a11y, Toaster assertive errors, dark-mode
  gaps, aria-pressed on like/chip state, CameraCapture mic-on-video + retry,
  ConfirmSheet spinner, upload beforeunload warning, emoji->SVG icons.

Backend
- social.rs: like/comment SSE broadcasts now carry the fresh count so feed clients
  patch one card in place instead of refetching page 1.
- admin.rs / main.rs: supporting changes for the above.

Verified: svelte-check 0 errors, frontend build clean, /feed SSR 200. Runtime feed
scroll-feel validation still owed (documented in FOLLOWUPS.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-06-30 19:16:48 +02:00
parent bbec815854
commit e79e020566
42 changed files with 1245 additions and 328 deletions

View File

@@ -6,6 +6,7 @@
<script lang="ts">
import { focusTrap } from '$lib/actions/focus-trap';
import { scrollLock } from '$lib/actions/scroll-lock';
interface Props {
open: boolean;
@@ -50,6 +51,7 @@
class="fixed inset-0 z-50 bg-black/40"
aria-label="Schließen"
onclick={onCancel}
disabled={busy}
tabindex="-1"
></button>
<div
@@ -60,6 +62,7 @@
aria-labelledby={titleId}
data-testid="confirm-sheet"
use:focusTrap={{ onclose: onCancel }}
use:scrollLock
>
<div class="mb-4 flex justify-center">
<div class="h-1 w-10 rounded-full bg-gray-300 dark:bg-gray-600"></div>
@@ -77,17 +80,21 @@
onclick={handleConfirm}
disabled={busy}
data-testid="confirm-sheet-confirm"
class="mb-3 w-full rounded-xl py-3 text-sm font-semibold text-white transition disabled:opacity-60 {tone === 'danger'
class="mb-3 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-sm font-semibold text-white transition disabled:opacity-60 {tone === 'danger'
? 'bg-red-600 hover:bg-red-700 active:bg-red-700 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400'
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400'}"
>
{#if busy}
<span class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-white" aria-hidden="true"></span>
{/if}
{confirmLabel}
</button>
<button
type="button"
onclick={onCancel}
disabled={busy}
data-testid="confirm-sheet-cancel"
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50 active:bg-gray-100 disabled:opacity-60 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
>
{cancelLabel}
</button>