empowered.house ↗

states/empty.ex

Empty state

An empty state: a grid of dashed placeholder slots with one add action in the middle.

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

<div class="h-96 rounded-xl border border-border">
  <.states_empty animated />
</div>
default
isometric
three by two · custom copy
seven columns · hover
light surface
isometric · fade out · still

Props

Prop Type Default What it does
title string "Add your first workflow" 34 characters at most.
text string "Nothing runs here yet. Start with one process." The line under the title, 54 characters at most. Empty hides it.
hint string "Or pick one of 12 templates" A mono line at the bottom, 44 characters at most. Empty hides it.
columns integer 5 Placeholder columns, 3 to 7.
rows integer 3 Placeholder rows, 1 to 4.
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/empty.ex
empty.ex
defmodule EHVisuals.States.Empty do
  @moduledoc """
  An empty state: a grid of dashed placeholder slots with the add action in
  the middle one, and a line that says what to do first.
  """

  use EHVisuals.Composition,
    name: "states/empty",
    title: "Empty state",
    description:
      "An empty state: a grid of dashed placeholder slots with one add action in the middle."

  attr(:title, :string, default: "Add your first workflow", doc: "34 characters at most.")

  attr(:text, :string,
    default: "Nothing runs here yet. Start with one process.",
    doc: "The line under the title, 54 characters at most. Empty hides it."
  )

  attr(:hint, :string,
    default: "Or pick one of 12 templates",
    doc: "A mono line at the bottom, 44 characters at most. Empty hides it."
  )

  attr(:columns, :integer, default: 5, doc: "Placeholder columns, 3 to 7.")
  attr(:rows, :integer, default: 3, doc: "Placeholder rows, 1 to 4.")

  attr(:label, :string,
    default: "An empty grid of placeholder slots with an add button in the middle"
  )

  frame_attrs()

  @spec states_empty(map()) :: Phoenix.LiveView.Rendered.t()
  def states_empty(assigns) do
    columns = assigns.columns |> max(3) |> min(7)
    rows = assigns.rows |> max(1) |> min(4)
    cell_w = Float.round((320 - (columns - 1) * 10) / columns, 1)
    add = {div(columns, 2), div(rows - 1, 2)}

    slots =
      for row <- 0..(rows - 1), column <- 0..(columns - 1), {column, row} != add do
        %{
          x: Float.round(20 + column * (cell_w + 10), 1),
          y: 20 + row * 48,
          index: column + row + 1
        }
      end

    {add_column, add_row} = add
    title_y = 20 + rows * 48 - 10 + 34
    text_y = if assigns.text == "", do: title_y, else: title_y + 20
    hint_y = if assigns.hint == "", do: text_y, else: text_y + 22

    assigns =
      assigns
      |> assign(:slots, slots)
      |> assign(:cell_w, cell_w)
      |> assign(:add_x, Float.round(20 + add_column * (cell_w + 10), 1))
      |> assign(:add_y, 20 + add_row * 48)
      |> assign(:title_y, title_y)
      |> assign(:text_y, text_y)
      |> assign(:hint_y, hint_y)
      |> assign(:height, hint_y + 24)

    ~H"""
    <.frame name="states/empty" 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" />
          <rect
            :for={slot <- @slots}
            class={"ehv-states-empty__slot ehv-a-fade ehv-i-#{slot.index}"}
            x={slot.x}
            y={slot.y}
            width={@cell_w}
            height="38"
            rx="8"
          />
          <circle
            class="ehv-fill-lime ehv-a-ping ehv-i-8"
            cx={@add_x + @cell_w / 2}
            cy={@add_y + 19}
            r="9"
          />
          <g class="ehv-a-pop ehv-i-6">
            <rect
              class="ehv-states-empty__add"
              x={@add_x}
              y={@add_y}
              width={@cell_w}
              height="38"
              rx="8"
            />
            <path
              class="ehv-line-lime ehv-round ehv-states-empty__plus"
              d={"M#{@add_x + @cell_w / 2 - 5.5} #{@add_y + 19}h11 M#{@add_x + @cell_w / 2} #{@add_y + 13.5}v11"}
            />
          </g>
          <text
            class="ehv-t-strong ehv-a-fade ehv-i-8"
            x="180"
            y={@title_y}
            font-size="15"
            text-anchor="middle"
          >
            {@title}
          </text>
          <text
            :if={@text != ""}
            class="ehv-t-2 ehv-a-fade ehv-i-9"
            x="180"
            y={@text_y}
            font-size="11.5"
            text-anchor="middle"
          >
            {@text}
          </text>
          <text
            :if={@hint != ""}
            class="ehv-mono ehv-a-fade ehv-i-10"
            x="180"
            y={@hint_y}
            font-size="8"
            text-anchor="middle"
          >
            {String.upcase(@hint)}
          </text>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/states/empty.css
empty.css
/* states/empty */
.ehv-states-empty { --ehv-max-w: 400px; --ehv-step: 70ms; }

.ehv-states-empty__slot { fill: none; stroke: var(--ehv-border-strong); stroke-width: 1; stroke-dasharray: 3 4; }
.ehv-states-empty__add { fill: var(--ehv-lime-soft); stroke: var(--ehv-lime); stroke-width: 1.25; }
.ehv .ehv-states-empty__plus { stroke-width: 1.5; }

.ehv--hover .ehv-states-empty__add { transition: fill 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-states-empty__add { fill: var(--ehv-lime-fill); }
.ehv--hover .ehv-states-empty__plus { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-states-empty__plus { stroke: var(--ehv-on-lime); }