sections/components.ex
Components
A showcase grid of interface building blocks: a button, an input, a toggle, a badge, a checkbox and a select.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_components animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Components" |
|
heading |
string | "One kit for every internal tool" |
|
items |
list | [%{text: "Approve", kind: "button"}, %{text: "PO-2291", kind: "input"}, ...] |
Up to six `%{kind, text}`, three to a row. Kind is `button`, `input`, `toggle`, `badge`, `checkbox` or `select`. Text is up to about 11 characters, 7 for a toggle. |
accent |
integer | 0 |
Index of the building block drawn in lime. A negative index marks none. |
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/sections/components.ex
defmodule EHVisuals.Sections.Components do
@moduledoc """
A showcase grid of interface building blocks: a button, an input, a toggle,
a badge, a checkbox and a select, one of them in lime.
"""
use EHVisuals.Composition,
name: "sections/components",
title: "Components",
description:
"A showcase grid of interface building blocks: a button, an input, a toggle, a badge, a checkbox and a select."
@items [
%{kind: "button", text: "Approve"},
%{kind: "input", text: "PO-2291"},
%{kind: "toggle", text: "Sync"},
%{kind: "badge", text: "Posted"},
%{kind: "checkbox", text: "Matched"},
%{kind: "select", text: "Finance"}
]
@kinds ~w(button input toggle badge checkbox select)
attr(:eyebrow, :string, default: "Components")
attr(:heading, :string, default: "One kit for every internal tool")
attr(:items, :list,
default: @items,
doc:
"Up to six `%{kind, text}`, three to a row. Kind is `button`, `input`, `toggle`, `badge`, `checkbox` or `select`. Text is up to about 11 characters, 7 for a toggle."
)
attr(:accent, :integer,
default: 0,
doc: "Index of the building block drawn in lime. A negative index marks none."
)
attr(:label, :string,
default: "A grid of six interface building blocks with the button drawn in lime"
)
frame_attrs()
@spec sections_components(map()) :: Phoenix.LiveView.Rendered.t()
def sections_components(assigns) do
tiles =
assigns.items
|> Enum.take(6)
|> Enum.with_index()
|> Enum.map(fn {item, index} ->
%{
kind: if(item.kind in @kinds, do: item.kind, else: "button"),
text: item.text,
index: index,
x: 24 + rem(index, 3) * 120,
y: 76 + div(index, 3) * 88
}
end)
rows = max(div(length(tiles) + 2, 3), 1)
assigns =
assigns
|> assign(:tiles, tiles)
|> assign(:height, 76 + rows * 80 + (rows - 1) * 8 + 24)
~H"""
<.frame name="sections/components" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 400 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="399" height={@height - 1} rx="12" />
<text class="ehv-mono ehv-a-fade ehv-i-1" x="24" y="34" font-size="8.5">
{String.upcase(@eyebrow)}
</text>
<text class="ehv-t-strong ehv-a-fade ehv-i-2" x="24" y="58" font-size="16">{@heading}</text>
<g
:for={tile <- @tiles}
class={"ehv-sections-components__item ehv-a-rise ehv-i-#{tile.index + 3}"}
>
<rect
class="ehv-card-2 ehv-sections-components__tile"
x={tile.x + 0.5}
y={tile.y + 0.5}
width="111"
height="79"
rx="10"
/>
<text class="ehv-mono" x={tile.x + 10} y={tile.y + 18} font-size="7.5">
{String.upcase(tile.kind)}
</text>
<.control
kind={tile.kind}
text={tile.text}
x={tile.x}
cx={tile.x + 56}
cy={tile.y + 49}
on={tile.index == @accent}
/>
</g>
</g>
</svg>
</.frame>
"""
end
attr(:kind, :string, required: true)
attr(:text, :string, required: true)
attr(:x, :any, required: true)
attr(:cx, :any, required: true)
attr(:cy, :any, required: true)
attr(:on, :boolean, required: true)
# One building block, centred on {cx, cy} inside a tile that starts at x.
defp control(%{kind: "button"} = assigns) do
assigns = assign(assigns, :w, clamp(28 + String.length(assigns.text) * 5.6, 48, 92))
~H"""
<rect
class={if @on, do: "ehv-fill-lime", else: "ehv-card-3"}
x={@cx - @w / 2}
y={@cy - 13}
width={@w}
height="26"
rx="6"
/>
<text
class={["ehv-t-strong", @on && "ehv-t-on-lime"]}
x={@cx}
y={@cy + 4}
font-size="10.5"
text-anchor="middle"
>
{@text}
</text>
"""
end
defp control(%{kind: "input"} = assigns) do
assigns =
assign(assigns, :caret, Float.round(assigns.x + 22 + String.length(assigns.text) * 5.9, 1))
~H"""
<rect
class={["ehv-well", @on && "ehv-sections-components__field--on"]}
x={@x + 10.5}
y={@cy - 12.5}
width="91"
height="25"
rx="6"
/>
<text x={@x + 19} y={@cy + 4} font-size="10.5">{@text}</text>
<line
class={[
"ehv-sections-components__caret",
if(@on, do: "ehv-line-lime", else: "ehv-line-ink")
]}
x1={@caret}
x2={@caret}
y1={@cy - 6}
y2={@cy + 6}
/>
"""
end
defp control(%{kind: "toggle"} = assigns) do
assigns =
assign(assigns, :left, Float.round(assigns.cx - (47 + text_w(assigns.text)) / 2, 1))
~H"""
<rect
class={if @on, do: "ehv-fill-lime", else: "ehv-fill-strong"}
x={@left}
y={@cy - 11}
width="40"
height="22"
rx="11"
/>
<circle
class={
if @on,
do: "ehv-sections-components__knob ehv-sections-components__knob--on",
else: "ehv-sections-components__knob ehv-fill-ink"
}
cx={@left + 29}
cy={@cy}
r="8"
/>
<text class="ehv-t-2" x={@left + 47} y={@cy + 4} font-size="10.5">{@text}</text>
"""
end
defp control(%{kind: "badge"} = assigns) do
w = Float.round(27 + String.length(assigns.text) * 5.5, 1)
assigns = assigns |> assign(:w, w) |> assign(:left, Float.round(assigns.cx - w / 2, 1))
~H"""
<rect
class={if @on, do: "ehv-fill-lime-soft", else: "ehv-card-3"}
x={@left}
y={@cy - 10}
width={@w}
height="20"
rx="6"
/>
<circle class={if @on, do: "ehv-fill-lime", else: "ehv-fill-3"} cx={@left + 11} cy={@cy} r="2.5" />
<text
class={["ehv-mono", if(@on, do: "ehv-t-lime", else: "ehv-t-2")]}
x={@left + 19}
y={@cy + 3}
font-size="8"
>
{String.upcase(@text)}
</text>
"""
end
defp control(%{kind: "checkbox"} = assigns) do
assigns =
assign(assigns, :left, Float.round(assigns.cx - (24 + text_w(assigns.text)) / 2, 1))
~H"""
<rect
class={if @on, do: "ehv-fill-lime", else: "ehv-fill-2"}
x={@left}
y={@cy - 8}
width="16"
height="16"
rx="4.5"
/>
<path
class={[
"ehv-sections-components__tick ehv-round",
@on && "ehv-sections-components__tick--on"
]}
d={"M#{@left + 4} #{@cy} l2.8 2.8 l5 -5.6"}
/>
<text x={@left + 24} y={@cy + 4} font-size="10.5">{@text}</text>
"""
end
defp control(assigns) do
~H"""
<rect
class={["ehv-well", @on && "ehv-sections-components__field--on"]}
x={@x + 10.5}
y={@cy - 12.5}
width="91"
height="25"
rx="6"
/>
<text x={@x + 19} y={@cy + 4} font-size="10.5">{@text}</text>
<path
class={["ehv-round", if(@on, do: "ehv-line-lime", else: "ehv-line-ink")]}
d={"M#{@x + 87} #{@cy - 2} l3.5 4 l3.5 -4"}
/>
"""
end
defp text_w(text), do: String.length(text) * 5.5
defp clamp(value, low, high), do: Float.round(min(max(value, low), high) * 1.0, 1)
end
assets/css/sections/components.css
/* sections/components */
.ehv-sections-components { --ehv-max-w: 440px; }
.ehv .ehv-sections-components__field--on { stroke: var(--ehv-lime); }
.ehv-sections-components__knob { transform-box: fill-box; }
.ehv-sections-components__knob--on { fill: var(--ehv-on-lime); }
.ehv-sections-components__tick { fill: none; stroke: var(--ehv-canvas); stroke-width: 1.5; }
.ehv-sections-components__tick--on { stroke: var(--ehv-on-lime); }
/* The caret in the input is the one live thing. */
@keyframes ehv-sections-components-blink { 50% { opacity: 0; } }
.ehv--animated .ehv-sections-components__caret {
animation: ehv-sections-components-blink 1.1s steps(1) infinite;
}
.ehv--hover .ehv-sections-components__tile { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-components__item:hover .ehv-sections-components__tile {
stroke: var(--ehv-lime);
}