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
Types
@type action_bindings() :: %{optional(action_name()) => callback_id()}
@type action_name() :: String.t()
@type animation() :: %{ :id => String.t(), optional(:duration_ms) => pos_integer(), optional(:repeat) => boolean(), optional(:from) => number(), optional(:to) => number() }
@type background_pattern_options() :: [ color: gradient_color(), width: number(), interval: number() ]
@type border_radius_axis() ::
:all
| :top
| :right
| :bottom
| :left
| :top_left
| :top_right
| :bottom_left
| :bottom_right
@type box_shadow_options() :: [ color: gradient_color(), x: number(), y: number(), blur: number(), spread: number() ]
@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() }
@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() }
@type callback_id() :: String.t()
@type canvas_color() :: gradient_color()
@type canvas_command() :: canvas_rect_command() | canvas_rounded_rect_command() | canvas_pattern_rect_command()
@type canvas_node() :: %{ :kind => :canvas, :commands => [canvas_command()], optional(:id) => node_id(), optional(:style) => style(), optional(:events) => canvas_events() }
@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() }
@type color_token() :: :red | :green | :blue | :yellow | :black | :white | :gray
@type data_table_cell() :: %{ :column_id => node_id(), :children => [data_table_cell_node()], optional(:style) => style() }
@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() }
@type data_table_cell_node() :: text_node() | spacer_node() | data_table_cell_div_node()
@type data_table_column_width() :: :auto | {:px, number()} | {:fr, pos_integer()}
@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() }
@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() }
@type data_table_row() :: %{ :id => node_id(), :cells => [data_table_cell()], optional(:style) => style() }
@type data_table_sort() :: %{column_id: node_id(), direction: :asc | :desc}
@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() }
@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() }
@type gradient_color() :: color_token() | String.t()
@type icon_node() :: %{ :kind => :icon, :source => image_source(), optional(:id) => node_id(), optional(:style) => style() }
@type image_node() :: %{ :kind => :image, :source => image_source(), optional(:id) => node_id(), optional(:style) => style(), optional(:object_fit) => image_object_fit(), optional(:grayscale) => boolean() }
@type image_object_fit() :: :fill | :contain | :cover | :scale_down | :none
@type ir_node() :: text_node() | div_node() | scroll_node() | image_node() | icon_node() | button_node() | checkbox_node() | radio_node() | select_node() | uniform_list_node() | list_node() | data_table_node() | tree_node() | canvas_node() | popover_node() | spacer_node() | text_input_node() | textarea_node()
@type linear_gradient_stop() :: {gradient_color(), number()}
@type list_item() :: %{id: node_id(), children: [list_row_node()]}
@type list_node() :: %{ :kind => :list, :items => [list_item()], optional(:id) => node_id(), optional(:style) => style(), optional(:item_style) => style(), optional(:events) => list_events() }
@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() }
@type list_row_change_events() :: %{optional(:change) => String.t()}
@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() }
@type list_row_click_events() :: %{optional(:click) => String.t()}
@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() }
@type list_row_node() :: text_node() | spacer_node() | list_row_div_node() | list_row_button_node() | list_row_checkbox_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() }
@type node_id() :: String.t()
@type popover_anchor() :: :top_left | :top_right | :bottom_left | :bottom_right
@type popover_anchor_fit() ::
:switch_anchor | :snap_to_window | :snap_to_window_with_margin
@type popover_anchor_position_mode() :: :window | :local
@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() }
@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() }
@type scroll_axis() :: :x | :y | :both
@type scroll_node() :: %{ :kind => :scroll, :children => [ir_node()], optional(:id) => node_id(), optional(:axis) => scroll_axis(), optional(:style) => style() }
@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() }
@type shortcut_binding() :: {String.t(), action_name()}
@type style() :: [style_op()]
@type style_axis() :: :all | :x | :y | :top | :right | :bottom | :left
@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
@type style_op() :: style_flag() | 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()}
@type text_events() :: %{optional(:click) => String.t()}
@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() }
@type text_node() :: %{ :kind => :text, :content => String.t(), optional(:id) => node_id(), optional(:style) => style(), optional(:runs) => [text_run()], optional(:events) => text_events() }
@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() }
@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
@spec button( String.t(), keyword() ) :: button_node()
@spec canvas( [canvas_command()], keyword() ) :: canvas_node()
@spec checkbox(String.t(), boolean(), keyword()) :: checkbox_node()
@spec data_table([data_table_column()], [data_table_row()], keyword()) :: data_table_node()
@spec icon( image_source(), keyword() ) :: icon_node()
@spec image( image_source(), keyword() ) :: image_node()
@spec popover(String.t(), boolean(), [ir_node()], keyword()) :: popover_node()
@spec radio(String.t(), String.t(), boolean(), keyword()) :: radio_node()
@spec scroll( [ir_node()], keyword() ) :: scroll_node()
@spec select( [select_option()], keyword() ) :: select_node()
@spec spacer(keyword()) :: spacer_node()
@spec text_input( String.t(), keyword() ) :: text_input_node()
@spec textarea( String.t(), keyword() ) :: textarea_node()
@spec uniform_list( [uniform_list_item()], keyword() ) :: uniform_list_node()
@spec validate(ir_node() | Guppy.IR.Validated.t()) :: :ok | {:error, term()}
@spec validated(ir_node()) :: {:ok, Guppy.IR.Validated.t()} | {:error, term()}
@spec validated!(ir_node()) :: Guppy.IR.Validated.t()