defmodule PhiaUi.Components.Editor.ContentBlocks do
@moduledoc """
Editor Content Block Components — 14 rich content blocks for embedding
interactive and media elements inside a rich text editor.
These components cover collapsible toggle lists, tabbed content, video/audio
players, bookmark link previews, CTA buttons, progress indicators, labeled
dividers, math blocks, social embeds, PDF viewers, and map embeds.
All components render meaningful HTML without JavaScript (progressive
enhancement) and support dark mode via Tailwind v4 design tokens.
## Components
### Interactive Blocks
- `toggle_list/1` — collapsible list container
- `toggle_list_item/1` — individual toggleable details/summary item
- `tab_block/1` — tabbed content sections with tab buttons
- `tab_block_panel/1` — individual tab panel (shows/hides by active tab)
### Media Blocks
- `video_block/1` — HTML5 video player with poster support
- `audio_block/1` — audio player with labeled header
- `bookmark_card/1` — URL preview card with favicon, image, and domain
- `social_embed_block/1` — rich social card with provider icon
### Content Widgets
- `button_block/1` — CTA button inside editor content
- `progress_bar_block/1` — inline progress indicator with label
- `divider_with_label/1` — HR with centered text label
### Specialized Blocks
- `math_block/1` — LaTeX block with input and preview area
- `pdf_viewer_block/1` — iframe PDF viewer with download link
- `map_embed_block/1` — iframe map embed with aspect-video container
"""
use Phoenix.Component
import PhiaUi.ClassMerger, only: [cn: 1]
# ============================================================================
# 1. toggle_list/1
# ============================================================================
attr :id, :string, required: true, doc: "Unique identifier for the toggle list."
attr :class, :string, default: nil, doc: "Additional CSS classes."
slot :inner_block, required: true, doc: "Toggle list items."
@doc """
Renders a collapsible list container.
Wraps toggle list items in a semantic `role="list"` container with
`data-type="toggleList"` for editor integration.
## Example
<.toggle_list id="my-toggles">
<.toggle_list_item id="item-1" label="Section A" open={true}>
Content for section A.
"""
def toggle_list(assigns) do
~H"""
{render_slot(@inner_block)}
"""
end
# ============================================================================
# 2. toggle_list_item/1
# ============================================================================
attr :id, :string, required: true, doc: "Unique identifier for the toggle item."
attr :label, :string, required: true, doc: "Summary label text."
attr :open, :boolean, default: false, doc: "Whether the item is initially expanded."
attr :class, :string, default: nil, doc: "Additional CSS classes."
slot :inner_block, required: true, doc: "Collapsible content."
@doc """
Renders an individual toggleable item using `/`.
The chevron icon rotates when the item is open. Uses native HTML disclosure
so it works without JavaScript.
## Example
<.toggle_list_item id="faq-1" label="What is PhiaUI?" open={true}>
PhiaUI is a component library for Phoenix LiveView.
"""
def toggle_list_item(assigns) do
~H"""
{@label}
{render_slot(@inner_block)}
"""
end
# ============================================================================
# 3. tab_block/1
# ============================================================================
attr :id, :string, required: true, doc: "Unique identifier for the tab block."
attr :tabs, :list,
default: [],
doc: "List of maps with `:label` and `:id` keys representing each tab."
attr :active_tab, :string, default: nil, doc: "The ID of the currently active tab."
attr :class, :string, default: nil, doc: "Additional CSS classes."
slot :inner_block, required: true, doc: "Tab panels (use tab_block_panel components)."
@doc """
Renders tabbed content sections with a tab button bar and bottom border indicator.
The `active_tab` defaults to the first tab ID when not provided. Each tab button
dispatches a `phx-click` event for server-side tab switching.
## Example
<.tab_block
id="code-tabs"
tabs={[%{id: "elixir", label: "Elixir"}, %{id: "js", label: "JavaScript"}]}
active_tab="elixir"
>
<.tab_block_panel id="panel-elixir" tab_id="elixir" active_tab="elixir">
Elixir code here.
<.tab_block_panel id="panel-js" tab_id="js" active_tab="elixir">
JS code here.
"""
def tab_block(assigns) do
active =
assigns.active_tab ||
case assigns.tabs do
[first | _] -> Map.get(first, :id, "")
_ -> ""
end
assigns = assign(assigns, :resolved_active, active)
~H"""
{render_slot(@inner_block)}
"""
end
# ============================================================================
# 4. tab_block_panel/1
# ============================================================================
attr :id, :string, required: true, doc: "Unique identifier for the tab panel."
attr :tab_id, :string, required: true, doc: "The tab ID this panel belongs to."
attr :active_tab, :string, default: nil, doc: "The currently active tab ID."
attr :class, :string, default: nil, doc: "Additional CSS classes."
slot :inner_block, required: true, doc: "Panel content."
@doc """
Renders an individual tab panel that shows or hides based on matching
`tab_id` to `active_tab`.
## Example
<.tab_block_panel id="panel-1" tab_id="overview" active_tab="overview">
Overview content here.
"""
def tab_block_panel(assigns) do
~H"""
{render_slot(@inner_block)}
"""
end
# ============================================================================
# 5. video_block/1
# ============================================================================
attr :id, :string, required: true, doc: "Unique identifier for the video block."
attr :src, :string, required: true, doc: "URL of the video source."
attr :poster, :string, default: nil, doc: "URL of the poster/thumbnail image."
attr :controls, :boolean, default: true, doc: "Whether to show native video controls."
attr :autoplay, :boolean, default: false, doc: "Whether to autoplay the video."
attr :class, :string, default: nil, doc: "Additional CSS classes."
@doc """
Renders an HTML5 video player in a rounded container.
Supports poster image, native controls, and autoplay (muted when autoplay
is enabled for browser compatibility).
## Example
<.video_block id="demo-vid" src="/videos/demo.mp4" poster="/images/poster.jpg" />
"""
def video_block(assigns) do
~H"""
"""
end
# ============================================================================
# 6. audio_block/1
# ============================================================================
attr :id, :string, required: true, doc: "Unique identifier for the audio block."
attr :src, :string, required: true, doc: "URL of the audio source."
attr :title, :string, default: nil, doc: "Optional title label for the audio."
attr :class, :string, default: nil, doc: "Additional CSS classes."
@doc """
Renders an audio player with a labeled header.
Uses native `