empowered.house ↗

agents/handoff.ex

Handoff

Three cards in a row: the agent drafts, a person decides, the system acts.

Use it
# in the module that renders the template
use EHVisuals

<div class="h-96 rounded-xl border border-border">
  <.agents_handoff animated />
</div>
default
isometric
not decided yet
custom copy
light surface
no glow · still

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
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
handoff.css
/* agents/handoff */
.ehv-agents-handoff { --ehv-max-w: 520px; --ehv-step: 140ms; --ehv-iso-scale: 0.82; }