devices/laptop.ex
Laptop
A laptop in outline presenting an app screen: a small dashboard with three figures, a bar chart and a list.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.devices_laptop animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "Operations" |
The app name on the screen. |
period |
string | "Week 46" |
|
stats |
list | [%{label: "Orders", value: "1,284"}, %{label: "On time", value: "96.2%"}, ...] |
Up to three `%{label, value}`. Keep values under 10 characters. |
chart_title |
string | "Orders per day" |
|
values |
list | [18, 24, 21, 30, 27, 34, ...] |
The bar series, oldest first, 3 to 12 numbers. The last bar is lime. |
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/devices/laptop.ex
defmodule EHVisuals.Devices.Laptop do
@moduledoc """
A laptop drawn in outline, presenting an app screen: a small dashboard with
three figures, a bar chart and a list.
"""
use EHVisuals.Composition,
name: "devices/laptop",
title: "Laptop",
description:
"A laptop in outline presenting an app screen: a small dashboard with three figures, a bar chart and a list."
@stats [
%{label: "Orders", value: "1,284"},
%{label: "On time", value: "96.2%"},
%{label: "Open", value: "38"}
]
@values [18, 24, 21, 30, 27, 34, 31, 42]
@list_widths [52, 40, 58, 34]
attr(:title, :string, default: "Operations", doc: "The app name on the screen.")
attr(:period, :string, default: "Week 46")
attr(:stats, :list,
default: @stats,
doc: "Up to three `%{label, value}`. Keep values under 10 characters."
)
attr(:chart_title, :string, default: "Orders per day")
attr(:values, :list,
default: @values,
doc: "The bar series, oldest first, 3 to 12 numbers. The last bar is lime."
)
attr(:label, :string, default: "A laptop showing a small operations dashboard with a bar chart")
frame_attrs()
@spec devices_laptop(map()) :: Phoenix.LiveView.Rendered.t()
def devices_laptop(assigns) do
values = Enum.take(assigns.values, 12)
top = Enum.max([1 | values])
slot = 170 / max(length(values), 1)
last = length(values) - 1
bars =
for {value, index} <- Enum.with_index(values) do
height = Float.round(max(value / top * 68, 3.0), 1)
%{
index: index,
x: Float.round(82 + index * slot + slot * 0.19, 1),
y: Float.round(200 - height, 1),
w: Float.round(slot * 0.62, 1),
h: height,
last: index == last
}
end
tiles =
for {stat, index} <- assigns.stats |> Enum.take(3) |> Enum.with_index() do
%{
index: index,
x: Float.round(72 + index * 101.3, 1),
label: stat.label,
value: stat.value
}
end
assigns =
assigns
|> assign(:bars, bars)
|> assign(:tiles, tiles)
|> assign(:list, Enum.with_index(@list_widths))
~H"""
<.frame name="devices/laptop" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 440 250">
<g class="ehv-h-lift">
<g class="ehv-a-rise">
<rect class="ehv-card" x="50.5" y="4.5" width="339" height="226" rx="12" />
<rect class="ehv-well" x="60" y="14" width="320" height="206" rx="4" />
<circle class="ehv-fill-4" cx="220" cy="9.3" r="1.3" />
<path
class="ehv-card-2"
d="M18.5 230.5 H421.5 V237.5 A8 8 0 0 1 413.5 245.5 H26.5 A8 8 0 0 1 18.5 237.5 Z"
/>
<path class="ehv-line-soft" d="M194 230.5 v1.5 a3 3 0 0 0 3 3 h46 a3 3 0 0 0 3 -3 v-1.5" />
</g>
<g class="ehv-a-fade ehv-i-2">
<rect class="ehv-card-3" x="72" y="24" width="12" height="12" rx="3.5" />
<text class="ehv-t-strong" x="91" y="34" font-size="11">{@title}</text>
<text class="ehv-mono" x="368" y="33.5" font-size="7.5" text-anchor="end">
{String.upcase(@period)}
</text>
<line class="ehv-line-faint" x1="60.5" x2="379.5" y1="44.5" y2="44.5" />
</g>
<g :for={tile <- @tiles} class={"ehv-a-rise ehv-i-#{tile.index + 3}"}>
<rect class="ehv-card" x={tile.x + 0.5} y="52.5" width="92.3" height="38" rx="6" />
<text class="ehv-mono" x={tile.x + 10} y="66.5" font-size="7">
{String.upcase(tile.label)}
</text>
<text class="ehv-t-strong" x={tile.x + 10} y="83.5" font-size="14">{tile.value}</text>
</g>
<g class="ehv-a-fade ehv-i-6">
<rect class="ehv-card" x="72.5" y="98.5" width="190" height="112" rx="6" />
<text class="ehv-mono" x="82" y="113" font-size="7">{String.upcase(@chart_title)}</text>
<line class="ehv-line-faint" x1="82" x2="252" y1="200.5" y2="200.5" />
<rect class="ehv-card" x="270.5" y="98.5" width="97.5" height="112" rx="6" />
<rect class="ehv-fill-strong" x="280" y="108" width="36" height="5" rx="2.5" />
<g :for={{width, index} <- @list}>
<line
class="ehv-line-faint"
x1="280"
x2="358"
y1={122.5 + index * 21}
y2={122.5 + index * 21}
/>
<circle class="ehv-node-soft" cx="284" cy={133 + index * 21} r="3.5" />
<rect
class="ehv-fill-soft"
x="293"
y={130.5 + index * 21}
width={width}
height="5"
rx="2.5"
/>
</g>
</g>
<rect
:for={bar <- @bars}
class={[
"ehv-a-grow-y",
"ehv-i-#{min(bar.index + 7, 24)}",
if(bar.last, do: "ehv-fill-lime", else: "ehv-fill-4")
]}
x={bar.x}
y={bar.y}
width={bar.w}
height={bar.h}
rx="1.5"
/>
</g>
</svg>
</.frame>
"""
end
end
assets/css/devices/laptop.css
/* devices/laptop */
.ehv-devices-laptop { --ehv-max-w: 480px; }