<%= category %>
Properties: <%= node.name %>
Select a node to edit its properties
<%= @generated_code %>
defmodule DalaDev.Server.DesignLive do use Phoenix.LiveView @grid_size 20 @default_zoom 100 @impl true def mount(_params, _session, socket) do design = %{ nodes: [] } component_types = [ %{type: :column, name: "Column", icon: "☰", category: "Layout"}, %{type: :row, name: "Row", icon: "☱", category: "Layout"}, %{type: :box, name: "Box", icon: "□", category: "Layout"}, %{type: :text, name: "Text", icon: "T", category: "Content"}, %{type: :button, name: "Button", icon: "▣", category: "Content"}, %{type: :icon, name: "Icon", icon: "★", category: "Content"}, %{type: :image, name: "Image", icon: "🖼", category: "Content"}, %{type: :divider, name: "Divider", icon: "─", category: "Content"}, %{type: :spacer, name: "Spacer", icon: "◇", category: "Content"}, %{type: :text_field, name: "Text Field", icon: "▭", category: "Input"}, %{type: :toggle, name: "Toggle", icon: "⊘", category: "Input"}, %{type: :slider, name: "Slider", icon: "◫", category: "Input"}, %{type: :switch, name: "Switch", icon: "⇄", category: "Input"}, %{type: :progress, name: "Progress", icon: "▰", category: "Feedback"}, %{type: :activity_indicator, name: "Activity", icon: "◌", category: "Feedback"}, %{type: :tab_bar, name: "Tab Bar", icon: "▤", category: "Navigation"}, %{type: :scroll, name: "Scroll", icon: "↕", category: "Navigation"}, %{type: :modal, name: "Modal", icon: "⏏", category: "Overlay"}, %{type: :safe_area, name: "Safe Area", icon: "⊡", category: "System"}, %{type: :status_bar, name: "Status Bar", icon: "▤", category: "System"}, %{type: :list, name: "List", icon: "☰", category: "Content"}, %{type: :webview, name: "WebView", icon: "◉", category: "Media"}, %{type: :camera_preview, name: "Camera", icon: "☐", category: "Media"}, %{type: :video, name: "Video", icon: "▶", category: "Media"} ] socket = socket |> assign(:design, design) |> assign(:component_types, component_types) |> assign(:selected_node, nil) |> assign(:grid_visible, true) |> assign(:snap_to_grid, true) |> assign(:zoom, @default_zoom) |> assign(:grid_size, @grid_size) |> assign(:active_tab, "components") |> assign(:export_format, "dsl") |> assign(:generated_code, "") {:ok, socket} end @impl true def handle_event("add_component", %{"type" => type_str}, socket) do type = String.to_atom(type_str) design = socket.assigns.design new_node = %{ id: generate_id(), type: type, name: Atom.to_string(type) |> String.replace("_", " ") |> String.capitalize(), x: 100, y: 100, props: default_props(type), children: [] } design = %{design | nodes: design.nodes ++ [new_node]} socket = assign(socket, :design, design) socket = assign(socket, :generated_code, generate_code(design, socket.assigns.export_format)) {:noreply, socket} end def handle_event("select_node", %{"id" => node_id}, socket) do socket = assign(socket, :selected_node, node_id) {:noreply, socket} end def handle_event( "update_property", %{"node_id" => node_id, "key" => key, "value" => value}, socket ) do design = socket.assigns.design nodes = Enum.map(design.nodes, fn node -> if node.id == node_id do props = Map.put(node.props, key, value) %{node | props: props} else node end end) design = %{design | nodes: nodes} socket = assign(socket, :design, design) socket = assign(socket, :generated_code, generate_code(design, socket.assigns.export_format)) {:noreply, socket} end def handle_event("delete_node", %{"id" => node_id}, socket) do design = socket.assigns.design nodes = Enum.reject(design.nodes, &(&1.id == node_id)) design = %{design | nodes: nodes} socket = assign(socket, :design, design) socket = assign(socket, :selected_node, nil) socket = assign(socket, :generated_code, generate_code(design, socket.assigns.export_format)) {:noreply, socket} end def handle_event("move_node", %{"id" => node_id, "x" => x, "y" => y}, socket) do design = socket.assigns.design nodes = Enum.map(design.nodes, fn node -> if node.id == node_id do %{node | x: String.to_integer(x), y: String.to_integer(y)} else node end end) design = %{design | nodes: nodes} socket = assign(socket, :design, design) {:noreply, socket} end def handle_event("clear_canvas", _params, socket) do design = %{nodes: []} socket = assign(socket, :design, design) socket = assign(socket, :selected_node, nil) socket = assign(socket, :generated_code, "") {:noreply, socket} end def handle_event("toggle_grid", _params, socket) do socket = assign(socket, :grid_visible, !socket.assigns.grid_visible) {:noreply, socket} end def handle_event("toggle_snap", _params, socket) do socket = assign(socket, :snap_to_grid, !socket.assigns.snap_to_grid) {:noreply, socket} end def handle_event("set_zoom", %{"zoom" => zoom}, socket) do socket = assign(socket, :zoom, String.to_integer(zoom)) {:noreply, socket} end def handle_event("set_export_format", %{"format" => format}, socket) do socket = assign(socket, :export_format, format) socket = assign(socket, :generated_code, generate_code(socket.assigns.design, format)) {:noreply, socket} end defp generate_id do :crypto.strong_rand_bytes(8) |> Base.encode64() |> String.replace(~r/[^A-Za-z0-9]/, "") |> String.slice(0, 8) end defp default_props(:column), do: %{"direction" => "vertical", "spacing" => "10"} defp default_props(:row), do: %{"direction" => "horizontal", "spacing" => "10"} defp default_props(:box), do: %{"width" => "100", "height" => "100", "color" => "#ffffff"} defp default_props(:text), do: %{"text" => "Text", "size" => "16", "color" => "#000000"} defp default_props(:button), do: %{"text" => "Button", "action" => ""} defp default_props(:icon), do: %{"name" => "star", "size" => "24"} defp default_props(:image), do: %{"src" => "", "width" => "100", "height" => "100"} defp default_props(:divider), do: %{"orientation" => "horizontal"} defp default_props(:spacer), do: %{"height" => "20"} defp default_props(:text_field), do: %{"placeholder" => "Enter text", "value" => ""} defp default_props(:toggle), do: %{"value" => "false"} defp default_props(:slider), do: %{"min" => "0", "max" => "100", "value" => "50"} defp default_props(:progress), do: %{"value" => "50", "max" => "100"} defp default_props(:activity_indicator), do: %{} defp default_props(:tab_bar), do: %{"tabs" => "Tab 1,Tab 2,Tab 3"} defp default_props(:scroll), do: %{"direction" => "vertical"} defp default_props(:modal), do: %{"title" => "Modal"} defp default_props(:safe_area), do: %{} defp default_props(:status_bar), do: %{"style" => "default"} defp default_props(:list), do: %{"items" => "Item 1,Item 2,Item 3"} defp default_props(:webview), do: %{"url" => "https://example.com"} defp default_props(:camera_preview), do: %{} defp default_props(:video), do: %{"src" => ""} defp default_props(:native_view), do: %{"component" => ""} defp default_props(:switch), do: %{"value" => "false"} defp default_props(_), do: %{} defp generate_code(design, format) do case format do "dsl" -> generate_dsl_code(design.nodes) "map" -> generate_map_code(design.nodes) _ -> generate_dsl_code(design.nodes) end end defp generate_dsl_code(nodes) do nodes_to_dsl(nodes) end defp generate_map_code(nodes) do nodes_to_map(nodes) end defp nodes_to_dsl(nodes) do nodes |> Enum.map(&node_to_dsl/1) |> Enum.join("\n") end defp node_to_dsl(%{type: type, props: props, children: children}) do props_str = props_to_dsl(props) children_str = if children && children != [] do children |> Enum.map(&node_to_dsl/1) |> Enum.join("\n ") |> (fn s -> " do\n " <> s <> "\nend" end).() else "" end "#{type}(#{props_str})#{children_str}" end defp props_to_dsl(props) do props |> Enum.map(fn {k, v} -> "#{k}: #{inspect(v)}" end) |> Enum.join(", ") end defp nodes_to_map(nodes) do nodes |> Enum.map(&node_to_map/1) |> inspect(pretty: true) end defp node_to_map(%{type: type, props: props, children: children}) do %{ type: type, props: props, children: children } end defp preview_node(%{type: :text, props: props}) do text = Map.get(props, "text", "Text") "#{text}" end defp preview_node(%{type: :button, props: props}) do text = Map.get(props, "text", "Button") "" end defp preview_node(%{type: :text_field, props: props}) do placeholder = Map.get(props, "placeholder", "Input") "" end defp preview_node(%{type: :toggle}), do: "
Select a node to edit its properties
<%= @generated_code %>