typograph-editor-documentation
    Preparing search index...

    Release Notes - Version 1.0.0-20260929

    A book gets a cover, the pages around it stop changing on their own, and a designer can finally say what a customer may change.

    A cover wrap — back cover, spine and front cover as one printed sheet — can now be made from the document's own last and first page. Building it touched how pages are counted, grouped and paired, and three faults came to light there: turning facing pages off and on multiplied every frame lying across the spine, a document forgot that its author had turned facing pages off, and a block of pages kept together showed as loose pages in the page bar.

    Edit permissions — which elements a customer may retype, restyle, move or delete in the viewer — can now be authored, as roles applied to elements, and checked with Preview as viewer. Until now every element fell back to one fixed default, because nothing could say otherwise.

    • Page ▸ Create cover. Builds the cover from the document's last page (the back cover) and its first page (the front cover), with a spine between them, and adds it to the document as one spread. One cover per document; the menu item is greyed out once there is one.

      ⚠️ The two pages are copied, not moved. The body of the book keeps its own pages and its own numbering: page 1 is still page 1, and no page changes which side of the binding it falls on. The cost is that the cover does not follow later edits to those two pages — it is seeded from them once. Change the front page after making the cover and the cover keeps the old artwork.

      ⚠️ A cover is not pages. It carries no page numbers, does not count towards the page total, and page reordering leaves it alone — it is a fixed block, shown as one row in the page bar.

      This version makes a perfect-bound cover: 3 mm bleed on all four outer edges and none at the two folds, which is what the print path already does for pages that meet. No hinge, no board wrap.

      Undo removes the cover in one step.

    • The spine is set in the page panel. Select the cover and the page panel gains a Cover section with one field: Spine, in millimetres, 5 mm to start with. Changing it widens or narrows the spine and moves the whole front cover — everything on it — across by the same amount; the back cover stays where it is. Anything centred on the spine, such as a spine title, stays centred.

      ⚠️ The spine is your number, not a calculation. How thick a book is depends on the paper and the page count, and the document knows neither. So the spine does not change when pages are added or removed: set it again when the page count is final. A spine that resized itself would be a cover that no longer fitted the book it was made for.

      ⚠️ Split spread is not available on a cover. Its three parts are one printed sheet; split, they would be three orphans.

    • A document remembers whether its pages were shown as facing pairs. Turn facing pages off, save, and the document reopens with them off. It used to reopen paired every time — not because the setting was forgotten, but because opening a document undid it.

      ⚠️ A document saved before this release says nothing about facing pages, and opens paired exactly as it always has. The setting is written only once someone actually chooses it.

    • Roles: decide what a customer may do with each element. Select an element and the parameter bar gains a Role panel: five built-in roles — Fixed, Fill in the text, Place a picture, Fill in and place, Full edit — the document's own roles, and Custom. It says in plain words what a customer may and may not do with that element. New role… builds one as questions — may the customer retype the words? resize the frame? delete it? — and Save to my library keeps it for other templates. Edit a role and every element using it follows; the editor says how many first.

      ⚠️ Built-in roles never change meaning once shipped, so they open read-only — duplicate one to make your own. Deleting a role never changes what a customer may do: its elements keep what it granted.

    • View ▸ Preview as viewer. The editor always lets you do everything, so permissions could only be set blind. Preview switches to the viewer's own rules until you click Back to editing — and ⚠️ anything you change while previewing is undone when you leave.

      How roles, the library and preview work, and what they do not do yet (limits on resize and move are next): docs/EditPermissions.md.

    • Turning facing pages off and on no longer multiplies frames across the spine. A frame lying across the spine was copied onto both pages when they were separated, and nothing recognised the two copies as one when they were put back together — so every off/on cycle doubled it. Measured: 9 elements became 51 after three cycles, one frame becoming 32. Saved documents grew the same way.

      ⚠️ One rule to know: while facing pages are hidden, a frame across the spine is shown on both pages. When they are joined again, the copy on the left-hand page is kept. An edit made to the right-hand copy while they were apart is discarded.

      ⚠️ A document already saved with duplicated frames can still carry them. The duplicates lie exactly on top of each other, so they can be selected and deleted.

    • A block of pages kept together is one row in the page bar. With Keep pages together on and facing pages hidden, the block showed as separate pages numbered like the pages around it, so it read as part of the run rather than as one unit. It now shows as one row, labelled with its range (1 - 2), and drags as a whole — which is what it always was in the document.

    • Documents converted from InDesign have clean measurements. A converted A4 page arrived 209.99999999993608 × 296.99999999946107 mm wide, and every frame, margin and bleed on it carried the same tail of digits nobody typed. The converter now rounds every millimetre measurement to a thousandth of a millimetre: the page is 210 × 297, and the X and Y fields show what the designer set.

      ⚠️ This is in the conversion: documents already converted keep their long numbers until they are converted again. Point sizes, angles, opacity and tracking are not rounded — three decimals would change values somebody actually chose.

    Everything a host shows applies the moment it loads the new bundle. The cover, the facing-pages setting and the page bar all work on documents exactly as they are.

    ⚠️ Older templates are safe. They carry a block of permissions nobody chose — an earlier editor wrote "everything allowed" onto every element it saved. Those are still ignored: only permissions saved by this release count, so no existing template gains the ability to delete or resize its artwork in the viewer (TYP-760).

    • document.create_cover(spine_mm = 5, bleed_mm = 3) builds one container, cover: true, with three records of unequal width named Back, Spine and Front — records_authoritative, the shape that flag exists for. get_cover(), get_spine_width() and set_spine_width(mm) complete it.

    • ⚠️ A cover sits outside the page count, and three places had to learn that: logical_pages() skips it, page_side()'s number walk skips it, and repaginate() steps over it without advancing its counter. A cover that moved the parity would flip the side of every page after it — and with it every inside/outside anchor (TYP-731), side-relative alignment (TYP-732) and page-number field (TYP-643). That is the whole reason the pages are copied rather than consumed. Measured on a four-page document: sides 1r 2l 3r 4l before and after.

    • ⚠️ The spine width is stored once: it is the middle record's own width. There is no second field to disagree with it — the reasoning of TYP-737, which stopped caching the rotation centre.

    • ⚠️ Print needs nothing. TcPdfGenerator::innerEdges() already suppresses bleed on an edge that abuts another record, so the two folds get none and the outer edges get all of it. That holds only because the spine is a real record; a spine expressed as a gap would have bled into the fold.

    • ⚠️ set_spine_width() decides which page an element is on with record_index_for_element() — its centre — asked before the records change. The first version moved an element when its left edge was on the front cover, which left behind exactly what a cover is made of: a background starting in the bleed, a shape hanging over the spine. On a real brochure 10 of 13 front elements moved and the 306 mm background did not, which reads as "the front did not move at all". Front moves by the full change, spine by half, back not at all.

    • ⚠️ create_cover() records its own undo step, {funct:'structure'}, like every other change to the page array (apply_facing_structure, move_logical_page, delete_logical_page). Do not wrap it in set_undo()/set_redo(): those snapshot the active page, and the cover is a spread that did not exist yet — so undo restored the page you were on and the cover stayed. The editor did exactly that until this release.

    • The copied artwork gets fresh uuids through regenerate_element_uuids(); two elements sharing an identity is what TYP-756 was about.

    • The viewer shows a cover but cannot create one. Creating a container is governed by nothing in edit_options, which is per element, so can() has no question to answer — and the default would be "yes, always" in the one product whose point is filling a template in without restructuring it. How a host grants it is still open.

    • ⚠️ The flag moved from the page bar to the document: facing_pages: boolean|null on the template. It lived on the page bar, which setup() builds and a host may not have at all — so set_show_facing_pages() returned silently without one, and a viewer round trip would have re-paired a document its author had unpaired. The no-page-bar early return is gone, so the toggle also works headlessly.

    • ⚠️ null means never stated, and stays absent on the wire. Writing a value nobody chose would turn every document that passes through a save into one claiming a decision.

    • ⚠️ The load still repaginates when the flag is null. A document of all single-page containers looks exactly like one written before facing structure existed, so deriving the setting would either stop upgrading old documents or re-pair deliberately unpaired ones. A stated flag takes normalize_facing_structure() instead.

    • Splitting is not the fault. A straddling frame cannot belong to one of two separate single-page spreads, so each page keeps a copy — one element, one uuid, shown on two pages. The shared uuid is the design: it is what lets the join recognise the pair.

    • ⚠️ The collapse is in absorb_page(), not merge(). repaginate() reassembles pairs with extract_record() + absorb_page(); merge() is the manual merge-spreads path. The guard went into merge() first on the strength of its name, and the repro suite said the count was still doubling. Both carry it now. The twin already in the target wins.

    • ⚠️ extract_record() moves rather than copies, on purpose, and a test pins it. Its pages can end up far apart, where two copies of a frame would never meet again to be collapsed. Making it copy "for consistency" reintroduces the unbounded growth.

    • ⚠️ copy_properties_to() assigned over_flow by reference, so a copy and its source shared one chain object and rewriting either rewrote both. Copied by value now, which every caller assumed.

    • window.run_facing_toggle_tests() from index.html — it needs a page bar, so it does not run headlessly.

    • The classifier was already right: container_kind() returns 'whole' for islands, spreads of more than two pages and vertical spreads. The row builder's facing-off branch ignored it and emitted one row per record. record_index: null is all the fix needs, because every consumer already reads it as "the whole container" — the range badge, the joined names, the click, and the drag layer.
    • typograph.setup({ …, host: { … } }) is now the one way a host supplies images, fonts, documents, templates and clips, and saves them. The reference is docs/HostContract.md in canvas.

    • The old set_*_callback setters keep working and log a deprecation warning. The editor calls none of them, so it logs nothing new.

    • ⚠️ Canvas's hardcoded localhost:8888 URL rewrites are gone. A host on a local development setup that relied on them to reach its fonts and images now states its asset root through setup({ assets }). The ICC profiles and language files are configurable the same way.

    • Builder/MillimetrePrecision is the last pass of DocumentBuilder::build(), not a rounding at each of the forty-odd places a point becomes a millimetre. Those feed each other, so rounding early only rounds an intermediate, and a new conversion site would bring the noise back unseen.

    • ⚠️ Only millimetres. Classes that hold nothing else (Position, Size, Point, Page, Columns, PageGuide, TextWrapOffset) are rounded whole; PageElement and Spread, which mix in points, degrees and fractions, only by named property. The border and the text stroke are points (TYP-722).

    • loaded_edit_options() keeps a block only when it carries version, a whole number ≥ 1, and discards it otherwise — which is what it did with every block before. toJSON() stamps k_edit_options_version (= 1) on every block it writes, keeping a version already there: it says which rules authored the block, and a re-save does not change that.

    • ⚠️ Why a marker and not simply "restore the load": an earlier library build stamped editor_defaults() — every flag true — onto every element it saved. Keeping those would hand each such element delete, resize, rotate and clip in the viewer: silent, on existing templates, and failing open. Measured: 27 stored blocks, 0 authored — and every stored template is ruled not authored, so the marker is the whole rule. No shape heuristic.

    • ⚠️ Every copy lost its block. copy_properties_to() tested the copy's edit_options, which a fresh copy never has — so undo, redo, duplicate and paste all dropped it. With blocks kept, an undo would silently lift a restriction. Now cloned, so a copy restricted later cannot restrict its source.

    • ⚠️ Placing clip elements would have granted everything. addcontentfrompage() materialised a missing block from editor_defaults() to set extra.clip, and that block now survives a save. It starts from viewer_v1_defaults(), what an unauthored element already gets in the viewer.

    • ⚠️ A missing branch in a present block DENIES — _bool() is false for an undefined branch and can() stops at the first false — so a partial block such as {extra:{clip:true}} would not even be selectable. Setting one flag authors all 27; materialise the rest from viewer_v1_defaults(). The assessment said otherwise until this release (§5c, corrected).

    • extra.clip is consulted nowhere today — only clip_edit is — so the clip flag that now survives a save changes no behaviour yet.

    The reference is docs/EditPermissions.md; the design record is ASSESSMENT-edit_permissions_authoring.md in canvas. The decisions that shape the code:

    • ⚠️ Bound, with the expansion beside it. An element carries role_uuid and keeps its own edit_options as a cache and fallback — the font_uuid-beside-a-family pattern (TYP-733/734). apply_roles() refreshes the expansion at load, beside resolve_page_anchors() and apply_autofit(), and after every role edit. An unresolvable role is inert; validate() reports role_missing.
    • ⚠️ Built-ins are frozen and never stored — reserved ROLE/… uuids in canvas src/roles.ts, like the reserved swatches. A revised meaning is a new built-in.
    • Every role operation is one structure undo step on the document API (get_roles, create_role, update_role, delete_role, apply_role, detach_role, role_usage).
    • ⚠️ Preview as viewer is the real rules: EditorUI mode 'previewing' puts the canvas in 'viewer' mode and subsets EditoMouseManager to kViewerStates, which is exactly what viewer.ts does — both lists now live in viewerstates.ts. Leaving undoes to the undo position preview began at.
    • Three editor traps paid for here: menu_active() defaults to false, so a new menu item is dead until listed there; .dialogcontent sets flex-wrap: wrap; undo and redo replace spread objects, so never hold a Spread across them.
    1. document-schema — 7ab0c32 (cover, TYP-759; facing_pages was already in), 9af47be (version on EditOptions, TYP-760), 3dc0cda (roles and role_uuid, TYP-761). Merged.
    2. indesign-converter ed3d0e8 — the rounded geometry. Merged.
    3. The lock bumps in the three services that read a document — converter-service, publisher-service and document-service — once per schema step: chore/bump-for-20260929 and chore/bump-schema-760 (merged, schema 9af47be), then chore/bump-schema-761 (schema 3dc0cda). ALLOW_EXTRA_ATTRIBUTES=false makes an unmodelled key a hard failure, so without the last one a document with a role fails validation and PDF generation. The other services pin the schema but never read a document.
    4. canvas f0f812b — the cover fixes, TYP-760 and TYP-761. Merged.
    5. editor-ui TYP-761-roles-ui last, pinned to canvas f0f812b, because merging it deploys. It carries TYP-759-cover-ui beneath it, so it ships the whole release. ⚠️ The cover's undo fix and canvas's only work together: the new editor over an older canvas records no undo step for a cover at all.