Guppy.IR (guppy v0.2.0)

Copy Markdown View Source

Helpers and validation for Guppy's Elixir-owned UI IR.

Elixir processes build full-tree IR values, Guppy validates the supported schema, and the native runtime renders those trees through GPUI while routing events back to the owning process.

Style tokens are represented as ordered lists. That order is preserved across the bridge so later tokens can override earlier ones.

Summary

Functions

Builds a button with a text label; wire :events (e.g. %{click: callback}).

Builds a data-only canvas that paints ordered rect/rounded-rect/pattern commands.

Builds a checkbox; checked is Elixir-owned and change events report toggles.

Builds a semantic virtualized table from column specs and row data.

Builds a container node with flex/grid layout, styling, and event options.

Builds an icon node; source matches image/2 sources.

Builds an image node. source is a URL string or a {:path, path}, {:uri, uri}, or {:embedded, key} tuple.

Builds a virtualized list of variable-height rows with optional row controls.

Builds a popover trigger labeled label whose overlay children render while open is true. Anchor/fit/close behavior is controlled through opts.

Builds a radio button carrying value; group state lives in Elixir.

Builds a text node from a list of styled runs.

Builds a scrollable container; :axis may be :x, :y, or :both.

Builds a select control over options; :value and :open are Elixir-owned.

Builds a flexible spacer node.

Builds a text leaf node; opts may carry :id, :style, and :events.

Builds a single-line text input; value is Elixir-owned, edits arrive as change events.

Builds a multi-line text input with the same contract as text_input/2.

Builds a semantic tree with Elixir-owned selection and expansion state.

Builds a virtualized list of uniform-height items.

Returns the raw IR tree from a Guppy.IR.Validated wrapper (or the term unchanged).

Validates a full IR tree, returning :ok or {:error, reason}.

Validates an IR tree and wraps it in Guppy.IR.Validated so later render calls can skip the full-tree walk.

Like validated/1 but raises ArgumentError on invalid IR.

Types

action_bindings()

@type action_bindings() :: %{optional(action_name()) => callback_id()}

action_name()

@type action_name() :: String.t()

animation()

@type animation() :: %{
  :id => String.t(),
  optional(:duration_ms) => pos_integer(),
  optional(:repeat) => boolean(),
  optional(:from) => number(),
  optional(:to) => number()
}

background_pattern_options()

@type background_pattern_options() :: [
  color: gradient_color(),
  width: number(),
  interval: number()
]

border_radius_axis()

@type border_radius_axis() ::
  :all
  | :top
  | :right
  | :bottom
  | :left
  | :top_left
  | :top_right
  | :bottom_left
  | :bottom_right

box_shadow_options()

@type box_shadow_options() :: [
  color: gradient_color(),
  x: number(),
  y: number(),
  blur: number(),
  spread: number()
]

button_events()

@type button_events() :: %{
  optional(:click) => String.t(),
  optional(:hover) => String.t(),
  optional(:focus) => String.t(),
  optional(:blur) => String.t(),
  optional(:key_down) => String.t(),
  optional(:key_up) => String.t(),
  optional(:context_menu) => String.t(),
  optional(:mouse_down) => String.t(),
  optional(:mouse_up) => String.t(),
  optional(:mouse_move) => String.t()
}

button_node()

@type button_node() :: %{
  :kind => :button,
  :label => String.t(),
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:hover_style) => style(),
  optional(:focus_style) => style(),
  optional(:focus_visible_style) => style(),
  optional(:in_focus_style) => style(),
  optional(:active_style) => style(),
  optional(:disabled_style) => style(),
  optional(:animation) => animation(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:actions) => action_bindings(),
  optional(:shortcuts) => [shortcut_binding()],
  optional(:events) => button_events()
}

callback_id()

@type callback_id() :: String.t()

canvas_color()

@type canvas_color() :: gradient_color()

canvas_command()

canvas_events()

@type canvas_events() :: %{
  optional(:click) => String.t(),
  optional(:context_menu) => String.t()
}

canvas_node()

@type canvas_node() :: %{
  :kind => :canvas,
  :commands => [canvas_command()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:events) => canvas_events()
}

canvas_pattern_rect_command()

