返回 CodeWhale
session-media.tsx
根目录 / web / components / session-media.tsx
1 /**
2 * <SessionMedia> — renders one entry from web/lib/media-manifest.ts.
3 *
4 * Two states, both honest:
5 *
6 * pending — a plainly labeled panel: "Recording pending release
7 * candidate". No mock terminal, no staged screenshot, no
8 * recycled clip. The panel exists so the absence of footage is
9 * visible and explained, not hidden.
10 *
11 * published — a <video> with the declared poster, per-locale WebVTT
12 * captions, a transcript link, and a GIF fallback link. The
13 * reduced-motion contract is structural (REDUCED_MOTION_POLICY
14 * = "static-poster-no-autoplay"): preload="none", no autoPlay,
15 * user-initiated playback only. The optional GIF is a link,
16 * not an autoplaying reduced-motion substitute.
17 *
18 * Server component: no client JS, SSG-safe.
19 */
20
21 import { REDUCED_MOTION_POLICY, type MediaAsset } from "@/lib/media-manifest";
22 import { StatusBadge } from "./status-badge";
23
24 const MEDIA_PLAN_DOC =
25 "https://github.com/Hmbown/CodeWhale/blob/main/docs/releases/v0.9.2-media-plan.md";
26 const REPO_BLOB_BASE = "https://github.com/Hmbown/CodeWhale/blob/main";
27
28 export function SessionMedia({ asset, locale = "en" }: { asset: MediaAsset; locale?: string }) {
29 const isZh = locale === "zh";
30
31 if (asset.status === "pending") {
32 return (
33 <figure
34 className="session-media session-media-pending"
35 data-media-id={asset.id}
36 data-media-status={asset.status}
37 data-reduced-motion-policy={REDUCED_MOTION_POLICY}
38 >
39 <div className="session-media-stage">
40 <StatusBadge kind="pending" locale={locale} label={asset.pendingLabel} />
41 <p className="session-media-pending-note">
42 {isZh
43 ? "此处不展示占位或摆拍影像。录制完成后,这段真实会话将带海报帧、双语字幕、文字稿和可选的 GIF 回退下载一同发布。"
44 : "No placeholder or staged footage is shown here. When the recording exists, the real session ships with a poster frame, bilingual captions, a transcript, and an optional GIF fallback download."}
45 </p>
46 </div>
47 <figcaption className="session-media-caption">
48 <strong>{isZh ? asset.title.zh : asset.title.en}</strong>
49 <span>{isZh ? asset.description.zh : asset.description.en}</span>
50 <a href={MEDIA_PLAN_DOC} target="_blank" rel="noreferrer">
51 {isZh ? "录制计划与验收清单 ↗" : "Recording plan and acceptance checklist ↗"}
52 </a>
53 </figcaption>
54 </figure>
55 );
56 }
57
58 // status === "published": manifest tests require every field and verify
59 // file presence/byte budgets plus poster dimensions. The human recording
60 // checklist remains authoritative for actual video duration/dimensions.
61 const poster = asset.poster!;
62 const video = asset.video!;
63 const captions = asset.captions ?? [];
64
65 return (
66 <figure
67 className="session-media"
68 data-media-id={asset.id}
69 data-media-status={asset.status}
70 data-reduced-motion-policy={REDUCED_MOTION_POLICY}
71 >
72 <video
73 controls
74 preload="none"
75 poster={`/${poster.src}`}
76 width={video.width}
77 height={video.height}
78 aria-label={isZh ? poster.alt.zh : poster.alt.en}
79 >
80 <source src={`/${video.src}`} type="video/mp4" />
81 {captions.map((track) => (
82 <track
83 key={track.srclang}
84 kind="captions"
85 src={`/${track.src}`}
86 srcLang={track.srclang}
87 label={track.label}
88 default={track.srclang === "en"}
89 />
90 ))}
91 </video>
92 <figcaption className="session-media-caption">
93 <strong>{isZh ? asset.title.zh : asset.title.en}</strong>
94 <span>{isZh ? asset.description.zh : asset.description.en}</span>
95 {asset.gifFallback && (
96 <a href={`/${asset.gifFallback.src}`}>
97 {isZh ? "GIF 下载回退(无视频环境)" : "GIF fallback download (no-video environments)"}
98 </a>
99 )}
100 {asset.transcript && (
101 <a href={`${REPO_BLOB_BASE}/${asset.transcript}`} target="_blank" rel="noreferrer">
102 {isZh ? "文字稿 ↗" : "Transcript ↗"}
103 </a>
104 )}
105 </figcaption>
106 </figure>
107 );
108 }
109
109 lines Plain Text