empowered.house ↗

process/flow.ex

Flow

A process as an event line: steps on a rail, with a packet travelling to the current one.

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

<div class="h-96 rounded-xl border border-border">
  <.process_flow animated />
</div>
default
isometric
three steps · first one running
five steps · done
light surface
fade out · no glow · still

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
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
flow.css
/* process/flow */
.ehv-process-flow { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }