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_optionsandrole_uuidonPageElementin the canvas package atsrc/page.ts; the roles model issrc/roles.ts; the gate iscan()insrc/editpolicy.ts; the schema isPageElement\EditOptions,RoleandTemplate::roles. Design record:ASSESSMENT-edit_permissions_authoring.mdat 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:
| 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:
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.--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
validate() and anything reading the
saved file see the same thing whether or not they know what a role is.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.apply_roles()), so a
role changed while the file was elsewhere reaches its elements.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.
validate() reports it.Select an element — or several — and the parameter bar shows Role under Locked:
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.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.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.extra.clip_edit); extra.clip is saved and read back but nothing consults it yet.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'] }) |