# `MishkaCmsCoreWeb.Live.Admin.Builder.CanvasLive`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core_web/live/admin/builder/canvas_live.ex#L5)

The page-builder canvas: the document itself, rendered inside a same-origin iframe with the
**site's** compiled CSS rather than the admin's.

## Why this is a second LiveView and not a div

Three things fall out of the iframe that no same-document approach can give:

  * **Style isolation without portals.** The site's stylesheet cannot reach the editor chrome and
    the admin's cannot reach the page. The React editor rendered into an iframe through
    `createPortal` and had to bridge native events back across the boundary by hand.
  * **Real viewports.** A tablet frame is an 834px-wide iframe, so the site's own media and
    container queries actually fire. A CSS transform on a div only *looks* narrower.
  * **Ordinary bindings.** Selecting an element is `phx-click`, not a synthetic event forwarded
    across documents.

## Rendering is a function call

A component node renders through `MishkaCmsCore.Runtime.LiveViewHelpers.component/1`, which
`apply/3`s an already-compiled module. There is no `Code.eval_quoted/3` anywhere on this path and
no HTTP round trip per component — the entire Phase-C re-render queue of the React editor exists
because only the server could render a component and the editor was not the server. Here it is.

Slot children become real Phoenix slot entries: `%{__slot__: name, inner_block: rendered}`, which
`Phoenix.Component.render_slot/2` accepts directly. That is what makes the *inside* of a component
editable — the opaque-leaf problem the JSON encoder created, resolved at the ownership boundary.

## Chrome styling

Every piece of chrome this LiveView draws — the empty-page chooser, the floating element toolbar,
the layout panel, the two placeholders — is a CSS rule in one of the nonced `<style>` blocks below,
keyed on an id or a `data-pb-*` marker, not a `style` attribute. A Content-Security-Policy has no
nonce to offer an attribute, so `style-src-attr 'unsafe-inline'` can only be closed by the
attributes not existing; a rule, unlike an attribute, can be marked as ours.

Every selector is anchored to an id, which matters more here than anywhere else in the admin: this
frame is served the AUTHOR's compiled stylesheet, so the chrome and the document being edited share
one cascade. An id-anchored rule outranks any utility that stylesheet contains, which is the
property the `style` attribute used to supply for free.

Two blocks, because Preview keeps half of it. `#pb-canvas-chrome` is switched off in preview — the
hover outline, the selection ring, the chooser and the toolbar are editing affordances — while
`#pb-canvas-ui` holds what is drawn in both modes: the stage itself, the empty-page well, the
expression chip and the "could not render" placeholder.

## The one `style` attribute that stays

`wrapper_style/1` still renders one, on the nodes that need it, and it is not the editor's:

  * the AUTHOR's own `style` attribute, lifted off the node and re-attached to the wrapper. It is
    page content, arbitrary, and unbounded — no finite rule set can express it, and turning it into
    a per-node rule would change where it sits in the cascade, because an attribute outranks every
    selector and a `[data-node-id="…"]` rule does not.
  * `@locked_style` and `@hollow_style`, which are appended AFTER it precisely so they win that
    clash — and which two hook routines (`lift` and `openEmpties` in `admin_hooks.js`) save and
    restore through `el.style`, expecting to find them there.

So the canvas cannot be the surface that closes `style-src-attr`: a document that carries a `style`
attribute anywhere, or a kit component that renders one, puts it back. What this file can promise
is that the EDITOR never adds one. `present/1` returns `nil` rather than `""` and `wrapper_attrs/1`
splats it as a runtime map — which is what actually drops it, since HEEx renders a written-out
`style={nil}` as `style=""` — so a page with no authored styling renders through this canvas with
no `style` attribute anywhere in it.

## Where the CSS comes from

Two sources, and both are needed.

The site's **compiled** bundle is LINKED from `MishkaCmsCoreWeb.AssetsController` when it has a
content hash to address it by, and inlined from `AssetsState` when it does not. Inlining was the
original answer, because a site that is inactive, undeployed or never compiled has no hash — and
that is exactly the site someone opens the editor on. But the bundle is 3.6 MB on a real site, and
inlining put all of it in the document of every preview the examples modal opened: re-sent and
re-parsed each time. Linked, it is brotli-compressed, cached `immutable` for a year, and blocks
rendering — so the frame paints once, already styled, instead of flashing unstyled icons. The hash
is in the URL, so a recompile is a new URL rather than a stale cache. The inline path remains for
the hashless case, which is why both still exist.

Tailwind's **browser** build then generates any utility the moment its class appears in the DOM,
which is what makes an edit visible immediately instead of after a compile. On its own it would not
know the kit's design tokens, so the site's `@theme` source travels with it.

# `literal`

```elixir
@spec literal(String.t()) :: {:ok, term()} | :error
```

The value of an attribute EXPRESSION, when the canvas can resolve it without executing anything.

Public so the kit-wide example sweep can hold the shipped bundle to the same rule the canvas
applies, rather than restating it and drifting.

---

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