apps/form.ex
Form
A short internal form with filled fields, one in focus, a rule and one lime action.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.apps_form animated />
</div>
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
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
/* 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); }