agents/handoff.ex
Handoff
Three cards in a row: the agent drafts, a person decides, the system acts.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.agents_handoff animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
steps |
list | [%{label: "Draft", title: "Agent", detail: "Drafts the entry"}, %{label: "Decide", ...}, ...] |
Exactly three `%{label, title, detail}`. Keep details under 18 characters. |
approved |
boolean | true |
When false the person has not decided yet and the last card waits. |
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/agents/handoff.ex
defmodule EHVisuals.Agents.Handoff do
@moduledoc """
The agent drafts, a person decides, the system acts.
"""
use EHVisuals.Composition,
name: "agents/handoff",
title: "Handoff",
description: "Three cards in a row: the agent drafts, a person decides, the system acts."
@steps [
%{label: "Draft", title: "Agent", detail: "Drafts the entry"},
%{label: "Decide", title: "Marta", detail: "Approves it"},
%{label: "Act", title: "ERP", detail: "Posts the entry"}
]
attr(:steps, :list,
default: @steps,
doc: "Exactly three `%{label, title, detail}`. Keep details under 18 characters."
)
attr(:approved, :boolean,
default: true,
doc: "When false the person has not decided yet and the last card waits."
)
attr(:label, :string,
default: "An agent hands a draft to a person, who approves it for the ERP"
)
frame_attrs()
@spec agents_handoff(map()) :: Phoenix.LiveView.Rendered.t()
def agents_handoff(assigns) do
items =
for {step, index} <- assigns.steps |> Enum.take(3) |> Enum.with_index() do
step
|> Map.put(:index, index)
|> Map.put(:x, 12 + index * 150)
|> Map.put(:number, "0#{index + 1}")
end
assigns = assign(assigns, :items, items)
~H"""
<.frame name="agents/handoff" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 440 150">
<g class="ehv-h-lift">
<g class="ehv-a-fade ehv-i-3">
<line class="ehv-line-ink" x1="130" x2="154" y1="80" y2="80" />
<path class="ehv-fill-2" d="M154 75.5 l8 4.5 l-8 4.5 z" />
</g>
<g class="ehv-a-fade ehv-i-5">
<line
class={if @approved, do: "ehv-line-lime", else: "ehv-line ehv-dash"}
x1="280"
x2="304"
y1="80"
y2="80"
/>
<path
class={if @approved, do: "ehv-fill-lime", else: "ehv-fill-4"}
d="M304 75.5 l8 4.5 l-8 4.5 z"
/>
</g>
<g :for={item <- @items} class={"ehv-a-rise ehv-i-#{item.index * 2 + 1}"}>
<text
class={["ehv-mono", item.index == 1 && "ehv-t-lime"]}
x={item.x}
y="24"
font-size="9"
>
{item.number} {String.upcase(item.label)}
</text>
<rect
class={if item.index == 1, do: "ehv-card-lime", else: "ehv-card"}
x={item.x + 0.5}
y="38.5"
width="115"
height="83"
rx="10"
/>
<text
class={["ehv-t-strong", item.index == 2 && !@approved && "ehv-t-3"]}
x={item.x + 14}
y="88"
font-size="14"
>
{item.title}
</text>
<text
class={if item.index == 2 && !@approved, do: "ehv-t-4", else: "ehv-t-3"}
x={item.x + 14}
y="106"
font-size="11"
>
{item.detail}
</text>
</g>
<g class="ehv-a-pop ehv-i-2">
<rect class="ehv-card-3" x="26" y="50" width="20" height="20" rx="6" />
<rect class="ehv-fill-lime" x="32.5" y="56.5" width="7" height="7" rx="2" />
</g>
<g class="ehv-a-pop ehv-i-4">
<circle
class={if @approved, do: "ehv-fill-lime-soft", else: "ehv-node-soft"}
cx="186"
cy="60"
r="10"
/>
<path
:if={@approved}
class="ehv-line-lime ehv-round ehv-a-draw ehv-i-6"
d="M181.5 60 l3.2 3.2 l5.6 -6.4"
pathLength="1"
/>
<circle :if={!@approved} class="ehv-fill-3 ehv-a-pulse" cx="186" cy="60" r="2.5" />
</g>
<g class="ehv-a-pop ehv-i-6">
<rect class="ehv-card-3" x="326" y="50" width="20" height="20" rx="4" />
<line class="ehv-line" x1="330" x2="342" y1="57" y2="57" />
<line class="ehv-line" x1="330" x2="342" y1="63" y2="63" />
</g>
<g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-8">
<circle class="ehv-fill-ink" cy="80" r="2.5">
<animate attributeName="cx" from="130" to="158" dur="1.6s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0;1;1;0"
keyTimes="0;0.2;0.8;1"
dur="1.6s"
repeatCount="indefinite"
/>
</circle>
<circle :if={@approved} class="ehv-fill-lime" cy="80" r="2.5">
<animate
attributeName="cx"
from="280"
to="308"
dur="1.6s"
begin="0.8s"
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0;1;1;0"
keyTimes="0;0.2;0.8;1"
dur="1.6s"
begin="0.8s"
repeatCount="indefinite"
/>
</circle>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/agents/handoff.css
/* agents/handoff */
.ehv-agents-handoff { --ehv-max-w: 520px; --ehv-step: 140ms; --ehv-iso-scale: 0.82; }