process/flow.ex
Flow
A process as an event line: steps on a rail, with a packet travelling to the current one.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.process_flow animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
caption |
string | "Order to cash" |
|
steps |
list | ["Received", "Checked", "Approved", "Posted"] |
Two to five step names. |
current |
integer | 2 |
Index of the step that is running now. Earlier steps are done. |
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/process/flow.ex
defmodule EHVisuals.Process.Flow do
@moduledoc """
A process as an event line: steps on a rail, done in lime, with a packet
travelling to the step that is running now.
"""
use EHVisuals.Composition,
name: "process/flow",
title: "Flow",
description:
"A process as an event line: steps on a rail, with a packet travelling to the current one."
attr(:caption, :string, default: "Order to cash")
attr(:steps, :list,
default: ~w(Received Checked Approved Posted),
doc: "Two to five step names."
)
attr(:current, :integer,
default: 2,
doc: "Index of the step that is running now. Earlier steps are done."
)
attr(:label, :string, default: "A four step process with the third step running")
frame_attrs()
@spec process_flow(map()) :: Phoenix.LiveView.Rendered.t()
def process_flow(assigns) do
steps = layout(assigns.steps, assigns.current)
now = Enum.find(steps, List.last(steps), &(&1.state == :now))
assigns =
assigns
|> assign(:items, steps)
|> assign(:now_x, now.x)
~H"""
<.frame name="process/flow" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 440 180">
<g class="ehv-h-lift">
<text class="ehv-mono ehv-a-fade" x="20" y="18" font-size="9">
{String.upcase(@caption)}
</text>
<line class="ehv-line ehv-a-draw ehv-i-1" x1="20" x2="412" y1="142" y2="142" pathLength="1" />
<path class="ehv-fill-3 ehv-a-fade ehv-i-8" d="M412 137.5 l8 4.5 l-8 4.5 z" />
<line
class="ehv-line-lime ehv-a-draw ehv-i-3"
x1="20"
x2={@now_x}
y1="142"
y2="142"
pathLength="1"
/>
<g :for={step <- @items}>
<line
class={"ehv-line-soft ehv-dash ehv-a-fade ehv-i-#{step.index + 3}"}
x1={step.x}
x2={step.x}
y1="82"
y2="136"
/>
<g class={"ehv-a-rise ehv-i-#{step.index + 2}"}>
<rect
class={if step.state == :now, do: "ehv-card-lime", else: "ehv-card"}
x={step.x - step.w / 2}
y="40.5"
width={step.w}
height="41"
rx="8"
/>
<text
class={["ehv-t-strong", step.state == :next && "ehv-t-3"]}
x={step.x}
y="65.5"
font-size="12.5"
text-anchor="middle"
>
{step.name}
</text>
</g>
<rect
:if={step.state == :now}
class="ehv-fill-lime ehv-a-ping"
x={step.x - 5}
y="137"
width="10"
height="10"
rx="3"
/>
<rect
class={[marker(step.state), "ehv-a-pop ehv-i-#{step.index + 4}"]}
x={step.x - 5}
y="137"
width="10"
height="10"
rx="3"
/>
<text
class={[
"ehv-mono ehv-a-fade ehv-i-#{step.index + 4}",
step.state == :now && "ehv-t-lime"
]}
x={step.x}
y="166"
font-size="8.5"
text-anchor="middle"
>
{step.number}
</text>
</g>
<g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-10">
<circle class="ehv-fill-lime" cy="142" r="3">
<animate attributeName="cx" from="20" to={@now_x} dur="2.4s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0;1;1;0"
keyTimes="0;0.15;0.8;1"
dur="2.4s"
repeatCount="indefinite"
/>
</circle>
</g>
</g>
</svg>
</.frame>
"""
end
# Steps sit evenly between x = 62 and x = 378.
defp layout(names, current) do
count = length(names)
gap = if count > 1, do: 316 / (count - 1), else: 0
width = if count > 1, do: min(104, gap - 12), else: 104
for {name, index} <- Enum.with_index(names) do
%{
name: name,
index: index,
number: index |> Kernel.+(1) |> Integer.to_string() |> String.pad_leading(2, "0"),
x: Float.round(if(count > 1, do: 62 + index * gap, else: 220.0), 1),
w: Float.round(width * 1.0, 1),
state:
cond do
index < current -> :done
index == current -> :now
true -> :next
end
}
end
end
defp marker(:next), do: "ehv-node-soft"
defp marker(_state), do: "ehv-fill-lime"
end
assets/css/process/flow.css
/* process/flow */
.ehv-process-flow { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }