typograph-editor-documentation
    Preparing search index...

    Typograph — Edit permissions and roles

    This document describes how a template says what a customer may do with each element — retype it, restyle it, move it, delete it — and how a designer authors that in the editor.

    It exists for the viewer. The editor lets its user do everything, always; the viewer (viewer.js) lets a customer do only what the template permits. Until this release nothing could author those permissions, so every element fell back to one fixed default.

    Source of truth: edit_options and role_uuid on PageElement in the canvas package at src/page.ts; the roles model is src/roles.ts; the gate is can() in src/editpolicy.ts; the schema is PageElement\EditOptions, Role and Template::roles. Design record: ASSESSMENT-edit_permissions_authoring.md at the canvas repo root.


    Every element can carry edit_options: 27 yes/no permissions in eight groups — select, remove, resize (four edges), move (two axes), rotate, text (seven), the picture inside an image frame (eight) and clips (two). The viewer reads them through can(element, action).

    Nobody authors 27 switches per element across forty frames. The unit is a role: a named set of the 27, applied to many elements at once.

    Built-in roles shipped with Typograph, read-only, the same in every document
    The document's own roles built by the designer, stored in the template, editable
    Custom an element with permissions of its own and no role
    Not decided an element with neither — the viewer applies its default
    Role A customer may
    Fixed nothing — not even select it. Brand furniture.
    Fill in the text select, retype, change font, style and size. No geometry, no fit rules.
    Place a picture select, and move, scale and turn the picture inside its frame. Nothing else.
    Fill in and place both of the above — exactly the viewer's default, stated deliberately.
    Full edit everything, for an element the customer genuinely owns.

    ⚠️ Built-ins are frozen — their names and their grants. Once a template says Fill in the text, changing what that grants would change what every existing template permits. A revised meaning ships as a new built-in, and the old one stays for the documents that use it. That is also why a built-in opens read-only in the editor, with Duplicate to customise.

    The names above are the ones a template stores, and they stay English. An editor shown in Dutch (the locale option, see Embedding) shows a built-in under its Dutch name — Fill in the text reads "Tekst invullen" — while the template keeps Fill in the text. A role a designer named keeps that name in every language.

    ⚠️ Fill in and place is not the same as "not decided". They grant the same today, but only the first is a decision somebody made, and only the first stays put if the viewer's default ever changes.

    Since 20261023.1 (TYP-1046) the viewer applies an element's permissions in three places:

    • Selecting. A click on an element that may not be selected (Fixed) selects nothing, not even what lies beneath, and the element does not light up on hover when See it highlighted on hover is off.
    • The gestures. Moving, resizing, rotating, retyping and handling the picture each ask their own permission.
    • The toolbar. A control is offered only when its permission allows it:
    Toolbar control Permission
    font change the font
    size, leading, letter spacing change the size
    font style, alignment, caps, underline, strikethrough, super/subscript, lists, indents change the style
    the frame's text layout: vertical alignment, baseline, columns, drop cap, hyphenation change the style
    Fit text on/off (only on a frame with a Fit text rule, since 20261104.3) adjust the fit
    flip rotate it
    the picture's fit scale the picture
    the picture's horizontal and vertical alignment reposition the picture

    ⚠️ Fill and text colour and spellcheck are always offered. A role has no colour permission (the colour allow-list is not in document-schema yet), and spellcheck changes nothing in the document.

    ⚠️ Tables are not edited in the viewer (since 20261104.3, TYP-1411): the table's rows, columns, cell strokes and cell fill are left out of the viewer's toolbar whatever the role says. The words in a table's cells are retyped like any other text.

    ⚠️ Flip is not offered under the viewer's default (and Fill in and place), which do not allow rotating. Give an element Full edit, or a role of your own that allows rotate, to offer it.

    Since 20261030.1 (TYP-1279) a template can carry personalisation rules: what the customer is asked about a layer (document-schema's personalise block on an element) and how they edit the template (its personalisation, with the mode form, inline or mixed). In the viewer, a template with rules opens only the fields the customer edits on the design:

    • Which open. Every field under inline; the inline and both fields under mixed; none under form, where a form asks everything. A layer with same_as prints another field's answer and never opens. A hidden or locked layer stays shut even when it is a field.
    • Every other layer is locked: no hover highlight, no selection, no text editing; a press on it pans. A group that holds an open field may be selected, to reach the field, but not moved.
    • On an open field the permissions above still decide what the customer may do with it.
    • The mark. An open field has a dashed outline in --typograph-color-primary, solid on hover, with its label above it in a pill (--typograph-color-on-primary text). A host that themes the chrome defines both; without them the outline is #1B6AE3 with white text.

    A template without rules behaves exactly as before, and nothing changes in the editor, where the rules are being authored. The rules travel through open and save, so a save keeps them. Still to come: the editor's mode to author the rules (TYP-1278), and in the viewer a checklist beside the design and a toolbar limited to the rule, the later parts of TYP-1279.


    An element bound to a role carries both the reference and the permissions it currently grants:

    element.role_uuid    = '67c2b287-…';           // the binding — authoritative when it resolves
    element.edit_options = { version: 1, … }; // the expansion — a cache, and the fallback
    • Editing a role changes every element bound to it, on every page — it is a bulk edit, and the editor says how many elements it is about to change before it does.
    • The permissions are always written down, so the viewer, validate() and anything reading the saved file see the same thing whether or not they know what a role is.
    • A role that cannot be found is inert, not fatal. The element's own permissions keep applying, and validate() reports role_missing. This happens when an element arrives from another document — a clip, a replaced page — naming a role this document does not have.
    • When a document opens, every bound element is refreshed from its role (apply_roles()), so a role changed while the file was elsewhere reaches its elements.
    • No partial overrides. An element uses a role or is Custom. Choosing Custom detaches it: it keeps exactly what the role granted and loses the reference. Deleting a role detaches its elements the same way — deleting a role never changes what a customer may do.

    A role — or an element — can also say how far a customer may resize or move it:

    may be resized 80–120% keeping its proportions, and nudged ±12 mm

    Limit In Measured against
    smallest / largest width and height mm or % the element's size as drawn
    keep the proportions — a corner drag stays proportional; an edge handle is refused
    how far it may be nudged, either way mm or % where it was placed (% of its width or height)

    An empty limit does not limit. Limits only apply where the switch allows the action at all: a role that may not move has nothing to limit.

    ⚠️ A percentage is of the size as drawn, and stays that way. When a role with limits is applied, each element remembers its own size and position at that moment — its reference — and every percentage is measured from there, forever. It is never measured from the element's current size: a frame the customer enlarged to 120% and saved would otherwise be allowed 120% of that on the next open, and grow a little further every time.

    • The editor is never limited. Limits are rules for the customer; the designer setting them has to be able to go past them. Use Preview as viewer to feel them.
    • A group's limits are its own. They govern the group as a whole, not what is inside it.
    • A frame that grows with its text stops at its limit. With AutoSize on, a frame grows to fit its text — but not past a largest-height limit. The text then overflows, and validate() reports it.
    • ⚠️ An element that sits against a page edge (Sits against: inside / outside) has its horizontal position set by that rule on every page move, whatever its move limit says.

    Select an element — or several — and the parameter bar shows Role under Locked:

    • the five built-ins, as buttons;
    • this document's own roles, each saying what it grants and how many elements use it, with an edit button;
    • New role…, which starts from what the selection has now and applies the role once saved;
    • From my library, which copies a kept role into this document and applies it;
    • Custom;
    • In the viewer — two plain sentences: what a customer may and may not do with this element. Only the questions its type can answer are asked: a picture frame is not told it may retype.

    A role is edited as questions — May the customer … Select it · Retype the words · Change font, style and size · Change the fit rules · Resize the frame · Move it · Rotate it · Delete it — with the picture, clip and highlight switches one click further. Each question sets its whole group of the 27 at once. Beside them, Within these limits: Resize in % (one range for both width and height) or mm (a width and a height range), Keep the proportions, and Move — "up to ± … from where it was placed". A card is off, and says so, when its switch is off. A group whose switches disagree (a role allowing horizontal moves only, written by a host) shows as mixed and is left alone unless changed.

    Save to my library keeps a role in the designer's own library, in this browser. It is where a role is kept; the document is where it is used. Copying one into a document keeps its identity, so the same role in two templates is recognisably the same role. A library that cannot be read — a private window, storage switched off — is simply empty; no document depends on it.

    In the editor every permission is granted — the designer has to be able to edit an element they have just restricted. So View ▸ Preview as viewer switches the editor to the viewer's rules: the same interactions the viewer has, the same checks, the tools the viewer does not have hidden. What you cannot do there, a customer cannot do either.

    ⚠️ Whatever you change while previewing is undone when you go back. Typing into a frame to check that it may be retyped is testing, not authoring. Back to editing in the banner returns to exactly the state preview began in.


    The document API, on typograph.get_typograph_document(). Every change is one undo step.

    const doc = typograph.get_typograph_document();

    doc.get_roles(); // built-ins first, then the document's own
    doc.get_role(uuid); // one, or null
    doc.create_role(name, edit_options, uuid?, edit_constraints?); // uuid only to keep a library role's identity
    doc.update_role(uuid, { name?, edit_options?, edit_constraints? }); // edit_constraints: null removes the limits
    doc.delete_role(uuid); // → { ok, elements_detached, refused? } — detaches
    doc.apply_role(elements, uuid); // bind
    doc.detach_role(elements); // → Custom, keeping the current permissions
    doc.role_usage(uuid); // → { elements, pages, element_uuids }

    update_role and delete_role refuse a built-in (refused: 'builtin').

    Limits are written as edit_constraints:

    {
    resize: { min_width: { value: 80, unit: 'percent' }, max_width: { value: 120, unit: 'percent' },
    min_height: { value: 80, unit: 'percent' }, max_height: { value: 120, unit: 'percent' },
    lock_aspect_ratio: true },
    move: { horizontal: { value: 12, unit: 'mm' }, vertical: { value: 12, unit: 'mm' } },
    }

    Before the naming convention (TYP-1340) these were resize.aspect_lock and move.dx/move.dy. A stored template, and a role kept in the library, still read with those names; what is written carries the new ones.

    A role's limits never carry a reference: apply_role() captures one on each element.

    A block written by host code must be complete — all eight groups — and is marked with version when the canvas saves it. See the two traps below.


    {
    "roles": [ { "uuid": "67c2…", "name": "Headline text", "edit_options": { "version": 1, "…": "…" } } ],
    "spreads": [ { "elements": [ { "role_uuid": "67c2…", "edit_options": { "version": 1, "…": "…" } } ] } ]
    }
    • roles holds only the document's own roles, and is written only when there is one. Built-ins are never stored.
    • ⚠️ An edit_options block is honoured only when it carries version. Older editors wrote a block granting everything onto every element they saved, without anyone choosing it. Blocks without the marker are ignored, so no existing template gains delete or resize in the viewer. The canvas stamps the marker on every block it saves.
    • ⚠️ A block that is present but missing a group DENIES that group. It does not fall back to the default. Always write all eight.
    • An element's edit_constraints carries its limits and the reference rect {x, y, width, height} (mm) they are measured from; a role's carries the limits only.

    • Limits on an element without a role. Limits are authored on roles; an element set to Custom keeps whatever limits its role gave it, but has no editor of its own for them.
    • A box to stay inside. Limits are measured from where an element was placed; "must stay inside this area of the page" is not offered yet.
    • The viewer cannot author roles, and a viewer user cannot create one.
    • ⚠️ Place a clip is recorded but not enforced. The canvas checks only Edit a placed clip (extra.clip_edit); extra.clip is saved and read back but nothing consults it yet.
    • Roles do not travel with clips or replaced pages. An element arriving from another document keeps its own permissions and reports role_missing until it is given a role here.

    I want to…
    lock an element completely apply Fixed
    let the customer type their own words only apply Fill in the text
    let them swap and frame a photo apply Place a picture
    keep today's viewer behaviour, deliberately apply Fill in and place
    a rule of my own New role…, then Save to my library to reuse it
    let a frame grow or shrink a little a role with Resize limits, e.g. 80–120%
    let a logo be nudged but not moved a role with Move up to ± a few mm
    check what a customer can do View ▸ Preview as viewer
    find elements naming a role this document lacks validate({ checks: ['role_missing'] })