UIPackage
Menu

Terminal

terminal ui
Edit on GitHub

Terminal/command-line display with a macOS-style title bar, command history, prompt character, dark/light themes, auto-scroll, optional typing animation, and a line slot for custom formatting.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/terminal.json
Named registry: npx shadcn-vue@latest add @uipkge/terminal Installs to: app/components/ui/terminal/

Examples

Props

Name Type / Values Default Required
lines

Command history to render.

TerminalLine[] required
title

Window title shown in the title bar. Default 'bash'.

string 'bash' optional
promptChar

Prompt character. Default '$'.

string '$' optional
theme

Color theme. Default 'dark'.

'dark''light'
'dark' optional
autoScroll

Auto-scroll to bottom when new lines arrive. Default true.

boolean true optional
typing

Animate lines typing in one-by-one. Default false.

boolean false optional
typingSpeed

Typing speed in ms per line. Default 120.

number 120 optional
maxHeight

Max height before scrolling. Default '400px'.

string '400px' optional
class HTMLAttributes['class'] optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

TerminalLine
interface TerminalLine {
  /** Prompt prefix shown before the command. Omit for output-only lines. */
  prompt?: string
  /** Command text shown after the prompt. */
  command?: string
  /** Output lines rendered below the command. */
  output?: string
  /** Override the line type: 'command' renders prompt+command, 'output' renders plain text. */
  type?: 'command' | 'output'
}

Files installed (2)

  • app/components/ui/terminal/Terminal.vue 4.4 kB
    <script setup lang="ts">
    import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    export interface TerminalLine {
      /** Prompt prefix shown before the command. Omit for output-only lines. */
      prompt?: string
      /** Command text shown after the prompt. */
      command?: string
      /** Output lines rendered below the command. */
      output?: string
      /** Override the line type: 'command' renders prompt+command, 'output' renders plain text. */
      type?: 'command' | 'output'
    }
    
    interface Props {
      /** Command history to render. */
      lines: TerminalLine[]
      /** Window title shown in the title bar. Default 'bash'. */
      title?: string
      /** Prompt character. Default '$'. */
      promptChar?: string
      /** Color theme. Default 'dark'. */
      theme?: 'dark' | 'light'
      /** Auto-scroll to bottom when new lines arrive. Default true. */
      autoScroll?: boolean
      /** Animate lines typing in one-by-one. Default false. */
      typing?: boolean
      /** Typing speed in ms per line. Default 120. */
      typingSpeed?: number
      /** Max height before scrolling. Default '400px'. */
      maxHeight?: string
      class?: HTMLAttributes['class']
    }
    
    const props = withDefaults(defineProps<Props>(), {
      title: 'bash',
      promptChar: '$',
      theme: 'dark',
      autoScroll: true,
      typing: false,
      typingSpeed: 120,
      maxHeight: '400px',
    })
    
    const bodyRef = ref<HTMLElement | null>(null)
    const visibleCount = ref(props.typing ? 0 : props.lines.length)
    
    const resolvedLines = computed(() =>
      props.lines.map((l) => ({
        ...l,
        type: l.type ?? (l.prompt || l.command ? 'command' : 'output'),
        prompt: l.prompt ?? (l.type === 'output' ? '' : props.promptChar),
      })),
    )
    
    const shownLines = computed(() => resolvedLines.value.slice(0, visibleCount.value))
    
    function scrollToBottom() {
      if (!props.autoScroll || !bodyRef.value) return
      nextTick(() => {
        if (bodyRef.value) bodyRef.value.scrollTop = bodyRef.value.scrollHeight
      })
    }
    
    watch(
      () => props.lines.length,
      () => {
        if (props.typing) {
          // Reset typing animation when lines change.
          visibleCount.value = 0
          runTyping()
        } else {
          visibleCount.value = props.lines.length
          scrollToBottom()
        }
      },
    )
    
    let typingTimer: ReturnType<typeof setTimeout> | null = null
    function runTyping() {
      if (typingTimer) clearTimeout(typingTimer)
      const tick = () => {
        if (visibleCount.value < props.lines.length) {
          visibleCount.value++
          scrollToBottom()
          typingTimer = setTimeout(tick, props.typingSpeed)
        }
      }
      typingTimer = setTimeout(tick, props.typingSpeed)
    }
    
    onMounted(() => {
      if (props.typing) runTyping()
      else scrollToBottom()
    })
    
    onBeforeUnmount(() => {
      if (typingTimer) clearTimeout(typingTimer)
    })
    </script>
    
    <template>
      <div
        data-uipkge
        data-slot="terminal"
        :data-theme="theme"
        :class="
          cn(
            'relative overflow-hidden rounded-lg border font-mono text-sm shadow-sm',
            theme === 'dark' ? 'border-zinc-800 bg-zinc-950 text-zinc-200' : 'border-zinc-200 bg-zinc-50 text-zinc-800',
            props.class,
          )
        "
      >
        <!-- Title bar -->
        <div
          :class="
            cn(
              'flex items-center gap-2 border-b px-4 py-2.5',
              theme === 'dark' ? 'border-zinc-800 bg-zinc-900' : 'border-zinc-200 bg-zinc-100',
            )
          "
        >
          <div class="flex gap-1.5">
            <span class="size-3 rounded-full bg-red-500" />
            <span class="size-3 rounded-full bg-yellow-500" />
            <span class="size-3 rounded-full bg-green-500" />
          </div>
          <span :class="cn('ml-2 text-xs', theme === 'dark' ? 'text-zinc-400' : 'text-zinc-500')">{{ title }}</span>
        </div>
    
        <!-- Body -->
        <div ref="bodyRef" class="overflow-auto p-4 leading-relaxed" :style="{ maxHeight: props.maxHeight }">
          <div v-for="(line, i) in shownLines" :key="i" data-slot="terminal-line" class="break-words whitespace-pre-wrap">
            <slot name="line" :line="line" :index="i">
              <div
                v-if="line.type === 'command'"
                data-slot="terminal-command"
                class="flex flex-wrap items-baseline gap-x-1.5"
              >
                <span :class="cn('shrink-0 font-semibold', theme === 'dark' ? 'text-green-400' : 'text-green-600')">{{
                  line.prompt
                }}</span>
                <span>{{ line.command }}</span>
              </div>
              <div v-if="line.output" data-slot="terminal-output" class="text-zinc-400">{{ line.output }}</div>
            </slot>
          </div>
        </div>
      </div>
    </template>
  • app/components/ui/terminal/index.ts 0.1 kB
    export { default as Terminal } from './Terminal.vue'
    export type { TerminalLine } from './Terminal.vue'

Raw manifest: https://uipkge.dev/r/vue/terminal.json