Changelog

Every release of quoin-editor is listed here, newest first, in the spirit of Keep a Changelog. From 1.0.0 the package follows semantic versioning: a breaking change waits for the next major version. Until then, Quoin was 0.x and a minor version could contain breaking changes; those known at release were marked "Breaking". The 0.x sections are kept as they were released, "Docs" headings included. Moving from 0.4 to 1.0, and from 1.x to 2.0, is covered step by step in MIGRATION.md. Review finding ids such as F-17 are those of the 0.4.0 review in #73.

2.0.3 (2026-11-09)

Fixed

  • A closed toggle points the way the text runs on, so on a right to left page its chevron points left rather than right. Open points down whichever way the page runs, as it did. The badge on an image with no alt text moves to the inline start with it.
  • The horizontal arrows walk the text the way it runs on a right to left page. A caret at the edge of a block left it towards the wrong neighbour, so the left arrow at the end of an Arabic or Hebrew line went back to the block before rather than on to the next, and Shift with an arrow beside a formula or a mention took the atom on the wrong side. The arrows in the bubble toolbar and in the emoji picker's grid moved against the page too. An arrow names a side of the screen rather than a movement, so the view now reads one as it would be written on a left to right page, once, and the moves and the keymaps behind it are unchanged. The arrows that resize an image or a table column stay as they are, since a handle should move the way the key under the finger points.
  • The table's column menu puts a column where it says on a right to left page. Insert column left added it before this one in the document, which the page then drew on the right, and Move column left moved it right; the two move items were also disabled at the wrong ends, so the column a reader saw at the left edge could still be moved further left. The wording stays physical, since that is what the writer is looking at, and the indices turn round behind it.
  • Every menu, toolbar and popover opens on the correct side of a canvas whose page runs right to left. The twenty places that open one already asked to be placed at the inline start or end rather than at a left or a right, but whoever places them reads the direction through React rather than from the DOM, because they are drawn in a portal outside the canvas, and nothing ever told it. All twenty therefore resolved against a left to right default, so an Arabic, Hebrew, Persian or Urdu document had every menu on the wrong side and the arrow keys inside them mirrored. The canvas now reports the direction it is laid out in, read from its root, which also covers a page turned round with CSS rather than with a dir attribute. This adds @radix-ui/react-direction to the package's dependencies, a context provider of a few hundred bytes that draws nothing.
  • A callout whose tone the editor does not know is drawn as info in the read-only view, which is the tone the canvas already falls back to. The two renderers disagreed: the canvas normalised anything outside the five tones, while the view passed the stored value straight into data-tone and into the region's accessible name, so a document carrying a tone from untyped host code, or from a version that knew another one, drew unstyled in the view and read its raw value aloud. Both now take the one list.
  • A host renaming one entry of a label that holds a record keeps the English wording of every other entry in it. blockNames, groups, colors and calloutTones are maps of names rather than single names, and an override replaced the whole map, so renaming one slash group left the others undefined and the menu drew their headings blank.
  • englishLabels is frozen, with the records inside it. Every canvas on the page shares that one object, so a host writing to it, by accident or to patch a string, changed the wording under every other canvas and under the locale packs that fall back to it.
  • A composition over a formula, a mention or another atom is left alone. The guard that keeps a selection out of an atom read only the view's own composing flag, which the view sets after the key, while the browser reports the composition on the key itself. A first keystroke of a Japanese or Pinyin input beside an atom could therefore move the selection out mid-composition and drop the text being composed.
  • A host predicate passed to useAutosave as isRetryable that throws ends the retries instead of escaping into the save loop. The throw replaced the save's own failure, so the document stayed unsaved and nothing was reported.

2.0.2 (2026-11-09)

Fixed

  • The packaged stylesheet no longer carries the ::highlight() rule that paints a selection running across blocks, and the rule goes on the page at runtime instead, beside the registration of the highlight it styles. Lightning CSS, the parser Next.js and Turbopack use, does not know the Custom Highlight API and reported a parse failure for the rule on every consumer build. Nothing was lost, since the rule was passed through and the selection still painted, but the package's CSS is inlined into the consumer's own stylesheet, so the warning named their file and a byte offset in a minified line and read as a mistake of theirs. The rule now lands only in a browser that has the API, which is the only browser it ever applied in, and what is painted is unchanged.
  • The bubble toolbar and the menus anchored to a range read the document the canvas is in rather than the page's own, so a canvas a host draws inside an iframe shows them. Focus was read from the top document, which never holds the focused element of an iframe, so the toolbar never appeared at all; the listeners that wait for a selection to settle were bound to the same wrong document, and the side a range menu opens on was measured against the wrong window height.
  • The emoji search finds an emoji by a tag the dataset spells with capitals, such as the Shaka of the call me hand or the ZA of the South African flag. The query was folded to lower case and the tags were not, so 263 of the 1,923 emoji could not be reached by one of their tags at all. The tags are folded once as the dataset loads, so a search costs what it did and the tag keeps the spelling it is shown by.
  • Replace in the find panel steps to the next match when the replacement is the match itself, rather than leaving the panel where it stood. Replacing cat with cat changed nothing, which is right, but the panel then stayed on that match and pressing Replace again did nothing, so stepping past it meant reaching for the next button.
  • A file block draws the icon of a type its host spells with capitals: IMAGE/PNG now gives the image icon rather than falling through to the extension of the file name. Media types are case insensitive and the extension path already folded its case.
  • A failure raised while uploaded files are put into the document, rather than while they upload, reaches onError. The upload itself was always reported, but anything thrown after it, such as a host plugin that refuses the transaction carrying the blocks, became an unhandled rejection that only the console saw.
  • An address typed into the link popover that is an email address gets mailto:, not https://. ada@example.com was completed to https://ada@example.com, a link to the host example.com carrying ada as a user name.
  • An extension whose ui holds something other than a list where the canvas expects one no longer takes the canvas down with it. The engine carries the field untyped, so untyped host code could put a string in slashItems and the canvas spread it into the slash menu, and a value of any other shape reached a flatMap that threw.

2.0.1 (2026-11-07)

Added

  • The playground also switches comments, AI actions and a second collaborator on and off. Comments anchor a thread to a range and keep it in a store of the page’s own, AI streams canned text where a host would pass its model, and collaboration draws a second canvas with a Yjs document of its own that reconciles with the first over a BroadcastChannel. The settings are in two groups now, those a canvas reads as it mounts and those it answers to at once, because the three new ones are extensions and remount it where highlighting and read only do not.
  • A playground page on the site switches the editor between English and the seven locale packs, turns syntax colouring and the markdown source pane on and off, and draws the canvas read only, with the tsx for whatever is set beside it to copy. Picking a language downloads that pack and remounts the canvas, since a canvas reads its extensions once, and the other three take effect where they are. The home page links it from the hero, and two cards there name the code block colouring and the markdown pane, which shipped in 1.5.0 and 1.6.0 and the page had never mentioned. Nothing in the package changes.
  • The FAQ answers how to upgrade from 1.x to 2.0 beside the 0.4 answer it already carried, naming all seven removed exports and pointing at the section of the migration guide that covers them.

2.0.0 (2026-11-06)

