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:
@@ -127,9 +127,9 @@
|
||||
|
||||
function roleColor(r: string | null): string {
|
||||
switch (r) {
|
||||
case 'admin': return 'bg-red-100 text-red-700';
|
||||
case 'host': return 'bg-purple-100 text-purple-700';
|
||||
default: return 'bg-blue-100 text-blue-700';
|
||||
case 'admin': return 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-200';
|
||||
case 'host': return 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-200';
|
||||
default: return 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
|
||||
<div class="min-h-screen bg-gray-50 pb-24 dark:bg-gray-950">
|
||||
<!-- Header -->
|
||||
<div class="border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="border-b border-gray-200 bg-white pt-[env(safe-area-inset-top)] dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="mx-auto flex max-w-lg items-center px-4 py-4">
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">Mein Konto</h1>
|
||||
</div>
|
||||
@@ -213,7 +213,7 @@
|
||||
<span class="font-mono text-4xl font-bold tracking-widest text-gray-900 dark:text-gray-100">{$pin}</span>
|
||||
<button
|
||||
onclick={copyPin}
|
||||
class="rounded-md bg-amber-100 px-3 py-1.5 text-sm font-medium text-amber-800 transition hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-900/60"
|
||||
class="inline-flex min-h-11 items-center rounded-md bg-amber-100 px-4 py-2 text-sm font-medium text-amber-800 transition hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-900/60"
|
||||
>
|
||||
{pinCopied ? 'Kopiert!' : 'Kopieren'}
|
||||
</button>
|
||||
@@ -250,9 +250,9 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2 p-3" role="radiogroup" aria-label="Design">
|
||||
{#each [
|
||||
{ value: 'system', label: 'System', icon: '🖥️' },
|
||||
{ value: 'light', label: 'Hell', icon: '☀️' },
|
||||
{ value: 'dark', label: 'Dunkel', icon: '🌙' }
|
||||
{ value: 'system', label: 'System' },
|
||||
{ value: 'light', label: 'Hell' },
|
||||
{ value: 'dark', label: 'Dunkel' }
|
||||
] as opt (opt.value)}
|
||||
{@const selected = $themePreference === opt.value}
|
||||
<button
|
||||
@@ -265,7 +265,16 @@
|
||||
? 'border-blue-600 bg-blue-50 text-blue-700 dark:border-blue-500 dark:bg-blue-950/40 dark:text-blue-200'
|
||||
: 'border-gray-200 text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-700/50'}"
|
||||
>
|
||||
<span class="text-2xl leading-none">{opt.icon}</span>
|
||||
<!-- SVG icons render consistently everywhere (emoji show as tofu on some Android). -->
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
{#if opt.value === 'system'}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25A2.25 2.25 0 0 1 5.25 3h13.5A2.25 2.25 0 0 1 21 5.25Z" />
|
||||
{:else if opt.value === 'light'}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
|
||||
{/if}
|
||||
</svg>
|
||||
<span class="font-medium">{opt.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user