empowered.house ↗

keyboard/shortcut.ex

Shortcut

A keyboard shortcut as labelled keycaps with the action it triggers underneath.

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

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

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
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
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; }