Removed

  • Seven names the view layer only ever calls itself leave quoin-editor/view, and with it quoin-editor/headless and quoin-editor. BlockShell and SelectionSink, with the BlockShellProps and SelectionSinkProps that typed them, are drawn by EditorView itself and register themselves with the controller, so a second copy of either takes the registration from the first. VoidBlockFallback is what a block shell already falls back to when no renderer draws a block type, so a renderer table that names it changes nothing. defaultRenderers held the three renderers a canvas already reaches for on its own; a renderer of your own draws its text with EditableRegion, which is what those three do. isCrossSelection answers whether a text selection spans blocks or regions, which is what painting and the arrow keys ask; a host that wants the answer reads anchor and head of the public Selection, and were it to earn a place it belongs beside textSelection in quoin-editor/engine rather than in the React entry. BlockRenderProps and BlockRenderer stay, so a renderer table still types. The five values among the seven were marked deprecated in 1.8.0. Removing a published name is breaking, so this lands in 2.0.0; MIGRATION.md covers the move.

Fixed

  • A menu no longer throws where Element.prototype.scrollIntoView is absent, which is jsdom with no stub and any webview that omits it. Ten calls guarded the element they had queried but not the method on it, so an element that existed without the method was called anyway and threw a TypeError that left Quoin for the host's tree, replacing the page wherever an error boundary caught it. The calls are in the slash and language menus, the code block's caret reveal, a block link's target, a table cell, a heading jumped to from the table of contents and vertical caret movement. Keeping the highlighted entry in view is the whole of what a host without the method now loses. Two scrollTo calls that restore a scroll position after a change carried the same shape and are guarded with them.
  • An editable canvas warns in development when a definition of CODE, IMAGE or FILE replaces the preview that edits it and brings no editor of its own. Quoin edits those three in their preview rather than through an editor, so such a definition left a block the canvas drew and nobody could write in: no editable node, no language picker, and for an image or a file no toolbar and no way to fill the empty state. Nothing was logged, and the warning that would have caught it covered MARKDOWN, CALLOUT and TOGGLE only, whose consequence is milder since the canvas keeps drawing and typing into those. The migration guide said as much by naming only that first set, and now names both. Nothing about what the canvas draws changes.

1.8.0 (2026-11-05)

Added

  • Seven locale packs, one entry each, carrying every string the editor shows or speaks in that language: quoin-editor/locale-ar (Arabic), quoin-editor/locale-de (German), quoin-editor/locale-es (Spanish), quoin-editor/locale-fr (French), quoin-editor/locale-ja (Japanese), quoin-editor/locale-pt (Portuguese) and quoin-editor/locale-zh (Simplified Chinese). A pack covers the canvas and all fourteen feature label sets, so frLabels goes to the canvas and frTableLabels to the table, and it is data alone: no React, no side effects, about 5.5 KB gzip each, downloaded only by a host that imports one. A label that carries a count, a date, a number or a file size is written as its language writes it, through Intl, so Arabic gets its six plural forms and Japanese its one. Nothing is detected: the host picks the language, and a key a pack leaves out still falls back to English.
  • quoin-editor/headless exports the types its own signatures already named, so a host can write them down rather than reach them through Parameters and ReturnType: Announcer with Spoken, Engagement, FocusTarget, HostRegistry with HostEntry, and NodeReveal, all reached through ViewController; CaretPoint, which caretFromPoint returns; and BlockShellProps and SelectionSinkProps, taken by the two components named after them. They reach quoin-editor too, which re-exports the headless entry. Nothing new runs and no type changes shape.

Changed

  • The exports a host meets first carry a documentation comment, so an editor explains them on hover: BlockCanvas, EditorView, useQuoinEditor, createEditor, toMarkdown, toHTML, toPlainText, fromMarkdown, PALETTE and englishLabels. Nothing about their behaviour changes.
  • BlockShell, SelectionSink, defaultRenderers, VoidBlockFallback and isCrossSelection are marked deprecated, in an editor and on the headless reference, and they go in 2.0.0. All five are internal to the view layer and a host has no use for them: EditorView draws the first two itself and a second copy of either takes the registration with the controller from the first, a renderer of your own draws a block's text with EditableRegion rather than reaching for defaultRenderers, a block shell already falls back to VoidBlockFallback when no renderer draws a block's type, and a host reads anchor and head of the public Selection to tell a selection that spans blocks or regions from one held inside a single region. BlockRenderProps and BlockRenderer stay public, so a renderer table still types. Every name is still exported and still does what it did.
  • The comparison table weighs Quoin against Lexical and Plate as well, rechecked against each project's own pages and dated 2026-11-05. Lexical keeps an editor state it writes as JSON, draws no interface of its own, and ships neither comments nor translations: none of its forty packages covers either, and the comment plugin a reader finds is playground code. Plate keeps Slate JSON, converts markdown both ways, collaborates over Yjs and marks comments on text, and is MIT with a paid set of components behind its Notion-like template. A localization row goes in beside them: BlockNote ships 24 locales, Editor.js takes a dictionary the host writes, and Tiptap, Lexical and Plate ship none, since their interface strings live in the code a host copies into its own project. The comparison page reads in a wider column than the guide, because seven columns in a line of prose left each one too narrow to read.

1.7.0 (2026-11-04)

Added

  • quoin-editor/diagram draws a code block whose language names a diagram, such as mermaid, through a renderer you pass, which may answer at once or with a promise as the common engines do, with its source below the drawing where a writer already edits it. Quoin draws no diagrams itself and bundles no engine. The document is unchanged: a diagram is a fenced code block, and opens as one anywhere else. A renderer that throws or draws nothing leaves the source alone rather than losing the block.

1.6.0 (2026-11-04)

Added

  • quoin-editor/source shows the document as the markdown it is stored as, read only, beside or below the canvas. QuoinSource draws it with the block holding the caret marked, and useEditorSource gives the same markdown and the lines of that block for a pane of your own. What it shows is toMarkdown of the whole document, not of each block in turn, because a list item spells itself differently alone.

Changed

  • The CommonJS build ships no sourcemaps, and carries no comment pointing at one. The ESM build keeps its own, which is what a browser, a bundler and a reader step through. Both sets together had taken the tarball past the 5 MB it states; it is now about 0.96 MB packed and 4.0 MB unpacked, against 1.09 MB and 5.02 MB.

1.5.0 (2026-11-03)

Added

  • quoin-editor/highlight colours code blocks, in every language the block's language picker offers. usePrismHighlighter is a highlight for BlockCanvas, and the grammars behind it are a chunk fetched the first time a canvas using it mounts, so a page that imports none of this downloads none of it. Six theme tokens, --quoin-syntax-comment, -keyword, -string, -number, -function and -variable, colour the runs, each held by a test to 4.5:1 on a code block in both themes. The highlight prop is unchanged, so a highlighter of your own still replaces it.

Changed

  • An image with no alt text is announced as Image, no alt text, where it was Image, no description, so the editor speaks the term the badge beside it and the alt text popover already use. A host that sets imageNoAlt in its labels is not affected.
  • Every slash menu description is a noun phrase: List with bullets, Quotation, set apart, Image, uploaded or linked and File to download, where some were written as instructions and others carried a leading article. The block names above them are unchanged, and a host that sets slashDescriptions is not affected.
  • The comparison table is rechecked against each competitor's own pages, dated 2026-11-03. Editor.js's collaborative editing is on its roadmap and in progress, where the table said only that it was on the roadmap; BlockNote also runs on vanilla JavaScript when the host draws its own UI, where the table said React; and the toolbox and inline toolbar of Editor.js are cited to its repository, which documents them, where the home page cited no longer does. Every other cell holds, licences and paid tiers included.

