Rich text editor
RichTextEditor is the text editor of @amset/ui, built in-house with no third-party dependencies. It lives in a subpath, @amset/ui/rich-text, so anyone who does not use it loads not a single byte. It stores the document as JSON, exports it to HTML, Markdown and Word, renders it on the server with RichTextView, and lets you put the library’s own components inside the text.
Try it here
These are not screenshots: it is the library editor running on this page. Type, paste, press “/” and see what is stored.
The editor and what it stores
Type, paste something from Word or a web page, or press “/” at the start of a line to insert a table, a callout or a collapsible section. Below, the same document in each format, as you type.
<h2>Meeting notes</h2><p>Write here with <strong>bold</strong>, <em>italics</em> or a <a href="https://amset.tech">link</a>. Try <code>/</code> as well to insert blocks.</p><ul><li><p>Review the budget</p></li><li><p>Send the proposal to the client</p></li></ul><table><tbody><tr><th><p>Task</p></th><th><p>Owner</p></th></tr><tr><td><p>Design</p></td><td><p>Anna</p></td></tr><tr><td><p>Development</p></td><td><p>Louis</p></td></tr></tbody></table>With the library components inside
The card and the grid hold document text inside: type in them. Type “/” and the name of a component (Badge, Progress, Alert…) to insert it; with the cursor on one, the toolbar shows its properties. The chart takes its figures from this page’s data and the button calls an action of the page.
What the reader sees
Quarter summary
Before you start
The figures are updated every night. Type in here as anywhere else.
Orders
342
12 pending
Invoiced
18.420 €
this month
Press the report button: inside the editor it does nothing, in the view it calls the page action.
The same document as React code
import { BarChart, Button, Card, Grid, Stat } from '@amset/ui' export function Report({ actions, data }: { actions: Record<string, () => void>; data: Record<string, any> }) { return ( <> <h2>Quarter summary</h2> <Card title="Before you start"> <p>The figures are updated <strong>every night</strong>. Type in here as anywhere else.</p> </Card> <Grid columns={2}> <Stat label="Orders" value="342" detail="12 pending" /> <Stat label="Invoiced" value="18.420 €" detail="this month" /> </Grid> <BarChart label="Monthly sales" data={data.sales} /> <Button onPress={actions.save}> Save draft </Button> </> )}The safe HTML viewer
This HTML brings a script, an image with code, a JavaScript link and a form asking for a password. Change it as you like: the viewer draws it without running anything and says what it removed.
Nothing had to be removed.
What it brings
A field like the rest
Text and structure
Blocks
@ mentions. And your own blocks: you register a type with its render and the document stores only its name and its data.Warns while you type
Team review
Faithful paste
For agents
Getting started
The editor works controlled or not, with the document as JSON or HTML. JSON is what you should store: it loses nothing. RichTextView renders the same document without loading the editor.
import { RichTextEditor, RichTextView } from '@amset/ui/rich-text' export function Notes({ html, onChange }: { html: string; onChange: (html: string) => void }) { return <RichTextEditor label="Notes" format="html" defaultValue={html} onChange={onChange} rows={8} />} // Solo lectura, sin cargar el editor (también en el servidor)export function Preview({ html }: { html: string }) { return <RichTextView format="html" value={html} />}Formats
Each converter is imported only if you use it; the editor loads none of them.
- HTML:
toHtmlandfromHtml. Everything the editor can do comes out as valid HTML and is read back. - Markdown:
toMarkdownandfromMarkdown(CommonMark, plus GFM tables, tasks and strikethrough). What Markdown cannot say (underline, colour, alignment, merged cells) is written as HTML inside the Markdown, so the document survives the round trip. Formulas are stored between$$. A test checks it with 1500 random documents. - Word:
toDocxandfromDocxwrite and read .docx files with headings, nested lists, tables with merged cells, links, images and mentions. Suggestions come out as tracked changes and comments as Word comments, and are read back. Columns come out as a single column and formulas as the TeX, in code style. - React code:
toTsxturns the document into a React component that imports from@amset/ui.
import { toMarkdown, fromMarkdown, toDocx } from '@amset/ui/rich-text' const markdown = toMarkdown(doc)const back = fromMarkdown(markdown)const bytes = await toDocx(doc)Your own blocks
A block is a piece of yours (a product card, a chart, a form) inside the text. The document stores only its type and some JSON data with limits on size and depth; what you see is decided by your render, used the same in the editor, in RichTextView and on the server. A block nobody knows how to draw is kept as it is, not lost.
const blocks = { 'product-card': { label: 'Product card', defaultProps: { name: 'Product', price: '0 €' }, render: (props) => <ProductCard {...props} />, },} <RichTextEditor label="Page" blocks={blocks} />Review and proofread as a team
What several people need to work on the same text. Each piece is switched on with a prop, and whatever you do not switch on adds no weight.
Spellchecker
createWordSpellchecker or createLanguageToolSpellchecker.Comments
threads and onThreadsChange); the document only keeps the mark of which text they refer to.Suggestions
Version history
diffDocuments(before, after) gives the same comparison outside the editor.Columns
Formulas
import { RichTextEditor, createLanguageToolSpellchecker, diffDocuments } from '@amset/ui/rich-text' <RichTextEditor label="Contract" spellchecker={createLanguageToolSpellchecker({ url: '/api/spellcheck', language: 'en' })} review={{ author: 'Ana', threads, onThreadsChange: setThreads, suggesting, onSuggestingChange: setSuggesting }} versions={{ versions, load: loadVersion, onSave: saveVersion, onRestore: restoreVersion }}/> // The same comparison outside the editorconst changes = diffDocuments(before, after)Real-time editing, with several people typing at once and their cursors in view, is not here yet. It needs a sync server and per-user credentials, and will come later. Meanwhile, comments, suggestions and versions work on a document that is saved and reopened.
AMSET UI components inside the document
With amsetBlocks() the library components (159 today) become blocks: type “/” and the name, and the real component is inserted.
Generated catalogue
Loaded on demand
What the document stores
className, style, ref, id, raw HTML and functions are never stored, and links and images go through the same URL checks as the rest of the editor.Components with content
Your application’s actions and data
save). A chart carries no figures: it carries the name of a data source (sales). Your application resolves them with amsetBlocks({ actions, data }). Nothing runs inside the editor, and the properties form lets you choose where each list comes from.import { RichTextEditor, amsetBlocks } from '@amset/ui/rich-text' const blocks = amsetBlocks({ actions: { save: { label: 'Save draft', run: () => saveDraft() } }, data: { sales: { label: 'Monthly sales', value: sales } },}) <RichTextEditor label="Report" blocks={blocks} />{ "type": "component", "kind": "amset-card", "props": { "title": "Summary" }, "content": [{ "type": "paragraph", "inline": [{ "type": "text", "text": "Quarter figures", "marks": [] }] }]}A safe HTML viewer
HtmlViewer draws HTML you do not control: an email template, the body of a message, the HTML a model returns.
Document mode
onReport.Page mode
<iframe sandbox> without allow-same-origin and with a security policy that allows no network, forms, frames or foreign scripts. Only a small script of ours runs, with a nonce, which reports the height and link clicks.import { HtmlViewer } from '@amset/ui/rich-text' <HtmlViewer html={untrustedHtml} images="https" onReport={(report) => log(report)} /> // El diseño del autor, aislado en un iframe<HtmlViewer html={newsletter} mode="page" images="https" />To code and to agents
toTsx(doc) returns the code of a React component with the library components, actions as actions.save and data as data.sales, plus a list of warnings for what could not be carried over as is. For an assistant, listComponents(), describeComponent(kind) and createComponent(kind, props) give the catalogue, a component’s sheet and an already validated block, with the errors explained instead of a broken block.
import { toTsx, listComponents, createComponent } from '@amset/ui/rich-text' const { code, warnings } = toTsx(doc, { componentName: 'Report' }) listComponents() // the catalogueawait createComponent('amset-stat', { label: 'Orders', value: '342' })Security
- What is pasted, imported and loaded goes through allowlists; nothing unlisted reaches the document.
- The document never holds functions, scripts or arbitrary styles. Custom and library blocks are validated against their schema and with size limits.
- Links accept
http,https,mailto,teland relative paths; images accepthttp,httpsand own paths, neverdata:. - The HTML viewer is tested with more than 50 attack vectors and 600 randomly mutated documents.
Accessibility and weight
The field is a role="textbox" with its label, the toolbar works with the keyboard, important changes are announced and every story passes axe. The full editor weighs about 99 KB gzipped and RichTextView, which only reads, about 21 KB. The review and version panels, the formula converter, the AMSET UI components and the Markdown and Word converters load only if used.
