empowered.house ↗

process/stages.ex

Stages

A stage rule with numbered stages and the current one marked, for how-we-work sections.

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

<div class="h-96 rounded-xl border border-border">
  <.process_stages animated />
</div>
default
isometric
last stage
three stages · custom copy
light surface
no glow · still

Props

Prop Type Default What it does
stages list [%{title: "Land", detail: "Pick one workflow"}, %{title: "Install", detail: "Make it work"}, ...] Two to five `%{title, detail}`. Keep details under 18 characters.
current integer 1 Index of the stage you are in.
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/stages.ex
stages.ex
defmodule EHVisuals.Process.Stages do
  @moduledoc """
  The stage rule of an engagement: Land, Install, Enable, Expand, with the
  current stage marked.
  """

  use EHVisuals.Composition,
    name: "process/stages",
    title: "Stages",
    description:
      "A stage rule with numbered stages and the current one marked, for how-we-work sections."

  @stages [
    %{title: "Land", detail: "Pick one workflow"},
    %{title: "Install", detail: "Make it work"},
    %{title: "Enable", detail: "Make it yours"},
    %{title: "Expand", detail: "Grow what works"}
  ]

  attr(:stages, :list,
    default: @stages,
    doc: "Two to five `%{title, detail}`. Keep details under 18 characters."
  )

  attr(:current, :integer, default: 1, doc: "Index of the stage you are in.")
  attr(:label, :string, default: "Four engagement stages with the second one current")
  frame_attrs()

  @spec process_stages(map()) :: Phoenix.LiveView.Rendered.t()
  def process_stages(assigns) do
    count = max(length(assigns.stages), 1)
    column = 400 / count

    items =
      for {stage, index} <- Enum.with_index(assigns.stages) do
        stage
        |> Map.put(:index, index)
        |> Map.put(:x, Float.round(20 + index * column, 1))
        |> Map.put(
          :number,
          index |> Kernel.+(1) |> Integer.to_string() |> String.pad_leading(2, "0")
        )
        |> Map.put(:state, state(index, assigns.current))
      end

    assigns =
      assigns
      |> assign(:items, items)
      |> assign(:progress_x, Float.round(20 + min(assigns.current, count - 1) * column, 1))

    ~H"""
    <.frame name="process/stages" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 440 124">
        <g class="ehv-h-lift">
          <line class="ehv-line ehv-a-draw" x1="20" x2="420" y1="25" y2="25" pathLength="1" />
          <line
            class="ehv-line-lime ehv-a-draw ehv-i-2"
            x1="20"
            x2={@progress_x}
            y1="25"
            y2="25"
            pathLength="1"
          />
          <g :for={item <- @items}>
            <rect
              :if={item.state == :now}
              class="ehv-fill-lime ehv-a-ping"
              x={item.x}
              y="19"
              width="12"
              height="12"
              rx="3.5"
            />
            <rect
              class={[marker(item.state), "ehv-a-pop ehv-i-#{item.index + 2}"]}
              x={item.x}
              y="19"
              width="12"
              height="12"
              rx="3.5"
            />
            <g class={"ehv-a-rise ehv-i-#{item.index + 3}"}>
              <text
                class={["ehv-mono", item.state == :now && "ehv-t-lime"]}
                x={item.x}
                y="58"
                font-size="9"
              >
                {item.number}
              </text>
              <text
                class={["ehv-t-strong", item.state == :next && "ehv-t-3"]}
                x={item.x}
                y="82"
                font-size="15"
              >
                {item.title}
              </text>
              <text
                class={if item.state == :next, do: "ehv-t-4", else: "ehv-t-3"}
                x={item.x}
                y="101"
                font-size="11"
              >
                {item.detail}
              </text>
            </g>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end

  defp state(index, current) when index < current, do: :done
  defp state(index, current) when index == current, do: :now
  defp state(_index, _current), do: :next

  defp marker(:done), do: "ehv-fill-ink"
  defp marker(:now), do: "ehv-fill-lime"
  defp marker(:next), do: "ehv-node-soft"
end
assets/css/process/stages.css
stages.css
/* process/stages */
.ehv-process-stages { --ehv-max-w: 560px; --ehv-step: 120ms; --ehv-iso-scale: 0.82; }