calendar/event-list.ex
Event list
An agenda of upcoming events with their times, grouped by day, with the next one marked.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.calendar_event_list animated />
</div>
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
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
/* 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); }