empowered.house ↗

calendar/event-list.ex

Event list

An agenda of upcoming events with their times, grouped by day, with the next one marked.

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

<div class="h-96 rounded-xl border border-border">
  <.calendar_event_list animated />
</div>
default
isometric
one day · custom copy
two days · hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
title string "Upcoming"
range string "12 – 14 Oct" The period, top right.
days list [%{events: [%{time: "09:00 – 09:30", title: "Month-end close kickoff", place: "Finance"}, ...], ...}, ...] Up to four `%{weekday, day, events}`, six events in all. An event is `%{time, title, place}`; keep titles under 30 characters (24 for the marked one).
highlight integer 0 Index of the marked event, counted across all days. A negative index marks none.
badge string "Next" The pill on the marked event.
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/calendar/event-list.ex
event-list.ex
defmodule EHVisuals.Calendar.EventList do
  @moduledoc """
  An agenda: the upcoming events with their times, grouped by day, and the
  next one marked.
  """

  use EHVisuals.Composition,
    name: "calendar/event-list",
    title: "Event list",
    description:
      "An agenda of upcoming events with their times, grouped by day, with the next one marked."

  @days [
    %{
      weekday: "Mon",
      day: "12",
      events: [
        %{time: "09:00 – 09:30", title: "Month-end close kickoff", place: "Finance"},
        %{time: "13:00 – 13:45", title: "Stock count review", place: "Warehouse"}
      ]
    },
    %{
      weekday: "Tue",
      day: "13",
      events: [
        %{time: "10:30 – 11:00", title: "Supplier call with Acme Tools", place: "Purchasing"}
      ]
    },
    %{
      weekday: "Wed",
      day: "14",
      events: [
        %{time: "15:00 – 16:00", title: "Approval round, Q4 budget", place: "Marta Nowak"}
      ]
    }
  ]

  @max_events 6
  @row_h 48

  attr(:title, :string, default: "Upcoming")
  attr(:range, :string, default: "12 – 14 Oct", doc: "The period, top right.")

  attr(:days, :list,
    default: @days,
    doc:
      "Up to four `%{weekday, day, events}`, six events in all. An event is `%{time, title, place}`; keep titles under 30 characters (24 for the marked one)."
  )

  attr(:highlight, :integer,
    default: 0,
    doc: "Index of the marked event, counted across all days. A negative index marks none."
  )

  attr(:badge, :string, default: "Next", doc: "The pill on the marked event.")

  attr(:label, :string,
    default: "An agenda of four events over three days with the first one marked as next"
  )

  frame_attrs()

  @spec calendar_event_list(map()) :: Phoenix.LiveView.Rendered.t()
  def calendar_event_list(assigns) do
    {groups, bottom} = layout(assigns.days)

    assigns =
      assigns
      |> assign(:groups, groups)
      |> assign(:height, bottom + 8)
      |> assign(:badge_w, Float.round(14 + String.length(assigns.badge) * 5.9, 1))

    ~H"""
    <.frame name="calendar/event-list" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 340 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="339" height={@height - 1} rx="12" />
          <g class="ehv-a-fade ehv-i-1">
            <text class="ehv-t-strong" x="20" y="35" font-size="14.5">{@title}</text>
            <text class="ehv-mono" x="320" y="34" font-size="8.5" text-anchor="end">
              {String.upcase(@range)}
            </text>
          </g>
          <g :for={group <- @groups}>
            <line class="ehv-line-faint" x1="20" x2="320" y1={group.y + 0.5} y2={group.y + 0.5} />
            <g class={"ehv-a-fade ehv-i-#{min(group.stagger, 24)}"}>
              <text class="ehv-mono" x="20" y={group.y + 27} font-size="8">
                {String.upcase(group.weekday)}
              </text>
              <text class="ehv-t-display" x="19" y={group.y + 49} font-size="21">{group.day}</text>
            </g>
            <g :for={event <- group.events} class={"ehv-a-rise ehv-i-#{min(event.index + 2, 24)}"}>
              <rect
                class={[
                  "ehv-calendar-event-list__row",
                  event.index == @highlight && "ehv-calendar-event-list__row--on"
                ]}
                x="72.5"
                y={event.y + 0.5}
                width="247"
                height="41"
                rx="8"
              />
              <rect
                class={if event.index == @highlight, do: "ehv-fill-lime", else: "ehv-fill-4"}
                x="82"
                y={event.y + 10}
                width="3"
                height="22"
                rx="1.5"
              />
              <text
                class={[event.index == @highlight && "ehv-t-strong"]}
                x="94"
                y={event.y + 18.5}
                font-size="12"
              >
                {event.title}
              </text>
              <text class="ehv-mono" x="94" y={event.y + 32} font-size="7.5">
                {String.upcase("#{event.time} · #{event.place}")}
              </text>
              <g :if={event.index == @highlight}>
                <rect
                  class="ehv-fill-lime-soft"
                  x={311 - @badge_w}
                  y={event.y + 7}
                  width={@badge_w}
                  height="16"
                  rx="5"
                />
                <text
                  class="ehv-mono ehv-t-lime"
                  x={311 - @badge_w / 2}
                  y={event.y + 18}
                  font-size="7.5"
                  text-anchor="middle"
                >
                  {String.upcase(@badge)}
                </text>
              </g>
            </g>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end

  # Stacks the day groups under the header and numbers the events across days.
  # Returns the groups and the y of the bottom edge of the last one.
  defp layout(days) do
    {groups, {_count, bottom}} =
      days
      |> Enum.take(4)
      |> Enum.map_reduce({0, 52}, fn day, {count, y} ->
        events =
          day
          |> Map.get(:events, [])
          |> Enum.take(@max_events - count)
          |> Enum.with_index(count)
          |> Enum.map(fn {event, index} ->
            %{
              index: index,
              y: y + 12 + (index - count) * @row_h,
              title: Map.get(event, :title, ""),
              time: Map.get(event, :time, ""),
              place: Map.get(event, :place, "")
            }
          end)

        height = max(length(events), 1) * @row_h + 18

        group = %{
          y: y,
          weekday: Map.get(day, :weekday, ""),
          day: Map.get(day, :day, ""),
          stagger: count + 2,
          events: events
        }

        {group, {count + length(events), y + height}}
      end)

    {groups, bottom}
  end
end
assets/css/calendar/event-list.css
event-list.css
/* calendar/event-list */
.ehv-calendar-event-list { --ehv-max-w: 370px; --ehv-step: 110ms; }

.ehv-calendar-event-list__row { fill: var(--ehv-surface-2); stroke: var(--ehv-border-faint); stroke-width: 1; }
.ehv-calendar-event-list__row--on { stroke: var(--ehv-border-strong); }

.ehv--hover .ehv-calendar-event-list__row { transition: stroke 200ms var(--ehv-ease), fill 200ms var(--ehv-ease); }
.ehv--hover .ehv-calendar-event-list__row:hover { fill: var(--ehv-surface-3); stroke: var(--ehv-border-strong); }