# `Athanor.Editor`
[🔗](https://github.com/Arsenalist/athanor/blob/v0.1.0-beta.7/lib/athanor/editor.ex#L1)

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 `c:load/3` + `c:save/2`,
optionally override `c:render_header/1` and `c: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
    end

### 2. 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).

# `handle_asset_request`
*optional* 

```elixir
@callback handle_asset_request(
  socket :: Phoenix.LiveView.Socket.t(),
  request :: Athanor.Editor.AssetRequest.t()
) :: {:noreply, Phoenix.LiveView.Socket.t()}
```

Handle a request for an asset from an `:asset` field. Optional — when a
consumer does not implement it, the request is a no-op (the field's
paste-a-URL default still works).

Athanor builds an `%Athanor.Editor.AssetRequest{}` from the field
declaration + current value and hands it over. The consumer opens whatever
picker it wants (typically rendered via `c:render_outlet/1`) and, once an
asset is chosen, writes the result back via the existing
`{:update_component_props, node_id, props}` message. Athanor never performs
the upload or browse itself.

Return `{:noreply, socket}`.

# `load`

```elixir
@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).

# `render_header`
*optional* 

```elixir
@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).

# `render_outlet`
*optional* 

```elixir
@callback render_outlet(assigns :: map()) :: Phoenix.LiveView.Rendered.t()
```

Render arbitrary consumer-owned chrome into the editor's overlay layer.
Optional — library default renders nothing.

This is a **general** render outlet, not a modal-specific hook: its output
is placed into the shell's `:modals` slot, which is the editor's
fixed-position overlay layer. The consumer decides the form — a modal,
drawer, toast, slide-over, or an offscreen `<input type=file>`. Because
those are all `position: fixed`/`display: none`, DOM location is
irrelevant, so one outlet serves them all.

The asset picker opened in response to `c:handle_asset_request/2` is the
primary consumer of this outlet, but it is not asset-specific.

Receives the full LiveView assigns map (parity with `c:render_header/1`).

# `render_top_bar_actions`
*optional* 

```elixir
@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.

# `save`

```elixir
@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).

# `seed_default_props`
*optional* 

```elixir
@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.

# `canvas`

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 to `nil`.
* `viewport` (`:atom`) - :desktop | :tablet | :mobile. Defaults to `:desktop`.

# `components_panel`

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 to `nil`.
* `metadata` (`:map`) - Defaults to `%{}`.

# `config_panel`

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 to `nil`.
* `content` (`:map`) (required)
* `ctx` (`Athanor.Ctx`) (required)

# `shell`

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 to `nil`.
* `selected_component_id` (`:string`) - Selected node id, forwarded to slots. Defaults to `nil`.
* `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 to `true`.
## Slots

* `header`
* `sidebar_left`
* `sidebar_right`
* `modals`
* `inner_block` - Canvas region content.

# `zone_picker_modal`

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()}.

---

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