| 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 |