Most apps should use LivePanels.Canvas, a toolkit preset, and
canvas_runtime/1.
Use manual integration APIs when you are building a bridge package, custom render loop, manual protocol surface, durable layout store, or clustered shared backend.
Manual integration should still preserve the main boundary: LivePanels owns runtime behavior; the application owns product UI and product data.
Decide What Belongs In LivePanels
Before extending the integration, classify the behavior:
| Behavior | Usually belongs |
|---|---|
| item placement, resize, focus, visibility, z-order | LivePanels canvas runtime |
| graph ports, connector validation, connector state | LivePanels graph runtime |
| committed vector items and vector item edits | LivePanels drawing runtime |
| route authorization, workflow rules, domain records | application |
| toolbar layout, menus, palettes, inspectors | application UI |
| reusable integration with another UI framework | bridge package |
If the behavior changes canvas state, prefer a public command helper. If the behavior is product-specific meaning, keep it in the application and call LivePanels only for canvas mutations.
Mount Runtime Manually
Use LivePanels.Canvas.mount_runtime/4 when the use LivePanels.Canvas macro
is not the right fit:
defmodule MyAppWeb.BoardLive do
use MyAppWeb, :live_view
use LivePanels.Toolkit.Canvas
@impl true
def mount(params, session, socket) do
LivePanels.Canvas.mount_runtime(
[pubsub: MyApp.PubSub],
params,
session,
socket
)
end
endThis keeps the same runtime config and command behavior while giving the application direct lifecycle ownership.
Render Runtime-Created Items
Runtime-created items use the same app-owned HEEx frame as source-owned items. Declare an id-less item slot and branch on the item type or app props you store in item state:
<.canvas_runtime {canvas_runtime_attrs(assigns)}>
<:item :let={context}>
<section class="card-item">
<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>Server code creates those runtime items with direct payloads:
socket =
LivePanels.Canvas.Item.add!(
socket,
"report",
id: "report-#{System.unique_integer([:positive])}",
x: 120,
y: 80,
w: 640,
h: 480,
props: %{title: "Report"}
)Build Manual Protocol Attrs
Prefer named helpers such as add_item_attrs/2, remove_item_attrs/2, graph
attrs, and drawing attrs.
Use LivePanels.Toolkit.Integration.ProtocolAttrs only when a bridge or custom application
surface needs lower-level command envelopes:
<button {LivePanels.Toolkit.Integration.ProtocolAttrs.command_attrs(:select_all)}>
Select all
</button>
<button {LivePanels.Toolkit.Integration.ProtocolAttrs.item_command_attrs(:remove_item, item)}>
Remove
</button>Manual protocol attrs are public, but they are easier to misuse than named helpers. Keep them close to integration code.
Build Manual Render Loops
The ordinary render path is:
<.canvas_runtime {canvas_runtime_attrs(assigns)}>
<:item :let={context}>
...
</:item>
</.canvas_runtime>Use LivePanels.Toolkit.Integration.Render when you intentionally take over item ordering,
DOM ids, z-index, frame styles, or vector item rendering:
items =
LivePanels.Toolkit.Integration.Render.items_for_render(
LivePanels.Canvas.Read.items(socket),
LivePanels.Canvas.Read.z_order(socket)
)<LivePanels.Toolkit.Integration.Render.canvas_item_frame
item={context.item}
view={context.view}
client_id={@livepanels.client_id}
>
...
</LivePanels.Toolkit.Integration.Render.canvas_item_frame>When you need to render the frame element yourself, use
canvas_item_frame_attrs/4 for the same runtime-facing attributes:
<div
{LivePanels.Toolkit.Integration.Render.canvas_item_frame_attrs(
context.item,
context.view,
@livepanels.client_id,
class: "app-frame"
)}
>
...
</div>Manual render loops must preserve the DOM attrs and layer structure required by
the browser runtime. If you only need custom chrome for runtime-owned items, use
an id-less :item renderer instead.
Declare Browser Runtime Features
The canonical canvas_runtime/1 component emits feature tokens from configured
extensions. Manual render loops or custom hook setup must keep that public
configuration explicit so graph, drawing, presence, keyboard, and marquee
controllers mount only for canvases that use them:
<.canvas_runtime
{canvas_runtime_attrs(assigns,
runtime_features: [:keyboard, :selection, :graph]
)}
/>The JavaScript hook accepts the same feature vocabulary:
const LivePanelsCanvas = createLivePanelsCanvasHook({
features: { graph: true, drawing: false, presence: false }
})Use Read-Side State
Use LivePanels.Canvas.Read.state/1 only when lower-level helpers need the
canonical state:
state = LivePanels.Canvas.Read.state(socket)
connectors = LivePanels.Graph.connectors(state)Prefer narrower read helpers when possible:
items = LivePanels.Canvas.Read.items(socket)
selection = LivePanels.Canvas.Read.selection(socket)
history = LivePanels.Canvas.Read.history(socket)Connector rendering is part of canvas_runtime/1 when the graph extension is
configured. Read-side helpers are for inspectors and application decisions.
Mutations should still go through commands.
Implement Durable Layout Stores
Use LivePanels.Canvas.LayoutStore.Ecto when layouts must survive process and
server restarts:
defmodule MyApp.CanvasLayouts do
use LivePanels.Canvas.LayoutStore.Ecto,
repo: MyApp.Repo,
schema: MyApp.LivePanelsCanvasLayout
endPersist the layout payload unchanged. It is a versioned runtime codec, not a
app-authored item list. Implement LivePanels.Canvas.LayoutStore directly only
when the application uses a non-Ecto database or external cache.
Build Shared Backends
Use LivePanels.Canvas.Shared.Local for one-node shared canvases. Use
LivePanels.Canvas.Shared.Horde or a custom LivePanels.Canvas.Shared.Adapter when a
clustered deployment needs distributed coordinator lookup and supervision.
Changing the backend should not change command semantics. If it does, the backend is doing product work that belongs in the application.
Keep Manual Code Honest
Manual integration should still avoid:
- private runtime or compositor modules;
- browser controller internals;
- duplicate item, graph, drawing, history, or layout stores;
- DOM measurement as the canonical source of canvas state;
- application code that bypasses policy and command validation.
When in doubt, build an application helper around the public API instead of reaching into internals.