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>
56 lines
1.7 KiB
Svelte
56 lines
1.7 KiB
Svelte
<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>
|