Two fixes for pages that embed the editor: a document whose images came in without a size can be printed again, and the editor no longer asks for a clips file the page does not have. Toolbar controls explain themselves, and the editor's shortcuts work with Ctrl on Windows and Linux.
⚠️ The editor's ids and classes are renamed — check pages that style or look up editor
elements. Every id and class the editor's stylesheet styles now starts with typograph-:
.menubar is .typograph-menubar, #menulayer is #typograph-menulayer, #toolbar is
#typograph-toolbar. Your page's styles no longer reach the editor's elements and the editor's
no longer reach yours. CSS or scripts aimed at an old name must use the new one — see
Renamed ids and classes for the complete list. The gEditorUI keys
(toolbar.select, contentbar.images, …), the menu ids and #typograph are unchanged.
A new look, and a dark mode. The editor's chrome — menus, toolbar, drawers, panels, dialogs —
has a modern style, and can be dark: gEditorUI.set_theme('dark'), 'light' or 'system'. Only
the chrome changes; the document keeps its own colours. See
Theme in API-EditorUI.
Jump to a spread or page: typograph.show_spread() and typograph.show_page(). A host page
can now bring any spread, or one page of it, into view — exactly as a click in the page bar does,
with typograph_page_shown following. See
Showing a page.
Hide swatches from the palettes. gEditorUI.hide_swatches([...uuids]) keeps those swatches
out of every colour picker; show_swatches() brings them back. Elements that already use one keep
it. See Swatch visibility.
The viewer can let users re-order and delete pages. Two new page-bar options for
new Viewer({ page_bar: { … } }): allow_reorder (drag a page to a new place) and
allow_delete (a delete control on the selected page, which asks before it deletes and is never
offered for the last page). Both are off, so an existing viewer is unchanged. See
Options.
A document can open on a spread. View → Start on a Spread makes page 1 a left-hand page, so
pages pair 1-2, 3-4, 5-6 instead of 1, 2-3, 4-5 — for a brochure whose first two pages are one
spread. Everything that depends on a page's side follows: elements set against the inside or
outside edge, text aligned to the inside or outside, page numbers. View → Start on a Single Page
turns it back; either is one step to undo. A host can do the same with
typograph.set_first_page_left(true). When a document is saved as single pages with "keep pages
together", its opening spread stays one spread.
A document with uploaded or linked fonts prints again. The PDF service turned a document away ("fonts[0].font_family — This value should not be blank") when its fonts were named only by id, name and link — which is how a page that embeds the editor usually lists them, and how every document came back after a save in the editor. The editor now keeps each font's family and style, and fills them in from the font file when they are missing. A document saved before this release is repaired by opening it and saving it again.
Deleting one page of a spread is announced. Removing a single page out of a two-page spread —
from the Page menu, or the viewer's new delete control — sent no typograph_page_deleted; only
deleting a whole spread did. It fires for both now, and says which page went:
container_index and record_index (null for a whole spread). See
Page Action Events.
Tooltips on the toolbar. Hovering a toolbar control shows its name, what it does and, where it has one, its shortcut. The tooltip stays inside the editor's own box when the editor is embedded in a page.
Ctrl works as ⌘ on Windows and Linux. Every shortcut that uses ⌘ on a Mac uses Ctrl elsewhere,
and Ctrl-click does what ⌘-click does: removing an element from a selection on the canvas and in
the layer list, among others. A Ctrl-click on empty canvas or on an element that is not selected
leaves the selection alone; it used to clear it. Dragging with Ctrl held still moves, resizes or
draws a selection box without snapping. The menus, the tooltips and the comment panel show the
shortcut for the platform the editor runs on — Ctrl+Shift+G rather than ⇧⌘G.
One exception: the tab ruler. Its ⌃⌘T is Ctrl+Alt+T off a Mac, because Chrome keeps Ctrl+Shift+T for reopening a closed tab.
A document whose images have no size, or a size from the page, prints again. The PDF service
turned the whole document away ("Extra attributes aren't allowed, "w" is unknown") when an image's
size was saved in a short form it does not accept. That happened when a layout placed with
replace_page_content / replace_spread_content carried an image without a size, and when a page
passed image sizes as {w, h}. The editor now always saves the size in the form the PDF service
expects. A document saved before this release is repaired by opening it and saving it again.
No get_clips.php request when the editor starts. At every start the editor warmed its clips
list and, on a page without a clips list of its own, asked our demo server's php/get_clips.php —
a failed request on every page that embeds the editor without that server, whatever its setup
said. The editor now only warms a list the page provides; without one, clips are looked up when
the Clips tab is opened. legacy_php_fallback: false in setup() now turns that lookup off too,
like every other request to the demo server.
These come from the PDF service, not from the editor, and reach users when it deploys:
"rotate_pages": [{"page": 3, "degrees": 90}], with 90, 180 or 270 degrees. Pages are numbered the way the pages option
numbers them. The page is turned whole — its content, bleed and crop marks together; the document
itself is not changed."add_blank_last_page": true adds one blank page after the last,
the size of the last page and with the same crop marks — for printers that need an even count.fix/manifest-font-family-style (PR #171): tg_manifest.font had no font_family /
font_style, so every reader dropped them and every manifest the canvas wrote lacked them
(document-schema: NotBlank; the generator registers fonts under them). Kept now by all three
readers, and filled from the parsed file when a host leaves them out — family as
add_fonts_from_urls() reports it, style via FontManager.style_for_url(), the variant key the
file has in font_data. A font that did not load stays empty and the publisher refuses. Also:
pagereplace read raw.family, which the wire does not have. Checked with a MyVivenda manifest:
10 font violations → 0.dd32fd3 — TYP-818 (PR #172: page 1 on the left, per-page margins that travel) and
the font fix above (PR #171), on top of b94e7cf: — fix/manifest-image-size-wire (PR #168), TYP-766 chrome colours from
tokens (PR #153), TYP-805 typograph- prefix on the canvas names the editor styles (PR #165),
TYP-795 scripts/platform (PR #162), TYP-819 show_spread() / show_page() (PR #169) and
TYP-816/817 page-bar reorder and delete permissions (PR #170). Each of the three was brought up to date with a merge of
master and squash-merged.feat/TYP-819-show-spread): typograph.show_spread() / show_page() →
document.show_spread(container_index, record_index) → PageBar.show(), which is a row click
(select, then the page bar's own set_active_page()). Refused, with no change, for a missing
spread or page, before setup, during a spine edit, or outside set_visible_page_indexes(). The
page bar object exists whenever the canvas is set up, even hidden, so there is no no-page-bar
path. Browser suite run_show_spread_tests(): 24 passed. Documented in docs/PageSelection.md.feat/TYP-820-hide-swatches): src/swatchvisibility.ts holds the hidden
set with no imports, so the pickers ask it without importing EditorUI; EditorUI.hide_swatches()
/ show_swatches() / get_hidden_swatches() delegate to it. Filtered: SwatchPanel.set_content()
(rows keep their library-index ids, so click mapping is unchanged; the height counts shown rows;
the click-handler loop skips missing rows) and the stop-colour lists of swatchdialog.ts and
gradientdialog.ts. The POD dialog's "Document swatches" overview still lists all — it is a
report, not a picker. Session only, nothing on the wire. Verified in the asset manager: 3 of 22
hidden → 19 rows, the right three missing; shown again → 22; is_changed() and the saved
document unaffected. editor-ui has no unit-test runner.TYP-821-822-pdf-page-options):
PageOptionsInterface beside ConfigInterface (getRotatePages(), getAddBlankLastPage()), asked
by instanceof so the two repos merge in either order; rotation is the page's /Rotate (TCPDF
format Rotate); the blank page has the last page's size, bleed and crop marks and nothing else.
Publisher: Config fields, validate() refuses a bad entry, migration Version20261006150000,
CLI --rotate-pages / --add-blank-last-page. ⚠️ Merge order: document-generator → publisher-service
with its lock bump → deploy publisher. api-client gets the fields only after that deploy — the
publisher rejects unknown keys. Verified with real PDFs from the generate command.feat/TYP-816-817-viewer-page-bar, editor-ui branch of the same
name): canvas setup({page_bar_allow_reorder, page_bar_allow_delete}) and
set_page_bar_reorder() / set_page_bar_delete(); the viewer passes its page_bar.allow_*. In a
viewer the rows were draggable with no drop handler behind them — now only with the option. The
delete control asks inline (no browser dialog) and deletes through PageBar.remove_selected(),
the editor Page menu's path. Browser suite run_viewer_page_bar_tests(): 25 passed. Deleting one page of a spread now also sends
typograph_page_deleted, with container_index / record_index.firstPageLeft; canvas and editor-ui feat/TYP-818-first-page-left):
canvas first_page_left (wire, written only when true), is_left_hand_page(n) as the one parity
rule for repaginate(), page_side() and the page bar; set_first_page_left() is one structure
undo step carrying the setting; keep-together writes the opening spread whole. Editor: View menu
Start_on_a_Spread / Start_on_a_Single_Page (MenuAPI.md). ⚠️ Merge order: document-schema →
lock bumps in converter-, publisher- and document-service → canvas → editor-ui; before the schema a
template with the setting is refused ("firstPageLeft" is unknown — checked). The PDF generator
needs nothing. Browser suite run_first_page_left_tests(): 24 passed.preview/20261006-new-look; canvas PRs #153, #165): the restyle with colour tokens and the dark
palette (data-typograph-theme on <html>), every rule scoped under .typograph, and the
typograph- prefix. docs/CSSNames.md lists the 210 renamed names, generated by comparing every
class and id in stylesheets/style.css and in the editor and canvas sources before and after
(editor master vs this branch; canvas b61ae87 vs 307b8ae). Not prefixed yet: ids used only for
lookups and never styled (parmetersbarcontent, zoomin, undo, …) — Embedding.md said
"every id", corrected. TYP-795's tooltip, written before the prefix, used a bare .is-visible;
now .typograph-is-visible.scripts/platform — is_mac,
has_command, has_mac_control, shortcut_key, shortcut_label. control_defaults'
kCommandkey/kControlkey/kOptionkey/kShiftkey are no longer read by the editor.
src/tooltip.ts: one hover tooltip keyed on data-toolbar-group, mounted in editor_root() and
clamped with layer_limits() like the toolbar popups; destroy() removes it and its listeners.
Its entries were written into the 20261006 note on its branch and moved here when it was folded in.fix/manifest-image-size-wire (merged): tg_manifest.image.toJSON() writes size as
{width, height} from either spelling it holds, and {width: 0, height: 0} when there is none —
document-schema models it as a required Page\Size and deserialises with
ALLOW_EXTRA_ATTRIBUTES=false. The held value keeps its spelling and get_size() still answers
{w, h}. pagereplace.ts filled a missing size in as {w:0, h:0}; it now uses the wire's
spelling. set_manifest_data() and the document loader still TYPE the size as {w, h} — harmless
now, since the output is translated. Checked with the publisher's ManifestValidator: {w, h}
reproduces the two violations, {width, height} and 0 × 0 pass. tests/manifest_image_wire.test.ts.fix/clips-php-at-startup (folded into this branch): ContentBarClips.preload() returns
early without get_clips_callback; the php branch of resolve_urls() asks
host.use_php_fallback('php/get_clips.php') — it was the one php branch that did not — and treats
a failed response as an empty list rather than a JSON parse error. The other direct php calls in
the editor (get_manifest.php, save_page_json.php, get_templates.php, the upload and remove
endpoints) still do not consult the gate; they run only on a user action, and the routing for them
is Sebastiaan's feat/TYP-697-editor-providers.