Three things a host can do that it could not do before, and all three exist for the same reason: a
document that was built somewhere else has to be finished here. A layout arrives as JSON and
replaces what is on a page. A palette arrives abstracted as COLOR_1… and everything in the
document has to be pointed at it. And the fonts a converted document names have to actually resolve,
which until this release they did not.
A page's contents can be replaced with a supplied layout.
typograph.replace_page_content({container_index, record_index}, payload) swaps everything on one
page; typograph.replace_spread_content(container_index, payload) does every page of a spread.
The page keeps what makes it that page — size, margins, bleed, guides — and takes its name from the incoming layout. One undo step puts the whole thing back, the merged colours and styles included.
It answers with a report, never a boolean: how many pages were replaced, which text chains had to be broken, what was merged, what was dropped, and why it refused if it refused.
⚠️ A text chain crossing the replaced page is broken, not repaired — both ends cleared, and every frame it happened to is named in the report. A chain pointing at a frame that no longer exists is worse than one that has been cut: the walk that follows those links stops silently.
⚠️ Two payload shapes. A full bundle — what a saved document or a clip looks like — carries its own colours, styles and fonts and merges them. The spreads part on its own is accepted too, but only when every colour, font and image it names is already in this document's manifest (the canvas's own built-in colours excepted). Anything else is refused outright, not substituted: an unresolved colour would bake black into the text and an unresolved image would draw a placeholder, and neither announces itself.
File ▸ POD settings: point every reference at the colour or font you want to standardise on. A new dialog, after Save as Clip. It lists the colours the document actually uses and the fonts it holds, each with a replace with… picker of the others; OK applies the chosen rows.
The colours are shown as colours and the font names are set in their own faces, because a name is not a colour and a font name in someone else's typeface tells you nothing.
This is for the workflow where a template's palette is abstracted — COLOR_1, FONT_1 — so a
manifest can bind real values at load time. The abstract entries are added first; this is how
everything else in the document gets pointed at them.
⚠️ It repoints references; it never creates an asset. An unknown source or target is refused rather than invented. And the replaced colour or font stays in the library for now: removing it is only safe once the sweep is provably complete, and a missed reference would render black.
The same operation is available to a host directly, as typograph.substitute_swatch(from, to) and
typograph.substitute_font(from, to).
A rotated element no longer jumps when its page moves. Move a page — or replace its contents — and any rotated element on it drew in the wrong place, sometimes half off the paper. Moving or rotating the element snapped it back, resizing it did not, which is exactly the shape of a value that had been left behind: the rotation centre. It travels with the element now.
The tools tab on the right edge has its arrow back. Last release gave that edge a second drawer for comments, and in doing so replaced the tools tab's arrow with an icon — so the tab everyone reaches for looked like a different control. The arrow is back, and the tab is the arrow alone; the comments tab keeps its own mark, which is what tells the two apart.
A converted document's fonts can finally be found by uuid. Every font reference in a freshly converted and imported template resolved to nothing. The text still looked right — the canvas falls back to the font's family name, so the page rendered and nothing was logged — but nothing in the document could be reached by font, which is precisely what a host binding fonts through a manifest needs, and what the POD settings dialog above is for.
The cause: a converted font carries two identifiers, one per file and one per family, and the document is pointed at the family one while the import step re-keyed only the other. The import now carries both, so the document, the manifest and the editor's font table finally name the same thing.
⚠️ The uuids are still minted fresh on every conversion, so converting the same package twice still produces two different sets of font handles. That is filed separately; it only bites something that stores those handles between conversions.
Everything else in this release is the library's public surface and applies the moment a host loads the new bundle.
The page replace is src/pagereplace.ts; the decisions are in PLAN-page_replace.md. Three
traps are worth reading before touching it:
⚠️ Parse order bakes colour. Text colour is resolved into words as they are built, so the incoming layout's swatches are staged in the library for the parse and removed afterwards. A missing swatch does not fail — it silently bakes black.
⚠️ Element coordinates are spread-space, not page-space. A single-page layout dropped on the
right-hand page of a spread lands on the left one unless it is translated by the target page's
origin. move_native() is the accessor that does it, and it carries children, image rects, path
points and the rotation centre along.
⚠️ The structural undo snapshot did not carry assets. It held {pages, active}, so an undo
put the elements back and left the merged swatches, styles and manifest entries behind for good.
It now carries all four.
The substitution is src/substituteasset.ts, and it reaches three references that nothing
reached before. Spread.remap_swatch_uuids() covers elements — fills, strokes, shadows, a
frame's own text colours, story and table runs — and is reused here. It does not reach
document-level styles (where a standardised colour is most likely stated), gradient stops
(Sample.swatch_uuid — nothing has ever remapped them), or the manifest's own swatch
definitions (keyed by uuid, so a stale one re-binds on open). Each of those failed silently.
⚠️ A font reference is a uuid or a family name, and an unresolvable uuid is inert. The font
half of the substitution was wrong twice before this was measured: first a uuid-only sweep, then
family matching for the default sentinel only. What real documents carry is all three cases — a
default sentinel with the real face in font_family, a uuid the font table has never heard of,
and a manifest-bound uuid. Only a uuid that resolves outranks the family, so the rule is: match
by uuid, else by family when the uuid resolves to nothing. And the cached family has to move
with the uuid — changing one and leaving the other is how two earlier font bugs happened.
The dialog's popups are parented to the body while open, not to their row. The columns scroll,
and a scroll container clips its descendants on both axes — a list inside a row was cut off after
one or two entries, and position: fixed does not rescue an element that is still a descendant of
the clipper. Two consequences worth knowing if you touch it: the list has to state the interface
font itself, because 'Source Sans Pro' is declared on .typograph and the body is outside that
subtree; and the outside-click and outside-scroll guards have to ask where the event came from,
or the list closes on its own scrolling.
Each row of the POD dialog is its own undo step today, because each substitution opens and closes the structural bracket itself. Batching the dialog into one step needs a batch entry point on the canvas side; it is noted rather than faked.
⚠️ The rotation-centre fix is one line in move_native(), and the reason it was missed is
instructive: native_rotation_cx/cy are absolute coordinates stored beside the rect, so every
override that moves an element had to move them too, and the base class's own move did not.
Deriving the centre from the rect instead of storing it is filed as a follow-up — that is the
version that cannot be forgotten by the next override.
The tab arrow was never restyled. It was hidden — display: none on #parametersbar_resizer —
which is why un-hiding it brings back exactly the old one: the triangle is inline on the element
and the controller still flips it on open.