empowered.house ↗

apps/form.ex

Form

A short internal form with filled fields, one in focus, a rule and one lime action.

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

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

Props

Prop Type Default What it does
title string "New purchase request"
subtitle string "Goes to your team lead first"
fields list [%{label: "Supplier", value: "Northwind"}, %{label: "Amount", value: "€18,400"}, ...] Up to four `%{label, value}`.
focus integer 1 Index of the field in focus. A negative index focuses none.
note string "Needs approval above €5,000"
submit string "Send for approval"
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/form.ex
form.ex
defmodule EHVisuals.Apps.Form do
  @moduledoc """
  A short internal form: a few filled fields, one in focus, and one action.
  """

  use EHVisuals.Composition,
    name: "apps/form",
    title: "Form",
    description:
      "A short internal form with filled fields, one in focus, a rule and one lime action."

  @fields [
    %{label: "Supplier", value: "Northwind"},
    %{label: "Amount", value: "€18,400"},
    %{label: "Needed by", value: "12 December"}
  ]

  attr(:title, :string, default: "New purchase request")
  attr(:subtitle, :string, default: "Goes to your team lead first")
  attr(:fields, :list, default: @fields, doc: "Up to four `%{label, value}`.")

  attr(:focus, :integer,
    default: 1,
    doc: "Index of the field in focus. A negative index focuses none."
  )

  attr(:note, :string, default: "Needs approval above €5,000")
  attr(:submit, :string, default: "Send for approval")
  attr(:label, :string, default: "A purchase request form with three fields and a send button")
  frame_attrs()

  @spec apps_form(map()) :: Phoenix.LiveView.Rendered.t()
  def apps_form(assigns) do
    rows = assigns.fields |> Enum.take(4) |> Enum.with_index()
    note_y = 76 + length(rows) * 54 + 6

    assigns =
      assigns
      |> assign(:rows, rows)
      |> assign(:note_y, note_y)
      |> assign(:height, note_y + 30 + 34 + 20)

    ~H"""
    <.frame name="apps/form" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 320 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="319" height={@height - 1} rx="12" />
          <text class="ehv-t-strong ehv-a-fade ehv-i-1" x="20" y="38" font-size="15">{@title}</text>
          <text class="ehv-t-3 ehv-a-fade ehv-i-1" x="20" y="57" font-size="11.5">{@subtitle}</text>
          <g :for={{field, index} <- @rows} class={"ehv-a-rise ehv-i-#{index + 2}"}>
            <text class="ehv-mono" x="20" y={84 + index * 54} font-size="8">
              {String.upcase(field.label)}
            </text>
            <rect
              class={["ehv-well", index == @focus && "ehv-apps-form__input--focus"]}
              x="20.5"
              y={90.5 + index * 54}
              width="279"
              height="31"
              rx="7"
            />
            <text x="32" y={110.5 + index * 54} font-size="12.5">{field.value}</text>
            <line
              :if={index == @focus}
              class="ehv-line-lime ehv-apps-form__caret"
              x1={36 + String.length(field.value) * 6.5}
              x2={36 + String.length(field.value) * 6.5}
              y1={99 + index * 54}
              y2={113 + index * 54}
            />
          </g>
          <g class="ehv-a-fade ehv-i-6">
            <rect class="ehv-fill-lime" x="20" y={@note_y} width="16" height="16" rx="4.5" />
            <path
              class="ehv-apps-form__tick ehv-round ehv-a-draw ehv-i-8"
              d={"M24 #{@note_y + 8} l2.8 2.8 l5 -5.6"}
              pathLength="1"
            />
            <text class="ehv-t-2" x="45" y={@note_y + 12} font-size="11.5">{@note}</text>
          </g>
          <g class="ehv-a-rise ehv-i-7">
            <rect
              class="ehv-fill-lime ehv-apps-form__submit"
              x="20"
              y={@note_y + 30}
              width="280"
              height="34"
              rx="7"
            />
            <text
              class="ehv-t-strong ehv-t-on-lime"
              x="160"
              y={@note_y + 51.5}
              font-size="12.5"
              text-anchor="middle"
            >
              {@submit}
            </text>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/apps/form.css
form.css
/* apps/form */
.ehv-apps-form { --ehv-max-w: 340px; }

.ehv .ehv-apps-form__input--focus { stroke: var(--ehv-lime); }
.ehv-apps-form__tick { fill: none; stroke: var(--ehv-on-lime); stroke-width: 1.5; }

@keyframes ehv-apps-form-blink { 50% { opacity: 0; } }
.ehv--animated .ehv-apps-form__caret { animation: ehv-apps-form-blink 1.1s steps(1) infinite; }

.ehv--hover .ehv-apps-form__submit { transition: fill 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-apps-form__submit { fill: var(--ehv-ink); }