返回 CodeWhale
terminal-capture.tsx
根目录 / web / components / terminal-capture.tsx
1 import type { CSSProperties } from "react";
2 import {
3 TERMINAL_CAPTURE_FRAMES,
4 type TerminalCaptureFrameId,
5 type TerminalCaptureStyle,
6 } from "@/lib/terminal-capture.generated";
7
8 const BRAILLE = /[⠀-⣿]/;
9
10 function runStyle(style: TerminalCaptureStyle): CSSProperties {
11 return {
12 color: style.fg,
13 backgroundColor: style.bg,
14 fontWeight: style.bold ? 700 : undefined,
15 fontStyle: style.italic ? "italic" : undefined,
16 opacity: style.dim ? 0.6 : undefined,
17 textDecoration: style.underline ? "underline" : style.strike ? "line-through" : undefined,
18 };
19 }
20
21 /**
22 * A real terminal frame as live HTML text: the PTY cells the terminal
23 * emulator parsed (lib/terminal-captures, compacted by
24 * scripts/render-terminal-capture.mjs), drawn as styled runs in the system
25 * monospace font. Nothing is retouched or staged.
26 *
27 * The whole frame always fits: the type scales with the frame's width and the
28 * grid's column count, like a screenshot, but stays live, selectable text.
29 * Assistive technology gets one summary (`label`); the glyph-by-glyph grid
30 * is presentation. Braille cells (the whale mark) are pinned to one column
31 * each, because system monospace fonts draw them from a fallback font.
32 */
33 export function TerminalCapture({
34 frame = "home",
35 label,
36 regionLabel,
37 }: {
38 frame?: TerminalCaptureFrameId;
39 label: string;
40 regionLabel: string;
41 }) {
42 const data = TERMINAL_CAPTURE_FRAMES[frame];
43 const styles = data.styles as readonly TerminalCaptureStyle[];
44 // The padding around the grid continues the frame's own top and bottom rows.
45 const top = styles[data.lines[0][0][1]].bg;
46 const bottom = styles[data.lines[data.lines.length - 1][0][1]].bg;
47 return (
48 <div className="term-capture-scroll" role="region" aria-label={regionLabel} tabIndex={0}>
49 <pre
50 className="term-capture"
51 role="img"
52 aria-label={label}
53 data-capture-file={data.file}
54 style={{ background: `linear-gradient(${top}, ${bottom})`, ["--cols" as string]: data.cols } as CSSProperties}
55 >
56 {data.lines.map((runs, row) => (
57 <span key={row} className="term-capture-row">
58 {runs.map(([text, index], run) => (
59 <span key={run} style={runStyle(styles[index])}>
60 {BRAILLE.test(text)
61 ? [...text].map((ch, i) =>
62 BRAILLE.test(ch) ? (
63 <span key={i} className="term-capture-cell">
64 {ch}
65 </span>
66 ) : (
67 ch
68 ),
69 )
70 : text}
71 </span>
72 ))}
73 </span>
74 ))}
75 </pre>
76 </div>
77 );
78 }
79
79 lines Plain Text