typograph-editor-documentation
    Preparing search index...

    Typograph Editor — UI Customisation API

    This document describes how to hide or disable parts of the editor UI. It is intended for developers who embed the Typograph editor and want to tailor its interface to their application.


    // The API object is available as a global after the editor bundle loads.
    const ui = window.gEditorUI;

    Hide — the element is removed from view.
    Disable — the element remains visible but is greyed out and cannot be interacted with.

    By default, hiding a top-level section removes it from the layout entirely (display: none) so no empty space is left behind. This is called collapse.

    For fields inside a section — for example, hiding just the X-position field inside the size panel — the default is keepSpace (visibility: hidden), which makes the field invisible while keeping its space so sibling fields stay aligned.

    You can override the default for any key:

    ui.hide('parameterbar.position_size.position');                   // keepSpace (default)
    ui.hide('parameterbar.position_size.position', { collapse: true }); // collapse instead
    ui.hide('parameterbar.shadow'); // collapse (default)
    ui.hide('parameterbar.shadow', { collapse: false }); // keepSpace instead

    Hide a UI element.

    ui.hide(key: string, options?: { collapse?: boolean }): EditorUI
    

    Show a previously hidden element.

    ui.show(key: string): EditorUI
    

    Make a UI element visible but non-interactive (greyed out).

    ui.disable(key: string): EditorUI
    

    Re-enable a previously disabled element.

    ui.enable(key: string): EditorUI
    

    Apply multiple changes with a single UI update at the end. Always use batch() when configuring more than one item at startup.

    ui.batch(fn: () => void): void
    

    All methods except batch() return this and can be chained.


    Provide a list of keywords that the editor user can insert into rich text at the cursor position.

    ui.set_keywords(keywords: string[]): EditorUI
    

    Keywords are sorted alphabetically and stored internally. Call with an empty array to clear all keywords.

    When at least one keyword is set, pressing ⌘K while editing rich text opens a floating keyword popup near the cursor. The popup contains a search field that filters the list as the user types. Clicking a keyword inserts it at the cursor; the popup stays open so multiple keywords can be inserted in a row (press Esc or click outside to close it). When no keywords are set, ⌘K does nothing.

    ui.set_keywords(['city', 'company', 'date', 'first_name', 'last_name']);
    

    The call can be made at any time — before or after the editor is loaded, and can be updated at runtime (for example, when the user switches context and a different keyword set applies).


    Controls how spreads are serialised when the user saves or exports. Two independent settings are provided — one for the Save command, one for the Save As / Export command:

    ui.set_save_mode(mode: 'spreads' | 'single_pages' | 'single_pages_keep_together'): EditorUI
    ui.get_save_mode(): 'spreads' | 'single_pages' | 'single_pages_keep_together'

    ui.set_export_mode(mode: 'spreads' | 'single_pages' | 'single_pages_keep_together'): EditorUI
    ui.get_export_mode(): 'spreads' | 'single_pages' | 'single_pages_keep_together'
    Mode Behaviour
    'spreads' (default) Spreads are saved exactly as authored.
    'single_pages' Every spread is split into single pages in the output.
    'single_pages_keep_together' Spreads are split into single pages except those with "Keep pages together" on (islands) — those stay as multi-page spreads.

    Both settings default to 'spreads', so the default behaviour is unchanged. The split happens only at serialisation time — the live document, its on-screen spreads, and the undo history are not modified.

    // Save keeps spreads; Export splits them into single pages
    ui.set_save_mode('spreads');
    ui.set_export_mode('single_pages');

    // Split into single pages but keep "Keep pages together" spreads intact
    ui.set_export_mode('single_pages_keep_together');

    The call can be made at any time and updated at runtime.


    Three modes: Editing, which is the editor as it has always been, Commenting, in which the canvas is inert and the user leaves anchored comments instead of changing the design, and Viewing, in which the document is read-only and there is no comment surface at all.

    ui.set_available_modes(
    modes: Array<'editing' | 'commenting' | 'viewing'>,
    options?: { render?: boolean },
    ): EditorUI
    ui.get_available_modes(): Array<'editing' | 'commenting' | 'viewing'>

    ui.set_mode(mode: 'editing' | 'commenting' | 'viewing'): EditorUI
    ui.get_mode(): 'editing' | 'commenting' | 'viewing'

    The default is ['editing'], which draws no mode switcher and changes nothing. A host that never calls set_available_modes gets exactly today's editor.

    What the host offers What the user sees
    ['editing'] (default) No switcher. Today's editor.
    ['editing', 'commenting'] A two-button switcher floating over the canvas, above the zoom control.
    ['commenting'] A locked label reading "Commenting" in the same place, with a tooltip explaining the access.
    ['viewing'] A locked label reading "View only". Inert canvas, no authoring bars, no comment rail.

    Pass { render: false } when the host draws its own control — it then owns the chrome and calls set_mode() itself, while the widget still applies the mode. portal-ui does this: mode is the same question as sharing, and it belongs in the bar that answers it. With render left alone the widget draws the control itself, floating over the bottom-right of the canvas above the zoom readout.

    Give a commenter ['commenting'] alone rather than disabling the Editing button: the locked label is what tells them on screen why the tools are gone, and a silently inert toolbar reads as a bug.

    set_mode is refused for a mode the host has not offered. If the current mode is dropped from the available list, the first available mode is adopted.

    Anything other than Editing calls typograph.set_interactive(false) and hides the authoring controls: the toolbar and content-bar keys documented below, plus the right-hand parameter bar and the page rail's add-page tile, which rebuild themselves and are therefore hidden by container.

    Two authoring routes need more than a muted canvas, because neither needs a selection:

    • The menu bar. Page, Arrange and Insert are hidden whole; New / Open / Save / Save as Template / Save as Clip, Cut / Paste / Delete / Step and Repeat, and the Convert to… items go with them. Hiding an item disables its keyboard shortcut too. Copy, Find and Replace, View, Save As and Export stay — the last two are downloads, which view access already grants. A menu the host hid itself through MenuAPI stays hidden when Editing returns.
    • The page rail's thumbnails, which are draggable and reorder the document when dropped. They are made undraggable for as long as the mode lasts, and the rail's own rebuilds are watched so it stays that way.

    It is not a security boundary — it is a user interface. The host still enforces permission on its own server.

    Fires whenever the rail opens, closes, or its thread count changes — enough for a host to keep its own Comments button in step.

    document.addEventListener('typograph_comments_toggled', (e) => {
    e.detail.open; // is the rail on screen
    e.detail.count; // threads it would list, under the current filter
    e.detail.available; // false when there is no comment source, or the mode is 'viewing'
    });

    Fires when the pin toggle flips, for a host drawing its own show-pins control.

    document.addEventListener('typograph_comment_pins_changed', (e) => {
    e.detail.visible; // whether pins are on screen in the current mode
    });
    document.addEventListener('typograph_mode_changed', (e) => {
    console.log(e.detail.mode); // 'editing' | 'commenting' | 'viewing'
    });

    The editor renders comments and reports what the user did. It performs no requests of its own — it holds no URL and no access token, and the host supplies every read and write. That keeps the bundle free of any one backend, so an embedder can wire it to their own.

    ui.set_comment_source(source: comment_source): EditorUI
    ui.set_comments_open(open: boolean): EditorUI
    ui.get_comments_open(): boolean
    ui.toggle_comments(): EditorUI
    ui.set_current_user(user: { id: string, name: string, avatar_url?: string }): EditorUI
    ui.refresh_comments(): EditorUI
    ui.select_comment(id: string): EditorUI

    ui.set_comment_pins_visible(visible: boolean): EditorUI
    ui.get_comment_pins_visible(): boolean
    ui.toggle_comment_pins(): EditorUI

    Nothing is drawn until set_comment_source is called: no pins, and no rail.

    The surface follows Google Drive's commenting, which is the interaction most reviewers already know:

    • A pin per thread carrying the thread starter's avatar, anchored in page millimetres so it holds its place under zoom, pan and page reorder. Replies do not change the face — they add a small count badge, and who replied is in the rail. Hovering a pin highlights its card and the other way round.
    • Pins that land within a pin's width of each other are drawn as one cluster: two faces stacked, plus a +N. Hover spreads the faces, a click opens a picker listing every thread in the group — the only way to reach the one underneath. The cluster is computed on screen positions, so zooming in dissolves it.
    • Pins are hidden while editing, and follow the rail: opening it shows them, closing it hides them again. The ◉ in the rail header is the override in between — hide the markers, keep reading the threads — and set_comment_pins_visible() is the same switch for a host. Commenting always draws the pins and Viewing never does, so get_comment_pins_visible() answers for the live mode rather than for the raw toggle.
    • Clicking a pin or a card navigates to the page the comment sits on, and opens the thread in the rail.
    • A 320px rail of cards, closed until it is asked for: the pins already say a page carries feedback, and the rail costs canvas. It opens on set_comments_open(true), on clicking a pin, on starting a comment, and on a deep link; it closes with the ✕ in its header, with Escape, or on set_comments_open(false).
    • Inside it, the open thread expands and the rest collapse to two lines and a reply count. Open / All in the header filters resolved threads.
    • Clicking the canvas drops the pin immediately and opens a composer. ⌘/Ctrl + Enter sends, Esc discards the draft and its pin, ⌘/Ctrl + Alt + M starts one in the middle of the view.
    • Resolve is the ✓ on the open card; edit and delete are behind its ⋮, and only on the reader's own comments and replies.
    • select_comment(id) given before the first list() resolves is remembered and applied when the threads arrive, so a deep link does not race the network.

    Every method is async and may reject. The widget applies the change locally first and rolls it back if the host rejects it, so a dropped pin appears under the cursor rather than a third of a second later — and disappears again if the write fails.

    interface comment_source {
    list: () => Promise<comment_thread[]>;
    create: (comment: string, position: comment_position) => Promise<comment_thread>;
    update: (id: string, comment: string) => Promise<comment_thread>;
    resolve: (id: string, resolved: boolean) => Promise<void>;
    remove: (id: string) => Promise<void>;
    create_reply: (comment_id: string, comment: string) => Promise<comment_reply>;
    update_reply: (comment_id: string, reply_id: string, comment: string) => Promise<comment_reply>;
    remove_reply: (comment_id: string, reply_id: string) => Promise<void>;
    }

    interface comment_position {
    page_uuid: string; // the Spread uuid the pin is anchored to
    x: number; // native millimetres, not screen pixels
    y: number;
    }

    interface comment_thread {
    id: string;
    user: { id: string, name: string, avatar_url?: string };
    comment: string;
    position: comment_position;
    resolved: boolean;
    created_at: string; // ISO 8601
    updated_at: string;
    replies: comment_reply[];
    }

    interface comment_reply {
    id: string;
    user: { id: string, name: string, avatar_url?: string };
    comment: string;
    created_at: string;
    updated_at: string;
    }

    There is deliberately no list_replies: list returns each thread with its replies already, and a second round trip per thread would buy nothing.

    Positions are native millimetres, so a comment placed at one zoom level lands in the same spot at another. page_uuid is the canvas Spread.uuid — stable across page insert and reorder, where an index is not. A thread whose page_uuid is an empty string is skipped rather than drawn on page one, which is how comments predating anchoring are handled.

    gEditorUI.set_current_user({ id: 'u-17', name: 'Robin de Vries' });

    gEditorUI.set_comment_source({
    list: () => fetch('/api/comments').then(r => r.json()),
    create: (comment, position) => fetch('/api/comments', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ comment, position }),
    }).then(r => r.json()),
    // …update, resolve, remove, create_reply, update_reply, remove_reply
    });

    gEditorUI.set_available_modes(['editing', 'commenting']);

    select_comment(id) brings the thread's page into view and expands its card — for landing a reader on the comment a notification named.

    Event detail
    typograph_comment_created { id }
    typograph_comment_updated { id }
    typograph_comment_resolved { id, resolved }
    typograph_comment_deleted { id }
    typograph_comment_selected { id }id is null when the selection is cleared

    Parent keys target the whole group. Child keys (indented) target individual items within that group.

    Key What it controls
    toolbar.select Selection tool
    toolbar.path Path / pen tool
    toolbar.grab Pan / grab tool
    toolbar.undo_redo Undo and Redo buttons (both together)
      toolbar.undo_redo.undo Undo button only
      toolbar.undo_redo.redo Redo button only
    toolbar.fill_color Fill colour swatch
    toolbar.stroke Stroke palette — icon trigger; opens a popup
      toolbar.stroke.stroke_size Stroke width input
      toolbar.stroke.stroke_pattern Stroke dash pattern selector
      toolbar.stroke.line_cap Line cap buttons
      toolbar.stroke.line_join Line join buttons
    toolbar.opacity_blend Opacity + blend palette — icon trigger; opens a popup
      toolbar.opacity_blend.opacity Opacity control
      toolbar.opacity_blend.blend_mode Blend mode selector
    toolbar.text_format All text formatting controls
      toolbar.font_face Font family selector
        toolbar.font_face.default_font Default font (Source Sans Pro) entry in the font picker
        toolbar.font_face.manage_fonts "Manage fonts…" item at the top of the font picker
      toolbar.font_style Font style selector
      toolbar.font_size Font size input
    toolbar.text_align Horizontal justification (icon popupmenu)
    toolbar.text_valign Vertical alignment (icon popupmenu)
    toolbar.decoration Underline / strikethrough palette — icon trigger; opens a popup
      toolbar.decoration.underline Underline switch + weight / offset / colour
      toolbar.decoration.strikethrough Strikethrough switch + weight / offset / colour
    toolbar.spacing Spacing palette — icon trigger; opens a popup
      toolbar.spacing.line_spacing Line spacing input
      toolbar.spacing.letter_spacing Letter spacing input
    toolbar.columns Columns palette — icon trigger; opens a popup
      toolbar.columns.columns_count Column count icon + input
      toolbar.columns.columns_gutter Gutter icon + input
      toolbar.columns.columns_balanced Balanced / unbalanced toggle buttons
    toolbar.drop_cap Drop-cap palette — icon trigger; opens a popup
      toolbar.drop_cap.drop_cap_lines Drop cap label + lines input
      toolbar.drop_cap.drop_cap_chars Chars label + characters input
      toolbar.drop_cap.drop_cap_size Size label + font size input
    toolbar.hyphenation Hyphenation palette — icon trigger; opens a popup
      toolbar.hyphenation.hyphenation_toggle Hyphenate label + on/off switch
      toolbar.hyphenation.hyphenation_before Before label + min-chars-before-break input
      toolbar.hyphenation.hyphenation_after After label + min-chars-after-break input
      toolbar.hyphenation.hyphenation_limit Limit label + max-consecutive-hyphenated-lines input (0 = unlimited)
    toolbar.spellcheck Spell-check palette — icon trigger; opens a popup
      toolbar.spellcheck.spellcheck_toggle Spell check label + on/off switch
    toolbar.image_controls Image scale, horizontal and vertical alignment

    Popup palettes: several groups (toolbar.stroke, toolbar.opacity_blend, toolbar.decoration, toolbar.spacing, toolbar.columns, toolbar.drop_cap, toolbar.hyphenation, toolbar.spellcheck) render as an icon-only trigger that opens a flyout panel. Hiding/disabling the palette key affects the trigger; the indented …<control> keys target individual controls inside the popup and are applied each time the popup opens.

    Text-edit vs frame toolbar: when editing text (double-click), the toolbar shows stroke + opacity/blend palettes, the justification popupmenu, and the decoration / spacing palettes. When a text frame is selected (not editing), it shows the vertical-alignment popupmenu and the columns, drop-cap, hyphenation and spell-check palettes. toolbar.text_align and toolbar.text_valign are now single icon popupmenus (no per-direction sub-keys).

    Key Tab
    contentbar.pages Pages / templates
    contentbar.text Text snippets
    contentbar.shapes Shapes
    contentbar.images Images
    contentbar.clips Clips
    contentbar.layers Layers

    Shown when the user clicks the canvas background.

    Key Section
    parameterbar.page.name Page name
    parameterbar.page.size Page width and height (both together)
      parameterbar.page.width Page width field only
      parameterbar.page.height Page height field only
    parameterbar.page.type Page type selector
    parameterbar.page.fill_color Page background colour
    parameterbar.page.bleed Entire bleed section
    parameterbar.page.locked Page locked toggle

    Individual bleed fields — default to keepSpace

    Key Field
    parameterbar.page.bleed.top Bleed top
    parameterbar.page.bleed.right Bleed right
    parameterbar.page.bleed.bottom Bleed bottom
    parameterbar.page.bleed.left Bleed left
    parameterbar.page.bleed.link Link / unlink all values button

    Sections

    Key Section
    parameterbar.position_size Entire name / position / size panel
    parameterbar.rotate Rotation
    parameterbar.lock Lock
    parameterbar.corners Corner radius
    parameterbar.shadow Shadow
    parameterbar.textwrap Text-wrap section (entire panel)

    Fields within the size / position section — default to keepSpace

    Key Field(s)
    parameterbar.position_size.name Element name input
    parameterbar.position_size.position X and Y position
    parameterbar.position_size.size Width and Height
    parameterbar.position_size.reference 9-point reference selector

    Text-wrap sub-controls — default to collapse

    Shown on the obstructing element (a shape/image that text flows around). The offset, side and inverse controls only appear once a wrap mode other than none is chosen.

    Key Control
    parameterbar.textwrap.mode The four wrap-mode icons (none / bounding box / jump / shape)
    parameterbar.textwrap.offset Standoff offset input
    parameterbar.textwrap.side Side selector (both / left / right / largest area)
    parameterbar.textwrap.inverse Inverse toggle (flow text inside the shape)

    ui.batch(() => {
    ui.hide('toolbar.path');
    ui.hide('toolbar.grab');
    });
    ui.batch(() => {
    ui.hide('contentbar.pages');
    ui.hide('contentbar.text');
    ui.hide('contentbar.shapes');
    ui.hide('contentbar.clips');
    });
    ui.batch(() => {
    ui.hide('parameterbar.shadow');
    ui.hide('parameterbar.rotate');
    ui.hide('parameterbar.corners');
    });
    ui.hide('parameterbar.position_size.name');
    // X, Y, Width, Height fields remain visible and aligned
    ui.disable('toolbar.blend_mode');
    ui.disable('parameterbar.rotate');
    document.addEventListener('typograph_setup_ready', () => {
    ui.batch(() => {
    ui.hide('toolbar.path');
    ui.hide('toolbar.grab');
    ui.hide('toolbar.blend_mode');
    ui.hide('toolbar.text_baseline');

    ui.hide('contentbar.pages');
    ui.hide('contentbar.text');
    ui.hide('contentbar.shapes');
    ui.hide('contentbar.clips');

    ui.hide('parameterbar.shadow');
    ui.hide('parameterbar.rotate');
    ui.hide('parameterbar.position_size.name');
    });
    });
    // Hide initially, reveal later based on application state
    ui.hide('toolbar.drop_cap');

    // ... later ...
    ui.show('toolbar.drop_cap');
    ui.set_keywords(['city', 'company', 'date', 'first_name', 'last_name']);
    // Press ⌘K while editing text to open the keyword popup
    // Different keyword sets for different document types
    function on_document_type_changed(type: string) {
    if (type === 'invoice')
    ui.set_keywords(['company', 'invoice_date', 'invoice_number', 'total']);
    else if (type === 'letter')
    ui.set_keywords(['city', 'date', 'first_name', 'last_name', 'salutation']);
    else
    ui.set_keywords([]); // disable keyword popup
    }