Lost
Seen 5,150 timesJUJuniper
Question
Grey, sealed, one corner dented. Last seen at the rail yards.
React 帖子卡片组件,用于社区动态:封面带浏览量,作者头像压在封面边缘,斜体标题前标有帖子类型,下附一行正文摘要。
# once per project: where @zzzui items come frompnpm dlx shadcn@latest registry add @zzzui=https://zzzui.catforest.dev/r/{name}.jsonpnpm dlx shadcn@latest add @zzzui/post-cardGrey, sealed, one corner dented. Last seen at the rail yards.
Pays well. Leave before midnight.
Anything not through by then waits until morning.
Thirty-one stalls, eleven nights, one map.
Bring two, take two. No spares, no trades.
Pick a post.
import { type ComponentType, useState } from "react";import { Avatar, AvatarFallback } from "@/components/zero/avatar";import { CellIcon } from "@/components/zero/icons/cell";import { CrateIcon } from "@/components/zero/icons/crate";import { FlareIcon } from "@/components/zero/icons/flare";import { KeyIcon } from "@/components/zero/icons/key";import { KitIcon } from "@/components/zero/icons/kit";import { Masonry } from "@/components/zero/masonry";import { PostCard, PostCardAuthor, PostCardCover, PostCardExcerpt, PostCardTitle,} from "@/components/zero/post-card";/** A poster for a cover: a ground, a big leaning headline, an item icon. */function Poster({ tone, ink, icon: Icon, line, tall,}: { tone: string; ink: string; icon: ComponentType<{ className?: string }>; line: string; tall?: boolean;}) { return ( <div className={`grid grid-rows-[auto_1fr] justify-items-center gap-2 overflow-hidden bg-(image:--zero-hatch) px-4 pt-10 pb-7 ${tall ? "aspect-[4/5]" : "aspect-[16/10]"}`} style={{ backgroundColor: tone, color: ink }} > <span className="justify-self-stretch origin-left rotate-(--zero-tilt) font-black font-zero text-zero-xl uppercase italic leading-none"> {line} </span> <Icon className="size-16 self-center opacity-90" /> </div> );}const posts = [ { id: 1, kind: "Question", title: "Has anyone seen this crate?", excerpt: "Grey, sealed, one corner dented. Last seen at the rail yards.", author: "Juniper", views: 5150, poster: { tone: "var(--color-zero-rarity-legendary)", ink: "var(--color-zero-ink)", icon: CrateIcon, line: "Lost", tall: true, }, }, { id: 2, kind: "Commission", title: "Need a courier: one box, two streets, no questions", excerpt: "Pays well. Leave before midnight.", author: "B-Side", views: 3301, poster: { tone: "var(--color-zero-rarity-rare)", ink: "var(--color-zero-ink)", icon: KeyIcon, line: "Two streets", }, }, { id: 3, kind: "Notice", title: "The north gate shuts at two tonight", excerpt: "Anything not through by then waits until morning.", author: "Dispatch", views: 199861, poster: { tone: "var(--color-zero-accent)", ink: "var(--color-zero-ink)", icon: FlareIcon, line: "Gate shuts", tall: true, }, }, { id: 4, kind: "Post", title: "Every power cell vendor after midnight, ranked", excerpt: "Thirty-one stalls, eleven nights, one map.", author: "NoodleCartographer", views: 27014, poster: { tone: "var(--color-zero-raised)", ink: "var(--color-zero-rarity-uncommon)", icon: CellIcon, line: "Ranked", }, }, { id: 5, kind: "Post", title: "Kit swap at the old market, Thursday", excerpt: "Bring two, take two. No spares, no trades.", author: "Rook", views: 880, poster: { tone: "var(--color-zero-rarity-epic)", ink: "var(--color-zero-ink)", icon: KitIcon, line: "Swap", }, },];export default function PostCardFeed() { const [open, setOpen] = useState<string | null>(null); return ( <div className="grid w-full gap-4"> <Masonry columnWidth="15rem"> {posts.map((post) => ( <PostCard key={post.id}> <PostCardCover views={post.views}> <Poster {...post.poster} /> </PostCardCover> <PostCardAuthor avatar={ <Avatar> <AvatarFallback> {post.author.slice(0, 2).toUpperCase()} </AvatarFallback> </Avatar> } > {post.author} </PostCardAuthor> <PostCardTitle kind={post.kind}> <button type="button" onClick={() => setOpen(post.title)}> {post.title} </button> </PostCardTitle> <PostCardExcerpt>{post.excerpt}</PostCardExcerpt> </PostCard> ))} </Masonry> <p className="font-medium font-zero-text text-zero-sm text-zero-text-2"> {open ? `Opened: ${open}` : "Pick a post."} </p> </div> );}图片铺满卡片宽度,角落显示浏览量;作者头像压在图片下边缘,旁边是作者名。标题为斜体,前面的小斜切块标出帖子类型(「Question」「Commission」),其后是一行正文摘要,超出部分以省略号截断。
在 PostCardTitle 中放入链接或按钮:它的点击区域会覆盖整张卡片。悬停时卡片上浮并显示强调色边框,聚焦时显示黄色焦点框;整张卡片在 Tab 顺序中只占一个位置,并以标题作为可访问名称。多张卡片可用 Masonry 排布。开启「减少动态效果」时,卡片不上浮。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| views | number | — | 浏览量,显示在角落。 |
| viewsLabel | (views: string) => string | "Seen {views} times" | 读屏软件为浏览次数读出的文字,参数是已格式化的数字。 |
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| avatar | ReactNode | — | 一个 Avatar,压在封面下边缘。 |
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| kind | string | — | 帖子类型,以小斜切块显示在标题前。 |