code/terminal.ex
Terminal
A terminal with one command, its output line by line and a waiting prompt.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.code_terminal animated />
</div>
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
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
/* 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; }