Repository navigation
Editor
Generated from docs/editor.md in the repository. Edit it there; changes made on the wiki are overwritten.
@readmint/editor is the visual editor: a browser app that edits a design file and previews it
with the same renderer the CLI and the Action use, so the preview is the output.
pnpm editor # dev server (Vite) of packages/editor
pnpm --filter @readmint/editor app # static app in packages/editor/dist-app/The app is built from the workspace sources; CI builds it on every change. It has no runtime dependency outside the workspace and no UI framework: the DOM views are small classes that dispatch commands.
-
Canvas: width, height, background (no layer selected).
-
Layers: add a rect, ellipse, text or group (into the selected group, else at the top level); select in the canvas or the layer tree; move by dragging; resize by the corner handles; reorder and remove in the layer tree. A new design is an empty canvas: nothing is predefined.
-
Properties: the fields of the selected layer's type, as the design format defines them. A number field takes a number or a
{{binding}}; a paint takes#rrggbb,none, a binding or a gradient as JSON; a text'scontentmay hold placeholders. An invalid value is refused with the format's issue; the design is unchanged.-
Orbit (design-format.md): a layer shows
center.x,center.y,radius,radiusY,angle,speed,uprightandfade.from/fade.to(written together, or both emptied to remove the fade), with "Add orbit" (a circle of radius 0 at the origin) and "Remove orbit". The format checks the values; an orbit outside an absolute group is refused with its message. -
Let: the layer's
letas rows of name and expression, in evaluation order, with "Move up", "Remove" and "Add binding" (local<n>={{ null }}). Names are at mostLocalNames.maxLengthcharacters and expressions at mostDesignLimits.maxBindingLength, the format's own limits; a list overmaxLetEntriesis refused before it is built and "Add binding" is disabled at the cap. -
Palette (canvas, nothing selected): the modes (name and Remove), then a group per token: its name and Remove, and a
row per mode with a text input and a colour picker, which fits the 280 px panel (a pick keeps the colour's alpha digits). "Add palette", "Add mode" (a copy of the
first mode), "Add token" (black in every mode), renaming and removing keep every mode on the same tokens; removing
the last mode removes the palette. Over
maxPaletteModes/maxPaletteTokensthe add buttons are disabled and an edit is refused. Removing a token a binding still reads is refused by the format. Each change is one undoable edit; editing these keeps the layer'sanimateas it is. -
Timeline (the timeline panel): the canvas shows "Add timeline" (3000 ms, looping) when the design has none, else
its duration (1..60000 ms), a loop checkbox and "Remove timeline" (refused by the format while a layer animates).
A selected layer shows Animation: one group per track it has, in the order of the format's table
(
animatedProperties:opacity,x,y,scale,rotationfor every layer, plusfillandstrokeon a rect or ellipse,fillon text, andfill,strokeanddrawon a path), with the track'sduration,delay,stagger(a number or a{{binding}}),cyclesandeasing, and a keyframe table: per row the offset, the value (a number, a colour or a{{binding}}), an easing (inherit, the five keywords, orcubic-bezierwith its four numbers), "Add after" (the new keyframe takes the offset midway to the next row and copies the value) and "Remove". A track withcyclesis listed once withcyclesas a number, never unrolled. "Add track" lists the properties the layer has no track for, with two keyframes that keep the frame as it is:opacity,fillandstrokestart from the layer's own value (a literal or a{{binding}}; afillorstroketrack is refused with a note when the layer has no colour to start from),x,yandrotation(offsets on top of the layer) from 0,scalefrom 1 anddrawfrom the full stroke. It is disabled, with a note, while the design has no timeline. Keyframes are 2 to 32 per track (DesignLimits.minTrackKeyframes/maxTrackKeyframes): the buttons are disabled at the bounds and a longer list is refused before it is read. Each change stores one whole track and is one undoable edit; the other tracks,orbitandletstay as they are, and a value that changes nothing is no entry. A refused value (an offset outside 0..1, offsets that decrease, a non-finite number, an unknown easing,cyclesthat is not an integer in 1..1000) leaves the design unchanged and lists the format's issue with its path. -
Transport (under the canvas, shown while the design has a timeline): a Play / Pause button, a scrubber and the time in
milliseconds. The button is named after what it does: it reads "Pause" while playing and "Play" while paused (its
accessible name is that text; no pressed state). Playing, the preview is the animated SVG, exactly what
readmint renderwrites; paused attit is the framereadmint render --time twrites (the time is a whole number of milliseconds within the timeline). Scrubbing or typing a time pauses at it; Play restarts the animation from its start. The playhead is session state: never an undo entry, never part of an export or the design; an edit, an undo or a redo keeps the paused time (cut to a shorter timeline) and renders at it. While playing, the scrubber follows a clock of its own; nothing is rendered per frame. - Keyboard focus: renaming a mode, token or binding keeps the focus in its input (Tab moves on as usual) and "Move up" keeps it on the moved row. After a Remove the focus goes to the next row's Remove, else to the group's Add button ("Add token", "Add mode", "Add binding", "Add orbit"); after "Add orbit" to the first orbit field. In an animation track the focus stays in that track: after a keyframe Remove it goes to the next row's Remove, else to the track's "Add keyframe"; "Add after" and "Add keyframe" focus the new row's offset. Long mode names wrap, and each mode label shows the full name as a tooltip.
-
Orbit (design-format.md): a layer shows
-
Fonts: upload a TTF, OTF, WOFF or WOFF2 file (see below).
-
Images: upload a PNG, JPEG, GIF or WebP file; it is embedded in the design with an image layer showing it (below).
-
Fixture data: load a JSON file of data namespaces, as the CLI's
--datafile; bindings render against it. The editor never fetches data: a JSON document the CLI's--data-jsonor the Action'sdata-jsonwould load is given here as fixture data under its namespace,{ "<namespace>": { "version": 0, "document": <the JSON> } }(data-sources.md). A feed the CLI's--data-feedor the Action'sdata-feedwould load is likewise given as fixture data under its namespace,{ "<namespace>": { "version": 0, "title": ..., "link": ..., "entries": [...] } }(data-sources.md). WakaTime stats the CLI's--data-wakatimeor the Action'sdata-wakatimewould load are given the same way,{ "wakatime": { "version": 0, "range": "last_7_days", "totalSeconds": ..., "languages": [...], ... } }; the editor never takes an API key (data-sources.md). Registry entries the CLI's--data-registryor the Action'sdata-registrywould load are given the same way,{ "registry": { "version": 0, "npm": [{ "name": ..., "version": ..., "weeklyDownloads": ... }], "maven": [...] } }(data-sources.md). -
Settings: "Load settings" takes a settings file (the CLI's
--propsand the Action'sprops-file: JSON,{ "<section>": { "<prop>": value } }) and the "Settings section" list applies one of its sections to the root component layers, asapplyDesignPropsof@readmint/formatdoes. Importing a design file picks the section named like it (propsSection:header.jsonandheader.dark.jsontakeheader) when the file has one; loading a settings file again keeps the current section if the new file still has it, else takes that one. New keeps the settings file but chooses no section, and "No section" renders the design as it is. The chosen section's props win over the layer's own values in the preview and the raster export, so editing such a prop in the property panel shows no change until "No section". The settings are an input of the session, like fixture data: never part of the design, its history or its export. A section that does not apply (a wrong type, a design without a root component layer) is listed as asettingsissue by its pointer, never with a value; the preview then shows the design without it and the raster export is refused with anexportissue, as the CLI would exit with 1.With a section chosen, the settings form (left column, headed by the section name) edits its props: one field per prop the root components declare, in declaration order: a text or colour input, a number input (a number or a
{{binding}}), a checkbox, a select for an enum, and for a list one input per item with "Add item" and "Remove item" (new items are strings; an edited number or boolean item stays one while the text fits). A prop the section lacks shows what renders instead, greyed: the chosen variant's value (when it applies), else the chosen manifest render's, else the value its root component layer sets, else the design's default; it is written only once edited; "Unset" removes a prop from the section. A change is committed when the field changes or Enter is pressed and the preview and the raster export follow. Every edit is checked as the CLI reads a--propsfile (a prop name is an id, a value a string, number, boolean or a list of those) and refused with a message that names the prop and never quotes a value; a list longer thanmaxSettingsListItems(1000, the format's cap) is shown read-only without inputs. Settings edits are not undo steps: like loading the file they belong to the session. Warnings do not block the preview: a prop in the section that no root component declares (listed in the form with "Remove"), and a declared prop that neither the section, a root component layer nor the prop's default sets (nothing is rendered for it). The unknown prop makes the CLI refuse the section, so the PNG and WebP export is refused with a message naming it until it is removed; at most 100 warnings are listed."Export settings" (enabled once a file is loaded) saves
settings.jsonin the format--propsandprops-fileread. The text of the loaded file is kept: with no edit the export is exactly the loaded file, and an edit replaces only the value of the edited section (two-space indented at its key's indent), so every other section stays byte for byte, BOM and line ends included. A settings file is read as strict UTF-8 as the CLI does (otherwise refused with "settings file is not valid UTF-8") and its BOM is kept. New, Import and loading keep the loaded text. Importing the export gives the same sections, andreadmint render --propsrenders the same image as the preview. -
Render manifest: "Load manifest" takes a render manifest (the CLI's
render --manifestfile, JSON, parsed byparseRenderManifestof@readmint/format; the cap ismaxManifestBytes, strict UTF-8) and the "Render" list shows its renders as<name> (<design>), with "No render" first. A render applies to the open design when the last segment of itsdesignpath is the imported file's name (repo.json): the browser has no folder, so import the design file separately, and two designs of one file name in different folders cannot be told apart. A render of another design file is listed but disabled, and choosing it by command is refused with an issue naming that file. Choosing a render previews and exports the design asreadmint render --manifestrenders it, in the CLI's order: the render'spropsfirst (applyDesignProps), then the variant, then the settings section named like the render (the render's name only: no fallback to the design file's section; the section is chosen when the settings file has it, else none, and loading a settings file prefers it). A section prop that the render also sets wins, with a warning. The preview is free to use any section, but the PNG and WebP export of a chosen render is refused unless the settings section is the onerender --manifestuses (the render's name when the settings file has it, else none); the message names it. A render that listsvariantsoffers only those (with an empty list the Variant list is disabled and says so) and drops a chosen variant it does not list; a render without a list keeps the session's variant, and a variant that sets a prop the render also sets is amanifestissue with the CLI's pointer and message (manifest /renders/<i>/props/<prop>: the prop <prop> is also set by the <variant> variant; set it in one place): the preview then shows the render without the variant and the raster export is refused, as the CLI exits with 1. A render whose props do not fit the design is amanifestissue by pointer, never a value. Loading a manifest again keeps the chosen render only if it still names the open design file, under the rules of choosing it (a variant it no longer lists is dropped, its section is chosen again); otherwise it behaves as "No render". Importing a design file with a manifest loaded keeps the chosen render if it names the file, else takes the first render that does (repo.jsontakesrepo-1), and chooses its section; "No render" returns to the file's own section. The manifest and the chosen render are inputs of the session, like the settings: never part of the design, its history or an export; undo and redo keep them, and New keeps the manifest with no render. -
Variant: the "Variant" list switches the preview and the PNG and WebP export between Light (the design as it is) and
dark, the render variant of@readmint/format(renderVariants):theme: darkset on the root component layers, asreadmint render --variant dark. The design must declare athemeprop on a root component; otherwise the list is disabled and says why ("no top-level component declarestheme"). The variant is applied first and the settings section after it, as the CLI does, so a section that setsthemewins, with a warning naming the section and the prop; with a variant chosen the settings form shows the variant's value, greyed, for a prop the section lacks. A variant that does not fit the design (an enum withoutdark, another type) is listed as avariantissue by its pointer, never with a value; the preview then shows the design without it and the PNG and WebP export is refused. The variant is an input of the session, like fixture data: never part of the design, its history or any export, and switching it is not an undo step (undo and redo keep it). Import keeps it when the imported design declaresthemeand drops it otherwise; New clears it. Importing acard.dark.jsontwin still picks thecardsettings section, as before. With a manifest render chosen the list is limited to the variants the render lists (Render manifest). Next to the list, "Palette mode: " names the palette mode the last successful render used (after the variant, the section and the manifest render); it is empty for a design without a palette and after a failed render. -
Import and export: import a JSON or YAML design file (validated with the format's limits); export the design as
design.json(two-space JSON, a final line feed, the same bytes for the same design), asdesign.yaml(YAML 1.2, keys in the design's own order, no anchors; refused with anexportissue naming both lengths when it would pass the YAML reader's 1 MiB, then export JSON), or asdesign.png/design.webp(below). A file export re-imports to the same design; an image is what the CLI renders.
Every edit of the design can be undone and redone (below). Not in this version: motion paths, graph editors and dragging keyframes on the canvas, APNG or GIF export, and scale, frame rate or quality settings for the raster export.
Dragging writes whole numbers. It never changes a coordinate or size bound to data, nor the place of a layer a stack
or grid layout positions (its x/y are offsets in its slot; the property panel says so): such a drag is refused with
an issue. A path keeps its size (its data is its shape); a text's handles set its wrapping width. In a turned group a
drag is turned back into the group's coordinates, so the layer follows the pointer; a layer on an orbit moves its
x/y offset from its orbit point the same way (its box is drawn at the orbit's angle at the start of the timeline). Resize handles appear only on a
layer that neither it nor any group around it turns (whole turns aside): turns that cancel out on the canvas still swap
what the layer calls width and height.
Ctrl+Z (Cmd+Z) undoes the newest edit of the design; Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z, Cmd+Y) redoes it, as do the toolbar's Undo and Redo buttons, which are disabled when there is nothing to step to. The letter is the one the layout types; on a layout without Latin letters (Cyrillic, Greek) it is the key in the Z or Y place. In a text field, a select or editable content the keys are the field's own: the editor's undo never takes them there.
Every command that changes the design is one entry (a drag is one, recorded when the pointer is released; an upload
adds its asset and layer as one); a refused command, an edit that changes nothing (a drag that rounds to no move, a
value set to what it is, a let, palette or track that stays the same), a selection, moving the playhead, loading fixture data, attaching a file for a path-sourced asset and reading
font licences are not entries. When the design changes during a drag (an undo, a redo, an upload finishing), the drag
is dropped: releasing the pointer changes nothing. Undo and redo restore the design and the layer selected
with it, and clear the issue list; fixture data, settings, the manifest and its render, attached files and font checks are inputs of the session and stay as
they are. A new edit after an undo drops what could have been redone. The history holds the last
EditorLimits.maxHistory (100) edits, the oldest dropped first; entries share the unchanged parts of the design, so
they cost little. New and Import start an empty history.
Every change re-renders with renderPreview, which is renderSvg(design, { data, assets, layerBoxes: true }) and
nothing more ({ time } added while the transport is paused, above); design is effectiveDesign(state): the design with the chosen render's props, the variant and the chosen settings section applied. It runs in a module Web Worker: a hostile design can keep only the worker busy, and a render running
longer than EditorLimits.renderTimeoutMs (10 s) is stopped (the worker is replaced) and reported. A worker that fails
(an error, a result it cannot send) is reported at once and replaced for the next render. A newer edit's render always
wins: an older result arriving later is dropped.
The SVG is shown as an <img> of its exact bytes (a blob URL), as the CLI preview and a README show it: its ids and
styles never meet the page or another copy, and it runs no script. The editor draws its own selection overlay from
the renderer's layerBoxes (renderer.md), which never change the SVG. The render's issues and
warnings are listed under the editor with their design pointer; one inside a layer selects it when clicked. After a
render fails, the image keeps showing the last good render, but clicking the canvas selects nothing until a render
succeeds (the layer tree still does): the old boxes may no longer match the design.
Exporting the design and rendering it with readmint render design.json --data <fixture> (and --props <settings> when a section is chosen, --variant dark when the dark variant is; the design file's name picks the section) gives the preview's bytes; with a manifest render chosen, readmint render --manifest renders.json --out-dir o --data <fixture> [--props <settings>] [--variant dark] (--variant dark for a render that lists no variants and the session's Dark) writes o/<render>.svg (and <render>.dark.svg when the render lists dark) with the preview's bytes;
a golden suite (packages/editor/test/editor-session.golden.ts, Node and Chromium) and a CLI test pin it.
The toolbar's "Export PNG" and "Export WebP" make the image the CLI makes for the same design, fixture data and settings section:
readmint render design.json --data data.json --format png|webp with its defaults (scale 2, lossless WebP). The export
is renderSvg(design, { data, assets, time }) for each frame, as the CLI renders it, passed to exportRaster of
@readmint/raster/browser (raster.md): a still design gives a still image, a design with a
timeline an animated WebP (a PNG of its first frame, or, with the transport paused, of the frame at the playhead:
--format png --time <t>; the WebP is always the whole animation). The files are design.png and design.webp, and design.dark.png and design.dark.webp with the dark variant chosen; with a manifest render chosen the stem is the render's name (repo-3.png, repo-3.dark.png), as the files readmint render --manifest writes, and the bytes are those files'. With a render chosen the export also needs the settings section render --manifest uses (the render's name when the settings file has that section, else none), and is refused with an export issue naming it otherwise.
It runs in a module Web Worker started for the export and terminated when it ends: the worker fetches the two
WebAssembly files from the app's own origin, exports, and answers with the bytes. Nothing of raster export is in the
initial chunk or the preview worker; the worker file is loaded only when a button is clicked. One export runs at a time
(a second click is reported as an export issue); one running longer than EditorLimits.rasterTimeoutMs (120 s) is
stopped and reported, as is a worker that fails. The rasterizer's own limits (RasterLimits, as in the CLI) apply, and
a refusal reaches the issue list in its words.
The bytes are pinned: packages/editor/test/editorRasterParity.ts holds the SHA-256 of the CLI's PNG and WebP of
test designs made through the editor's commands; Node (the CLI and the export code), Chromium (the export code, and
the mounted editor with its real worker) and the built app (a download from its page) must all match it.
An uploaded font becomes a font asset whose src is a base64 data: URI, so the design file carries it. Family,
weight and style come from the font (the file name when it names no family); the asset id is made from the file name.
- The file is refused before it is parsed when it is larger than
EditorLimits.maxFontBytes(the renderer'smaxFontBytes, 4 MiB); a malformed, unsupported (collection) or over-large font is refused with the parser's reason. A font that would make the exported file longer thanDesignLimits.maxSourceLength(what the CLI reads) is refused, naming both lengths, as is every other edit that would. A refused font leaves the design unchanged. - A font whose OS/2 fsType forbids embedding is stored with
licence.embedding: "restricted": its text is drawn as paths and the font is never embedded. Its embedding control is disabled with the reason, and the command refuses to set it toallowed; this holds for fonts of imported designs too (their data URIs are read again). Any font may be set to restricted. The licence name (SPDX) is editable. An edit changes only the field it names: every other licence field (reservedFontNameincluded) and asset field is kept. Any asset id the format allows works,constructorincluded. - Reading the fonts of an imported design (or of attached font files) is bounded as in the renderer: each distinct
source is decoded once, and a font may decode to no more than what is left of
EditorLimits.maxTotalFontBytes(the renderer'smaxTotalFontBytes, 16 MiB): a WOFF or WOFF2 declaring more is refused before it inflates anything, and a font that reads is charged its decoded size. The first font that cannot be read (malformed, unsupported, or past the allowance) stops the check with an issue at its pointer, as it stops a render, so a font that fails costs at most its own inflate once. The fonts from it on are not locked in the panel, and the renderer still never embeds a font whose fsType forbids it. An upload reads one file of at mostmaxFontBytes.
An uploaded image becomes an image asset whose src is a base64 data: URI of the type its bytes are (sniffed, never
taken from the file name), so the design file carries it, and an image layer at the canvas's top-left corner shows it:
at its pixel size as it is shown (EXIF orientations that turn it a quarter swap width and height), scaled down, never
up, to fit a canvas of literal size, in whole pixels. It goes into the selected group, else to the top level, and is
selected. The asset id is made from the file name (image when nothing of it is left), the layer takes the same id when
it is free. Undo removes the asset and the layer together.
Before anything is added, the file goes through the checks the renderer runs on one image asset, in its order, with
its bounds (readImageFile):
- a file larger than
EditorLimits.maxImageBytes(the renderer'smaxImageBytes, 4 MiB) is refused before it is read; - the format is sniffed from the bytes: PNG, JPEG, GIF or WebP; anything else, SVG included (it can carry scripts), or a malformed or truncated file is refused with the sniffer's reason. A JPEG's image data is walked to its Huffman symbols, never decoded to pixels (renderer.md): data the browsers and resvg would draw apart is refused, as is a file past the check's step bound;
- a side longer than the renderer's
maxImageSide(8192 pixels) or more animation frames than itsmaxAnimationFrames(1000) is refused, naming the limit; - an image that would make the exported file longer than
DesignLimits.maxSourceLengthis refused, naming both lengths. Base64 makes the data URI a third larger than the file, so this bounds an upload to about 3 MiB in practice, less what the design already holds.
A refused image leaves the design unchanged, with an image issue saying why; so does a file that cannot be read or a
check that fails to load. The check loads on the first upload, never with the page, and runs on the page's thread: it
decodes no pixels, but a JPEG's symbol walk takes about 0.1 s for a 3 MB baseline file, 0.2 s for a progressive one.
The renderer keeps an accepted JPEG's result per module instance (renderer.md, Images): the preview
worker checks it once for all its later renders, the raster worker once for every frame and the pixel count of an
export; the upload check on the page thread keeps its own.
The upload checks the file alone. What a render bounds over the whole design is the preview's to report, as for an
imported design, with the design kept: the JPEG data of all its images together (maxRenderEntropySteps), the SVG's
maxOutputBytes (5 MiB; an image's data URI is written once however many layers show it), and for a raster export
the image pixels it decodes (RasterLimits.maxImagePixels, raster.md). The asset panel lists a data:
image or font as embedded in the design, by the CLI's rule (a data: source is the renderer's to decode, a
{{binding}} is fetched, anything else is a path), without decoding it: a malformed one is the preview's issue, never
a file to attach. A golden fixture (session-image) and a CLI test pin that an uploaded image previews as the CLI
renders it.
An imported design may name font or image files by path. The asset panel lists them as needing a file; a file picked
there is used by the preview for this session only (same size limits, checked before the file is read) and never
written into the design, which keeps
the path, so the CLI reads the same file next to the design. Image URLs bound to data are not fetched: the editor makes
no network request but the raster worker's fetch of its two WebAssembly files from the app's own origin, and it never
handles a token. The renderer's missing-asset issue is shown instead. Only an asset given by path takes a file:
attachAsset refuses one for an embedded (data:) or bound asset with an asset issue, as the CLI never reads a file
for those and the preview would otherwise draw bytes the CLI does not.
EditorLimits: maxFixtureBytes 8 MiB (the CLI's), maxSettingsBytes (three bytes per character of DesignLimits.maxSourceLength, as the CLI's --props cap; an edit is refused when the file would pass maxSourceLength characters, as the CLI reads it), maxManifestBytes (three bytes per character of RenderManifestLimits.maxSourceLength, as the CLI's manifest cap), maxSettingsListItems 1000 (a prop list; the format's cap), maxFontBytes and maxImageBytes 4 MiB and
maxTotalFontBytes 16 MiB (the renderer's), maxHistory 100 edits, a track's 2 to 32 keyframes (DesignLimits), each track property once per layer, a timeline of 1..60000 ms and cycles 1..1000 (ValueRanges), renderTimeoutMs 10 000, rasterTimeoutMs
120 000. Every file the user picks is size-checked before it is read. Design files are bounded by DesignLimits on import and on every edit: an edit that would make the exported
file longer than maxSourceLength is refused.
The app's page sets default-src 'none'; script-src 'self'; worker-src 'self'; style-src 'self'; img-src 'self' blob: data:; font-src data:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none': only the app's own
scripts, worker and stylesheet load, nothing inline runs, and the page makes no request (the dev server alone allows
connect-src 'self' for its reload socket). A test builds the app and drives it in Chromium under this policy, raster
export included: the policy of the page does not apply to its workers, which take the policy of the headers their own
files are served with. A host that serves the app with a policy header must allow, for the raster worker,
connect-src 'self' (it fetches the two WebAssembly files) and script-src 'wasm-unsafe-eval' (it compiles them); the
page itself needs neither.
The initial chunk holds the editor and the format's validation (about 20 kB gzip, budget 120 kB). The YAML parser
(reading and writing), the font parser and the image check (the renderer's sniffer, without the renderer) load on
first use; the renderer lives only in the two workers: the preview
worker (budget 250 kB gzip) and the raster worker, which also holds the rasterizer and the encoders (about 184 kB gzip,
budget 230 kB). The two WebAssembly files (resvg 2.5 MB, libwebp 0.5 MB) are separate assets, never inlined, fetched
only by a raster export. A test bundles the app and checks the budgets, which chunk holds what, that the rasterizer is
in the raster worker alone, and that the editor pins the WebAssembly packages to the versions @readmint/raster pins.
The app build also writes licences/THIRD-PARTY.txt: the name, version, licence and licence texts of every npm
package bundled into the page or a worker (fflate, gifenc, yaml, the font and WebAssembly packages), made by the
same script as the Action bundle's banner, with the notices of the WebAssembly code (resvg's Rust crates, libwebp).
import {
mountEditor,
renderPreview,
exportDesign,
uploadFont,
uploadImage,
EditorLimits,
} from "@readmint/editor";
const editor = mountEditor(document.getElementById("app")!, {
initial: design, // optional, default an empty canvas
save: ({ name, text }) => store(name, text), // optional, default a download (design.json, design.yaml)
saveImage: ({ name, mimeType, bytes }) => store(name, bytes), // optional, default a download (design.png, design.webp, design.dark.png, design.dark.webp; <render>.png ... with a manifest render chosen)
saveSettings: ({ name, text }) => store(name, text), // optional, default a download (settings.json)
});
editor.dispatch((state) => moveLayer(state, "/layers/0", 10, 0));
editor.dispatch((state) => uploadImage(state, { name: "logo.png", bytes })); // bytes: Uint8Array
editor.undo(); // as Ctrl+Z; editor.redo() as Ctrl+Shift+ZThe state (EditorState: design, selection, fixture data, settings, manifest, variant, session assets, font checks, issues) is immutable; every
command (addLayer, moveLayer, resizeLayer, setLayerProperty, reorderLayer, removeLayer, selectLayer,
uploadFont, uploadImage, setFontAsset, attachAsset, loadFixture, loadSettings, chooseSection, loadManifest, chooseRender, setSettingsProp, setVariant, setLayerLet, setLayerTrack, setTimeline, setPalette) is a pure function returning a new state
whose design passes validateDesign, or the same design with the issues saying why not. setVariant(state, name) chooses the variant name (or none with undefined) and changes no design; an unknown name, a design without theme or a variant the chosen manifest render does not list is refused with a variant issue (EditorIssue source variant). settingsFields reports from: "variant" for a value the variant gives and from: "render" for one the chosen manifest render gives. loadManifest(state, text, designFile?) keeps the parsed renders (a refused manifest leaves the state, with manifest issues; the chosen render stays only while it names designFile, under the rules of choosing it) and chooseRender(state, name, designFile) applies a render of that design file, or none with undefined (EditorIssue source manifest). setSettingsProp(state, prop, value) sets or (with undefined) removes a prop of the chosen settings section and changes no design; exportSettings(settings) is the text "Export settings" saves and settingsFields(state) the fields of the form. effectiveDesign(state) returns, besides the design and its issues, warnings (an EditorIssue with severity: "warning", shown as warning: ...) and unknownProps. readImageFile(bytes) is
the check an upload runs. The history (EditorHistory) is an immutable value of its own, kept by mountEditor beside
the state: recordEdit(history, before, after) records a design change, undo(history, state) and redo(...) return
the next history and state (undefined when there is nothing to step to), canUndo and canRedo say whether there is. paletteEdits holds the pure operations on a palette's ordered modes and tokens (addMode, addToken, renameMode, renameToken, removeMode, removeToken, setColour) that setPalette takes. setLayerTrack(state, pointer, property, track) sets or (with undefined) removes one animation track of a layer and setTimeline(state, animation) the design's timeline; renderPreview and the raster export take an optional time (milliseconds), the frame readmint render --time writes.
Home · Getting started · Examples · Source
See it first
Start here
Guides
Reference
Contributors