empowered.house ↗

connections/sync.ex

Sync

Two systems that hold the same record, with changes travelling both ways.

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

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

Props

Prop Type Default What it does
left map %{title: "Stock list", kind: "Spreadsheet"} `%{kind, title}` of the left card.
right map %{title: "Inventory", kind: "ERP"} `%{kind, title}` of the right card.
status string "In sync"
stamp string "Last change 09:41:07"
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/connections/sync.ex
sync.ex
defmodule EHVisuals.Connections.Sync do
  @moduledoc """
  Two systems that hold the same record and keep it in step.
  """

  use EHVisuals.Composition,
    name: "connections/sync",
    title: "Sync",
    description: "Two systems that hold the same record, with changes travelling both ways."

  attr(:left, :map,
    default: %{kind: "Spreadsheet", title: "Stock list"},
    doc: "`%{kind, title}` of the left card."
  )

  attr(:right, :map,
    default: %{kind: "ERP", title: "Inventory"},
    doc: "`%{kind, title}` of the right card."
  )

  attr(:status, :string, default: "In sync")
  attr(:stamp, :string, default: "Last change 09:41:07")
  attr(:label, :string, default: "A spreadsheet and an ERP keeping the same record in sync")
  frame_attrs()

  @spec connections_sync(map()) :: Phoenix.LiveView.Rendered.t()
  def connections_sync(assigns) do
    assigns =
      assigns
      |> assign(:cards, [{assigns.left, 10, 1}, {assigns.right, 270, 3}])
      |> assign(:pill_w, 26 + String.length(assigns.status) * 6.2)

    ~H"""
    <.frame name="connections/sync" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 420 160">
        <g class="ehv-h-lift">
          <g :for={{card, x, step} <- @cards} class={"ehv-a-rise ehv-i-#{step}"}>
            <rect class="ehv-card" x={x + 0.5} y="20.5" width="139" height="99" rx="10" />
            <text class="ehv-mono" x={x + 14} y="43" font-size="8">{String.upcase(card.kind)}</text>
            <text class="ehv-t-strong" x={x + 14} y="64" font-size="13.5">{card.title}</text>
            <rect class="ehv-fill-lime-soft" x={x + 8} y="76" width="124" height="16" rx="4" />
            <rect class="ehv-fill-lime" x={x + 14} y="81.5" width="70" height="5" rx="2.5" />
            <rect class="ehv-fill-soft" x={x + 14} y="101" width="92" height="5" rx="2.5" />
          </g>
          <g class="ehv-a-fade ehv-i-4">
            <line class="ehv-line-ink" x1="158" x2="254" y1="48" y2="48" />
            <path class="ehv-fill-2" d="M254 43.5 l8 4.5 l-8 4.5 z" />
            <line class="ehv-line-ink" x1="166" x2="262" y1="92" y2="92" />
            <path class="ehv-fill-2" d="M166 87.5 l-8 4.5 l8 4.5 z" />
          </g>
          <g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-8">
            <circle class="ehv-fill-lime" cy="48" r="2.5">
              <animate attributeName="cx" from="158" to="258" dur="2s" repeatCount="indefinite" />
              <animate
                attributeName="opacity"
                values="0;1;1;0"
                keyTimes="0;0.2;0.8;1"
                dur="2s"
                repeatCount="indefinite"
              />
            </circle>
            <circle class="ehv-fill-lime" cy="92" r="2.5">
              <animate
                attributeName="cx"
                from="262"
                to="162"
                dur="2s"
                begin="1s"
                repeatCount="indefinite"
              />
              <animate
                attributeName="opacity"
                values="0;1;1;0"
                keyTimes="0;0.2;0.8;1"
                dur="2s"
                begin="1s"
                repeatCount="indefinite"
              />
            </circle>
          </g>
          <g class="ehv-a-pop ehv-i-6">
            <rect class="ehv-well" x={210 - @pill_w / 2} y="60" width={@pill_w} height="20" rx="6" />
            <circle class="ehv-fill-lime ehv-a-pulse" cx={210 - @pill_w / 2 + 11} cy="70" r="2.5" />
            <text
              class="ehv-mono ehv-t-lime"
              x={210 + @pill_w / 2 - 9}
              y="73.5"
              font-size="8.5"
              text-anchor="end"
            >
              {String.upcase(@status)}
            </text>
          </g>
          <text class="ehv-mono ehv-a-fade ehv-i-7" x="210" y="146" font-size="8" text-anchor="middle">
            {String.upcase(@stamp)}
          </text>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/connections/sync.css
sync.css
/* connections/sync */
.ehv-connections-sync { --ehv-max-w: 500px; --ehv-iso-scale: 0.82; }