empowered.house ↗

devices/laptop.ex

Laptop

A laptop in outline presenting an app screen: a small dashboard with three figures, a bar chart and a list.

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

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

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
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
laptop.css
/* devices/laptop */
.ehv-devices-laptop { --ehv-max-w: 480px; }