@type canvas_pattern_rect_command() :: %{
  :op => :pattern_rect,
  :x => number(),
  :y => number(),
  :width => number(),
  :height => number(),
  :color => canvas_color(),
  :line_width => number(),
  :interval => number(),
  optional(:radius) => number()
}

canvas_rect_command()

@type canvas_rect_command() :: %{
  :op => :rect,
  :x => number(),
  :y => number(),
  :width => number(),
  :height => number(),
  :fill => canvas_color(),
  optional(:radius) => number()
}

canvas_rounded_rect_command()

@type canvas_rounded_rect_command() :: %{
  op: :rounded_rect,
  x: number(),
  y: number(),
  width: number(),
  height: number(),
  fill: canvas_color(),
  radius: number()
}

checkbox_events()

@type checkbox_events() :: %{
  optional(:change) => String.t(),
  optional(:focus) => String.t(),
  optional(:blur) => String.t()
}

checkbox_node()

@type checkbox_node() :: %{
  :kind => :checkbox,
  :label => String.t(),
  :checked => boolean(),
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:hover_style) => style(),
  optional(:focus_style) => style(),
  optional(:focus_visible_style) => style(),
  optional(:in_focus_style) => style(),
  optional(:active_style) => style(),
  optional(:disabled_style) => style(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:events) => checkbox_events()
}

color_token()

@type color_token() :: :red | :green | :blue | :yellow | :black | :white | :gray

data_table_cell()

@type data_table_cell() :: %{
  :column_id => node_id(),
  :children => [data_table_cell_node()],
  optional(:style) => style()
}

data_table_cell_div_node()

@type data_table_cell_div_node() :: %{
  :kind => :div,
  :children => [data_table_cell_node()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:disabled) => boolean(),
  optional(:events) => list_row_click_events()
}

data_table_cell_node()

@type data_table_cell_node() ::
  text_node() | spacer_node() | data_table_cell_div_node()

data_table_column()

@type data_table_column() :: %{
  :id => node_id(),
  :label => String.t(),
  optional(:width) => data_table_column_width(),
  optional(:sortable) => boolean(),
  optional(:pinned) => boolean(),
  optional(:style) => style()
}

data_table_column_width()

@type data_table_column_width() :: :auto | {:px, number()} | {:fr, pos_integer()}

data_table_events()

@type data_table_events() :: %{
  optional(:row_click) => String.t(),
  optional(:cell_click) => String.t(),
  optional(:sort) => String.t(),
  optional(:column_reorder) => String.t(),
  optional(:column_resize) => String.t(),
  optional(:row_context_menu) => String.t(),
  optional(:cell_context_menu) => String.t()
}

data_table_node()

@type data_table_node() :: %{
  :kind => :data_table,
  :columns => [data_table_column()],
  :rows => [data_table_row()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:header_style) => style(),
  optional(:row_style) => style(),
  optional(:cell_style) => style(),
  optional(:selected_row_id) => node_id(),
  optional(:selected_cell) => {node_id(), node_id()},
  optional(:sort) => data_table_sort(),
  optional(:events) => data_table_events()
}

data_table_row()

@type data_table_row() :: %{
  :id => node_id(),
  :cells => [data_table_cell()],
  optional(:style) => style()
}

data_table_sort()

@type data_table_sort() :: %{column_id: node_id(), direction: :asc | :desc}

div_events()

@type div_events() :: %{
  optional(:click) => String.t(),
  optional(:hover) => String.t(),
  optional(:focus) => String.t(),
  optional(:blur) => String.t(),
  optional(:key_down) => String.t(),
  optional(:key_up) => String.t(),
  optional(:context_menu) => String.t(),
  optional(:drag_start) => String.t(),
  optional(:drag_move) => String.t(),
  optional(:drop) => String.t(),
  optional(:mouse_down) => String.t(),
  optional(:mouse_up) => String.t(),
  optional(:mouse_move) => String.t(),
  optional(:scroll_wheel) => String.t()
}

div_node()

