import Link from "next/link"; import { ChevronRight, Clock3 } from "lucide-react"; import { Badge } from "@/components/ui/Badge"; import { StatusDot } from "@/components/ui/StatusDot"; import type { ProjectResponse } from "@/lib/api/types"; import { formatProjectUpdatedAt, pendingReviewCount, } from "@/lib/projects/projects"; import { cardClass, cn, focusRing } from "@/lib/ui/variants"; interface ProjectCardProps { project: ProjectResponse; compact?: boolean; } // 缺一句话故事时的中性占位——避免卡片正文塌陷成空白(P3-2)。 const LOGLINE_PLACEHOLDER = "还没有一句话故事,进入写作台补上一句梗概。"; // 书脊/封面暖色板:全部取自现有语义 token(朱砂/琥珀),仅调透明度,无硬编码 hex。 const COVER_TINTS = [ "bg-[var(--color-cinnabar-wash)] text-cinnabar", "bg-cinnabar/10 text-cinnabar", "bg-cinnabar/[0.16] text-cinnabar", "bg-overdue/10 text-overdue", "bg-overdue/[0.18] text-overdue", "bg-surface-strong text-body-strong", ] as const; // 由 id/title 确定性取暖色底色块(djb2 变体哈希,同一本书永远同一色)。 function coverTint(seed: string): string { let hash = 0; for (const ch of seed) hash = (hash * 31 + (ch.codePointAt(0) ?? 0)) >>> 0; return COVER_TINTS[hash % COVER_TINTS.length] ?? COVER_TINTS[0]; } // 取书名首个字符作封面字(Array.from 保证 CJK/emoji 不被截断)。 function coverGlyph(title: string): string { return Array.from(title.trim())[0] ?? "书"; } interface CoverProps { project: ProjectResponse; size: "sm" | "md"; } // 书脊色块:填补此前的空白,给每本书一个稳定的视觉锚点。 function Cover({ project, size }: CoverProps) { const tint = coverTint(project.id || project.title || ""); const box = size === "md" ? "h-12 w-12 text-title-lg" : "h-10 w-10 text-title-md"; return ( ); } // 作品卡(UX §6.1):书脊色块 + 书名衬线 + 一句话故事 + 元信息行。 // M1 无字数/章数端点 → 不展示该统计(避免编造 API)。 export function ProjectCard({ project, compact = false }: ProjectCardProps) { const pendingCount = pendingReviewCount(project); const updatedLabel = formatProjectUpdatedAt(project.updated_at); const title = project.title || "未命名作品"; const logline = project.logline?.trim(); if (compact) { return ( 〈{title}〉 {logline ?? LOGLINE_PLACEHOLDER}