1.4.1 (2026-11-02)

Changed

  • The npm description and keywords and the README's first sentence call Quoin a rich markdown editor, a block editor and a rich text editor for React, the phrases npm search and search engines match on. The code is unchanged.

1.4.0 (2026-11-02)

Changed

  • Quoin draws its own icons and no longer depends on lucide-react, so an install is about 36 MB and 4,300 files lighter and the canvas about 3.5 kB gzip smaller. The drawings are Lucide's, so nothing looks different. An icon carries its name in data-quoin-icon, such as data-quoin-icon="check", where it had the lucide and lucide-check classes. A custom block's icon still takes any component that accepts a className and aria-hidden, a lucide one included: if you import from lucide-react, list it in your own dependencies.
  • The emoji data comes from emojibase-data 18. It adds nine emoji, such as 🫝 pickle and 🛙 lighthouse, and an emoji that is drawn as an emoji by default, such as 👍, is inserted without the U+FE0F variation selector it carried.

Fixed

  • The comment marker beside a block is named after the count it shows, 1 comment or 3 comments, where it was Comments (1), so a voice command that says what is on screen reaches it. A host that sets showComments in its labels is not affected.

1.3.0 (2026-11-01)

Added

  • TextSelection has a new optional assoc, -1 for a caret drawn at the end of the line before a soft wrap. The editor sets it when ↑, ↓ or a click leaves the caret there; a host that never sets it sees the same selections as before. It is never written to step, document or Yjs JSON.

Changed

  • Markdown shortcuts convert list items as well as paragraphs: # , > , 1. , - and [] typed at the start of a bullet, a numbered item or a task turn it into that block and keep its indent, as in Notion. A prefix for the kind the item already is, such as - in a bullet, stays text, and the divider, $$ and code fence shortcuts still turn paragraphs only.

Fixed

  • The undo history survives a change the editor does not record, such as a new blocks array, a transaction with addToHistory: false or a remote change without a collaboration history, where it was cleared. Undo and redo carry the writer's edits over that change and never remove text it added; an edit whose text it removed or rewrote is dropped, and older edits still undo. A Yjs collaboration keeps its own history as before.
  • ↑ and ↓ keep the caret on the line they move to where a soft wrap ends it. With the caret's horizontal position past the end of a wrapped line, the caret was drawn at the start of the next line in some cases, and typed text started the next line; the caret now stops before the space the line wraps at, or, where it wraps after another character such as a hyphen, is drawn at the end of the line. A click past the end of a wrapped line keeps that side too, so the next ↑ or ↓ starts from the line clicked.

1.2.3 (2026-11-01)

Fixed

  • Nothing in the package changes from 1.2.2. The test that bounds the work done when a collaborator retypes a block counts the blocks it reads instead of timing them, so a release no longer fails on a slow runner.

1.2.2 (2026-11-01)

Fixed

  • Popovers, the bubble toolbar, dialogs, the AI response bar, the find panel, the mobile toolbar and the keys in the shortcuts dialog draw a soft hairline edge in --quoin-border, where they drew a near-black one. The stylesheet's reset gives every border that names no colour --quoin-border, instead of the text colour Tailwind 4 falls back to.
  • An editor whose first block has no text, such as a table of contents, an image, a divider or an embed, no longer draws that block as selected or says "1 block selected" as soon as the page loads, server-rendered HTML included. The block is drawn as selected once focus enters the editor or a menu of it, or once the reader or the host sets a selection, and stays so when focus leaves.

Changed

  • Fields draw their border in --quoin-input, a lighter grey that still reaches 3:1 on the page, a popover and a highlighted row, and darken it on hover. A focused field shows the focus outline alone, where its border also turned to the ring colour. Outline buttons draw their border in --quoin-border.
  • Menus, popovers, the AI response bar and the find panel are rounded to --quoin-radius, and dialogs a step more, over a lightly blurred backdrop.
  • The rule beside a streaming AI answer is drawn in --quoin-ring, cobalt by default, where it used --quoin-primary.

1.2.1 (2026-11-01)

Changed

  • The default theme is slate with a cobalt accent. The focus ring, text selection, the ring of a selected block and the bar of a highlighted menu row are cobalt where they were near-black or grey, and the neutrals carry a slight cool tint. A host that sets its own --quoin- tokens keeps its values, except that the bar of a highlighted row now reads --quoin-ring where it read --quoin-foreground.
  • A tip callout draws its rule and wash from a new --quoin-tip token, violet by default, where it used --quoin-primary and drew grey. Set --quoin-tip to your primary colour to keep the old look.
  • Menus, popovers and toolbars cast a softer two-layer shadow, coloured by a new --quoin-shadow token.
  • Blocks have 2px of room above and below, so consecutive list items and paragraphs no longer touch.

Fixed

  • A callout whose text starts with a line of pipes and dashes, such as |-, is written to markdown with a blank line after its alert marker, so it reads back as the callout and not as a one-column table.

1.2.0 (2026-10-31)

Fixed

  • An extension's priority means one thing for blocks, marks, atoms, keys and commands: the higher wins. A block, mark or atom spec from an extension at a lower priority than another extension, core included, no longer replaces it, where the lower priority won for schema entries and the higher for keys and commands. Extensions at the default priority resolve as before. priority is now described in the extensions docs.
  • Undo separates edits made in two places: typing at the start of a block and then at its end within the grouping delay is two undo steps, as it is in a collaboration, and a caret that lands where the last edit ended still groups. Undoing a formatting change no longer collapses positions inside the range, so collaborators' carets, highlights and comment ranges stay where they were.
  • Undoing formatting is now a step of a new kind, restoreMarks, in step JSON; a host that relays steps between clients of different versions needs the same version on both ends, as older versions cannot read it. This is the one change to the step wire format.
  • A legacy block beside a real block named like one of its derived ids, such as a beside a-2, is diffed under the same ids the editor gives its pieces, so the diff no longer reports a deletion, a change and an insertion for an unchanged document.
  • A codec that has loaded a document with such a real block gives the same ids to a later document without it as a new codec does.
  • Typing right after a local change that is not recorded and moves no text, such as opening a toggle or an upload result filling an image, is its own undo step instead of joining the step before it. A remote or reconciled change of that kind leaves the step open.
  • diffBlocks reports the word changes of a block that moved and was edited: its insert now holds a change, as an edited block in place is reported. QuoinDiff showed them already and is unchanged.
  • A change inside Chinese, Japanese or Thai text, written without spaces, marks only the changed words where the runtime has Intl.Segmenter, where it marked the whole run.
  • Diffing two versions that differ in more than 2,000 blocks, or two texts in more than 2,000 words, reports the older version as deleted and the newer as inserted, whole and with no block marked as moved, so the time and memory stay bounded: two documents of 5,000 blocks with nothing in common diff in a fraction of a second, where it took seconds and hundreds of megabytes.
  • On a read-only canvas whose text a page translator or another script rewrote, a drag across blocks no longer throws on every pointer move.
  • Text a script or a browser extension rewrites in one block while a composition runs in another reaches the document when the composition ends, where the document kept the old text while the page showed the new one.
  • Command with Up or Down on a Mac, and Control with Home or End elsewhere, go to the start or the end of the document, where they stopped at the edge of the block. These keys also leave a selected divider, image or other block for that edge, where on a Mac Command with Up or Down did nothing. Shift with Control+Home or Control+End extends the selection there; on a Mac, Command with Shift and Up or Down moves the block where it can, and extends the selection to the edge where it cannot, such as in the first block. Shift with Up or Down across blocks keeps the caret's horizontal position, where it kept the character offset and drifted sideways between a heading and a paragraph.