@type div_node() :: %{
  :kind => :div,
  :children => [ir_node()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:hover_style) => style(),
  optional(:focus_style) => style(),
  optional(:focus_visible_style) => style(),
  optional(:in_focus_style) => style(),
  optional(:active_style) => style(),
  optional(:disabled_style) => style(),
  optional(:animation) => animation(),
  optional(:disabled) => boolean(),
  optional(:stack_priority) => non_neg_integer(),
  optional(:occlude) => boolean(),
  optional(:focusable) => boolean(),
  optional(:tab_stop) => boolean(),
  optional(:tab_index) => integer(),
  optional(:track_scroll) => boolean(),
  optional(:anchor_scroll) => boolean(),
  optional(:scroll_to) => boolean(),
  optional(:tooltip) => String.t(),
  optional(:actions) => action_bindings(),
  optional(:shortcuts) => [shortcut_binding()],
  optional(:events) => div_events()
}

gradient_color()

@type gradient_color() :: color_token() | String.t()

icon_node()

@type icon_node() :: %{
  :kind => :icon,
  :source => image_source(),
  optional(:id) => node_id(),
  optional(:style) => style()
}

image_node()

@type image_node() :: %{
  :kind => :image,
  :source => image_source(),
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:object_fit) => image_object_fit(),
  optional(:grayscale) => boolean()
}

image_object_fit()

@type image_object_fit() :: :fill | :contain | :cover | :scale_down | :none

image_source()

@type image_source() ::
  String.t()
  | {:uri, String.t()}
  | {:path, String.t()}
  | {:embedded, String.t()}

ir_node()

linear_gradient_stop()

@type linear_gradient_stop() :: {gradient_color(), number()}

list_events()

@type list_events() :: %{
  optional(:click) => String.t(),
  optional(:context_menu) => String.t()
}

list_item()

@type list_item() :: %{id: node_id(), children: [list_row_node()]}

list_node()

@type list_node() :: %{
  :kind => :list,
  :items => [list_item()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:item_style) => style(),
  optional(:events) => list_events()
}

list_row_button_node()

@type list_row_button_node() :: %{
  :kind => :button,
  :label => String.t(),
  :id => node_id(),
  optional(:style) => style(),
  optional(:hover_style) => style(),
  optional(:focus_style) => style(),
  optional(:focus_visible_style) => style(),
  optional(:in_focus_style) => style(),
  optional(:active_style) => style(),
  optional(:disabled_style) => style(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:events) => list_row_click_events()
}

list_row_change_events()

@type list_row_change_events() :: %{optional(:change) => String.t()}

list_row_checkbox_node()

@type list_row_checkbox_node() :: %{
  :kind => :checkbox,
  :label => String.t(),
  :checked => boolean(),
  :id => node_id(),
  optional(:style) => style(),
  optional(:hover_style) => style(),
  optional(:focus_style) => style(),
  optional(:focus_visible_style) => style(),
  optional(:in_focus_style) => style(),
  optional(:active_style) => style(),
  optional(:disabled_style) => style(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:events) => list_row_change_events()
}

list_row_click_events()

@type list_row_click_events() :: %{optional(:click) => String.t()}

list_row_div_node()

@type list_row_div_node() :: %{
  :kind => :div,
  :children => [list_row_node()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:disabled) => boolean(),
  optional(:events) => list_row_click_events()
}

list_row_node()

list_row_radio_node()

@type list_row_radio_node() :: %{
  :kind => :radio,
  :label => String.t(),
  :value => String.t(),
  :checked => boolean(),
  :id => node_id(),
  optional(:style) => style(),
  optional(:hover_style) => style(),
  optional(:focus_style) => style(),
  optional(:focus_visible_style) => style(),
  optional(:in_focus_style) => style(),
  optional(:active_style) => style(),
  optional(:disabled_style) => style(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:events) => list_row_change_events()
}

node_id()

@type node_id() :: String.t()

point()

@type point() :: {number(), number()}

popover_anchor()

@type popover_anchor() :: :top_left | :top_right | :bottom_left | :bottom_right

popover_anchor_fit()

@type popover_anchor_fit() ::
  :switch_anchor | :snap_to_window | :snap_to_window_with_margin

popover_anchor_position_mode()

@type popover_anchor_position_mode() :: :window | :local

popover_events()

