This document outlines the steps to create Terminal
component styled withTailwind CSS and using some npm dependency libraries.
Node.js
and npm
installed on your machine.Tailwind CSS
installed in your project.CVA(class-variance-authority)
is a utility for managing CSS class names based on various conditions.clsx
is a tiny utility for constructing className strings conditionally.1// terminal.types.ts
2type Variant = "success" | "error" | "default";
3
4export interface Output {
5 text: string;
6 variant: Variant;
7}
8
9export interface CommandLine {
10 id: string;
11 prompt: string;
12 command: string;
13 outputs: Output[];
14}
15
16export interface TerminalProps {
17 welcome: string;
18 prompt: string;
19 commands: CommandLine[];
20 className?: string;
21 onCommand?: (command: string) => void;
22}
23
1// terminal.component.tsx
2import clsx from "clsx";
3import { useRef } from "react";
4import type { TerminalProps } from "./terminal.types";
5
6const Terminal: React.FC<TerminalProps> = ({
7 welcome,
8 prompt,
9 commands,
10 className,
11 onCommand,
12}) => {
13 const scrollRef = useRef<HTMLDivElement>(null);
14 const inputRef = useRef<HTMLInputElement>(null);
15 const value = useRef<string>("");
16
17 const stopPropagation = (e: React.MouseEvent<HTMLDivElement>) => {
18 e.stopPropagation();
19 };
20
21 const handleClick = () => {
22 const input = inputRef.current;
23 if (!input) return;
24 input.focus();
25 input.setSelectionRange(-1, -1);
26 };
27
28 const handleChangeText = (e: React.ChangeEvent<HTMLInputElement>) => {
29 value.current = e.target.value;
30 };
31
32 const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
33 if (e.key === "Enter") {
34 onCommand?.(value.current);
35 value.current = "";
36 inputRef.current!.value = "";
37 setTimeout(() => {
38 scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight);
39 });
40 }
41 };
42
43 return (
44 <div
45 ref={scrollRef}
46 className={clsx(
47 "overflow-auto rounded-md bg-neutral-900 px-4 py-6 text-sm font-semibold",
48 className,
49 )}
50 onClick={handleClick}
51 >
52 <p className="whitespace-pre text-white">{welcome}</p>
53 {commands.map((item) => (
54 <div key={item.id} className="mt-2">
55 <div className="flex items-center text-white">
56 <p className="text-neutral-400">{item.prompt}</p>
57 <p className="ml-2 text-neutral-100">{item.command}</p>
58 </div>
59 {item.outputs.length > 0 && (
60 <div className="mt-1">
61 {item.outputs.map((output, index) => (
62 <p
63 key={index}
64 className={clsx("whitespace-pre", {
65 "text-lime-300": output.variant === "success",
66 "text-red-400": output.variant === "error",
67 "text-gray-400": output.variant === "default",
68 })}
69 >
70 {output.text}
71 </p>
72 ))}
73 </div>
74 )}
75 </div>
76 ))}
77 <div className="mt-2">
78 <div className="flex items-center text-white">
79 <p className="text-neutral-400">{prompt}</p>
80 <div className="ml-2 flex-1 text-neutral-100">
81 <input
82 ref={inputRef}
83 type="text"
84 className="w-full bg-transparent outline-none"
85 onClick={stopPropagation}
86 onChange={handleChangeText}
87 onKeyDown={handleKeyDown}
88 />
89 </div>
90 </div>
91 </div>
92 </div>
93 );
94};
95
96export default Terminal;
97