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 leftadded it before this one in the document, which the page then drew on the right, andMove column leftmoved 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
dirattribute. This adds@radix-ui/react-directionto 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
infoin 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 intodata-toneand 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,colorsandcalloutTonesare 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. englishLabelsis 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
useAutosaveasisRetryablethat 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
Shakaof the call me hand or theZAof 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
catwithcatchanged 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/PNGnow 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:, nothttps://.ada@example.comwas completed tohttps://ada@example.com, a link to the hostexample.comcarryingadaas a user name. - An extension whose
uiholds 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 inslashItemsand the canvas spread it into the slash menu, and a value of any other shape reached aflatMapthat 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 itquoin-editor/headlessandquoin-editor.BlockShellandSelectionSink, with theBlockShellPropsandSelectionSinkPropsthat typed them, are drawn byEditorViewitself and register themselves with the controller, so a second copy of either takes the registration from the first.VoidBlockFallbackis what a block shell already falls back to when no renderer draws a block type, so a renderer table that names it changes nothing.defaultRenderersheld the three renderers a canvas already reaches for on its own; a renderer of your own draws its text withEditableRegion, which is what those three do.isCrossSelectionanswers whether a text selection spans blocks or regions, which is what painting and the arrow keys ask; a host that wants the answer readsanchorandheadof the publicSelection, and were it to earn a place it belongs besidetextSelectioninquoin-editor/enginerather than in the React entry.BlockRenderPropsandBlockRendererstay, 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.scrollIntoViewis 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 aTypeErrorthat 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. TwoscrollTocalls 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,IMAGEorFILEreplaces the preview that edits it and brings no editor of its own. Quoin edits those three in their preview rather than through aneditor, 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 coveredMARKDOWN,CALLOUTandTOGGLEonly, 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) andquoin-editor/locale-zh(Simplified Chinese). A pack covers the canvas and all fourteen feature label sets, sofrLabelsgoes to the canvas andfrTableLabelsto 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, throughIntl, 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/headlessexports the types its own signatures already named, so a host can write them down rather than reach them throughParametersandReturnType:AnnouncerwithSpoken,Engagement,FocusTarget,HostRegistrywithHostEntry, andNodeReveal, all reached throughViewController;CaretPoint, whichcaretFromPointreturns; andBlockShellPropsandSelectionSinkProps, taken by the two components named after them. They reachquoin-editortoo, 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,PALETTEandenglishLabels. Nothing about their behaviour changes. BlockShell,SelectionSink,defaultRenderers,VoidBlockFallbackandisCrossSelectionare 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:EditorViewdraws 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 withEditableRegionrather than reaching fordefaultRenderers, a block shell already falls back toVoidBlockFallbackwhen no renderer draws a block's type, and a host readsanchorandheadof the publicSelectionto tell a selection that spans blocks or regions from one held inside a single region.BlockRenderPropsandBlockRendererstay 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/diagramdraws a code block whose language names a diagram, such asmermaid, 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/sourceshows the document as the markdown it is stored as, read only, beside or below the canvas.QuoinSourcedraws it with the block holding the caret marked, anduseEditorSourcegives the same markdown and the lines of that block for a pane of your own. What it shows istoMarkdownof 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/highlightcolours code blocks, in every language the block's language picker offers.usePrismHighlighteris ahighlightforBlockCanvas, 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,-functionand-variable, colour the runs, each held by a test to 4.5:1 on a code block in both themes. Thehighlightprop 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 wasImage, no description, so the editor speaks the term the badge beside it and the alt text popover already use. A host that setsimageNoAltin its labels is not affected. - Every slash menu description is a noun phrase:
List with bullets,Quotation, set apart,Image, uploaded or linkedandFile 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 setsslashDescriptionsis 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 indata-quoin-icon, such asdata-quoin-icon="check", where it had thelucideandlucide-checkclasses. A custom block'siconstill takes any component that accepts aclassNameandaria-hidden, a lucide one included: if you import fromlucide-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 commentor3 comments, where it wasComments (1), so a voice command that says what is on screen reaches it. A host that setsshowCommentsin its labels is not affected.
1.3.0 (2026-11-01)
Added
TextSelectionhas a new optionalassoc,-1for 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
blocksarray, a transaction withaddToHistory: falseor 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-ringwhere it read--quoin-foreground. - A tip callout draws its rule and wash from a new
--quoin-tiptoken, violet by default, where it used--quoin-primaryand drew grey. Set--quoin-tipto your primary colour to keep the old look. - Menus, popovers and toolbars cast a softer two-layer shadow, coloured by a new
--quoin-shadowtoken. - 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
prioritymeans 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.priorityis 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
abesidea-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.
diffBlocksreports the word changes of a block that moved and was edited: itsinsertnow holds achange, as an edited block in place is reported.QuoinDiffshowed 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()andonChangegive 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, whileclass,style,data-*andaria-*are kept. Widget decorations write no attributes, and block decorations keep theirdata-andaria-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
inputthat takes text, atextareaor 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
renderersor newblockTypes, 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 inQuoinLabelsand English where missing. A host that setsnoAltBadgeand notaddAltTextkeeps the 1.1.0 name, its translated visible text alone. - A host renderer for a text block that draws no
EditableRegionfor 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 flatblocksarray, counting from 0 with nested blocks included, kept current as blocks move; only a block whose index changed draws again) andonRemove, which removes the block as one undo step and puts the caret where deleting the selected block puts it. A preview receivesonChangeon 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 andQuoinViewpass none. The canvas always passes all of these, and always passes a preview itsblockId. They are optional inBlockEditorPropsandBlockPreviewProps, so an editor rendered directly with onlydataandonChangestill compiles. - A block definition takes an optional
parse, which reads a block's storeddatainto the shape itseditorandpreviewtake, to fill required fields and migrate older shapes. The canvas,QuoinViewandQuoinDiffhand its result toeditorandpreview; 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 callsonChange. A block inserted withoutinitialDatais seeded withparse({}), soparseshould accept{}; when seeding throws, nothing is inserted and the error goes to the canvas'sonError, or to a warning in development without one. Whenparsethrows while drawing, the canvas draws the block's error fallback and reports the error toonError, andQuoinViewandQuoinDiffdraw the block without its preview and warn once per block type in development.initialDatastays optional; the custom blocks page recommendsparsefor 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 andquoin-editor/yjslikealt), 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,QuoinViewandtoHTMLdraw none. Output is unchanged: both cases are written withalt="". Asdecorativeis now a known key of image data, a value other thantrueis no longer kept, where 1.0 kept it as an extra field. New labelsdecorativeImageandnoAltBadge. - The labels added in this release,
blockError,decorativeImageandnoAltBadge, are optional inQuoinLabelsand read as English where missing, so a complete translation typed asQuoinLabelsstill compiles.englishLabelsis typedRequired<QuoinLabels>.
Changed
- The default
--quoin-warningand--quoin-inputreach 3:1 where they did not. Light--quoin-warninggoes fromoklch(0.65 0.16 65)tooklch(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 fromoklch(0.922 0 0)(1.26:1 on the page) tooklch(0.62 0 0)(3.64:1) in light, and fromoklch(1 0 0 / 15%)to the opaqueoklch(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
useQuoinanduseLabelshooks, with line thresholds of 95 percent onsrc/view, 97 onsrc/blocks, 98 onsrc/ui, 98 onsrc/context.tsx(useQuoin) and 98 onsrc/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/headlessfrom 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'sSourceMap.findEntrymake, 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-oraria-: a decoration withstyle,className, an event handler ordangerouslySetInnerHTMLno 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
blockErrorlabel (Block could not be drawn) in its own element, the other blocks keep working, the error goes toonErrorwhen one is given, and React reports it as it does any caught error, and the block is drawn again when its data changes.EditorViewtakesblockErrorLabelandonBlockErrorfor 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
tasklabel that is empty or only white space names the checkboxTask, and aframeTitlelabel that returns one titles the frame with its provider's name, or with its page's address when the provider has no name.toHTML,QuoinViewandQuoinDifftreat ataskLabel, anemptyTasklabel, aframeTitleor 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 theidin a block's marker in the sharedY.Textis 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 sharedY.Textthat theY.TextAPI never writes, such as values put in throughY.Arrayon 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
tasklabel that is empty or only white space as its name.
1.0.1 (2026-10-26)
Added
toDocxtakes an optionalbaseUrl, the address of the page the document comes from: a link or a file whose address is a relative path, such as/files/a.pdfornotes/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#topstay text. A base that is not an absolutehttporhttpsaddress is ignored, a user and password in it are left out of the links, and an empty link stays text.commentsExtensiontakes an optionalinitialLayout: '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
TransactionandViewControllerkeep their private state in native private fields, so a proxy around one of them, such as Vue'sreactiveor Valtio'sproxy, 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
droporigin, rather thancommand. - 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
alignor asalign: [null, null], which the editor andQuoinDifftreat as equal; a host comparing saved JSON should normalisealign(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
onErrorofyjsCollaborationis the adapter's own, apart from the canvas's; pass both the same handler (collaboration page).
Fixed
- Under a collaboration,
editor.chain().undo().run()andeditor.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'scolorthat holds what could leave the declaration or load a resource (any of;{}\"'<>!, a/*, orurl(,image(,image-set(orexpression(), rather than writing a value such asred;background-image:url(...)into the page as it is, and the canvas in the browser leaves out the same values. Other colours, such asvar(--name, #fff),hsl(var(--primary)),color-mix()andlight-dark(), are drawn by the canvas as in 1.0, and now byQuoinViewtoo, which uses the same rule. toHTML,QuoinViewandQuoinDiffno longer write an empty accessible name: an emptytaskLabelnames a task with no textTask(inQuoinDiff, theemptyTasklabel, andTaskwhen that is empty too), and a frame whoseframeTitleis empty is titled with its provider's name, or with its page's address when the provider has no name.toDocxwrites a link to anhttporhttpsaddress with no host, such ashttp:fooorhttp://?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/aian 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, smoothscroll-behavioror 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
yjsCollaborationthat 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
yjsCollaborationand 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. yjsCollaborationreports to itsonErrora 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
blocksarray 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
taskorframeTitlelabel is empty or blank, and ataskLabelorframeTitleof spaces only givestoHTML,QuoinViewandQuoinDiffa 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. AlsotoMarkdown,fromMarkdown,toHTML,toPlainText,fromHTML,diffBlocksandcountDoc.quoin-editor/headless: the engine and its editable view, with no canvas UI:useQuoinEditor,EditorView,EditableRegionandViewController. 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:BlockCanvasrebuilt 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+/).BlockCanvaspropsextensions,highlight,autoFocus(off by default),placeholder,labelsandblockLink, and a ref whoseQuoinEditorHandlegives theeditor, thecontroller,focus()andtoMarkdown().defineExtensionfor extensions of your own: blocks, marks, atoms, commands, keymaps, input and paste rules, plugins, decorations, transaction hooks, and auifield (ExtensionUI) that adds slash items, bubble items, block menu items, block renderers and overlays to the canvas.QuoinLabels,englishLabels,resolveLabelsanduseLabels: every string the canvas shows or speaks, English by default and replaced key by key throughlabels.CodeHighlighterandCodeToken, for thehighlightprop that colours code blocks.- The optional
indentfield ofQuoinBlock, the nesting depth of list items and of the blocks inside a toggle. - The engine reads and writes the
DIVIDER,TOGGLE,TABLE,EMBED,BOOKMARK,MATHandTOCblock 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 anunfurloption of your own.quoin-editor/math: math blocks and formulas in a line, drawn by yourrenderMath, such as KaTeX.quoin-editor/toc: a table of contents block that follows the headings.quoin-editor/mention:@mentions, found by your ownsearch.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.yjsis 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
MARKDOWNblock 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>-3and on), each written back as the canonical markdown of its line. A table in it becomes aTABLEblock and an image alone on its line anIMAGEblock. - Nested list items keep their depth in
indentrather than in leading spaces of their markdown. - An image's
captionand a callout'sbodyare inline markdown, typed in place like a paragraph. - The core blocks write new optional keys into
dataonce a writer uses what they hold:align,colorandbackgroundonMARKDOWN,icononCALLOUT,url,alignandwidthPercentonIMAGE, andmimeonFILE, besideindenton the block itself. Every 0.4 key keeps its name and meaning. QuoinConfig, whichuseQuoin()returns, gainshighlightand the requiredcontrollerandlabels.QuoinProviderfills them in, so only aQuoinConfigbuilt 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.
onDeletealso 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.defineBlocktakes any icon component that accepts a class name andaria-hidden, not only a lucide one. A custom block shows itspreviewat rest; itseditoropens on a double click, its Edit button or Enter on the selected block, and Escape closes it. A definition forDIVIDERdraws everywhere, and one forMARKDOWN,CALLOUTorTOGGLEonly on a read-only canvas, which warns in development when the canvas is editable.quoin-editor/headlessexports the view and the engine, andformatShortcutis exported from the main entry only. The main entry re-exports everything inquoin-editor/headless.- A canvas given a collaboration extension is uncontrolled while the shared document is connected: it ignores any
blocksarray it did not emit, and warns in development. - The stylesheet declares
color-schemein light and dark, and reads new tokens:--quoin-selection, the text and background colours--quoin-color-*and--quoin-bg-*,--quoin-comment,--quoin-comment-activeand--quoin-comment-edge,--quoin-diff-insertedand--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
sourcesContentand name their sources relative to the map. They still map each line to its file and line undersrc/. Every entry is exported, and the package publishesdist, the README, the license, this changelog and MIGRATION.md. @radix-ui/react-context-menuis a new dependency, for the block menu on a right click.
Removed
- The 0.4 textarea helpers, from both
quoin-editorandquoin-editor/headless:applyEditandEdit,lineAt,toggleLinePrefixandLinePrefix,continueList,indentList,wrapSelection,linkPasteandWrapKind,insertCommandandInsertableCommand,shouldOpenPalette,caretRectandCaretRect, anduseMarkdownKeymap. Engine commands do their work; MIGRATION.md maps each one. TEXT_COMMANDSandTextCommand: the slash menu's items come from the canvas and from extensions.useBlockList, withBlockListandUndoToken: the editor's own commands and history cover it.useAutoGrow: nothing is drawn in a textarea any more.formatShortcutfromquoin-editor/headless(it stays inquoin-editor).- The dependencies
@dnd-kit/core,@dnd-kit/modifiers,@dnd-kit/sortableand@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-labeland@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.tsholds 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,/docxand/yjscarry none.npm run check:directiveschecks 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-activedescendantfrom 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.tsxandBlockCard.tsxare 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.tschecks 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,labelandselectprimitives are gone, with@radix-ui/react-labeland@radix-ui/react-select. - F-31 [CQ-17]:
toggleLinePrefixis gone with the textarea helpers. - F-32 [CQ-18, PKG-3]: one build with code splitting, so
quoin-editorandquoin-editor/headlessshare each export, whichsrc/index.test.tschecks 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
autoFocusis 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
toDocxlinks a hyperlink only when its address is an absolutehttp,https,mailtoortelone; 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\shareaddress as a network share.QuoinViewandQuoinDiffwrite the colour of ahighlighttoken into astyleattribute 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, andBlockCanvastakes a ref. Remains: a block'spreviewstill cannot write its data, andBlockEditorPropsgained noblockId,indexoronRemove. - 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:sizeholds 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:
QuoinViewrenders a document on a server, with real headings, lists and links, the canvas draws text formatted instead of raw markdown, andtoHTMLandQuoinViewdraw an image as afigurewithwidth,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
toHTMLwrites a callout as<aside role="note" data-tone>, so colour is no longer the only cue. Remains: the light--quoin-warningkeeps 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:
iconaccepts any component. Remains:initialDatastays optional when the data has required fields, and a definition has noparseormigrate. - F-42 [A11Y-22]. Fixed:
color-schemeis declared in light and dark. Remains:--quoin-inputkeeps 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 therepository,homepageandbugsfields 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
useAutosavesaves 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).useAutosavekeeps 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
useAutosavehas 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 whenisRetryablesays no,statusbecomesfailedanderrorholds the reason. A success clears the error and any pending backoff (F-05). - An image block records its
storageKeyas 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). UploadFieldwaits for an asynconUploadedand reports its rejection throughonError, 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,InputandTextareaforward 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
readOnlyis 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
useAutosavegains the'failed'status, themaxRetries,isRetryableandflushOnHideoptions, anderrorandflush()on its result. Whensaveresolves{ ok: false }, that value is theerror.UploadFieldProps.onUploadedmay return a promise.useAutoGrowis typed to returnReact.RefObject<T>, so the ref it returns is accepted by React 18 and 19 types.- Default tokens: light
--quoin-ringis darker, dark--quoin-ringis lighter, and light--quoin-destructiveand--quoin-muted-foregroundare slightly darker. A host that maps--quoin-ringto a pale colour should give it one that reaches 3:1. - A host that does not echo
onChangeand restores a copy of a state from its last 64 edits now sees it ignored as a late echo. Remount the canvas with a Reactkeyto 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-ringis documented.
0.4.0 (2026-10-12)
Breaking
- Each callout tone draws in a colour of its own. New tokens:
--quoin-info,--quoin-successand--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
editorfor content that is never edited in the canvas; omitting botheditorandpreviewis a type error. - A read-only canvas warns once per type, in the developer console, when a definition has no
preview.
Changed
useAutosave'ssavemay return anything; only an explicit{ ok: false }(or a rejection) counts as a failure.defineBlockdefaults its data type toRecord<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
quoincascade 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
onDeletetells the host which block a deletion removed and the index it held.UploadFieldandFilePickerButtonare exported for custom blocks.
Fixed
- Builds under Tailwind v3 hosts.
0.1.1 (2026-10-09)
Fixed
package.jsonis exposed through theexportsmap, 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
onChangehandling no longer loses typing. - A block type nothing registered is drawn and reported through
onError. useBlockListcaps the removals it keeps for undo.
Changed
- The markdown commit the keymap is keyed on is memoised.
Docs
- A custom block reaching
uploadthroughuseQuoin, 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 (blocksandonChange) 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
defineBlockwith plain Reacteditorandpreviewcomponents. - A markdown surface with keyboard shortcuts, list continuation, indentation and a slash palette.
- Host-owned uploads through an injected
uploadfunction andresolveAssetUrl. quoin-editor/headless: the markdown text transforms with no UI dependency.- Scoped styles with a class-based dark mode.