empowered.house ↗

apps/window.ex

App window

A business app in its window: navigation, one action and a table of records with their status.

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

<div class="h-96 rounded-xl border border-border">
  <.apps_window animated />
</div>
default
isometric
custom app
hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
title string "orders.yourcompany.com"
nav list ["Orders", "Stock", "Invoices", "Customers"] Up to six sidebar items. The first is active.
heading string "Orders"
action string "New order"
columns list ["Order", "Customer", "Status"] Three column names.
rows list [%{id: "PO-2291", name: "Northwind", status: "Posted", tone: "ok"}, %{...}, ...] Up to four `%{id, name, status, tone}`. Tone is `ok`, `wait` or `risk`.
footer string "4 of 128 orders"
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/apps/window.ex
window.ex
defmodule EHVisuals.Apps.Window do
  @moduledoc """
  A business app in its window: navigation, a heading, one action and a table
  of records with their status.
  """

  use EHVisuals.Composition,
    name: "apps/window",
    title: "App window",
    description:
      "A business app in its window: navigation, one action and a table of records with their status."

  @rows [
    %{id: "PO-2291", name: "Northwind", status: "Posted", tone: "ok"},
    %{id: "PO-2290", name: "Acme Tools", status: "Waiting", tone: "wait"},
    %{id: "PO-2289", name: "Globex", status: "Posted", tone: "ok"},
    %{id: "PO-2288", name: "Initech", status: "Blocked", tone: "risk"}
  ]

  attr(:title, :string, default: "orders.yourcompany.com")

  attr(:nav, :list,
    default: ~w(Orders Stock Invoices Customers),
    doc: "Up to six sidebar items. The first is active."
  )

  attr(:heading, :string, default: "Orders")
  attr(:action, :string, default: "New order")
  attr(:columns, :list, default: ~w(Order Customer Status), doc: "Three column names.")

  attr(:rows, :list,
    default: @rows,
    doc: "Up to four `%{id, name, status, tone}`. Tone is `ok`, `wait` or `risk`."
  )

  attr(:footer, :string, default: "4 of 128 orders")

  attr(:label, :string,
    default: "An orders app with a table of four purchase orders and their status"
  )

  frame_attrs()

  @spec apps_window(map()) :: Phoenix.LiveView.Rendered.t()
  def apps_window(assigns) do
    assigns =
      assigns
      |> assign(:nav_items, assigns.nav |> Enum.take(6) |> Enum.with_index())
      |> assign(:items, assigns.rows |> Enum.take(4) |> Enum.with_index())
      |> assign(:action_w, 24 + String.length(assigns.action) * 6.2)

    ~H"""
    <.frame name="apps/window" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 460 300">
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="459" height="299" rx="12" />
          <g class="ehv-a-fade ehv-i-1">
            <circle :for={x <- [18, 32, 46]} class="ehv-node-soft" cx={x} cy="17.5" r="4" />
            <text class="ehv-mono" x="230" y="21" font-size="8.5" text-anchor="middle">{@title}</text>
            <line class="ehv-line-soft" x1="0.5" x2="459.5" y1="35.5" y2="35.5" />
            <line class="ehv-line-faint" x1="108.5" x2="108.5" y1="36" y2="299.5" />
          </g>
          <g :for={{item, index} <- @nav_items} class={"ehv-a-slide ehv-i-#{index + 2}"}>
            <rect
              :if={index == 0}
              class="ehv-fill-surface-3"
              x="8"
              y={46 + index * 28}
              width="92"
              height="24"
              rx="6"
            />
            <rect
              :if={index == 0}
              class="ehv-fill-lime"
              x="16"
              y={54 + index * 28}
              width="8"
              height="8"
              rx="2.5"
            />
            <rect
              :if={index != 0}
              class="ehv-node-soft"
              x="16.5"
              y={54.5 + index * 28}
              width="7"
              height="7"
              rx="2"
            />
            <text class={[index != 0 && "ehv-t-3"]} x="32" y={62 + index * 28} font-size="11.5">
              {item}
            </text>
          </g>
          <g class="ehv-a-fade ehv-i-3">
            <text class="ehv-t-strong" x="128" y="69" font-size="16">{@heading}</text>
            <rect
              class="ehv-fill-lime"
              x={440 - @action_w}
              y="52"
              width={@action_w}
              height="24"
              rx="6"
            />
            <text
              class="ehv-t-strong ehv-t-on-lime"
              x={440 - @action_w / 2}
              y="68"
              font-size="10.5"
              text-anchor="middle"
            >
              {@action}
            </text>
            <text class="ehv-mono" x="128" y="102" font-size="8">
              {String.upcase(Enum.at(@columns, 0, ""))}
            </text>
            <text class="ehv-mono" x="206" y="102" font-size="8">
              {String.upcase(Enum.at(@columns, 1, ""))}
            </text>
            <text class="ehv-mono" x="440" y="102" font-size="8" text-anchor="end">
              {String.upcase(Enum.at(@columns, 2, ""))}
            </text>
            <line class="ehv-line-soft" x1="128" x2="440" y1="111.5" y2="111.5" />
          </g>
          <g :for={{row, index} <- @items} class={"ehv-a-rise ehv-i-#{index + 4}"}>
            <line
              :if={index > 0}
              class="ehv-line-faint"
              x1="128"
              x2="440"
              y1={111.5 + index * 38}
              y2={111.5 + index * 38}
            />
            <text class="ehv-mono ehv-t-2" x="128" y={134.5 + index * 38} font-size="9.5">
              {row.id}
            </text>
            <text x="206" y={135 + index * 38} font-size="12.5">{row.name}</text>
            <rect
              class={"ehv-apps-window__pill--#{row.tone}"}
              x={440 - pill_w(row.status)}
              y={120.5 + index * 38}
              width={pill_w(row.status)}
              height="20"
              rx="6"
            />
            <text
              class={["ehv-mono", "ehv-apps-window__pill-text--#{row.tone}"]}
              x={440 - pill_w(row.status) / 2}
              y={134 + index * 38}
              font-size="8"
              text-anchor="middle"
            >
              {String.upcase(row.status)}
            </text>
          </g>
          <text class="ehv-mono ehv-t-4 ehv-a-fade ehv-i-8" x="128" y="283" font-size="8">
            {String.upcase(@footer)}
          </text>
        </g>
      </svg>
    </.frame>
    """
  end

  defp pill_w(status), do: Float.round(18 + String.length(status) * 5.8, 1)
end
assets/css/apps/window.css
window.css
/* apps/window */
.ehv-apps-window { --ehv-max-w: 500px; }

.ehv-apps-window__pill--ok { fill: var(--ehv-lime-soft); }
.ehv-apps-window__pill--wait { fill: var(--ehv-surface-3); stroke: var(--ehv-border-soft); stroke-width: 1; }
.ehv-apps-window__pill--risk { fill: var(--ehv-coral-soft); }

.ehv svg .ehv-mono.ehv-apps-window__pill-text--ok { fill: var(--ehv-lime); }
.ehv svg .ehv-mono.ehv-apps-window__pill-text--wait { fill: var(--ehv-ink-2); }
.ehv svg .ehv-mono.ehv-apps-window__pill-text--risk { fill: var(--ehv-coral); }