Escape

The editor

Intent

<quario-editor> is a banded document designer whose design surface is the same layout the viewer paints. It takes your instance and your definition, and returns the edited definition on every change. Reach for it when your users should author their own documents rather than receive yours.

Design decisions

The editor takes your instance rather than making one. Locale, currency, timezone, query budgets, and the license all stay yours. A document authored in the editor compiles the same way it will in your service.

Every change carries a plan, not only a schema. A change event returns the edited definition together with the problems the same traversal found. One descent serves both persisting and reporting, which is what plan exists for.

The design surface is the layout, not a second renderer. What an author drags is the same display list the PDF target writes, so what they arrange is what will print.

Undo is a snapshot stack. The editor keeps whole definitions rather than inverse operations. A definition is small, plain JSON, and a snapshot can’t drift from what an inverse operation meant to undo.

Span coverage holds by construction. A total row must cover its columns exactly once. The editor maintains that as an invariant rather than letting an author reach a state the engine would reject.

A failed render resolves the mount. The element reports through an error event and stays mounted, the same contract the viewer honours.

An editor you insert again renders once more, unless its last render landed on the sheet. Two cases leave the render owed. One is a property written while the element was out of the document. The other is a document that compiled to nothing because it still has problems. A document you’re typing into is the ordinary case of one that doesn’t yet compile. The debt can therefore stand for a stretch of an editing session, where a run is the plan alone and nothing renders or paints.

API walkthrough

Mounting

Import @quario/editor/register to define the element, or import QuarioEditor and define it yourself. Set properties as element properties, since an instance and a registry aren’t strings.

editor.ts
import '@quario/editor/register'
import type { EditorPage } from '@quario/editor'

const el = document.createElement('quario-editor')

const page: EditorPage = { size: 'A4', margin: 54 }

el.schema = definition
el.instance = q
el.functions = {}
el.data = data
el.page = page

document.body.append(el)

instance is a quario() you made. functions is the registry the definition may call. It should be the registry your service will compile with, or an author will build a document that calls something you don’t have.

data is sample data for the preview. It never leaves the browser and it isn’t part of the definition.

Handling a change

editor.ts
const el = document.createElement('quario-editor')

el.addEventListener('change', (event) => {
  const { schema, problems, warnings } = event.detail
  if (problems.length === 0) save(schema)
  else showProblems(problems)
  for (const w of warnings) console.warn(w.path, w.message)
})

problems is the same structured list plan returns, each with a path, a message and the author source behind it. Persist on an empty list, and surface the rest against the nodes they name.

The event also carries warnings, the advisory list the rail shows as Issues beside the problems. A document carrying only warnings compiles, so a save button driven by problems.length should stay driven by it.

Don’t re-validate what the editor gave you. The plan came from the traversal that produced the change.

Reacting to failure

error carries { error, kind }, where kind is mount-render before any render lands, update-render after, and host-option for a property the editor rejected.

renderComplete resolves once the pages within reach have painted, not every page. A host that reads pixels off a page far down the document finds it blank until it’s scrolled to.

Anchoring your own UI to a node

boxes(path) answers every rectangle the render drew for a schema node, in document order, as client rectangles. Use it to attach your own UI to a node: a badge, a comment marker, a step in a product tour. It answers for every page of the document, whether or not that page has an element yet.

It answers an empty array for a node that draws nothing, for a group, and for a path the schema doesn’t have. A render that hasn’t landed answers empty too. Await renderComplete first.

Reaching the document and the bands off the page

The rail opens with a Document panel. It sits above Properties, and it shows whether or not you have selected anything. The report default belongs to the document rather than to any band, so no selection on the sheet could reach it. The panel offers exactly family and size, with the same controls, fx toggle, and inline errors every other field has. It writes the document root, so the preview repaints at the new base as you type.

That panel also lists page.header, page.footer and empty. None of the three renders on the preview. Only a paginating target draws a page band, and empty renders only over a dataset with no rows. So until now the editor preserved them and offered no way in. Selecting one opens a panel holding its items, with the verbs that add, remove, and reorder them. The breadcrumb leads back up from an item. They’re drop targets like any other band, under the same anchor rule: a page band binds page, and empty binds nothing row-scoped.

A run or a cell that may carry a link gets an href field. Its placeholder is where an author meets the #label form for an internal target. A table row withholds it, as it withholds every declaration that presents a value.

Properties groups the styles a node may carry into Text, Alignment, Fill, Format, Link, Spacing, Padding, and Border. A group shows only what the selected node declares, and its + shows the rest. Padding draws as a grid of four sides, and a border as a table of sides by width, line, and colour. A group stays open as you select another node, so you can compare two nodes side by side.

Every style control that un-declares by emptying carries a × beside it: a number, a colour, a currency code, a text field, an expression. The × appears only while the document declares that name. Its presence means one thing everywhere: the document declares this name, and clearing it restores the layer beneath. A select keeps its blank option and gains no ×.

Fonts, chrome, and language

fonts takes the same record the viewer and the PDF target take. Pass the identical object to all of them, and keep it across renders.

colorScheme takes 'light', 'dark' or 'auto' and reaches the chrome. The sheet stays the document’s, as it does in the viewer.

strings works as the viewer’s does, key by key, with lang set to match. It changes the chrome’s words only. The Issues list keeps the engine’s messages, and a field keeps its schema name, because an author writes those names into the definition.

Where the boundary sits

An author using the editor is inside the trust boundary. They’re writing a definition, and a definition is configuration you trust. If the people editing aren’t the people you would let write a config file, the editor isn’t the control that changes that. Trust and CSP says what that boundary does and doesn’t claim.