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>
51 lines
1.6 KiB
TypeScript
51 lines
1.6 KiB
TypeScript
// Body scroll-lock for open dialogs/sheets. While any locker is active, the
|
|
// document body can't scroll behind the overlay. Ref-counted so that nested or
|
|
// stacked dialogs (e.g. a ConfirmSheet opened from the LightboxModal) don't let
|
|
// the first one to close unlock the page out from under the others. Compensates
|
|
// for the vanished scrollbar width so the layout doesn't jump on lock.
|
|
|
|
import type { ActionReturn } from 'svelte/action';
|
|
|
|
let lockCount = 0;
|
|
let savedOverflow = '';
|
|
let savedPaddingRight = '';
|
|
|
|
function lock() {
|
|
if (typeof document === 'undefined') return;
|
|
if (lockCount === 0) {
|
|
const body = document.body;
|
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
savedOverflow = body.style.overflow;
|
|
savedPaddingRight = body.style.paddingRight;
|
|
body.style.overflow = 'hidden';
|
|
if (scrollbarWidth > 0) {
|
|
const current = parseFloat(getComputedStyle(body).paddingRight) || 0;
|
|
body.style.paddingRight = `${current + scrollbarWidth}px`;
|
|
}
|
|
}
|
|
lockCount++;
|
|
}
|
|
|
|
function unlock() {
|
|
if (typeof document === 'undefined') return;
|
|
lockCount = Math.max(0, lockCount - 1);
|
|
if (lockCount === 0) {
|
|
document.body.style.overflow = savedOverflow;
|
|
document.body.style.paddingRight = savedPaddingRight;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Locks body scroll for the lifetime of the node. Mount the node only while the
|
|
* dialog is open (e.g. inside `{#if open}` or alongside a `class:` toggle) so the
|
|
* action's create/destroy line up with open/close.
|
|
*/
|
|
export function scrollLock(node: HTMLElement): ActionReturn {
|
|
lock();
|
|
return {
|
|
destroy() {
|
|
unlock();
|
|
}
|
|
};
|
|
}
|