sections/comments.ex
Comments
A comments thread with avatars, names, comment lines, an indented reply and vote counts.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_comments animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Discussion" |
|
heading |
string | "Month-end close checklist" |
|
comments |
list | [%{name: "Marta Nowak", time: "2h ago", votes: 12}, %{name: "Jan Kowalski", ...}, ...] |
Up to five `%{name, time, votes}`. Add `reply: true` to indent a comment under the one before it. |
voted |
integer | 0 |
Index of the comment whose vote is marked in lime. A negative index marks none. |
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/sections/comments.ex
defmodule EHVisuals.Sections.Comments do
@moduledoc """
A comments thread: avatars, names, comment lines, a reply indented under
one comment, and vote counts.
"""
use EHVisuals.Composition,
name: "sections/comments",
title: "Comments",
description:
"A comments thread with avatars, names, comment lines, an indented reply and vote counts."
@comments [
%{name: "Marta Nowak", time: "2h ago", votes: 12},
%{name: "Jan Kowalski", time: "1h ago", votes: 4, reply: true},
%{name: "Ewa Lis", time: "40m ago", votes: 7},
%{name: "Piotr Zielinski", time: "12m ago", votes: 2}
]
# Widths of the two text lines of a comment, cycled by position.
@bodies [{236, 152}, {204, 92}, {244, 176}, {188, 64}]
attr(:eyebrow, :string, default: "Discussion")
attr(:heading, :string, default: "Month-end close checklist")
attr(:comments, :list,
default: @comments,
doc:
"Up to five `%{name, time, votes}`. Add `reply: true` to indent a comment under the one before it."
)
attr(:voted, :integer,
default: 0,
doc: "Index of the comment whose vote is marked in lime. A negative index marks none."
)
attr(:label, :string,
default: "A thread of four comments with vote counts, one of them a reply to the first"
)
frame_attrs()
@spec sections_comments(map()) :: Phoenix.LiveView.Rendered.t()
def sections_comments(assigns) do
rows =
assigns.comments
|> Enum.take(5)
|> Enum.with_index()
|> Enum.map(fn {comment, index} ->
reply = index > 0 and Map.get(comment, :reply, false)
indent = if reply, do: 34, else: 0
{first, second} = Enum.at(@bodies, rem(index, length(@bodies)))
%{
comment: comment,
index: index,
reply: reply,
y: 86 + index * 50,
avatar_x: 36 + indent,
text_x: 58 + indent,
widths: [min(first, 262 - indent), min(second, 262 - indent)]
}
end)
count = length(rows)
assigns =
assigns
|> assign(:rows, rows)
|> assign(:count, if(count == 1, do: "1 comment", else: "#{count} comments"))
|> assign(:height, 86 + max(count - 1, 0) * 50 + 35 + 22)
~H"""
<.frame name="sections/comments" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 400 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="399" height={@height - 1} rx="12" />
<text class="ehv-mono ehv-a-fade ehv-i-1" x="24" y="34" font-size="8.5">
{String.upcase(@eyebrow)}
</text>
<text class="ehv-mono ehv-a-fade ehv-i-1" x="376" y="34" font-size="8.5" text-anchor="end">
{String.upcase(@count)}
</text>
<text class="ehv-t-strong ehv-a-fade ehv-i-2" x="24" y="58" font-size="16">{@heading}</text>
<line class="ehv-line-faint" x1="24" x2="376" y1="72.5" y2="72.5" />
<g
:for={row <- @rows}
class={"ehv-sections-comments__row ehv-a-rise ehv-i-#{row.index + 3}"}
>
<path
:if={row.reply}
class={"ehv-line ehv-a-draw ehv-i-#{row.index + 4}"}
d={"M36 #{row.y - 22} V#{row.y + 6} Q36 #{row.y + 12} 42 #{row.y + 12} H54"}
pathLength="1"
/>
<circle class="ehv-card-3" cx={row.avatar_x} cy={row.y + 12} r="12" />
<text
class="ehv-mono ehv-t-2"
x={row.avatar_x}
y={row.y + 15}
font-size="8"
text-anchor="middle"
>
{initials(row.comment.name)}
</text>
<text x={row.text_x} y={row.y + 9} font-size="12">
<tspan>{row.comment.name}</tspan>
<tspan class="ehv-mono" dx="5" font-size="7.5">{String.upcase(row.comment.time)}</tspan>
</text>
<rect
:for={{width, line} <- Enum.with_index(row.widths)}
class={"ehv-fill-soft ehv-a-grow-x ehv-i-#{row.index + 4 + line}"}
x={row.text_x}
y={row.y + 18 + line * 11}
width={width}
height="6"
rx="3"
/>
<rect
class={
if row.index == @voted,
do: "ehv-fill-lime-soft",
else: "ehv-line-soft"
}
x="336.5"
y={row.y + 1.5}
width="39"
height="21"
rx="6"
/>
<path
class={[
"ehv-sections-comments__chevron ehv-round",
if(row.index == @voted, do: "ehv-line-lime", else: "ehv-line-ink")
]}
d={"M344 #{row.y + 14} l3.5 -4 l3.5 4"}
/>
<text
class={["ehv-mono", if(row.index == @voted, do: "ehv-t-lime", else: "ehv-t-2")]}
x="369"
y={row.y + 15}
font-size="8.5"
text-anchor="end"
>
{row.comment.votes}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
defp initials(name) do
name |> String.split() |> Enum.take(2) |> Enum.map_join(&String.first/1) |> String.upcase()
end
end
assets/css/sections/comments.css
/* sections/comments */
.ehv-sections-comments { --ehv-max-w: 440px; }
.ehv-sections-comments__chevron { transform-box: fill-box; }
.ehv--hover .ehv-sections-comments__chevron { transition: transform 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-comments__row:hover .ehv-sections-comments__chevron { transform: translateY(-2px); }