# Changelog

All notable changes to `@contrail/documents` are documented here.

Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.5.0] - 2026-10-10

Also shipped on the 1.x line as 1.22.0.

### Added

- `clientCreatedOn` on `WriteTransactionRequest`, `DocumentTransactionPayload`, and
  `DocumentTransactionMessage`: when the writing client created the transaction (ISO 8601, client
  clock). The server stores it with the transaction and passes it through in the broadcast, so a
  receiver can measure how long the edit took to reach it.

## [2.4.0] - 2026-09-26

Also shipped on the 1.x line as 1.21.0.

### Added

- `DocumentContainerService.buildIndex` and `DocumentContainerIndex` — which containers hold which
  elements, for one document, resolved once and read back by lookup. A container is a frame or a
  section; membership is geometry, draw order and kind, with nothing inherited and nothing stored.
  The rule is written out in `src/containers/DOCUMENT_CONTAINERS_SPEC.md`, which the code is
  measured against.
- `DocumentContainerUtil` — the container geometry as statics. `anchorFor` answers "which point is
  this element placed by" for a caller holding an element rather than a measured box, and
  `resizeKeepingAnchor` returns the size a new value needs with the position that keeps that point
  still. Applying one without the other drags the corner membership reads.
- `SECTION_ELEMENT_TYPE` and `FRAME_ELEMENT_TYPE` in `document-element-constants`, alongside
  `SHAPE_ELEMENT_TYPES`, which moved here for the board's shape handling.
- `DocumentElementFactory.getBoundingClientRectScaled` — the space an element covers on the canvas,
  with `scale` applied. `getBoundingClientRect` still returns the unscaled box, which is what layout
  works in: `@contrail/document-table` places table members from it, and scaling it moves a scaled
  member into a different cell. Anything deciding what an element overlaps or sits on wants the
  scaled one.

### Removed

- `DocumentContainerIndex.containerPathAtPoint`. It applied the anchor rule alone, because a bare
  point has nothing to compare against a container, and returned the same shape as `containerIdsOf`,
  so it read as a second way to ask one question. `containerPathFor(element, kind)` replaces it and
  applies every rule. Callers that hold a position pass the element with that position on it. This
  is the one removal a consumer has to act on.
- `ContainerKind.layer`, `DocumentContainer.name` and `DocumentContainer.depth`, and
  `DocumentContainerIndex.maxDepth`. The container API has not shipped in a released version, so
  nothing outside this repository can be holding them. Read a container's name from
  `documentElement.name`.

### Deprecated

- `isContainerBinding()` in favour of `DynamicTextUtil.takesValueFromContainer()`. Same rule, named
  for what it decides. The old name still works and delegates to the new one.

### Fixed

- A dynamic text is anchored at its drawn top-left corner rather than its centre. Its size comes
  from the value it resolves to and that value comes from its container, so an anchor that moved
  with the size let the two choose each other: near an edge they never settled, flipping between a
  value and "Move to a frame" on every read. The corner is the rotated one, so a text turned on its
  side anchors where it appears (VIBE-9567).
- `isDynamicText` requires a `propertyBindings.text` binding, the one the value is read from. The
  text tool creates an element with empty bindings and empty metadata, both truthy, so an
  unconfigured text was anchored at its corner while the canvas centred it.
- An element covering a container is not inside it, whatever its type. The rule it replaces looked
  only at shapes wider and taller than the container, so a background image joined the frame it
  covered.
- A line or callout is measured from the elements it connects even with no stored `position`, where
  it was previously left out of the index entirely.
- `min` and `max` rendered an infinity sign when every member value was empty; both fall through to
  `n/a`. `calculateSize` no longer throws on an element that reaches it without a `size`, which on
  the server failed the read for the whole document.

- `min` and `max` over a date property rendered nothing at all. `Number` of a date string is `NaN`,
  so every member was dropped; they now order by the instant each date names.
- `uniqueArray` sorted the rendered text rather than the value, so `11/20/2026` came after
  `01/15/2027` and `100` before `9`. It now orders by value, falling back to text order for values
  that are neither a date nor a number.

### Changed

- `anchorFor` and the index measure through the same factory call, so a caller holding an element
  gets the answer the index would give it. Reading `size` directly disagreed for the types whose box
  is not position plus size.
- A container path is a containment chain, walked outward from the container the element sits on, so
  two containers that merely cross no longer both claim it.
- `buildIndex` scans element types before measuring, so a document with no containers of the kinds
  asked for returns an empty index without a geometry pass.