@type popover_events() :: %{
  optional(:click) => String.t(),
  optional(:close) => String.t()
}

popover_node()

@type popover_node() :: %{
  :kind => :popover,
  :label => String.t(),
  :open => boolean(),
  :children => [ir_node()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:popover_style) => style(),
  optional(:anchor) => popover_anchor(),
  optional(:anchor_position) => point(),
  optional(:anchor_offset) => point(),
  optional(:anchor_position_mode) => popover_anchor_position_mode(),
  optional(:anchor_fit) => popover_anchor_fit(),
  optional(:snap_margin) => number(),
  optional(:close_on_click_outside) => boolean(),
  optional(:stack_priority) => non_neg_integer(),
  optional(:disabled) => boolean(),
  optional(:events) => popover_events()
}

radio_node()

@type radio_node() :: %{
  :kind => :radio,
  :label => String.t(),
  :value => String.t(),
  :checked => boolean(),
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:hover_style) => style(),
  optional(:focus_style) => style(),
  optional(:focus_visible_style) => style(),
  optional(:in_focus_style) => style(),
  optional(:active_style) => style(),
  optional(:disabled_style) => style(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:events) => checkbox_events()
}

scroll_axis()

@type scroll_axis() :: :x | :y | :both

scroll_node()

@type scroll_node() :: %{
  :kind => :scroll,
  :children => [ir_node()],
  optional(:id) => node_id(),
  optional(:axis) => scroll_axis(),
  optional(:style) => style()
}

select_events()

@type select_events() :: %{
  optional(:click) => String.t(),
  optional(:change) => String.t(),
  optional(:close) => String.t(),
  optional(:focus) => String.t(),
  optional(:blur) => String.t()
}

select_node()

@type select_node() :: %{
  :kind => :select,
  :options => [select_option()],
  optional(:id) => node_id(),
  optional(:value) => String.t(),
  optional(:open) => boolean(),
  optional(:placeholder) => String.t(),
  optional(:style) => style(),
  optional(:list_style) => style(),
  optional(:option_style) => style(),
  optional(:anchor) => popover_anchor(),
  optional(:anchor_offset) => point(),
  optional(:anchor_fit) => popover_anchor_fit(),
  optional(:snap_margin) => number(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:events) => select_events()
}

select_option()

@type select_option() :: %{
  :value => String.t(),
  :label => String.t(),
  optional(:disabled) => boolean()
}

shortcut_binding()

@type shortcut_binding() :: {String.t(), action_name()}

spacer_node()

@type spacer_node() :: %{
  :kind => :spacer,
  optional(:id) => node_id(),
  optional(:style) => style()
}

style()

@type style() :: [style_op()]

style_axis()

@type style_axis() :: :all | :x | :y | :top | :right | :bottom | :left

style_flag()

@type style_flag() ::
  :grid
  | :flex
  | :flex_col
  | :flex_row
  | :flex_wrap
  | :flex_nowrap
  | :flex_none
  | :flex_auto
  | :flex_grow
  | :flex_shrink
  | :flex_shrink_0
  | :flex_1
  | :col_span_full
  | :row_span_full
  | :size_full
  | :w_full
  | :h_full
  | :w_32
  | :w_64
  | :w_96
  | :h_32
  | :min_w_32
  | :min_h_0
  | :min_h_full
  | :max_w_64
  | :max_w_96
  | :max_w_full
  | :max_h_32
  | :max_h_96
  | :max_h_full
  | :gap_1
  | :gap_2
  | :gap_4
  | :p_1
  | :p_2
  | :p_4
  | :p_6
  | :p_8
  | :px_2
  | :py_2
  | :pt_2
  | :pr_2
  | :pb_2
  | :pl_2
  | :m_2
  | :mx_2
  | :my_2
  | :mt_2
  | :mr_2
  | :mb_2
  | :ml_2
  | :relative
  | :absolute
  | :top_0
  | :right_0
  | :bottom_0
  | :left_0
  | :inset_0
  | :top_1
  | :right_1
  | :top_2
  | :right_2
  | :bottom_2
  | :left_2
  | :text_left
  | :text_center
  | :text_right
  | :whitespace_normal
  | :whitespace_nowrap
  | :truncate
  | :text_ellipsis
  | :line_clamp_2
  | :line_clamp_3
  | :text_xs
  | :text_sm
  | :text_base
  | :text_lg
  | :text_xl
  | :text_2xl
  | :text_3xl
  | :leading_none
  | :leading_tight
  | :leading_snug
  | :leading_normal
  | :leading_relaxed
  | :leading_loose
  | :font_thin
  | :font_extralight
  | :font_light
  | :font_normal
  | :font_medium
  | :font_semibold
  | :font_bold
  | :font_extrabold
  | :font_black
  | :italic
  | :not_italic
  | :underline
  | :line_through
  | :items_start
  | :items_center
  | :items_end
  | :justify_start
  | :justify_center
  | :justify_end
  | :justify_between
  | :justify_around
  | :cursor_pointer
  | :rounded_sm
  | :rounded_md
  | :rounded_lg
  | :rounded_xl
  | :rounded_2xl
  | :rounded_full
  | :border_1
  | :border_2
  | :border_dashed
  | :border_t_1
  | :border_r_1
  | :border_b_1
  | :border_l_1
  | :shadow_sm
  | :shadow_md
  | :shadow_lg
  | :overflow_scroll
  | :overflow_x_scroll
  | :overflow_y_scroll
  | :overflow_hidden
  | :overflow_x_hidden
  | :overflow_y_hidden

