LivePanels item interaction has two sides.
The browser owns immediate pointer feel: drag previews, resize previews, marquee rectangles, and local gesture state. The server owns accepted canvas state: final placement, selection, focus, z-order, history, persistence, and shared fanout.
This guide shows the application APIs for normal workspace interaction.
Make An Item Draggable
Put drag_handle_attrs/1 on the element that should start a drag:
<header {drag_handle_attrs(class: "item-titlebar")}>
{context.item.label}
</header>The application chooses the titlebar markup. The attrs tell the browser runtime that this element is a drag handle for the containing item frame.
During a drag, the browser previews movement locally. On release, it sends a placement command. The server validates and accepts or rejects the final frame.
Make An Item Resizable
Use the standard resize handles:
<.resize_handles :if={context.view.resizable?} />Or render one handle yourself:
<button {resize_handle_attrs(:se)} aria-label="Resize" />Read context.view.resizable? instead of rechecking item capabilities in the
application. The render view already answers whether resize should be offered.
Move Or Resize From Server Code
Use LivePanels.Canvas.Item when an application event or app process changes item
placement:
socket = LivePanels.Canvas.Item.move_by!(socket, "note-1", {24, 0})
socket = LivePanels.Canvas.Item.move_to!(socket, "note-1", {120, 80})
socket = LivePanels.Canvas.Item.resize_by!(socket, "note-1", {40, 20})
socket = LivePanels.Canvas.Item.resize_to!(socket, "note-1", {420, 300})For exact placement, use place/3:
socket =
LivePanels.Canvas.Item.place!(socket, "note-1", %{
space: :world,
anchor: :top_left,
x: 120,
y: 80,
w: 420,
h: 300
})Do not update item placement by assigning your own item list. Commands are what connect validation, history, persistence, shared mode, and projection.
Select Items
Use selection attrs for browser controls:
<button {select_item_attrs(item)}>Select</button>
<button {toggle_item_selection_attrs(item)}>Toggle</button>
<button {clear_selection_attrs()}>Clear</button>
<button {select_all_attrs()}>Select all</button>Use selection commands from server code:
socket = LivePanels.Canvas.Selection.select!(socket, "note-1")
socket = LivePanels.Canvas.Selection.include!(socket, ["note-1", "note-2"])
socket = LivePanels.Canvas.Selection.toggle!(socket, "note-1")
socket = LivePanels.Canvas.Selection.clear!(socket)Selection is actor-scoped. In a shared canvas, each participant can have a different selection unless your app chooses to coordinate selection in product state.
The render context exposes selection for the current actor:
<section class={["item-frame", context.selected? && "is-selected"]}>
{context.content}
</section>Use Marquee Selection
Put marquee attrs on the world or viewport region where the gesture should start:
<div {marquee_selection_attrs(:world, class: "selection-plane")}></div>The browser renders the marquee gesture. The server accepts the resulting selection command.
Use graph-specific selection helpers when the marquee should include connectors as well as items.
Focus Items
Focus is the current actor's active item and usually promotes the item in the canvas stack. It is separate from selection.
socket = LivePanels.Canvas.Item.focus!(socket, "note-1")The slot item exposes focus:
<article class={["item-frame", context.focused? && "is-focused"]}>
{context.content}
</article>Use focus_exempt_attrs/1 on controls inside an item frame that should not
change item focus when clicked:
<button {focus_exempt_attrs()} phx-click="open-menu">Menu</button>Set interaction: %{promote_on_focus: false} on item types or item
creation opts when focus should preserve the current stack order. The item still
becomes active, selection helpers still work, and focus-loss attachments still
dismiss; only automatic z-order promotion is skipped. Use the z-order commands
below when the user explicitly reorders layers.
Reorder Selected Items
Use z-order commands for selected items:
socket = LivePanels.Canvas.Selection.z_order!(socket, :front)
socket = LivePanels.Canvas.Selection.z_order!(socket, :back)
socket = LivePanels.Canvas.Selection.z_order!(socket, :forward)
socket = LivePanels.Canvas.Selection.z_order!(socket, :backward)From markup:
<button {z_order_selection_attrs(:front)}>Bring front</button>
<button {z_order_selection_attrs(:back)}>Send back</button>LivePanels keeps z-order in canvas state so projection, persistence, shared mode, graph connectors, and vector items can stay consistent.
Align, Distribute, And Layout
Selection layout helpers operate on visible world-space items:
<button {align_selection_attrs(:left)}>Align left</button>
<button {distribute_selection_attrs(:horizontal)}>Distribute</button>
<button {layout_attrs(:grid)}>Grid</button>
<button {layout_attrs(:skyline_pack)}>Compact</button>Server equivalents:
socket = LivePanels.Canvas.Selection.align!(socket, :left)
socket = LivePanels.Canvas.Selection.distribute!(socket, :horizontal)
socket = LivePanels.Canvas.Arrange.layout!(socket, :grid)
socket = LivePanels.Canvas.Arrange.layout!(socket, :skyline_pack, preserve_size: true)These are still canvas commands. They do not bypass policy, history, or shared coordination.
Canvas layout strategies are headless planning contracts. Canvas.Arrange
submits commands, while Canvas.Layout exposes planning adapters for external
engines:
socket = LivePanels.Canvas.Arrange.layout!(socket, :masonry, scope: :visible, gap: 12, columns: :auto)
socket = LivePanels.Canvas.Arrange.layout!(socket, :master_stack, master: :active, ratio: 0.62, stack: :right)
socket = LivePanels.Canvas.Arrange.layout!(socket, :ordered_treemap, weight: :priority)
adapter = LivePanels.Canvas.Layout.adapter(state, :visible, viewport: viewport)
commands = LivePanels.Canvas.Layout.adapter_commands(state, external_result)
strategies = LivePanels.Canvas.Layout.registered_strategies(state)Built-in strategy keys include :horizontal, :vertical, :grid, :cascade,
:masonry, :fit_rows, :fit_columns, :skyline_pack, :master_stack,
:bsp_balance, :bsp_split, and :ordered_treemap. Browser controls send the
registered strategy key plus params. Server code may also pass a module that
implements LivePanels.Canvas.Layout.Strategy; browser payloads cannot name
modules. Custom browser-visible strategy keys come from canvas extensions:
LivePanels.Canvas.Extension.new!(:workflow,
authorize: MyApp.CanvasAuthorize,
apply: MyApp.CanvasApply,
commands: %{},
layout_strategies: %{workflow_board: MyApp.WorkflowBoardLayout}
)Keyboard Scope
Put keyboard_scope_attrs/1 on the element that should receive canvas keyboard
behavior:
<main {keyboard_scope_attrs(class: "board-page")}>
...
</main>Use focus exemptions for inputs, menus, and controls where ordinary browser or app keyboard behavior should win.
Input Policy
Use one runtime input policy when an app needs to change wheel, pointer, or keyboard behavior:
input = %{
wheel: %{bare: :z, shift: :x, command: :y},
drag: %{
primary: :mode,
middle: :pan,
secondary: :host,
space_primary: :pan,
space_middle: :pan,
space_secondary: :pan
},
keyboard: %{
nudge: %{base: :snap_grid, large_multiplier: 4},
overrides: %{
"Space" => :temporary_pan,
"+" => :zoom_in,
"-" => :zoom_out,
"0" => :reset_zoom
}
}
}
<.canvas_runtime {canvas_runtime_attrs(assigns, input: input)} />For markup composition, emit the same contract as flat attrs:
<.canvas_runtime
{canvas_runtime_attrs(assigns)}
{input_attrs(
wheel_bare: :z,
wheel_shift: :x,
wheel_command: :y,
drag_secondary: :pan,
keyboard_nudge_large_multiplier: 4
)}
/>Wheel dimensions are :x, :y, :z, :none, and :host. :z zooms around
the pointer, :x and :y pan the camera, :none consumes the gesture, and
:host leaves it to application code. The shipped wheel mapping is bare :z,
Shift :x, and Ctrl/Cmd :y.
Pointer drag actions are :mode, :pan, :none, and :host. The shipped drag
mapping is primary :mode, middle :pan, secondary :host, and Space plus any
of those buttons :pan. :mode follows the active cursor mode so draw, erase,
marquee, item drag, resize, and drop interactions keep priority over camera pan.
Keyboard overrides replace or add chords by public runtime action name. The
runtime ships Escape cancel, Ctrl/Cmd undo-redo-copy-cut-paste-duplicate-select,
Delete/Backspace remove, arrow-key nudging, V/H/M select-pan-marquee,
Space temporary pan, +/-/0/Home/F/Shift+F camera actions, bracket z-order
actions, and the drawing tool keys 1 through 9, P, Shift+P, A, L,
R, O, D, and E.
The JS entrypoint exports LIVEPANELS_INPUT_DEFAULTS for hosts that need to
inspect the shipped keyboard.shortcuts and keyboard.reservedButUnbound
tables before applying overrides.
What LivePanels Owns
LivePanels owns the accepted item frame, selection set, focus target, z-order, and command validation.
The browser owns transient previews. Your application owns where handles and controls appear and how focused or selected items look.