empowered.house ↗

states/maintenance.ex

Maintenance state

A maintenance state: a stack of service tiles with one pulled out and worked on, and when it is back.

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

<div class="h-96 rounded-xl border border-border">
  <.states_maintenance animated />
</div>
default
isometric
three services · custom copy
five services · last one · hover
light surface
isometric · fade out · still

Props

Prop Type Default What it does
services list ["Orders", "Invoices", "Stock", "Reports"] Two to five service names, 14 characters at most.
active integer 1 Index of the service that is pulled out.
running string "Running" The status of every other service.
status string "In maintenance" 14 characters at most.
title string "Back in 10 minutes" 34 characters at most.
note string "Scheduled · 02:00 to 02:10" The mono line under the title, 40 characters at most.
progress integer 60 How far the work is, 0 to 100.
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/states/maintenance.ex
maintenance.ex
defmodule EHVisuals.States.Maintenance do
  @moduledoc """
  A maintenance state: a stack of service tiles with one pulled out and being
  worked on, and a line that says when it is back.
  """

  use EHVisuals.Composition,
    name: "states/maintenance",
    title: "Maintenance state",
    description:
      "A maintenance state: a stack of service tiles with one pulled out and worked on, and when it is back."

  attr(:services, :list,
    default: ~w(Orders Invoices Stock Reports),
    doc: "Two to five service names, 14 characters at most."
  )

  attr(:active, :integer, default: 1, doc: "Index of the service that is pulled out.")
  attr(:running, :string, default: "Running", doc: "The status of every other service.")
  attr(:status, :string, default: "In maintenance", doc: "14 characters at most.")
  attr(:title, :string, default: "Back in 10 minutes", doc: "34 characters at most.")

  attr(:note, :string,
    default: "Scheduled · 02:00 to 02:10",
    doc: "The mono line under the title, 40 characters at most."
  )

  attr(:progress, :integer, default: 60, doc: "How far the work is, 0 to 100.")

  attr(:label, :string,
    default: "Four service tiles with the invoices tile pulled out for maintenance"
  )

  frame_attrs()

  @spec states_maintenance(map()) :: Phoenix.LiveView.Rendered.t()
  def states_maintenance(assigns) do
    services = Enum.take(assigns.services, 5)
    count = max(length(services), 1)
    active = assigns.active |> max(0) |> min(count - 1)

    rows =
      for {name, index} <- Enum.with_index(services) do
        %{name: name, index: index, y: 24 + index * 46, active: index == active}
      end

    title_y = 24 + count * 46 - 8 + 32
    bar_y = title_y + 32
    progress = assigns.progress |> max(0) |> min(100)

    assigns =
      assigns
      |> assign(:rows, rows)
      |> assign(:title_y, title_y)
      |> assign(:bar_y, bar_y)
      |> assign(:bar_w, Float.round(312 * progress / 100, 1))
      |> assign(:height, bar_y + 4 + 22)

    ~H"""
    <.frame name="states/maintenance" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 360 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="359" height={@height - 1} rx="12" />
          <g :for={row <- @rows}>
            <g :if={!row.active} class={"ehv-a-rise ehv-i-#{row.index + 1}"}>
              <rect class="ehv-card-2" x="24.5" y={row.y + 0.5} width="249" height="37" rx="9" />
              <rect class="ehv-node-soft" x="38.5" y={row.y + 15.5} width="8" height="8" rx="2.5" />
              <text class="ehv-t-2" x="58" y={row.y + 23.5} font-size="12.5">{row.name}</text>
              <text class="ehv-mono" x="260" y={row.y + 22.5} font-size="7.5" text-anchor="end">
                {String.upcase(@running)}
              </text>
            </g>
            <g :if={row.active}>
              <rect
                class={"ehv-states-maintenance__slot ehv-a-fade ehv-i-#{row.index + 1}"}
                x="24.5"
                y={row.y + 0.5}
                width="249"
                height="37"
                rx="9"
              />
              <g class={"ehv-a-slide ehv-i-#{row.index + 3}"}>
                <g class="ehv-states-maintenance__lifted">
                  <rect class="ehv-card-lime" x="86.5" y={row.y - 6.5} width="249" height="37" rx="9" />
                  <circle class="ehv-fill-lime-soft" cx="107" cy={row.y + 12} r="11" />
                  <g class="ehv-states-maintenance__wrench">
                    <path
                      class="ehv-line-lime ehv-round"
                      transform={"translate(#{100} #{row.y + 5})"}
                      d="M10.9 2.2 L8.9 4.2 L9.8 5.1 L10.7 6 L12.7 4 A3.6 3.6 0 0 1 8 8.2 L3.6 12.6 A1.2 1.2 0 0 1 1.9 10.9 L6.3 6.5 A3.6 3.6 0 0 1 10.9 2.2 Z"
                    />
                  </g>
                  <text class="ehv-t-strong" x="128" y={row.y + 16.5} font-size="12.5">
                    {row.name}
                  </text>
                  <text
                    class="ehv-mono ehv-t-lime"
                    x="322"
                    y={row.y + 15.5}
                    font-size="7.5"
                    text-anchor="end"
                  >
                    {String.upcase(@status)}
                  </text>
                </g>
              </g>
            </g>
          </g>
          <text class="ehv-t-strong ehv-a-fade ehv-i-6" x="24" y={@title_y} font-size="15">
            {@title}
          </text>
          <text class="ehv-mono ehv-a-fade ehv-i-7" x="24" y={@title_y + 18} font-size="8.5">
            {String.upcase(@note)}
          </text>
          <g class="ehv-a-fade ehv-i-8">
            <rect class="ehv-fill-soft" x="24" y={@bar_y} width="312" height="4" rx="2" />
            <rect
              class="ehv-fill-lime ehv-a-grow-x ehv-i-9"
              x="24"
              y={@bar_y}
              width={@bar_w}
              height="4"
              rx="2"
            />
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/states/maintenance.css
maintenance.css
/* states/maintenance */
.ehv-states-maintenance { --ehv-max-w: 400px; }

/* The place the pulled-out tile leaves behind. */
.ehv-states-maintenance__slot { fill: none; stroke: var(--ehv-border-strong); stroke-width: 1; stroke-dasharray: 3 4; }

/* The wrench works back and forth around its own centre. */
@keyframes ehv-states-maintenance-turn {
  from { transform: rotate(-16deg); }
  to { transform: rotate(18deg); }
}
.ehv-states-maintenance__wrench { transform-box: fill-box; transform-origin: center; }
.ehv--animated .ehv-states-maintenance__wrench {
  animation: ehv-states-maintenance-turn 1.1s ease-in-out infinite alternate;
  animation-delay: calc(var(--ehv-delay, 0ms) + 6 * var(--ehv-step) + var(--ehv-dur));
}

.ehv--hover .ehv-states-maintenance__lifted { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-states-maintenance__lifted { transform: translate(8px, -3px); }