Drawing apps need vector items and elements to survive reloads, copy/paste, undo/redo, and collaboration.
LivePanels handles committed drawing state through the canvas model. The application handles files, durable assets, domain records, and product workflows.
Persist Drawings
Configure a layout store normally:
use LivePanels.Canvas,
pubsub: MyApp.PubSub,
session: [layout_store: MyApp.DrawingLayouts]
use LivePanels.Toolkit.DrawingThe layout payload can include:
- committed vector items;
- drawing element items;
- ordinary content items;
- graph connectors if the drawing app also uses graph capabilities;
- z-order, placement, visibility, history-relevant canvas state;
- persistent item props.
Persist the layout payload unchanged. When your app changes its own drawing schema, transform that app-owned data before handing the layout to LivePanels.
Drafts Are Not Persisted
Active drafts are browser-local.
They are intentionally not written to the layout store, copied to the clipboard, added to history, or broadcast to other participants. The completed vector item is persisted only after it becomes canvas state.
This is the right tradeoff for drawing apps because pointer movement is noisy and often abandoned.
History
Drawing commands participate in canvas history:
<button {undo_attrs()}>Undo</button>
<button {redo_attrs()}>Redo</button>Use transactions when one user action creates multiple vector items or elements:
socket =
LivePanels.Canvas.transaction!(socket, fn tx ->
tx
|> LivePanels.Drawing.vector_item!(:rectangle, box, id: "stamp-box", style: box_style)
|> LivePanels.Drawing.vector_item!(:text, label, id: "stamp-label", text: "Approved", style: text_style)
end)Clipboard
Committed vector items and elements use normal selection clipboard helpers:
<button {copy_selection_attrs()}>Copy</button>
<button {paste_selection_attrs()}>Paste</button>If the selection includes ordinary items and vector items, they copy together. Drafts do not copy because they have no item id yet.
Shared Drawing Canvases
Enable shared mode normally:
use LivePanels.Canvas,
pubsub: MyApp.PubSub,
session: [
mode: :shared,
id: {:param, "board_id"},
backend:
{LivePanels.Canvas.Shared.Local,
layout_store: MyApp.DrawingLayouts}
]
use LivePanels.Toolkit.DrawingAccepted drawing changes are shared:
- committed vector item creation;
- committed vector item style, text, label, and point edits;
- erase/removal commands;
- element creation;
- item move, resize, visibility, and z-order changes;
- undo/redo when the shared backend accepts those commands.
Active drafts remain outside canonical canvas state. Shared canvases broadcast throttled draw intent for live previews, then share the final vector item through the accepted draw command.
Participant Viewports
Shared drawing state does not require every participant to share a camera.
Viewport changes are actor-scoped. Use app-level state only when the product needs presenter mode, guided review, or "follow me" behavior.
Export Excalidraw
Export committed drawing state as an Excalidraw document map:
def handle_event("export_excalidraw", _params, socket) do
content =
socket
|> LivePanels.Canvas.Read.state()
|> LivePanels.Drawing.to_excalidraw()
|> Jason.encode!(pretty: true)
socket =
push_event(socket, "download-file", %{
filename: "drawing.excalidraw",
mime_type: "application/vnd.excalidraw+json",
content: content
})
{:noreply, socket}
endThe drawing subsystem returns a map instead of JSON so applications can choose their own JSON library, download path, or persistence target.
Import Excalidraw
Validate the document first:
with {:ok, document} <- Jason.decode(content),
{:ok, commands} <- LivePanels.Drawing.excalidraw_commands(document),
false <- commands == [] do
socket = LivePanels.Drawing.import_excalidraw!(socket, document)
{:noreply, socket}
else
_ -> {:noreply, put_flash(socket, :error, "Could not import that file.")}
endexcalidraw_commands/2 converts supported external elements to canonical
LivePanels commands. Unsupported or malformed external elements are ignored or
reported as errors depending on the document shape.
Domain Records
Decide whether the drawing layout is:
- the source of truth for annotations;
- a projection of app-owned annotations;
- a collaborative scratchpad attached to another record.
For source-of-truth drawings, canvas persistence may be enough.
For domain-backed annotations, store records in your app and create LivePanels vector items as the canvas projection of those records. Stable item ids are the bridge between the domain row and the committed vector item.
Recovery Checklist
Before production:
- run
mix livepanels.check; - verify layout restore with real drawing payloads;
- test copy/paste of mixed item and vector item selections;
- test undo/redo for create, edit, erase, move, and resize;
- test shared drawing with simultaneous users;
- test import/export with representative Excalidraw files;
- audit image, iframe, and embed source persistence;
- monitor layout persistence failures.
Most drawing recovery bugs come from unstable ids, external asset URLs that no longer resolve, or treating drafts as if they were committed state.