sections/faq.ex
FAQ
A frequently-asked-questions section with five questions as rows, one of them open with its answer.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_faq animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "FAQ" |
|
heading |
string | "Questions we hear first" |
|
items |
list | [%{q: "How long does a rollout take?"}, %{a: "No. It sits on top of the ERP and writes back to" <> ..., q: "Does it replace our ERP?"}, %{...}, ...] |
Up to six `%{q, a}`. A question is up to about 48 characters. The answer of the open row wraps onto up to three lines; without an `a` it is drawn as two text lines. |
open |
integer | 1 |
Index of the open question. A negative index leaves all of them closed. |
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/faq.ex
defmodule EHVisuals.Sections.Faq do
@moduledoc """
A frequently-asked-questions section: questions as rows, one of them open
with its answer.
"""
use EHVisuals.Composition,
name: "sections/faq",
title: "FAQ",
description:
"A frequently-asked-questions section with five questions as rows, one of them open with its answer."
@items [
%{q: "How long does a rollout take?"},
%{
q: "Does it replace our ERP?",
a: "No. It sits on top of the ERP and writes back to it, so the ledger stays where it is."
},
%{q: "Who approves what an agent does?"},
%{q: "Can we start with one process?"},
%{q: "Where is our data stored?"}
]
attr(:eyebrow, :string, default: "FAQ")
attr(:heading, :string, default: "Questions we hear first")
attr(:items, :list,
default: @items,
doc:
"Up to six `%{q, a}`. A question is up to about 48 characters. The answer of the open row wraps onto up to three lines; without an `a` it is drawn as two text lines."
)
attr(:open, :integer,
default: 1,
doc: "Index of the open question. A negative index leaves all of them closed."
)
attr(:label, :string,
default: "A list of five frequently asked questions with the second one open"
)
frame_attrs()
@spec sections_faq(map()) :: Phoenix.LiveView.Rendered.t()
def sections_faq(assigns) do
{rows, bottom} =
assigns.items
|> Enum.take(6)
|> Enum.with_index()
|> Enum.map_reduce(74, fn {item, index}, y ->
open = index == assigns.open
answer = if open, do: answer(item), else: []
skeleton = open and answer == []
height = if open, do: 32 + max(length(answer), 2) * 15 + 10, else: 32
row = %{
q: item.q,
index: index,
y: y,
h: height,
open: open,
answer: Enum.with_index(answer),
skeleton: skeleton,
rule: index > 0 and not open and index - 1 != assigns.open
}
{row, y + height}
end)
assigns = assigns |> assign(:rows, rows) |> assign(:height, bottom + 16)
~H"""
<.frame name="sections/faq" 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-t-strong ehv-a-fade ehv-i-2" x="24" y="58" font-size="16">{@heading}</text>
<g :for={row <- @rows} class={"ehv-sections-faq__row ehv-a-rise ehv-i-#{row.index + 3}"}>
<rect
class={if row.open, do: "ehv-card-2", else: "ehv-sections-faq__hit"}
x="12.5"
y={row.y + 0.5}
width="375"
height={row.h - 1}
rx="8"
/>
<line
:if={row.rule}
class="ehv-line-faint"
x1="24"
x2="376"
y1={row.y + 0.5}
y2={row.y + 0.5}
/>
<text
class={[row.open && "ehv-t-strong", !row.open && "ehv-t-2"]}
x="24"
y={row.y + 20}
font-size="12"
>
{row.q}
</text>
<circle :if={row.open} class="ehv-fill-lime-soft" cx="364" cy={row.y + 16} r="9" />
<path
class={[
"ehv-sections-faq__icon ehv-round",
if(row.open, do: "ehv-line-lime", else: "ehv-line-ink")
]}
d={
if row.open,
do: "M360 #{row.y + 16} h8",
else: "M360 #{row.y + 16} h8 M364 #{row.y + 12} v8"
}
/>
<text
:for={{line, index} <- row.answer}
class={"ehv-t-2 ehv-a-fade ehv-i-#{min(row.index + 5 + index, 24)}"}
x="24"
y={row.y + 39 + index * 15}
font-size="11.5"
>
{line}
</text>
<rect
:for={{width, index} <- Enum.with_index(if(row.skeleton, do: [308, 196], else: []))}
class={"ehv-fill-soft ehv-a-grow-x ehv-i-#{min(row.index + 5 + index, 24)}"}
x="24"
y={row.y + 33 + index * 15}
width={width}
height="6"
rx="3"
/>
</g>
</g>
</svg>
</.frame>
"""
end
defp answer(item) do
case Map.get(item, :a) do
text when is_binary(text) and text != "" -> text |> wrap(54) |> Enum.take(3)
_none -> []
end
end
defp wrap(text, limit) do
text
|> String.split()
|> Enum.reduce([], fn
word, [] ->
[word]
word, [line | rest] ->
if String.length(line) + 1 + String.length(word) <= limit,
do: ["#{line} #{word}" | rest],
else: [word, line | rest]
end)
|> Enum.reverse()
end
end
assets/css/sections/faq.css
/* sections/faq */
.ehv-sections-faq { --ehv-max-w: 440px; }
/* A closed row has no surface, only something for the pointer to land on. */
.ehv-sections-faq__hit { fill: transparent; }
.ehv--hover .ehv-sections-faq__icon { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-faq__row:hover .ehv-sections-faq__icon { stroke: var(--ehv-lime); }