hero/process-map.ex
Process map
A process mapped across swimlanes, with the step to fix first picked out in lime.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.hero_process_map animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
caption |
string | "Your process · the map" |
|
lanes |
list | ["Customer", "Your team", "Systems"] |
Three lane names, top to bottom. |
steps |
list | [%{system: "Mail", title: "Order comes in", lane: 0}, %{system: "Spreadsheet", ...}, ...] |
Up to five `%{lane, title, system}` in order. `lane` is 0, 1 or 2. Keep titles under 16 characters. |
highlight |
integer | 1 |
Index of the marked step. A negative index marks none. |
highlight_label |
string | "Fix first" |
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/hero/process-map.ex
defmodule EHVisuals.Hero.ProcessMap do
@moduledoc """
A process mapped across swimlanes, with the step to fix first marked.
"""
use EHVisuals.Composition,
name: "hero/process-map",
title: "Process map",
description:
"A process mapped across swimlanes, with the step to fix first picked out in lime."
@steps [
%{lane: 0, title: "Order comes in", system: "Mail"},
%{lane: 1, title: "Check stock", system: "Spreadsheet"},
%{lane: 2, title: "Reserve stock", system: "ERP"},
%{lane: 1, title: "Confirm date", system: "CRM"},
%{lane: 0, title: "Order confirmed", system: "Mail"}
]
attr(:caption, :string, default: "Your process · the map")
attr(:lanes, :list,
default: ["Customer", "Your team", "Systems"],
doc: "Three lane names, top to bottom."
)
attr(:steps, :list,
default: @steps,
doc:
"Up to five `%{lane, title, system}` in order. `lane` is 0, 1 or 2. Keep titles under 16 characters."
)
attr(:highlight, :integer,
default: 1,
doc: "Index of the marked step. A negative index marks none."
)
attr(:highlight_label, :string, default: "Fix first")
attr(:label, :string,
default: "A process map over three lanes with one manual step marked to fix first"
)
frame_attrs()
@spec hero_process_map(map()) :: Phoenix.LiveView.Rendered.t()
def hero_process_map(assigns) do
items =
for {step, index} <- assigns.steps |> Enum.take(5) |> Enum.with_index() do
step
|> Map.put(:index, index)
|> Map.put(:x, 96 + index * 120)
|> Map.put(:y, 73 + step.lane * 104)
|> Map.put(:on, index == assigns.highlight)
end
assigns =
assigns
|> assign(:items, items)
|> assign(:links, items |> Enum.chunk_every(2, 1, :discard) |> Enum.map(&connector/1))
|> assign(:tag_w, 14 + String.length(assigns.highlight_label) * 5.6)
~H"""
<.frame name="hero/process-map" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 684 372">
<text class="ehv-mono ehv-a-fade" x="684" y="24" font-size="9.5" text-anchor="end">
{String.upcase(@caption)}
</text>
<g :for={{lane, index} <- Enum.with_index(@lanes)} class={"ehv-a-fade ehv-i-#{index}"}>
<line class="ehv-line-soft" x1="0" x2="684" y1={44.5 + index * 104} y2={44.5 + index * 104} />
<text class="ehv-t-3" x="0" y={70 + index * 104} font-size="12">{lane}</text>
</g>
<line class="ehv-line-soft ehv-a-fade ehv-i-3" x1="0" x2="684" y1="356.5" y2="356.5" />
<g :for={link <- @links} class={"ehv-a-fade ehv-i-#{link.step}"}>
<path
class={"ehv-line-ink ehv-round ehv-a-draw ehv-i-#{link.step}"}
d={link.d}
pathLength="1"
/>
<path class="ehv-fill-2" d={link.head} />
</g>
<g :for={item <- @items}>
<rect
:if={item.on}
class="ehv-hero-process-map__halo ehv-a-pulse ehv-i-12"
x={item.x - 5}
y={item.y - 5}
width="114"
height="56"
rx="11"
/>
<g class={"ehv-a-rise ehv-i-#{item.index * 2 + 2}"}>
<rect
class={if item.on, do: "ehv-card-lime", else: "ehv-card"}
x={item.x + 0.5}
y={item.y + 0.5}
width="103"
height="45"
rx="7"
/>
<text class="ehv-t-strong" x={item.x + 10} y={item.y + 20} font-size="12">
{item.title}
</text>
<text
class={["ehv-mono", item.on && "ehv-t-lime"]}
x={item.x + 10}
y={item.y + 35}
font-size="8"
>
{String.upcase(item.system)}
</text>
<g :if={item.on}>
<rect
class="ehv-fill-lime"
x={item.x}
y={item.y - 24}
width={@tag_w}
height="16"
rx="4"
/>
<text class="ehv-mono ehv-t-on-lime" x={item.x + 7} y={item.y - 13} font-size="7.5">
{String.upcase(@highlight_label)}
</text>
</g>
</g>
</g>
</svg>
</.frame>
"""
end
# An elbow from the right edge of one step to the left edge of the next.
defp connector([from, to]) do
x1 = from.x + 104
y1 = from.y + 23
x2 = to.x
y2 = to.y + 23
%{
d: "M#{x1} #{y1} H#{x1 + 8} V#{y2} H#{x2 - 5}",
head: "M#{x2} #{y2} l-6 -3.5 v7 z",
step: from.index * 2 + 3
}
end
end
assets/css/hero/process-map.css
/* hero/process-map */
.ehv-hero-process-map { --ehv-max-w: 760px; --ehv-step: 110ms; --ehv-iso-scale: 0.74; }
.ehv-hero-process-map__halo {
fill: var(--ehv-lime-soft);
stroke: none;
}