Items are the units of a LivePanels canvas.

An item has an id, type, label, placement, visibility, z-order, props, and capabilities. It may render LiveView content, expose graph ports, represent a vector item, or exist only as runtime state.

The application decides what an item looks like. LivePanels owns the runtime facts that make the item movable, selectable, persistent, shareable, and commandable.

Declare Items In Markup

Use <:item> inside canvas_runtime/1 when the host source code should own the item instance and its content:

<.canvas_runtime {canvas_runtime_attrs(assigns)}>
  <:item id="note-1" at={{120, 80}} size={{560, 360}} resize>
    <section class="note-card">
      <header {drag_handle_attrs()}>Note</header>
      <p>Any HEEx can live here.</p>
      <.resize_handles />
    </section>
  </:item>
</.canvas_runtime>

The id is the runtime item id. at declares {x, y}. size declares {w, h}. The slot entry creates the canvas item through the normal command, state, persistence, and shared-canvas path; the slot body is the rendered item content.

The slot may receive a render context:

<:item :let={context} id="note-1" at={{120, 80}} size={{560, 360}} resize>
  <section class={"note-card #{if context.selected?, do: "is-selected"}"}>
    <header {drag_handle_attrs()}>{context.item.id}</header>
    <p>{context.item.w} x {context.item.h}</p>
    <.resize_handles :if={context.view.resizable?} />
  </section>
</:item>

Declared items are source-owned. To remove one permanently, stop rendering that slot. Runtime-created items use commands plus an id-less item renderer slot.

Render Runtime Items

When an item is created from a browser control or server command, the host still owns the item body in HEEx. Add one id-less :item slot to render runtime-owned items:

<.canvas_runtime {canvas_runtime_attrs(assigns)}>
  <:item :let={context}>
    <section class="note-card">
      <header {drag_handle_attrs()}>{context.item.label}</header>
      <p>{LivePanels.Item.prop(context.item, :body, "")}</p>
      <.resize_handles :if={context.view.resizable?} />
    </section>
  </:item>
</.canvas_runtime>

The item command payload supplies type, placement, props, and capabilities. Common options include:

  • :label;
  • :props;
  • :resize;
  • :interaction, such as interaction: %{removable: false};
  • :drag_drop;
  • graph :ports;
  • :render, such as frame overflow or stream update behavior.

Use mix livepanels.check when a declaration is not behaving as expected. The task validates canvas declarations before you debug browser wiring.

Add Items From App Controls

Use add_item_attrs/2 for an app-rendered browser control that creates a runtime item from an explicit type and option payload:

<button {add_item_attrs(:note)}>New note</button>

You can pass placement and item props as attrs:

<button
  {add_item_attrs(:note,
    x: 160,
    y: 120,
    w: 380,
    h: 240,
    props: %{body: "Created from the toolbar"}
  )}
>
  New note
</button>

The button is your markup. The attr helper supplies the command payload.

Add Items From Server Code

Use LivePanels.Canvas.Item.add/3 when an application event or server process should create an item:

def handle_event("create-note", _params, socket) do
  socket =
    LivePanels.Canvas.Item.add!(socket, :note,
      id: "note-#{System.unique_integer([:positive])}",
      x: 120,
      y: 80,
      w: 360,
      h: 220,
      props: %{body: "Created on the server"}
    )

  {:noreply, socket}
end

The command still goes through the same validation, policy, state update, history, persistence, and projection path as a browser action.

Place Items

Placement describes where an item lives:

%{
  space: :world,
  anchor: :top_left,
  x: 120,
  y: 80,
  w: 360,
  h: 220
}

World-space items move with the camera. They are the normal choice for panels, cards, graph nodes, and vector items.

Viewport-space items stay pinned to the screen while the world pans and zooms. Use them for HUDs, overlays, or controls that should belong to the canvas but not to the world.

Viewport items can also declare responsive frames. Configure named viewport profiles on the canvas, then put viewport_frames on the viewport item:

