h8r4npp3p: Add the new-posts pill, feed list motion and app-wide reduced-motion switch-off

This commit is contained in:
2026-10-19 22:24:38 -06:00
parent f250cdf09c
commit 5e391ae678
10 changed files with 150 additions and 26 deletions
+2 -1
View File
@@ -236,7 +236,8 @@ export function Feed({
function showNew() {
setPosts((cur) => mergePosts(cur, pending));
setPending([]);
list.current?.scrollTo({ top: 0, behavior: "smooth" });
const calm = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
list.current?.scrollTo({ top: 0, behavior: calm ? "auto" : "smooth" });
}
return (
+25 -9
View File
@@ -1,6 +1,7 @@
import type { ReactNode, Ref } from "react";
import { canDelete, type CardPost } from "@/lib/feed-model";
import { Composer, type ComposerProps } from "./Composer";
import { GrowButton } from "./Buttons";
import { PostCard } from "./PostCard";
export type FeedStatus = "loading" | "ready" | "error";
@@ -33,6 +34,24 @@ function Spinner() {
return <span className="spinner" role="status" aria-label="Loading" />;
}
/** Shimmer placeholder card shown while the next page loads. */
function SkeletonCard() {
return (
<div className="feed-skel" role="status" aria-label="Loading more posts" data-testid="feed-skeleton">
<div className="feed-skel-head">
<span className="feed-skel-dot" />
<span className="feed-skel-lines">
<span className="feed-skel-line feed-skel-w40" />
<span className="feed-skel-line feed-skel-w25" />
</span>
</div>
<span className="feed-skel-line feed-skel-w100" />
<span className="feed-skel-line feed-skel-w80" />
<span className="feed-skel-media" />
</div>
);
}
function FeedPanel({ children, composer }: { children: ReactNode; composer?: ReactNode }) {
return (
<section className="tile panel feed" aria-labelledby="h-feed">
@@ -69,16 +88,17 @@ export function FeedView(p: FeedViewProps) {
<FeedPanel composer={p.composer ? <Composer {...p.composer} /> : null}>
<div ref={p.listRef} className="feed-list panel-scroll fade-edges">
{p.newCount > 0 ? (
<button type="button" className="new-posts" onClick={p.onShowNew}>
{p.newCount === 1 ? "1 new post" : `${p.newCount} new posts`}
</button>
<div className="feed-pill">
<GrowButton label={p.newCount === 1 ? "1 new post" : `${p.newCount} new posts`} onClick={p.onShowNew} />
</div>
) : null}
{p.posts.length === 0 ? (
<p className="feed-message">No posts yet.</p>
) : (
p.posts.map((post) => <PostCard
p.posts.map((post, i) => <PostCard
key={post.key}
post={post}
index={i}
now={p.now}
onToggleLike={p.onToggleLike}
likeBusy={p.likeBusy?.has(post.key)}
@@ -96,11 +116,7 @@ export function FeedView(p: FeedViewProps) {
<button type="button" onClick={p.onRetry}>Try again</button>
</div>
) : null}
{p.loadingMore ? (
<div className="feed-center">
<Spinner />
</div>
) : null}
{p.loadingMore ? <SkeletonCard /> : null}
<div ref={p.sentinelRef} className="sentinel" aria-hidden="true" />
</div>
</FeedPanel>
+5 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, type CSSProperties } from "react";
import { HexAvatar } from "@/components/HexAvatar";
import { CommentsSection } from "@/components/CommentsSection";
import { VideoPlayer } from "@/components/VideoPlayer";
@@ -10,6 +10,8 @@ import { formatDuration, relativeTime, type CardMedia, type CardPost } from "@/l
export interface PostCardProps {
post: CardPost;
now?: number;
/** Position in the list; the first cards rise in one after another. */
index?: number;
onToggleLike?: (post: CardPost) => void;
likeBusy?: boolean;
likeError?: boolean;
@@ -89,7 +91,7 @@ function PostVideo({ media, author }: { media: CardMedia; author: string }) {
);
}
export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSignedOut, canDelete, onDelete, deleteBusy, deleteError }: PostCardProps) {
export function PostCard({ post, now, index = 0, onToggleLike, likeBusy, likeError, onSignedOut, canDelete, onDelete, deleteBusy, deleteError }: PostCardProps) {
const [open, setOpen] = useState(false);
const [menu, setMenu] = useState(false);
const [confirming, setConfirming] = useState(false);
@@ -111,7 +113,7 @@ export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSigne
}, [menu]);
return (
<article className="post" data-testid="post-card">
<article className="post" data-testid="post-card" style={{ "--i": index < 8 ? index : 0 } as CSSProperties}>
<header className="post-head">
<HexAvatar name={post.authorName} pictureUrl={post.authorPictureUrl} size="lg" />
<div className="post-who">