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:

BehaviorUsually belongs
item placement, resize, focus, visibility, z-orderLivePanels canvas runtime
graph ports, connector validation, connector stateLivePanels graph runtime
committed vector items and vector item editsLivePanels drawing runtime
route authorization, workflow rules, domain recordsapplication
toolbar layout, menus, palettes, inspectorsapplication UI
reusable integration with another UI frameworkbridge 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
end

This 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
end

Persist 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.