Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/heex/components.ex
defmodule Raxol.HEEx.Components do
@moduledoc """
Terminal-specific Phoenix function components for HEEx templates.
These components render terminal UI elements within HEEx templates,
providing a familiar Phoenix component syntax for terminal applications.
## Usage
defmodule MyApp do
use Raxol.HEEx
def render(assigns) do
~H\"\"\"
<.terminal_box padding={2} border="single">
<.terminal_text color="green" bold>Hello!</.terminal_text>
</.terminal_box>
\"\"\"
end
end
## Available Components
- `terminal_box/1` - Container with border and padding
- `terminal_text/1` - Styled text element
- `terminal_button/1` - Clickable button
- `terminal_row/1` - Horizontal layout container
- `terminal_column/1` - Vertical layout container
- `terminal_input/1` - Text input field
- `terminal_progress/1` - Progress bar
- `terminal_divider/1` - Horizontal divider line
"""
if Code.ensure_loaded?(Phoenix.Component) do
use Phoenix.Component
require Logger
alias Raxol.Style
alias Raxol.Style.Borders
# ============================================================================
# Container Components
# ============================================================================
@doc """
Renders a box container with optional border and padding.
## Attributes
- `padding` - Integer padding inside the box (default: 0)
- `border` - Border style: "single", "double", "rounded", "none" (default: "none")
- `color` - Text color (default: :default)
- `background` - Background color (default: :default)
- `width` - Fixed width (default: auto)
- `height` - Fixed height (default: auto)
## Slots
- `inner_block` - Required. The content to render inside the box.
## Examples
<.terminal_box border="single" padding={1}>
Content here
</.terminal_box>
<.terminal_box border="double" color="blue" background="white">
Styled box
</.terminal_box>
"""
attr :padding, :integer, default: 0
attr :border, :string, default: "none"
attr :color, :any, default: :default
attr :background, :any, default: :default
attr :width, :integer, default: nil
attr :height, :integer, default: nil
attr :id, :string, default: nil
slot(:inner_block, required: true)
def terminal_box(assigns) do
border_style = parse_border_style(assigns.border)
style =
Style.new(%{
border: border_style,
padding: assigns.padding,
color: parse_color(assigns.color),
background: parse_color(assigns.background),
width: assigns.width,
height: assigns.height
})
assigns = assign(assigns, :style, style)
~H"""
<div
data-terminal-component="box"
data-style={encode_style(@style)}
id={@id}
>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a horizontal row container for layout.
## Attributes
- `gap` - Space between children (default: 0)
- `justify` - Horizontal alignment: "start", "center", "end", "between" (default: "start")
- `align` - Vertical alignment: "start", "center", "end" (default: "start")
## Slots
- `inner_block` - Required. The content to render in the row.
## Examples
<.terminal_row gap={2} justify="between">
<.terminal_text>Left</.terminal_text>
<.terminal_text>Right</.terminal_text>
</.terminal_row>
"""
attr :gap, :integer, default: 0
attr :justify, :string, default: "start"
attr :align, :string, default: "start"
attr :id, :string, default: nil
slot(:inner_block, required: true)
def terminal_row(assigns) do
~H"""
<div
data-terminal-component="row"
data-gap={@gap}
data-justify={@justify}
data-align={@align}
id={@id}
>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a vertical column container for layout.
## Attributes
- `gap` - Space between children (default: 0)
- `align` - Horizontal alignment: "start", "center", "end" (default: "start")
## Slots
- `inner_block` - Required. The content to render in the column.
## Examples
<.terminal_column gap={1}>
<.terminal_text>Line 1</.terminal_text>
<.terminal_text>Line 2</.terminal_text>
</.terminal_column>
"""
attr :gap, :integer, default: 0
attr :align, :string, default: "start"
attr :id, :string, default: nil
slot(:inner_block, required: true)
def terminal_column(assigns) do
~H"""
<div
data-terminal-component="column"
data-gap={@gap}
data-align={@align}
id={@id}
>
<%= render_slot(@inner_block) %>
</div>
"""
end
# ============================================================================
# Text Components
# ============================================================================
@doc """
Renders styled text.
## Attributes
- `color` - Text color (default: :default)
- `background` - Background color (default: :default)
- `bold` - Bold text (default: false)
- `italic` - Italic text (default: false)
- `underline` - Underlined text (default: false)
- `strikethrough` - Strikethrough text (default: false)
- `dim` - Dimmed text (default: false)
## Slots
- `inner_block` - Required. The text content.
## Examples
<.terminal_text color="green" bold>Success!</.terminal_text>
<.terminal_text color="red" underline>Error</.terminal_text>
"""
attr :color, :any, default: :default
attr :background, :any, default: :default
attr :bold, :boolean, default: false
attr :italic, :boolean, default: false
attr :underline, :boolean, default: false
attr :strikethrough, :boolean, default: false
attr :dim, :boolean, default: false
attr :id, :string, default: nil
slot(:inner_block, required: true)
def terminal_text(assigns) do
decorations = build_text_decorations(assigns)
style =
Style.new(%{
color: parse_color(assigns.color),
background: parse_color(assigns.background),
text_decoration: decorations
})
assigns = assign(assigns, :style, style)
~H"""
<span
data-terminal-component="text"
data-style={encode_style(@style)}
id={@id}
>
<%= render_slot(@inner_block) %>
</span>
"""
end
# ============================================================================
# Interactive Components
# ============================================================================
@doc """
Renders a clickable button.
## Attributes
- `disabled` - Whether the button is disabled (default: false)
- `role` - Button style: "primary", "secondary", "danger", "success" (default: "primary")
- `phx-click` - Phoenix click event name
## Slots
- `inner_block` - Required. The button label.
## Examples
<.terminal_button phx-click="submit" role="primary">Submit</.terminal_button>
<.terminal_button disabled>Disabled</.terminal_button>
"""
attr :disabled, :boolean, default: false
attr :role, :string, default: "primary"
attr :id, :string, default: nil
attr :rest, :global, include: ~w(phx-click phx-value-id)
slot(:inner_block, required: true)
def terminal_button(assigns) do
~H"""
<button
data-terminal-component="button"
data-role={@role}
disabled={@disabled}
id={@id}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
end
@doc """
Renders a text input field.
## Attributes
- `value` - Current input value (default: "")
- `placeholder` - Placeholder text (default: "")
- `disabled` - Whether input is disabled (default: false)
- `type` - Input type: "text", "password" (default: "text")
- `phx-change` - Phoenix change event name
- `phx-blur` - Phoenix blur event name
## Examples
<.terminal_input value={@query} phx-change="search" placeholder="Search..." />
<.terminal_input type="password" phx-change="set_password" />
"""
attr :value, :string, default: ""
attr :placeholder, :string, default: ""
attr :disabled, :boolean, default: false
attr :type, :string, default: "text"
attr :id, :string, default: nil
attr :rest, :global, include: ~w(phx-change phx-blur phx-focus name)
def terminal_input(assigns) do
~H"""
<input
data-terminal-component="input"
type={@type}
value={@value}
placeholder={@placeholder}
disabled={@disabled}
id={@id}
{@rest}
/>
"""
end
# ============================================================================
# Display Components
# ============================================================================
@doc """
Renders a progress bar.
## Attributes
- `value` - Current progress (0-100, default: 0)
- `max` - Maximum value (default: 100)
- `width` - Bar width in characters (default: 20)
- `color` - Bar color (default: :green)
- `show_percentage` - Show percentage text (default: true)
- `filled_char` - Character for filled portion (default: "=")
- `empty_char` - Character for empty portion (default: "-")
## Examples
<.terminal_progress value={75} />
<.terminal_progress value={@progress} color="blue" width={30} />
"""
attr :value, :integer, default: 0
attr :max, :integer, default: 100
attr :width, :integer, default: 20
attr :color, :any, default: :green
attr :show_percentage, :boolean, default: true
attr :filled_char, :string, default: "="
attr :empty_char, :string, default: "-"
attr :id, :string, default: nil
def terminal_progress(assigns) do
percentage = min(100, max(0, round(assigns.value / assigns.max * 100)))
filled_count = round(assigns.width * percentage / 100)
empty_count = assigns.width - filled_count
filled = String.duplicate(assigns.filled_char, filled_count)
empty = String.duplicate(assigns.empty_char, empty_count)
assigns =
assigns
|> assign(:percentage, percentage)
|> assign(:filled, filled)
|> assign(:empty, empty)
~H"""
<div data-terminal-component="progress" id={@id}>
<span data-color={@color}>[<%= @filled %></span><span><%= @empty %>]</span>
<%= if @show_percentage do %>
<span><%= @percentage %>%</span>
<% end %>
</div>
"""
end
@doc """
Renders a horizontal divider line.
## Attributes
- `char` - Character to use for the divider (default: "-")
- `width` - Width of the divider (default: 40)
- `color` - Divider color (default: :default)
## Examples
<.terminal_divider />
<.terminal_divider char="=" width={60} color="blue" />
"""
attr :char, :string, default: "-"
attr :width, :integer, default: 40
attr :color, :any, default: :default
attr :id, :string, default: nil
def terminal_divider(assigns) do
line = String.duplicate(assigns.char, assigns.width)
assigns = assign(assigns, :line, line)
~H"""
<div data-terminal-component="divider" data-color={@color} id={@id}>
<%= @line %>
</div>
"""
end
# ============================================================================
# Private Helpers
# ============================================================================
defp parse_border_style(style) do
case style do
"single" -> Borders.new(%{style: :solid, width: 1})
"double" -> Borders.new(%{style: :double, width: 1})
"rounded" -> Borders.new(%{style: :rounded, width: 1})
"none" -> Borders.new(%{style: :none, width: 0})
_ -> Borders.new()
end
end
defp parse_color(color) when is_atom(color), do: color
defp parse_color(color) when is_binary(color),
do: String.to_existing_atom(color)
defp parse_color(_color), do: :default
defp build_text_decorations(assigns) do
[]
|> maybe_add_decoration(assigns.bold, :bold)
|> maybe_add_decoration(assigns.italic, :italic)
|> maybe_add_decoration(assigns.underline, :underline)
|> maybe_add_decoration(assigns.strikethrough, :strikethrough)
end
defp maybe_add_decoration(list, true, decoration), do: [decoration | list]
defp maybe_add_decoration(list, false, _decoration), do: list
defp encode_style(%Style{} = style) do
style
|> Map.from_struct()
|> Jason.encode!()
rescue
e ->
Logger.warning("Failed to encode style: #{Exception.message(e)}")
"{}"
end
defp encode_style(_), do: "{}"
else
@doc false
def __phoenix_component_not_available__ do
raise "Raxol.HEEx.Components requires the :phoenix_live_view dependency"
end
end
end