empowered.house ↗

browser/loading.ex

Browser loading

A browser window while a page loads, with a progress line under the address bar and skeleton blocks for the content.

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

<div class="h-96 rounded-xl border border-border">
  <.browser_loading animated />
</div>
default
isometric
almost there · custom copy
just started · hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
url string "erp.yourcompany.com/orders" Up to 44 characters.
progress integer 62 How far the page has loaded, 0 to 100.
status string "Waiting for the ERP" The status bar line, up to 40 characters.
rows integer 3 Skeleton list rows under the three tiles, 1 to 5.
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/browser/loading.ex
loading.ex
defmodule EHVisuals.Browser.Loading do
  @moduledoc """
  A browser window while a page loads: a progress line under the address bar
  and skeleton blocks where the content will be.
  """

  use EHVisuals.Composition,
    name: "browser/loading",
    title: "Browser loading",
    description:
      "A browser window while a page loads, with a progress line under the address bar and skeleton blocks for the content."

  @row_widths [150, 118, 172, 104, 140]

  attr(:url, :string, default: "erp.yourcompany.com/orders", doc: "Up to 44 characters.")
  attr(:progress, :integer, default: 62, doc: "How far the page has loaded, 0 to 100.")

  attr(:status, :string,
    default: "Waiting for the ERP",
    doc: "The status bar line, up to 40 characters."
  )

  attr(:rows, :integer, default: 3, doc: "Skeleton list rows under the three tiles, 1 to 5.")

  attr(:label, :string,
    default: "A browser window loading a page, with skeleton blocks in place of the content"
  )

  frame_attrs()

  @spec browser_loading(map()) :: Phoenix.LiveView.Rendered.t()
  def browser_loading(assigns) do
    progress = assigns.progress |> max(0) |> min(100)
    count = assigns.rows |> max(1) |> min(5)
    status_y = 184 + count * 24 + 4

    rows =
      for index <- 0..(count - 1) do
        %{index: index, y: 184 + index * 24, w: Enum.at(@row_widths, index)}
      end

    assigns =
      assigns
      |> assign(:percent, progress)
      |> assign(:bar_w, Float.round(progress / 100 * 398, 1))
      |> assign(
        :tiles,
        for(index <- 0..2, do: %{index: index, x: Float.round(24 + index * 121.3, 1)})
      )
      |> assign(:items, rows)
      |> assign(:status_y, status_y)
      |> assign(:height, status_y + 28)

    ~H"""
    <.frame name="browser/loading" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 400 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="399" height={@height - 1} rx="12" />
          <g class="ehv-a-fade ehv-i-1">
            <path class="ehv-line-ink ehv-round" d="M21 16 l-4 4 l4 4" />
            <path class="ehv-line ehv-round" d="M33 16 l4 4 l-4 4" />
            <path class="ehv-line-ink ehv-round" d="M49 17 l6 6 m0 -6 l-6 6" />
            <rect class="ehv-well" x="68" y="9" width="314" height="22" rx="11" />
            <g class="ehv-browser-loading__spin">
              <circle class="ehv-line-soft" cx="82" cy="20" r="4.5" />
              <path class="ehv-line-lime ehv-round" d="M82 15.5 a4.5 4.5 0 0 1 4.5 4.5" />
            </g>
            <text class="ehv-mono ehv-t-2" x="94" y="23" font-size="8.5">{@url}</text>
            <line class="ehv-line-soft" x1="0.5" x2="399.5" y1="40.5" y2="40.5" />
          </g>
          <g class="ehv-a-fade ehv-i-2">
            <rect
              class="ehv-fill-lime ehv-a-grow-x ehv-i-2"
              x="1"
              y="39.5"
              width={@bar_w}
              height="2"
            />
            <circle class="ehv-fill-lime ehv-a-pulse ehv-i-6" cx={1 + @bar_w} cy="40.5" r="2.5" />
          </g>
          <g class="ehv-a-fade ehv-i-3">
            <rect
              class="ehv-fill-soft ehv-a-pulse ehv-i-0"
              x="24"
              y="60"
              width="56"
              height="6"
              rx="3"
            />
            <rect
              class="ehv-fill-strong ehv-a-pulse ehv-i-1"
              x="24"
              y="76"
              width="168"
              height="14"
              rx="4"
            />
            <rect
              class="ehv-fill-soft ehv-a-pulse ehv-i-2"
              x="24"
              y="100"
              width="236"
              height="6"
              rx="3"
            />
            <rect class="ehv-line-soft" x="306.5" y="70.5" width="69" height="23" rx="6" />
            <rect
              class="ehv-fill-soft ehv-a-pulse ehv-i-2"
              x="324"
              y="79.5"
              width="34"
              height="5"
              rx="2.5"
            />
          </g>
          <g :for={tile <- @tiles} class={"ehv-a-rise ehv-i-#{tile.index + 4}"}>
            <rect class="ehv-line-soft" x={tile.x + 0.5} y="122.5" width="108.3" height="47" rx="8" />
            <rect
              class={"ehv-fill-soft ehv-a-pulse ehv-i-#{tile.index + 3}"}
              x={tile.x + 12}
              y="134"
              width="40"
              height="5"
              rx="2.5"
            />
            <rect
              class={"ehv-fill-strong ehv-a-pulse ehv-i-#{tile.index + 4}"}
              x={tile.x + 12}
              y="148"
              width="64"
              height="10"
              rx="3"
            />
          </g>
          <g :for={row <- @items} class={"ehv-a-rise ehv-i-#{row.index + 7}"}>
            <line class="ehv-line-faint" x1="24" x2="376" y1={row.y + 0.5} y2={row.y + 0.5} />
            <g class={"ehv-a-pulse ehv-i-#{row.index + 7}"}>
              <circle class="ehv-fill-soft" cx="31" cy={row.y + 12.5} r="6" />
              <rect class="ehv-fill-soft" x="46" y={row.y + 9.5} width={row.w} height="6" rx="3" />
              <rect class="ehv-fill-soft" x="328" y={row.y + 7.5} width="48" height="10" rx="5" />
            </g>
          </g>
          <g class="ehv-a-fade ehv-i-10">
            <line
              class="ehv-line-faint"
              x1="0.5"
              x2="399.5"
              y1={@status_y + 0.5}
              y2={@status_y + 0.5}
            />
            <text class="ehv-mono" x="24" y={@status_y + 17.5} font-size="8">
              {String.upcase(@status)}
            </text>
            <text
              class="ehv-mono ehv-t-lime"
              x="376"
              y={@status_y + 17.5}
              font-size="8"
              text-anchor="end"
            >
              {@percent}%
            </text>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/browser/loading.css
loading.css
/* browser/loading */
.ehv-browser-loading { --ehv-max-w: 440px; }

/* The spinner in the address bar turns around the centre of its own track. */
@keyframes ehv-browser-loading-spin { to { transform: rotate(360deg); } }

.ehv-browser-loading__spin { transform-box: fill-box; transform-origin: center; }
.ehv--animated .ehv-browser-loading__spin { animation: ehv-browser-loading-spin 1.1s linear infinite; }