- **A section nests inside another only when it is drawn above it.** A section drawn over one it
  encloses hides it, so the hidden section holds nothing and a `section.count` on it reads zero.
  This changes existing boards wherever the outer section was drawn last.
- `containerIdsOf` and `memberIdsOf` return copies. `readonly` is erased at runtime, and a caller
  sorting what it got back reordered the index's own list for every later read.
- A member list is a set of ids, so two elements sharing an id count once.
- `min` and `max` no longer read an empty member as zero, and `uniqueArray` no longer emits one as
  a blank entry. A text box narrower than its own padding measures without a fixed width.
- A nullish entry in the element list is dropped rather than throwing.

## [2.3.0] - 2026-09-21

### Added

- `isValidSortIndex(sortIndex)` — whether a value is a key `fractional-indexing` could have produced, so every client writing a sort index is held to one definition of valid rather than each caller inventing its own. `fractional-indexing` keeps its `validateOrderKey` private, so the check asks the generator to mint a key after the candidate: that path validates its bound first and cannot fail for any other reason. The generator's own grammar check ignores the digit alphabet, and accepts `a0!` or `a0 `, so the candidate is screened against the exported `BASE_62_DIGITS` first. Costs ~130ns for a short key and ~350ns for a 387-character one.

### Changed

- `allElementsHaveSortIndex` now requires each element's key to be valid, not merely a non-empty string. Callers that treat an unkeyed collection as needing fresh keys — board snapshot restore is the one today — now also re-key a collection whose keys are present but malformed, instead of restoring ordering that cannot sort.
- `SortOrderErrorMessage.SORT_INDEX_NOT_USABLE` describes the real rule ("sortIndex must be a valid fractional index") rather than "must be a non-empty string". The `SORT_INDEX_NOT_USABLE` code is unchanged.

## [2.2.1] - 2026-09-15

### Changed

- The DESIGN-150 annotation-row collapse is reverted, to go out in a later release alongside the rest of the change. `calculateAnnotationHeight` returns its flat `fontSize + ANNOTATION_PADDING_Y` minimum again whether or not the row draws anything, and `getComponentSize` reserves a flat 18px row in every item card's height.

### Removed

- `ItemComponentService.isAnnotationRowEmpty` and `ItemComponentService.getAnnotationRowHeight`, both added in 2.2.0, and the optional `annotations` parameter they were reached through on `DocumentElementFactory.getBoundingClientRect` and `getSize`. Removed under a patch because 2.2.0 published hours earlier and nothing had consumed them: the canvas renderer mirrors the predicate privately rather than importing it, and every `contrail-apps` app pins this package on its 1.x range.

## [2.2.0] - 2026-09-15

### Added

- `ItemComponentService.isAnnotationRowEmpty(annotationElement, annotations)` — whether an item card's annotation row will render nothing, because the row is hidden or the item has no `category === 'property'` annotations. The single predicate behind the reservation, so the layout service and the canvas renderers cannot drift apart on the question. A missing row element is not treated as hidden: renderers supply a default row for item components that have none stored, so only the annotation data decides there.
- `ItemComponentService.getAnnotationRowHeight(annotationElement, annotations, width)` — the row's reserved height, wrapping onto further rows when the badges do not fit `width`, and `0` when the row is empty. Every creation path must size the row through here so a card laid out by the Item Chooser and one laid out by board generation agree.

### Fixed

- `updateSizeAndPosition` no longer reserves space above the thumbnail for an annotation row that renders nothing. `calculateAnnotationHeight` returned a flat `fontSize + ANNOTATION_PADDING_Y` minimum even when the row was hidden or the item had no annotations, pushing the thumbnail down ~8px on every such card (DESIGN-150). It now returns `0` in those cases and the thumbnail sits at the card origin. Cards whose row does show a badge are unchanged.

## [1.19.0] - 2026-09-11

### Added