Added

  • BlockCodec.toEngineBlocks(block, taken?) takes the ids of the other blocks of the document, so the ids it gives the pieces of a legacy block avoid theirs.

1.1.2 (2026-10-30)

Fixed

  • A block whose content returns to what was stored is reported as the stored object, whether by undo or by retyping, so getBlocks() and onChange give back the host's own object. A block written in a legacy form, such as *old style*, is not rewritten as _old style_, and an intact 0.4 multi-paragraph group is emitted as the original block.
  • A mark that spans more of the text is written outside the marks it contains, so nested emphasis such as _a **b** c_ is stored without HTML tags in more cases. Inline code stays innermost.

1.1.1 (2026-10-29)

Fixed

  • An inline decoration no longer writes an event-handler attribute: any name starting with on, in any case, is left out, while class, style, data-* and aria-* are kept. Widget decorations write no attributes, and block decorations keep their data- and aria- rule.
  • A custom block's preview no longer draws again when a block is inserted, removed or moved above it: only an open editor, which receives index, follows the block's position.
  • Mod+Z, Mod+Shift+Z and Mod+Y, where bound, pressed on a control in a custom block's preview, such as a checkbox, undo and redo the document as they do on the block itself, where Chromium and WebKit did nothing. A text field in the preview (an input that takes text, a textarea or a contenteditable element) keeps its own undo.
  • A block whose renderer threw is drawn again when its renderer changes, as when the host passes a fixed renderer in renderers or new blockTypes, where it waited for its data to change.
  • A click on the fallback of a text block whose renderer threw selects the block's text as a whole, as a block selection of that one block rather than a node selection, so Delete removes it, where it put a caret in a block with no text drawn to hold it.
  • The sourcemaps of the client entries ('use client' at the top of the file) map every line again: the directive sits on a line of its own and the map starts with an empty line, where 1.1.0 put it on the first line of code and left the first line's columns off by the length of the directive.

Changed

  • The "No alt text" badge on an image is named "No alt text, add alt text": its visible text first, then what it opens, where it was named by its visible text alone. Its visible text is unchanged. New label addAltText, optional in QuoinLabels and English where missing. A host that sets noAltBadge and not addAltText keeps the 1.1.0 name, its translated visible text alone.
  • A host renderer for a text block that draws no EditableRegion for its text has its text selected as a whole by a click, as a block selection of that one block rather than a node selection, as a block whose renderer threw does. ArrowUp and ArrowDown pass over such a block since 1.1.0, which put the caret in text nothing showed before; the headless page now says so.

1.1.0 (2026-10-29)

Added

  • A custom block's editor receives blockId, index (the block's index in the flat blocks array, counting from 0 with nested blocks included, kept current as blocks move; only a block whose index changed draws again) and onRemove, which removes the block as one undo step and puts the caret where deleting the selected block puts it. A preview receives onChange on an editable canvas, written like an editor's: writes to one block that each follow the last within the history's grouping window (500 ms by default) undo together, so it can hold a control such as a checkbox, and a write made while focus is on the block leaves it selected as a whole; a read-only canvas and QuoinView pass none. The canvas always passes all of these, and always passes a preview its blockId. They are optional in BlockEditorProps and BlockPreviewProps, so an editor rendered directly with only data and onChange still compiles.
  • A block definition takes an optional parse, which reads a block's stored data into the shape its editor and preview take, to fill required fields and migrate older shapes. The canvas, QuoinView and QuoinDiff hand its result to editor and preview; it runs when the block is drawn, once per stored data, and writes nothing, so the stored data changes only when the editor or preview calls onChange. A block inserted without initialData is seeded with parse({}), so parse should accept {}; when seeding throws, nothing is inserted and the error goes to the canvas's onError, or to a warning in development without one. When parse throws while drawing, the canvas draws the block's error fallback and reports the error to onError, and QuoinView and QuoinDiff draw the block without its preview and warn once per block type in development. initialData stays optional; the custom blocks page recommends parse for data with required fields and shows a versioned migration.
  • An image can be marked as deliberately decorative: its data takes decorative: true (stored only when true, kept by lossless markdown and quoin-editor/yjs like alt), and the alt text popover has a "Decorative image" checkbox that stores it and clears the alt text, and hides the hint that calls the alt text required while checked; typing alt text unchecks it. The popover closes, storing nothing, when the alt text or the flag changes under it. An editable canvas draws a "No alt text" badge, which opens the popover, on an image with an empty alt text that is not decorative; a read-only canvas, QuoinView and toHTML draw none. Output is unchanged: both cases are written with alt="". As decorative is now a known key of image data, a value other than true is no longer kept, where 1.0 kept it as an extra field. New labels decorativeImage and noAltBadge.
  • The labels added in this release, blockError, decorativeImage and noAltBadge, are optional in QuoinLabels and read as English where missing, so a complete translation typed as QuoinLabels still compiles. englishLabels is typed Required<QuoinLabels>.

Changed

  • The default --quoin-warning and --quoin-input reach 3:1 where they did not. Light --quoin-warning goes from oklch(0.65 0.16 65) to oklch(0.6 0.16 65), the rule of a warning callout against its wash from 2.84:1 to 3.40:1 (dark stays at 7.82:1). --quoin-input, the border of the outline button, goes from oklch(0.922 0 0) (1.26:1 on the page) to oklch(0.62 0 0) (3.64:1) in light, and from oklch(1 0 0 / 15%) to the opaque oklch(0.56 0 0) (4.25:1 on the page, 3.85:1 on a popover) in dark. A host that sets these tokens keeps its own values.
  • The coverage gate in CI now also covers the canvas and the useQuoin and useLabels hooks, with line thresholds of 95 percent on src/view, 97 on src/blocks, 98 on src/ui, 98 on src/context.tsx (useQuoin) and 98 on src/i18n (useLabels), next to the engine's 90.
  • The build runs each output chunk through Rollup's tree shaking after esbuild, which merges a chunk's imports and drops arguments no caller passes: the main entry goes from 155,039 to 152,741 B gzip and quoin-editor/headless from 70,129 to 69,824 B, and the main entry with any feature entry is about 2.2 KB smaller. The 'use client' directive is on the same files as before, and the engine, view, diff, docx and yjs entries still carry none. The sourcemaps leave out a segment that maps to the same place as the one before it on its line, which keeps the place a greatest lower bound lookup finds, the one browsers and Node's SourceMap.findEntry make, and keeps the tarball about 1 MB packed; a least upper bound lookup or a list of every generated position for a source place can differ. The build fails on mappings that hold a character that is not a base64 digit.

Fixed

  • The attributes an extension's block decoration puts on its block's element are filtered to the names that start with data- or aria-: a decoration with style, className, an event handler or dangerouslySetInnerHTML no longer reaches the element, and can no longer make React throw.
  • A renderer that throws while drawing its block no longer unmounts the whole canvas: that block draws a fallback named by the new blockError label (Block could not be drawn) in its own element, the other blocks keep working, the error goes to onError when one is given, and React reports it as it does any caught error, and the block is drawn again when its data changes. EditorView takes blockErrorLabel and onBlockError for the same.
  • ArrowUp and ArrowDown pass over a text block whose renderer threw, rather than putting the caret in a block that has no text to hold it.

