empowered.house ↗

code/terminal.ex

Terminal

A terminal with one command, its output line by line and a waiting prompt.

Use it
# in the module that renders the template
use EHVisuals

<div class="h-96 rounded-xl border border-border">
  <.code_terminal animated />
</div>
default
isometric
with a warning · custom copy
replays in view · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
title string "terminal"
command string "mix ehos.deploy orders"
lines list [%{text: "compiled 42 modules", tone: "ok"}, %{text: "migrations up to date", tone: "ok"}, ...] Up to six `%{text, tone}`. Tone is `ok`, `info` or `warn`.
Frame props, shared by every composition
Prop Type Default What it does
animated boolean false Turns motion on. Without it the composition renders in its final, still state.
trigger "mount" | "in-view" | "in-view-repeat" "in-view" When the entrance plays: on page load, once when scrolled into view, or every time it comes back into view.
hover boolean false Enables the hover micro-interactions the composition supports.
glow boolean true The warm incandescent halo behind the composition.
fade_out boolean false Fades the bottom edge into the surface underneath.
isometric boolean false Tilts the composition into the isometric plane of the platform stack.
theme "dark" | "light" "dark" `dark` for the site canvas, `light` for the cream reversed surface.
class string nil Extra classes for the frame.
rest global — Any other HTML attribute, set on the frame.

Source

Two files. Copy them into your app if you would rather own the code than depend on the library.

lib/eh_visuals/code/terminal.ex
terminal.ex
defmodule EHVisuals.Code.Terminal do
  @moduledoc """
  A terminal: one command, its output line by line, and a waiting prompt.
  """

  use EHVisuals.Composition,
    name: "code/terminal",
    title: "Terminal",
    description: "A terminal with one command, its output line by line and a waiting prompt."

  @lines [
    %{text: "compiled 42 modules", tone: "ok"},
    %{text: "migrations up to date", tone: "ok"},
    %{text: "3 workflows, 1 agent, 12 events", tone: "info"},
    %{text: "live at orders.yourcompany.com in 38s", tone: "ok"}
  ]

  attr(:title, :string, default: "terminal")
  attr(:command, :string, default: "mix ehos.deploy orders")

  attr(:lines, :list,
    default: @lines,
    doc: "Up to six `%{text, tone}`. Tone is `ok`, `info` or `warn`."
  )

  attr(:label, :string, default: "A terminal deploying an app, every step passing")
  frame_attrs()

  @spec code_terminal(map()) :: Phoenix.LiveView.Rendered.t()
  def code_terminal(assigns) do
    rows = assigns.lines |> Enum.take(6) |> Enum.with_index()
    prompt_y = 84 + length(rows) * 22

    assigns =
      assigns
      |> assign(:rows, rows)
      |> assign(:prompt_y, prompt_y)
      |> assign(:prompt_step, min(length(rows) * 2 + 4, 24))
      |> assign(:height, prompt_y + 22)

    ~H"""
    <.frame name="code/terminal" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 400 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="399" height={@height - 1} rx="12" />
          <g class="ehv-a-fade ehv-i-1">
            <circle :for={x <- [18, 32, 46]} class="ehv-node-soft" cx={x} cy="17.5" r="4" />
            <text class="ehv-mono" x="200" y="21" font-size="8.5" text-anchor="middle">{@title}</text>
            <line class="ehv-line-soft" x1="0.5" x2="399.5" y1="35.5" y2="35.5" />
          </g>
          <g class="ehv-a-fade ehv-i-2">
            <text class="ehv-code ehv-t-lime" x="20" y="62" font-size="11">$</text>
            <text class="ehv-code" x="36" y="62" font-size="11">{@command}</text>
          </g>
          <g :for={{line, index} <- @rows} class={"ehv-a-fade ehv-i-#{min(index * 2 + 4, 24)}"}>
            <path
              :if={line.tone == "ok"}
              class="ehv-line-lime ehv-round"
              d={"M20.5 #{80.5 + index * 22} l2.6 2.6 l4.6 -5.2"}
            />
            <circle
              :if={line.tone == "info"}
              class="ehv-fill-4"
              cx="24"
              cy={80.5 + index * 22}
              r="1.8"
            />
            <path
              :if={line.tone == "warn"}
              class="ehv-line-coral ehv-round"
              d={"M24 #{76.5 + index * 22} v5 m0 2.6 v0.4"}
            />
            <text
              class={["ehv-code", tone(line.tone)]}
              x="36"
              y={84 + index * 22}
              font-size="10.5"
            >
              {line.text}
            </text>
          </g>
          <g class={"ehv-a-fade ehv-i-#{@prompt_step}"}>
            <text class="ehv-code ehv-t-lime" x="20" y={@prompt_y} font-size="11">$</text>
            <rect
              class="ehv-fill-lime ehv-code-terminal__caret"
              x="36"
              y={@prompt_y - 10.5}
              width="7"
              height="13"
              rx="1.5"
            />
          </g>
        </g>
      </svg>
    </.frame>
    """
  end

  defp tone("warn"), do: "ehv-t-coral"
  defp tone("info"), do: "ehv-t-3"
  defp tone(_tone), do: "ehv-t-2"
end
assets/css/code/terminal.css
terminal.css
/* code/terminal */
.ehv-code-terminal { --ehv-max-w: 440px; --ehv-step: 170ms; --ehv-dur: 300ms; }

@keyframes ehv-code-terminal-blink { 50% { opacity: 0; } }
.ehv--animated .ehv-code-terminal__caret { animation: ehv-code-terminal-blink 1.1s steps(1) infinite; }