style_length()

@type style_length() :: {:px, number()} | {:rem, number()} | {:fraction, number()}

style_op()

@type style_op() :: style_flag() | style_value()

style_value()

@type style_value() ::
  {:padding, style_axis(), style_length()}
  | {:margin, style_axis(), style_length() | :auto}
  | {:gap, :all | :x | :y, style_length()}
  | {:width, style_length() | :auto}
  | {:height, style_length() | :auto}
  | {:size, style_length() | :auto}
  | {:min_width, style_length() | :auto}
  | {:min_height, style_length() | :auto}
  | {:max_width, style_length() | :auto}
  | {:max_height, style_length() | :auto}
  | {:aspect_ratio, number()}
  | {:position, :relative | :absolute}
  | {:inset, :all | :top | :right | :bottom | :left, style_length() | :auto}
  | {:display, :block | :flex | :grid | :none}
  | {:visibility, :visible | :hidden}
  | {:overflow, :all | :x | :y, :visible | :clip | :hidden | :scroll}
  | {:allow_concurrent_scroll, boolean()}
  | {:restrict_scroll_to_axis, boolean()}
  | {:debug, boolean()}
  | {:debug_below, boolean()}
  | {:cursor, atom()}
  | {:border_width, style_axis(), {:px | :rem, number()}}
  | {:border_radius, border_radius_axis(), {:px | :rem, number()}}
  | {:border_style, :solid | :dashed}
  | {:shadow, :none | :"2xs" | :xs | :sm | :md | :lg | :xl | :"2xl"}
  | {:flex_direction, :column | :column_reverse | :row | :row_reverse}
  | {:flex_wrap, :wrap | :wrap_reverse | :nowrap}
  | {:flex_item, :one | :auto | :initial | :none | :grow | :shrink | :shrink_0}
  | {:flex_basis, style_length() | :auto}
  | {:flex_grow, number()}
  | {:flex_shrink, number()}
  | {:align_items, :start | :end | :center | :baseline | :stretch}
  | {:align_self, :start | :end | :center | :baseline | :stretch}
  | {:justify_content,
     :start | :end | :center | :between | :around | :evenly | :stretch}
  | {:align_content,
     :normal | :start | :end | :center | :between | :around | :evenly | :stretch}
  | {:bg, color_token()}
  | {:text_color, color_token()}
  | {:text_bg, color_token()}
  | {:font_size, :xs | :sm | :base | :lg | :xl | :"2xl" | :"3xl"}
  | {:text_size, {:px | :rem, number()}}
  | {:line_height, :none | :tight | :snug | :normal | :relaxed | :loose}
  | {:line_height_length, {:px | :rem | :fraction, number()}}
  | {:font_weight_value, number()}
  | {:font_family, String.t()}
  | {:font_fallbacks, [String.t()]}
  | {:font_features, [{String.t(), non_neg_integer()}]}
  | {:border_color, color_token()}
  | {:bg_hex, String.t()}
  | {:text_color_hex, String.t()}
  | {:text_bg_hex, String.t()}
  | {:border_color_hex, String.t()}
  | {:text_decoration_color, color_token()}
  | {:text_decoration_color_hex, String.t()}
  | {:text_decoration_style, :solid | :wavy}
  | {:text_decoration_thickness, number()}
  | {:strikethrough_color, color_token()}
  | {:strikethrough_color_hex, String.t()}
  | {:strikethrough_thickness, number()}
  | {:bg_linear_gradient,
     angle: number(), from: linear_gradient_stop(), to: linear_gradient_stop()}
  | {:bg_pattern_slash, background_pattern_options()}
  | {:box_shadow, [box_shadow_options()]}
  | {:opacity, number()}
  | {:line_clamp, pos_integer()}
  | {:grid_cols, pos_integer()}
  | {:grid_rows, pos_integer()}
  | {:col_start, integer() | :auto}
  | {:col_end, integer() | :auto}
  | {:row_start, integer() | :auto}
  | {:row_end, integer() | :auto}
  | {:col_span, pos_integer() | :full}
  | {:row_span, pos_integer() | :full}
  | {:w_px, number()}
  | {:w_rem, number()}
  | {:w_frac, number()}
  | {:h_px, number()}
  | {:h_rem, number()}
  | {:h_frac, number()}
  | {:scrollbar_width_px, number()}
  | {:scrollbar_width_rem, number()}

