# Render Nodes And Ports

LivePanels graph nodes are app-rendered item shells with runtime port attrs.

The application decides what a node looks like. LivePanels needs enough attrs to know
which DOM element represents each declared port and which command to send when
the user starts or completes a connection.

## Render A Node Shell

Use the `:item` slot:

```heex
<.canvas_runtime {canvas_runtime_attrs(assigns)}>
  <:item :let={context}>
    <article class={["node-card", context.selected? && "is-selected"]}>
      <header {drag_handle_attrs(class: "node-card__title")}>
        {context.item.label}
      </header>

      <section class="node-card__body">
        {context.content}
      </section>

      <.resize_handles :if={context.view.resizable?} />
    </article>
  </:item>
</.canvas_runtime>
```

Graph nodes still need ordinary item behavior: drag, focus, selection, resize,
visibility, z-order, and item content.

## Render Port Controls

`context.ports` contains render-ready ports for the current node:

```heex
<button
  :for={port <- context.ports}
  class={"node-port node-port--#{port.direction}"}
  {graph_port_anchor_attrs(port)}
  {connection_attrs(context.item, port)}
>
  {port.label}
</button>
```

Then choose the command attrs from direction:

```elixir
defp connection_attrs(item, %{direction: :output} = port), do: begin_connector_attrs(item, port)
defp connection_attrs(item, %{direction: :input} = port), do: complete_connector_attrs(item, port)
defp connection_attrs(_item, _port), do: %{}
```

`graph_port_anchor_attrs/2` identifies the port for geometry and browser
interaction. `begin_connector_attrs/3` and `complete_connector_attrs/3` emit graph
commands.

## Position Ports From Their Anchor

Use the same anchor declaration to style ports:

```elixir
defp port_style(%{anchor: %{edge: edge, offset: offset}}) do
  case edge do
    :left -> "left:-0.5rem; top:#{offset}px; transform:translateY(-50%);"
    :right -> "right:-0.5rem; top:#{offset}px; transform:translateY(-50%);"
    :top -> "top:-0.5rem; left:#{offset}px; transform:translateX(-50%);"
    :bottom -> "bottom:-0.5rem; left:#{offset}px; transform:translateX(-50%);"
  end
end
```

That keeps visible handles aligned with the endpoint coordinates the server
will use for connector projection.

## Render Row-Based Ports

For table, form, or workflow nodes, derive port rows from item props:

```heex
<div :for={entry <- @column_port_entries} class="column-row">
  <span>{entry.column.name}</span>

  <button
    class="column-port"
    style={port_style(entry.port)}
    title={port_title(entry)}
    {graph_port_anchor_attrs(entry.port)}
    {connection_attrs(@item, entry.port)}
  />
</div>
```

Build the entries by matching declared ports to domain rows:

```elixir
defp column_port_entries(columns, ports) do
  port_lookup = Map.new(ports, &{&1.name, &1})

  columns
  |> Enum.flat_map(fn column ->
    [
      %{column: column, port: Map.get(port_lookup, "col_#{column.slot}_in")},
      %{column: column, port: Map.get(port_lookup, "col_#{column.slot}_out")}
    ]
  end)
  |> Enum.reject(&is_nil(&1.port))
end
```

The row is product UI. The port is runtime metadata.

## Show Connection Counts

Use `LivePanels.Graph.Query` to show connected ports:

```elixir
defp connector_count(connectors, item_id, endpoint, port_name) do
  connectors
  |> Enum.count(fn connector ->
    case {endpoint, connector} do
      {:from, %{from: %{item_id: ^item_id, anchor: anchor}}} ->
        LivePanels.Item.Connector.port_name(anchor) == port_name

      {:to, %{to: %{item_id: ^item_id, anchor: anchor}}} ->
        LivePanels.Item.Connector.port_name(anchor) == port_name

      _ ->
        false
    end
  end)
end
```

Or use query helpers from the canvas LiveView:

```elixir
state = LivePanels.Canvas.Read.state(socket)
connectors = LivePanels.Graph.connectors(state)
connected = LivePanels.Graph.Query.connectors_for_port(connectors, "table-1", "col_2_out")
```

Render counts as application UI:

```heex
<span :if={entry.outgoing_count > 0} class="port-count">
  {entry.outgoing_count}
</span>
```

## Pending Connector State

The runtime projects the current actor's connector draft as `@livepanels.connector_draft`.
Pass it to node components when the node should show compatible targets or
reconnect state:

```heex
<.node context={context} pending_connector={@livepanels.connector_draft} />
```

Use pending state for visual hints only. Validation still happens on the server
when the command completes.

## Accessibility

Ports are real controls. Give them names:

```heex
<button
  aria-label={"Connect #{context.item.label} #{port.label}"}
  title={port.label}
  {graph_port_anchor_attrs(port)}
  {connection_attrs(context.item, port)}
/>
```

If a visible port is decorative and another labelled control starts the
connection, put graph attrs on the labelled control instead.

## Application Responsibilities

The application owns:

- node layout and visual hierarchy;
- port shape, label, hover state, count badges, and disabled hints;
- node inspectors and property editors;
- how domain data appears inside the node.

LivePanels owns:

- port metadata projection;
- graph command attrs;
- endpoint validation;
- connector state and connector projection.