1.0.2 (2026-10-27)

Fixed

  • A task's checkbox and an embed's frame are never left with an empty or blank name. In the canvas, a task label that is empty or only white space names the checkbox Task, and a frameTitle label that returns one titles the frame with its provider's name, or with its page's address when the provider has no name. toHTML, QuoinView and QuoinDiff treat a taskLabel, an emptyTask label, a frameTitle or a provider name of only white space as empty too, rather than writing it as the name.
  • With quoin-editor/yjs, a collaborator's change to a block's type reads only that block again from the shared document, rather than the whole document, as does a collaborator's change to the rows or columns of a table, or any other change that only puts in or takes out the start of a region: at 20,000 blocks a type change takes about 8 ms, against about 75 ms before, close to the 5 ms of a collaborator's typing. A block that concurrent edits left out of shape still reads the whole document, as before.
  • With quoin-editor/yjs, the caret stays on its character when a collaborator's change reads blocks of the shared document again, such as turning the paragraph above it into a code block: it was put back by the layout from before the change and could jump within its block or into another one.
  • With quoin-editor/yjs, a change another writer makes to the id in a block's marker in the shared Y.Text is read: the editor takes the document as the text now reads, where it kept showing the block under its old id.
  • With quoin-editor/yjs, content in the shared Y.Text that the Y.Text API never writes, such as values put in through Y.Array on the same name, counts as one skipped embed per value, rather than being dropped, so the positions the editor holds match those of Yjs.

Known limits

  • In the canvas, a task's text box still takes a task label that is empty or only white space as its name.

1.0.1 (2026-10-26)