text_events()

@type text_events() :: %{optional(:click) => String.t()}

text_input_events()

@type text_input_events() :: %{
  optional(:change) => String.t(),
  optional(:focus) => String.t(),
  optional(:blur) => String.t(),
  optional(:context_menu) => String.t()
}

text_input_node()

@type text_input_node() :: %{
  :kind => :text_input,
  :value => String.t(),
  optional(:id) => node_id(),
  optional(:placeholder) => String.t(),
  optional(:style) => style(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:actions) => action_bindings(),
  optional(:shortcuts) => [shortcut_binding()],
  optional(:events) => text_input_events()
}

text_node()

@type text_node() :: %{
  :kind => :text,
  :content => String.t(),
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:runs) => [text_run()],
  optional(:events) => text_events()
}

text_run()

@type text_run() :: %{:text => String.t(), optional(:style) => style()}

textarea_node()

@type textarea_node() :: %{
  :kind => :textarea,
  :value => String.t(),
  optional(:id) => node_id(),
  optional(:placeholder) => String.t(),
  optional(:style) => style(),
  optional(:disabled) => boolean(),
  optional(:tab_index) => integer(),
  optional(:actions) => action_bindings(),
  optional(:shortcuts) => [shortcut_binding()],
  optional(:events) => text_input_events()
}

tree_events()

@type tree_events() :: %{
  optional(:select) => String.t(),
  optional(:toggle) => String.t(),
  optional(:context_menu) => String.t()
}

tree_item()

@type tree_item() :: %{
  :id => node_id(),
  :label => String.t(),
  optional(:expanded) => boolean(),
  optional(:children) => [tree_item()],
  optional(:style) => style()
}

tree_node()

@type tree_node() :: %{
  :kind => :tree,
  :nodes => [tree_item()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:row_style) => style(),
  optional(:selected_id) => node_id(),
  optional(:events) => tree_events()
}

uniform_list_item()

@type uniform_list_item() :: %{id: node_id(), label: String.t()}

uniform_list_node()

@type uniform_list_node() :: %{
  :kind => :uniform_list,
  :items => [uniform_list_item()],
  optional(:id) => node_id(),
  optional(:style) => style(),
  optional(:item_style) => style(),
  optional(:events) => list_events()
}

Functions

button(label, opts \\ [])

@spec button(
  String.t(),
  keyword()
) :: button_node()

Builds a button with a text label; wire :events (e.g. %{click: callback}).

canvas(commands, opts \\ [])

@spec canvas(
  [canvas_command()],
  keyword()
) :: canvas_node()

