Editor surface for Athanor — turn-key LiveView + composable primitives for building consumer page-builders.
Three usage modes
1. Turn-key (use Athanor.Editor.Live)
Consumer module becomes the LiveView. Implement load/3 + save/2,
optionally override render_header/1 and render_top_bar_actions/1.
defmodule MyApp.PageBuilderLive do
use Athanor.Editor.Live,
page_settings_component: MyApp.PageSettings
@impl Athanor.Editor
def load(params, session, socket) do
page = MyContext.get_page(params["id"])
{:ok, %{content: page.content, metadata: page.metadata,
ctx_assigns: %{account_id: session["account_id"]}}}
end
@impl Athanor.Editor
def save(socket, %{content: c, metadata: m}) do
MyContext.save_page(socket.assigns.page, content: c, metadata: m)
end
end2. Composable (build your own LiveView)
Use Athanor.Editor.shell/1 as the layout primitive and fill its slots
with library LiveComponents (Athanor.Editor.Canvas,
Athanor.Editor.ComponentsPanel, Athanor.Editor.ConfigPanel,
Athanor.Editor.ZonePickerModal) or your own widgets.
3. Page-level settings
Pass any Athanor.Component as page_settings_component: and it renders
at the top of the left sidebar via Athanor.AutoEditorForm. Reuses
every field-schema feature (fields/0, resolve_fields,
resolve_data, custom field LCs).
Summary
Callbacks
Load initial editor state. Called by the library during mount/3.
Render the top header bar. Optional — library provides a barebones default. Consumers override for branding (back button, brand logo, page title display).
Render the right-side action area of the top bar (Save button, viewport switcher, etc.). Optional — library provides a default with just Save + viewport.
Persist the current editor state. Called by the library on "save" event.
Hook for consumers to seed extra props when a component is first
added to the canvas. Called as seed_default_props(component, type, socket) immediately after the library builds the new node from
default_props/0. Optional — default no-op.
Functions
Renders the editor canvas — iterates the content tree, wraps each
top-level node with edit chrome (Configure button + selection
border), dispatches per-node rendering via
Athanor.Renderer.node_component/1.
Left-sidebar content. Renders the components palette from
Athanor.Registry.components_metadata/0 and, when
page_settings_component is provided, renders that component's form
via Athanor.AutoEditorForm ABOVE the palette.
Right-sidebar content. Renders the selected component's config form
via Athanor.AutoEditorForm when the selected node's module declares
fields/0. Falls back to the legacy editor_form/0 LC when set,
or to a "no configuration needed" placeholder when neither applies.
Renders nothing when nothing is selected (parent shell hides the
sidebar region in that case).
Layout primitive for the editor — top bar + 3 columns + modal layer.
Floats a modal layer for adding a component into a Columns zone.
Rendered into the shell's :modals slot by consumer LVs when
column_picker is set to {parent_id, zone_name}. On submit emits
"add_component_to_zone" with the parent + zone + chosen type.
Callbacks
@callback load(params :: map(), session :: map(), socket :: Phoenix.LiveView.Socket.t()) :: {:ok, %{content: map(), metadata: map(), ctx_assigns: map()}} | {:error, term()}
Load initial editor state. Called by the library during mount/3.
Return:
{:ok, %{content: tree, metadata: map, ctx_assigns: map}}
or:
{:error, term} to abort mount (consumer can short-circuit via
push_navigate etc. in their own mount/3 before delegating).
@callback render_header(assigns :: map()) :: Phoenix.LiveView.Rendered.t()
Render the top header bar. Optional — library provides a barebones default. Consumers override for branding (back button, brand logo, page title display).
@callback render_top_bar_actions(assigns :: map()) :: Phoenix.LiveView.Rendered.t()
Render the right-side action area of the top bar (Save button, viewport switcher, etc.). Optional — library provides a default with just Save + viewport.
@callback save( socket :: Phoenix.LiveView.Socket.t(), state :: %{content: map(), metadata: map()} ) :: {:ok, any()} | {:error, term()}
Persist the current editor state. Called by the library on "save" event.
Receives %{content: tree_map, metadata: flat_map}. Returns
{:ok, anything} for success (library shows success toast) or
{:error, term} for failure (library shows error toast).
@callback seed_default_props( component :: map(), type :: String.t(), socket :: Phoenix.LiveView.Socket.t() ) :: map()
Hook for consumers to seed extra props when a component is first
added to the canvas. Called as seed_default_props(component, type, socket) immediately after the library builds the new node from
default_props/0. Optional — default no-op.
Use cases: injecting brand_id/account_id into per-page-defaults
for legacy components that read those props at render time.
Functions
Renders the editor canvas — iterates the content tree, wraps each
top-level node with edit chrome (Configure button + selection
border), dispatches per-node rendering via
Athanor.Renderer.node_component/1.
Children of container nodes (Columns zones) get their OWN edit chrome from the container's render(:live, edit_mode=true) — the library Columns renders the per-zone "Add Component" button and per-child Configure button when ctx.edit_mode? + ctx.select_component_callback are set.
Attributes
content(:map) (required) - editor_content map (must have "content" key).ctx(Athanor.Ctx) (required)selected_component_id(:string) - Defaults tonil.viewport(:atom) - :desktop | :tablet | :mobile. Defaults to:desktop.
Left-sidebar content. Renders the components palette from
Athanor.Registry.components_metadata/0 and, when
page_settings_component is provided, renders that component's form
via Athanor.AutoEditorForm ABOVE the palette.
Attributes
ctx(Athanor.Ctx) (required)page_settings_component(:atom) - Defaults tonil.metadata(:map) - Defaults to%{}.
Right-sidebar content. Renders the selected component's config form
via Athanor.AutoEditorForm when the selected node's module declares
fields/0. Falls back to the legacy editor_form/0 LC when set,
or to a "no configuration needed" placeholder when neither applies.
Renders nothing when nothing is selected (parent shell hides the
sidebar region in that case).
Attributes
selected_component_id(:string) - Defaults tonil.content(:map) (required)ctx(Athanor.Ctx) (required)
Layout primitive for the editor — top bar + 3 columns + modal layer.
All slot regions are present in the DOM (with stable testids) so
consumers can mount the same layout shape regardless of which slots
they fill. Slot contents receive a context map with the editor's
current display state (page_title, selected_component_id,
viewport) via :let={ctx}.
Slots
:header— top bar content. Consumer puts back button, title, save button, viewport switcher.:sidebar_left— left panel. Typical content: page settings form (top) + components palette (below).:sidebar_right— right panel. Typical content: selected component's config form (when a node is selected).:modals— floating modal overlay. Library's own zone-picker modal renders here from the consumer LV; consumers can stack additional modals.
Attributes
page_title(:string) - Current page title, forwarded to slots. Defaults tonil.selected_component_id(:string) - Selected node id, forwarded to slots. Defaults tonil.viewport(:atom) - Current preview viewport (:desktop|:tablet|:mobile). Defaults to:desktop.show_components_panel(:boolean) - When false, hides the left sidebar and renders an expand button in the canvas margin. Defaults totrue.
Slots
headersidebar_leftsidebar_rightmodalsinner_block- Canvas region content.
Floats a modal layer for adding a component into a Columns zone.
Rendered into the shell's :modals slot by consumer LVs when
column_picker is set to {parent_id, zone_name}. On submit emits
"add_component_to_zone" with the parent + zone + chosen type.
Attributes
column_picker(:any) (required) - nil OR {parent_id :: String.t(), zone_name :: String.t()}.