states/error.ex
Error state
An error state: a request travels into a failed service, and the services that depend on it are dimmed.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.states_error animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
caption |
string | "Incident · 14:02" |
The line in the top left corner. Empty hides it. |
request |
string | "POST /orders" |
The incoming call, 14 characters at most. |
service |
string | "Orders API" |
The failed service, 12 characters at most. |
status |
string | "503 · Timeout" |
14 characters at most. |
message |
string | "Retry in 30 s" |
The quiet last line of the card. |
dependents |
list | ["Invoicing", "Stock sync", "Notifications"] |
One to four services that wait on the failed one, 13 characters at most. |
waiting |
string | "Waiting" |
The status under every dependent. |
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/states/error.ex
defmodule EHVisuals.States.Error do
@moduledoc """
An error state: a request travels into a service that has failed, and the
services that depend on it are dimmed.
"""
use EHVisuals.Composition,
name: "states/error",
title: "Error state",
description:
"An error state: a request travels into a failed service, and the services that depend on it are dimmed."
attr(:caption, :string,
default: "Incident · 14:02",
doc: "The line in the top left corner. Empty hides it."
)
attr(:request, :string,
default: "POST /orders",
doc: "The incoming call, 14 characters at most."
)
attr(:service, :string,
default: "Orders API",
doc: "The failed service, 12 characters at most."
)
attr(:status, :string, default: "503 · Timeout", doc: "14 characters at most.")
attr(:message, :string, default: "Retry in 30 s", doc: "The quiet last line of the card.")
attr(:dependents, :list,
default: ["Invoicing", "Stock sync", "Notifications"],
doc: "One to four services that wait on the failed one, 13 characters at most."
)
attr(:waiting, :string, default: "Waiting", doc: "The status under every dependent.")
attr(:label, :string,
default: "A request reaching a failed orders service while three dependent services wait"
)
frame_attrs()
@spec states_error(map()) :: Phoenix.LiveView.Rendered.t()
def states_error(assigns) do
names = Enum.take(assigns.dependents, 4)
count = length(names)
pill_w = Float.round(22 + String.length(assigns.request) * 5.8, 1)
dependents =
for {name, index} <- Enum.with_index(names) do
%{name: name, index: index, y: Float.round(126 + (index - (count - 1) / 2) * 48, 1)}
end
assigns =
assigns
|> assign(:items, dependents)
|> assign(:pill_w, pill_w)
|> assign(:line_x, 12 + pill_w)
|> assign(:still_x, Float.round(12 + pill_w + (176 - 12 - pill_w) * 0.62, 1))
~H"""
<.frame name="states/error" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 460 240">
<g class="ehv-h-lift">
<g :if={@caption != ""} class="ehv-a-fade">
<circle class="ehv-fill-coral ehv-a-pulse" cx="16" cy="17" r="3" />
<text class="ehv-mono ehv-t-2" x="26" y="20" font-size="8.5">
{String.upcase(@caption)}
</text>
</g>
<line
class="ehv-line ehv-a-draw ehv-i-2"
x1={@line_x}
x2="170"
y1="126"
y2="126"
pathLength="1"
/>
<path class="ehv-fill-3 ehv-a-fade ehv-i-4" d="M169 122 l7 4 l-7 4 z" />
<circle
class="ehv-fill-lime ehv-states-error__packet"
cx={@still_x}
cy="126"
r="3"
/>
<g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-6">
<circle :for={delay <- [0, 1.2]} class="ehv-fill-lime" cy="126" r="3">
<animate
attributeName="cx"
from={@line_x}
to="172"
dur="2.4s"
begin={"#{delay}s"}
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0;1;1;0"
keyTimes="0;0.15;0.85;1"
dur="2.4s"
begin={"#{delay}s"}
repeatCount="indefinite"
/>
</circle>
</g>
<g class="ehv-a-rise ehv-i-1">
<rect class="ehv-card" x="12.5" y="111.5" width={@pill_w} height="29" rx="8" />
<text
class="ehv-mono ehv-t-2"
x={12.5 + @pill_w / 2}
y="129"
font-size="8.5"
text-anchor="middle"
>
{String.upcase(@request)}
</text>
</g>
<g :for={item <- @items} class={"ehv-a-fade ehv-i-#{item.index + 5}"}>
<g class="ehv-states-error__dependent">
<path class="ehv-line ehv-dash" d={"M304 126 H324 V#{item.y} H344"} />
<rect class="ehv-card" x="344.5" y={item.y - 18.5} width="103" height="37" rx="8" />
<text class="ehv-t-2" x="356" y={item.y - 1.5} font-size="11.5">{item.name}</text>
<text class="ehv-mono" x="356" y={item.y + 10.5} font-size="7">
{String.upcase(@waiting)}
</text>
</g>
</g>
<rect
class="ehv-states-error__alarm"
x="176.5"
y="86.5"
width="127"
height="79"
rx="10"
/>
<g class="ehv-a-rise ehv-i-3">
<rect
class="ehv-states-error__failed"
x="176.5"
y="86.5"
width="127"
height="79"
rx="10"
/>
<circle class="ehv-fill-coral-soft" cx="285" cy="105" r="8.5" />
<path class="ehv-line-coral ehv-round" d="M282 102 l6 6 M288 102 l-6 6" />
<text class="ehv-t-strong" x="190" y="110" font-size="12.5">{@service}</text>
<text class="ehv-mono ehv-t-coral" x="190" y="128" font-size="8">
{String.upcase(@status)}
</text>
<line class="ehv-line-faint" x1="190" x2="290" y1="138.5" y2="138.5" />
<text class="ehv-mono" x="190" y="153" font-size="7.5">{String.upcase(@message)}</text>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/states/error.css
/* states/error */
.ehv-states-error { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }
.ehv-states-error__failed { fill: var(--ehv-surface-1); stroke: var(--ehv-coral); stroke-width: 1.25; }
.ehv-states-error__dependent { opacity: 0.7; }
/* A coral ring leaves the failed card every few seconds. */
@keyframes ehv-states-error-alarm {
from { opacity: 0.7; transform: scale(1); }
60%, to { opacity: 0; transform: scale(1.16, 1.26); }
}
.ehv-states-error__alarm {
fill: none;
stroke: var(--ehv-coral);
stroke-width: 1;
opacity: 0;
transform-box: fill-box;
transform-origin: center;
}
.ehv--animated .ehv-states-error__alarm {
animation: ehv-states-error-alarm 2.4s var(--ehv-ease) infinite;
animation-delay: calc(var(--ehv-delay, 0ms) + 4 * var(--ehv-step) + var(--ehv-dur));
}
/* The still packet stands in for the travelling ones when nothing moves. */
.ehv--animated .ehv-states-error__packet { display: none; }
@media (prefers-reduced-motion: reduce) {
.ehv--animated .ehv-states-error__packet { display: inline; }
}
.ehv--hover .ehv-states-error__dependent { transition: opacity 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-states-error__dependent { opacity: 0.85; }