Search

Ctrl + K

Terminal

This document outlines the steps to create Terminal component styled withTailwind CSS and using some npm dependency libraries.

Prerequisites

  • 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.
Step 1: Create TypeScript types
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
Step 2: Create Terminal component
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