# `LivePanels.Canvas`
[🔗](https://github.com/livepanels/livepanels/blob/v0.1.0/lib/livepanels/canvas.ex#L1)

LiveView application entrypoint for the LivePanels canvas domain.

Wires the server-owned canvas model into a LiveView through a
`Phoenix.LiveView.on_mount/1` hook. In Phoenix apps, start with your app's
normal LiveView macro, then add LivePanels:

    use MyAppWeb, :live_view

    use LivePanels.Canvas,
      pubsub: MyApp.PubSub

The application remains responsible for layouts, verified routes, HTML imports,
and any framework conventions installed by `MyAppWeb`. Applications that want
explicit lifecycle ownership can call `mount_runtime/4` from their own
`mount/3` instead. The abstract canvas model lives under `LivePanels.Canvas.*`;
graph behavior lives under `LivePanels.Graph.*`; drawing behavior lives under
`LivePanels.Drawing.*`; runtime projection and shared coordination are wired
internally by the compositor runtime; item content descriptors and render
drivers live under `LivePanels.Item.Content.*`. Applications own the full render
tree and use the imported runtime component and helper modules for the
browser protocol boundary.

## Setup

    defmodule MyAppWeb.CanvasLive do
      use MyAppWeb, :live_view

      use LivePanels.Canvas,
        pubsub: MyApp.PubSub

      def render(assigns) do
        ~H"""
        <.canvas_runtime {canvas_runtime_attrs(assigns)}>
          <:item id="starter-card" at={{80, 80}} size={{640, 420}} resize>
            <section class="starter-card">
              <header {drag_handle_attrs()}>Starter card</header>
              <p>Put ordinary HEEx content directly inside the item declaration.</p>
            </section>
          </:item>
        </.canvas_runtime>
        """
      end
    end

Markup item declarations are the ordinary item path: put `<:item>` entries
directly inside `canvas_runtime/1`, declare their frame with attrs such as
`id`, `at`, and `size`, then place HEEx inside the slot. Runtime-created items
are created from explicit item payloads and rendered through the id-less
`<:item :let={context}>` slot. LivePanels does not ship visual chrome.
Applications render their own shell inside `canvas_runtime/1` slots, use
ordinary Phoenix `live/` and `components/` folders for host code, and import
application controls progressively through toolkit modules.

> #### `use LivePanels.Canvas` {: .info}
>
> When you `use LivePanels.Canvas`, the module attaches
> `Phoenix.LiveView.on_mount({LivePanels.Canvas, opts})`, installs compile-time
> validation that the caller has already used Phoenix LiveView, and defines
> `__livepanels_canvas_config__/0` returning the configured options.

Canvas application mutations and queries live on deliberate owner modules:

- `LivePanels.Canvas.Item` for item lifecycle, placement, attachment,
  focus, visibility, props, grouping, and item metadata
- `LivePanels.Canvas.Selection` for actor selection
- `LivePanels.Canvas.Arrange` for alignment, distribution, stacking,
  registered layout strategies, and reserved layout areas
- `LivePanels.Canvas.History` for undo and redo
- `LivePanels.Canvas.Clipboard` for copy, cut, and paste
- `LivePanels.Canvas.Viewport` for anchors, stepping, and fitting
- `LivePanels.Canvas.Shared` and `LivePanels.Canvas.Region` for shared
  participant sessions and region boundaries
- `LivePanels.Canvas.Render` for render stream refreshes
- `LivePanels.Canvas.Read` for read-side socket accessors

Graph and drawing commands stay module-qualified on `LivePanels.Graph` and
`LivePanels.Drawing`.

## Runtime Component Contract

`canvas_runtime/1` owns the root, viewport, stage, stream, frame, and runtime
hook attrs required by the bundled JS. Applications provide slots and ordinary
styling. Use `LivePanels.Toolkit.Canvas` for `canvas_runtime/1`,
`canvas_runtime_attrs/2`, `resize_handles/1`, `drag_handle_attrs/1`,
`resize_handle_attrs/2`, `focus_exempt_attrs/1`,
`keyboard_scope_attrs/1`, item controls, selection controls, layout controls,
history controls, clipboard controls, and canvas-wide viewport JS helpers.

The exact `data-livepanels-*` DOM protocol is an internal contract between
the toolkit components and bundled JS hook. It is documented for maintainers
in maintainer docs, not as setup material for ordinary
applications.

Runtime browser features are explicit. `canvas_runtime/1` emits feature tokens
from the configured extensions, so graph and drawing controllers mount only on
canvases that enabled those extensions. Custom render loops can pass
`runtime_features:` to `canvas_runtime/1`.

## Runtime Protocol

The bundled hook and toolkit helpers own the browser/server protocol. The
maintainer-facing DOM and payload details live in
the source modules and protocol contract tests. Applications usually create their own
buttons, menus, keyboard shortcuts, and toolbar controls with toolkit attrs or
module-qualified action calls.

## Application lifecycle

LivePanels attaches its runtime with `on_mount {LivePanels.Canvas, opts}`.
Applications own their ordinary `mount/3`, `handle_event/3`, and `handle_info/2`
callbacks. LivePanels intercepts its own protocol through LiveView lifecycle
hooks before application callbacks run, so no `super` call is needed.

Applications that do not want a `use LivePanels.Canvas` entrypoint can compose the
runtime directly:

    use MyAppWeb, :live_view

    @impl true
    def mount(params, session, socket) do
      with {:ok, socket} <-
             LivePanels.Canvas.mount_runtime(
               [pubsub: MyApp.PubSub],
               params,
               session,
               socket
             ) do
        {:ok, socket}
      end
    end

Applications can participate in command acceptance by configuring a canvas
policy module:

    defmodule MyApp.CanvasPolicy do
      @behaviour LivePanels.Canvas.Policy

      @impl true
      def authorize(%LivePanels.Canvas.Policy.Request{action: :remove_item} = request) do
        if MyApp.Workflows.item_locked?(request.canvas_id, request.item_ids, request.actor) do
          {:error, :active_workflow_reference}
        else
          :ok
        end
      end

      def authorize(_request), do: :ok
    end

    on_mount {LivePanels.Canvas, pubsub: MyApp.PubSub, policy: MyApp.CanvasPolicy}

The policy receives a normalized `LivePanels.Canvas.Policy.Request`. Returning
`{:error, reason}` rejects the command before reducers apply it; browser
command acknowledgements report the rejected status, inspected error, stable
reason string, and diagnostic message. Application command helpers return
command results by default; use the `!` variant when a LiveView callback should
receive the updated socket or raise on rejection.

Returning `:ok` accepts the command. Returning `{:ok, metadata}` accepts the
command and stores metadata under `:policy` in command metadata. Returning
`{:error, reason}` or `{:halt, reason}` rejects the canvas command before
LivePanels state changes. Policy validates command acceptance; it does not
append commands, rewrite commands, or coordinate application database
writes. Run application writes from application callbacks or context modules,
then issue LivePanels commands through the public command helpers:

    def handle_event("move_card", %{"id" => id, "dx" => dx, "dy" => dy}, socket) do
      delta = {String.to_integer(dx), String.to_integer(dy)}

      case MyApp.Boards.move_card(id, delta) do
        {:ok, card} ->
          socket =
            LivePanels.Canvas.transaction!(socket, fn tx ->
              tx
              |> LivePanels.Canvas.Item.move_by!(id, delta)
              |> LivePanels.Canvas.Item.merge_props!(id, %{
                card_id: card.id,
                card_version: card.lock_version
              })
            end)

          {:noreply, socket}

        {:error, reason} ->
          {:noreply, assign(socket, :canvas_error, reason)}
      end
    end

LivePanels validates the command before invoking policy. Policy runs behind a
timeout boundary because it executes in the reducer and shared-coordinator
command path. A policy timeout rejects the LivePanels command and kills the
policy process.

## Options

| Option | Default | Description |
|---|---|---|
| `:pubsub` | required | Your app's PubSub module |
| `:domains` | `[]` | Canvas domains to mount, such as `:graph` and `:drawing`; custom domain modules may also be supplied |
| `:policy` | none | `LivePanels.Canvas.Policy` module or `{module, opts}` for application command acceptance; `timeout_ms:` controls callback timeout |
| `:session` | `:local` | `:local`, `:shared`, or a keyword/map with `:mode`, `:id`, `:backend`, `:layout_store`, and `:coordinator` |
| `:initial` | `%{connectors: []}` | Initial canvas model data; `:connectors` seed the graph domain |
| `:viewport` | `%{profiles: []}` | Viewport profile rules evaluated from measured screen, usable viewport, and zoom metrics |
| `:persistence` | `%{debounce_ms: 25, load_timeout_ms: 5_000}` | Layout persistence timing |
| `:interaction` | `%{lease_timeout_ms: 15_000, broadcast_sync_ms: 16}` | Interaction lease and shared fanout timing |

Items are authored in HEEx. A source-owned item has an `id`; the id-less item
slot renders runtime-owned items:

    <.canvas_runtime {canvas_runtime_attrs(assigns)}>
      <:item id="chart" type="chart" at={{80, 80}} size={{720, 520}}>
        <MyApp.Chart.render />
      </:item>

      <:item :let={context}>
        <MyApp.RuntimeItem.render context={context} />
      </:item>
    </.canvas_runtime>

Viewport profiles name measured viewport breakpoints. Viewport items can use
those names in `viewport_frames` to project a different rendered frame
for the current socket without mutating canonical item placement:

    viewport: [profiles: [compact: [screen_width_lte: 720]]],

    <:item
      id="toolbar"
      type="toolbar"
      space={:viewport}
      at={{16, 16}}
      size={{360, 72}}
      anchor={:bottom_right}
      viewport_frames={
        %{
          default: %{anchor: :bottom_right, x: 16, y: 16, w: 360, h: 72},
          compact: %{anchor: :top_left, x: 0, y: 0, w: :fill, h: 96}
        }
      }
    >
      <MyApp.Toolbar.render />
    </:item>

Profile conditions may target `screen_width`, `screen_height`,
`usable_width`, `usable_height`, and `zoom` with `_lte` or `_gte` suffixes.
`viewport_frames` supports `:space`, `:anchor`, `:x`, `:y`, `:w`, and `:h`;
`:space` must resolve to `:viewport` when present. Dimensions may be positive
numbers, `:fill`, or `{:fill, inset: n, min: n, max: n}`.

Graph startup topology belongs to the canvas initial model:

    initial: [
      connectors: [
        [from: {"source", "out"}, to: {"target", "in"}]
      ]
    ],
    domains: [:graph, :drawing]

Initial connectors are applied through the graph connection reducer, so port
capability checks and typed-port snapping match user-driven graph commands.

Shared canvases use `LivePanels.Canvas.Shared` to select coordinator lookup,
coordinator ownership, participant tracking ownership, and the layout store
as one backend value. Put the backend and optional layout store under
`session:` so sharing concerns stay together:

    use LivePanels.Canvas,
      pubsub: MyApp.PubSub,
      session: [
        mode: :shared,
        id: {:param, "board_id"},
        backend: LivePanels.Canvas.Shared.Local,
        layout_store: MyApp.CanvasLayouts
      ]

`LivePanels.Canvas.Geometry.interaction_sync_ms/1` is the browser-to-server
half of the interaction sync story. Shared canvases use a lower default
network cadence and layer `interaction: [broadcast_sync_ms: ...]` on top for
server-to-participant fanout, so remote observers can be coalesced more or
less aggressively without changing the originating client's local drag
cadence.

## Runtime canvas identity

`session: [id: ...]` is resolved before the application `mount/3` callback because the
runtime must know which local or shared state to mount first. Omit the option
to use the built-in lookup order: `params["canvas_id"]`, `session["canvas_id"]`,
and, in shared mode, `params["shared_canvas_id"]`,
`session["shared_canvas_id"]`, or `session["livepanels_shared_canvas_id"]`. If
none are present, LivePanels generates an id for that mount.

Shared boards with app-specific route or session keys should configure an
explicit runtime source:

    use MyAppWeb, :live_view

    use LivePanels.Canvas,
      pubsub: MyApp.PubSub,
      session: [mode: :shared, id: {:param, "board_id"}]

`{:session, key}` reads from the LiveView session. `{:resolver, module,
function}` calls `module.function(params, session)` and requires a non-empty
string return value.

## Application render loop

Applications render the surrounding page and visible chrome. `canvas_runtime/1`
covers the canonical runtime root/viewport/stage structure plus the default
item render loop. `canvas_runtime_attrs/2` pulls `@livepanels`, the current
runtime streams, and `@livepanels.cursor_mode` from LiveView assigns. Applications
can pass `cursor_mode:` as an explicit component override when cursor policy
is app-owned. Runtime projections stay under `@livepanels`, including
`canvas_id`, `client_id`, `mode`, `cursor_mode`, `selection`, `history`,
`clipboard`, `connector_draft`, `connector_drafts`, `render_items`, and
`connectors`; root assigns with those names remain
app-owned. Toolkit modules expose these helpers as explicit imports.
Assigned item values are `%LivePanels.Item{}` structs; application chrome can
read and pattern match on fields such as `item.id`, `item.type`,
`item.x`, `item.y`, `item.w`, and `item.h` directly.

The `:item` slot declares source-owned items when it has an `id`, and acts as
the renderer for runtime-owned items when it has no `id`. Both forms can
receive a `LivePanels.Item.RenderContext` with `:let`; the canonical item is
`context.item`, render decisions live under `context.view`, rendered content
lives under `context.content`, and ports live under `context.ports`.

`t:LivePanels.Item.t/0` is the public canonical item state record.
Application render code should use the `LivePanels.Item` projection
yielded by `canvas_runtime/1` or returned by `items_for_render/2`; application code
that needs canonical data, dimensions, or type filtering can inspect the
item struct directly. Mutations still go through owner modules and commands.

Toolkit preset imports:

- `LivePanels.Toolkit.Canvas` exposes canvas runtime helpers and canvas
  control attrs.
- `LivePanels.Toolkit.Graph` uses the canvas preset and exposes graph DOM
  attrs plus marker helpers.
- `LivePanels.Toolkit.Drawing` uses the canvas preset and exposes vector item
  controls.

Applications that want a custom mix of helpers can `import` the individual toolkit
modules directly. `LivePanels.Toolkit.Integration.ProtocolAttrs` and
`LivePanels.Toolkit.Integration.Render` are integration escape hatches for command
envelopes and render loops, not first-path imports.

Item slots can declare direct content adapters with attrs such as `live_view`,
`remote`, and `canvas`. Static content belongs in the item slot body.
`LivePanels.Canvas.Render.refresh/2` refreshes rendered stream items when an
item renderer depends on application assigns outside canonical canvas state.
Integration frame helpers such as `canvas_item_frame/1`,
`canvas_item_frame_attrs/4`, `items_for_render/2`, `canvas_item_z_index/1`,
and `canvas_item_style/2` live in `LivePanels.Toolkit.Integration.Render`. Integration
command-envelope attrs live in the
`:protocol_attrs` group as `command_attrs/2` and `item_command_attrs/3`.

Use `select_item_drag_handle_attrs/2` when an app-rendered item body should
both select and drag the item without adopting titlebar chrome. Nested
interactive controls should keep using focus-exempt command attrs.

Canvas, graph, and drawing state changes remain explicit module calls:
`LivePanels.Canvas.Item.add/3`, `LivePanels.Canvas.Item.remove/2`,
`LivePanels.Graph.connect/2`, and `LivePanels.Drawing.vector_item/4`.

# `actor`

```elixir
@type actor() :: %{
  :client_id =&gt; String.t(),
  :viewer_id =&gt; String.t(),
  optional(:viewport) =&gt; map(),
  optional(:role) =&gt; :editor | :viewer,
  optional(:capabilities) =&gt; [atom() | String.t()],
  optional(:region_id) =&gt; String.t() | nil
}
```

# `command_meta`

```elixir
@type command_meta() :: map()
```

# `command_target`

```elixir
@type command_target() :: socket() | LivePanels.Canvas.Transaction.t()
```

First argument accepted by canvas command helpers.

Use a LiveView socket for an immediate command or the transaction context
received inside `transaction/2`.

# `socket`

```elixir
@type socket() :: Phoenix.LiveView.Socket.t()
```

# `mount_runtime`

```elixir
@spec mount_runtime(keyword() | map(), map() | :not_mounted_params, map(), socket()) ::
  {:ok, socket()}
```

Mounts the canvas runtime into an existing LiveView socket.

This is the explicit equivalent of the `on_mount {LivePanels.Canvas, opts}`
path installed by `use LivePanels.Canvas`. Call it from an application `mount/3` when
the LiveView should own the lifecycle setup directly, then import the toolkit
helper modules or preset modules the template actually uses.

# `transaction`

```elixir
@spec transaction(socket(), (LivePanels.Canvas.Transaction.t() -&gt;
                         LivePanels.Canvas.Transaction.t())) ::
  {:ok, socket(), command_meta()} | {:error, term(), socket(), command_meta()}
```

Applies one canvas transaction through the ordinary socket command path.

The callback receives a transaction context. Pass that context to the same
owner modules used for direct socket mutations, then return it:

    socket =
      LivePanels.Canvas.transaction!(socket, fn tx ->
        tx
        |> LivePanels.Canvas.Item.move_by!("item-a", {24, 0})
        |> LivePanels.Canvas.Item.merge_props!("item-a", %{accent: "red"})
        |> LivePanels.Graph.connect!(from: {"item-a", "out"}, to: {"item-b", "in"})
      end)

The commands authorize, reduce, stream, persist, broadcast, and record history
as one logical canvas operation. If any staged command is rejected, the result
is `{:error, reason, socket, meta}` and the model is not changed. Use
`transaction!/2` when rejected commands should raise.

# `transaction!`

```elixir
@spec transaction!(socket(), (LivePanels.Canvas.Transaction.t() -&gt;
                          LivePanels.Canvas.Transaction.t())) ::
  socket()
```

Applies one canvas transaction and returns the updated socket.

Raises when any staged command is rejected. Use `transaction/2` when
application code needs the command result tuple.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
