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 asinteraction: %{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}
endThe 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.