---
title: "Changelog — quario"
description: "Every published release of the 10 quario packages the reference documents, read from the same install the documentation is built from."
url: "https://getquario.com/changelog"
---

## CURRENT VERSIONS

|                           |                          |
| ------------------------- | ------------------------ |
| [quario](#quario)         | [0.15.0](#quario-0-15-0) |
| [@quario/html](#html)     | [0.13.1](#html-0-13-1)   |
| [@quario/pdf](#pdf)       | [0.14.0](#pdf-0-14-0)    |
| [@quario/xlsx](#xlsx)     | [0.12.1](#xlsx-0-12-1)   |
| [@quario/csv](#csv)       | [0.10.2](#csv-0-10-2)    |
| [@quario/docx](#docx)     | [0.5.1](#docx-0-5-1)     |
| [@quario/viewer](#viewer) | [0.15.0](#viewer-0-15-0) |
| [@quario/editor](#editor) | [0.10.0](#editor-0-10-0) |
| [@quario/layout](#layout) | [0.11.0](#layout-0-11-0) |
| [@quario/mcp](#mcp)       | [0.2.1](#mcp-0-2-1)      |

## quario

0.15.0

### quario 0.15.0

### Minor Changes

- A group aggregate that reads another aggregate of the same group now causes a render error, also when the group declares `take`. Before, a group with `take` gave such an aggregate a value that changed with the order of the declarations. For example, `{ "a": "sum:=@.n", "b": "sum:=g.a" }` gave `b` the total of `a`, and the same two keys in the other order gave `b` the value 0. A group without `take` already caused this render error. To share a calculation between two aggregates, write it in each aggregate.
- The engine exports `pageBox(page, at)` and `paper(page, opening, at)`, the rules every target with a page follows. `pageBox` checks a host’s `page` option. `paper` says which size, margin and orientation are in force for a render, and the page box they come to. `@quario/layout` and `@quario/docx` now use them, and their behaviour does not change.
- `plan()` and the compiled report now carry `readPaths`. It maps each compiled source to the member paths it reads off its anchors and group handles, such as `["@", "qty"]` or `["$", "input", "customer"]`. A host can check a sample or a data feed against a definition before it renders. A path ends in `null` where the source reads further through a key the definition does not name.
- `plan()` now returns `uses`: a frozen map from each declaration a target’s `capabilities` descriptor answers for to the schema paths where the document writes it. Join it with any descriptor to see every withdrawal, unread band and approximation a document meets on that target. It needs no option, adds no problem, and is present while the document has problems.
- A release refuses a revoked license key. A refunded or reversed payment voids a key, and each release carries a list of the voided keys. A listed key gets the marking, and the warning names the revocation. A version that is already installed keeps its answer. If you see this warning for a key that you paid for, contact us through getquario.com.

### Patch Changes

- `plan()` and `validate()` refuse a lead on the first row of a grid after a height-declared header. Before, the plan reported no problem, and the render then failed on the same lead. A row that the document hides outright is not the first row, so the rule reads the next one.

  The plan reports an item that is not an object after a height-declared header as a problem. Before, the plan failed with a `TypeError`.

  A slot that renders nothing carries its `path`. Before, an image slot in a split or a grid had no `path` when its `source` was nullish or its `visible` was false.

0.14.0

### quario 0.14.0

### Minor Changes

- **`quario(options)` now refuses an option it does not understand, like every target factory.** Before, the engine read the keys it knew and ignored the rest. A typo such as `quario({ licence })` therefore marked every render as unlicensed, and nothing said why. Now an unknown key, a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the call. The message names the option path, such as `options.license`.

  **`locale`, `timeZone` and `currency` are now checked at the call.** Before, a value that `Intl` refused did not fail. Every formatted cell showed its raw value instead. Now `quario({ locale: "en_US" })` and `quario({ timeZone: "Europe/Amsterdm" })` throw a `TypeError`. `currency` must be three upper-case letters, the same rule that a cell’s own `currency` declaration follows. A cell’s own `currency` still fails soft.

  **The `query` budgets are now checked at the call.** Before, the engine checked them only when a report compiled its first query. A report with no query never reported a bad budget. The error still comes from padvinder, in its own words.

  **`plan(schema, functions, options)` now refuses an unknown key in `options`.** Before, `{ target: [...] }` checked nothing and said nothing. A `targets` that is not an array also throws a `TypeError`.

  **A bad `schemes` allowlist now throws a `TypeError`, not an `Error`.** The message starts with `options.schemes:`, not `schemes:`. A `null` allowlist still throws.

  **What this changes for you.** An options object that carries keys for another consumer stops working when you pass it to `quario()`. Pass `quario()` only the six keys it takes: `query`, `license`, `locale`, `currency`, `timeZone` and `schemes`.

- **A group can now order and cut its instances.** Declare `instances` beside `sort`, with an optional `sort` and an optional `take` inside it. They order and cut the group’s instances, as the group’s own `sort` and `take` order and cut the rows inside each instance. Each `instances.sort` key reads the group’s own handle once per instance, so `instances: { sort: [{ by: "=region.subtotal", dir: "desc" }], take: 5 }` keeps the five regions with the largest subtotals, largest first. `@` and `run` are unbound there. A dropped instance never renders, and no running value steps over it. The totals around the group still count it, as they count a row that a group’s `take` drops. A group without `instances` renders every instance in first-seen order, so existing reports render as before.

- **An image item now accepts SVG bytes.** Before, `source` had to yield PNG or JPEG bytes, and SVG bytes were a render error. Now the engine also names UTF-8 text whose root element is `<svg` as an SVG, and the image event carries `format: "svg"`. The engine reads the size from the root’s `width` and `height` when both are absolute lengths, and else from its `viewBox`. An SVG that states no size this way is the same render error as a PNG that is too short to state its size. The message for bytes that are not an image now says “PNG, JPEG or SVG”.

  The HTML target puts an SVG in an `<img>` under `data:image/svg+xml`, so the SVG never becomes markup in the fragment. The PDF target draws an SVG as vector paths. It draws no picture that the SVG names, and it fetches nothing. SVG `<text>` draws in the standard Helvetica. The viewer paints an SVG through a `data:` URI, so a page that shows one needs the Content Security Policy grant `img-src data:`. A PNG or a JPEG still needs no grant there. The XLSX and DOCX targets cannot draw an SVG, so an SVG image is a render error that names the item.

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.

0.13.0

### quario 0.13.0

### Minor Changes

- A band gains a `grid` item: splits that share one column geometry.

  A statement is twenty lines of a label and an amount, on the same two widths. As splits, each line restated its widths:

  ```jsonc
  { "type": "split", "slots": [
    { "type": "text", "value": "Opening", "width": 70 },
    { "type": "text", "value": "{{ @.opening }}", "width": 30, "style": { "align": "right" } }
  ] }
  ```

  A grid states the widths, and each column’s style, once:

  ```jsonc
  {
    "type": "grid",
    "columns": [{ "width": 70 }, { "width": 30, "style": { "align": "right" } }],
    "rows": [
      { "slots": [{ "type": "text", "value": "Opening" }, { "type": "text", "value": "{{ @.opening }}" }] },
      {},
      { "slots": [{ "type": "text", "value": "Total" }, { "type": "text", "value": "{{ @.total }}" }], "style": { "bold": true } }
    ]
  }
  ```

  A row is a split without `type`, and it keeps every rule a split has. A column’s `style` is the layer under each of its slots, and a slot’s own declarations win. A row that names fewer slots than there are columns leaves the rest empty, so `{}` is a spacer line and a one-slot row is a label-only line.

  Each row reaches a target as the split it is, so the event stream does not change and no target needed an update. No existing report changes.

- The package now ships a JSON Schema of the report document.

  The file is `lib/schema.json`, and the package exports it as `quario/schema.json`. Point `$schema` at the installed copy, and an editor checks the document and shows the documentation for each key:

  ```jsonc
  { "$schema": "./node_modules/quario/lib/schema.json", "data": "$.orders[*]" }
  ```

  A report document accepts one new top-level key, `$schema`. Its value must be a string. The engine checks the type and reads nothing else, so a document keeps its `$schema` through an upgrade. Anywhere below the top level, `$schema` stays an unknown key.

  The JSON Schema proves the shape of each value, and `validate()` still proves the document. A document can pass the JSON Schema and fail `validate()`: a rule across two values, a reserved name, or an expression that does not compile. The strict structured-output modes of OpenAI and Anthropic do not accept the file, because a report document leaves most of its keys out.

  The TypeScript declarations are stricter in one place. A parameter’s value under `params` is now typed as any JSON value except a plain object. The engine always read a plain object there as a declaration, so a document that the new type refuses was never the document it looked like.

- **A license key has an end date and no start date.** A key covers every version released on or before its `to` date. Before, a key also named a `from` date, and a version released before that date stayed marked. The engine now ignores a `from` in a key. When a key ends before the release date of the installed version, the warning names both dates.

- A value the runtime has no text form for is now a located render error rather than `[object Object]` in the document.

  A cell reaches its reader through `String(value)`. A plain object, a `Map` and a `Set` have no text form of their own, so that falls back to a tag: `[object Object]`, `[object Map]`, `[object Set]`. A tag lays out, paginates and lands in a worksheet cell, and the document is wrong. A few values have no text form at all, and `String()` throws on them — an object with no prototype, a boxed symbol. The engine now refuses both kinds and names the cell:

  ```
  detail[0].value [{{ @.address }}]: a value with no text form, which would print as [object Object]
  ```

  The rule reads the value, not its source, so a registered function’s result and a data field answer to it alike. A value that says what it is still prints it: a class with its own `toString`, an array (`[1, 2]` prints `1,2`), a `Date`. An absent read still prints nothing. A computed property is outside the rule, because a group’s `by` is an identity rather than text — `by: "=@"` makes each row its own instance — and a `where` and a `visible` are read for truth.

  Every entry in the function registry must be a function. `plan` reports one that is not at `functions.<name>`, where before it raised an unhandled `TypeError` out of `plan` itself at a caller who had only asked a question.

  A report that printed `[object Object]` stops rendering. It was printing a value no reader could use.

- A document may now name its paper and turn it, and its margin may name each side.

  ```jsonc
  {
    "page": {
      "size": "letter",
      "orientation": "landscape",
      "margin": { "top": 36, "right": 29, "bottom": 36, "left": 29 }
    }
  }
  ```

  `page.size` takes `"A4"` or `"letter"`. A `[width, height]` pair stays a target option. `page.orientation` takes `"portrait"` or `"landscape"`, and it turns whichever paper is in force. `page.margin` still takes one number for all four sides.

  The rule `page.margin` already followed now covers all three keys. The document’s key wins where it declares one, a target option applies where it does not, and both declaring the same key is a render error. `orientation` in a document beside a target’s `size` is not both.

  Each target writes the paper: the PDF and the layout draw on it, HTML writes an `@page` size and per-side margin, and DOCX writes `pgSz` with `w:orient` and `pgMar` per side. XLSX writes the paper code, the orientation and each print margin. CSV withdraws all three.

  The capability descriptors gain `page.size` and `page.orientation`, and both are names a document may mark `required`.

  A target’s margin error now reads `expected a non-negative number, or { top, right, bottom, left }, that leaves the page room`.

- `params` is now what the report declares it needs, and a render may supply one.

  A bare value under `params` still **is** the parameter’s value, so a document of constants reads the way it always did:

  ```jsonc
  { "params": { "year": 2025, "branded": true } }
  ```

  A plain object is a **declaration** instead, closed at `default` and `type`:

  ```jsonc
  {
    "params": {
      "year": { "type": "number", "default": 2025 },
      "region": { "type": "string" }
    }
  }
  ```

  `type` names a JSON kind — `string`, `number`, `boolean`, `array`, `object` — because a parameter is a JSON value. There is no `date` and no `enum`.

  **`default` absent makes the parameter required.** The engine rejects a render that supplies no value for it, before any expression runs. That is the main thing declaring buys: a report that states what it needs hears about it when a render does not bring it.

  A host supplies values as the last argument to `render` and to `stream`:

  ```js
  await report.render(html(), data, { region: "EU" });
  ```

  Only a declared key may be supplied, a supplied value answers to the declared `type`, and values stay JSON and deep-frozen — now per render rather than once at compile.

  **One kind of document changes meaning.** The engine reads an object-valued parameter as a declaration, so a document that wrote one for its value wraps it:

  ```jsonc
  { "params": { "config": { "default": { "retries": 3 } } } }
  ```

  An empty object is the sharpest case. `{ "config": {} }` now declares a parameter with no default, so every render of that document is refused until one supplies a value, or until the document wraps it.

  Nothing else moves. A bare string, number, boolean or array is untouched.

  A declaration may also carry a `label`, the text a person reads for the parameter. A labelled parameter is one a person supplies, so a host panel shows only those. The engine never renders a label.

  A `null` default beside a `type` makes a typed parameter optional, and a render may then supply `null`:

  ```jsonc
  { "params": { "branch": { "type": "string", "default": null, "label": "Branch" } } }
  ```

  A host reads what a document declares from `report.params` and from `plan(...).params`: one frozen `{ name, required, type?, default?, label? }` per parameter, in declaration order. A plan carries them whether or not the document compiled, so an editor can render a panel beside the problems.

0.12.0

### quario 0.12.0

### Minor Changes

- A registered function may be asynchronous. The engine resolves every such call before the banded walk, in one wave, and the walk itself stays synchronous.

  Write the call as the whole value of a cell in a detail row, and take its arguments from the row:

  ```jsonc
  { "data": "$.orders[*]", "detail": [{ "type": "text", "value": "{{ customerName(@.customerId) }}" }] }
  ```

  The engine calls the function once for every row it renders, starts every call together, and waits for all of them before the first event reaches a target. A row that `where` or `take` removed is never fetched. Deduplication and any limit on how many calls run at once belong to the function itself: a report over ten thousand rows starts ten thousand calls.

  Three rules hold. The call is the whole value of a cell, so `{{ lookup(@.id).name }}` and `{{ "#" + lookup(@.id) }}` are errors that name the function and the cell. The arguments come from the row, so a cell that reads no row cannot hold one. And `render()` resolves, while `stream()` reports the call it cannot wait for.

  A function that rejects fails the render with an error that names the function and the cell, and keeps the class the function threw.

### Patch Changes

- **The declarations now refuse what the engine refuses.** Eight style declarations type-checked at nodes the engine rejects them on, so a report that compiled could still fail to render: `valign` on a stacked item, flow spacing on a table cell or a total cell, and `format`, `currency`, `href` and flow spacing on a row box. Each site now states the narrowing the engine has always applied, through four new exported types — `StackedStyleDeclarations`, `CellStyleDeclarations`, `RowStyleDeclarations`, and the `SlotStyleDeclarations` that was already there.

  No document that the engine accepts has stopped type-checking. The narrowing removes only names the engine was already refusing.

  **`SCHEMA.md` now states two keys it always admitted.** The document-shape block never listed `required`, and the group-definition block never listed `collapsed` — the latter while three target sections linked to that section for it, and while the section said a group is collapsible without a field for it. Both are stated now, `collapsed` with the paragraph it never had.

- **The declarations now carry `required`, and a group’s `label` and `collapsed`.** `ReportSchema` states `required`, and `Group` states both `label` and `collapsed`. The engine has admitted all three and `SCHEMA.md` specifies them, so a TypeScript author marking a declaration required, naming an instance for navigation, or opening a group shut was told it was an error and had to cast.

  `required` is keyed by a new exported type, `RequirableName`, which spells the same names `REQUIRABLE` carries. A marking no target could answer for is therefore an error where an author writes it, rather than a `plan` problem after the fact — `{ "negative": true }` names a modifier inside `format` rather than a declaration a target answers for, and the compiler now says so. `collapsed` reads `boolean`, because the engine accepts `false` for it as plainly as `true`.

0.11.1

### quario 0.11.1

### Patch Changes

- **The declarations now carry `href` and `schemes`.** `StyleDeclarations` states `href`, `RunStyleDeclarations` picks it up as the twelfth inline name, and `QuarioOptions` states the `schemes` allowlist beside the three names that present `format`. The engine has admitted both since 0.11.0 and `SCHEMA.md` specifies them; the published types said otherwise, so a TypeScript consumer writing a link, or the allowlist that admits one, was told it was an error. The `format` declaration’s own comment said a kind carries one modifier, which stopped being true when a number kind took three. `test/types.check.ts` exercises the allowlist and both spellings of a link, since a hand-written surface drifts exactly where nothing reads it.

0.11.0

### quario 0.11.0

### Minor Changes

- **`isBoxName(name)` is exported.** It answers whether a style name is part of the box — a padding or a border part — which is the partition the spec defines a table row’s box by. A target that strips the box off a style, or a tool that lists the box names, imports it beside `STYLE_NAMES` rather than matching the two prefixes itself.
- **A number kind reads `negative` and `zero`.** `format` gains two modifiers on `number`, `currency` and `percent`: `negative` is `"minus"` or `"parens"`, and `zero` is `"number"` or `"dash"`. Under `parens` a negative reads as its magnitude in parentheses, and under `dash` a zero reads as `-`. Both defaults are what every kind presented before, so no existing report moves. They compose with `digits`, with the percent kind’s scaling and with a currency’s code. A value outside either pair is a located definition error, and `date` takes neither. The resolved declaration a cell carries now states all three of a number kind’s modifiers, so no consumer defaults one for itself.
- **A group may declare `collapsed`.** One optional literal, `true` or `false`, asking that the instance open **shut**: its header and footer are shown and the content between them is not. Aggregates are untouched — the rows stay in the partition and still count, so a footer totals what nobody is shown. The `group-start` event carries it, and the new `collapse(stream)` helper is what a target with nowhere to hide a row reads the stream through: it drops a collapsed instance’s content, nested instances whole, and keeps its brackets and its own bands. An expression is a located definition error.
- **A group may declare a `label`.** One optional template beside `name` and `by`, resolved once per instance in group scope — the instance’s own handle and `$` are bound, `@` is unbound as in any group band — and carried on `group-start` as the resolved text. It names the instance for navigation rather than for the page: a target with somewhere to put a navigation name spends it there, and the others withdraw it, so no page changes. A value that is not a template string is a located definition error, and a hollow instance stays unlisted whether it declares one or not.
- **A run may carry an `href`.** The twelfth name in the styled run’s inline subset, and the one declaration this engine judges rather than passes on: a URL names a location, and `javascript:` is a string-to-code path that escaping at the markup edge cannot answer. A URL is admitted before it crosses, against a host allowlist — `quario({ schemes })`, defaulting to `https:` and `mailto:` — so a target receives a URL already admitted or none at all. A refused value is omitted, so the text renders and the link does not; a URL stating no scheme is refused with the rest, because a scheme list cannot say “no scheme” without admitting `//host/path`. An `href` beginning `#` names a group’s `label` and needs no allowlist. A `schemes` option the engine cannot read throws at construction rather than falling back to the default, because an allowlist quietly replaced is a host believing it restricted something it had not. A table row refuses `href`, as it refuses every declaration that presents a value.
- **`REPORT_STYLE_NAMES`.** The two names a report default may declare, `family` and `size`, in the vocabulary’s own order — the same export `RUN_STYLE_NAMES` already is, so a tool that offers them reads the engine rather than keeping a copy.
- **A document may mark a declaration `required`, and `plan` checks it against the targets you name.** `"required": { "uppercase": true }` at the report root says which declarations this document cannot do without; `plan(schema, funcs, { targets })` takes the `capabilities` descriptors of the targets it is meant for, and every required declaration one of them withdraws or leaves unread becomes a problem of its own at `required.<name>`, which nulls the report as any other problem does. An approximated declaration is not one. Every withdrawal was silent by design before this, so this is how an author says that one of them matters. Additive throughout: no target sees the marking, no render changes because of it, and passing no descriptors checks nothing. A name outside the set the support matrix answers for is a definition error, because a marking nothing can check would go quiet exactly when it was needed. `REQUIRABLE` exports that set.

### Patch Changes

- **The declarations now state every `format` modifier the engine settles.** `FormatDeclaration` and `ResolvedFormat` carry `negative` and `zero` beside `digits` and `form`, and `FORMAT_VOCABULARY` offers the two readings and their defaults — `negatives`, `zeroes`, `defaultNegative`, `defaultZero` — with `modifiers` naming every modifier a kind takes rather than one of them. The engine has always read and resolved those declarations; the types said otherwise, so a TypeScript consumer writing one was told it was an error, and a tool reading the vocabulary to offer them found only the count.

- **An error quario replaces keeps the one it replaced, as `cause`.** A failure inside one of your own functions reaches you named where it happened — `detail[0].value [{{ total() }}]: ...` — which is a copy, rebuilt from the class and a prefixed message. The stack that says which line of your function threw used to go with the original. It now rides along: read `error.cause` for the throw as it happened, and the message for where quario was when it happened.

  The viewer’s and the editor’s own property checks do the same. A `page` or `fonts` value either element refuses throws a `TypeError` naming the property, and that error now carries the layout’s own account of the value behind it.

- **An all-authored split whose slot widths sum under 100 warns, as a table’s columns already did.** A split’s slots carry the same percentage shares under the same arithmetic, so the plan now reports the trailing width they leave unused, on the split’s `slots` path. The report still renders; the warning is advisory, like every other.

0.10.0

### quario 0.10.0

### Minor Changes

- **A group’s `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.

  The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.

  **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.

  The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.

  The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.

  The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.

- **Two new reducers: `first` and `last`.** They report the value an expression takes on the first and the last row in scope. Like every reducer, each one arrives on three surfaces at once: a declared aggregate on a report or a group, a runner under `run`, and a function you can call inline over a row’s own array.

  ```jsonc
  // the customer a region's rows open and close on
  { "aggregates": { "opened": "first:=@.customer", "closed": "last:=@.customer" } }
  ```

  ```
  {{ first(@.lines, l => l.sku) }}   the first line's article number
  ```

  They are the first reducers that read the **order** of the rows they fold, so the spec now says what each scope hands them. Every scope reports the ends of the rows it folded, which is not always what the page ends on. A report folds after `where`, its `sort` and its `take`, and before it splits the rows into groups. A group that declares a `sort` and no `take` folds its partition before it orders that partition, so its handle holds the partition’s ends. Declare `first` under a group’s `run` when a detail row needs the row that group opens on as rendered.

  An empty scope gives `null` for both, next to `avg`, `min` and `max`. Neither reducer skips a null: a scope whose first row holds `null` reports that `null`.

  Reports that already render are unaffected. A document that spelled `first:=...` before was a definition error, and it now compiles.

- **A hollow instance’s `group-end` carries `hollow: true`.** An instance whose bands emitted nothing between its two brackets is hollow, and a hollow instance is not there: no space, no gap, no page turned by opening one, no outline entry. The stream now says which instances those are, on the closing bracket, so a target reads one field rather than counting the events between the brackets for itself. An instance holding nothing but hollow instances is hollow too, and one holding a child that drew is not. The field is absent, never `false`, on an instance that drew.

  `@quario/layout`, and through it `@quario/pdf`, the viewer and the editor, read the field and keep no tracker of their own. What they render is unchanged.

- **`splits(events)` folds a split bracket into one event.** A split reaches the event stream as `split-start`, one `item` or `image` per slot, then `split-end`, and every consumer that wanted a split whole kept that bracket for itself. `splits` does it once: read `walk(splits(events), handlers)` and register a `split` handler, which receives the opening event’s `role`, `path`, `slots` and `style` plus the slot events in order under `items`. Every other event passes through unchanged, lazily.

  The stream itself is unchanged. A consumer that registers no `split-start` handler still receives the slots as ordinary events and stacks them, as before. The `SplitStartEvent` declaration now names its `path`, which the stream always carried.

  Every built-in target reads its splits this way now, so a custom target can too.

0.9.0

### quario 0.9.0

### Minor Changes

- **Every target factory now refuses an option it does not understand.** An unknown key, a key with a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx` already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding one.

  The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A fourth document property is one edit rather than three.

  **What this changes for you.** One options object spread across several targets stops working if any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`, `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works. TypeScript does not warn about this: excess-property checking fires on an object literal and not on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.

  Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and `{ meta: { title: config.title } }` over a config that carries neither are both fine. `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws instead of turning path stamping on. A `fonts` mapping given as an array is refused by `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.

  Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests `instanceof Error` is unaffected. One that compares the constructor is not.

  `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null` and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }` is a property you did not write rather than one of the wrong type, and an inherited enumerable key is no longer reported as an option you wrote.

  `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the object you passed no longer changes what a configured target writes.

### Patch Changes

- The engine verifies a license key against a new signing key.

0.8.0

### quario 0.8.0

### Minor Changes

- **BREAKING: an image whose header does not state its size now fails on every target.** A PNG or JPEG truncated before its dimensions used to render on the HTML and CSV targets — the browser coped, and CSV has no picture to place — while failing on the PDF and XLSX targets, which have to know how big a picture is before they can put it anywhere. One file, two verdicts, depending on where the report was sent.

  It is now a single render error, raised where every other verdict on image bytes is raised and naming the item’s `source` the same way — for example `header[0].source [=$.input.logo]: could not read the image's size from its bytes`. A report that renders as a PDF today is unaffected: those bytes already failed there. What changes is a report that renders as HTML today on bytes no other target would accept: it now fails, and the file it names is one no target could ever have drawn correctly.

  Nothing further has changed about what the engine promises: a file whose size reads but whose pixel data is corrupt is still nobody’s guarantee, and each target does whatever its own machinery does with it.

- **The image event carries the picture’s size.** `width` and `height`, in pixels, read from the same header the format is sniffed from. A target — the built-in ones, or your own — places a picture from two fields instead of parsing a PNG’s IHDR and a JPEG’s frame header for itself.

- **A page-number token says which page value it is.** A value token whose interpolation is exactly `{{ page.number }}` or `{{ page.total }}` now carries `field` on the event stream, holding that same string. The `value` is unchanged — the number the page band was called with — so nothing renders differently; `field` tells a target whose own document format numbers pages that it may write its own live field there instead of the number this render saw. Anything computed from them (`{{ page.number + 1 }}`, `{{ pad(page.total) }}`) is an ordinary value and carries no `field`, so write the bare token wherever a live number matters.

### Patch Changes

- **A PNG wider or taller than 65535 pixels is no longer misplaced.** Its dimensions were read from the low half of each of IHDR’s four-byte fields, so a 70000-pixel panorama measured 4464 pixels and was laid out at that size, confidently and silently.
- **Every published README says where the documentation is.** Each package now carries a Documentation section pointing at the reference, at the report schema that normatively specifies what a report may declare, and at the package’s own API. The paragraphs that used to end on an unstated contract — the event stream’s field semantics, the style vocabulary, page columns, the Content Security Policy a fragment with images needs, the formula mangling, and each target’s own contract — link the page that states it. Every link is an absolute URL, so it resolves from the npm package page as readily as from an installed copy.

0.7.0

### quario 0.7.0

### Minor Changes

- **Styled runs.** Bold a word, colour a phrase, or format one value inside a sentence, and have it survive into the PDF and the spreadsheet — not only the HTML, which was the only place `<b>` in a template ever meant anything. A run carries the inline declarations only: no padding, borders or spacing, which belong to a whole line or block.

  A cell’s tokens carry a run’s resolved style on the tokens it covers, and `styledRuns(tokens)` groups them back out — the same rule every built-in target reads them through, so a custom target cannot drift from them. `RUN_STYLE_NAMES` is the inline half of the vocabulary as a list, beside `STYLE_NAMES`, for a tool that offers an author what a run may wear.

- **`plan()` now reports declarations that nothing will read.** A definition can be perfectly valid and still say something the engine quietly drops, and until now the only way to find out was to notice the output was wrong. `plan()` returns a `warnings` list beside `problems` — same shape, same document order, one entry per declaration — and it currently catches three:

  - a `format` on a cell whose value can never present a single interpolation — `"Due {{ due }}"` under `format: "date"` — which the engine leaves unread;
  - a `currency` on a cell whose `format` is not `"currency"`, which the engine reads only under that kind and otherwise ignores, so the cell renders in the instance’s currency and the code you wrote does nothing;
  - a table where every column declares a `width` and the widths total under 100, which leaves the trailing share of the table unused — usually a column edited down without its neighbours being adjusted.

  `problems` is unchanged and still fatal: a definition with problems compiles to nothing, while one carrying only warnings compiles and renders exactly as before. Hosts that ignore the new field are unaffected, and `validate()` is untouched.

  Warnings are advisory and deliberately incomplete — they catch what can be seen in the definition alone, without data, so a quiet `warnings` list is not a promise that every declaration will be read.

- **A `format` declaration now needs one value to speak about, and a cell can give it one.** Previously a `format` on a cell reached every interpolation in it and skipped the literal text between them, which meant `"{{ amount }} of {{ quantity }}"` under `format: "currency"` rendered the quantity as money too — and the page and the spreadsheet disagreed about it, the page formatting inside a sentence where the sheet did not.

  A cell `value` may now be a list of **styled runs** — `{ "value": "…", "style": { … } }` — each with its own declarations, and `format` applies to a run holding a single interpolation.

  **What changes in an existing report:** a cell that mixes text and an interpolation under a `format` stops presenting that value and renders it plainly, in every target. `"Due {{ due }}"` with `format: "date"` was `Due 14 Aug 2026` on the page and `Due 2026-08-14` in the sheet; it is now the plain form in both. To restore it, split the value and leave the declaration where it is:

  ```jsonc
  {
    "value": [{ "value": "Due " }, { "value": "{{ due }}" }],
    "style": { "format": "date" }
  }
  ```

  A cell’s `format` reaches its runs, so nothing moves and nothing is declared twice. `plan()` reports the cells this affects wherever it can see them without data.

- **`round`, `floor` and `ceil` reuse their formatters instead of rebuilding one per call.** The three reach the decimal you wrote through `Intl`, and each call built a fresh formatter to do it — so a detail column of `{{ round(@.qty * @.price, 2) }}` paid for one per row. A hundred-thousand row report with two such columns drained in 3.5 s; it now drains in 0.2 s. Every answer is the value it was before: the rounding mode and the digit count are both part of what a reused formatter is found by, so no call can be handed one built for another.

- **`round`, `floor` and `ceil` name themselves when they refuse an `n`.** All three threw `n must be a number from 0 to 100`, so a cell calling more than one was told how it failed and never which call; the message now opens with the function, as a reducer’s already did. The located path still names the cell — one cell holds as many calls as you write, which is the whole reason the name is worth having.

  **An `n` that cannot be read as a number at all now gets the same answer.** A `BigInt` or a `Symbol` reaching `n` from render data used to surface the runtime’s own `Cannot convert a BigInt value to a number`, and an object carrying a throwing `valueOf` surfaced whatever it threw — each located at the cell but naming neither the function nor anything an author could act on. All of them are now the same refusal.

- **`isDiagnostic` is documented for what it actually answers.** The README said it was true for “one of the stack’s located errors: quario’s own, or one thrown directly by xprsn, sjabloon, or padvinder”, and its example read a `false` as “one of your own functions failed”. Only an engine’s own error is a diagnostic: quario’s verdicts on a document — an unknown reducer, a misused inline reducer, image bytes it will not vouch for — are located just the same and are not diagnostics, so a host following that example rethrew report faults as its own. Nothing about the guard changes; it never behaved the way the page described.

- **A misused inline reducer says what it is, instead of how it folds.** `{{ min(1, 2) }}` — the two-argument scalar `min` quario does not have — failed at the cell with `(rows || []).map is not a function`, the fold’s own internals, naming neither the reducer nor the mistake. It now reads `min is a reducer over an array, not a scalar function; got a number`, and a second argument that is not a lambda says so in the same words rather than reaching a host as `of is not a function`. Register a function of your own to shadow a built-in reducer when a report needs the scalar.

  **Two reducers used to answer instead of failing**, each reading a `length` off a value that is not a collection: `count('ab')` presented `2` — a string’s own length, folded as if it were a count of rows — while `count(1)` and `avg(1, 2)` presented nothing at all. All three are now the same located error. A reducer over an **absent** array is unchanged and still not a mistake: `sum(@.lines)` on a row carrying no lines is `0`, exactly as an empty array gives.

0.6.0

### quario 0.6.0

### Minor Changes

- **`imageError(path, said, cause)`**, the mint a render target raises an image failure through. The engine vouches for an image’s magic numbers and no further, so every failure past them belongs to whichever target was sizing or embedding the file — and each was wording it its own way. This names the item that asked for the bytes and keeps the failing class and message behind it, so one report reads the same however it is rendered. It is not a diagnostic: `isDiagnostic` does not answer for one.

- **Formatted cells render dramatically faster.** The engine built a fresh `Intl` formatter for every presented cell and now keeps them, keyed on everything a formatter is built from — the kind, the locale, the digit count, the currency code, and for a date its form and timezone. A report of a hundred thousand formatted cells in mixed currencies went from 3.2 s to 0.2 s rendering to HTML. Nothing about what a cell presents changes.

  The store is bounded and empties past its bound rather than growing, so a report presenting an unusual number of distinct formats costs what it always did and never a formatter belonging to another cell.

- **`format` takes a digit count, and a date takes a form.** The declaration is still a closed kind, and now the kind may carry the one modifier it understands: `number`, `percent` and `currency` take `digits`, and `date` takes `form`.

  ```jsonc
  { "header": "Rate",  "value": "{{ @.rate }}",
    "style": { "format": { "kind": "percent", "digits": 3 } } }
  { "header": "Total", "value": "{{ @.amt }}",
    "style": { "format": { "kind": "currency", "digits": 0 }, "currency": "EUR" } }
  { "header": "Due",   "value": "{{ @.due }}",
    "style": { "format": { "kind": "date", "form": "long" } } }
  ```

  The bare kind stays the shorthand — `"number"` means `{ "kind": "number" }` — so no existing declaration has to change.

  `digits` is a whole number from 0 to 20 and overrides the count the kind would otherwise present, **a currency’s minor units included**: the example above shows `€1,235` on the page and builds `"EUR"#,##0` in the worksheet. The minor units remain the default, so a JPY cell still shows no decimals beside a EUR one that shows two.

  On `percent`, `digits` counts the places of the **presented percentage**, not of the stored fraction: `digits: 2` on `0.12345` gives `12.35%`. Making the stored value agree still takes `round(x, 4)` — the two count different things, a factor of 100 apart.

  `form` is one of `short`, `medium`, `long`, `full`. It is honoured exactly in HTML and PDF, and **approximated in the worksheet**: a sheet date is a real typed date its reader re-presents, so the shape is yours while the language a month or weekday name is spelled in follows whoever opens the file. Pinning that would stop the sheet reading naturally for its reader.

  A modifier a kind does not understand is a definition error naming the cell — `{ "kind": "date", "digits": 2 }` fails, as does a `digits` outside 0 to 20. `format` layers whole: a cell that names it restates the whole declaration, so naming a kind again resets the modifier. Only the whole declaration may be an `=` expression; a computed count is written `"={'kind':'number','digits':@.dp}"`. Custom `#,##0.00` patterns still belong in `funcs`, and a fraction-digit count no longer needs one.

- **`sort` is stable**: rows whose keys compare equal keep the order they arrived in. The engine has always sorted this way; the promise is new.

- `currencyOf(style, options)` joins the package’s exports beside `format()`. It answers which currency code a cell wears — its own when it declares one, else the instance default — for a target that needs the code itself rather than presented text. A cell that declared a code the engine could not accept carries `style.currency` as `null`, so `style.currency ?? yourDefault` is the wrong spelling and this helper is the right one.

- **A cell may name its own currency.** `currency` is a new style declaration beside `format` — a three-letter ISO 4217 code saying what a money cell is denominated in.

  ```jsonc
  { "header": "Amount", "value": "{{ @.amt }}",
    "style": { "format": "currency", "currency": "=@.ccy" } }
  ```

  Until now the code lived only on the instance, so a report whose rows arrive in different currencies — a revenue listing, a statement, an outstanding- invoices table — had to format through `funcs`, and that costs the value: a formatter returns a string, so the cell leaves the typed seam and the worksheet gets text with no number format. A declared code keeps the number.

  The cell’s own code beats the instance’s, which stays the default for documents that only ever hold one denomination. Locale and timezone do not follow it into the document: a locale describes the reader, while a currency code describes the value. The declaration takes an expression like any other, so a total under a group keyed by currency reads `"=byCcy.key"`. It shares `format`’s sites exactly — text items, column cells, headers and totals, and a column’s own `style.currency` declares an amount column once — and, like `format`, it is refused on an image, on `row.style` and on the report default. It is unread without `format: "currency"`.

  A literal that is not three uppercase letters is a definition error. An expression that resolves to something else stays lenient, and the cell then renders as plain display text: it does **not** fall back to the instance’s currency, because labelling a figure in a denomination nobody named is worse than not formatting it.

- **`round`, `floor`, `ceil` and `abs` are callable from any expression**, without registering anything. `round(x, n)` gives `x` to `n` decimal places; `n` defaults to 0 and must be a number from 0 to 100; a fraction in it truncates. This is the lever a report needs when arithmetic across exact values lands on an inexact one: a receipt whose `{{ $.subtotal + $.vatLow + $.vatHigh }}` computes `83.75999999999999` writes `83.76` once the sum is wrapped in `round(…, 2)`. It matters in the two targets that keep the number rather than presenting it — the workbook stores the value, and a CSV field shows it as text with no number format to hide behind. A registered function of the same name still wins, so nothing an existing report does changes.

  They round the **decimal you wrote**, not the binary value it is stored as: `round(2.675, 2)` is `2.68` and `round(0.615, 2)` is `0.62`. Note that a `percent` cell presents its value multiplied by 100, so matching a two-decimal percent display takes `round(x, 4)`. A value that is not a finite number passes through untouched rather than becoming zero; an `n` outside the range is an error naming the cell it came from.

- `split-start` events carry `path`, the split definition’s schema path, as `item` and `image` events already do. A consumer can now name the definition behind a split without tracking position.

- `fractionDigits(kind, options)` is exported beside `format()`: how many fraction digits a kind presents, or nothing for a kind that carries no count. It is the table the official targets read, and it is public so a consumer writing its own target presents the same digits they do.

- **A `date` cell with no `form` now presents `medium`, not the runtime’s own default.** A `date` that names no form used to render whatever the platform’s default date format was — under `en-US`, `8/14/2026` — which matched none of the four forms you can now ask for. It renders `Aug 14, 2026` instead, and `14 Aug 2026` under `en-IE`. In the worksheet the number format moves from `yyyy-mm-dd` to `dd mmm yyyy`.

  If you want the old shape back, say so: `{ "kind": "date", "form": "short" }` is the nearest of the four. This is the one change here that moves output under an existing report.

- **`style.format` crosses the event stream resolved, not as authored.** A custom target reading `style.format` now sees `{ kind, digits }` or `{ kind, form }` where it saw a bare string: `"number"` arrives as `{ kind: "number", digits: 2 }` and `"date"` as `{ kind: "date", form: "medium" }`. A `currency` whose code cannot be read arrives as `{ kind: "currency" }` with no count at all, which is the signal to write no number format. Read `style.format.kind` where you read `style.format`, and take the digit count off the declaration rather than computing one.

- **`fractionDigits` takes the whole style.** Its first argument was the kind and is now the style, matching `format()` and `currencyOf()`. `fractionDigits("number")` becomes `fractionDigits({ format: "number" })`. Targets no longer need it at all — the count rides on the declaration the stream carries.

- **`typed()`’s second argument is the resolved declaration.** Pass `style.format` as it arrives on the stream; it reads the kind from the object. The shorthand string is still accepted, and omitting the argument is unchanged.

- **A rejected box value is now dropped from the resolved style rather than carried on it.** A `border*` or `padding*` declaration written as an `=` expression used to cross the render stream with whatever the expression answered, even a value the vocabulary rejects; every other name was already dropped. It is now dropped too, so one rule covers the whole vocabulary and `currency` is its only exception.

  Nothing a report renders changes. A border side is still won whole by the node that names any of its three parts — that is now settled from the declarations rather than from what they resolve to, so a rejected part still cannot hand the node its row’s other two. A side left incomplete still contributes nothing rather than becoming a solid black stroke, and a rejected padding still falls to the target’s own inset.

  What changes is what a consumer reading the stream itself sees. Where a cell had `style.borderTopColor` holding an unusable value, the key is now absent:

  ```js
  // before
  cell.style; // { borderTopWidth: 1, borderTopColor: "2-6-A-2-2" }
  // after
  cell.style; // { borderTopWidth: 1 }
  ```

  Every target in this release already checked such values before reading them and is unaffected. A consumer that inferred border-side ownership from which keys were present should read the declarations instead; one that validates what it reads needs no change.

- **`format()` now takes the cell’s resolved `style` instead of its `format` kind.** The signature is `format(value, style, options)`, where it was `format(value, kind, options)`. This is what lets the helper read a cell’s own `currency` code alongside the kind. Callers pass one property less:

  ```js
  // before
  format(token.value, style?.format, intl);
  // after
  format(token.value, style, intl);
  ```

  A caller holding only a kind wraps it as `{ format: kind }`.

- **A table’s `total` is now a block holding its rows**, so what belongs to the totals as a whole can be said once. Before, `total` was the array of rows itself:

  ```jsonc
  "total": [{ "cells": [...] }, { "cells": [...] }]
  ```

  Now it is an object whose `rows` is that same array, plus a `style` and a `visible` of its own:

  ```jsonc
  "total": {
    "style": { "paddingTop": 0, "paddingBottom": 0 },
    "rows": [{ "cells": [...] }, { "cells": [...] }]
  }
  ```

  The rows themselves are unchanged — still `{ "cells", "style?", "visible?" }`, still covering every column exactly once. A `total` that is still an array is a definition error reading `detail.total: expected an object`, and a `total` with no `rows` reads `detail.total.rows: expected an array`. Total cell paths gain the one segment: `detail.total.rows[r].cells[i]`, which is what a `total-row` cell now carries as its `path` and what a located problem names.

  The block’s `style` is the layer below each row’s — box under box, every other declaration under the row’s own — and takes a total row’s vocabulary, the whole style set less flow spacing and `format`. Where the block, a row and a cell all speak, the innermost one that names any of a border side’s three keys takes that side whole, exactly as a cell already did over a row; padding layers per name. Exact `false` on the block’s `visible` omits the totals entirely: the rows are never evaluated, none of them votes on a width-less column, and the output is what leaving `total` out gives.

  This is why the change earns a break: a table’s header row has `detail.header` and its data rows have `detail.row`, but the totals — the one kind whose rows a document writes out one by one — had no level above them, so “these sit at the band’s line pitch, not the table’s row pitch” had to be spelled on every row. `columns` is unaffected and needs no such block: a declaration reaching every column reaches every data cell, which is what `detail.row`’s style already says from the other side.

- **`table-start` now carries the header row**, instead of a `header` cell on each column that a custom target had to collect for itself. The event’s `header` is `{ cells, style? }` — the same shape a `row` or `total-row` event has — where `cells` holds one cell per column no neighbour’s `span` covers, so it is shorter than `columns` whenever a header spans. Each header cell carries its column’s `path`, as a row cell does; a spanning header takes the first column it covers. What is left on `columns` is the geometry: one entry per column, holding a `width` percentage when the document authored one. Nothing an existing report produces changes in any of the shipped targets — this is the seam between the engine and a target, not the document.

  ```js
  // before
  const headers = event.columns
    .filter((column) => column.header)
    .map((column) => Object.assign({}, column.header, { path: column.path }));
  const headerStyle = event.style;

  // after
  const headers = event.header.cells;
  const headerStyle = event.header.style;
  ```

  `columns[i].header`, `columns[i].path` and `table-start.style` are gone. A target that reads only `columns[i].width` is unaffected.

- **`format` now presents a fixed number of fraction digits, and every target presents the same number.** Until now the digits were whatever each edge happened to default to: `format: "number"` showed up to three decimals in HTML and PDF and exactly two in a worksheet, and `format: "percent"` showed none in HTML and PDF against the worksheet’s two — so one cell in one document read `12%` in the PDF and `12.35%` in the workbook beside it.

  `number` and `percent` are now **two places, always**, in every target that presents. `currency` is its currency’s own minor units, so a JPY amount has none and a BHD amount has three, where every currency previously got two. Author-visible, with no document change:

  | value              | before      | now        |
  | ------------------ | ----------- | ---------- |
  | `number` `1000`    | `1,000`     | `1,000.00` |
  | `number` `0.12345` | `0.123`     | `0.12`     |
  | `percent` `0.21`   | `21%`       | `21.00%`   |
  | `currency` JPY     | `¥1,234.57` | `¥1,235`   |

  Only the digit count converges. Grouping separators, a currency symbol’s position and a date’s shape still follow each target’s own conventions, and in a worksheet they follow whoever opens the file. `date` is unchanged. A report that needs different digits formats through `funcs`, as before.

- A `currency` code that is not a readable currency now presents nothing in every target, rather than the worksheet inventing a number format from it while the other targets fell back to the unformatted value.

- **A split slot is typed with the declarations the traversal accepts.** `SplitSlot` reused the band items’ style types, so TypeScript refused `valign` on an image slot — legal there, and only there — and admitted `spaceBefore`/`spaceAfter` on a slot, which the engine rejects. Two new exported types, `SlotStyleDeclarations` and `SlotImageStyleDeclarations`, now say what a slot may declare.

- **A page band closure is typed as what it returns.** `PageBandRenderers` declared `header`/`footer` as returning item and image events only, while a split in a page band has always come back as its bracket — `split-start`, one event per slot, `split-end`. The closures now return `PageBandEvent[]`, a new exported union naming all four, so a TypeScript host reading a page band no longer types `role` onto a `split-end` that carries none.

- **A computed style value the schema does not accept now leaves the layer below in place**, which is what the schema has always said it does — “contributes nothing, same as omit”. The name is dropped from the resolved style rather than passed on, so no target has to decide what an unreadable value means.

  Before, the value reached each target and they disagreed. A detail item with `"bold": "=@.flag"` over a `1` rendered bold in a PDF and a worksheet but not in HTML. On a report header, where those two targets carry a heavier default of their own, a `0` rendered *not* bold in both while HTML left the header bold. The same held for every other declaration: a report header whose `"size": "=@.pt"` resolved to `"big"` rendered at 14pt in HTML, at the body size in a PDF, and at no size at all in a worksheet. Now all three keep the layer below in every case, and only a real `false` turns a flag off.

  This covers `family`, `size`, the five flags, `color`, `background`, `align`, `valign`, `format`, `spaceBefore` and `spaceAfter`, and only where the value comes from an `=` expression — a literal has always been checked when the report was compiled. It reaches inside a split too: a slot whose `format` expression resolves to something other than the four kinds now keeps the split’s own kind instead of overwriting it.

  Two declarations are deliberately unchanged. A border or padding name is kept rather than dropped, because a node that names any part of a border side takes that side whole from its row — dropping the name would hand the node the row’s other two and draw an edge nobody declared. And a `currency` code that is not a code still crosses as `null` rather than disappearing, so a cell that named a denomination is never presented under another one.

- A `format` kind that an `=` expression resolved to the name of a built-in object member — `"toString"`, `"constructor"` — now contributes nothing and the cell falls back to display text, as the documented rule says any unrecognised kind does. Before, such a cell could render `[object Undefined]`. A literal was never affected: it is checked against the four kinds.

- **A pinned report header now refuses a lead on every band that can sit under its box, and only where the document settles which one that is.** When `header` declares a `height`, an authored `spaceBefore` on the first item of the band below the box would push that band off the pin, so it is refused. Which band is below the box was read too narrowly, and which item leads it too eagerly.

  Reports that used to validate and now do not: a lead on the `empty` band’s first item, and a lead on a group header below the first one. Both were already refused at render, but only on the render that reached them — an `empty` band’s lead detonated the first time a filter emptied the row set, on a report that had validated clean for months. Both are definition errors now, on every render.

  Reports that used to be refused and now validate: a lead on an item that may not appear at all — one whose `visible` is an expression, or an image whose `source` yields nothing. Such an item was blamed as though it always occupied, so a document that renders correctly could not be compiled. The check now stays silent wherever only the data decides which item comes first, and the render refuses it at the moment the answer exists.

  Two smaller repairs come with it. A `spaceBefore` that is not a paintable number — negative, `NaN`, infinite — no longer produces two problems on one key, one telling you to make it `0` and one telling you it is not a valid measurement; only the second is reported, because it is the one that is wrong. And a `split` that leads a band now names itself in the error rather than reporting against the report header, which is a band this rule never covers.

0.5.0

### quario 0.5.0

### Minor Changes

- **`STYLE_NAMES`**, every name in the style vocabulary as a read-only list, in the order the specification’s table lists them. A tool that offers the vocabulary — the editor’s style rail — reads it from here instead of carrying a copy that has to learn each new name.
- **`span` lets a cell cover several table columns**, so a label reaches across them instead of being pushed sideways by empty cells. A literal positive integer; absent is 1. It is legal on the two cells a document writes: a column’s `header`, where the columns it covers omit their own `header`, and a total row’s cells, whose spans must sum to the column count exactly. A data row’s cells are the columns’ own, so there is no cell there to carry one — a line that runs across the table is a `split` item below it. A span covers and does not vote: a cell over more than one column has no say in their widths, and a column nothing votes on takes the cell-padding floor.
- **`valign`** joins the style vocabulary: `"top"`, `"middle"` or `"bottom"`, a literal or an `=` expression like `align`. It is legal exactly where a box is taller than its content asked for — table cells, headers, totals, `row`, and split slots, text or image — and a definition error on a stacked item and on a band image, whose boxes have no such slack. A row’s or a split’s layers under its cells’ or slots’ own. Undeclared is not a declaration: each target keeps its own default.
- **A table row’s `style` now resolves onto that row’s cells**, the box included, instead of meaning something different on every output. Before, padding and border on `detail.header`, `detail.row` or a total row’s `style` reached no cell at all: a fragment dropped them, a worksheet turned them into an edge on each cell, and a page drew one box around the row. Now they layer under each cell’s own, the way `bold` and `valign` on a row already did, so one declaration means one thing everywhere. What this makes possible is declining the cell padding a row at a time: `"paddingTop": 0, "paddingBottom": 0` on `detail.row` sets the pitch for every data row, where before it had to be written on each cell. Two things move for documents that already declared a box on a row. A row’s `borderLeft` is now an edge on each of the row’s cells rather than one at the row’s outer left — write it on the first column’s cells to get the single edge back. And a row’s border now occupies height, as a cell’s border always has, so a bordered row is taller by its border’s width. A border side is won whole by the cell: a cell naming any of that side’s three keys takes the side entirely, so a cell that failed soft on one name does not inherit the row’s other two. On the event stream, a `row`, `total-row` or `table-start` `style` now carries only what layers by ordinary means, and a row whose whole block was box carries no `style` at all.

0.4.0

### quario 0.4.0

### Minor Changes

- **`format: "date"` now reads a date string, not only a `Date`.** A JSON document has no date type, so the kind could not be reached from parsed data without reviving every date field by hand first. It now revives two forms itself: a calendar date `2026-08-14`, and a timestamp naming its offset (`2026-08-14T12:30:00Z` or `+02:00`). **This changes existing output**: a cell declaring the kind over `"2026-08-14"` rendered `2026-08-14` and now renders `14/8/2026` under an `en-IE` instance. A zoneless `2026-08-14T00:00:00` is not read — it means local time, so it would present a different day per machine — and neither is a loose `14/8/2026`, a partial `2026-08`, a lowercase `t`/`z`, or a day that does not exist. Anything unread renders as authored, as before.

  The revival is the `Date` a host would have injected, timezone included: a calendar date is UTC midnight, so under a western instance timezone it presents as the previous day, exactly as `new Date("2026-08-14")` does. Only `date` revives — `number`, `currency` and `percent` never read a string, because JSON already carries numbers — and epoch milliseconds stay a number under every kind.

- **`typed(tokens, kind?)` takes the cell’s `format` kind.** Passing `"date"` opts into the same revival, for consumers that write typed cells. The one-argument call is unchanged.

- **The TypeScript declarations accept the box.** The per-side `padding*` and `border*` names, and the table’s `detail.header`, were validated by the engine from 0.3.0 but were missing from the shipped declarations, so a report declaring the padding or the border sides that release introduced was rejected by the compiler as an unknown property and needed a cast to get past it. They now type exactly as the engine reads them, on a band image’s `style` as well. Four names come with them, for annotating your own helpers: `LineStyle` (`"solid" | "dashed" | "dotted"`), beside `Align` and `FormatKind`; `TableHeaderBox`, the type of `detail.header`; and `BoxDeclarations` with `Side`, the per-side names as one type, which `StyleDeclarations` was built from without being nameable. Nothing about rendering changes: a report that compiled through a cast produces the same output without one.

0.3.0

### quario 0.3.0

### Minor Changes

- **`format` is a closed style name.** `number` / `currency` / `percent` / `date` on text items, column cells, headers, and totals. Not images, not `row.style`, not the report default. Locale, currency code, and timezone live on `quario({ locale, currency, timeZone })`. The public `format()` helper is how targets present a kind; a kind on the wrong type contributes nothing.
- **The report header may pin a `height` from the page top.** Dual-shaped like `detail`: an item array, or `{ height, items }`. `page.margin` is a document field (one number, all four sides), required with `height` and legal without. Authored `spaceBefore` on the first occupying item of the next band is refused.
- **`spaceBefore` / `spaceAfter` return as item flow spacing.** Blank space before or after a band item, in points, including band images and splits as band items. Adjacent gaps add. Table cells, `row.style`, headers, totals, and split slots refuse the names. `spaceBefore` drops at a fresh body page or strip top; page-band items keep it. Leading and inset stay cut.
- **Per-side padding and border on the closed style vocabulary.** `paddingTop` / `Right` / `Bottom` / `Left` (points, ≥ 0) and, per side, `border*Width`, `border*Style` (`solid` | `dashed` | `dotted`), `border*Color` (`#rgb` / `#rrggbb`). A border side is all three names or none; width `0` is no stroke; an incomplete literal is a definition error, and an incomplete expression result at render contributes nothing rather than a solid black stroke. The names are legal wherever `background` is, including images, plus `row.style`. The report default still takes only `family` and `size`. Column `%` widths are border-box.
- **`detail.header` is the header-row box.** `{ style }` only, a different path from `columns[i].header`. It crosses the seam as `table-start.style`.
- **A table total is N rows.** Before, `total` was one row: a flat cell array or `{ style, cells }`. After, it is absent or a non-empty array of `{ cells, style?, visible? }`. A one-row total is `[{ cells: [...] }]`. `total: []` is a definition error. Paths are `detail.total[r].cells[i]`. The stream yields one `total-row` per emitted row.
- **A visible text item occupies a line at its own `size`.** Empty display used to take the report default’s leading in PDF and collapse in HTML; `"a\n\nb"` broke in PDF and collapsed to a space in HTML. A visible item now occupies at least one line set in that item’s `size`, and a literal newline is a line break, on every target that can show a line. Empty table cells stay contentless for height. This is not a spacing primitive; `visible: false` is still how an item leaves the layout.

0.2.0

### quario 0.2.0

### Minor Changes

- **A report default: one `style` block for the whole document.** A top-level `style` beside `header`/`detail`/`footer` states the typeface a report is set in, so a document with one face names it once instead of on every item, every table column, every header and every total cell. It takes `family` and `size`; any other declaration there is a located definition error, exactly as a text declaration on an image item is. Values are literals or `=` expressions like any other style block’s, resolved once per render in report scope.
- **`report-start` carries the resolved report default as `style`.** A report-level fact, never merged into an item’s own `style`: an event’s `style` stays what the author wrote on that node, and a consumer composes the default itself, once — under its own band-role defaults and under every event’s own style. Absent when the report declares none, so a consumer written before this field renders in its own baseline exactly as it did.
- **An `uppercase` style declaration.** A boolean beside `bold` and `italic`, literal or an `=` expression, for the capitalised column labels business forms are usually set with. It is capitals, not small caps — real small caps need a font feature the PDF target’s built-in faces cannot supply, so the declaration promises only what every target can draw. Image items keep refusing it, as they refuse every text declaration.
- **A `split` item places values across the line instead of down the band.** The invoice header’s “seller left, customer right”, which a band could not say before. `{ "type": "split", "slots": [...] }` takes two or more slots, each an ordinary `text` or `image` item plus an optional `width` percentage under the same rules a table column’s obeys; width-less slots divide what the sized ones leave, evenly. A split is always the full content width and never nests — it says what sits beside what, never where anything is — and a slot that renders nothing keeps its width, so a line’s geometry never moves with the data. Splits may appear in every item array except table and total cells.
- **`split-start` / `split-end` bracket a split’s slots on the event stream.** `split-start` carries the slot geometry, then one ordinary `item` or `image` event per slot in order, then `split-end`. Existing consumers need no change: the walk driver’s missing-handler rule means a target that ignores the bracket still receives the slot items and renders them stacked.
- **`quario().plan(schema, funcs?)` hands the whole traversal over at once.** Returns `{ report, problems, anchors }` from one descent: the compiled report (`null` while the document has problems), every problem as `{ path, source?, message, diagnostic? }`, and a map from each compiled source’s schema path to the anchors and group handles it reads. A host that validates and renders per edit — an editor — pays one traversal instead of two, can point at a problem in its own UI without parsing a message apart, and can tell where a node may safely move. `validate()` is unchanged: its strings are those problems’ `message` fields.
- **Every problem keeps its own located diagnostic.** `validate()` retained `start`/`end` offsets on at most one problem per document; the structured list carries each problem’s own, so a consumer can underline the offending character in every faulty expression rather than only the first.
- **Events name the definition behind them.** `item`, `image` and `group-start` events carry `path`, the schema path of the node that produced them; `table-start` carries `detail` and its columns their own, and every table cell carries its column definition’s path (a total cell its `detail.total[i]` entry). Rows carry no identity — a row is data, not schema. Additive: a consumer that ignores the field is unaffected.
- **`display(value)` joins one value the way `text()` does.** The scalar display rule behind the token join, re-exported beside `text()` so a stream consumer that stringifies token values itself renders exactly what the official targets render — Dates included.
- **`maxDepth: Infinity` opts a query budget out.** The data query’s traversal budgets accept an explicit `Infinity` per key for “this budget, unbounded”. The 500-deep default is unchanged — it is now padvinder’s own, applied for every consumer rather than added by quario at the seam.
- **Located data-query errors carry padvinder’s code and span.** A `data` query that does not parse now surfaces with padvinder’s `code` and `start`/`end` offsets into the query you wrote — filter faults included — alongside the band/item path quario already attached. The code names the category of mistake: `PADVINDER_MISSING_ROOT`, `PADVINDER_BAD_SELECTOR`, `PADVINDER_UNCLOSED_BRACKET`, `PADVINDER_BAD_STRING`, `PADVINDER_UNKNOWN_FUNCTION`, or `PADVINDER_SYNTAX` for a path character or filter body that is open-endedly not a query. Traversal budgets exceeded at render time keep `limit`/`actual` and carry no span.
- **Cells render straight over the engine scope chain.** A text cell no longer allocates a wrapper scope and an anchor pair per cell per row — quario’s chain already binds `$` at the render base and `@` on the detail row, and sjabloon now renders over it as-is. A 4-column stream over a million rows went from 1.7s to 0.5s. No report changes what it renders. Requires sjabloon 0.11.
- **A compiled report’s `functions` carry signatures.** Each entry is now `{ name, arity, doc? }` instead of a bare name — `arity` from the function’s declared parameter count (or its own numeric `arity` where rest parameters mislead `length`), `doc` from an own `doc` string when it carries one — in the same call-first-seen order. `names` is unchanged. Requires xprsn 0.11 and sjabloon 0.11.
- **A bare `Date` renders as ISO 8601 UTC, the same on every machine.** Display text for a `Date` value was `String(date)`, which bakes the rendering host’s timezone and locale into the output — so one report produced different bytes on different machines. Every target now renders a valid `Date` as its `toISOString()` form through the one shared display rule; an invalid `Date` keeps its deterministic `Invalid Date` text. Reports that want a formatted date keep using a registered function, exactly as before.
- **A bad literal pattern in the data query is a definition error.** A typo’d I-Regexp written as a string literal in `match()`/`search()` used to produce a plausible empty report with no signal; `report()` and `validate()` now surface it at compile time as a located error, with offsets at the pattern literal in the query. The class and `code` are treffer’s — a malformed pattern is a `SyntaxError` with `TREFFER_SYNTAX`, one past treffer’s limits a `RangeError` with a `TREFFER_MAX_*` code and `limit`/`actual` — on the same rule that already gives xprsn’s codes to a fault in an expression: the engine that decided the fault names it. A pattern that arrives from render data keeps RFC 9535 semantics and still matches nothing at render time. Requires padvinder 0.8.
- **Each engine relocates its own diagnostic.** A located error is now a copy made by the engine that raised it (xprsn, sjabloon, or padvinder), so it carries every field that engine puts on a diagnostic — nothing is lost in quario’s re-throw, today or when an engine adds a field — and it still passes that engine’s own `isDiagnostic`. Host errors are wrapped as plain errors with no diagnostic metadata, exactly as before. Requires xprsn 0.10, sjabloon 0.9, and padvinder 0.5. No report changes what it renders.
- **A host error cannot pose as the report’s diagnostic.** `report()` rethrows the first definition problem’s located engine error; it chose that error by probing for a `code` property, so a host error class that stamps `code` on itself in its constructor was rethrown as if the engines had raised it. The choice now goes through the same identity-based authentication as `isDiagnostic`; everything else falls back to a plain `SyntaxError` naming the first problem, as before.

0.1.0

### quario 0.1.0

### Minor Changes

- **A compile-once report engine.** You hand it a JSON report definition and data; it plans once and streams render events that every target consumes. Text cells go through sjabloon, everything else through xprsn, rows through padvinder. Unlicensed output carries a marking on the opening event; each target only decides where that wording goes.

## @quario/html

0.13.1

### @quario/html 0.13.1

### Patch Changes

- Updated dependencies
  - quario\@0.15.0

0.13.0

### @quario/html 0.13.0

### Minor Changes

- **An image item now accepts SVG bytes.** Before, `source` had to yield PNG or JPEG bytes, and SVG bytes were a render error. Now the engine also names UTF-8 text whose root element is `<svg` as an SVG, and the image event carries `format: "svg"`. The engine reads the size from the root’s `width` and `height` when both are absolute lengths, and else from its `viewBox`. An SVG that states no size this way is the same render error as a PNG that is too short to state its size. The message for bytes that are not an image now says “PNG, JPEG or SVG”.

  The HTML target puts an SVG in an `<img>` under `data:image/svg+xml`, so the SVG never becomes markup in the fragment. The PDF target draws an SVG as vector paths. It draws no picture that the SVG names, and it fetches nothing. SVG `<text>` draws in the standard Helvetica. The viewer paints an SVG through a `data:` URI, so a page that shows one needs the Content Security Policy grant `img-src data:`. A PNG or a JPEG still needs no grant there. The XLSX and DOCX targets cannot draw an SVG, so an SVG image is a render error that names the item.

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.
- **A row’s `align` now reaches its cells when you link `style.css`.** Before, the stylesheet set `text-align: left` on every `th` and `td`. That rule stopped the `align` on a header row, a detail row or a total block, which the target writes on the `<tr>`. A total block with `align: "right"` therefore rendered its cells left-aligned. The stylesheet now sets the left default on the row, so a row’s `align` reaches the cells. A cell’s own `align` still wins. A table without any `align` still reads left, `th` included.
- Updated dependencies
  - quario\@0.14.0

0.12.0

### @quario/html 0.12.0

### Minor Changes

- **Breaking: a report definition can no longer write markup.** This target now escapes the literal text of a definition as well as every interpolated value.

  Before, literal template text reached the fragment as live HTML:

  ```jsonc
  { "type": "text", "value": "<b>Total</b> {{ $.total }}" }
  ```

  That cell rendered a bold `Total`. Now it renders the text `<b>Total</b>`, followed by the escaped value. A header cell follows the same rule. So does a `<script>` or an `<img onerror>` in a report header, which is the point: a definition is untrusted for markup, as its data is. An agent can write one, and nothing it writes can execute.

  An authored entity is text too. `&amp;` in a definition shows as `&amp;`, and a plain `&` is escaped once, so it shows as `&`.

  To keep the formatting, declare it. Use `style` on the item, or split the cell into styled runs:

  ```jsonc
  {
    "type": "text",
    "value": [
      { "value": "Total", "style": { "bold": true } },
      { "value": " {{ $.total }}" }
    ]
  }
  ```

  There is no raw form and no switch to restore the old behaviour. `{{{ }}}` is still a definition error. Every other target already read literal text as text, so only this one changes.

- A document may now name its paper and turn it, and its margin may name each side.

  ```jsonc
  {
    "page": {
      "size": "letter",
      "orientation": "landscape",
      "margin": { "top": 36, "right": 29, "bottom": 36, "left": 29 }
    }
  }
  ```

  `page.size` takes `"A4"` or `"letter"`. A `[width, height]` pair stays a target option. `page.orientation` takes `"portrait"` or `"landscape"`, and it turns whichever paper is in force. `page.margin` still takes one number for all four sides.

  The rule `page.margin` already followed now covers all three keys. The document’s key wins where it declares one, a target option applies where it does not, and both declaring the same key is a render error. `orientation` in a document beside a target’s `size` is not both.

  Each target writes the paper: the PDF and the layout draw on it, HTML writes an `@page` size and per-side margin, and DOCX writes `pgSz` with `w:orient` and `pgMar` per side. XLSX writes the paper code, the orientation and each print margin. CSV withdraws all three.

  The capability descriptors gain `page.size` and `page.orientation`, and both are names a document may mark `required`.

  A target’s margin error now reads `expected a non-negative number, or { top, right, bottom, left }, that leaves the page room`.

- **Breaking: a split’s `style` now ranks over its band’s role default.** The PDF, the viewer, the editor and the HTML fragment now agree with the DOCX and XLSX targets and with the spec.

  Before, a split in a report header ignored the type it declared:

  ```jsonc
  {
    "type": "split",
    "style": { "size": 8, "bold": false },
    "slots": [
      { "type": "text", "value": "{{ $.input.seller }}" },
      { "type": "text", "value": "{{ $.input.customer }}" }
    ]
  }
  ```

  Its slots rendered bold at the report header’s 1.4× size. A group-header split with `"bold": false` rendered bold too. Now the slots render at 8pt and not bold, as the same declaration on each slot does. A split with no `style` renders as before.

  In the reference stylesheet, each band-role rule now selects `.q-<role>:not(.q-slot > *)`: a band’s text item or split, and never an item in a split’s slot. A slot’s item inherits its type from the split. Before, a slot’s item in a styled report-header split took 1.4× the split’s size. The group-header, group-footer and report-footer margins now also sit on a split rather than on each of its slots. A host rule on `.q-item.q-report-header` that loads after this sheet still wins.

### Patch Changes

- Updated dependencies
  - quario\@0.13.0

0.11.1

### @quario/html 0.11.1

### Patch Changes

- Updated dependencies
  - quario\@0.12.0

0.11.0

### @quario/html 0.11.0

### Minor Changes

- **A `capabilities` descriptor.** The target exports what it makes of each declaration the spec’s support matrix names — `resolved`, `approximated`, `withdrawn` or `unread` — beside its factory, so a host can ask before it renders rather than reading the prose. It is keyed by the schema name an author writes (`valign`, `break`, `page.margin`, and `column.width` / `slot.width` where one word carries two declarations), hand-written and gated against that matrix. It carries no version of its own: the package that ships it is the version.
- **`negative` and `zero` reach the markup edge.** The target stringifies through the engine’s `format()`, so a cell declaring either presents there too: a negative in parentheses, a zero as a dash.
- **A collapsed group does not draw its content.** An instance whose group declares `collapsed` renders as its header and footer, with the rows between them left out — a page has nowhere to put a row a reader could open. Aggregates are unchanged, so a footer still totals them.
- **A run that links becomes an anchor.** A run carrying `href` is wrapped in an `<a>` inside its own span, so the style the run declared paints the link, and the URL is escaped at the one markup edge like every other interpolated value. A real anchor rather than a styled span, because keyboard reachability comes with it. The engine admitted the URL before it crossed, so this target decides nothing about it.
- **The report header scales with the size it sits in.** The shipped stylesheet’s `.q-item.q-report-header` rule is `font-size: 1.4em` rather than `14pt`. A document declaring `"size": 20` now renders its report header at 28pt where it rendered at 14pt, smaller than its own body. This is visible to every host page: an unstyled report’s header is now 1.4× the host’s own body type rather than a fixed 14pt, so a host whose body is not 10pt sees the headline move. A host rule on the class still wins, as before.

### Patch Changes

- **A cell’s own `href` now links its text.** `href` sits on a cell as well as in a run’s inline subset, but this target read it only off a run that declared a style of its own, so `{ style: { href } }` on the cell rendered the text with no anchor at all. It now asks the style the run wears rather than the one it declares, which is the cell’s wherever the run is silent. A run declaring its own `href` still replaces the cell’s rather than nesting inside it.
- Updated dependencies
  - quario\@0.11.0

0.10.0

### @quario/html 0.10.0

### Minor Changes

- **A group’s `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.

  The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.

  **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.

  The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.

  The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.

  The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.

### Patch Changes

- Updated dependencies
  - quario\@0.10.0

0.9.0

### @quario/html 0.9.0

### Minor Changes

- **Every target factory now refuses an option it does not understand.** An unknown key, a key with a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx` already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding one.

  The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A fourth document property is one edit rather than three.

  **What this changes for you.** One options object spread across several targets stops working if any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`, `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works. TypeScript does not warn about this: excess-property checking fires on an object literal and not on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.

  Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and `{ meta: { title: config.title } }` over a config that carries neither are both fine. `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws instead of turning path stamping on. A `fonts` mapping given as an array is refused by `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.

  Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests `instanceof Error` is unaffected. One that compares the constructor is not.

  `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null` and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }` is a property you did not write rather than one of the wrong type, and an inherited enumerable key is no longer reported as an option you wrote.

  `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the object you passed no longer changes what a configured target writes.

### Patch Changes

- Updated dependencies
  - quario\@0.9.0

0.8.0

### @quario/html 0.8.0

### Minor Changes

- **A picture states its own size, so a page stops reflowing around it.** Each `<img>` now carries `width` and `height` attributes holding the image’s natural size in pixels, which lets a browser reserve the right aspect ratio before the picture decodes. The `fit` mapping is unchanged and still decides the rendered width, now paired with `height:auto` so the height follows the width instead of staying pinned: an image capped narrower than its file scales rather than squashing. Nothing an author writes changes, and no existing report renders differently once its images have decoded.

### Patch Changes

- **Every published README says where the documentation is.** Each package now carries a Documentation section pointing at the reference, at the report schema that normatively specifies what a report may declare, and at the package’s own API. The paragraphs that used to end on an unstated contract — the event stream’s field semantics, the style vocabulary, page columns, the Content Security Policy a fragment with images needs, the formula mangling, and each target’s own contract — link the page that states it. Every link is an absolute URL, so it resolves from the npm package page as readily as from an installed copy.
- Updated dependencies
  - quario\@0.8.0

0.7.0

### @quario/html 0.7.0

### Minor Changes

- **Styled runs render as spans.** A cell value written as a list of styled runs emits one `<span style="…">` per styled run, carrying that run’s whole resolved inline style; unstyled runs stay bare, so a cell with no runs emits exactly the markup it always did. Every interpolated value is escaped as before, and the span’s own CSS is escaped like any other attribute value.

- **A `format` inside a sentence now presents where the engine says it does.** A cell mixing literal text and one interpolation under a `format` renders the value plainly, as the spreadsheet and CSV targets already did; splitting the value into styled runs is how a value inside a sentence is formatted.

- **The reference stylesheet no longer paints a look no other target has.** `@quario/html/style.css` dropped the hairline under a table’s header row, the hairline and bold weight on its total rows, and the rule above the report footer. A report that declares no borders now renders without them on screen, which is what the PDF and the worksheet have always shown for the same report — the difference was the stylesheet’s alone, and it also surfaced as a cell whose computed border colour failed keeping a black stroke here while every other target drew nothing. The sheet now carries the band-role defaults, the table’s structure and the honor and pagination rules, and no look beyond them. The classes are unchanged, so a host that wants the old look adds three rules to its own sheet after the shipped one:

  ```css
  .q-table thead th {
    border-bottom: 0.5pt solid #000;
  }
  .q-table tfoot td {
    border-top: 0.5pt solid #000;
    font-weight: bold;
  }
  .q-item.q-report-footer {
    border-top: 1pt solid #000;
    padding-top: 4pt;
  }
  ```

0.6.0

### @quario/html 0.6.0

### Minor Changes

- **A cell’s own `currency` code is honoured** at the markup edge, ahead of the instance’s default, so a listing whose rows arrive in different currencies presents each in its own. A code the engine could not accept leaves the cell as plain display text rather than presenting it in the instance’s currency.
- **Numbers presented through `format` now show a fixed two fraction digits, matching every other target.** `format: "number"` on `1000` renders `1,000.00` where it rendered `1,000`, and `0.12345` renders `0.12` where it rendered `0.123`; `format: "percent"` on `0.21` renders `21.00%` where it rendered `21%`. `format: "currency"` follows the currency’s own minor units, so a JPY amount loses the two decimals it never had. `date` is unchanged, as are grouping separators and symbol placement. The digits come from the engine, so a cell reads the same here as it does in a workbook.

0.5.0

### @quario/html 0.5.0

### Minor Changes

- **A spanning cell emits `colspan`**, ahead of its `style`; a span of one emits no attribute. The `<colgroup>` still names every column, so the geometry is the document’s and only the cells merge — which means the header row can carry fewer `<th>` than there are columns.
- **`valign`** renders as `vertical-align`, on a cell or on the row’s `<tr>`, which the browser’s own `inherit` on cells carries down. On a split slot the item itself becomes a grid with `align-content`, so the content moves inside the box the slot wrapper already stretches.
- **A row’s box now reaches the cells** instead of being dropped. Padding and border on `detail.header`, `detail.row` or a total row’s `style` are emitted inline on each `<th>`/`<td>`, where they also beat the reference stylesheet’s own cell padding. The `<tr>` still carries the row’s other declarations and still never carries a box, which it could not honour. Fragments of documents that declared a box on a row gain borders and padding they previously rendered without.
- **Unstyled table cells sit at the top of their row.** The reference stylesheet now pins `vertical-align: top` on `.q-table tr`, which the cells inherit. Before, a browser centred the shorter cells of a wrapping row while the PDF painted them from the top, so the same report read differently in the two. A host stylesheet overrides it the ordinary way.

0.4.0

### @quario/html 0.4.0

### Minor Changes

- **A split slot’s box now fills the split’s height.** Before, a slot’s background and border stopped at that slot’s own content, leaving a gap under any slot shorter than its neighbours. The slot wrapper now carries inline `display:grid`, so the item inside it fills the row; a slot’s item markup is unchanged. See the `@quario/layout` changelog for the rule and for what it costs a report that relied on the short box.
- **A date string under `format: "date"` now presents as a date.** The engine’s `format()` helper revives the two read forms, so a cell that rendered `2026-08-14` verbatim now renders the presented date. See the `quario` changelog for the forms and the timezone rule.

0.3.0

### @quario/html 0.3.0

### Minor Changes

- **`format` stringifies at the markup edge.** The kind is presented through the engine’s `format()` helper from the instance locale, then escaped like any other interpolated value.
- **`page.margin` becomes `@page{margin:<n>pt}`.** Report-header `height` is unread: a CSS height would pin in the body box, not from the page top.
- **`spaceBefore` / `spaceAfter` map to `margin-top` / `margin-bottom`.** Report and group containers are flex columns so adjacent authored gaps add rather than collapse.
- **Padding and complete border sides map to inline CSS.** `box-sizing:border-box` rides the same attribute. A side that does not resolve all three names emits nothing, so a missing colour never becomes the browser’s solid black. Header-row, row, and total-row boxes are withdrawn from `<tr>` — CSS does not box a table row — while other row styles still inherit.
- **`<tfoot>` holds every total row.** Extra rows are extra `<tr>`; the section still closes at `table-end`.
- **A visible text item occupies a line, and a literal newline is a break.** Before, an empty item collapsed and a newline became a space. The honor rules now hang off `.q-item` and `.q-table th, td` in the reference stylesheet, the same pattern as `.q-break` — class is the hook, the rule is overridable. Empty table cells still collapse. Host CSS still owns leading; this is not `line-height: 1.4`.
- **A declared `false` flag now emits its CSS off-value.** `bold: false` used to emit nothing, so the reference stylesheet’s band-role `font-weight: bold` still won on screen while PDF rendered regular. Exact `false` now writes `font-weight:normal` (and the matching off-value for italic, uppercase, and the decorations), which beats the sheet. Omit and a non-boolean result still emit nothing.

0.2.0

### @quario/html 0.2.0

### Minor Changes

- **`html({ fonts })` says what a family name means on your page.** A report declares the typeface it wants — `family: "Instrument Sans"`, or the portable `family: "mono"` — and this option resolves that name to whatever CSS means on the page it is displayed in: `html({ fonts: { "Instrument Sans": "var(--font-instrument-sans)" } })`. Any CSS `font-family` value works, so a custom property, a font stack or a quoted name are all fine; a value containing `;` or `}` throws from `html()` rather than at render. Names match case-insensitively, and `sans`, `serif` and `mono` can be mapped like any other, which is what lets one definition say “the mono one” and each surface answer for itself. Useful wherever your faces are registered under names you do not control — a build tool that hashes `@font-face` names, for instance. The mapping resolves families a report declares; the face of text that declares none stays your stylesheet’s, so it is still one CSS rule away.
- **The report default rides a report root.** Every fragment is now wrapped in one `<div class="q-report">`, and a report that declares a `style` carries it there as an inline `font-family`/`font-size` — written once for the document rather than repeated on every item, with CSS inheritance doing the rest. The unlicensed marking stays outside the root, as the fragment’s first element and the root’s sibling, so no authored style can reach it.
- **`uppercase` maps to `text-transform:uppercase`.** The markup carries the text exactly as authored and only its rendering is capitalised, so what a reader selects, copies, or hears from a screen reader is your own casing.
- **Splits render as a flex row of sized slots.** A [split](https://getquario.com/docs) becomes `<div class="q-split q-<role>">` carrying inline `display:flex`, with each slot a `<div class="q-slot">` sized inline — `width:<n>%` for an authored share, `flex:1` for a width-less one. The slot item inside keeps its ordinary `q-item` container, so an item’s markup never depends on where it sits. Placement is emitted inline for the same reason page columns are: it is document structure, not one of the visual defaults this target leaves to your stylesheet.
- **`html({ paths: true })` maps rendered output back to the schema.** Each element whose event carries a schema path gains `data-q-path`: item and image containers, each group instance’s container, the table, and every `<th>`/`<td>` (the column definition’s path; total cells their own entry). One definition renders many times, so one path appears on every instance of it. Escaped like every generated attribute, and off by default — the attribute is weight a plain display pays for nothing.
- **A family name that could not be written as CSS now falls back instead of being rewritten.** A `family` carrying characters that cannot appear in a family name was stripped down to whatever survived and emitted anyway, which named a face that could not exist — `family: "var(--my-font)"` became `font-family:'var--my-font'`. Such a name now contributes nothing, so the text renders in the face above it: the report default, or your stylesheet’s baseline. Ordinary names are unaffected. To resolve a name your page knows under a different one, map it with `html({ fonts })`.
- **The fragment now has a root element.** `<div class="q-report">` wraps every render, whether or not a report declares a default. Host CSS that assumed items and tables were the fragment’s top-level children needs a descendant selector; nothing else about the markup moved.
- **The reference stylesheet’s baseline face moved onto `.q-report`.** It was on `.q-item`, `.q-table` and `.q-unlicensed` — a direct rule, which would have beaten the report default an author writes on the document. The marking keeps its own rule, being outside the root. Same look, one rule up.
- **A bare `Date` escapes to ISO 8601 UTC, the same on every machine.** Interpolated `Date` values rendered through `String(date)`, which bakes the host’s timezone and locale into the markup. They now render as `toISOString()` text through the engine’s shared display rule — escaped like every other value — so the same report produces the same HTML everywhere. Formatted dates keep coming from registered functions.
- **The reference stylesheet sets a base font family.** A fragment dropped into a page with no font of its own inherited the browser default — a serif — where every other target rendered a grotesque. `@quario/html/style.css` now sets `sans-serif` on report text, as an ordinary rule any host overrides by source order.

0.1.0

### @quario/html 0.1.0

### Minor Changes

- **HTML fragments for compiled reports.** Semantic tables, stable `q-*` classes, every interpolated value escaped. Pass `html()` to `report.render` and wrap the fragment in your own page. The default look ships as `@quario/html/style.css`, not as inline style in the markup.

## @quario/pdf

0.14.0

### @quario/pdf 0.14.0

### Minor Changes

- Breaking: every instance of a table now shares one set of column widths. Before, a table under a group sized its columns from the content of each instance alone. The columns therefore moved left and right from one group to the next. Now the layout sizes each column from the widest content that column holds in any instance of the render. The columns line up down the document. A table that renders once keeps the widths its own content asks for. The text that a report draws does not change, but the PDF of a report with a table under a group changes its geometry. The viewer and the editor show the same change, because they draw the same layout.

  The layout reads the event stream once, as before. It measures the votes of a repeated table before it places the first instance, and it uses those measurements again when it places the rows. On the benchmark report, a flat report and a grouped report take the same time as before. A grouped report with no repeated cell text takes about 12% longer.

### Patch Changes

- A link to `#` and a name goes only to a group instance that declares that name as its `label`. Before, the PDF and the viewer also matched an instance with no `label` whose outline title read the same, which is its first header’s text. A link to a name that no instance declares now goes nowhere, as the spec says.

  In `@quario/layout`, a `Mark` carries `label` where its instance declares one. The new `destinations(marks)` and `destination(href, byLabel)` say where a link goes. The PDF target and the viewer use them.

- Updated dependencies

  - quario\@0.15.0
  - @quario/layout\@0.11.0

0.13.0

### @quario/pdf 0.13.0

### Minor Changes

- **An outline step now lands on its group instance.** Before, the layout gave each outline entry a `y` that the target measured up from the page bottom. The `Mark` type said that `y` reads down from the top, as an op’s `y` does. The viewer read it that way, so an outline row or an internal link scrolled near the foot of the page and not to the instance. The layout now gives `mark.y` from the top of the page. The viewer step lands on the header of the instance. The PDF target turns the value over for each bookmark and link destination, so the PDF bytes do not change. A host that reads `marks` from the layout target and turned `y` over itself must stop doing that.

- **An image item now accepts SVG bytes.** Before, `source` had to yield PNG or JPEG bytes, and SVG bytes were a render error. Now the engine also names UTF-8 text whose root element is `<svg` as an SVG, and the image event carries `format: "svg"`. The engine reads the size from the root’s `width` and `height` when both are absolute lengths, and else from its `viewBox`. An SVG that states no size this way is the same render error as a PNG that is too short to state its size. The message for bytes that are not an image now says “PNG, JPEG or SVG”.

  The HTML target puts an SVG in an `<img>` under `data:image/svg+xml`, so the SVG never becomes markup in the fragment. The PDF target draws an SVG as vector paths. It draws no picture that the SVG names, and it fetches nothing. SVG `<text>` draws in the standard Helvetica. The viewer paints an SVG through a `data:` URI, so a page that shows one needs the Content Security Policy grant `img-src data:`. A PNG or a JPEG still needs no grant there. The XLSX and DOCX targets cannot draw an SVG, so an SVG image is a render error that names the item.

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.

- Updated dependencies

  - quario\@0.14.0
  - @quario/layout\@0.10.0

0.12.0

### @quario/pdf 0.12.0

### Minor Changes

- A document may now name its paper and turn it, and its margin may name each side.

  ```jsonc
  {
    "page": {
      "size": "letter",
      "orientation": "landscape",
      "margin": { "top": 36, "right": 29, "bottom": 36, "left": 29 }
    }
  }
  ```

  `page.size` takes `"A4"` or `"letter"`. A `[width, height]` pair stays a target option. `page.orientation` takes `"portrait"` or `"landscape"`, and it turns whichever paper is in force. `page.margin` still takes one number for all four sides.

  The rule `page.margin` already followed now covers all three keys. The document’s key wins where it declares one, a target option applies where it does not, and both declaring the same key is a render error. `orientation` in a document beside a target’s `size` is not both.

  Each target writes the paper: the PDF and the layout draw on it, HTML writes an `@page` size and per-side margin, and DOCX writes `pgSz` with `w:orient` and `pgMar` per side. XLSX writes the paper code, the orientation and each print margin. CSV withdraws all three.

  The capability descriptors gain `page.size` and `page.orientation`, and both are names a document may mark `required`.

  A target’s margin error now reads `expected a non-negative number, or { top, right, bottom, left }, that leaves the page room`.

### Patch Changes

- **Text sits in the middle of its line.** A line is 1.4 times its largest font size. Before, all of that extra space went below the text, so a capital touched whatever stood above its line, an image for example. Now half of the extra space goes above the text and half below, as a browser lays out a line.

  Every line keeps its height, so every page breaks where it did before. Each line of text is drawn 0.2 times its font size lower than before. The PDF, the viewer and the editor all change, because they draw the same layout. The HTML target already split the space this way.

- The PDF target now uses `@cantoo/pdf-lib` 2.11. The pages draw the same text in the same places. The bytes differ, because the library compresses streams with a different deflate implementation. A corrupt PNG still gives a located render error that keeps the class of the decoder error. That class and its message now come from the new decoder, so a corrupt PNG can give an `Error` where it gave a `RangeError` before.

- Updated dependencies

  - quario\@0.13.0
  - @quario/layout\@0.9.0

0.11.1

### @quario/pdf 0.11.1

### Patch Changes

- Updated dependencies

  - quario\@0.12.0
  - @quario/layout\@0.8.1

0.11.0

### @quario/pdf 0.11.0

### Minor Changes

- **A `capabilities` descriptor.** The target exports what it makes of each declaration the spec’s support matrix names — `resolved`, `approximated`, `withdrawn` or `unread` — beside its factory, so a host can ask before it renders rather than reading the prose. It is keyed by the schema name an author writes (`valign`, `break`, `page.margin`, and `column.width` / `slot.width` where one word carries two declarations), hand-written and gated against that matrix. It carries no version of its own: the package that ships it is the version.
- **A collapsed group does not draw its content.** An instance whose group declares `collapsed` renders as its header and footer, with the rows between them left out — a page has nowhere to put a row a reader could open. Aggregates are unchanged, so a footer still totals them.
- **A group’s `label` titles its bookmark.** The document writes the layout’s outline marks, so an instance that declares a label is bookmarked under it rather than under its first header line.
- **A linked run becomes a link annotation.** Each rectangle the layout drew for a linked run gets a `/Link` annotation with no border: a URI action for an external URL, and a `/Dest` to the outline entry’s own destination where the `href` names a group’s `label`. A link to a label no instance carried is dropped rather than invented.
- **The report header scales with the report default.** The document paints the layout’s list, and there the band-role size is 1.4× the size it sits over rather than a fixed 14. A document declaring no size keeps its bytes. A document declaring `"size": 20` now renders its report header at 28 where it rendered at 14, smaller than its own body.

### Patch Changes

- Updated dependencies

  - quario\@0.11.0
  - @quario/layout\@0.8.0

0.10.0

### @quario/pdf 0.10.0

### Minor Changes

- **A group’s `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.

  The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.

  **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.

  The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.

  The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.

  The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.

### Patch Changes

- A font parser that will not load no longer reports itself as a missing package.

  Passing `fonts` needs the optional peer `fontkit`, and every failure to load it used to arrive as `options.fonts: install fontkit to embed TrueType families` — advice a host had often already taken, with the real error discarded. A stale bundler cache, a 504 from a dev server or a throw from inside the parser all read as an install that was already correct.

  The target no longer loads the parser itself. It awaits the one `@quario/layout` loads to measure the same faces, so both halves of a render report a failure in one wording:

  ```
  options.fonts: the font parser did not load. Install fontkit to use TrueType families
  ```

  The error that actually threw is attached as `cause`. Before, the two halves loaded the parser separately and concurrently, so one failure produced two errors and a host saw whichever won the race.

  A face `fontkit` reads but pdf-lib refuses keeps its message unchanged — still `options.fonts.<family>.<variant>:` followed by what pdf-lib said — and now carries pdf-lib’s own error as `cause` as well.

- Updated dependencies

  - @quario/layout\@0.7.0
  - quario\@0.10.0

0.9.0

### @quario/pdf 0.9.0

### Minor Changes

- **Every target factory now refuses an option it does not understand.** An unknown key, a key with a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx` already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding one.

  The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A fourth document property is one edit rather than three.

  **What this changes for you.** One options object spread across several targets stops working if any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`, `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works. TypeScript does not warn about this: excess-property checking fires on an object literal and not on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.

  Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and `{ meta: { title: config.title } }` over a config that carries neither are both fine. `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws instead of turning path stamping on. A `fonts` mapping given as an array is refused by `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.

  Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests `instanceof Error` is unaffected. One that compares the constructor is not.

  `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null` and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }` is a property you did not write rather than one of the wrong type, and an inherited enumerable key is no longer reported as an option you wrote.

  `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the object you passed no longer changes what a configured target writes.

### Patch Changes

- The package now accepts only a version of `@cantoo/pdf-lib` it can import. The declared range is `~2.9.1` rather than `^2.9.1`.

  Earlier versions accepted `@cantoo/pdf-lib` 2.11.0, whose ESM build imports its font metric JSON without an import attribute. A fresh install could resolve to it, and importing `@quario/pdf` then failed on Node before rendering anything, with `TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]` naming a `.compressed.json` file inside that package. Node 22 and Node 24 both report it. If you saw that error, reinstall — nothing in your own code has to change.

- Updated dependencies

  - quario\@0.9.0
  - @quario/layout\@0.6.0

0.8.0

### @quario/pdf 0.8.0

### Minor Changes

- **An image’s box is now as wide as the container it sits in.** A `background` or `border*` on an image item hugged the picture and now spans the content width, or the slot’s share inside a split; the picture itself does not move. See the `@quario/layout` changelog for the rule and for what it costs a report that relied on a border hugging a logo.

### Patch Changes

- **Every published README says where the documentation is.** Each package now carries a Documentation section pointing at the reference, at the report schema that normatively specifies what a report may declare, and at the package’s own API. The paragraphs that used to end on an unstated contract — the event stream’s field semantics, the style vocabulary, page columns, the Content Security Policy a fragment with images needs, the formula mangling, and each target’s own contract — link the page that states it. Every link is an absolute URL, so it resolves from the npm package page as readily as from an installed copy.

- Updated dependencies

  - @quario/layout\@0.5.0
  - quario\@0.8.0

0.7.0

### @quario/pdf 0.7.0

### Minor Changes

- **Styled runs reach the page.** A cell value written as a list of styled runs draws each run in its own face, size and colour, underlines and strikes through per run rather than across the line, and paints a run’s `background` as a highlight behind its text.
- **A `format` inside a sentence now presents where the engine says it does.** A cell mixing literal text and one interpolation under a `format` draws the value plainly, as the spreadsheet and CSV targets already did; splitting the value into styled runs is how a value inside a sentence is formatted.

0.6.0

### @quario/pdf 0.6.0

### Minor Changes

- **A cell’s own `currency` code is honoured**, ahead of the instance’s default, so a listing whose rows arrive in different currencies presents each in its own. A code the engine could not accept leaves the cell as plain display text rather than presenting it in the instance’s currency.
- **Numbers presented through `format` now show a fixed two fraction digits, matching every other target.** `format: "number"` on `1000` renders `1,000.00` where it rendered `1,000`, and `0.12345` renders `0.12` where it rendered `0.123`; `format: "percent"` on `0.21` renders `21.00%` where it rendered `21%`. `format: "currency"` follows the currency’s own minor units, so a JPY amount loses the two decimals it never had. `date` is unchanged, as are grouping separators and symbol placement. The digits come from the engine, so a cell reads the same here as it does in a workbook.
- **An image these bytes cannot embed now fails with the item’s name on it.** A PNG whose pixel data is corrupt past the size in its header used to reject the render with `Invalid typed array length: 0` — the PDF writer’s own words, naming neither an image nor an item, so a report with two pictures gave no way to tell which was at fault. The message now begins with the item’s `source` path and says the image could not be embedded, keeping the writer’s account after it and on `cause`. A JPEG is unchanged: its header is read but its pixel data never is, so a corrupt one still embeds without complaint.

0.5.0

### @quario/pdf 0.5.0

### Minor Changes

- **A spanning cell draws as one box** across the columns it covers, through `@quario/layout`. It takes no part in measuring them, and a spanning row that slices across pages or page-column strips follows the geometry of the strip each slice lands in.
- **`valign`** on table cells and split slots, through `@quario/layout`: middle and bottom place the content in the height its row or split leaves over it.
- **A row’s box is drawn by the row’s cells**, through `@quario/layout`. A box declared on a table row used to be one rect across the columns; each covered cell now draws its own. A row’s `borderBottom` still reads as one continuous edge, a row’s `borderLeft` becomes an edge on every cell rather than one at the row’s outer left, and a bordered row is taller by its border’s width, since a border occupies height as a cell’s always has.
- **Embedding TrueType fonts now needs `fontkit`, not `@pdf-lib/fontkit`.** Install `fontkit` instead; nothing else about `options.fonts` changes. The old package’s bundle crashed with a bare `ReferenceError` on any OpenType face needing a shaping state machine — which is every Devanagari, Bengali, Tamil, Khmer or Myanmar webfont in practice, and some fifty further scripts besides. Those faces now measure, embed and draw. Which scripts a face supports remains the font’s and the parser’s to answer, not this package’s.
- **`pdf-lib` is now `@cantoo/pdf-lib`.** A maintained fork, and what `fontkit`’s subsetting requires. Rendered documents are unchanged in what they draw: every drawn string is identical and every filled area lands in the same place, but the file’s bytes differ, so a host comparing digests against stored output will see them move once.

0.4.0

### @quario/pdf 0.4.0

### Minor Changes

- **A split slot’s box now fills the split’s height.** A slot’s background and border stopped at that slot’s own content and now take the split’s height, the way a table cell’s take their row’s. See the `@quario/layout` changelog for the rule and for what it costs a report that relied on the short box.
- **A date string under `format: "date"` now presents as a date.** The drawn text comes from the engine’s `format()` helper, which revives the two read forms. See the `quario` changelog for the forms and the timezone rule.
- **The layout moved to `@quario/layout`; this target paints its list.** Measurement, wrapping, pagination, page furniture, the marking’s geometry and the page-size table now live in the layout package, which this target depends on and runs first; pdf-lib writes the resulting display list. **The move itself changed no output**, byte for byte, for the same input; the fix below is a separate change and does move some. The `page` and `fonts` options are the layout’s own, so the same object configures `pdf()`, the viewer and the editor. The font mapping’s shape is now refused at the factory call rather than at render.
- **A nested group’s footer keeps the page columns.** Inside a page-column region, the first footer of a group nested in it ended the region: the strips collapsed and every band after it was laid out across the page, as if `columns` were absent. Only the node that declared `columns` closes its own region now — a nested group’s header and footer are region content, laid out in the strips.
- **A padded or bordered item or row that breaks across a page keeps its box.** Content too tall for one page is drawn as slices, and a slice now carries the box sides the break left it — the top on the first, the bottom on the last. Previously an item that overflowed lost its box entirely, so the same declaration drew a border on short data and none on long; a bordered table cell drew all four sides on every slice, putting an edge across the middle of its own text; and a row’s own border vanished when the row was too tall for a page. A slice’s lines are also drawn inside the horizontal padding they were wrapped for, rather than flush to the column. **This changes output** for reports where such content breaks.

0.3.0

### @quario/pdf 0.3.0

### Minor Changes

- **`format` stringifies at the edge from the instance locale.** A PDF without a host locale still uses `en-US` / UTC, so the bytes stay reproducible.
- **A height-declared report header pins from the page top.** Leftover sits under the items; the next band starts at the pin. The half-line group gap drops after it. `page.margin` on the document is the inset when the host omits it; both is a render error.
- **`spaceBefore` / `spaceAfter` skip the cursor.** Adjacent gaps add. `spaceBefore` drops at a fresh body page or strip top; page-band items keep it.
- **The box is honoured in layout.** Padding and border inset wrap width; a named padding `0` beats the cell omakase (`PADX` 6 / `PADY` 2) on that side. Incomplete sides draw nothing.
- **The last data row keeps the whole emitted total block.** A stack taller than a page degrades like a tall row.
- **An unstyled table has no rules.** This target used to draw two 0.5 pt rules, under the header and above the total, that no declaration could ask for or refuse. Those strokes are gone. The strokes an author wants are the box they declared. Cell padding omakase stays on undeclared cell sides.
- **A visible text item occupies a line at its own `size`, empty or not.** An empty or whitespace-only value used to sit at the report’s base leading (\~14 pt at the 10 pt baseline). It now occupies `1.4 ×` the item’s size, the same as a glyph line. A literal newline is a line break; the blank line among `"a\n\nb"` is that size too. A table cell that is empty or only horizontal whitespace still has no content height.
- **A missing TrueType glyph draws as `?`, not `.notdef`.** Characters outside WinAnsi already substituted `?`; an embedded face’s cmap holes drew a box instead, and text extraction hid it. Same rule for every face: a character the face cannot draw becomes `?`.
- **CR, LF, and CRLF are one hard line break.** Cell text already broke on LF; a CRLF left a CR on the previous line, and a lone CR did not break. All three are now one break, matching `SCHEMA.md` Cell values. Wrapping still applies within each line.

0.2.0

### @quario/pdf 0.2.0

### Minor Changes

- **The report default sets the document’s face and base size.** A report’s top-level `style` replaces this target’s own baseline, so its `size` scales row heights and band gaps with the type rather than leaving them at a size nothing is set in. It is the layer *under* the band-role defaults: a report declaring `size: 12` still renders its report header at 14, and an item’s own style wins over both.
- **`uppercase` draws capitals.** With no text-transform to defer to, this target capitalises the string before measuring it, so wrapping and column widths are those of the text actually drawn. The mapping is Unicode default case, never the host’s locale, so output stays byte-reproducible.
- **Splits lay out across the content width.** Each slot is measured and wrapped inside its own share, all slots draw from a common top, and the split takes the height of its tallest slot so the band below clears them all. **A split is never broken across a page**: one that does not fit the remaining height moves whole, exactly as an image does, and one taller than any page renders in full past the bottom margin. The split’s own style is the layer under each slot’s, and its `background` fills the whole split behind them.
- **A bare `Date` draws as ISO 8601 UTC, the same on every machine.** Cell text and outline bookmark titles for `Date` values used `String(date)`, which bakes the host’s timezone and locale into the document — at odds with this target’s byte-reproducibility guarantee. Both now render through the engine’s shared display rule, so a `Date` group key titles its bookmark with the same ISO text its cells draw.
- **`options.baseSize`.** A document’s type size is the document’s own, so it is the report’s `style.size` — portable, travelling with the definition to every target — rather than a host option one target honoured. Text with nothing declared still renders at 10 points. Replace `pdf({ baseSize: 11 })` with `"style": { "size": 11 }` on the report.

0.1.0

### @quario/pdf 0.1.0

### Minor Changes

- **Paginated PDFs from the same compiled report.** The band flow places headers, groups, and tables; pdf-lib writes the file. Page furniture and an outline from the group tree. Optional TrueType embedding through `@pdf-lib/fontkit`.

## @quario/xlsx

0.12.1

### @quario/xlsx 0.12.1

### Patch Changes

- `xlsx({ page })` takes the same `page` option as the other targets with a page. `page.margin` takes one number for all four sides, or an object with `top`, `right`, `bottom` and `left`. Before, the target refused the object. The target now refuses a paper name that it does not know, such as `"A5"`. Before, it accepted the name and printed on the reader’s default paper. The target also refuses a margin that leaves the page no room. A document and a host that both declare a page key now cause a `TypeError`, as in the other targets.
- Updated dependencies
  - quario\@0.15.0

0.12.0

### @quario/xlsx 0.12.0

### Minor Changes

- **An image item now accepts SVG bytes.** Before, `source` had to yield PNG or JPEG bytes, and SVG bytes were a render error. Now the engine also names UTF-8 text whose root element is `<svg` as an SVG, and the image event carries `format: "svg"`. The engine reads the size from the root’s `width` and `height` when both are absolute lengths, and else from its `viewBox`. An SVG that states no size this way is the same render error as a PNG that is too short to state its size. The message for bytes that are not an image now says “PNG, JPEG or SVG”.

  The HTML target puts an SVG in an `<img>` under `data:image/svg+xml`, so the SVG never becomes markup in the fragment. The PDF target draws an SVG as vector paths. It draws no picture that the SVG names, and it fetches nothing. SVG `<text>` draws in the standard Helvetica. The viewer paints an SVG through a `data:` URI, so a page that shows one needs the Content Security Policy grant `img-src data:`. A PNG or a JPEG still needs no grant there. The XLSX and DOCX targets cannot draw an SVG, so an SVG image is a render error that names the item.

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.
- Updated dependencies
  - quario\@0.14.0

0.11.0

### @quario/xlsx 0.11.0

### Minor Changes

- A document may now name its paper and turn it, and its margin may name each side.

  ```jsonc
  {
    "page": {
      "size": "letter",
      "orientation": "landscape",
      "margin": { "top": 36, "right": 29, "bottom": 36, "left": 29 }
    }
  }
  ```

  `page.size` takes `"A4"` or `"letter"`. A `[width, height]` pair stays a target option. `page.orientation` takes `"portrait"` or `"landscape"`, and it turns whichever paper is in force. `page.margin` still takes one number for all four sides.

  The rule `page.margin` already followed now covers all three keys. The document’s key wins where it declares one, a target option applies where it does not, and both declaring the same key is a render error. `orientation` in a document beside a target’s `size` is not both.

  Each target writes the paper: the PDF and the layout draw on it, HTML writes an `@page` size and per-side margin, and DOCX writes `pgSz` with `w:orient` and `pgMar` per side. XLSX writes the paper code, the orientation and each print margin. CSV withdraws all three.

  The capability descriptors gain `page.size` and `page.orientation`, and both are names a document may mark `required`.

  A target’s margin error now reads `expected a non-negative number, or { top, right, bottom, left }, that leaves the page room`.

### Patch Changes

- Updated dependencies
  - quario\@0.13.0

0.10.1

### @quario/xlsx 0.10.1

### Patch Changes

- Updated dependencies
  - quario\@0.12.0

0.10.0

### @quario/xlsx 0.10.0

### Minor Changes

- **A `capabilities` descriptor.** The target exports what it makes of each declaration the spec’s support matrix names — `resolved`, `approximated`, `withdrawn` or `unread` — beside its factory, so a host can ask before it renders rather than reading the prose. It is keyed by the schema name an author writes (`valign`, `break`, `page.margin`, and `column.width` / `slot.width` where one word carries two declarations), hand-written and gated against that matrix. It carries no version of its own: the package that ships it is the version.

- **`negative` and `zero` write a sectioned number format.** A cell that declares either gets `positive;negative;zero` — `#,##0.00;(#,##0.00);"-"` — so the grid presents what the page presents **and the cell stays a number**, where a host formatting recipe producing the same text would leave text where an amount belongs. A declaration that names neither writes one section, so no existing sheet moves.

- **A collapsed group keeps its rows and hides them.** This is the one target that can: the content rows are written hidden at their outline level and the group is shut, so a reader opens it again. The control goes on the row after the group, which is the instance’s footer where it declares one. Needs `werkmap@^0.7.0`.

- **A group’s `label` names the worksheet a boundary opens.** Where an instance declares one, the tab takes it in place of the key’s display text and goes through the same sanitising, so an author naming a worksheet chooses the words rather than the bytes. A label that survives none of it falls back to `Report`, exactly as an unspellable key does.

- **The report header scales with the report default.** A report-header cell renders at 1.4× the report default’s size rather than a fixed 14. A document declaring no size keeps its worksheet byte for byte, because 1.4× the baseline 10 is the same 14. A document declaring `"size": 20` now renders its report header at 28 where it rendered at 14, smaller than its own body.

- **A cell that points at one place is a hyperlink.** A cell carries one hyperlink and a rich-text run carries none, so a cell links where all of its text points somewhere — the cell’s own `href`, a lone run’s, or several runs that agree — and a run pointing inside a longer cell stays inert, the way a run-level `background` does. A URL leaves the workbook as an external relationship. An `href` naming a group’s `label` points at the worksheet that label opened, resolved once every worksheet is open, so a report may point forward through itself. A label that opened no worksheet drops its link and leaves the text as it is.

  The support matrix recorded this target as withdrawing `href`, and it now approximates it. The writer moves to `werkmap` 0.8, which is where the cell-level hyperlink comes from.

- **Page bands print.** `page.header` and `page.footer` become the worksheet’s print header and footer, on every printed page, with nothing added to the grid. The target reads each band at page 1 and at page 2 of 2, as the DOCX target does, so a bare `{{ page.number }}` or `{{ page.total }}` becomes the reader’s own live page number or page count, anything computed from a page value freezes at that reading, and a band that differs on the first page gives the first page its own header or footer — the cover-page idioms resolve exactly. Text items only, each item one line, in the section its `align` names, with `bold` and `size` as its look. A split, an image and every other style in a page band are withdrawn. A band that stores past the format’s 255 characters is a render error naming the band. Before this, the target left page bands unread.

- **The group tree reaches the worksheet as row outline levels.** An instance’s header and footer items sit at its parent’s outline level and everything between them one level deeper, so a reader collapses and expands an instance from the worksheet’s margin and keeps its header and footer in view, as a subtotal keeps its label. Nothing is collapsed or hidden, and an ungrouped worksheet’s bytes do not move. A grouped worksheet also states the format’s default row height, 15 points, because the format requires one beside the levels. Before this, the worksheet kept the group tree as row order alone.

### Patch Changes

- **The autofilter is the sort affordance too, and the docs now say so.** No behaviour changes: an Excel autofilter has always sorted as well as filtered. The option’s documentation states it, beside the hazard it carries — quario writes every value once as a literal, so a reader who reorders the region sees a running total that no longer runs.
- Updated dependencies
  - quario\@0.11.0

0.9.0

### @quario/xlsx 0.9.0

### Minor Changes

- **A worksheet column now fits what it holds.** Until now every column came out at the application’s own default width, so a long product name and a short amount got the same column and the reader widened both by hand. The target sizes each column from the content that column actually carries.

  A cell votes with a count of the characters it shows, and the widest vote wins. A table’s own cells vote: the header cells, the data cells and the total cells. A band item, a split slot, an image anchor and the unlicensed marking row do not, because none of them is a table column — a band item is a line of prose that lands in column A, and a data column stretched to fit prose is not what the report asked for. A cell spanning several columns stays silent too, which is the rule a span already followed everywhere else.

  Text votes its longest line, so a cell that wraps gets a column wide enough for the widest line rather than the whole string. A number and a date hold no text of their own, because the reader’s application writes the value out — so each votes the characters quario presents it as, the same text the same declaration produces on a rendered page. That is what keeps an amounts column or a dates column from arriving too narrow and showing `####`. A boolean votes nothing, because the reader’s application spells `TRUE` and `FALSE` in the reader’s own language.

  Because a number and a date are measured as quario presents them, a vote follows the locale and time zone the render was configured with. The same report and the same data still produce the same file.

  The winning vote gains one character of slack, and a column stops at 60 characters, so one very long cell cannot produce a column nobody can scroll past. A column nothing voted on is left alone and keeps the application default.

  An authored column `width` percentage still resolves to nothing here, and that has not changed. A percentage is a share of a table’s width, and a worksheet’s columns belong to the whole sheet rather than to one table. The measuring never reads it. All of this is best-effort, like every other presentation number this target picks that no author wrote, and it may change without a breaking change.

- `xlsx({ filter: true })` puts an autofilter over a worksheet’s one table.

  The range covers the table’s header row and the data rows under it. It adds the reader’s dropdown controls and removes no row, so every row you wrote is still in the document.

  A total row sits outside the range. A total is not data, and a filter that hides one surprises a reader. A filter that sorts it into the middle of the data is worse.

  A worksheet that holds more than one table filters nothing. A grouped report renders one table per group instance, so this is the ordinary shape rather than an edge case. An autofilter needs one header row over one contiguous body, and such a worksheet has neither. Covering the first table alone would leave you working a control that does not reach the rest.

  A `break` is how you get one. It opens a worksheet for each instance, and each of those holds one table. A report without a table filters nothing, and neither does a table with no data row.

  The key is off unless you ask for it. Switching it on for every report would put a dropdown row into every workbook this target writes, and that changes what an existing report produces.

- `xlsx({ page: { orientation, fit } })` turns the paper and scales the sheet.

  `orientation` takes `portrait` or `landscape`. `page.fit` takes `true` or `false`, and it scales a worksheet to one page wide and as many pages tall as it needs. It is a different key from the `fit` an image item carries, which scales a picture. Both join `size` and `margin` in the same print setup, and both reach every worksheet the report opens.

  Both are host options rather than report declarations. Turning the page changes how this package prints a document, and it does not change what the document is. One report definition therefore still renders the same document in every target.

  The factory refuses a word it does not know, at the call that wrote it. `orientation: "sideways"` throws a `TypeError` naming `options.page.orientation`.

- `xlsx({ page: { size, margin } })` writes print setup.

  `margin` is all four page margins in points, and `size` is `A4` or `letter`. A document that declares its own `page.margin` wins, and the host option applies where it does not — declaring both is a render error, the same rule the PDF target already follows.

  Print setup reaches **every worksheet**, so a report that opens many prints the same way throughout. A report that declares neither key carries no print setup at all, and a reader applies its own defaults rather than this package’s.

  A `[width, height]` page size is not written: a worksheet prints on named stock, because the format carries a paper code rather than a width and a height. Picking the nearest named stock would be a size you did not ask for.

- The header row that stays in view on screen now repeats on paper.

  A spreadsheet separates a view from a page. This target already froze the first table’s header row, and it left a printed page without one. It now writes the same rows as a print title, scoped to that one worksheet. Every printed page therefore carries the header the frozen view holds.

  One intent, two spellings in the format. Both take the same rows, and a report that freezes nothing repeats nothing.

- The workbook is now written by [werkmap](https://github.com/getquario/werkmap), quario’s own spreadsheet writer, in place of exceljs.

  **Output bytes differ from 0.8.x for every report.** Compare or cache a rendered workbook by its content, and that fingerprint changes once.

  What a reader opens is the same document: the same values and types, fonts, fills, alignments, merged ranges, frozen header row and floating images. Two details differ inside the file. A solid fill now carries the background slot Excel itself writes beside it, and the workbook no longer carries a theme part it never referred to.

  Determinism improves. Before, only the document model was stable, because the old writer stamped every archive entry with the packing time. Now the same schema, data and options produce byte-identical output on one runtime — a runtime being the engine and the zlib build inside it.

  The package’s dependencies drop from nine, and 22 MB installed, to one that has none of its own.

- A group’s `break` now opens a worksheet, so one report can produce many.

  **A report that declares `break` produces a different workbook than it did.** Before, every band landed on one worksheet named `Report`; now a boundary starts a new one. A report that declares no `break` is unchanged, down to the worksheet name.

  The worksheet layout follows the page layout the same declaration produces in PDF, boundary for boundary — including `break: "around"` giving one worksheet per instance, and a report footer landing on a worksheet of its own when a trailing boundary falls before it. A group instance that draws nothing still opens its worksheet, empty, because it declared the boundary.

  A worksheet a boundary opened is named from the group instance’s key: its text, with `: \ / ? * [ ]` replaced by spaces, runs of spaces collapsed, trimmed, cut to 31 characters. A key that survives none of that is named `Report`, and a repeated name takes `(2)`. Keys come from your data, so a name a spreadsheet cannot spell is sanitised rather than refused — a render does not start failing because someone renamed a region.

  Nothing caps how many worksheets a report produces. A group over a high-cardinality key opens one per instance, and the whole workbook is built in memory.

  The unlicensed-render marking now repeats on every worksheet. Each worksheet also freezes its own first table header.

  `reset` is still ignored, and now says why: it restarts page numbering, and this target writes no page numbers.

### Patch Changes

- **A group’s `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.

  The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.

  **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.

  The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.

  The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.

  The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.

- Updated dependencies

  - quario\@0.10.0

0.8.0

### @quario/xlsx 0.8.0

### Minor Changes

- **Every target factory now refuses an option it does not understand.** An unknown key, a key with a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx` already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding one.

  The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A fourth document property is one edit rather than three.

  **What this changes for you.** One options object spread across several targets stops working if any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`, `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works. TypeScript does not warn about this: excess-property checking fires on an object literal and not on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.

  Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and `{ meta: { title: config.title } }` over a config that carries neither are both fine. `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws instead of turning path stamping on. A `fonts` mapping given as an array is refused by `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.

  Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests `instanceof Error` is unaffected. One that compares the constructor is not.

  `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null` and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }` is a property you did not write rather than one of the wrong type, and an inherited enumerable key is no longer reported as an option you wrote.

  `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the object you passed no longer changes what a configured target writes.

- **`meta` is now read once, at the factory call.** It was read inside the render, so a host that mutated its options object between renders got a different workbook from the same configured target. The documented behaviour was always the factory call. If you were mutating an options object to change the document properties, build a new target instead.

### Patch Changes

- Updated dependencies
  - quario\@0.9.0

0.7.1

### @quario/xlsx 0.7.1

### Patch Changes

- **Every published README says where the documentation is.** Each package now carries a Documentation section pointing at the reference, at the report schema that normatively specifies what a report may declare, and at the package’s own API. The paragraphs that used to end on an unstated contract — the event stream’s field semantics, the style vocabulary, page columns, the Content Security Policy a fragment with images needs, the formula mangling, and each target’s own contract — link the page that states it. Every link is an absolute URL, so it resolves from the npm package page as readily as from an installed copy.
- Updated dependencies
  - quario\@0.8.0

0.7.0

### @quario/xlsx 0.7.0

### Minor Changes

- **Styled runs become rich text.** A cell of more than one styled run is written as rich text, each run’s font merged over the cell’s — so bold, italic, underline, strikethrough, size, face and colour survive into the grid. A run-level `background` is not read: a spreadsheet’s rich-text runs carry fonts only, and filling the cell would colour text nobody asked to colour. A cell of exactly one run stays a typed cell, so a single styled amount is still a number.
- **A run inside a sentence presents its value.** A run holding one interpolation under a `format` shows the same decimals here as on the page, where a mixed cell used to show the bare value.

0.6.0

### @quario/xlsx 0.6.0

### Minor Changes

- **A date cell’s number format follows the `form` its report declares.** The four forms map to `dd/mm/yy`, `dd mmm yyyy`, `dd mmmm yyyy` and `dddd, dd mmmm yyyy`. The shape is the document’s; the language a month or weekday name is spelled in stays the reader’s, because that is what a spreadsheet application supplies and pinning it would stop the file reading naturally for whoever opens it.

- **A declared digit count builds the pattern.** A `number` cell asking for three digits writes `#,##0.000`, and a `currency` cell asking for none writes `"EUR"#,##0` — the same count the page presents, taken off the declaration the engine resolved rather than computed here.

- **A cell’s own `currency` code picks the number format**, ahead of the instance’s default: `"JPY"#,##0` where the row says `JPY`, since the code decides the digits too. The cell stays a number either way. A code the engine could not accept writes no number format at all, rather than labelling the cell in the instance’s currency.

- **A `date` cell with no declared form now writes `dd mmm yyyy`, not `yyyy-mm-dd`.** A bare `date` means the `medium` form throughout, so the grid agrees with the page. A report that wants the ISO shape has no form for it; the nearest is `{ "kind": "date", "form": "short" }`, which writes `dd/mm/yy`. The cell is still a real typed date either way.

- **A cell’s number format now comes from the engine’s fraction-digit count rather than from four patterns this target held of its own.** The visible change is `currency`: the pattern followed the currency’s own minor units, so an amount in a currency with none — JPY — is `"JPY"#,##0` where it was `"JPY"#,##0.00`, and one with three is `"BHD"#,##0.000`. `number` (`#,##0.00`), `percent` (`0.00%`) and `date` (`yyyy-mm-dd`) are unchanged in a worksheet; what changed for those is that HTML and PDF now show the same decimals the grid always did, instead of their own.

- A `currency` code that is not a readable currency no longer produces a number format at all. The cell keeps its value; before, the code was pasted straight into a pattern while every other target fell back to the unformatted value.

  Only the digit count is shared. The grouping separator and the symbol’s position still come from the application that opens the file, which is what lets one workbook read naturally wherever it is opened.

- A `format` kind reaching this target from an `=` expression is now looked up as an own key. Before, a kind resolving to the name of a built-in object member threw out of the render (`valueOf`, `hasOwnProperty`) or wrote `[object Undefined]` into the cell’s number format (`toString`). A literal was never affected: it is checked against the four kinds.

- **An image whose size cannot be read now names the item that asked for it.** The failure said only that the size could not be read from the bytes, so a report with two pictures gave no way to tell which one was bad. The message now begins with the item’s `source` path, as every other render error does and as it does from every other target.

0.5.0

### @quario/xlsx 0.5.0

### Minor Changes

- **Chrome merges, data does not.** A cell spanning several columns in the header row or a total row is written into the first column it covers, the rest of the range is written empty, and the range is merged. Nothing in the data region merges, so sort, filter and column selection keep working over it.
- **`valign`** writes the cell’s vertical alignment for table cells and split slots. Undeclared writes nothing, so the spreadsheet application keeps its own default.

0.4.0

### @quario/xlsx 0.4.0

### Minor Changes

- **A date string under `format: "date"` now reaches the grid as a date.** The cell carries a real date value under the `yyyy-mm-dd` number format instead of the author’s text, so it sorts and computes as a date. Only a cell declaring the kind is affected; a string in a form the engine does not read stays text. See the `quario` changelog for the forms.

0.3.0

### @quario/xlsx 0.3.0

### Minor Changes

- **`format` maps to a number format; the cell stays typed.** `number` is `#,##0.00`, `percent` `0.00%`, `date` `yyyy-mm-dd`, `currency` the instance currency code as `"USD"#,##0.00`. A kind on the wrong type contributes nothing.
- **Cell borders map; padding and flow spacing do not.** `solid` is exceljs `thin`; `dashed` and `dotted` keep their names. A header-row, row, or total-row box fans onto that row’s cells; a cell that named any of a side’s three keys owns that side whole. Padding is unread: a worksheet cell has no inset. Flow spacing is unread: a grid has no flow.
- **`page.margin` and report-header `height` are unread.** A grid has no page top to pin from.
- **A table total emits N rows.** Each `total-row` is one worksheet row, as each data row is.
- **A cell whose display contains a newline wraps.** `wrapText` is this target’s mapping of a literal newline as a line break, so `"one\ntwo"` is two lines in the grid. Single-line cells are unchanged.

0.2.0

### @quario/xlsx 0.2.0

### Minor Changes

- **The report default reaches every cell.** A report’s top-level `style` replaces this target’s baseline, so a document declaring a face and size gets them in cells that declare nothing of their own. It is the layer under the band-role defaults — a report declaring `size: 12` still writes its report header at 14 — and under each cell’s own style.
- **`uppercase` is accepted and deliberately not read.** A spreadsheet font has no text-transform, and writing capitals into the cell instead would turn presentation into data — the cell would stop round-tripping and would sort differently. The cell keeps the text you wrote; every other declaration on it still applies. Same posture as the column widths this target withdrew.
- **A split is one row, its slots the cells across it.** The grid’s own reading of values placed beside each other. Slot `width` shares go unread, on exactly the ground the table’s column widths do — a worksheet’s columns are global to the sheet. The split’s own style is what its slots sit under, layering the way a table row’s does, and a slot that renders nothing writes an empty cell so the cells either side keep their columns.
- **Every cell is written at 10 points unless something declares otherwise.** The size was previously left to the writer’s own default of 11. Now that a report can declare its own size, this target and the PDF carrying different numbers for one declaration is exactly what the targets’ agreement rule forbids. A worksheet’s row height follows its font size, so existing unstyled sheets come out slightly tighter.
- **Display-text `Date`s join as ISO 8601 UTC.** A `Date` inside a mixed cell (literal text plus interpolation) joined through `String(date)`, which bakes the host’s timezone and locale into the worksheet. It now joins through the engine’s shared display rule as `toISOString()` text. Typed cells are untouched: a cell that is one bare `Date` interpolation still writes a native date.
- **Cells carry their font face explicitly.** Text declaring no `family` used to inherit whatever the workbook writer defaulted to, which agreed with the other targets only by coincidence. It now writes the same face that `family: "sans"` resolves to, so the baseline is a rule rather than a property of the library underneath.

0.1.0

### @quario/xlsx 0.1.0

### Minor Changes

- **A workbook from the same compiled report.** Bands become rows; a bare interpolation that is a number, boolean, or Date stays that type. The first table freezes its header row. exceljs writes the file. Column width percentages are unread: a worksheet has no table for a share to be a share of.

## @quario/csv

0.10.2

### @quario/csv 0.10.2

### Patch Changes

- Updated dependencies
  - quario\@0.15.0

0.10.1

### @quario/csv 0.10.1

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.
- Updated dependencies
  - quario\@0.14.0

0.10.0

### @quario/csv 0.10.0

### Minor Changes

- A document may now name its paper and turn it, and its margin may name each side.

  ```jsonc
  {
    "page": {
      "size": "letter",
      "orientation": "landscape",
      "margin": { "top": 36, "right": 29, "bottom": 36, "left": 29 }
    }
  }
  ```

  `page.size` takes `"A4"` or `"letter"`. A `[width, height]` pair stays a target option. `page.orientation` takes `"portrait"` or `"landscape"`, and it turns whichever paper is in force. `page.margin` still takes one number for all four sides.

  The rule `page.margin` already followed now covers all three keys. The document’s key wins where it declares one, a target option applies where it does not, and both declaring the same key is a render error. `orientation` in a document beside a target’s `size` is not both.

  Each target writes the paper: the PDF and the layout draw on it, HTML writes an `@page` size and per-side margin, and DOCX writes `pgSz` with `w:orient` and `pgMar` per side. XLSX writes the paper code, the orientation and each print margin. CSV withdraws all three.

  The capability descriptors gain `page.size` and `page.orientation`, and both are names a document may mark `required`.

  A target’s margin error now reads `expected a non-negative number, or { top, right, bottom, left }, that leaves the page room`.

### Patch Changes

- Updated dependencies
  - quario\@0.13.0

0.9.1

### @quario/csv 0.9.1

### Patch Changes

- Updated dependencies
  - quario\@0.12.0

0.9.0

### @quario/csv 0.9.0

### Minor Changes

- **A `capabilities` descriptor.** The target exports what it makes of each declaration the spec’s support matrix names — `resolved`, `approximated`, `withdrawn` or `unread` — beside its factory, so a host can ask before it renders rather than reading the prose. It is keyed by the schema name an author writes (`valign`, `break`, `page.margin`, and `column.width` / `slot.width` where one word carries two declarations), hand-written and gated against that matrix. It carries no version of its own: the package that ships it is the version.

### Patch Changes

- **`collapsed` is withdrawn.** Every record is written, and an order is all a record has, so dropping the rows would change the data rather than how it opens.
- Updated dependencies
  - quario\@0.11.0

0.8.1

### @quario/csv 0.8.1

### Patch Changes

- **A group’s `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.

  The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.

  **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.

  The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.

  The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.

  The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.

- Updated dependencies

  - quario\@0.10.0

0.8.0

### @quario/csv 0.8.0

### Minor Changes

- **Every target factory now refuses an option it does not understand.** An unknown key, a key with a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx` already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding one.

  The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A fourth document property is one edit rather than three.

  **What this changes for you.** One options object spread across several targets stops working if any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`, `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works. TypeScript does not warn about this: excess-property checking fires on an object literal and not on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.

  Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and `{ meta: { title: config.title } }` over a config that carries neither are both fine. `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws instead of turning path stamping on. A `fonts` mapping given as an array is refused by `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.

  Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests `instanceof Error` is unaffected. One that compares the constructor is not.

  `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null` and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }` is a property you did not write rather than one of the wrong type, and an inherited enumerable key is no longer reported as an option you wrote.

  `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the object you passed no longer changes what a configured target writes.

### Patch Changes

- Updated dependencies
  - quario\@0.9.0

0.7.1

### @quario/csv 0.7.1

### Patch Changes

- **Every published README says where the documentation is.** Each package now carries a Documentation section pointing at the reference, at the report schema that normatively specifies what a report may declare, and at the package’s own API. The paragraphs that used to end on an unstated contract — the event stream’s field semantics, the style vocabulary, page columns, the Content Security Policy a fragment with images needs, the formula mangling, and each target’s own contract — link the page that states it. Every link is an absolute URL, so it resolves from the npm package page as readily as from an installed copy.
- Updated dependencies
  - quario\@0.8.0

0.7.0

### @quario/csv 0.7.0

### Minor Changes

- **Styled runs flatten to their joined text.** A cell value written as a list of styled runs writes the runs’ text as one field. This target withdraws every style, and a run’s is no different.

0.6.0

### @quario/csv 0.6.0

### Minor Changes

- Updated dependencies
  - quario\@0.6.0

0.5.0

### @quario/csv 0.5.0

### Minor Changes

- **Spans keep the record rectangular.** A cell covering several columns writes its value in the first field it covers and leaves the rest empty, so every record is the same width. This target reads no geometry, so there is nothing here for a merge to mean.

0.4.0

### @quario/csv 0.4.0

### Minor Changes

- Updated dependencies
  - quario\@0.4.0

0.3.0

### @quario/csv 0.3.0

### Minor Changes

- **A table total emits N records.** Each `total-row` is one record, as each data row is.
- **`format` is unread.** Bare interpolations stay `1000` / ISO dates, as every other style is unread here.
- **`page.margin` and report-header `height` are unread.** This target has no page to inset or pin on.

0.2.0

### @quario/csv 0.2.0

### Minor Changes

- **A split is one record, its slots the fields.** Slots are values beside each other, which is what a record is. Slot widths go unread, as every width is here. Images stay unread too, but a slot holding one still writes an empty field, so the fields either side keep their positions.
- **Display-text `Date`s join as ISO 8601 UTC.** A `Date` inside a mixed cell (literal text plus interpolation) joined through `String(date)`, so the field carried the host’s timezone and locale. It now joins through the engine’s shared display rule as `toISOString()` text — the same form a typed single-`Date` field has always used, so every `Date` in a CSV now reads the same way.

0.1.0

### @quario/csv 0.1.0

### Minor Changes

- **CSV records in render order.** A bare interpolation stays typed text (a number’s decimal, a Date as ISO 8601 UTC); everything else is display text, then an apostrophe on formula-like cells, then RFC 4180 quoting. The unlicensed marking is a trailing line, so record 1 stays the report’s.

## @quario/docx

0.5.1

### @quario/docx 0.5.1

### Patch Changes

- A link to `#` and a name that no group declares as its `label` keeps its text and loses its link. Before, the document held a hyperlink to a bookmark that did not exist, and Word showed a link that went nowhere. A link to a declared label stays a link, also when it comes before the group in the document or sits in a page band.
- The engine exports `pageBox(page, at)` and `paper(page, opening, at)`, the rules every target with a page follows. `pageBox` checks a host’s `page` option. `paper` says which size, margin and orientation are in force for a render, and the page box they come to. `@quario/layout` and `@quario/docx` now use them, and their behaviour does not change.
- Updated dependencies
  - quario\@0.15.0

0.5.0

### @quario/docx 0.5.0

### Minor Changes

- **An image item now accepts SVG bytes.** Before, `source` had to yield PNG or JPEG bytes, and SVG bytes were a render error. Now the engine also names UTF-8 text whose root element is `<svg` as an SVG, and the image event carries `format: "svg"`. The engine reads the size from the root’s `width` and `height` when both are absolute lengths, and else from its `viewBox`. An SVG that states no size this way is the same render error as a PNG that is too short to state its size. The message for bytes that are not an image now says “PNG, JPEG or SVG”.

  The HTML target puts an SVG in an `<img>` under `data:image/svg+xml`, so the SVG never becomes markup in the fragment. The PDF target draws an SVG as vector paths. It draws no picture that the SVG names, and it fetches nothing. SVG `<text>` draws in the standard Helvetica. The viewer paints an SVG through a `data:` URI, so a page that shows one needs the Content Security Policy grant `img-src data:`. A PNG or a JPEG still needs no grant there. The XLSX and DOCX targets cannot draw an SVG, so an SVG image is a render error that names the item.

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.
- Updated dependencies
  - quario\@0.14.0

0.4.0

### @quario/docx 0.4.0

### Minor Changes

- A document may now name its paper and turn it, and its margin may name each side.

  ```jsonc
  {
    "page": {
      "size": "letter",
      "orientation": "landscape",
      "margin": { "top": 36, "right": 29, "bottom": 36, "left": 29 }
    }
  }
  ```

  `page.size` takes `"A4"` or `"letter"`. A `[width, height]` pair stays a target option. `page.orientation` takes `"portrait"` or `"landscape"`, and it turns whichever paper is in force. `page.margin` still takes one number for all four sides.

  The rule `page.margin` already followed now covers all three keys. The document’s key wins where it declares one, a target option applies where it does not, and both declaring the same key is a render error. `orientation` in a document beside a target’s `size` is not both.

  Each target writes the paper: the PDF and the layout draw on it, HTML writes an `@page` size and per-side margin, and DOCX writes `pgSz` with `w:orient` and `pgMar` per side. XLSX writes the paper code, the orientation and each print margin. CSV withdraws all three.

  The capability descriptors gain `page.size` and `page.orientation`, and both are names a document may mark `required`.

  A target’s margin error now reads `expected a non-negative number, or { top, right, bottom, left }, that leaves the page room`.

### Patch Changes

- Updated dependencies
  - quario\@0.13.0

0.3.1

### @quario/docx 0.3.1

### Patch Changes

- Updated dependencies
  - quario\@0.12.0

0.3.0

### @quario/docx 0.3.0

### Minor Changes

- **A `capabilities` descriptor.** The target exports what it makes of each declaration the spec’s support matrix names — `resolved`, `approximated`, `withdrawn` or `unread` — beside its factory, so a host can ask before it renders rather than reading the prose. It is keyed by the schema name an author writes (`valign`, `break`, `page.margin`, and `column.width` / `slot.width` where one word carries two declarations), hand-written and gated against that matrix. It carries no version of its own: the package that ships it is the version.

- **A run that carries an `href` is a Word hyperlink.** A URL becomes a relationship of the part the run lands in, marked as leaving the package, and one relationship serves every run pointing at the same place. An `href` beginning `#` becomes a `w:anchor`, and the first instance of the group carrying that `label` writes the bookmark it names, beside the heading that item already takes. A run whose text is empty writes no link and relates to nothing. The support matrix recorded this target as withdrawing `href`, and it now resolves it.

  Two things a page band’s own part had wrong are fixed with it. A header or footer now carries its **own relationships part**, so a picture or a link inside a band resolves in the part that refers to it rather than only in `document.xml`’s. And `[Content_Types].xml` no longer writes an `Override` for a relationship that names no WordprocessingML part. That entry typed a picture’s bytes as a document part, which said nothing true about them.

- **`negative` and `zero` reach the runs.** The target presents through the engine’s `format()`, so a cell declaring either reads the same in Word as on the page.

- **A collapsed group does not draw its content.** An instance whose group declares `collapsed` renders as its header and footer, with the rows between them left out — a page has nowhere to put a row a reader could open. Aggregates are unchanged, so a footer still totals them.

- **The report header scales with the report default.** A report-header run is written at 1.4× the report default’s size rather than a fixed 14, rounded to Word’s half-points. A document declaring no size keeps its bytes, because 1.4× the base 10 is the same 14. A document declaring `"size": 20` now renders its report header at 28 where it rendered at 14, smaller than its own body.

### Patch Changes

- Updated dependencies
  - quario\@0.11.0

0.2.0

### @quario/docx 0.2.0

### Minor Changes

- **A group’s `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.

  The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.

  **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.

  The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.

  The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.

  The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.

### Patch Changes

- Updated dependencies
  - quario\@0.10.0

0.1.1

### @quario/docx 0.1.1

### Patch Changes

- **Every target factory now refuses an option it does not understand.** An unknown key, a key with a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx` already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding one.

  The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A fourth document property is one edit rather than three.

  **What this changes for you.** One options object spread across several targets stops working if any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`, `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works. TypeScript does not warn about this: excess-property checking fires on an object literal and not on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.

  Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and `{ meta: { title: config.title } }` over a config that carries neither are both fine. `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws instead of turning path stamping on. A `fonts` mapping given as an array is refused by `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.

  Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests `instanceof Error` is unaffected. One that compares the constructor is not.

  `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null` and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }` is a property you did not write rather than one of the wrong type, and an inherited enumerable key is no longer reported as an option you wrote.

  `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the object you passed no longer changes what a configured target writes.

- Updated dependencies

  - quario\@0.9.0

0.1.0

### @quario/docx 0.1.0

### Minor Changes

- **A Word target.** `@quario/docx` renders the same report definition every other target reads into the bytes of a `.docx` — real Word tables, inline pictures, and a grouping that becomes the navigation pane.

  It is a **flow** target rather than a painter: it states the geometry and the typography and lets Word paginate, so the promise is the preview’s look rather than the preview’s page breaks. Page bands become a section’s header and footer, and a bare `{{ page.number }}` or `{{ page.total }}` becomes a live field the reader’s own application recomputes.

  ```js
  import { docx } from "@quario/docx";
  import { quario } from "quario";

  const bytes = await quario()
    .report(schema)
    .render(docx({ page: { size: "A4" }, meta: { title: "Sales 2026" } }), data);
  ```

  Its one runtime dependency is `fflate`, confined to the module that writes the zip — pure JavaScript, so the same schema, data and options produce byte-identical output on every supported runtime. SCHEMA.md’s “The DOCX target” states the full contract.

### Patch Changes

- Updated dependencies
  - quario\@0.8.0

## @quario/viewer

0.15.0

### @quario/viewer 0.15.0

### Minor Changes

- `<quario-viewer>` gives the host page more control.

  - `pageNumber` reads the current page and goes to a page when you assign it. A number after the last page goes to the last page. A number that you assign before a render lands waits for that render. `pageCount` reads the page count.
  - The `pagechange` event fires when the page number or the page count changes. The first render that lands fires it too.
  - The `link` event fires before a URL link opens. `event.preventDefault()` stops the new tab. A host page uses this to drill through to another report.
  - `bar: false` removes the bar. The page keys, the progress strip and the outline panel stay.
  - Children with `slot="bar"` show in the bar, before the view and export controls.

  Fix: an `outline` write after the first render now turns the outline on or off. Before, only the value at the first render counted.

- `<quario-viewer>` now has page controls and a single-page view. The left of the bar shows first page, previous page, a page-number field, next page and last page. The field shows the current page and the page count, for example “3 of 120”. When the element is narrower than 360px, the bar removes the first-page and last-page controls. The Home and End keys do the same work.

  The view menu replaces the zoom menu. It shows **Fit width**, **Fit page**, the percentages, **Continuous** and **Single page**. Fit page makes the whole page fit in the viewer. A new `view` property takes `"continuous"` (the default) or `"single"`. In the single view, the page controls and the keys change the page, and the mouse wheel never does.

  Breaking: the `zoom` property takes `"width"` where it took `"fit"`. Before, `zoom: "fit"` fitted one page to the width of the viewer. Now `zoom: "width"` does that, and `zoom: "page"` fits the whole page. The viewer rejects `"fit"` and names the `zoom` property on the error panel. The name of the trigger changes from “Zoom, 62%” to “View, 62%”.

  `sheet()` in `@quario/landing` takes an optional sixth argument, `origin`. It tells the sheet where the scroll extent starts, so a surface that shows one page at a time paints the pages near that page.

- `<quario-viewer>` takes a `params` property. The viewer passes it to the render of the preview and to the render of each export. Before, the viewer passed no parameter values. A report that declares a required parameter failed to show and failed to export. An optional parameter always took its default. A `params` write renders the report again, as a `data` write does.

- The `strings` record of `<quario-viewer>` takes a new `percent` key. Each percentage row of the view menu shows the result of this function. The function gets the percentage, for example `(percent) => percent + " %"` for French. The English is “50%”.

- `<quario-viewer>` prints. Set `print` to `true` to add a Print row to the export menu. Print renders the report through your `pdf` target and opens the print dialog of the browser. The viewer opens no tab.

  - The row shows only when `targets` holds a `pdf` target and the browser can show a PDF in a frame.
  - If your page has a Content Security Policy, it must allow `frame-src blob:`. Without that grant, the viewer shows “Could not print” and fires an `export` error.
  - `strings` takes two new keys: `print` and `printFailed`.

- A screen reader now reads a table on the viewer’s pages as a table. Its headings are the column headers. Each page holds its own part of a table. That part starts with the headings again. Each page of the layout list carries `tables` for this. A table there lists its rows, and each row lists its cells. A cell names the range of the page’s `lines` that it drew. An empty cell is an empty range, so each cell after it keeps its column.

- `<quario-viewer>` searches the whole report.

  - A search button in the bar opens a find row under the bar. The row has a field, a count such as “3 of 41”, and previous and next controls. Enter goes to the next match. Shift+Enter goes to the previous match. Escape closes the row. A screen reader announces the count.
  - Ctrl+F, or Cmd+F, opens the row when the focus is in the viewer. Elsewhere on the page the browser’s own find stays.
  - The viewer goes to the current match and highlights it. In the single-page view it shows the page of the match.
  - A search ignores case and accents. It finds the text the pages show, so a formatted value matches as formatted. A page header or page footer matches on every page that shows it.
  - The `query` property reads and writes the search. The `searchchange` event fires with `{ query, match, matches }`. A page without the bar keeps the search through both.
  - New `strings` keys: `search`, `findField`, `matchCount`, `previousMatch` and `nextMatch`. The `--qv-match` custom property colors the highlight.
  - The bar is 32px wider for the search button, 46px on a touch screen. Below 384px it removes the first-page and last-page controls. On a touch screen it removes them below 500px, and below 400px it also removes the page count and the previous-page and next-page controls.

- `<quario-viewer>` takes a new `strings` property. It replaces the English words of the chrome, key by key. A key that you leave out stays English. Fixed text takes a string, for example `firstPage: "Erste Seite"`. A message that carries a value takes a function of that value, for example `pageCount: (count) => "von " + count`. An unknown key, or a string where the viewer expects a function, or the reverse, is a host mistake that the error panel names as `strings.<key>`. If a function throws, the viewer draws the English text and reports the error one time for each record, as a `host-option` error. The marking and the message of an error do not change. Set the `lang` attribute on the element to the language of your strings.

  Breaking: the fifth argument of `sheet()` in `@quario/landing` is now `name` where it was `named`. Before, `true` named each page canvas “Page n of N” and `false` hid it from assistive technology. Now the argument takes a function from the page index and the page count to the name, `pageName` by default, or `false` as before. Replace `true` with `pageName`, or leave the argument out.

- `<quario-viewer>` zooms by touch, by trackpad and by key.

  - A pinch on the sheet zooms the viewer, not the page. Before, the browser enlarged the whole page: the pages went soft and the bar went off the screen.
  - A trackpad pinch, or Ctrl with the mouse wheel, zooms the viewer when the pointer is on the sheet. Elsewhere the browser zooms the page as before.
  - With the focus on the sheet, + and - go to the next and the previous percentage of the view menu. 0 goes to 100%.
  - A gesture zooms to any percentage from 25 to 200. The point under the fingers or the pointer stays in its place. When the gesture stops, the viewer paints the pages again, so the text stays sharp.
  - On a touch screen, the bar’s controls and the page-number field are 44px by 44px. The bar removes the first-page and last-page controls below 450px, and the page count below 360px. Thus the bar still fits in 320px.

### Patch Changes

- A fast scroll through a long report stays smooth. Before, the viewer and the editor painted every page that came near the view during a fast scroll. On a slow device, that work dropped the frame rate a lot, and the page in view was blank on most frames anyway. Now a frame that moves more than half the height of the view paints no page. The page outline shows where each page is. The pages paint on the first frame that moves less, and on the frame after the scroll stops.

- The editor’s properties panel puts the styles in groups: Text, Alignment, Fill, Format, Link, Spacing, Padding and Border. A group shows only the styles that the selected node declares. A group that declares nothing is one line. The `+` of a group shows its other styles, and the `−` hides them again. The group stays open when you select another node.

  Each group shows its styles in the shape of their values. A value is beside its name. The four sides of padding are a grid with a mark for each side. The border is a table of sides by width, line and colour. Bold, italic, underline, strikethrough and uppercase are toggles with icons. A held toggle is on, an outlined toggle is off, and a dimmed toggle declares nothing. A style that holds an expression is a row of its own. A colour shows its value as text beside the swatch, and you can type a colour there. A colour that declares nothing shows a crossed square, not black. The `fx` toggle shows when the pointer or the focus is on its field. The rail is 296px wide.

  The chrome of the editor and the viewer has a new finish. The bar and the panels are white on a grey backdrop in the light scheme. The text is darker, and the focus colour is a deeper blue. The scrollbars and the selected text use the palette. The stage of the editor does not show a focus ring when you click the sheet.

  The editor has two new tokens, `--qe-icon-active` and `--qe-active`, which match the viewer’s `--qv-icon-active` and `--qv-active`. The other tokens keep their names, and a value that a host sets still wins.

- `<quario-editor>` takes a new `strings` property. It replaces the English words of the chrome, key by key. A key that you leave out stays English. Fixed text takes a string, for example `undo: "Rückgängig"`. Five messages carry a value and take a function of it: `hidden`, `asText`, `showGroup`, `hideGroup` and `pageName`. An unknown key, or a string where the editor expects a function, or the reverse, is a host mistake that the error panel names as `strings.<key>`. If a function throws, the editor draws the English text and reports the error one time for each record, as a `host-option` error. The marking, the message of an error, the messages of the engine and the names of the schema do not change. Set the `lang` attribute on the element to the language of your strings.

  `@quario/landing` exports `messages()` and `speaker()`, the convention that the viewer and the editor share for their `strings` records.

  The viewer now checks its `strings` record through `@quario/landing`. An unknown key now reads “expected a message the chrome draws”, where it read “expected a message the viewer draws”.

- A link to `#` and a name goes only to a group instance that declares that name as its `label`. Before, the PDF and the viewer also matched an instance with no `label` whose outline title read the same, which is its first header’s text. A link to a name that no instance declares now goes nowhere, as the spec says.

  In `@quario/layout`, a `Mark` carries `label` where its instance declares one. The new `destinations(marks)` and `destination(href, byLabel)` say where a link goes. The PDF target and the viewer use them.

- The pages stay sharp when the device pixel ratio changes. Before, the viewer and the editor painted the pages again only when a box changed size. A window that moved to a monitor with a different ratio, or a page zoom when the host fixes the size of the element, kept the old backing store, and the text was soft. Now the viewer and the editor paint the visible pages again at the new ratio.

  `sheet()` in `@quario/landing` has a new `watch()` method. It paints the reach again when the ratio changes, and it returns a function that stops the watch.

- The README shows how your page draws a parameter panel from `report.params`. The recipe draws an input for each labelled `string`, `number` or `boolean` parameter and assigns the values to `params`. It also quotes the message that the error panel shows when a required parameter has no value.

- The README shows two more recipes that use a parameter. In the first, a control on your page chooses the sort order of the rows. In the second, a checkbox hides the detail rows and keeps the group headers, the group footers and their subtotals. The viewer passes `params` to each export, so each downloaded file shows the same rows as the screen.

- Updated dependencies

  - @quario/landing\@0.5.0
  - quario\@0.15.0
  - @quario/layout\@0.11.0

0.14.0

### @quario/viewer 0.14.0

### Minor Changes

- **An outline step now lands on its group instance.** Before, the layout gave each outline entry a `y` that the target measured up from the page bottom. The `Mark` type said that `y` reads down from the top, as an op’s `y` does. The viewer read it that way, so an outline row or an internal link scrolled near the foot of the page and not to the instance. The layout now gives `mark.y` from the top of the page. The viewer step lands on the header of the instance. The PDF target turns the value over for each bookmark and link destination, so the PDF bytes do not change. A host that reads `marks` from the layout target and turned `y` over itself must stop doing that.

- **An image item now accepts SVG bytes.** Before, `source` had to yield PNG or JPEG bytes, and SVG bytes were a render error. Now the engine also names UTF-8 text whose root element is `<svg` as an SVG, and the image event carries `format: "svg"`. The engine reads the size from the root’s `width` and `height` when both are absolute lengths, and else from its `viewBox`. An SVG that states no size this way is the same render error as a PNG that is too short to state its size. The message for bytes that are not an image now says “PNG, JPEG or SVG”.

  The HTML target puts an SVG in an `<img>` under `data:image/svg+xml`, so the SVG never becomes markup in the fragment. The PDF target draws an SVG as vector paths. It draws no picture that the SVG names, and it fetches nothing. SVG `<text>` draws in the standard Helvetica. The viewer paints an SVG through a `data:` URI, so a page that shows one needs the Content Security Policy grant `img-src data:`. A PNG or a JPEG still needs no grant there. The XLSX and DOCX targets cannot draw an SVG, so an SVG image is a render error that names the item.

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.

- Updated dependencies

  - quario\@0.14.0
  - @quario/landing\@0.4.1
  - @quario/layout\@0.10.0

0.13.0

### @quario/viewer 0.13.0

### Minor Changes

- **The viewer’s pages can be read and copied.** The viewer now puts the report’s text over each page it paints. A screen reader reads it, a magnifier follows it, and a reader selects it and copies it. Nothing needs to be declared or turned on.

  A screen reader meets each page as a group with the name “Page n of N”. It reads the page header first, then the body, then the page footer. The first line of a group header is a heading at the depth of that group. An image with an `alt` has that name, and an image without one is skipped as decoration. The unlicensed marking is read on every page and is never part of a copy.

  A copy takes the text as the report wrote it. A paragraph that wraps on the page copies as one line, and so does a row of a table, with a space between its cells. The text covers the pages near the viewport, as the paint does. Every page that the selected text spans stays while the reader keeps the text selected. Select all therefore takes the pages near the viewport, and the PDF export is the whole report.

  What changes for a host:

  - A page canvas is now hidden from assistive technology. Before, it was an image with the name “Page n of N”. That name is now on the text over the page.
  - A linked run is now a link inside that text. Before, it was an empty region over the words, named by an `aria-label`. The `.qv-link` class stays. The `.qv-links` layer is now `.qv-mirror`.
  - A `#` link to a label that no group carries is now plain text that a reader can select. Before, it had nothing over it.
  - `--qv-focus` also tints selected text.

  `@quario/layout` gives each page a `lines` array. Each entry names the text ops of one laid-out line and the schema node the line belongs to. The first line of a group header also carries the depth of its outline entry. A line that continues as the same line of text carries a `join`: what a wrap dropped, or a space to the next cell of a table row or slot of a split. An image op carries its rendered `alt`. `fontOf(font, size)` returns the CSS font the painter draws a run in, and `TextOp.href` is now declared.

  `@quario/landing`’s `sheet()` takes a fifth argument, `named`. Pass `false` and each page canvas is hidden from assistive technology instead of being an image named “Page n of N”.

### Patch Changes

- **Text sits in the middle of its line.** A line is 1.4 times its largest font size. Before, all of that extra space went below the text, so a capital touched whatever stood above its line, an image for example. Now half of the extra space goes above the text and half below, as a browser lays out a line.

  Every line keeps its height, so every page breaks where it did before. Each line of text is drawn 0.2 times its font size lower than before. The PDF, the viewer and the editor all change, because they draw the same layout. The HTML target already split the space this way.

- Updated dependencies

  - quario\@0.13.0
  - @quario/layout\@0.9.0
  - @quario/landing\@0.4.0

0.12.0

### @quario/viewer 0.12.0

### Minor Changes

- **The exports are now one button and a menu.** The bar used to draw a labelled button for every format you passed, so it grew wider with each one and needed 343px to show four without pushing the zoom control off its left edge. It now draws a single **Export** button that opens a menu of the formats, in the order you passed them, and the bar needs 82px whatever you pass. Choosing a row downloads that format, as clicking its button did. While an export renders, the button shows a spinner and the format’s own row is disabled — the button stays live, so you can start a second format before the first finishes.

### Patch Changes

- **The zoom button now draws a magnifier with a plus in it.** The bar’s zoom trigger used to draw a plain magnifier, the same glyph a search box draws, so at a glance it read as a way to search the report rather than a way to change its size. Nothing else about the control moves: the button opens the same menu, carries the same name (“Zoom, 62%”), and sits in the same place at the same size.

- Updated dependencies

  - quario\@0.12.0
  - @quario/layout\@0.8.1
  - @quario/landing\@0.3.2

0.11.0

### @quario/viewer 0.11.0

### Minor Changes

- **A collapsed group does not draw its content.** The sheet paints the layout’s pages, so an instance whose group declares `collapsed` reads as its header and footer alone.

- **A group’s `label` titles its outline row.** The panel lists the layout’s marks, so an instance that declares a label reads under it rather than under its first header line.

- **A linked run is clickable, and reachable with a keyboard.** Every run carrying an `href` now gets a region of its own over the words the page paints, so a reader clicks it or reaches it with Tab and opens it with Enter. A URL is an anchor opening in a tab of its own. An `href` naming a group’s `label` is a button that steps the sheet to that instance, the way an outline row does, and a label no instance carries is no region at all. The regions follow the reach as the pages do, and move with the zoom.

  The size budget rises from 8.5 kB to 9.5 kB to hold them.

- **An optional outline panel.** Set `outline` to `true` and the bar carries one more control, which opens a panel beside the sheet. The panel lists the report’s group instances, nested by depth, in document order, and a row scrolls the sheet to where that instance begins. The list is the group tree the PDF target writes as bookmarks, so a report declares nothing for it. An instance whose bands all resolve hidden is not in it. The module is off by default. The reader opens and closes the panel.

### Patch Changes

- **An error quario replaces keeps the one it replaced, as `cause`.** A failure inside one of your own functions reaches you named where it happened — `detail[0].value [{{ total() }}]: ...` — which is a copy, rebuilt from the class and a prefixed message. The stack that says which line of your function threw used to go with the original. It now rides along: read `error.cause` for the throw as it happened, and the message for where quario was when it happened.

  The viewer’s and the editor’s own property checks do the same. A `page` or `fonts` value either element refuses throws a `TypeError` naming the property, and that error now carries the layout’s own account of the value behind it.

- **`renderComplete` answers for the newest render at the moment it settles.** A render that starts behind a pending `await` is the one the promise then waits for, so a host awaiting it while properties keep changing waits for the last change’s render. Before, the promise could settle for the render in flight when it was asked, or miss one that started behind it.

- **The viewer reports a target it cannot export, rather than dropping it.** A `targets` entry whose name is not `pdf`, `xlsx`, `csv`, `docx` or `html` used to disappear: no button, no error. It is now a host mistake like any other malformed property. The error panel names the entry by its index and the name it carried, and the last good render stays up. An `html` target is still accepted and not read, so one list can serve both `render` and the viewer.

- Updated dependencies

  - quario\@0.11.0
  - @quario/layout\@0.8.0
  - @quario/landing\@0.3.1

0.10.0

### @quario/viewer 0.10.0

### Minor Changes

- **A viewer you insert again renders once more, unless its last render landed on the sheet.** It used to do that only for a render that stopped mid-flight, which left three ways for the newest render to go missing with nothing owed.

  A render that failed while the element was out of the document is one of them. The error panel reaches nobody there, so the verdict reached nobody, and the element came back showing the previous pages with no panel and no re-render owed. A property the host rejected while the element was out of the document behaved the same way. So did a property written while it was out of the document.

  All three now leave the render owed, and inserting the element collects it. What you see on reconnect is the panel the failure earns, or the pages the newest properties produce.

  A failure the panel already showed is owed again too. Removing and inserting such an element re-runs the render and reports the same failure once. It is not announced twice.

### Patch Changes

- Updated dependencies

  - @quario/layout\@0.7.0
  - quario\@0.10.0
  - @quario/landing\@0.3.0

0.9.0

### @quario/viewer 0.9.0

### Minor Changes

- The viewer offers a Word download. Pass a `docx` target in `targets`, and the bar carries a `DOCX` button beside the others. The buttons keep the order you gave. The file carries the content type `application/vnd.openxmlformats-officedocument.wordprocessingml.document`.

  Earlier versions dropped a `docx` target. The viewer rendered no button for it, and it reported no failure.

### Patch Changes

- Updated dependencies

  - quario\@0.9.0
  - @quario/layout\@0.6.0
  - @quario/landing\@0.2.1

0.8.0

### @quario/viewer 0.8.0

### Minor Changes

- **An image’s box is now as wide as the container it sits in.** A `background` or `border*` on an image item hugged the picture and now spans the content width, or the slot’s share inside a split; the picture itself does not move. See the `@quario/layout` changelog for the rule and for what it costs a report that relied on a border hugging a logo.

- **Only the pages you can see are on the sheet.** A page outside the reach used to be an empty canvas holding its place; now it has no element at all, and the sheet carries the document’s extent itself — its height and width are written from the layout, and it paints the page silhouettes. The stretch a reader scrolls through is exactly what it was, and a scale change no longer costs the browser a relayout of every page: that was around 10 microseconds a page, so a five-thousand-page report paid roughly 50 ms of it each time.

  This changes the sheet’s markup, which matters if you style it. `.qv-page` is now absolutely positioned inside `.qv-sheet`, and is present only for the pages on screen and one viewport height either side. A rule that gave a page a margin, or that relied on the pages being a flow of siblings, no longer applies — the sheet places its own pages, and does it with inline styles a stylesheet rule cannot override. `--qv-sheet-shadow` is unchanged.

  One cost, stated: a page outside the reach has no `role="img"` node, so assistive technology sees the few images of the reach renumbering as the reader scrolls rather than a document of named pages.

### Patch Changes

- **Every published README says where the documentation is.** Each package now carries a Documentation section pointing at the reference, at the report schema that normatively specifies what a report may declare, and at the package’s own API. The paragraphs that used to end on an unstated contract — the event stream’s field semantics, the style vocabulary, page columns, the Content Security Policy a fragment with images needs, the formula mangling, and each target’s own contract — link the page that states it. Every link is an absolute URL, so it resolves from the npm package page as readily as from an installed copy.

- Updated dependencies

  - @quario/layout\@0.5.0
  - @quario/landing\@0.2.0
  - quario\@0.8.0

0.7.0

### @quario/viewer 0.7.0

### Minor Changes

- **`fonts` is validated as a host property.** It was the one the viewer never checked, so a malformed record reached the target inside the render and came back as a render failure about a report that was never at fault. Its shape is now checked with the other host properties and named on `fonts` rather than on the target’s `options.fonts`. A face that will not parse, or a missing parser, is still found while the report is measured and remains a render failure there.

- **A rejected host property reports as `host-option`.** `ViewerErrorKind` gains a fourth member, so a host switching exhaustively over the kind needs a case for it. A `page`, `zoom`, `filename`, `colorScheme` or `fonts` the viewer rejected was reported as `mount-render` or `update-render` — a render that was never attempted — and the panel said “Could not render the report”, which reads as the report being at fault. It now says the viewer is misconfigured and that the report was not the problem.

  A malformed `report` or `targets` still reports as a render failure: those are refused where the render begins rather than when the property is written, so nothing yet tells them apart from a render that failed.

0.6.0

### @quario/viewer 0.6.0

### Minor Changes

- **Numbers presented through `format` now show a fixed two fraction digits**, the same as every other target: `1,000.00` where the preview showed `1,000`, `21.00%` where it showed `21%`, and a currency’s own minor units in place of a universal two. The digits come from the engine, so a page on screen and the PDF of it agree. A formatted cell is also up to three characters wider, so a line that just fitted can wrap and move a page break.
- **A page the browser cannot fully paint no longer fails the render.** An image whose pixel data is corrupt past the size in its header left that page blank *and* rejected `renderComplete`, so a host awaiting it saw an unhandled rejection instead of an answer, and every page after it on screen went unpainted too. Such an image is now drawn as nothing and the page is drawn around it, marking and all; `renderComplete` resolves `true`, `rendered` fires, and no `error` event is raised. It never rejects for this reason again: a page that cannot be drawn is blank, not a failed render.

0.5.0

### @quario/viewer 0.5.0

### Minor Changes

- **Previewing with `fonts` now needs `fontkit`, not `@pdf-lib/fontkit`.** Install `fontkit` instead. The viewer measures host TrueType faces through `@quario/layout`, whose optional parser this is; the old package crashed on any OpenType face needing a shaping state machine, so those faces now preview where they used to throw.
- **The bar’s controls are drawn icons.** The zoom trigger, the menu’s check and the error panel’s dismiss are drawn from Lucide’s set rather than by hand, at one weight and one grid, and the dismiss no longer relies on the host’s font to draw a multiplication sign. They paint in `currentColor`, so `--qv-icon` and `--qv-icon-active` still recolour them.
- **An export button names its format in words.** Each one is a download icon with `PDF`, `XLSX` or `CSV` beside it, in place of the sheet drawing that lettered the format inside itself. The buttons are wider; the accessible name is unchanged.
- **A zoom no longer leaves a page part-drawn.** Changing the zoom while a page was still painting could leave that page carrying content at the old scale on a canvas sized for the new one — the rest of it blank — and nothing repainted it afterwards: scrolling away and back left it as it was. A page whose paint is still in flight is now replaced rather than reused, so the superseded paint has nowhere to land.

0.4.0

### @quario/viewer 0.4.0

### Minor Changes

- **`fonts`.** The font mapping a host passes to `pdf({ fonts })`, so the preview measures and draws in the same TrueType faces the document embeds.
- **`renderComplete` and `rendered` settle once the pages on screen are painted.** Both already answered for the newest render reaching the sheet; what that means is now stated: the pages the reader can see carry their pixels, and the pages further down the report do not hold the promise up. A screenshot or a pixel-reading test taken at that moment sees what the reader sees.
- **A split slot’s box now fills the split’s height.** A slot’s background and border stopped at that slot’s own content and now take the split’s height, the way a table cell’s take their row’s. See the `@quario/layout` changelog for the rule and for what it costs a report that relied on the short box.
- **The sheet has pages, and they break where the PDF’s do.** The viewer depends on `@quario/layout` and paints the same display list the pdf target writes, one canvas per page, in the faces the document itself uses. Zoom is a repaint, so text stays crisp at 200%; the menu’s first row reads “Fit page” and fit sizes one page to the width. Breaking: `targets` is exports only — no `"html"` target is required or read — and a `page` change re-lays the report out rather than resizing a sheet. The unlicensed marking is painted per page from the list; the `--qv-mark` token is gone with the DOM stamps.
- **A long report no longer asks for a backing store per page.** The viewer paints the pages near the viewport — the ones on screen and one screenful either side — and keeps every other page sized but blank. A report of a few hundred pages used to ask the browser for gigabytes of canvas; past what it would grant, pages came up blank with nothing reported, which on a HiDPI display took only a moderately long report. Every page still holds its place, so the scrollbar and the scroll extent are the whole report from the first frame, and a zoom step now repaints a handful of pages instead of walking the document.

0.3.0

### @quario/viewer 0.3.0

### Minor Changes

- **Report and group containers are flex columns.** Adjacent authored `spaceBefore` / `spaceAfter` margins add rather than collapse, matching the html reference stylesheet.
- **The adopted sheet occupies a line and preserves authored newlines.** Same `min-height: 1lh` and `white-space: pre-line` the html reference stylesheet now carries on `.q-item`, and `pre-line` on table cells.

0.2.0

### @quario/viewer 0.2.0

### Minor Changes

- **The sheet is now page-shaped, so short reports reserve a full page.** `page` sized the sheet’s width and padding and ignored its height, so a report shorter than a page was drawn on paper the shape of its own content — an A4 half-page invoice came out twice as wide as it was tall while the PDF export was a true A4 page. The sheet now takes at least one page of height as well. It remains a minimum, so a longer report keeps growing on the one continuous sheet, and nothing about a report of a page or more changes.

  **This is visible if you size a container to the viewer.** An A4 sheet at fit-width is roughly a thousand pixels tall against five hundred before, so a box fitted to a short report roughly doubles. Set `zoom` to a percentage small enough where the box has to stay small. The sheet still draws no page boundaries.

- **The adopted sheet’s baseline face moved onto `.q-report`.** It was on `.q-item` and `.q-table`, where a direct rule would have beaten a report’s own declared family. Same look; the fragment’s new root is what carries it.

- **The sheet no longer draws report text in the platform’s own face.** It hard-coded `system-ui`, which is SF Pro on macOS, Segoe UI on Windows and Roboto on Android — so the same report showed a different typeface to every visitor, and none of them matched what the PDF exported. Text declaring no `family` now renders in `sans-serif`, the same declaration the PDF target resolves to Helvetica. The viewer’s own chrome keeps `system-ui`, which is what a toolbar should do.

0.1.0

### @quario/viewer 0.1.0

### Minor Changes

- **An embeddable `<quario-viewer>`.** Assign a compiled report, the targets to render with, and data; it shows the HTML report on a continuous sheet, with zoom, fit, and export buttons for the targets you passed. It is not a target and never walks the event stream.

## @quario/editor

0.10.0

### @quario/editor 0.10.0

### Minor Changes

- The editor’s properties panel puts the styles in groups: Text, Alignment, Fill, Format, Link, Spacing, Padding and Border. A group shows only the styles that the selected node declares. A group that declares nothing is one line. The `+` of a group shows its other styles, and the `−` hides them again. The group stays open when you select another node.

  Each group shows its styles in the shape of their values. A value is beside its name. The four sides of padding are a grid with a mark for each side. The border is a table of sides by width, line and colour. Bold, italic, underline, strikethrough and uppercase are toggles with icons. A held toggle is on, an outlined toggle is off, and a dimmed toggle declares nothing. A style that holds an expression is a row of its own. A colour shows its value as text beside the swatch, and you can type a colour there. A colour that declares nothing shows a crossed square, not black. The `fx` toggle shows when the pointer or the focus is on its field. The rail is 296px wide.

  The chrome of the editor and the viewer has a new finish. The bar and the panels are white on a grey backdrop in the light scheme. The text is darker, and the focus colour is a deeper blue. The scrollbars and the selected text use the palette. The stage of the editor does not show a focus ring when you click the sheet.

  The editor has two new tokens, `--qe-icon-active` and `--qe-active`, which match the viewer’s `--qv-icon-active` and `--qv-active`. The other tokens keep their names, and a value that a host sets still wins.

- `<quario-editor>` takes a new `strings` property. It replaces the English words of the chrome, key by key. A key that you leave out stays English. Fixed text takes a string, for example `undo: "Rückgängig"`. Five messages carry a value and take a function of it: `hidden`, `asText`, `showGroup`, `hideGroup` and `pageName`. An unknown key, or a string where the editor expects a function, or the reverse, is a host mistake that the error panel names as `strings.<key>`. If a function throws, the editor draws the English text and reports the error one time for each record, as a `host-option` error. The marking, the message of an error, the messages of the engine and the names of the schema do not change. Set the `lang` attribute on the element to the language of your strings.

  `@quario/landing` exports `messages()` and `speaker()`, the convention that the viewer and the editor share for their `strings` records.

  The viewer now checks its `strings` record through `@quario/landing`. An unknown key now reads “expected a message the chrome draws”, where it read “expected a message the viewer draws”.

### Patch Changes

- A fast scroll through a long report stays smooth. Before, the viewer and the editor painted every page that came near the view during a fast scroll. On a slow device, that work dropped the frame rate a lot, and the page in view was blank on most frames anyway. Now a frame that moves more than half the height of the view paints no page. The page outline shows where each page is. The pages paint on the first frame that moves less, and on the frame after the scroll stops.

- Selecting a node before any page is on the sheet no longer throws. Before, a document that had problems from its first plan showed no pages, and a click on its issue threw an uncaught error. Now the click selects the node and the overlay paints nothing until a page is there.

- The pages stay sharp when the device pixel ratio changes. Before, the viewer and the editor painted the pages again only when a box changed size. A window that moved to a monitor with a different ratio, or a page zoom when the host fixes the size of the element, kept the old backing store, and the text was soft. Now the viewer and the editor paint the visible pages again at the new ratio.

  `sheet()` in `@quario/landing` has a new `watch()` method. It paints the reach again when the ratio changes, and it returns a function that stops the watch.

- Updated dependencies

  - @quario/landing\@0.5.0
  - quario\@0.15.0
  - @quario/layout\@0.11.0

0.9.3

### @quario/editor 0.9.3

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.

- Updated dependencies

  - quario\@0.14.0
  - @quario/landing\@0.4.1
  - @quario/layout\@0.10.0

0.9.2

### @quario/editor 0.9.2

### Patch Changes

- **Text sits in the middle of its line.** A line is 1.4 times its largest font size. Before, all of that extra space went below the text, so a capital touched whatever stood above its line, an image for example. Now half of the extra space goes above the text and half below, as a browser lays out a line.

  Every line keeps its height, so every page breaks where it did before. Each line of text is drawn 0.2 times its font size lower than before. The PDF, the viewer and the editor all change, because they draw the same layout. The HTML target already split the space this way.

- Updated dependencies

  - quario\@0.13.0
  - @quario/layout\@0.9.0
  - @quario/landing\@0.4.0

0.9.1

### @quario/editor 0.9.1

### Patch Changes

- Updated dependencies

  - quario\@0.12.0
  - @quario/layout\@0.8.1
  - @quario/landing\@0.3.2

0.9.0

### @quario/editor 0.9.0

### Minor Changes

- **A Document section on the rail.** The report default belongs to the document rather than to any band, so nothing on the sheet could be selected to reach it and the `style` key the report default feature exists for had nowhere to be edited. The rail now opens with a Document panel, above Properties and present whether or not anything is selected, offering 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.
- **The bands off the page are reachable.** `page.header`, `page.footer` and `empty` render nothing on the preview — a page band is drawn by a paginating target alone, and `empty` renders only over a dataset with no rows — so until now the editor preserved them and offered no way in. The Document section lists all three; selecting one opens a panel holding its items, with the verbs that add, remove and reorder them, and the breadcrumb leads back up from an item. They are drop targets like any other band, under the same anchor rule: a page band binds `page`, and `empty` binds nothing row-scoped. The sheet still shows nothing for them, and the reveal toggle is unchanged — a band the preview never shows has nothing to have left out.
- **The style rail offers `href`.** A run or a cell that may carry a link gets a field of its own, whose 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 rather than describing a band of cells.
- **One un-declare gesture across the properties panel.** Every style control that un-declares by emptying — a number, a colour, a currency code, a text field, an expression — now carries the × beside it, and the × appears only while the document declares that name. It used to appear always beside a colour, never beside a padding or a spacing, and only conditionally beside the flags and `size`. Its presence now means the same thing in every row: this name is declared, and clearing it hands the layer beneath back. A select keeps its blank option and gains no ×.

### Patch Changes

- **The box names are read from the engine.** Both packages used to decide for themselves which style names belong to the box. They now ask the engine’s `isBoxName`, so the three agree by construction. Nothing renders differently.

- **The format control offers the modifiers again.** Choosing a kind now draws a control for each modifier that kind takes — the digit count, how a negative reads and how a zero reads on a number, currency or percent, and the form on a date — and each one writes the whole declaration back, so setting one no longer drops its neighbours. The panel had offered none since `format` grew from one modifier to three.

- **An error quario replaces keeps the one it replaced, as `cause`.** A failure inside one of your own functions reaches you named where it happened — `detail[0].value [{{ total() }}]: ...` — which is a copy, rebuilt from the class and a prefixed message. The stack that says which line of your function threw used to go with the original. It now rides along: read `error.cause` for the throw as it happened, and the message for where quario was when it happened.

  The viewer’s and the editor’s own property checks do the same. A `page` or `fonts` value either element refuses throws a `TypeError` naming the property, and that error now carries the layout’s own account of the value behind it.

- **`renderComplete` answers for the newest render at the moment it settles.** A render that starts behind a pending `await` is the one the promise then waits for, so a host awaiting it during continuous editing waits for the last keystroke’s render. Before, the promise could settle for the render in flight when it was asked, or miss one that started behind it.

- Updated dependencies

  - quario\@0.11.0
  - @quario/layout\@0.8.0
  - @quario/landing\@0.3.1

0.8.0

### @quario/editor 0.8.0

### Minor Changes

- **A group’s `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.

  The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.

  **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.

  The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.

  The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.

  The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.

- **An editor you insert again renders once more, unless its last render landed on the sheet.** Two cases changed.

  A property written while the element was out of the document used to plan and render where nothing could receive the list, and left nothing owed. The editor came back showing the previous document. It now owes that render and produces it on reconnect.

  A document that compiled to no list — one with problems — left nothing owed for the same reason, so a standing debt survived it. It now keeps the debt until a render reaches the sheet.

  A render that failed while the element was out of the document already came back re-rendering, and still does. What changed there is why: the debt is now what every run carries until it lands, rather than a case the disconnect check happened to catch.

- **The design surface paints only what you can see.** An edit no longer slows down with the document. The editor laid one element over every rectangle the render drew, on every page. It rebuilt all of them behind each keystroke — 157,798 elements on a 987-page report. It now draws an element only where it shows something. Three things qualify: the selected node, the node under the pointer, and an item you drag. It draws them only over the pages on screen. A keystroke on that report drops from 946 ms to 503 ms. On a 99-page report it drops from 97 ms to 77 ms. Nothing about editing changes. There is simply far less to rebuild.

  **New: `boxes(path)` on `<quario-editor>`.** It answers every rectangle the render drew for a node, in document order, as client rectangles. Use it to anchor your own UI to a node: a badge, a comment marker, a step in a tour. It answers for every page, on screen or not. It answers an empty array for a node that draws nothing, for a path the document does not have, and before a render lands. Await `renderComplete` first.

  **Four class names change.** If you style the design surface directly, rename as follows.

  - `.qe-hit` is now `.qe-selection`.
  - `.qe-boxes` — the layer over the sheet — is now `.qe-overlay`.
  - `.qe-overlay` — the layer that holds grips, carets and placeholders — is now `.qe-furniture`.
  - `.qe-box` is gone. There is no element per rectangle any more, so there is nothing to select. Use `boxes(path)` to find where a node is drawn.

  This release does not change the `--qe-*` theming tokens, `.qe-peek` or `.qe-lifted`.

  **`@quario/landing` gains `pagesIn(top, bottom, pad)`** on the paged sheet. It answers which pages a band of the sheet covers. The two surface elements run this package for you, so you do not call it directly.

### Patch Changes

- Updated dependencies

  - @quario/layout\@0.7.0
  - quario\@0.10.0
  - @quario/landing\@0.3.0

0.7.1

### @quario/editor 0.7.1

### Patch Changes

- A keystroke no longer plans the document twice.

  The editor commits what an author types when they pause, and that commit re-planned and re-rendered the very document the keystroke had already planned — a second compile and a second layout pass per keystroke, for a preview identical to the one already on screen. On a long report that is the larger part of what typing costs. The editor now re-plans when the document it would plan is a different one, so a pause commits without redrawing anything.

  The `change` event is unaffected: it still arrives once per gesture, at the pause, carrying the committed document with the problems and warnings found on it. A burst of keystrokes is still one change, two commits in one turn are still one change, and a document whose plan has not settled still delivers nothing until it has.

- Updated dependencies

  - quario\@0.9.0
  - @quario/layout\@0.6.0
  - @quario/landing\@0.2.1

0.7.0

### @quario/editor 0.7.0

### Minor Changes

- **Only the pages you can see are on the sheet.** A page outside the reach used to be an empty canvas holding its place; now it has no element at all, and the sheet carries the document’s extent itself — its height and width are written from the layout, and it paints the page silhouettes. The stretch a reader scrolls through is exactly what it was, and an edit no longer builds and sizes a canvas for every page of the document before the first one is painted.

  This changes the sheet’s markup, which matters if you style it. `.qe-page` is now absolutely positioned inside `.qe-sheet`, and is present only for the pages on screen and one viewport height either side. A rule that gave a page a margin, or that relied on the pages being a flow of siblings, no longer applies — the sheet places its own pages, and does it with inline styles a stylesheet rule cannot override. `--qe-sheet-shadow` is unchanged.

  One cost, stated: a page outside the reach has no `role="img"` node, so assistive technology sees the few images of the reach renumbering as the reader scrolls rather than a document of named pages.

- **An image’s box is now as wide as the container it sits in.** A `background` or `border*` on an image item hugged the picture and now spans the content width, or the slot’s share inside a split; the picture itself does not move. See the `@quario/layout` changelog for the rule and for what it costs a report that relied on a border hugging a logo.

### Patch Changes

- **Turning a style flag off now turns it off.** Unchecking bold, italic, underline, strikethrough or uppercase used to erase the setting from your report definition rather than set it to off. On most items you could not tell the difference — nothing else was turning it on, so erased and off looked the same. On a report header or a group header you could: those are bold to begin with, so unchecking bold changed your definition and left the sheet exactly as it was, and checking it again changed nothing back. Unchecking now writes off, which wins over the heading’s own weight, so the sheet follows the box.

  Going back to “however this heading normally looks” is the new **×** beside the checkbox. It is there only while the flag is set to something, so an item you have deliberately un-bolded now reads differently from one that has never mentioned bold at all — a distinction the panel could not show before. `size` gained the same ×; it clears exactly what emptying the field already cleared.

- **A click during a fast scroll no longer lands on nothing.** Which page the pointer was over was found by looking at the pages currently drawn, so in the moment after a quick scroll or a pane resize — before the sheet had caught up and drawn the page you had arrived at — a click or a hover over that page selected nothing. The page under the pointer is now worked out from the document’s own geometry, which does not wait for anything to be drawn, so the page you can see is the page you can click. On a long report this is also less work per pointer move: finding the page used to cost a step for every page in the document, and now costs a handful however long the report is.

- **A selection marks every rendering of the node the same.** Selecting a table column highlights the column, and the heading cell and each body cell it draws are one selection — so the editor no longer marks the particular cell you happened to click more strongly than the rest. That mark suggested you had selected a single row, which is not something a report definition has: the rows are your data, and a column is one thing to edit however many times it is drawn. It also did not survive the next redraw, so it moved on its own while you typed.

  A selection is now a tinted region with an edge down each side rather than an outline around every cell, so selecting a column of a long table reads as the column rather than as twenty boxes.

- **Every published README says where the documentation is.** Each package now carries a Documentation section pointing at the reference, at the report schema that normatively specifies what a report may declare, and at the package’s own API. The paragraphs that used to end on an unstated contract — the event stream’s field semantics, the style vocabulary, page columns, the Content Security Policy a fragment with images needs, the formula mangling, and each target’s own contract — link the page that states it. Every link is an absolute URL, so it resolves from the npm package page as readily as from an installed copy.

- Updated dependencies

  - @quario/layout\@0.5.0
  - @quario/landing\@0.2.0
  - quario\@0.8.0

0.6.0

### @quario/editor 0.6.0

### Minor Changes

- **Styled runs are authored from the rail.** Select part of a cell’s value in the Properties panel and press a style chip: the editor splits the value into styled runs behind you, so bolding a word is styling rather than data entry. A selection that partly overlaps an existing run splits that run — the part you selected takes the declaration and the rest keeps what it had — and a selection covering the whole value writes the cell’s own style instead, so a document that never needed runs never grows one. A selection can never split a `{{ … }}` tag: it opens outward to the whole tag first.

  A chip whose runs disagree shows indeterminate, and pressing it sets all of them. Below the value field, a value written as runs lists them one per row, each editable and deletable; deleting the last leaves the empty value a new item starts with. The list is deliberately not reorderable — a run list is a sentence, and dragging a fragment past another produces text nobody typed.

  The editor writes the smallest document that says what you meant: adjacent runs that end up styled alike are merged, and a single unstyled run collapses back to a plain template string. Both happen when a gesture ends, never while you are typing. Undo is per run: typing in one run and then in another is two steps, not one.

- **The rail lists the engine’s warnings beside its problems, and the `change` event carries them.** quario’s `plan()` now reports declarations that nothing will read — a `currency` on a cell that is not formatted as one, a table whose column widths leave a trailing share to nobody. The rail’s **Problems** section is now **Issues** and lists both, an advisory in its own colour, each entry still selecting the node it names. The `change` event’s detail gains `warnings` alongside `schema` and `problems`, so a host that persists on change sees what the author sees; the field is additive and no listener breaks. A warning is never fatal: a document carrying only warnings compiles and renders as it always did, so a save button driven by `problems.length` should stay driven by it.

- **`fonts` is validated as a host property.** It was the one the editor never checked, so a malformed record reached the target inside the render and came back as a render failure about a document that was never at fault. Its shape is now checked with the other host properties and reported as `host-option`, named on `fonts` rather than on the target’s `options.fonts`. A face that will not parse, or a missing parser, is still found while the report is measured and remains a render failure there.

- **The sheet carries pixels only in the reach, and `renderComplete` settles on it.** Every page used to take a backing store and be painted before a render settled, so a large document asked for gigabytes: 13.4 GB at a thousand pages on a HiDPI display. Only the pages on screen and one screen either side carry pixels now — the store is flat at 27 MB whatever the page count — and every page keeps its size, so nothing about scrolling changes.

  `renderComplete` settles once **the reach** has finished trying to paint, where it used to wait for every page. A host that awaited it and then read pixels off a page far down the document will now find that page blank until it is scrolled to. This is what the viewer has meant since it gained a reach.

- **A rejected host property reports as `host-option`, not as a render failure.** `EditorErrorKind` gains a third member, so a host switching exhaustively over the kind needs a case for it. Previously a `page`, `instance`, `functions` or `colorScheme` the editor rejected was reported as `mount-render` or `update-render` — a render that was never attempted — and the panel told the reader “The report could not be displayed”, which reads as the document being at fault. It now says the editor is misconfigured and that the report was not the problem.

- **A large document no longer throws while reaching the sheet.** The editor handed the sheet’s box elements over one call argument each, so a document drawing more boxes than a call takes arguments — around 175,000, roughly a thousand pages of a wide table — failed while building the sheet. Nothing was drawn, nothing reached the error panel, and no `error` event fired.

- **`renderComplete` no longer rejects.** It is documented never to, and a throw while putting the list on the sheet made that false: that work runs in the render task’s own callback, outside the guard that turns every other failure into a value. Such a failure is now reported on the panel and through the `error` event like any other, and `renderComplete` answers `false`. A render that fails on its first attempt also no longer counts as having landed, so it reports as `mount-render` rather than `update-render`.

- **Reordering a group keeps it selected.** Pressing Move up or Move down in the Groups list used to clear the selection, closing the Properties panel on the group being moved — so moving a group several places meant re-selecting it between presses. The moved group now stays selected, at the index it landed on.

0.5.0

### @quario/editor 0.5.0

### Minor Changes

- **The Properties panel edits a `format` modifier.** The kind picker now carries a second control beside it: a digit spinner for `number`, `percent` and `currency`, capped at the count the engine accepts, and a form picker for `date`. A kind that takes no modifier draws none, and changing the kind drops the old modifier rather than carrying one across that the new kind would refuse.

  Clearing the modifier writes the declaration back as the bare kind, so a document only holds the object form while it is saying something with it.

- The style rail offers a control for the new `currency` declaration: a three-character field that upper-cases as you type, cleared back to the instance’s own code.

- **Merge and unmerge a total cell.** A total row’s cell can now be made to span several columns from the editor, not only preserved from a document that already declared one. Select the cell and **Merge** absorbs the cell to its right, keeping the selected cell’s own content — repeat to reach further across. **Unmerge** splits a spanning cell back into single columns. Merge is offered only while there is a neighbour to absorb, and Unmerge only while the cell spans more than one column.

- **Numbers presented through `format` now show a fixed two fraction digits**, the same as every other target: the preview renders `1,000.00` where it rendered `1,000`, `21.00%` where it rendered `21%`, and a currency’s own minor units in place of a universal two. The digits come from the engine, so what the preview shows is what the rendered document shows. A formatted cell is also up to three characters wider, so a line that just fitted can wrap.

- **A group’s verbs moved into the Properties panel.** A group row in the Groups list now carries its name and the pair that reorders it, and nothing else. The four verbs that were marked `H`, `F`, `⇱` and `×` on that row are in the panel instead, where they are written out: **Add a header band** / **Remove the header band**, the same pair for the footer, **Remove the group, keeping its items**, and **Remove the group and its bands**. Click a group’s name to select it and the panel offers them; the row now also shows which group is selected. Each band verb is one button whose label says which of the two it currently is, so nothing presents adding and removing a band as a toggle — removing one still discards its contents, and there is no hidden band to restore.

- **BREAKING: a `change` event now needs a plan behind it.** The event’s `problems` describes the document the event carries, on every path. Two consequences, both visible from a host:

  - When a report fails to render, the event now carries the problem list the document actually earned — which is empty, since a document with problems never reaches the render at all. It used to repeat whatever the last render that worked had found, so a `problems.length` check could be answering for an older document.
  - When nothing could be checked — a property the editor rejects, or the engine itself throwing — no `change` event is sent at all. It used to send one carrying the previous document’s problems. The edits are not lost: they are held, and the next render that gets far enough to check the document delivers the settled result as a single event. Listen for `error` to know you are in that window; a save button driven by `problems.length` should be treated as stale until the next `change` arrives.

- **The totals block is selectable**, from the breadcrumb above a total row or a total cell. It carries the block’s own `style` and `visible`, and the “Add total row” verb sits on it as it does on the table and on a row. There is no hit box for it on the sheet: the block is exactly its rows, so no rectangle drawn is the block rather than one of them.

0.4.0

### @quario/editor 0.4.0

### Minor Changes

- **Table columns keep a merged cell intact.** A total row whose label runs across several columns, or a header that covers its neighbours, now survives every column verb. Adding a column inside such a cell widens it; adding one at its edge gives the new column a cell of its own. Removing a column narrows the cell rather than breaking it, and removing the one a header was written on keeps that label on the columns it still covers. Duplicating a column under a merged header widens the header instead of leaving two of them. Reordering columns inside a merged region is free, because nothing there is lost by the move; dragging a column that has a cell of its own into the middle of a merged one is refused, and the drop indicator will not go there. A merged cell can be preserved and edited around, but not yet created in the editor.
- **A column a header covers is no longer offered a header of its own** in the properties rail, where an empty field used to invite an edit that could only produce an error.
- **Vertical alignment** in the style rail for table cells and total rows: a three-button group beside the horizontal one, writing `valign`.
- **The controls that were text characters are drawn icons.** Undo and redo, the gutter’s grip and its insert `+`, the alignment triple, a group’s move pair and its remove-with-bands, the field clear and the error panel’s dismiss are drawn from Lucide’s set at one weight and one grid, in place of characters whose weight and size shifted with whatever font the host resolved. They paint in `currentColor`, so `--qe-icon` still recolours the bar and rail while the gutter furniture keeps its own sheet-relative grey, and each control keeps the accessible name it had.
- **Reveal, `fx`, the grip menu and the Columns verbs stay text.** Where the label is the information, a drawing would be a downgrade, so those controls are unchanged.
- **Adding a total row selects the row it added.** The selection landed on the row above the new one, so the properties panel showed the wrong cells and a second Add total row inserted in the wrong place. Adding a column had always selected the new column; the two verbs now answer the same way.
- **A `change` event never carries an uncommitted document.** Typing into a field between a commit and the plan that followed it could deliver the keystroke’s document, which had entered no history and matched no undo. The host now hears nothing until the gesture ends, and the event’s schema and problems describe the same committed document.
- **The style rail offers `uppercase`.** The rail carried its own copy of the style vocabulary and had never learned the name; it now reads the engine’s list, so a declaration the engine accepts is one the rail can write.

0.3.0

### @quario/editor 0.3.0

### Minor Changes

- **`fonts`.** The font mapping a host passes to `pdf({ fonts })`, so the author designs in the faces the document will embed.
- **A split slot’s box now fills the split’s height.** A slot’s background and border stopped at that slot’s own content and now take the split’s height, the way a table cell’s take their row’s. See the `@quario/layout` changelog for the rule and for what it costs a report that relied on the short box.
- **The design surface is the layout list.** The editor depends on `@quario/layout` and lays the planned report out on pages, painted per page; every rectangle maps back to its schema node through the list’s hit boxes, so selection, the gutter grip, drops and column drags resolve through the layout rather than through rendered markup. Breaking: the `target` property is gone — no html target is passed — and the `target` error kind with it.
- **A page is drawn the size the viewer draws it.** The design surface used to put a point on a pixel, so a page stood about a quarter smaller here than in the viewer showing the same report. A point is now 96 dpi over 72 on both, and the two agree. Breaking: an embedding sized around the old, smaller sheet needs its container re-checked.
- **`renderComplete` waits for the paint.** It settled as soon as the report had been laid out, with the pages still painting, so a host that awaited it and then read the sheet could read a blank one. It now settles once every page has finished painting.

0.2.0

### @quario/editor 0.2.0

### Minor Changes

- **The style rail carries the box.** Padding is a count from zero, border style a closed choice, border colour a picker, on every node that wears the vocabulary. An image shows align, background, and the box, not the typeface names it cannot wear.
- **`format` is a closed choice on the style rail.** `number` / `currency` / `percent` / `date`, on nodes that have a cell value. Not images, not a total-row box, not the table itself. `spaceBefore` / `spaceAfter` are counts from zero on items and images.
- **A table total is N rows the editor can add and remove.** Column operations splice each row’s `cells`. Removing the last row drops the key rather than leaving `total: []`.
- **Report and group containers are flex columns.** Adjacent authored `spaceBefore` / `spaceAfter` margins add rather than collapse, matching the html reference stylesheet.
- **The adopted sheet occupies a line and preserves authored newlines.** Same `min-height: 1lh` and `white-space: pre-line` the html reference stylesheet now carries on `.q-item`, and `pre-line` on table cells.

0.1.0

### @quario/editor 0.1.0

### Minor Changes

- **An embeddable `<quario-editor>`.** A banded document designer whose design surface is the rendered preview itself: assign a starter schema, a quario instance, the html target and sample data; the report renders on the sheet, clicking output selects the definition behind it, and items, table columns and groups rearrange structurally — every edit recompiles and re-renders through the real engine. A properties panel edits the selected node’s literals, styles, templates and expressions; problems list with their exact spans; undo and redo walk whole gestures. The edited document reaches the host on every change, frozen, with its problem list. A failure reaches the error panel and the `error` event once per distinct failure, however many renders it survives, and a panel the author dismisses stays dismissed.
- **The preview answers to the keystroke, undo to the gesture.** Typing in the properties panel and dragging a colour picker move the report and its problem list live, so a half-written expression shows what is wrong with it as it is written. Neither takes an undo step or reaches the host until the gesture ends — on blur, on Enter, on the picker’s release, or after a half-second pause — so one edit is one undo, not one per character. Interrupting an edit never loses it: pressing another control or selecting something else leaves the typing behind as its own undo step rather than folding it into what came next.
- **The adopted sheet’s baseline face moved onto `.q-report`.** It was on `.q-item` and `.q-table`, where a direct rule would have beaten a report’s own declared family. Same look; the fragment’s new root is what carries it.

## @quario/layout

0.11.0

### @quario/layout 0.11.0

### Minor Changes

- A link to `#` and a name goes only to a group instance that declares that name as its `label`. Before, the PDF and the viewer also matched an instance with no `label` whose outline title read the same, which is its first header’s text. A link to a name that no instance declares now goes nowhere, as the spec says.

  In `@quario/layout`, a `Mark` carries `label` where its instance declares one. The new `destinations(marks)` and `destination(href, byLabel)` say where a link goes. The PDF target and the viewer use them.

- Breaking: every instance of a table now shares one set of column widths. Before, a table under a group sized its columns from the content of each instance alone. The columns therefore moved left and right from one group to the next. Now the layout sizes each column from the widest content that column holds in any instance of the render. The columns line up down the document. A table that renders once keeps the widths its own content asks for. The text that a report draws does not change, but the PDF of a report with a table under a group changes its geometry. The viewer and the editor show the same change, because they draw the same layout.

  The layout reads the event stream once, as before. It measures the votes of a repeated table before it places the first instance, and it uses those measurements again when it places the rows. On the benchmark report, a flat report and a grouped report take the same time as before. A grouped report with no repeated cell text takes about 12% longer.

- A screen reader now reads a table on the viewer’s pages as a table. Its headings are the column headers. Each page holds its own part of a table. That part starts with the headings again. Each page of the layout list carries `tables` for this. A table there lists its rows, and each row lists its cells. A cell names the range of the page’s `lines` that it drew. An empty cell is an empty range, so each cell after it keeps its column.

### Patch Changes

- A paragraph that a page break splits keeps its join at the break. Before, the last line on the page had no `TextLine.join`, so a copy out of the viewer ran the two lines together. Now that line joins the first line of the next page with the text the wrap dropped, as at any other wrap.
- Fix: a long table no longer holds the main thread while the layout places it. Before, the layout measured and placed a whole table in one task. A table report of about 1,000 A4 pages held the main thread for about 250 ms. A report of about 3,000 pages held it for about 800 ms. During that time the viewer’s progress strip stopped. Clicks waited too. The layout now hands the loop back every 1,000 rows, so no task is longer than one frame on that report. The display list is the same as before.
- The engine exports `pageBox(page, at)` and `paper(page, opening, at)`, the rules every target with a page follows. `pageBox` checks a host’s `page` option. `paper` says which size, margin and orientation are in force for a render, and the page box they come to. `@quario/layout` and `@quario/docx` now use them, and their behaviour does not change.
- Updated dependencies
  - quario\@0.15.0

0.10.0

### @quario/layout 0.10.0

### Minor Changes

- **An outline step now lands on its group instance.** Before, the layout gave each outline entry a `y` that the target measured up from the page bottom. The `Mark` type said that `y` reads down from the top, as an op’s `y` does. The viewer read it that way, so an outline row or an internal link scrolled near the foot of the page and not to the instance. The layout now gives `mark.y` from the top of the page. The viewer step lands on the header of the instance. The PDF target turns the value over for each bookmark and link destination, so the PDF bytes do not change. A host that reads `marks` from the layout target and turned `y` over itself must stop doing that.

- **An image item now accepts SVG bytes.** Before, `source` had to yield PNG or JPEG bytes, and SVG bytes were a render error. Now the engine also names UTF-8 text whose root element is `<svg` as an SVG, and the image event carries `format: "svg"`. The engine reads the size from the root’s `width` and `height` when both are absolute lengths, and else from its `viewBox`. An SVG that states no size this way is the same render error as a PNG that is too short to state its size. The message for bytes that are not an image now says “PNG, JPEG or SVG”.

  The HTML target puts an SVG in an `<img>` under `data:image/svg+xml`, so the SVG never becomes markup in the fragment. The PDF target draws an SVG as vector paths. It draws no picture that the SVG names, and it fetches nothing. SVG `<text>` draws in the standard Helvetica. The viewer paints an SVG through a `data:` URI, so a page that shows one needs the Content Security Policy grant `img-src data:`. A PNG or a JPEG still needs no grant there. The XLSX and DOCX targets cannot draw an SVG, so an SVG image is a render error that names the item.

### Patch Changes

- **The package now carries edition 1.1 of the Quario License Agreement.** A license now covers one production deployment, not one developer. Each deployment needs one license key. Development, staging, CI and test environments need no key. A key covers every release dated up to one year after you buy it, and those releases stay licensed for good. A key is not a subscription, and it never renews automatically. A refunded or reversed payment voids the keys it bought. Software that your customers install or resell needs separate OEM terms.
- Updated dependencies
  - quario\@0.14.0

0.9.0

### @quario/layout 0.9.0

### Minor Changes

- **Text sits in the middle of its line.** A line is 1.4 times its largest font size. Before, all of that extra space went below the text, so a capital touched whatever stood above its line, an image for example. Now half of the extra space goes above the text and half below, as a browser lays out a line.

  Every line keeps its height, so every page breaks where it did before. Each line of text is drawn 0.2 times its font size lower than before. The PDF, the viewer and the editor all change, because they draw the same layout. The HTML target already split the space this way.

- A document may now name its paper and turn it, and its margin may name each side.

  ```jsonc
  {
    "page": {
      "size": "letter",
      "orientation": "landscape",
      "margin": { "top": 36, "right": 29, "bottom": 36, "left": 29 }
    }
  }
  ```

  `page.size` takes `"A4"` or `"letter"`. A `[width, height]` pair stays a target option. `page.orientation` takes `"portrait"` or `"landscape"`, and it turns whichever paper is in force. `page.margin` still takes one number for all four sides.

  The rule `page.margin` already followed now covers all three keys. The document’s key wins where it declares one, a target option applies where it does not, and both declaring the same key is a render error. `orientation` in a document beside a target’s `size` is not both.

  Each target writes the paper: the PDF and the layout draw on it, HTML writes an `@page` size and per-side margin, and DOCX writes `pgSz` with `w:orient` and `pgMar` per side. XLSX writes the paper code, the orientation and each print margin. CSV withdraws all three.

  The capability descriptors gain `page.size` and `page.orientation`, and both are names a document may mark `required`.

  A target’s margin error now reads `expected a non-negative number, or { top, right, bottom, left }, that leaves the page room`.

- **Breaking: a split’s `style` now ranks over its band’s role default.** The PDF, the viewer, the editor and the HTML fragment now agree with the DOCX and XLSX targets and with the spec.

  Before, a split in a report header ignored the type it declared:

  ```jsonc
  {
    "type": "split",
    "style": { "size": 8, "bold": false },
    "slots": [
      { "type": "text", "value": "{{ $.input.seller }}" },
      { "type": "text", "value": "{{ $.input.customer }}" }
    ]
  }
  ```

  Its slots rendered bold at the report header’s 1.4× size. A group-header split with `"bold": false` rendered bold too. Now the slots render at 8pt and not bold, as the same declaration on each slot does. A split with no `style` renders as before.

  In the reference stylesheet, each band-role rule now selects `.q-<role>:not(.q-slot > *)`: a band’s text item or split, and never an item in a split’s slot. A slot’s item inherits its type from the split. Before, a slot’s item in a styled report-header split took 1.4× the split’s size. The group-header, group-footer and report-footer margins now also sit on a split rather than on each of its slots. A host rule on `.q-item.q-report-header` that loads after this sheet still wins.

- **The viewer’s pages can be read and copied.** The viewer now puts the report’s text over each page it paints. A screen reader reads it, a magnifier follows it, and a reader selects it and copies it. Nothing needs to be declared or turned on.

  A screen reader meets each page as a group with the name “Page n of N”. It reads the page header first, then the body, then the page footer. The first line of a group header is a heading at the depth of that group. An image with an `alt` has that name, and an image without one is skipped as decoration. The unlicensed marking is read on every page and is never part of a copy.

  A copy takes the text as the report wrote it. A paragraph that wraps on the page copies as one line, and so does a row of a table, with a space between its cells. The text covers the pages near the viewport, as the paint does. Every page that the selected text spans stays while the reader keeps the text selected. Select all therefore takes the pages near the viewport, and the PDF export is the whole report.

  What changes for a host:

  - A page canvas is now hidden from assistive technology. Before, it was an image with the name “Page n of N”. That name is now on the text over the page.
  - A linked run is now a link inside that text. Before, it was an empty region over the words, named by an `aria-label`. The `.qv-link` class stays. The `.qv-links` layer is now `.qv-mirror`.
  - A `#` link to a label that no group carries is now plain text that a reader can select. Before, it had nothing over it.
  - `--qv-focus` also tints selected text.

  `@quario/layout` gives each page a `lines` array. Each entry names the text ops of one laid-out line and the schema node the line belongs to. The first line of a group header also carries the depth of its outline entry. A line that continues as the same line of text carries a `join`: what a wrap dropped, or a space to the next cell of a table row or slot of a split. An image op carries its rendered `alt`. `fontOf(font, size)` returns the CSS font the painter draws a run in, and `TextOp.href` is now declared.

  `@quario/landing`’s `sheet()` takes a fifth argument, `named`. Pass `false` and each page canvas is hidden from assistive technology instead of being an image named “Page n of N”.

### Patch Changes

- Updated dependencies
  - quario\@0.13.0

0.8.1

### @quario/layout 0.8.1

### Patch Changes

- Updated dependencies
  - quario\@0.12.0

0.8.0

### @quario/layout 0.8.0

### Minor Changes

- **`negative` and `zero` reach the page.** The measurer and the painter present through the engine’s `format()`, so a cell declaring either is measured and drawn as it reads: a negative in parentheses, a zero as a dash.
- **A collapsed group does not draw its content.** An instance whose group declares `collapsed` renders as its header and footer, with the rows between them left out — a page has nowhere to put a row a reader could open. Aggregates are unchanged, so a footer still totals them.
- **A group’s `label` titles its outline entry.** Where an instance declares one, the entry takes it and no group-header item takes it back — the label exists to say what the visible text does not. Without one the entry falls back exactly as before: the first header item’s text, else `name: key`. A hollow instance is still no entry at all.
- **A linked run is a phrase of its own, carrying where it points.** Two stretches that link to different places are never merged into one phrase even when they draw identically, because a phrase is the rectangle a painter hangs a link on. A text op carries `href` beside its `x`, `y`, `w` and `h`; a run wrapping across two lines is two ops, which is two rectangles.
- **The report header scales with the report default.** The band-role size is a factor, 1.4× the size it sits over, rather than a fixed 14. A document declaring no size keeps its bytes, because 1.4× the baseline 10 is the same 14. A document declaring `"size": 20` now renders its report header at 28 where it rendered at 14, smaller than its own body. Before, the headline kept the target’s fixed size whatever the document declared. The PDF target paints this list, so its pages move with it.

### Patch Changes

- **The box names are read from the engine.** Both packages used to decide for themselves which style names belong to the box. They now ask the engine’s `isBoxName`, so the three agree by construction. Nothing renders differently.
- **A table inside a nested group is measured once.** A group instance is held until the layout knows whether it moves whole to the next page, and an instance inside another was held twice: once by the outer instance and again by its own hold when the outer one handed it back. Each hold wrapped every cell of the table again to measure it. The inner hold now takes the measurement the outer one already made, where the width is the same, and only files the row heights back. Nothing is placed differently. A report grouped two or three levels deep lays out faster.
- **A column is no longer abandoned when a group instance is too tall to hold.** Laying a report out in columns buffers what is coming so the strips can be sized. That buffer stops once what it holds is more than the columns can take, and where it stopped inside a group instance, the height it had for that instance was a part of it rather than the whole. A part is small enough to read as an instance the next column could take, so the instance moved there, left the column it was in short, and ran past the bottom of the new one anyway. Such an instance now continues in the column it is in, which is what an instance too tall to move has always done everywhere else.
- **A font that parses and then fails is named on the declaration that brought it in.** A face whose bytes will not parse already reported as `options.fonts.<name>.<variant>: ...`. Everything the parser does afterwards now reports the same way: shaping a run to measure it, and reading the face’s own metrics or character set. Such a failure used to reach you as the parser’s own error, with nothing in it to say which family you declared. The parser’s account is kept as `cause`.
- Updated dependencies
  - quario\@0.11.0

0.7.0

### @quario/layout 0.7.0

### Minor Changes

- A font parser that will not load no longer reports itself as a missing package.

  Passing `fonts` needs the optional peer `fontkit`, and every failure to load it used to arrive as `options.fonts: install fontkit to measure TrueType families` — advice a host had often already taken. A stale bundler cache, a 504 from a dev server, a network failure or a throw from inside the parser all read as an install that was already correct, and the real error was discarded.

  The message now says what did not happen instead of why:

  ```
  options.fonts: the font parser did not load. Install fontkit to use TrueType families
  ```

  The error that actually threw is attached as `cause`, so the 504 or the stack behind it is one expand away in a console.

  Why the message no longer names the cause: a failed dynamic import cannot be read as “the package is absent”. Node says so with `ERR_MODULE_NOT_FOUND`, but no browser carries a `code` at all, and the one message browsers do give covers a 404, a 500, a wrong MIME type, an unreachable host and a nested import failing inside a module that itself loaded fine.

  `loadFontkit` is now exported. It loads the parser once and hands the same module to every caller, which is how `@quario/pdf` registers the parser this package measured with.

- **A group instance now moves whole to a fresh page rather than breaking across one, wherever a page could hold the whole of it.** A short instance near the foot of a page used to split, leaving a line or two behind. It travels together now. Every target that paginates gains it, so the PDF target, the viewer and the editor all do.

  The rule already applied inside a page-column region, where an instance that fits one strip opens the next instead of crossing the boundary. It applies outside one too now, where the fresh column is the next page rather than the next strip. The two cases read the same way, and an author writes nothing to get either.

  An instance longer than a page still breaks, the way anything too tall does. The target learns an instance’s height by holding its content until the instance closes, and it gives up holding at one page of content, so that is the limit. When an outer instance gives up, each instance inside it is still kept whole on its own.

  This is best-effort rather than a guarantee, and the spec’s list of this target’s best-effort behaviours says so. A group header still travels with its first content unit, which is the guarantee, and which is what decides for an instance too long to move.

- **A group’s `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.

  The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.

  **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.

  The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.

  The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.

  The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.

### Patch Changes

- **A long report with no borders lays out about a seventh faster.** Both figures below come from one paired run. A thousand-page table drops from 447 ms to 380 ms. A hundred-page one drops from 48 ms to 37 ms. Every target that paginates gains it, so the PDF target, the viewer and the editor all do.

  The box model built three property names for every side of every cell. That is twelve strings a cell, to discover that the cell declares no border. It reads the side’s width first now, and stops there when there is none.

  A report that does declare borders gains less, because the early stop never fires for a side that resolves. It still saves the name building, which every cell paid. Nothing else changes: the pages, the boxes and the text are identical to the byte.

- **The longest reports lay out a little faster again.** A thousand-page table drops from 376 ms to 350 ms, the median of three paired runs. Shorter reports gain less: a hundred-page one drops from 38 ms to 37 ms.

  To decide how wide a column should be, the layout asked each cell how wide its text would be with nothing to wrap against. It answered that by laying the text out in full, then keeping one number and throwing the lines away. It now computes the number directly. The pages, the boxes and the text are identical to the byte.

- **Long reports lay out about a quarter faster.** A thousand-page table drops from 620 ms to 473 ms. A hundred-page one drops from 64 ms to 52 ms. Every target that paginates goes through this, so the PDF target, the viewer and the editor all gain it. A keystroke in the editor is most of a layout pass. An author editing a long report feels it directly.

  Measuring text is the most expensive thing the layout does, and it measured each piece of text four times. It measures it once now. Nothing else changes: the pages, the boxes and the text are identical to the byte.

- Updated dependencies

  - quario\@0.10.0

0.6.0

### @quario/layout 0.6.0

### Minor Changes

- **Every target factory now refuses an option it does not understand.** An unknown key, a key with a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx` already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding one.

  The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A fourth document property is one edit rather than three.

  **What this changes for you.** One options object spread across several targets stops working if any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`, `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works. TypeScript does not warn about this: excess-property checking fires on an object literal and not on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.

  Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and `{ meta: { title: config.title } }` over a config that carries neither are both fine. `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws instead of turning path stamping on. A `fonts` mapping given as an array is refused by `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.

  Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests `instanceof Error` is unaffected. One that compares the constructor is not.

  `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null` and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }` is a property you did not write rather than one of the wrong type, and an inherited enumerable key is no longer reported as an option you wrote.

  `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the object you passed no longer changes what a configured target writes.

### Patch Changes

- Updated dependencies
  - quario\@0.9.0

0.5.0

### @quario/layout 0.5.0

### Minor Changes

- **An image’s box is now as wide as the container it sits in.** A `background` or `border*` on an image item hugged the picture and now spans the content width — or the slot’s share inside a split — with `fit` sizing the picture and `align` placing it inside, the way a text item’s box has always been drawn. The picture itself does not move. Breaking: a report that relied on a border hugging a logo now draws that border across the full width, and no declaration asks for the old shape.

- **A page of the display list no longer carries `width` and `height`.** Read the list’s own `width` and `height` instead: they are the paper every page of the report is drawn on, and every page’s pair was a copy of them. A report has one page size for the whole render — that has always been so, and page size is a target option rather than something a document declares — so a page is identified and positioned, and asked nothing about its size. A page now carries `{ number, total, ops, boxes }`.

  If you consume the list yourself, the fix is one substitution:

  ```js
  canvas.width = list.width * 2; // was list.pages[i].width * 2
  canvas.height = list.height * 2; // was list.pages[i].height * 2
  ```

  `paint()` is unchanged in shape — it still takes one page — and it now fills the white over the whole canvas rather than over the page’s own rectangle. **Size the canvas to the page before you call it**, as the sizing example above does: the ops are in the page’s own coordinates, so a canvas short of the paper clips them whatever the fill covers. That was always so; it is now written down in the README and on `paint()` itself.

- **`Layout.pages` is typed as a non-empty list.** A render always lays out at least one page — the first page opens whether or not anything renders on it — so `pages[0]` no longer needs a check for a list that cannot exist. Runtime behaviour is unchanged; this only states in the type what was already true.

- **`Paper`** — the page box a report is drawn on, `{ width, height }` in points, and what a display list’s own `width` and `height` are. `Layout` extends it, so `list.width` is unchanged and the pair now has a name to refer to.

### Patch Changes

- **A page’s right and bottom edge could be left unpainted.** Where a caller rounds the backing store up to a whole device pixel — A4 at 100% asks for 793.71 and takes 794 — `paint()` filled its white over the page’s exact rectangle and left the fraction past it transparent. On a canvas with white behind it that is invisible; on one without, a hairline of whatever is behind the canvas showed along two edges. The fill now covers the whole canvas.
- **Every published README says where the documentation is.** Each package now carries a Documentation section pointing at the reference, at the report schema that normatively specifies what a report may declare, and at the package’s own API. The paragraphs that used to end on an unstated contract — the event stream’s field semantics, the style vocabulary, page columns, the Content Security Policy a fragment with images needs, the formula mangling, and each target’s own contract — link the page that states it. Every link is an absolute URL, so it resolves from the npm package page as readily as from an installed copy.
- Updated dependencies
  - quario\@0.8.0

0.4.0

### @quario/layout 0.4.0

### Minor Changes

- **Per-run typography, decoration and highlights.** A styled run resolves its own face, size and colour, and a line takes the largest size among its runs. `underline` and `strikethrough` are drawn per run — over that run’s width, in that run’s colour — where they used to stroke the whole line in the first piece’s colour. A run’s `background` paints a highlight rectangle behind its text; a cell’s own background is still painted once, at cell scope.
- **`checkFonts` is public.** It checks the shape of a `fonts` mapping without loading a parser, and takes an optional name to prefix a failure with, the way `pageBox` does — so a surface validating its own `fonts` property can report the mistake against that property rather than against `options.fonts`.
- **An outline `Mark` carries only what it declares.** Every mark shipped an extra `titled` boolean — bookkeeping for whether a header had claimed the entry — which was never part of the `Mark` interface and which nothing reads. It is the open group instance’s own state now, and no longer travels on the object a consumer receives.

0.3.0

### @quario/layout 0.3.0

### Minor Changes

- The text join reads a cell’s own `currency` code, ahead of the instance’s default, so every target built on this package presents a per-cell denomination.
- **A group instance that renders nothing no longer takes up space.** A group whose header and footer items all resolve `visible: false`, with nothing visible under it either, used to open the same half-line gap as any other instance and could push the content after it onto a new page. A run of them spaced whatever followed by a half-line each, so collapsing a level left the rows above it at uneven distances. Such an instance now occupies nothing at all and adds no PDF bookmark, which is what makes a group collapsible while its rows stay in the aggregates. A group that declares `break: "page"` or `reset: "page"` still starts its page either way. Every document with such a group renders slightly shorter than it did.
- **Numbers presented through `format` now show a fixed two fraction digits, matching every other target** — `1,000.00` where `1,000` was rendered, `21.00%` where `21%` was, and a currency’s own minor units in place of a universal two. Because this package presents text and then **measures** it, a formatted cell is now up to three characters wider than it was: a line that just fitted can wrap, which can move a page break in a PDF, the viewer, or an editor preview. Nothing else about wrapping changed.
- **An image failure now names the item that asked for the bytes.** A file too short to carry a size failed saying only that the size could not be read, naming no item, so a report with two pictures gave no way to tell which one was bad. The message is now prefixed with the item’s `source` path, as every other render error is.
- **An image the browser cannot decode no longer costs the whole page.** A PNG or JPEG whose pixel data is corrupt past the size in its header lays out like any other — the size is all that is read of it — and used to throw out of `paint()` before its first draw op, leaving the page blank: no white fill, none of the other content, and no licence marking. The image is now drawn as nothing and the page is drawn around it, so what a bad image costs is the image.

0.2.0

### @quario/layout 0.2.0

### Minor Changes

- **A spanning cell is drawn as one box** across the columns it covers, starting where the first of them starts. It takes no part in allocating their widths, and a column no cell votes on opens at the cell-padding floor, so an empty table still shows its geometry. A spanning row too tall for the page it is on slices like any other, each slice following the geometry of the page or page-column strip it lands in.

- **`valign`** places a cell’s lines, or a slot’s lines or picture, in the slack its row or split leaves over its own content: middle halves it, bottom takes it. The box does not move, and a picture is not scaled. A row too tall for any page, sliced across pages, paints from the top.

- **A row’s box is drawn by the row’s cells.** A box declared on a table row used to be one rect across the summed column widths; it is now each covered cell’s own, so a row’s `borderBottom` still reads as one continuous edge while a row’s `borderLeft` becomes an edge on every cell rather than one at the row’s outer left. A row’s border also occupies height now, as a cell’s always has, so a bordered row is taller by its border’s width.

- **Measuring TrueType families now needs `fontkit`, not `@pdf-lib/fontkit`.** Install `fontkit` instead; nothing else about `options.fonts` changes. The old package’s bundle crashed with a bare `ReferenceError` on any OpenType face needing a shaping state machine — which is every Devanagari, Bengali, Tamil, Khmer or Myanmar webfont in practice, and some fifty further scripts besides. Those faces now measure where they used to throw. Which scripts a face supports remains the font’s and the parser’s to answer, not this package’s.

- **A group header no longer strands above a split.** A split cannot be broken across a page, so a header that introduces one has to keep the whole of it company — but the keep-together test measured a split by its first two lines of text, and a split holds its text in its slots, not in itself. It therefore measured as nothing: the header was drawn at the foot of the page with room reserved for none of what followed, the split moved on to the next page, and the instance’s header repeated above it there. A record card whose total row is a split was drawn twice — once orphaned at a page bottom, once whole. The header now moves with the split, exactly as it already moved with an image.

  Where a split is taller than the page has left, the header behaves as it always has before content no page can hold: it degrades to plain paginated flow and repeats nothing.

0.1.0

### @quario/layout 0.1.0

### Minor Changes

- **The paged display list.** `layout({ page, fonts })` is a render target resolving every page of the report — ops in points from the top-left, one hit box per schema node drawn, the page’s `{ number, total }`, the outline marks, and on an unlicensed render the marking. The algorithm is the PDF target’s typesetter, with its home moved here so a preview can paint the same pages the document has.

- **One measurer.** Base-14 families measure against `@pdf-lib/standard-fonts`’ AFM metrics over WinAnsi, TrueType families against `@pdf-lib/fontkit`’s shaping of the same bytes the PDF embeds, so a preview breaks its lines where the document breaks them.

- **A box survives a page break.** An item or table row no page can hold whole is drawn as slices, and a slice carries the box sides the break left it: the top belongs to the first slice, the bottom to the last, and left and right to every one. A declared border is never stroked through the middle of the content it encloses, and a long paragraph no longer loses the box a short one draws. Each slice reserves the bottom padding it may yet owe, so that edge stays above the bottom margin, and each carries a hit box, so an item that breaks is selectable on every page it reaches.

- **`paint` and `hit`.** A Canvas 2D painter for one page, and the hit-test that maps a point on a page back to the schema node drawn there.

  Text is drawn in the face the document will use. A family you supply is registered from your own bytes and its text drawn as the browser shapes it, which is the shaping the PDF gets from the same file — so ligatures, joined scripts and accents look on screen the way they will on paper. Text in the built-in families is drawn character by character at the widths the layout measured, because the typeface a browser has for them only stands in for the one the PDF writes, and without the correction a line would drift as it ran.

  Preview fidelity inside a run is not promised for a family you supply: the browser and the document agree on the glyphs and their widths, and may differ by a fraction on the kerning between them. Where a line breaks, how wide a column is and where a page ends are the layout’s, and identical.

- **`pageBox`.** The page-size table and its validation, in one home for the PDF target, the viewer and the editor.

- **`PX_PER_POINT`.** How big a point is on screen — 96 dpi over PostScript’s 72 — so every surface that shows a page shows it at the same size, and a zoom is a factor on top of it.

- **A split slot’s box now fills the split’s height.** Before, a slot’s background and border were exactly as tall as that slot’s own content and padding asked for, so any slot shorter than the tallest one drew a box that stopped short of the row, leaving a gap between it and the row below. Now every slot’s box takes the split’s height — the way a table cell’s box already takes its row’s — so a bordered split row draws one straight edge across it whether or not a slot wraps. Only the box stretches: a slot’s text stays at the top of it, and an image’s picture is neither scaled nor moved. A slot that renders nothing holds the full-height box too, as a hidden cell does, and a slot’s hit box follows its painted one. A report that relied on the short box — a slot background used as a chip beside taller content — now draws it full height; give that content a narrower slot of its own.

- **A date string under `format: "date"` now presents as a date.** The text join presents the kind through the engine’s `format()` helper, which now revives the two read forms. See the `quario` changelog for the forms.

- **A nested group’s footer keeps the page columns.** Inside a page-column region, the first footer of a group nested in it ended the region: the strips collapsed and every band after it was laid out across the page, as if `columns` were absent. Only the node that declared `columns` closes its own region now — a nested group’s header and footer are region content, laid out in the strips.

## @quario/mcp

0.2.1

### @quario/mcp 0.2.1

### Patch Changes

- 27cabd1: **The server can be listed in the official MCP Registry.** The package declares its registry name, `com.getquario/mcp`. The registry checks that name in the published package before it accepts a listing. Nothing changes in how the server runs.

0.2.0

### @quario/mcp 0.2.0

### Minor Changes

- 3de746f: **Render data can name an image by path.** `{ "$image": "assets/logo.png" }` reads a PNG or JPEG from inside the data root and decodes it to bytes before the render, as `{ "$base64": … }` does. An agent no longer pastes a picture as base64 into every call. The path resolves against the data root, also inside a `dataPath` file, and the server checks it after it resolves symlinks. The server reads no file that is not a PNG or JPEG, and it reads each file once per call. Without a data root, `$image` is refused. A refusal names its place in the data, such as `$.input.logo`.

0.1.0

### @quario/mcp 0.1.0

### Minor Changes

- **An MCP server that exposes quario as two tools.** `npx @quario/mcp` starts a local stdio server for an agent client such as Claude Code or Claude Desktop. `validate_report` checks a report definition and returns its problems and warnings, each at the path it sits at, and can check the definition’s `required` declarations against named targets. `render_report` renders a definition to html, pdf, xlsx, docx or csv and writes the output to a file, returning its path rather than its content.

  The launch configuration is the host: the license key, the presentation defaults, the `href` schemes, the query budgets, the data root and the output directory are environment variables, and no tool argument can reach them. A call may still set `locale`, `currency`, `timeZone` and the target’s own options. Render data arrives inline or as a JSON file inside the data root, and an image arrives as a `{ "$base64": … }` object that decodes to bytes before the render. The server never overwrites a file, and invalid configuration stops it at startup with the message on stderr.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Changelog","item":"https://getquario.com/changelog"}]}
```
