keyboard/shortcut.ex
Shortcut
A keyboard shortcut as labelled keycaps with the action it triggers underneath.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.keyboard_shortcut animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
keys |
list | ["Cmd", "Shift", "K"] |
One to four key labels, each up to six letters. |
action |
string | "Open the command menu" |
Up to 40 characters. |
context |
any | "Anywhere in EHOS" |
The mono line under the action. `nil` hides it. |
highlight |
any | 2 |
Index of the key shown pressed and lime, from 0. `nil` leaves all keys up. |
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/keyboard/shortcut.ex
defmodule EHVisuals.Keyboard.Shortcut do
@moduledoc """
A keyboard shortcut: flat keycaps with a plus between them and the action
it triggers underneath.
"""
use EHVisuals.Composition,
name: "keyboard/shortcut",
title: "Shortcut",
description: "A keyboard shortcut as labelled keycaps with the action it triggers underneath."
attr(:keys, :list,
default: ~w(Cmd Shift K),
doc: "One to four key labels, each up to six letters."
)
attr(:action, :string, default: "Open the command menu", doc: "Up to 40 characters.")
attr(:context, :any,
default: "Anywhere in EHOS",
doc: "The mono line under the action. `nil` hides it."
)
attr(:highlight, :any,
default: 2,
doc: "Index of the key shown pressed and lime, from 0. `nil` leaves all keys up."
)
attr(:label, :string, default: "The shortcut Cmd, Shift and K, which opens the command menu")
frame_attrs()
@spec keyboard_shortcut(map()) :: Phoenix.LiveView.Rendered.t()
def keyboard_shortcut(assigns) do
labels = Enum.take(assigns.keys, 4)
widths = Enum.map(labels, &max(48, 28 + String.length(&1) * 9.4))
total = Enum.sum(widths) + max(length(labels) - 1, 0) * 30
width = Float.round(max(340, total + 56) / 1, 1)
{caps, _x} =
labels
|> Enum.zip(widths)
|> Enum.with_index()
|> Enum.map_reduce((width - total) / 2, fn {{text, w}, index}, x ->
on = index == assigns.highlight
cap = %{
text: text,
index: index,
on: on,
x: Float.round(x, 1),
w: Float.round(w / 1, 1),
y: if(on, do: 29, else: 26),
font: if(String.length(text) == 1, do: 17, else: 14.5),
plus: index > 0 && Float.round(x - 15, 1)
}
{cap, x + w + 30}
end)
assigns =
assigns
|> assign(:caps, caps)
|> assign(:width, width)
|> assign(:middle, Float.round(width / 2, 1))
|> assign(:height, if(assigns.context, do: 146, else: 130))
~H"""
<.frame name="keyboard/shortcut" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 #{@width} #{@height}"}>
<g class="ehv-h-lift">
<rect
class="ehv-card ehv-a-rise"
x="0.5"
y="0.5"
width={@width - 1}
height={@height - 1}
rx="12"
/>
<g :for={cap <- @caps}>
<path
:if={cap.plus}
class={"ehv-line-ink ehv-round ehv-a-fade ehv-i-#{cap.index * 2 + 1}"}
d={"M#{cap.plus - 4.5} 50 h9 M#{cap.plus} 45.5 v9"}
/>
<g class={"ehv-a-pop ehv-i-#{cap.index * 2 + 2}"}>
<rect
class={[
"ehv-keyboard-shortcut__base",
cap.on && "ehv-keyboard-shortcut__base--on"
]}
x={cap.x}
y="30"
width={cap.w}
height="44"
rx="9"
/>
<g class={["ehv-keyboard-shortcut__cap", cap.on && "ehv-keyboard-shortcut__cap--on"]}>
<rect
class={[
"ehv-keyboard-shortcut__face",
cap.on && "ehv-keyboard-shortcut__face--on"
]}
x={cap.x}
y={cap.y}
width={cap.w}
height="44"
rx="9"
/>
<text
class={["ehv-t-strong", cap.on && "ehv-t-on-lime"]}
x={cap.x + cap.w / 2}
y={cap.y + 22 + cap.font * 0.36}
font-size={cap.font}
text-anchor="middle"
>
{cap.text}
</text>
</g>
</g>
</g>
<g class="ehv-a-rise ehv-i-9">
<text class="ehv-t-strong" x={@middle} y="107" font-size="13.5" text-anchor="middle">
{@action}
</text>
<text
:if={@context}
class="ehv-mono"
x={@middle}
y="124"
font-size="8.5"
text-anchor="middle"
>
{String.upcase(@context)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/keyboard/shortcut.css
/* keyboard/shortcut */
.ehv-keyboard-shortcut { --ehv-max-w: 380px; --ehv-step: 110ms; }
/* A keycap is two rounded rectangles: the base, and the face a few units above it. */
.ehv-keyboard-shortcut__base { fill: var(--ehv-canvas); stroke: var(--ehv-border-strong); stroke-width: 1; }
.ehv-keyboard-shortcut__base--on { stroke: var(--ehv-lime); }
.ehv-keyboard-shortcut__face { fill: var(--ehv-surface-3); stroke: var(--ehv-border-strong); stroke-width: 1; }
.ehv-keyboard-shortcut__face--on { fill: var(--ehv-lime-fill); stroke: var(--ehv-lime); }
/* Hover presses the keys that are still up. */
.ehv--hover .ehv-keyboard-shortcut__cap { transition: transform 160ms var(--ehv-ease); }
.ehv--hover:hover .ehv-keyboard-shortcut__cap { transform: translateY(3px); }
.ehv--hover:hover .ehv-keyboard-shortcut__cap--on { transform: none; }