states/maintenance.ex
Maintenance state
A maintenance state: a stack of service tiles with one pulled out and worked on, and when it is back.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.states_maintenance animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
services |
list | ["Orders", "Invoices", "Stock", "Reports"] |
Two to five service names, 14 characters at most. |
active |
integer | 1 |
Index of the service that is pulled out. |
running |
string | "Running" |
The status of every other service. |
status |
string | "In maintenance" |
14 characters at most. |
title |
string | "Back in 10 minutes" |
34 characters at most. |
note |
string | "Scheduled · 02:00 to 02:10" |
The mono line under the title, 40 characters at most. |
progress |
integer | 60 |
How far the work is, 0 to 100. |
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/maintenance.ex
defmodule EHVisuals.States.Maintenance do
@moduledoc """
A maintenance state: a stack of service tiles with one pulled out and being
worked on, and a line that says when it is back.
"""
use EHVisuals.Composition,
name: "states/maintenance",
title: "Maintenance state",
description:
"A maintenance state: a stack of service tiles with one pulled out and worked on, and when it is back."
attr(:services, :list,
default: ~w(Orders Invoices Stock Reports),
doc: "Two to five service names, 14 characters at most."
)
attr(:active, :integer, default: 1, doc: "Index of the service that is pulled out.")
attr(:running, :string, default: "Running", doc: "The status of every other service.")
attr(:status, :string, default: "In maintenance", doc: "14 characters at most.")
attr(:title, :string, default: "Back in 10 minutes", doc: "34 characters at most.")
attr(:note, :string,
default: "Scheduled · 02:00 to 02:10",
doc: "The mono line under the title, 40 characters at most."
)
attr(:progress, :integer, default: 60, doc: "How far the work is, 0 to 100.")
attr(:label, :string,
default: "Four service tiles with the invoices tile pulled out for maintenance"
)
frame_attrs()
@spec states_maintenance(map()) :: Phoenix.LiveView.Rendered.t()
def states_maintenance(assigns) do
services = Enum.take(assigns.services, 5)
count = max(length(services), 1)
active = assigns.active |> max(0) |> min(count - 1)
rows =
for {name, index} <- Enum.with_index(services) do
%{name: name, index: index, y: 24 + index * 46, active: index == active}
end
title_y = 24 + count * 46 - 8 + 32
bar_y = title_y + 32
progress = assigns.progress |> max(0) |> min(100)
assigns =
assigns
|> assign(:rows, rows)
|> assign(:title_y, title_y)
|> assign(:bar_y, bar_y)
|> assign(:bar_w, Float.round(312 * progress / 100, 1))
|> assign(:height, bar_y + 4 + 22)
~H"""
<.frame name="states/maintenance" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 360 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="359" height={@height - 1} rx="12" />
<g :for={row <- @rows}>
<g :if={!row.active} class={"ehv-a-rise ehv-i-#{row.index + 1}"}>
<rect class="ehv-card-2" x="24.5" y={row.y + 0.5} width="249" height="37" rx="9" />
<rect class="ehv-node-soft" x="38.5" y={row.y + 15.5} width="8" height="8" rx="2.5" />
<text class="ehv-t-2" x="58" y={row.y + 23.5} font-size="12.5">{row.name}</text>
<text class="ehv-mono" x="260" y={row.y + 22.5} font-size="7.5" text-anchor="end">
{String.upcase(@running)}
</text>
</g>
<g :if={row.active}>
<rect
class={"ehv-states-maintenance__slot ehv-a-fade ehv-i-#{row.index + 1}"}
x="24.5"
y={row.y + 0.5}
width="249"
height="37"
rx="9"
/>
<g class={"ehv-a-slide ehv-i-#{row.index + 3}"}>
<g class="ehv-states-maintenance__lifted">
<rect class="ehv-card-lime" x="86.5" y={row.y - 6.5} width="249" height="37" rx="9" />
<circle class="ehv-fill-lime-soft" cx="107" cy={row.y + 12} r="11" />
<g class="ehv-states-maintenance__wrench">
<path
class="ehv-line-lime ehv-round"
transform={"translate(#{100} #{row.y + 5})"}
d="M10.9 2.2 L8.9 4.2 L9.8 5.1 L10.7 6 L12.7 4 A3.6 3.6 0 0 1 8 8.2 L3.6 12.6 A1.2 1.2 0 0 1 1.9 10.9 L6.3 6.5 A3.6 3.6 0 0 1 10.9 2.2 Z"
/>
</g>
<text class="ehv-t-strong" x="128" y={row.y + 16.5} font-size="12.5">
{row.name}
</text>
<text
class="ehv-mono ehv-t-lime"
x="322"
y={row.y + 15.5}
font-size="7.5"
text-anchor="end"
>
{String.upcase(@status)}
</text>
</g>
</g>
</g>
</g>
<text class="ehv-t-strong ehv-a-fade ehv-i-6" x="24" y={@title_y} font-size="15">
{@title}
</text>
<text class="ehv-mono ehv-a-fade ehv-i-7" x="24" y={@title_y + 18} font-size="8.5">
{String.upcase(@note)}
</text>
<g class="ehv-a-fade ehv-i-8">
<rect class="ehv-fill-soft" x="24" y={@bar_y} width="312" height="4" rx="2" />
<rect
class="ehv-fill-lime ehv-a-grow-x ehv-i-9"
x="24"
y={@bar_y}
width={@bar_w}
height="4"
rx="2"
/>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/states/maintenance.css
/* states/maintenance */
.ehv-states-maintenance { --ehv-max-w: 400px; }
/* The place the pulled-out tile leaves behind. */
.ehv-states-maintenance__slot { fill: none; stroke: var(--ehv-border-strong); stroke-width: 1; stroke-dasharray: 3 4; }
/* The wrench works back and forth around its own centre. */
@keyframes ehv-states-maintenance-turn {
from { transform: rotate(-16deg); }
to { transform: rotate(18deg); }
}
.ehv-states-maintenance__wrench { transform-box: fill-box; transform-origin: center; }
.ehv--animated .ehv-states-maintenance__wrench {
animation: ehv-states-maintenance-turn 1.1s ease-in-out infinite alternate;
animation-delay: calc(var(--ehv-delay, 0ms) + 6 * var(--ehv-step) + var(--ehv-dur));
}
.ehv--hover .ehv-states-maintenance__lifted { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-states-maintenance__lifted { transform: translate(8px, -3px); }