- `MaskDefinition` and `MaskShapeKey` on `DocumentElement` — `maskDefinition` records that an image is masked into one of nine preset shapes, with an optional clockwise `rotation`. Its presence changes how `cropDefinition`'s percents are read: element-space (the mask window within the element) instead of the source-image crop they otherwise mean. Anything deriving image geometry must check `hasShapeMask` before treating a crop as a source crop — reading one as the other samples the wrong region of the bitmap.
- `@contrail/documents/util/mask` — the shared mask model, exported from the package root: the nine shapes as renderer-agnostic cubic path segments (`getMaskShapeSegments`), `rotatedRectAABB` for the bounds a tilted shape actually occupies, `rotatePoint`, and the mask-rect/percent conversions, so exporters and the canvas cannot drift apart. Segments are cubic-only because cubics survive affine maps exactly, letting each consumer render them however it draws — `Path2D` in a browser, an SVG composite in a service.
- `getMaskBakeGeometry`, `getMaskBakeLayout` and `maskShapeSegmentsToSvgPath` — the bake inputs a consumer needs to flatten a mask into a bitmap, and an SVG serializer for renderers that go through SVG rather than a canvas path. The layout scales to the source bitmap's own resolution rather than the element's on-board size, so a shrunk image does not bake at reduced detail, and re-bases a rotated mask onto its own bounding box. Only the rasterizer stays per-consumer: `Path2D` in a browser, an SVG composite in a service.
- `hasShapeMask` is a type guard and the complete discriminator: it requires a shape key the renderer knows and all four crop percents, so an origin of `0` passes while a missing origin (previously multiplied into `NaN` by `cropDefinitionToMaskRect`) and an unknown shape (previously thrown on by `getMaskShapeSegments`) are rejected up front. The true branch narrows the percents to numbers.

## [1.18.0] - 2026-09-10

### Added

- `DocumentChange.gestureId?: string` — optional marker a producer sets to correlate the
  actions of one user gesture across a batch.

## [1.17.0] - 2026-09-09

### Added

- `DocumentChange.skipUndo?: boolean` — optional marker a producer sets on a forward
  change definition to keep the action out of undo history.

## [1.16.0] - 2026-08-28

### Added