Added

  • toDocx takes an optional baseUrl, the address of the page the document comes from: a link or a file whose address is a relative path, such as /files/a.pdf or notes/a.pdf, is linked to that address read against it, rather than written as its text. A network path (//host/x, \\host\share, /\host) and a fragment such as #top stay text. A base that is not an absolute http or https address is ignored, a user and password in it are left out of the links, and an empty link stays text.
  • commentsExtension takes an optional initialLayout: 'wide' | 'narrow', the layout drawn on the server and in the first render before the canvas is measured, so a server-rendered wide canvas no longer shifts by 236 px as it hydrates. The default stays 'narrow'.

Changed

  • Transaction and ViewController keep their private state in native private fields, so a proxy around one of them, such as Vue's reactive or Valtio's proxy, cannot read it and its methods throw: wrap the object that holds them, not these instances.
  • The transaction that moves the caret to where text is dropped has the drop origin, rather than command.
  • Docs: chains merge the meta of every part, so a key one part set is seen on the whole chain (extensions page).
  • Docs: two clients can hold a table's alignment as no align or as align: [null, null], which the editor and QuoinDiff treat as equal; a host comparing saved JSON should normalise align (collaboration page).
  • Docs: typing into a block whose region another edit took out can bring along text waiting before it, which then shows one block higher on every client (collaboration page).
  • Docs: edits a headless editor makes while unmounted stay local and are not sent when it connects again (collaboration page).
  • Docs: the onError of yjsCollaboration is the adapter's own, apart from the canvas's; pass both the same handler (collaboration page).

Fixed

  • Under a collaboration, editor.chain().undo().run() and editor.chain().redo().run() undo and redo when the chain holds nothing else, rather than doing nothing. A chain that holds other commands as well still does nothing.
  • A canvas rendered on the server, as with react-dom/server, leaves out a code token's color that holds what could leave the declaration or load a resource (any of ; { } \ " ' < > !, a /*, or url(, image(, image-set( or expression(), rather than writing a value such as red;background-image:url(...) into the page as it is, and the canvas in the browser leaves out the same values. Other colours, such as var(--name, #fff), hsl(var(--primary)), color-mix() and light-dark(), are drawn by the canvas as in 1.0, and now by QuoinView too, which uses the same rule.
  • toHTML, QuoinView and QuoinDiff no longer write an empty accessible name: an empty taskLabel names a task with no text Task (in QuoinDiff, the emptyTask label, and Task when that is empty too), and a frame whose frameTitle is empty is titled with its provider's name, or with its page's address when the provider has no name.
  • toDocx writes a link to an http or https address with no host, such as http:foo or http://?x, as its text, as it does other targets Word cannot open, rather than as a hyperlink.
  • With presence, a long collaborator name at the end of a line is cut off where the avatars' gutter begins, rather than over the avatars.
  • With presence, a collaborator's caret near the side of a wide table, and its name, are cut off at the table's side as the table's text is, rather than drawn whole past it.
  • With presence, a collaborator's caret, and with quoin-editor/ai an answer, in a scroller that sits inside another box that clips what overflows, such as a table in a custom block, is clipped by both, rather than only by the nearest one.
  • With presence, a collaborator's caret in a wide table moves with the table as it scrolls, rather than a frame behind it.
  • With quoin-editor/ai, the room kept under a block for its answer no longer adds to the block's bottom padding or to the row gap of a flex or grid canvas under a block another block follows: the answer takes that space first, and the room makes up only the rest. Padding the block paints, with a bottom border or a background, is left to the block.
  • With quoin-editor/ai, an answer growing above the part of the page being read no longer pushes that text down in a browser without scroll anchoring, such as Safari, or on a page that turns scroll anchoring off, smooth scroll-behavior or not, and in a canvas that scrolls itself.
  • With quoin-editor/comments, a thread just resolved or deleted no longer comes back for a moment when your store sets new copies of threads that have not changed before it records the resolution. It comes back once your store, having resolved or removed it, reopens it.
  • With quoin-editor/comments, a selection your code sets, as a command or an AI answer does, no longer closes a thread a click opened, "Text removed" included: only the writer's own caret moves close it, an undo, a redo and a drop included.
  • With quoin-editor/comments, a thread a click opened closes once the writer moves the caret out of its text, rather than staying open over the lines they go on to type.
  • A canvas drawn inside another canvas's tree owns its own text, menus and popovers: focus in them no longer counts as focus in the outer canvas, which kept the inner canvas's presence, comments and AI from seeing it as their own and let the outer canvas's go on as if focused.
  • With quoin-editor/yjs, a row two people add to a table at the same moment, followed by a move of the table and a new column, no longer has one client write cell text at the wrong place, which split it across cells for the others.
  • With quoin-editor/yjs, deleting a table while someone adds or removes one of its rows or columns no longer leaves hidden regions in the line after it, over which Enter threw: the cell text shows at the end of that line.
  • Presence on a second editor given a yjsCollaboration that already serves one, which stays unconnected, no longer writes to the awareness: unmounting it no longer takes the person and their selection out while the first canvas is still mounted.
  • An editor that disconnects from a yjsCollaboration and connects again within a frame, as StrictMode or a quick remount does, keeps what it can undo and redo. A collaboration left disconnected longer lets its undo history go, as in 1.0.
  • yjsCollaboration reports to its onError a change from the shared document that does not apply to the editor, as the error applying it raised, before the editor takes the document as the shared text reads, rather than recovering in silence.
  • A block that a collaborator's change leaves selected as a whole comes into view with the writer's next change, rather than only once the selection leaves it and comes back.
  • A collaborator's caret move drawn in the same render as a change from your code, such as a command that selects a block below the fold, no longer keeps that change from scrolling it into view. A collaborator's change to the document still scrolls nothing.
  • A table's row or column menu stays open while a collaborator types in its own row or column. It still closes when a change from elsewhere moves that row or column.
  • Two people changing one block's type at the same moment no longer hide the block's text: text left in a region the winning type does not have shows in that type's own region.
  • A caret in a block that a new blocks array changes on both sides of it stays on its character, rather than moving by the length of the later change.

Known limits

  • A task's checkbox and an embed's frame in the canvas still get no name when a host's task or frameTitle label is empty or blank, and a taskLabel or frameTitle of spaces only gives toHTML, QuoinView and QuoinDiff a blank name.
  • With presence, a collaborator's caret can stay off its character after a style change that reflows the text without resizing its block, until the next change, scroll or resize.
  • With quoin-editor/yjs, a table cell whose region concurrent row and column edits took out cannot be selected or typed into until the table is rewritten, as by adding or removing a row or a column. This is as in 1.0.0.

1.0.0 (2026-10-23)

Quoin 1.0 replaces the 0.4 canvas, a card per block with a textarea opened to edit it, by an editor of its own: an engine with no dependency, a view that types into the page in place, and a canvas UI on top. A 0.4 document loads as it is, and the 0.4 BlockCanvas props keep their names. MIGRATION.md lists what a 0.4 host changes.

Added

  • quoin-editor/engine: the editing engine alone, with no React, no DOM and no 'use client' banner. A document model of blocks with inline regions, steps, transactions and history, commands (editor.commands, editor.chain()), input and paste rules, and a markdown codec that reads a 0.4 document as it is and writes back only the blocks that were edited. Also toMarkdown, fromMarkdown, toHTML, toPlainText, fromHTML, diffBlocks and countDoc.
  • quoin-editor/headless: the engine and its editable view, with no canvas UI: useQuoinEditor, EditorView, EditableRegion and ViewController. Each text region is typed into in place, with IME composition, native caret movement, selections across blocks and the clipboard handled by the view.
  • quoin-editor: BlockCanvas rebuilt on the engine. Paragraphs, headings, bullet, numbered and task lists, quotes, callouts, toggles and dividers are typed in place, with live formatting from markdown shortcuts (**bold**, # , - , > and the rest). Code, image and file blocks are drawn and edited inside the canvas. The canvas adds a slash menu, a bubble toolbar for formatting and links, a link popover, a gutter with a drag handle and a "+" that adds a block below, a block menu (Turn into, Color, Duplicate, Copy, Copy link to block, Move up, Move down, Delete) reached from the handle, a right click on the gutter or Mod+/, drag and drop with a drop line, text and background colours, text alignment, a toolbar for touch screens and a keyboard shortcuts dialog (Mod+Shift+/).
  • BlockCanvas props extensions, highlight, autoFocus (off by default), placeholder, labels and blockLink, and a ref whose QuoinEditorHandle gives the editor, the controller, focus() and toMarkdown().
  • defineExtension for extensions of your own: blocks, marks, atoms, commands, keymaps, input and paste rules, plugins, decorations, transaction hooks, and a ui field (ExtensionUI) that adds slash items, bubble items, block menu items, block renderers and overlays to the canvas.
  • QuoinLabels, englishLabels, resolveLabels and useLabels: every string the canvas shows or speaks, English by default and replaced key by key through labels.
  • CodeHighlighter and CodeToken, for the highlight prop that colours code blocks.
  • The optional indent field of QuoinBlock, the nesting depth of list items and of the blocks inside a toggle.
  • The engine reads and writes the DIVIDER, TOGGLE, TABLE, EMBED, BOOKMARK, MATH and TOC block types, beside the five 0.4 types.
  • Opt-in features, each an entry of its own that a page loads only when it imports it:
    • quoin-editor/table: tables typed cell by cell, with row and column menus, column widths and cell ranges.
    • quoin-editor/embed: embeds framed from YouTube, Vimeo, Loom, Figma, CodePen, Spotify and SoundCloud, and bookmark cards, with an unfurl option of your own.
    • quoin-editor/math: math blocks and formulas in a line, drawn by your renderMath, such as KaTeX.
    • quoin-editor/toc: a table of contents block that follows the headings.
    • quoin-editor/mention: @ mentions, found by your own search.
    • quoin-editor/emoji: a : menu and an emoji picker, whose data loads the first time one opens.
    • quoin-editor/find: find and replace.
    • quoin-editor/stats: word and character counts, a limit, focus mode and typography rules.
    • quoin-editor/catalogue: catalogue(), every feature above at once.
  • Platform entries:
    • quoin-editor/yjs: real-time co-editing through a Yjs document and your own provider, with an undo of your own changes only. yjs is an optional peer dependency.
    • quoin-editor/presence: collaborators' selections, carets and avatars.
    • quoin-editor/comments: comment threads anchored to text, kept in your store.
    • quoin-editor/ai: answers of your model streamed into the document; Quoin never calls a model itself.
    • quoin-editor/view: QuoinView, read-only output with no hooks, for a server component.
    • quoin-editor/diff: QuoinDiff, the changes between two versions of a document, block by block and word by word.
    • quoin-editor/docx: toDocx, a Word document written in a browser, a worker or Node.

Changed

  • Text blocks are typed in place instead of in a textarea that a click opens. Undo and redo work across the whole document, and the caret moves from block to block with the arrows.
  • A new text block holds one line: a paragraph, a heading, a list item, a task or a quote. A 0.4 MARKDOWN block holding several paragraphs is kept as it is until it is first edited; it is then split into one block per line, the first keeping its id and the others taking ids derived from it (<id>-2, <id>-3 and on), each written back as the canonical markdown of its line. A table in it becomes a TABLE block and an image alone on its line an IMAGE block.
  • Nested list items keep their depth in indent rather than in leading spaces of their markdown.
  • An image's caption and a callout's body are inline markdown, typed in place like a paragraph.
  • The core blocks write new optional keys into data once a writer uses what they hold: align, color and background on MARKDOWN, icon on CALLOUT, url, align and widthPercent on IMAGE, and mime on FILE, beside indent on the block itself. Every 0.4 key keeps its name and meaning.
  • QuoinConfig, which useQuoin() returns, gains highlight and the required controller and labels. QuoinProvider fills them in, so only a QuoinConfig built by hand, such as a test double, needs them.
  • A structural copy of the document coming back is recognised as an echo within the last 256 changes the canvas reported, instead of 64.
  • onDelete also reports whole blocks removed by typing over a selected block or pasting over selected blocks. A line that Backspace or Delete merges into its neighbour is still not reported.
  • defineBlock takes any icon component that accepts a class name and aria-hidden, not only a lucide one. A custom block shows its preview at rest; its editor opens on a double click, its Edit button or Enter on the selected block, and Escape closes it. A definition for DIVIDER draws everywhere, and one for MARKDOWN, CALLOUT or TOGGLE only on a read-only canvas, which warns in development when the canvas is editable.
  • quoin-editor/headless exports the view and the engine, and formatShortcut is exported from the main entry only. The main entry re-exports everything in quoin-editor/headless.
  • A canvas given a collaboration extension is uncontrolled while the shared document is connected: it ignores any blocks array it did not emit, and warns in development.
  • The stylesheet declares color-scheme in light and dark, and reads new tokens: --quoin-selection, the text and background colours --quoin-color-* and --quoin-bg-*, --quoin-comment, --quoin-comment-active and --quoin-comment-edge, --quoin-diff-inserted and --quoin-diff-deleted, and --quoin-link. Every 0.4 token keeps its name.
  • English callout tone names are written in lower case in the tone chip and its menu.
  • One build with code splitting serves every entry, in ESM and CommonJS, and each entry is held to a gzip budget in CI.
  • The tarball is about 1 MB packed, under 5 MB unpacked: the sourcemaps leave out sourcesContent and name their sources relative to the map. They still map each line to its file and line under src/. Every entry is exported, and the package publishes dist, the README, the license, this changelog and MIGRATION.md.
  • @radix-ui/react-context-menu is a new dependency, for the block menu on a right click.

Removed

  • The 0.4 textarea helpers, from both quoin-editor and quoin-editor/headless: applyEdit and Edit, lineAt, toggleLinePrefix and LinePrefix, continueList, indentList, wrapSelection, linkPaste and WrapKind, insertCommand and InsertableCommand, shouldOpenPalette, caretRect and CaretRect, and useMarkdownKeymap. Engine commands do their work; MIGRATION.md maps each one.
  • TEXT_COMMANDS and TextCommand: the slash menu's items come from the canvas and from extensions.
  • useBlockList, with BlockList and UndoToken: the editor's own commands and history cover it.
  • useAutoGrow: nothing is drawn in a textarea any more.
  • formatShortcut from quoin-editor/headless (it stays in quoin-editor).
  • The dependencies @dnd-kit/core, @dnd-kit/modifiers, @dnd-kit/sortable and @dnd-kit/utilities (the canvas drags blocks itself), cmdk (the slash menu is the canvas's own), tailwind-merge, class-variance-authority, @radix-ui/react-label and @radix-ui/react-select.

Fixed

0.4.1 fixed F-01, F-03, F-04, F-05, F-06, F-07, F-08, F-10, F-16, F-18, F-19, F-24, F-25, F-27, F-39 and F-40, listed under 0.4.1 below. 1.0 closes these; findings it only partly addresses are under Known gaps:

  • F-02 [ARCH-1, CQ-12]: the card and textarea model is gone. Text is typed in place on one engine, with one history for the whole document and the caret moving across blocks.
  • F-09 [CQ-4, PERF-1]: a keystroke re-renders the one block it changes, since each block shell is memoised on its block (src/view/react/BlockShell.tsx). e2e/tests/headless/performance.spec.ts holds typing in a 1,000 block document to a p95 of 16 ms on a development machine, and to a ratio against a small document in CI.
  • F-13 [PKG-4]: the README no longer calls the headless entry safe for a server component; it carries a 'use client' banner, as the main entry does. quoin-editor/engine, /view, /diff, /docx and /yjs carry none. npm run check:directives checks the banner of each entry, and the consumer check renders the view, the diff and the Word export in plain Node.
  • F-15 [TEST-1]: CI runs Playwright on Chromium, Firefox and WebKit, typechecks the packed package and resolves each of its entries on React 18 and 19, renders the server entries in plain Node, and the release workflow runs the consumer check before publishing.
  • F-17 [A11Y-1]: Mod+Shift+Arrow and the block menu move a block one place, and every move, by keyboard or by drag, is announced with its new position (e2e/tests/canvas/shortcuts.spec.ts, drag.spec.ts).
  • F-21 [A11Y-8]: the slash menu is a combobox with aria-activedescendant from the moment it opens, and its result count is announced.
  • F-22 [A11Y-9]: the slash menu opens from the text typed, not from a key press, so a virtual keyboard or dictation opens it too.
  • F-23 [A11Y-10]: an upload in flight no longer disables the focused file input, and one status region, mounted from the start, announces its progress.
  • F-26 [CQ-11]: BlockCanvas.tsx and BlockCard.tsx are gone; the canvas file wires the engine, the view and the UI in 159 lines.
  • F-28 [CQ-14]: one suggestion menu serves the slash menu and every other menu typed in the text, and the shortcuts dialog reads one table, which scripts/keyboard-docs.test.ts checks against the keymaps: the dialog lists only keys the code binds, and every key the canvas's own keymaps bind.
  • F-29 [CQ-15, PKG-5]: the badge, label and select primitives are gone, with @radix-ui/react-label and @radix-ui/react-select.
  • F-31 [CQ-17]: toggleLinePrefix is gone with the textarea helpers.
  • F-32 [CQ-18, PKG-3]: one build with code splitting, so quoin-editor and quoin-editor/headless share each export, which src/index.test.ts checks against the built files.
  • F-34 [A11Y-7]: the editor is one tab stop, held by the block the selection is in. The gutter's controls are left out of the tab order, Mod+/ opens the block menu from the text, and the arrows move between blocks.
  • F-35 [A11Y-12]: every block menu item is named by the text it shows (src/gutter/BlockMenu.test.tsx).
  • F-36 [A11Y-14]: the canvas no longer takes focus on mount unless autoFocus is set.
  • F-37 [A11Y-16]: nothing animates or scrolls smoothly when the reader asks for reduced motion (e2e/tests/canvas/reader-settings.spec.ts).
  • F-38 [A11Y-17]: on a touch screen the gutter shows one 44 px handle beside the block being edited, and a toolbar above the keyboard carries the editing tools.
  • F-41 [A11Y-21]: the shortcuts dialog lists the canvas's keyboard model, the keyboard docs page adds the keys of the opt-in features, and the Accessibility docs page names what each test suite checks and what it does not.

Security

  • toDocx links a hyperlink only when its address is an absolute http, https, mailto or tel one; any other address, relative ones included, is written as plain text, since Word reads a relative address against the file's folder and a \\host\share address as a network share.
  • QuoinView and QuoinDiff write the colour of a highlight token into a style attribute only when it is one colour value (a hex colour, a keyword, a colour function or a custom property), so a token cannot add a declaration of its own.

Known gaps

Findings of #73 that 1.0 addresses only in part. Each part still open stays open in #73; none has a follow-up issue yet.

  • F-11 [API-1]. Fixed: extensions add commands, keymaps, input and paste rules, plugins and canvas UI through defineExtension, and BlockCanvas takes a ref. Remains: a block's preview still cannot write its data, and BlockEditorProps gained no blockId, index or onRemove.
  • F-12 [PKG-2]. Fixed: dnd-kit, cmdk, tailwind-merge and class-variance-authority are gone, features beyond the core blocks are entries of their own, and npm run check:size holds every entry to a budget. Remains: the build turns tree shaking off, since Rollup would strip the 'use client' banners, and leaves it to the host's bundler.
  • F-14 [API-4, A11Y-15]. Fixed: QuoinView renders a document on a server, with real headings, lists and links, the canvas draws text formatted instead of raw markdown, and toHTML and QuoinView draw an image as a figure with width, height, loading="lazy" and its caption, and a file as a link. Remains: an empty alt still marks an image decorative silently, with no choice to make it so on purpose.
  • F-20 [A11Y-6]. Fixed: a callout's tone is in its accessible name ("Callout, warning"), and toHTML writes a callout as <aside role="note" data-tone>, so colour is no longer the only cue. Remains: the light --quoin-warning keeps its 0.4 value, so the warning callout's rule keeps the contrast the review measured, 2.84:1 against its wash.
  • F-30 [CQ-16]. Fixed: ESLint with typescript-eslint and react-hooks runs in CI, with a coverage gate of 90 percent of lines on the engine. Remains: no coverage threshold on the canvas or the hooks.
  • F-33 [API-3]. Fixed: icon accepts any component. Remains: initialData stays optional when the data has required fields, and a definition has no parse or migrate.
  • F-42 [A11Y-22]. Fixed: color-scheme is declared in light and dark. Remains: --quoin-input keeps its 0.4 values, which the review measured at 1.26:1 in light and 1.47:1 in dark for field borders.

0.4.2 (2026-10-15)

A metadata-only patch. The code is the same as 0.4.1 and nothing in the public API changes.

Changed

  • The repository moved to AmineSeboui/quoin-editor, so the repository, homepage and bugs fields of the package, and the README badges and links, point at the new repository and its documentation site, https://amineseboui.github.io/quoin-editor.
  • This version is published with npm provenance from the new repository. The provenance of earlier versions still names the former repository, which no longer exists.

0.4.1 (2026-10-12)

A patch release that fixes the data-loss, accessibility and React 18 problems found in a review of 0.4.0. Nothing is removed from the public API.

Fixed

  • useAutosave saves the newest edit when its component unmounts, and when the page is hidden or closed (pagehide, or the tab turning hidden), instead of dropping up to one debounce interval of typing (F-01).
  • useAutosave keeps one save in flight. Edits made while it runs are sent in a single follow-up save with the newest payload, so an older payload can no longer land last (F-05).
  • A save that fails after useAutosave has unmounted no longer schedules retries nobody can stop (F-06).
  • A save that fails for good no longer retries forever: retries stop after maxRetries (default 8) or when isRetryable says no, status becomes failed and error holds the reason. A success clears the error and any pending backoff (F-05).
  • An image block records its storageKey as soon as the upload finishes, before the picture is measured. With overlapping uploads the last pick wins, and a late measurement never overwrites a newer image. Replacing an image drops the previous image's width and height, so a picture that cannot be measured is never stored with another picture's size (F-07).
  • UploadField waits for an async onUploaded and reports its rejection through onError, once. The "Image attached" text no longer carries a check mark that screen readers read out (F-25).
  • The markdown surface and the empty-document starter ignore keys that belong to an IME composition. Committing a candidate with Enter no longer continues a list, and Backspace, Tab and / inside a composition no longer exit a list, indent or open the palette (F-08).
  • On macOS the shortcuts use Command, so Ctrl+B, Ctrl+E and Ctrl+K keep their system text meanings. Elsewhere they use Ctrl, as before (F-27).
  • A lagging host that echoes the canvas back no longer rewinds it. An array the canvas emitted is recognised however late it comes back, so the limit of 64 unacknowledged emissions is gone, and a structural copy of one of the last 64 counts as its echo. Stores that clone the document on the way through, such as Redux, Zustand or a server round trip, keep every keystroke while they are within 64 edits (F-10).
  • Keyboard focus is visible on buttons, inputs, textareas, selects, the dialog close button, the markdown surface and the starter: a solid 2 px outline in --quoin-ring, which reaches 3:1 against the page and popover in light and dark, and which Windows forced colours mode keeps (F-04, F-18).
  • A highlighted row in a menu, the slash palette or a select carries an inset bar as well as a background, and the Delete label and shortcut hints reach 4.5:1 on it (F-19, F-40).
  • React 18 works: Button, Input and Textarea forward refs, so the refs that focus return, the drag handle and the menu triggers rely on are no longer dropped, and layout effects no longer run during server rendering. CI now checks the packed package on React 18 and 19 (F-03).
  • Inserting a block focuses and scrolls to it inside its own canvas, so two canvases on one page no longer interfere (F-16).
  • The last inserted block no longer reopens, and takes focus, when readOnly is toggled or the host undoes a change (F-24).
  • Inserting a block from the slash palette closes the source block's editor, so one editor is open at a time (F-39).

Changed

  • useAutosave gains the 'failed' status, the maxRetries, isRetryable and flushOnHide options, and error and flush() on its result. When save resolves { ok: false }, that value is the error.
  • UploadFieldProps.onUploaded may return a promise.
  • useAutoGrow is typed to return React.RefObject<T>, so the ref it returns is accepted by React 18 and 19 types.
  • Default tokens: light --quoin-ring is darker, dark --quoin-ring is lighter, and light --quoin-destructive and --quoin-muted-foreground are slightly darker. A host that maps --quoin-ring to a pale colour should give it one that reaches 3:1.
  • A host that does not echo onChange and restores a copy of a state from its last 64 edits now sees it ignored as a late echo. Remount the canvas with a React key to force the reset.
  • The package description, keywords and author are updated.

Docs

  • The README no longer warns that a store which clones the document loses text.
  • New Accessibility and Stability sections, which name the gaps that remain in 0.4.
  • --quoin-ring is documented.

0.4.0 (2026-10-12)

Breaking

  • Each callout tone draws in a colour of its own. New tokens: --quoin-info, --quoin-success and --quoin-warning.

0.3.0 (2026-10-11)

Answers an integrator's report from 0.1.1.

Added

  • The core block data shapes are exported (MarkdownBlockData, CalloutBlockData, CodeBlockData, ImageBlockData, FileBlockData, CoreBlockData) and documented, with a test that reads the keys back from each editor.
  • A block definition may omit editor for content that is never edited in the canvas; omitting both editor and preview is a type error.
  • A read-only canvas warns once per type, in the developer console, when a definition has no preview.

Changed

  • useAutosave's save may return anything; only an explicit { ok: false } (or a rejection) counts as a failure.
  • defineBlock defaults its data type to Record<string, unknown>.
  • A block whose type nobody registered no longer offers an Edit button.

Docs

  • Tailwind v3, v4 and no-Tailwind setups, and the install footprint.

0.2.0 (2026-10-10)

Breaking

  • Every CSS custom property is prefixed --quoin-.
  • All of the package's CSS ships under one top-level quoin cascade layer.
  • Tailwind's theme is imported by reference, so its tokens no longer land on the host's :root.
  • Utilities are built from the package's components only, not from the README prose.

Added

  • onDelete tells the host which block a deletion removed and the index it held.
  • UploadField and FilePickerButton are exported for custom blocks.

Fixed

  • Builds under Tailwind v3 hosts.

0.1.1 (2026-10-09)

Fixed

  • package.json is exposed through the exports map, so tooling can read the manifest.
  • The headless surface is re-exported from the main entry.
  • The caret lands where a click fell instead of at the top of the block.
  • Every emission a lagging host has not echoed yet is recognised, so delayed onChange handling no longer loses typing.
  • A block type nothing registered is drawn and reported through onError.
  • useBlockList caps the removals it keeps for undo.

Changed

  • The markdown commit the keymap is keyed on is memoised.

Docs

  • A custom block reaching upload through useQuoin, and a host token collision with its escape hatch.

0.1.0 (2026-10-09)

First release of Quoin, published on npm as quoin-editor (npm rejects the bare name quoin as too similar to an existing package).

Added

  • BlockCanvas: a controlled React block editor (blocks and onChange) with drag to reorder, block inserters, a block actions menu and a read-only mode.
  • Five core block types: markdown, callout, code, image and file, plus an open registry through defineBlock with plain React editor and preview components.
  • A markdown surface with keyboard shortcuts, list continuation, indentation and a slash palette.
  • Host-owned uploads through an injected upload function and resolveAssetUrl.
  • quoin-editor/headless: the markdown text transforms with no UI dependency.
  • Scoped styles with a class-based dark mode.
Esc

Type to search the guide, the comparison and the changelog.