states/not-found.ex
Not found state
A not-found state: a route line running into a barrier in front of a large 404.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.states_not_found animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
code |
string | "404" |
Three characters. |
route |
list | ["app", "orders"] |
One to three parts of the address that do exist, six characters at most. |
title |
string | "This page does not exist" |
40 characters at most. |
text |
string | "Nothing lives at /orders/2291. It may have been " <> ... |
62 characters at most. Empty hides it. |
action |
string | "Back to orders" |
The way out. Empty hides it. |
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/not-found.ex
defmodule EHVisuals.States.NotFound do
@moduledoc """
A not-found state: a route runs through the parts of the address that
exist and stops at a barrier in front of a large status code.
"""
use EHVisuals.Composition,
name: "states/not-found",
title: "Not found state",
description: "A not-found state: a route line running into a barrier in front of a large 404."
attr(:code, :string, default: "404", doc: "Three characters.")
attr(:route, :list,
default: ~w(app orders),
doc: "One to three parts of the address that do exist, six characters at most."
)
attr(:title, :string, default: "This page does not exist", doc: "40 characters at most.")
attr(:text, :string,
default: "Nothing lives at /orders/2291. It may have been archived.",
doc: "62 characters at most. Empty hides it."
)
attr(:action, :string, default: "Back to orders", doc: "The way out. Empty hides it.")
attr(:label, :string, default: "A route line stopped by a barrier in front of a large 404")
frame_attrs()
@spec states_not_found(map()) :: Phoenix.LiveView.Rendered.t()
def states_not_found(assigns) do
names = Enum.take(assigns.route, 3)
count = length(names)
gap = if count > 1, do: 64 / (count - 1), else: 0
stops =
for {name, index} <- Enum.with_index(names) do
%{
name: name,
index: index,
x: Float.round(if(count > 1, do: 28 + index * gap, else: 60.0), 1),
# With three stops the labels alternate, so they cannot collide.
label_y: if(count == 3 and rem(index, 2) == 1, do: 62, else: 95)
}
end
text_y = if assigns.text == "", do: 166, else: 186
action_y = text_y + 14
assigns =
assigns
|> assign(:stops, stops)
|> assign(:stripes, for(index <- 0..4, do: stripe(34 + index * 16)))
|> assign(:text_y, text_y)
|> assign(:action_y, action_y)
|> assign(:action_w, Float.round(38 + String.length(assigns.action) * 5.9, 1))
|> assign(:height, if(assigns.action == "", do: text_y + 18, else: action_y + 26 + 12))
~H"""
<.frame name="states/not-found" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 400 #{@height}"}>
<g class="ehv-h-lift">
<text
class="ehv-t-display ehv-states-not-found__code ehv-a-fade"
x="388"
y="119"
font-size="128"
text-anchor="end"
>
{@code}
</text>
<line
class="ehv-line-lime ehv-a-draw ehv-i-2"
x1="12"
x2="120"
y1="74"
y2="74"
pathLength="1"
/>
<line class="ehv-line-lime ehv-a-fade ehv-i-2" x1="12" x2="12" y1="69" y2="79" />
<g :for={stop <- @stops} class={"ehv-a-pop ehv-i-#{stop.index + 3}"}>
<circle class="ehv-node-lime" cx={stop.x} cy="74" r="4.5" />
</g>
<text
:for={stop <- @stops}
class={"ehv-mono ehv-t-2 ehv-a-fade ehv-i-#{stop.index + 3}"}
x={stop.x}
y={stop.label_y}
font-size="7.5"
text-anchor="middle"
>
{String.upcase(stop.name)}
</text>
<circle class="ehv-fill-lime ehv-states-not-found__packet" cx="106" cy="74" r="3" />
<g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-8">
<circle class="ehv-fill-lime" cy="74" r="3">
<animate attributeName="cx" from="12" to="116" dur="2.2s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0;1;1;0"
keyTimes="0;0.12;0.88;1"
dur="2.2s"
repeatCount="indefinite"
/>
</circle>
</g>
<g class="ehv-a-drop ehv-i-6">
<rect class="ehv-states-not-found__bar" x="120.5" y="34.5" width="13" height="79" rx="2" />
<polygon :for={points <- @stripes} class="ehv-fill-coral" points={points} />
<line class="ehv-line-coral ehv-round" x1="113" x2="141" y1="117.5" y2="117.5" />
</g>
<text class="ehv-t-strong ehv-a-fade ehv-i-8" x="12" y="166" font-size="15">{@title}</text>
<text :if={@text != ""} class="ehv-t-2 ehv-a-fade ehv-i-9" x="12" y="186" font-size="12">
{@text}
</text>
<g :if={@action != ""} class="ehv-a-fade ehv-i-10">
<rect
class="ehv-card-2 ehv-states-not-found__action"
x="12.5"
y={@action_y + 0.5}
width={@action_w}
height="25"
rx="7"
/>
<path
class="ehv-line-ink ehv-round"
d={"M33 #{@action_y + 13} h-10 M27 #{@action_y + 9} l-4 4 l4 4"}
/>
<text class="ehv-t-strong" x="41" y={@action_y + 17} font-size="11">{@action}</text>
</g>
</g>
</svg>
</.frame>
"""
end
# One diagonal stripe of the barrier, cut straight by its two long edges.
defp stripe(y), do: "121,#{y + 8} 133,#{y} 133,#{y + 8} 121,#{y + 16}"
end
assets/css/states/not-found.css
/* states/not-found */
.ehv-states-not-found { --ehv-max-w: 440px; }
/* The status code is the backdrop: outlined, in the tone of a surface. */
.ehv svg .ehv-states-not-found__code { fill: var(--ehv-surface-3); stroke: var(--ehv-ink-4); stroke-width: 1; }
.ehv-states-not-found__bar { fill: var(--ehv-coral-soft); stroke: var(--ehv-coral); stroke-width: 1; }
/* The still packet stands in for the travelling one when nothing moves. */
.ehv--animated .ehv-states-not-found__packet { display: none; }
@media (prefers-reduced-motion: reduce) {
.ehv--animated .ehv-states-not-found__packet { display: inline; }
}
.ehv--hover .ehv-states-not-found__action { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-states-not-found__action { stroke: var(--ehv-ink-2); }