- `ASSET_VIEW_TEMPLATE` and its `asset-view` entry in `ComponentRegistry` — the component definition for an asset card. Image-forward: one image bound to the asset's viewable, one text row bound to `asset.fileName` — the asset's seeded property, present in both client- and server-side hydration (a bare `asset.name` is frequently unset and the API's document rebind would blank the label). Consumers resolve it by slug through `DocumentElementFactory.createComponent('asset-view', …)`.

### Fixed

- `DocumentElementPropertyBindingHandler` only unwrapped `viewable.content` when it was an array, which is the item shape. An asset's `content` is a single object, so asset viewables never resolved `alternateUrls.originalFile`, and an SVG asset's high-resolution url fell back to a raster derivative instead of its source file. Both shapes are now handled; item viewables still select the record matching `primaryViewableId`.

## [1.15.1] - 2026-08-28

### Added

- `DYNAMIC_TEXT_DATA_SOURCE.SECTION`, the `DynamicTextContainer` type, `MOVE_TO_SECTION_TEXT`, and `isContainerBinding()`. `DynamicTextUtil.getDynamicTextValue` now evaluates `section.*` bindings (`name`, `count`, `itemFamilyCount`, `itemOptionCount`, `colorCount`) the same way it already evaluates `frame.*`.

### Changed

- `getDynamicTextValue`'s fourth parameter is typed `DynamicTextContainer | null` rather than `{ name, id }`. This is source-compatible: `source` is optional and a container without one is treated as a frame, so existing callers need no change. Which kind of container is rendered is read from the binding (`frame.count` vs `section.count`); `source` records what the caller resolved. The parameter is now nullable to match what it always accepted, since callers pass `null` for an element in no container.

### Removed

- A branch that cleared `property` for a container binding while handling a binding that names no container, which could never run.

## [1.15.0] - 2026-08-21

### Added

- `sortElementsByArrayOrder(elements, sortOrder)` and `assignSortIndexes(orderedElements)` — the two ordering transforms that convert, restore and the document read path had each grown their own copy of. `sortElementsByArrayOrder` puts elements missing from the array last, keeping their relative order.
- `SORT_MODE` — the named constants for `DocumentSortMode`. `DocumentSortMode` and `DOCUMENT_SORT_MODES` are now derived from it, so the mode values are declared in exactly one place. `DOCUMENT_SORT_MODES` is now `readonly DocumentSortMode[]` rather than a literal tuple; it stays exported because it is the runtime list behind the entity's `@IsIn` validation.

## [1.14.0] - 2026-07-29

### Added

- `InlineDocumentTransaction` and `S3DocumentTransaction` — the two named variants of `DocumentTransaction`, mirroring `InlineWriteTransactionRequest` / `S3WriteTransactionRequest`. Use these when you need a single variant, or something to extend.

### Changed

- `DocumentTransaction` is now a union of inline `operations` or an `operationsDownloadUrl`, matching the shape `DocumentTransactionMessage` already uses for WebSocket broadcasts. A transaction whose operations overflowed to S3 is read back as a presigned download URL rather than having its operations inlined, so a page of transactions cannot exceed the response size limit. Readers that assumed `operations` was always present must handle both variants, and anything that did `extends DocumentTransaction` should now extend `InlineDocumentTransaction` or `S3DocumentTransaction`.

## [1.13.0] - 2026-07-27

### Added

- `SortOrderErrorCode.SORT_MODE_MISMATCH` / `SortOrderErrorMessage.SORT_MODE_MISMATCH` — returned when an ordering write targets a representation the document isn't using (a `sortIndex` write to an `array`-mode document, or a sort-order-array write to an `elementIndexing` document). The response's `currentSortMode` says which representation the document is on, so a client can switch and retry.
- `SortOrderErrorCode.SORT_INDEX_NOT_USABLE` / `SortOrderErrorMessage.SORT_INDEX_NOT_USABLE` — returned when a `sortIndex` write to an `elementIndexing` document carries a value that is not a non-empty string. Such a value would leave the document claiming `elementIndexing` while the read path silently fell back to sort-order-array order.
- `DOCUMENT_SORT_MODES` — the sort modes as a runtime array, for validators that need to check a value against the same source as the `DocumentSortMode` type.

## [1.12.0] - 2026-07-21

### Added

- Reliable sort-order surface (PLAT-1270): `Document.sortMode` (`'array' | 'elementIndexing' | 'migrating'`, `undefined` = `array`; `'migrating'` is the transient mid-convert state where ordering edits are rejected); `DocumentElement.sortIndex` (fractional-indexing key).
- Sort utilities: `buildKeyBetween` / `buildNKeysBetween` (generate index key(s) between two neighbours), `compareSortIndex` (`(sortIndex, id)` comparator), and `allElementsHaveSortIndex`.
- `SortOrderErrorCode` / `SortOrderErrorMessage` — structured code for the mid-convert freeze (`SORT_MIGRATION_IN_PROGRESS`).

## [1.11.0] - 2026-06-29

### Added

- `WriteTransactionRequest` now accepts either inline `operations` or an `operationsS3Key` referencing a client-uploaded payload, supporting transactions larger than the request-body limit.
- `TransactionUploadTarget` type (`s3Key`, `presignedUploadUrl`, `expiresAt`) returned by the transaction upload-target endpoint.

## [1.10.0] - 2026-06-26

### Added

- `DocumentTransactionPayload` — a shared either-or contract (`operations` XOR `operationsS3Key`) so client and server agree on whether a transaction's operations are inline or overflowed to S3.

## [1.9.0] - 2026-06-26

### Added

- Document transaction contract types for the transactional write path: `DocumentOperation`, `DocumentElementPatch`, `WriteTransactionRequest`, and `WriteTransactionResponse`.
- `DocumentWriteMode` (`'transactional' | 'legacy'`) and an optional `writeMode` field on the `Document` interface, used to gate the transactional write path. Both `undefined` and `'legacy'` mean the legacy path, so existing documents need no backfill; `'transactional'` opts a document in.
- `DocumentTransaction` and `GetDocumentTransactionsResponse` types for reading committed transactions back (client catch-up and gap repair).
- `DocumentTransactionMessage` — the WebSocket broadcast shape for a committed transaction (carries `sequenceNumber`, `clientTransactionId`, and inline `operations` or an `operationsDownloadUrl`).
- Optional `lastSequenceNumber` field on the `Document` interface, recording the sequence number of the latest WAL transaction persisted into the document.

## [1.5.6] - 2026-04-29

### Fixed

- `DynamicTextUtil.getItemCount` now filters out component models missing both `item` and `color` to avoid errors when underlying models fail to fetch.

## [1.5.3] - 2026-04-22

### Added

- Thumbnail-only mode for color cards.

## [1.4.2] - 2026-03-31

- Publish new patch to include 1.3.29 changes and 1.4.0 minor update.

## [1.3.29] - 2026-03-30

- Added `imageSize` and `imagePosition` to the `DocumentElement` interface to support the canvas thumbnail crop feature.

## [1.4.0] - 2026-03-31

### Added

- Structured logging via `@contrail/telemetry`. Requires Node.js 20+.

## [1.3.25] - (initial changelog entry)

- Initial changelog. Prior releases did not include a changelog.
  See git history for changes predating this entry.
