connections/converge.ex
Converge
Several sources converging into one destination, with packets moving in.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.connections_converge animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
sources |
list | [%{name: "Mail", kind: "mail"}, %{name: "Spreadsheet", kind: "sheet"}, ...] |
Two to six `%{name, kind}`. `kind` picks the icon: `mail`, `sheet`, `erp`, `chat` or `docs`. Keep names under 12 characters. |
kicker |
string | "One record" |
The eyebrow of the destination card. |
title |
string | "Order 4812" |
Up to 12 characters. |
status |
string | "Up to date" |
Up to 14 characters. |
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/connections/converge.ex
defmodule EHVisuals.Connections.Converge do
@moduledoc """
Several sources feeding one record: each source sends its changes along a
path, and the paths meet in front of the destination.
"""
use EHVisuals.Composition,
name: "connections/converge",
title: "Converge",
description: "Several sources converging into one destination, with packets moving in."
@chip_w 132
@chip_h 36
@pitch 48
@sources [
%{name: "Mail", kind: "mail"},
%{name: "Spreadsheet", kind: "sheet"},
%{name: "ERP", kind: "erp"},
%{name: "Chat", kind: "chat"}
]
attr(:sources, :list,
default: @sources,
doc:
"Two to six `%{name, kind}`. `kind` picks the icon: `mail`, `sheet`, `erp`, `chat` or `docs`. Keep names under 12 characters."
)
attr(:kicker, :string, default: "One record", doc: "The eyebrow of the destination card.")
attr(:title, :string, default: "Order 4812", doc: "Up to 12 characters.")
attr(:status, :string, default: "Up to date", doc: "Up to 14 characters.")
attr(:label, :string, default: "Mail, a spreadsheet, the ERP and chat feeding one order record")
frame_attrs()
@spec connections_converge(map()) :: Phoenix.LiveView.Rendered.t()
def connections_converge(assigns) do
sources = Enum.take(assigns.sources, 6)
count = max(length(sources), 1)
height = max(48 + count * @chip_h + (count - 1) * (@pitch - @chip_h), 152)
mid = height / 2
first = mid - (count - 1) * @pitch / 2
items =
for {source, index} <- Enum.with_index(sources) do
y = first + index * @pitch
%{
name: source.name,
kind: Map.get(source, :kind, "other"),
index: index,
top: y - @chip_h / 2,
y: y,
d: "M#{14 + @chip_w} #{y} C204 #{y} 196 #{mid} 250 #{mid}",
begin: "#{Float.round(index * 0.55, 2)}s"
}
end
assigns =
assigns
|> assign(:items, items)
|> assign(:count, count)
|> assign(:height, height)
|> assign(:mid, mid)
|> assign(:chip, %{w: @chip_w, h: @chip_h, right: 14 + @chip_w})
~H"""
<.frame name="connections/converge" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 400 #{@height}"}>
<g class="ehv-h-lift">
<path
:for={item <- @items}
class={"ehv-line ehv-a-draw ehv-i-#{item.index + 2}"}
d={item.d}
pathLength="1"
/>
<line
class="ehv-line-lime ehv-a-draw ehv-i-8"
x1="250"
x2="270"
y1={@mid}
y2={@mid}
pathLength="1"
/>
<g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-12">
<circle :for={item <- @items} class="ehv-fill-lime" r="2.5" opacity="0">
<animateMotion path={item.d} dur="2.6s" begin={item.begin} repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0;1;1;0"
keyTimes="0;0.2;0.8;1"
dur="2.6s"
begin={item.begin}
repeatCount="indefinite"
/>
</circle>
</g>
<g :for={item <- @items} class={"ehv-a-slide ehv-i-#{item.index}"}>
<rect
class="ehv-card ehv-connections-converge__chip"
x="14.5"
y={item.top + 0.5}
width={@chip.w - 1}
height={@chip.h - 1}
rx="9"
/>
<rect class="ehv-card-2" x="21.5" y={item.top + 7.5} width="21" height="21" rx="6" />
<g transform={"translate(26 #{item.top + 12})"}>
<.icon kind={item.kind} />
</g>
<text class="ehv-t-strong" x="52" y={item.y + 4.5} font-size="12.5">{item.name}</text>
<circle class="ehv-node-soft" cx={@chip.right} cy={item.y} r="2.5" />
</g>
<rect
class="ehv-fill-lime ehv-a-pop ehv-i-8"
x="245.5"
y={@mid - 4.5}
width="9"
height="9"
rx="2.5"
/>
<g class="ehv-a-rise ehv-i-6">
<rect class="ehv-card-lime" x="270.5" y={@mid - 47.5} width="115" height="95" rx="12" />
<text class="ehv-mono ehv-t-lime" x="284" y={@mid - 26} font-size="7.5">
{String.upcase(@kicker)}
</text>
<text class="ehv-t-strong" x="284" y={@mid - 6} font-size="14.5">{@title}</text>
<line class="ehv-line-soft" x1="284" x2="372" y1={@mid + 7.5} y2={@mid + 7.5} />
<text class="ehv-mono" x="284" y={@mid + 23} font-size="7.5">
{@count} {if @count == 1, do: "SOURCE", else: "SOURCES"}
</text>
<circle class="ehv-fill-lime ehv-a-pulse" cx="287" cy={@mid + 34} r="2.5" />
<text class="ehv-mono ehv-t-2" x="295" y={@mid + 37} font-size="7.5">
{String.upcase(@status)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
# Icons live in a 12 by 12 box.
defp icon(%{kind: "mail"} = assigns) do
~H"""
<rect class="ehv-line-ink" y="1.5" width="12" height="9" rx="1.5" />
<path class="ehv-line-ink ehv-round" d="M0.5 2.5 L6 7 L11.5 2.5" />
"""
end
defp icon(%{kind: "sheet"} = assigns) do
~H"""
<rect class="ehv-line-ink" width="12" height="12" rx="1.5" />
<path class="ehv-line-ink" d="M0 4 H12 M0 8 H12 M4.5 0 V12" />
"""
end
defp icon(%{kind: "erp"} = assigns) do
~H"""
<ellipse class="ehv-line-ink" cx="6" cy="2.5" rx="5.5" ry="2" />
<path
class="ehv-line-ink"
d="M0.5 2.5 V9.5 C0.5 12 11.5 12 11.5 9.5 V2.5 M0.5 6 C0.5 8.5 11.5 8.5 11.5 6"
/>
"""
end
defp icon(%{kind: "chat"} = assigns) do
~H"""
<path class="ehv-line-ink ehv-round" d="M0.5 1 H11.5 V8.5 H6 L3 11.5 V8.5 H0.5 Z" />
"""
end
defp icon(%{kind: "docs"} = assigns) do
~H"""
<path class="ehv-line-ink ehv-round" d="M1.5 0.5 H7.5 L10.5 3.5 V11.5 H1.5 Z M4 6 H8 M4 8.5 H8" />
"""
end
defp icon(assigns) do
~H"""
<circle class="ehv-line-ink" cx="6" cy="6" r="5" />
<circle class="ehv-fill-2" cx="6" cy="6" r="1.5" />
"""
end
end
assets/css/connections/converge.css
/* connections/converge */
.ehv-connections-converge { --ehv-max-w: 440px; }
.ehv--hover .ehv-connections-converge__chip { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-connections-converge__chip:hover { stroke: var(--ehv-lime); }