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:
55
frontend/src/lib/components/IconButton.svelte
Normal file
55
frontend/src/lib/components/IconButton.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
// Icon-only button with a guaranteed ≥44px touch target (WCAG 2.5.5 / Apple HIG).
|
||||
// The visible icon stays whatever size the caller renders; the hit area is
|
||||
// enforced via min-h-11 min-w-11 (44px) so small glyphs are still comfortably
|
||||
// tappable. Always require an `aria-label` since there's no text content.
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
/** Accessible name — required, the button has no visible text. */
|
||||
label: string;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
type?: 'button' | 'submit';
|
||||
tone?: 'neutral' | 'danger';
|
||||
/** Marks toggle state for AT (e.g. like buttons). Omit for plain actions. */
|
||||
pressed?: boolean;
|
||||
/** Extra classes appended after the base styles. */
|
||||
class?: string;
|
||||
'data-testid'?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
onclick,
|
||||
disabled = false,
|
||||
title,
|
||||
type = 'button',
|
||||
tone = 'neutral',
|
||||
pressed,
|
||||
class: extra = '',
|
||||
'data-testid': testid,
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
const toneClass = $derived(
|
||||
tone === 'danger'
|
||||
? 'text-red-600 hover:bg-red-50 active:bg-red-100 dark:text-red-400 dark:hover:bg-red-950/40 dark:active:bg-red-950/60'
|
||||
: 'text-gray-500 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:active:bg-gray-700 dark:hover:text-gray-100'
|
||||
);
|
||||
</script>
|
||||
|
||||
<button
|
||||
{type}
|
||||
{onclick}
|
||||
{disabled}
|
||||
{title}
|
||||
aria-label={label}
|
||||
aria-pressed={pressed}
|
||||
data-testid={testid}
|
||||
class="inline-flex min-h-11 min-w-11 items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:pointer-events-none {toneClass} {extra}"
|
||||
>
|
||||
{@render children()}
|
||||
</button>
|
||||
Reference in New Issue
Block a user