Builds a data-only canvas that paints ordered rect/rounded-rect/pattern commands.

checkbox(label, checked, opts \\ [])

@spec checkbox(String.t(), boolean(), keyword()) :: checkbox_node()

Builds a checkbox; checked is Elixir-owned and change events report toggles.

data_table(columns, rows, opts \\ [])

@spec data_table([data_table_column()], [data_table_row()], keyword()) ::
  data_table_node()

Builds a semantic virtualized table from column specs and row data.

Selection (:selected_row_id, :selected_cell) and :sort are Elixir-owned state echoed back through table events.

div(children, opts \\ [])

@spec div(
  [ir_node()],
  keyword()
) :: div_node()

Builds a container node with flex/grid layout, styling, and event options.

See the module type docs for the supported opts keys (:id, :style, state styles, :events, focus/scroll behavior flags, :actions, and :shortcuts).

icon(source, opts \\ [])

@spec icon(
  image_source(),
  keyword()
) :: icon_node()

Builds an icon node; source matches image/2 sources.

image(source, opts \\ [])

@spec image(
  image_source(),
  keyword()
) :: image_node()

Builds an image node. source is a URL string or a {:path, path}, {:uri, uri}, or {:embedded, key} tuple.

list(items, opts \\ [])

@spec list(
  [list_item()],
  keyword()
) :: list_node()

Builds a virtualized list of variable-height rows with optional row controls.

popover(label, open, children, opts \\ [])

@spec popover(String.t(), boolean(), [ir_node()], keyword()) :: popover_node()

Builds a popover trigger labeled label whose overlay children render while open is true. Anchor/fit/close behavior is controlled through opts.

radio(label, value, checked, opts \\ [])

@spec radio(String.t(), String.t(), boolean(), keyword()) :: radio_node()

Builds a radio button carrying value; group state lives in Elixir.

rich_text(runs, opts \\ [])

@spec rich_text(
  [text_run() | String.t() | {String.t(), style()}],
  keyword()
) :: text_node()

Builds a text node from a list of styled runs.

Runs may be plain strings, {text, style} tuples, or %{text: ..., style: ...} maps; the node's content is the concatenation of all run text.

scroll(children, opts \\ [])

@spec scroll(
  [ir_node()],
  keyword()
) :: scroll_node()

Builds a scrollable container; :axis may be :x, :y, or :both.

select(options, opts \\ [])

@spec select(
  [select_option()],
  keyword()
) :: select_node()

Builds a select control over options; :value and :open are Elixir-owned.

spacer(opts \\ [])

@spec spacer(keyword()) :: spacer_node()

Builds a flexible spacer node.

text(content, opts \\ [])

@spec text(
  String.t(),
  keyword()
) :: text_node()

Builds a text leaf node; opts may carry :id, :style, and :events.

text_input(value, opts \\ [])

@spec text_input(
  String.t(),
  keyword()
) :: text_input_node()

Builds a single-line text input; value is Elixir-owned, edits arrive as change events.

textarea(value, opts \\ [])

@spec textarea(
  String.t(),
  keyword()
) :: textarea_node()

Builds a multi-line text input with the same contract as text_input/2.

tree(nodes, opts \\ [])

@spec tree(
  [tree_item()],
  keyword()
) :: tree_node()

Builds a semantic tree with Elixir-owned selection and expansion state.

uniform_list(items, opts \\ [])

@spec uniform_list(
  [uniform_list_item()],
  keyword()
) :: uniform_list_node()

Builds a virtualized list of uniform-height items.

unwrap(ir)

Returns the raw IR tree from a Guppy.IR.Validated wrapper (or the term unchanged).

validate(ir)

@spec validate(ir_node() | Guppy.IR.Validated.t()) :: :ok | {:error, term()}

Validates a full IR tree, returning :ok or {:error, reason}.

validated(validated)

@spec validated(ir_node()) :: {:ok, Guppy.IR.Validated.t()} | {:error, term()}

Validates an IR tree and wraps it in Guppy.IR.Validated so later render calls can skip the full-tree walk.

validated!(ir)

@spec validated!(ir_node()) :: Guppy.IR.Validated.t()

Like validated/1 but raises ArgumentError on invalid IR.