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/view/components.ex
defmodule Raxol.View.Components do
@moduledoc """
Basic view components for Raxol UI rendering.
This module provides fundamental components for building terminal UIs,
including text, boxes, rows, columns, and other layout elements.
"""
# Charts module may compile after this one
@compile {:no_warn_undefined, Raxol.UI.Charts.ViewBridge}
@compile {:no_warn_undefined, Raxol.UI.Charts.LineChart}
@compile {:no_warn_undefined, Raxol.UI.Charts.BarChart}
@compile {:no_warn_undefined, Raxol.UI.Charts.ScatterChart}
@compile {:no_warn_undefined, Raxol.UI.Charts.Heatmap}
@doc """
Creates a text component with the given content.
## Options
- `:content` - The text content to display
- `:style` - Optional style attributes
- `:id` - Optional component identifier
"""
@spec text(keyword() | map()) :: map()
def text(opts) when is_list(opts) do
text(Map.new(opts))
end
def text(%{content: content} = opts) do
%{
type: :text,
content: content,
fg: Map.get(opts, :fg),
bg: Map.get(opts, :bg),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id)
}
end
def text(_opts) do
%{
type: :text,
content: "",
style: %{}
}
end
@doc """
Creates a box component.
"""
@spec box(keyword() | map()) :: map()
def box(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :box,
style: Map.get(opts, :style, %{}),
children: Map.get(opts, :children, []),
id: Map.get(opts, :id)
}
end
@doc """
Creates a row layout component.
"""
@spec row(keyword() | map()) :: map()
def row(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :row,
style: Map.get(opts, :style, %{}),
children: Map.get(opts, :children, []),
id: Map.get(opts, :id),
gap: Map.get(opts, :gap, 0)
}
end
@doc """
Creates a column layout component.
"""
@spec column(keyword() | map()) :: map()
def column(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :column,
style: Map.get(opts, :style, %{}),
children: Map.get(opts, :children, []),
id: Map.get(opts, :id),
gap: Map.get(opts, :gap, 0)
}
end
@doc """
Creates a label component.
"""
@spec label(keyword() | map()) :: map()
def label(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :label,
attrs: [
content: Map.get(opts, :content, ""),
style: Map.get(opts, :style, %{})
],
id: Map.get(opts, :id)
}
end
@doc """
Creates a button component.
"""
@spec button(keyword() | map()) :: map()
def button(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :button,
content: Map.get(opts, :content, "Button"),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id),
on_click: Map.get(opts, :on_click)
}
end
@doc """
Creates an input field component.
"""
@spec input(keyword() | map()) :: map()
def input(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :input,
value: Map.get(opts, :value, ""),
placeholder: Map.get(opts, :placeholder, ""),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id),
on_change: Map.get(opts, :on_change)
}
end
@doc """
Creates a list component.
"""
@spec list(keyword() | map()) :: map()
def list(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :list,
items: Map.get(opts, :items, []),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id),
selected: Map.get(opts, :selected)
}
end
@doc """
Creates a spacer component.
"""
@spec spacer(keyword() | map()) :: map()
def spacer(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :spacer,
size: Map.get(opts, :size, 1),
direction: Map.get(opts, :direction, :vertical)
}
end
@doc """
Creates a divider component.
"""
@spec divider(keyword() | map()) :: map()
def divider(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :divider,
style: Map.get(opts, :style, %{}),
char: Map.get(opts, :char, "-")
}
end
@doc """
Creates an image component for inline terminal image display.
## Options
- `:src` - File path or raw binary image data (required)
- `:width` - Width in terminal cells (default: 20)
- `:height` - Height in terminal cells (default: 10)
- `:protocol` - Override protocol (:kitty, :iterm2, :sixel)
- `:preserve_aspect` - Preserve aspect ratio (default: true)
- `:style` - Optional style attributes
- `:id` - Optional component identifier
"""
@spec image(keyword() | map()) :: map()
def image(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :image,
src: Map.get(opts, :src),
width: Map.get(opts, :width, 20),
height: Map.get(opts, :height, 10),
protocol: Map.get(opts, :protocol),
preserve_aspect: Map.get(opts, :preserve_aspect, true),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id)
}
end
@doc """
Creates a progress bar component.
"""
@spec progress(keyword() | map()) :: map()
def progress(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :progress,
value: Map.get(opts, :value, 0),
max: Map.get(opts, :max, 100),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id)
}
end
@doc """
Creates a modal component.
"""
@spec modal(keyword() | map()) :: map()
def modal(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :modal,
title: Map.get(opts, :title, ""),
content: Map.get(opts, :content),
visible: Map.get(opts, :visible, false),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id)
}
end
@doc """
Creates a table component.
"""
@spec table(keyword() | map()) :: map()
def table(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :table,
headers: Map.get(opts, :headers, []),
rows: Map.get(opts, :rows, []),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id)
}
end
@doc """
Creates a select/dropdown component.
"""
@spec select(keyword() | map()) :: map()
def select(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :select,
options: Map.get(opts, :options, []),
selected: Map.get(opts, :selected),
placeholder: Map.get(opts, :placeholder, "Select..."),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id),
on_change: Map.get(opts, :on_change)
}
end
@doc """
Creates a checkbox component.
"""
@spec checkbox(keyword() | map()) :: map()
def checkbox(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :checkbox,
checked: Map.get(opts, :checked, false),
label: Map.get(opts, :label, ""),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id),
on_change: Map.get(opts, :on_change)
}
end
@doc """
Creates a radio button group component.
"""
@spec radio_group(keyword() | map()) :: map()
def radio_group(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :radio_group,
options: Map.get(opts, :options, []),
selected: Map.get(opts, :selected),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id),
on_change: Map.get(opts, :on_change)
}
end
@doc """
Creates a textarea component.
"""
@spec textarea(keyword() | map()) :: map()
def textarea(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :textarea,
value: Map.get(opts, :value, ""),
placeholder: Map.get(opts, :placeholder, ""),
rows: Map.get(opts, :rows, 5),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id),
on_change: Map.get(opts, :on_change)
}
end
@doc """
Creates a container component with optional scrolling.
"""
@spec container(keyword() | map()) :: map()
def container(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :container,
children: Map.get(opts, :children, []),
scrollable: Map.get(opts, :scrollable, false),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id)
}
end
@doc """
Creates a tabs component.
"""
@spec tabs(keyword() | map()) :: map()
def tabs(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
%{
type: :tabs,
tabs: Map.get(opts, :tabs, []),
active: Map.get(opts, :active, 0),
style: Map.get(opts, :style, %{}),
id: Map.get(opts, :id),
on_change: Map.get(opts, :on_change)
}
end
@doc """
Creates a split pane layout component.
## Options
* `:direction` - `:horizontal` or `:vertical` (default `:horizontal`)
* `:ratio` - Tuple for space distribution (default `{1, 1}`)
* `:min_size` - Minimum pane dimension (default `5`)
* `:id` - Optional identifier
* `:children` - Child elements (one per pane)
"""
@spec split_pane(keyword() | map()) :: map()
def split_pane(opts \\ []) do
Raxol.UI.Layout.SplitPane.new(opts)
end
@doc """
Helper to wrap content in a styled span.
"""
@spec span(binary(), keyword()) :: map()
def span(content, opts \\ []) do
text(Keyword.merge([content: content], opts))
end
# -- Chart Components --
# These render chart cell tuples and convert them to View DSL elements
# via ViewBridge, so they compose naturally in view/1 functions.
@doc """
Creates a braille line chart component.
## Options
- `:series` - List of `%{name: string, data: list, color: atom}` (required)
- `:width` - Chart width in terminal columns (default: 40)
- `:height` - Chart height in terminal rows (default: 10)
- `:show_axes` - Show Y-axis labels (default: false)
- `:show_legend` - Show series legend (default: false)
- `:min` / `:max` - Y-axis range (default: :auto)
- `:style` - Box style for the wrapper
- `:id` - Optional component identifier
"""
@spec line_chart(keyword() | map()) :: map()
def line_chart(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
render_chart(:line, opts)
end
@doc """
Creates a block-character bar chart component.
## Options
- `:series` - List of `%{name: string, data: list, color: atom}` (required)
- `:width` - Chart width in terminal columns (default: 40)
- `:height` - Chart height in terminal rows (default: 10)
- `:orientation` - `:vertical` or `:horizontal` (default: :vertical)
- `:show_axes` - Show axis labels (default: false)
- `:show_legend` - Show series legend (default: false)
- `:show_values` - Show value labels on bars (default: false)
- `:bar_gap` - Gap between bars within a group (default: 0)
- `:group_gap` - Gap between groups (default: 1)
- `:min` / `:max` - Value range (default: :auto)
- `:style` - Box style for the wrapper
- `:id` - Optional component identifier
"""
@spec bar_chart(keyword() | map()) :: map()
def bar_chart(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
render_chart(:bar, opts)
end
@doc """
Creates a braille scatter plot component.
## Options
- `:series` - List of `%{name: string, data: [{x, y}], color: atom}` (required)
- `:width` - Chart width in terminal columns (default: 40)
- `:height` - Chart height in terminal rows (default: 10)
- `:show_axes` - Show axis labels (default: false)
- `:show_legend` - Show series legend (default: false)
- `:x_range` / `:y_range` - Axis ranges as `{min, max}` or `:auto` (default: :auto)
- `:style` - Box style for the wrapper
- `:id` - Optional component identifier
"""
@spec scatter_chart(keyword() | map()) :: map()
def scatter_chart(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
render_chart(:scatter, opts)
end
@doc """
Creates a heatmap component.
## Options
- `:data` - 2D grid as `[[number]]` row-major (required)
- `:width` - Chart width in terminal columns (default: 40)
- `:height` - Chart height in terminal rows (default: 10)
- `:color_scale` - `:warm`, `:cool`, `:diverging`, or `fn/3` (default: :warm)
- `:show_values` - Show value labels in cells (default: false)
- `:min` / `:max` - Value range (default: :auto)
- `:style` - Box style for the wrapper
- `:id` - Optional component identifier
"""
@spec heatmap(keyword() | map()) :: map()
def heatmap(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
render_chart(:heatmap, opts)
end
@doc """
Creates a minimal sparkline (line chart with no axes or legend).
## Options
- `:data` - List of numbers (required)
- `:width` - Width in terminal columns (default: 20)
- `:height` - Height in terminal rows (default: 3)
- `:color` - Line color (default: :cyan)
- `:min` / `:max` - Y-axis range (default: :auto)
- `:style` - Box style for the wrapper
- `:id` - Optional component identifier
"""
@spec sparkline(keyword() | map()) :: map()
def sparkline(opts \\ []) do
opts = if is_list(opts), do: Map.new(opts), else: opts
data = Map.get(opts, :data, [])
color = Map.get(opts, :color, :cyan)
series = [%{name: "spark", data: data, color: color}]
chart_opts =
opts
|> Map.put(:series, series)
|> Map.put(:show_axes, false)
|> Map.put(:show_legend, false)
render_chart(:line, chart_opts)
end
# -- Chart rendering internals --
defp render_chart(chart_type, opts) do
w = Map.get(opts, :width, 40)
h = Map.get(opts, :height, 10)
style = Map.get(opts, :style, %{})
region = {0, 0, w, h}
chart_opts = chart_render_opts(chart_type, opts)
cells =
case chart_type do
:line ->
Raxol.UI.Charts.LineChart.render(
region,
Map.get(opts, :series, []),
chart_opts
)
:bar ->
Raxol.UI.Charts.BarChart.render(
region,
Map.get(opts, :series, []),
chart_opts
)
:scatter ->
Raxol.UI.Charts.ScatterChart.render(
region,
Map.get(opts, :series, []),
chart_opts
)
:heatmap ->
Raxol.UI.Charts.Heatmap.render(
region,
Map.get(opts, :data, []),
chart_opts
)
end
view = Raxol.UI.Charts.ViewBridge.cells_to_view(cells, style: style)
# Preserve chart type, series, and render opts so MCP ToolProvider
# can expose chart data as read-only tools via TreeWalker.
view =
view
|> Map.put(:type, chart_view_type(chart_type))
|> Map.put(:series, Map.get(opts, :series, Map.get(opts, :data, [])))
|> Map.put(:chart_opts, chart_opts)
case Map.get(opts, :id) do
nil -> view
id -> Map.put(view, :id, id)
end
end
defp chart_view_type(:line), do: :line_chart
defp chart_view_type(:bar), do: :bar_chart
defp chart_view_type(:scatter), do: :scatter_chart
defp chart_view_type(:heatmap), do: :heatmap
defp chart_render_opts(:line, opts) do
[
show_axes: Map.get(opts, :show_axes, false),
show_legend: Map.get(opts, :show_legend, false),
min: Map.get(opts, :min, :auto),
max: Map.get(opts, :max, :auto)
]
end
defp chart_render_opts(:bar, opts) do
[
orientation: Map.get(opts, :orientation, :vertical),
show_axes: Map.get(opts, :show_axes, false),
show_legend: Map.get(opts, :show_legend, false),
show_values: Map.get(opts, :show_values, false),
bar_gap: Map.get(opts, :bar_gap, 0),
group_gap: Map.get(opts, :group_gap, 1),
min: Map.get(opts, :min, :auto),
max: Map.get(opts, :max, :auto)
]
end
defp chart_render_opts(:scatter, opts) do
[
show_axes: Map.get(opts, :show_axes, false),
show_legend: Map.get(opts, :show_legend, false),
x_range: Map.get(opts, :x_range, :auto),
y_range: Map.get(opts, :y_range, :auto)
]
end
defp chart_render_opts(:heatmap, opts) do
[
color_scale: Map.get(opts, :color_scale, :warm),
show_values: Map.get(opts, :show_values, false),
min: Map.get(opts, :min, :auto),
max: Map.get(opts, :max, :auto)
]
end
end