apps/window.ex
App window
A business app in its window: navigation, one action and a table of records with their status.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.apps_window animated />
</div>
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
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
/* 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); }