返回 CodeWhale
native-terminal-gallery.tsx
根目录 / web / components / native-terminal-gallery.tsx
1 "use client";
2
3 import { useId, useState } from "react";
4 import Link from "next/link";
5 import { Icon } from "@/components/icon";
6 import { TerminalCapture } from "@/components/terminal-capture";
7 import { getNativeTerminalCopy, NATIVE_TERMINAL_VIEWS } from "@/lib/content/native-terminal";
8 import { TERMINAL_CAPTURE_FRAMES, type TerminalCaptureFrameId } from "@/lib/terminal-capture.generated";
9 import { cueWhale, type WhaleCue } from "@/lib/whale-motion";
10 import "./native-terminal-gallery.css";
11
12 // What the homepage whale acts out beside each captured view. Illustration
13 // only: it mirrors the screen the reader picked, not a running session.
14 const WHALE_CUES: Record<string, WhaleCue> = {
15 home: { presence: "Idle", label: "rest", chosen: true },
16 composer: { presence: "Listening", label: "listen", chosen: true },
17 workbar: { presence: "Working", kind: "editing", label: "write", chosen: true },
18 "workbar-fleet": { presence: "Working", kind: "delegating", parallel: 3, label: "pod", chosen: true },
19 "provider-picker": { presence: "Working", kind: "network", label: "connect", chosen: true },
20 help: { presence: "Thinking", label: "think", chosen: true },
21 };
22
23 function hasCapture(id: string): id is TerminalCaptureFrameId {
24 return Object.prototype.hasOwnProperty.call(TERMINAL_CAPTURE_FRAMES, id);
25 }
26
27 // New captured views appear when their real frame is added to the generated
28 // module. A named view without a capture never becomes a website button.
29 const views = Object.keys(NATIVE_TERMINAL_VIEWS).flatMap((id) =>
30 hasCapture(id) ? [{ id }] : [],
31 );
32
33 /** Website controls select real PTY captures; the terminal itself is unchanged. */
34 export function NativeTerminalGallery({
35 locale,
36 defaultFrame = "composer",
37 label,
38 regionLabel,
39 }: {
40 locale: string;
41 defaultFrame?: TerminalCaptureFrameId;
42 label: string;
43 regionLabel: string;
44 }) {
45 const [frame, setFrame] = useState<TerminalCaptureFrameId>(defaultFrame);
46 const captureId = useId();
47 const selected = views.find((view) => view.id === frame) ?? views[0];
48 if (!selected) return null;
49 const copy = getNativeTerminalCopy(locale);
50 const { label: viewLabel, description } = copy.views[selected.id];
51
52 return (
53 <div className="native-terminal-gallery">
54 <div className="native-terminal-controls" role="group" aria-label={copy.viewsLabel}>
55 {views.map((view) => (
56 <button
57 key={view.id}
58 type="button"
59 className="native-terminal-button"
60 aria-pressed={selected.id === view.id}
61 aria-controls={captureId}
62 onClick={() => {
63 setFrame(view.id);
64 // Only a reader's choice moves the whale; the page loads at rest.
65 const cue = WHALE_CUES[view.id];
66 if (cue) cueWhale(cue);
67 }}
68 >
69 {copy.views[view.id].label}
70 </button>
71 ))}
72 </div>
73 <p className="native-terminal-description" aria-live="polite" aria-atomic="true">{description}</p>
74 <div className="native-terminal-view" id={captureId} dir="ltr">
75 <TerminalCapture
76 frame={selected.id}
77 label={selected.id === "home" ? label : `${viewLabel}. ${description}`}
78 regionLabel={`${regionLabel} · ${viewLabel}`}
79 />
80 </div>
81 <div className="native-terminal-footer">
82 <Link href={`/${locale}/ratatui`} className="native-terminal-components-link">
83 {copy.componentsLink}
84 <Icon name="arrow-right" className="icon icon-flip" />
85 </Link>
86 </div>
87 </div>
88 );
89 }
90
90 lines Plain Text