<:item
  id="canvas-controls"
  type="canvas_controls"
  space={:viewport}
  anchor={:bottom_right}
  lock
  viewport_frames={%{
    default: %{
      space: :viewport,
      anchor: :bottom_right,
      x: 16,
      y: 16,
      w: {:fill, inset: 16, min: 280, max: 480},
      h: 72
    },
    compact: %{space: :viewport, anchor: :top_left, x: 0, y: 0, w: :fill, h: 96}
  }}
>
  <nav class="canvas-controls">...</nav>
</:item>

The frame keys are :space, :anchor, :x, :y, :w, :h, and :space must resolve to :viewport when present. Width and height may be positive numbers, :fill, or {:fill, inset: n, min: n, max: n}. The default frame is used when no configured profile matches.

Viewport frames are render projection only. LivePanels recomputes the frame for each connected socket from that socket's viewport snapshot, but Canvas.State.items, layout persistence, and shared coordination keep canonical item placement. World-space responsive layout remains application policy; use item commands or host layout code when world items should move at a breakpoint.

Place an existing item with LivePanels.Canvas.Item.place/3:

socket =
  LivePanels.Canvas.Item.place!(socket, "note-1", %{
    space: :world,
    anchor: :top_left,
    x: 240,
    y: 140,
    w: 420,
    h: 260
  })

Use move_by/3, move_to/3, resize_by/3, and resize_to/3 for common placement changes.

Store Item Props

Use LivePanels.Canvas.Item for app-owned item props:

socket =
  LivePanels.Canvas.Item.merge_props!(socket, "note-1", %{
    status: "review",
    accent: "blue"
  })

The canvas can persist item props that belong to layout/runtime recovery, but your application remains responsible for domain records. For example, a canvas item can store record_id, while your app owns the record itself.

An id-less :item slot receives a render context for runtime items:

<.canvas_runtime {canvas_runtime_attrs(assigns)}>
  <:item :let={context}>
    <section class={"note note--#{LivePanels.Item.prop(context.item, :accent, "plain")}"}>
      {context.content}
    </section>
  </:item>
</.canvas_runtime>

Hide, Show, Or Remove Items

Visibility is not removal.

socket = LivePanels.Canvas.Item.hide!(socket, "note-1")
socket = LivePanels.Canvas.Item.show!(socket, "note-1")
socket = LivePanels.Canvas.Item.toggle_visibility!(socket, "note-1")

A hidden item can remain in state, history, layout, graph relationships, and shared coordination. Remove an item only when it should leave the canvas:

socket = LivePanels.Canvas.Item.remove!(socket, "note-1")

From markup:

<button {remove_item_attrs(context)}>Close</button>

Attach Child Items

Some UIs need an item to belong to another item, such as a badge, inspector, or attached tool on a parent card.

socket =
  socket
  |> LivePanels.Canvas.Item.add!(:comment, id: "comment-1")
  |> LivePanels.Canvas.Item.attach!("note-1", "comment-1")

Use attachment when the child should remain a canvas item but track a parent relationship. Do not use it just to render normal markup inside a card; that is item content.

Capabilities Add Behavior

Capabilities describe what runtime behavior an item supports.

A note can be resizable and removable. A graph node can have ports. A drawing vector item can expose vector state. A locked item can keep placement fixed. interaction: %{promote_on_focus: false} lets an item become active without automatic z-order promotion.

Application code should read the render view instead of duplicating capability logic:

<.resize_handles :if={context.view.resizable?} />

For graph-capable items, render ports:

<button :for={port <- context.ports} {graph_port_anchor_attrs(port)}>
  {port.label}
</button>

The item attrs declare the capability. Your markup presents it.

What LivePanels Owns

LivePanels owns item identity, placement, visibility, focus, selection, z-order, capability checks, command handling, layout persistence, and projection.

Your application owns item shell markup, titles, buttons, menus, product-specific data, and how users choose to create or organize items.