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
raxol lib raxol playground property_editor.ex
Raw

lib/raxol/playground/property_editor.ex

defmodule Raxol.Playground.PropertyEditor do
@moduledoc """
Interactive property editor for the component playground.
Provides a user-friendly interface for editing component properties
with type validation and real-time preview updates.
"""
@doc """
Renders an interactive property editor for a component.
"""
def render_editor(component, current_props \\ %{})
def render_editor(nil, _current_props),
do: "No component selected. Use 'select <id>' to choose a component."
def render_editor(component, current_props),
do: render_component_editor(component, current_props)
@doc """
Validates and parses a property value based on its type.
"""
def parse_property_value(component, prop_name, value_string) do
prop_types = Map.get(component, :prop_types, %{})
expected_type = Map.get(prop_types, String.to_atom(prop_name))
case expected_type do
:string ->
{:ok, value_string}
:integer ->
case Integer.parse(value_string) do
{int, ""} -> {:ok, int}
_ -> {:error, "Must be an integer"}
end
:number ->
case Float.parse(value_string) do
{float, ""} ->
{:ok, float}
_ ->
case Integer.parse(value_string) do
{int, ""} -> {:ok, int}
_ -> {:error, "Must be a number"}
end
end
:boolean ->
case String.downcase(value_string) do
"true" -> {:ok, true}
"false" -> {:ok, false}
"1" -> {:ok, true}
"0" -> {:ok, false}
_ -> {:error, "Must be true or false"}
end
:atom ->
parse_atom_value(value_string)
:list ->
parse_list_value(value_string)
:map ->
parse_map_value(value_string)
_ ->
# Try to intelligently parse the value
intelligent_parse(value_string)
end
end
@doc """
Gets property suggestions and help text.
"""
def get_property_help(component, prop_name) do
prop_atom = String.to_atom(prop_name)
prop_types = Map.get(component, :prop_types, %{})
expected_type = Map.get(prop_types, prop_atom)
default_value = get_in(component, [:default_props, prop_atom])
help_text =
case expected_type do
:string ->
"Text value (use quotes for strings with spaces)"
:integer ->
"Whole number (e.g., 1, 42, -5)"
:number ->
"Number (e.g., 1, 3.14, -2.5)"
:boolean ->
"true or false"
:atom ->
"Atom value (e.g., :primary, :large, :center)"
:list ->
"List of values (e.g., [\"item1\", \"item2\", \"item3\"])"
:map ->
"Map/object (e.g., %{key: \"value\", color: :red})"
_ ->
"Any value"
end
default_text = format_default_value(default_value)
"#{help_text}#{default_text}"
end
@doc """
Gets all available properties for a component with their current values.
"""
def get_component_properties(component, current_props) do
all_props = Map.merge(component.default_props || %{}, current_props)
prop_types = Map.get(component, :prop_types, %{})
Enum.map_join(all_props, fn {key, value} ->
%{
name: key,
current_value: value,
type: Map.get(prop_types, key, :any),
help: get_property_help(component, to_string(key))
}
end)
|> Enum.sort_by(& &1.name)
end
@doc """
Renders property suggestions for tab completion.
"""
def get_property_suggestions(component, partial_name \\ "")
def get_property_suggestions(nil, _partial_name), do: []
def get_property_suggestions(component, partial_name) do
all_prop_names =
(component.default_props || %{})
|> Map.keys()
|> Enum.map(&to_string/1)
filter_property_names(all_prop_names, partial_name)
|> Enum.sort()
end
@doc """
Renders the property editor UI in terminal format.
"""
def render_terminal_editor(nil, _current_props) do
"""
#{IO.ANSI.yellow()}No component selected#{IO.ANSI.reset()}
Use 'select <component_id>' to choose a component first.
"""
end
def render_terminal_editor(component, current_props) do
properties = get_component_properties(component, current_props)
header = """
#{IO.ANSI.bright()}Property Editor - #{component.name}#{IO.ANSI.reset()}
#{String.duplicate("─", 50)}
"""
render_properties_content(header, properties)
end
# Private Functions
defp parse_atom_value(":" <> atom_name) do
{:ok, String.to_atom(atom_name)}
end
defp parse_atom_value(value_string) do
{:ok, String.to_atom(value_string)}
end
defp format_default_value(nil), do: ""
defp format_default_value(default_value),
do: " (default: #{inspect(default_value)})"
defp filter_property_names(names, ""), do: names
defp filter_property_names(names, partial) do
Enum.filter(names, &String.starts_with?(&1, partial))
end
defp render_properties_content(header, []) do
header <> "\n(No properties available)"
end
defp render_properties_content(header, properties) do
property_list = Enum.map_join(properties, "\n", &format_property_line/1)
footer = """
#{IO.ANSI.light_black()}Commands:#{IO.ANSI.reset()}
set <prop> <value> - Update property
reset <prop> - Reset to default
props - Show this editor
preview - Update preview
"""
header <> "\n" <> property_list <> footer
end
defp apply_parser_if_match(predicate, parser, trimmed) do
case predicate.(trimmed) do
true -> {:ok, parser.(trimmed)}
false -> nil
end
end
# Private Functions
defp render_component_editor(component, current_props) do
properties = get_component_properties(component, current_props)
"""
#{IO.ANSI.bright()}#{component.name} Properties:#{IO.ANSI.reset()}
#{String.duplicate("─", 40)}
#{render_property_list(properties)}
#{IO.ANSI.light_black()}Use 'set <prop> <value>' to update properties.#{IO.ANSI.reset()}
"""
end
defp render_property_list([]) do
" (No properties available)"
end
defp render_property_list(properties) do
properties
|> Enum.map_join("\n", &format_property_line/1)
end
defp format_property_line(prop) do
name_width = 15
padded_name = String.pad_trailing(to_string(prop.name), name_width)
type_color =
case prop.type do
:string -> IO.ANSI.green()
:integer -> IO.ANSI.blue()
:number -> IO.ANSI.blue()
:boolean -> IO.ANSI.yellow()
:atom -> IO.ANSI.magenta()
:list -> IO.ANSI.cyan()
:map -> IO.ANSI.cyan()
_ -> IO.ANSI.white()
end
value_display = format_value_display(prop.current_value)
type_display = "#{type_color}#{prop.type}#{IO.ANSI.reset()}"
" #{padded_name} #{value_display} #{IO.ANSI.light_black()}(#{type_display})#{IO.ANSI.reset()}"
end
defp format_value_display(value)
when is_binary(value) and byte_size(value) > 20 do
truncated = String.slice(value, 0, 17) <> "..."
"\"#{truncated}\""
end
defp format_value_display(value) when is_binary(value) do
"\"#{value}\""
end
defp format_value_display(value) when is_atom(value) do
":#{value}"
end
defp format_value_display(value) when is_list(value) and length(value) <= 3 do
inspect(value)
end
defp format_value_display(value) when is_list(value) do
"[#{length(value)} items]"
end
defp format_value_display(value)
when is_map(value) and map_size(value) <= 2 do
inspect(value)
end
defp format_value_display(value) when is_map(value) do
"%{#{map_size(value)} keys}"
end
defp format_value_display(value) do
inspect(value)
end
defp parse_list_value(value_string) do
# Simple list parsing for common cases
trimmed = String.trim(value_string)
parse_list_by_format(trimmed)
end
defp parse_list_by_format("[" <> _ = trimmed) when byte_size(trimmed) >= 2 do
case String.ends_with?(trimmed, "]") do
true -> parse_bracketed_list(trimmed)
false -> parse_unbracketed_list(trimmed)
end
end
defp parse_list_by_format(trimmed) when byte_size(trimmed) >= 2 do
parse_unbracketed_list(trimmed)
end
defp parse_list_by_format(trimmed), do: {:ok, [trimmed]}
defp parse_bracketed_list(trimmed) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
{result, _} = Code.eval_string(trimmed)
validate_list_result(result)
end) do
{:ok, result} -> result
{:error, _} -> parse_simple_list(trimmed)
end
end
defp validate_list_result(result) when is_list(result), do: {:ok, result}
defp validate_list_result(_), do: {:error, "Not a valid list"}
defp parse_unbracketed_list(trimmed) do
case String.contains?(trimmed, ",") do
true ->
items = String.split(trimmed, ",") |> Enum.map(&String.trim/1)
{:ok, items}
false ->
{:ok, [trimmed]}
end
end
defp parse_simple_list(list_string) do
# Remove brackets and split by comma
inner = String.slice(list_string, 1..-2//1) |> String.trim()
parse_inner_list(inner)
end
defp parse_inner_list(""), do: {:ok, []}
defp parse_inner_list(inner) do
items = String.split(inner, ",") |> Enum.map(&String.trim/1)
{:ok, items}
end
defp parse_map_value(value_string) do
trimmed = String.trim(value_string)
parse_map_string(trimmed)
end
defp parse_map_string("%{" <> _ = trimmed) do
case String.ends_with?(trimmed, "}") do
true -> eval_map_string(trimmed)
false -> {:error, "Map must start with %{ and end with }"}
end
end
defp parse_map_string(_),
do: {:error, "Map must start with %{ and end with }"}
defp eval_map_string(trimmed) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
{result, _} = Code.eval_string(trimmed)
validate_map_result(result)
end) do
{:ok, result} -> result
{:error, _} -> {:error, "Invalid map syntax"}
end
end
defp validate_map_result(result) when is_map(result), do: {:ok, result}
defp validate_map_result(_), do: {:error, "Not a valid map"}
defp intelligent_parse(value_string) do
trimmed = String.trim(value_string)
parse_by_type(trimmed)
end
defp parse_by_type("true"), do: {:ok, true}
defp parse_by_type("false"), do: {:ok, false}
defp parse_by_type("nil"), do: {:ok, nil}
defp parse_by_type(":" <> atom_name), do: {:ok, String.to_atom(atom_name)}
defp parse_by_type(trimmed) do
type_parsers = [
{&integer_string?/1, &String.to_integer/1},
{&float_string?/1, &String.to_float/1},
{&list_string?/1, &parse_list_value/1},
{&map_string?/1, &parse_map_value/1}
]
Enum.find_value(type_parsers, {:ok, trimmed}, fn {predicate, parser} ->
apply_parser_if_match(predicate, parser, trimmed)
end)
end
defp integer_string?(str), do: String.match?(str, ~r/^\d+$/)
defp float_string?(str), do: String.match?(str, ~r/^\d+\.\d+$/)
defp list_string?(str), do: String.starts_with?(str, "[")
defp map_string?(str), do: String.starts_with?(str, "%{")
end