terminal

PreviousNext

A terminal component.

Docs
eldorauiui

Preview

Loading preview…
registry/eldoraui/terminal.tsx
"use client"

import { useEffect, useMemo, useState, type ReactElement } from "react"
import { TerminalIcon } from "lucide-react"
import { GoTerminal } from "react-icons/go"

type TerminalStep = {
  text: string
  bold?: boolean
}

type TerminalProps = {
  command: string
  steps: TerminalStep[]
  pulseInterval?: number
  showLocalhost?: boolean
  hostBarTitle?: string
  hostMessage?: string
}

function MacControls() {
  return (
    <>
      <GoTerminal className="text-muted-foreground mr-1 size-4" />
      <div className="h-2 w-2 rounded-full bg-red-500" />
      <div className="h-2 w-2 rounded-full bg-yellow-500" />
      <div className="h-2 w-2 rounded-full bg-green-500" />
    </>
  )
}

function LocalHost({ title, message }: { title: string; message: string }) {
  return (
    <div className="bg-card animate-in fade-in slide-in-from-top-10 absolute right-4 bottom-5 z-10 overflow-hidden rounded-md border shadow-xl">
      <div className="bg-muted text-muted-foreground relative flex h-6 flex-row items-center border-b px-4 text-xs">
        <TerminalIcon className="absolute inset-2 size-3" />
        <p className="absolute inset-x-0 text-center">{title}</p>
      </div>
      <div className="text-card-foreground p-4 text-sm">{message}</div>
    </div>
  )
}

const Terminal = ({
  command,
  steps,
  pulseInterval = 100,
  showLocalhost = true,
  hostBarTitle = "localhost:3000",
  hostMessage = "New App Created!",
}: TerminalProps) => {
  const typingLen = useMemo(() => command.length, [command])
  const revealLen = useMemo(() => steps.length, [steps])
  const finalCount = typingLen + revealLen + 1

  const [counter, setCounter] = useState(finalCount)

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prev) => (prev >= finalCount ? prev : prev + 1))
    }, pulseInterval)
    return () => clearInterval(interval)
  }, [pulseInterval, finalCount])

  const elements: ReactElement[] = []

  const typedChars = Math.min(counter, typingLen)
  const isTyping = counter < typingLen

  elements.push(
    <span key="command" className="text-foreground">
      {command.substring(0, typedChars)}
      {isTyping && (
        <div className="bg-foreground inline-block h-3 w-1 animate-pulse" />
      )}
    </span>
  )

  if (!isTyping) {
    const shownSteps = Math.min(revealLen, counter - typingLen)
    if (shownSteps > 0) {
      elements.push(<span key="space"> </span>)
    }
    for (let i = 0; i < shownSteps; i++) {
      const step = steps[i]
      elements.push(
        <span
          key={`step-${i}`}
          className={
            step.bold ? "text-foreground font-bold" : "text-foreground"
          }
        >
          {step.text}
        </span>
      )
    }
  }

  const revealComplete = counter > typingLen + revealLen

  return (
    <div
      className="relative"
      onMouseEnter={() => {
        if (counter >= finalCount) {
          setCounter(0)
        }
      }}
    >
      {showLocalhost && revealComplete && (
        <LocalHost title={hostBarTitle} message={hostMessage} />
      )}

      <pre className="bg-card w-full min-w-[320px] overflow-hidden rounded-xl border text-[11px] shadow-lg sm:min-w-[480px] sm:text-[12px] md:min-w-[600px] md:text-[13px]">
        <div className="bg-muted flex flex-row items-center gap-2 border-b px-3 py-2 sm:px-4">
          <MacControls />
        </div>
        <div className="from-background to-muted min-h-[150px] bg-gradient-to-b sm:min-h-[180px] md:min-h-[200px]">
          <div className="grid p-3 whitespace-pre-wrap sm:p-4">{elements}</div>
        </div>
      </pre>
    </div>
  )
}

export default Terminal

Installation

npx shadcn@latest add @eldoraui/terminal

Usage

import { Terminal } from "@/components/ui/terminal"
<Terminal />