Editor de texto enriquecido
RichTextEditor es el editor de texto de @amset/ui, hecho aquí y sin dependencias de terceros. Vive en una subruta, @amset/ui/rich-text, así que quien no lo usa no carga ni un byte. Guarda el documento como JSON, lo exporta a HTML, Markdown y Word, lo pinta en el servidor con RichTextView y deja meter dentro del texto los propios componentes de la biblioteca.
Pruébalo aquí
No son capturas: es el editor de la biblioteca funcionando en esta página. Escribe, pega, pulsa «/» y mira qué se guarda.
El editor y lo que guarda
Escribe, pega algo de Word o de una web, o pulsa «/» al principio de una línea para insertar una tabla, un cuadro destacado o un desplegable. Debajo, el mismo documento en cada formato, al momento.
<h2>Notas de la reunión</h2><p>Escribe aquí con <strong>negrita</strong>, <em>cursiva</em> o un <a href="https://amset.tech">enlace</a>. Prueba también <code>/</code> para insertar bloques.</p><ul><li><p>Revisar el presupuesto</p></li><li><p>Enviar la propuesta al cliente</p></li></ul><table><tbody><tr><th><p>Tarea</p></th><th><p>Quién</p></th></tr><tr><td><p>Diseño</p></td><td><p>Ana</p></td></tr><tr><td><p>Desarrollo</p></td><td><p>Luis</p></td></tr></tbody></table>Con los componentes de la biblioteca dentro
La tarjeta y la rejilla llevan texto del documento dentro: escribe en ellas. Escribe «/» y el nombre de un componente (Insignia, Progreso, Alerta…) para insertarlo; al poner el cursor en uno, la barra enseña sus propiedades. La gráfica toma sus cifras de los datos de esta página y el botón llama a una acción de la página.
Así lo ve quien lo lee
Resumen del trimestre
Antes de empezar
Los datos se actualizan cada noche. Escribe aquí dentro como en cualquier otro sitio.
Pedidos
342
12 pendientes
Facturado
18.420 €
este mes
Pulsa el botón del informe: dentro del editor no hace nada, en la vista llama a la acción de la página.
El mismo documento como código React
import { BarChart, Button, Card, Grid, Stat } from '@amset/ui' export function Report({ actions, data }: { actions: Record<string, () => void>; data: Record<string, any> }) { return ( <> <h2>Resumen del trimestre</h2> <Card title="Antes de empezar"> <p>Los datos se actualizan <strong>cada noche</strong>. Escribe aquí dentro como en cualquier otro sitio.</p> </Card> <Grid columns={2}> <Stat label="Pedidos" value="342" detail="12 pendientes" /> <Stat label="Facturado" value="18.420 €" detail="este mes" /> </Grid> <BarChart label="Ventas por mes" data={data.sales} /> <Button onPress={actions.save}> Guardar borrador </Button> </> )}El visor de HTML seguro
Este HTML trae un script, una imagen con código, un enlace con JavaScript y un formulario que pide la contraseña. Cámbialo como quieras: el visor lo pinta sin ejecutar nada y dice qué ha quitado.
No ha hecho falta quitar nada.
Qué trae
Un campo como los demás
Texto y estructura
Bloques
@. Y bloques propios: registras un tipo con su render y el documento solo guarda su nombre y sus datos.Avisa mientras escribes
Revisión en equipo
Pegado fiel
Para agentes
Empezar
El editor funciona controlado o no, con el documento en JSON o en HTML. El JSON es lo que conviene guardar: no pierde nada. RichTextView pinta el mismo documento sin cargar el 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} />}Formatos
Cada conversor se importa solo si lo usas; el editor no carga ninguno.
- HTML:
toHtmlyfromHtml. Todo lo que el editor sabe hacer sale como HTML válido y se lee de vuelta. - Markdown:
toMarkdownyfromMarkdown(CommonMark, más las tablas, tareas y tachado de GFM). Lo que Markdown no sabe decir (subrayado, color, alineación, celdas combinadas) se escribe como HTML dentro del Markdown, así que el documento sobrevive al viaje de ida y vuelta. Las fórmulas se guardan entre$$. Una prueba lo comprueba con 1500 documentos aleatorios. - Word:
toDocxyfromDocxgeneran y leen ficheros .docx con títulos, listas anidadas, tablas con celdas combinadas, enlaces, imágenes y menciones. Las sugerencias salen como cambios con control de cambios y los comentarios como comentarios de Word, y se leen de vuelta. Las columnas salen en una sola y las fórmulas como el TeX, en estilo de código. - Código React:
toTsxconvierte el documento en un componente de React que importa de@amset/ui.
import { toMarkdown, fromMarkdown, toDocx } from '@amset/ui/rich-text' const markdown = toMarkdown(doc)const back = fromMarkdown(markdown)const bytes = await toDocx(doc)Bloques propios
Un bloque es una pieza tuya (una tarjeta de producto, una gráfica, un formulario) dentro del texto. El documento guarda solo su tipo y unos datos JSON con límites de tamaño y profundidad; lo que se ve lo decide tu render, que se usa igual en el editor, en RichTextView y en el servidor. Un bloque que nadie sabe pintar se conserva tal cual, no se pierde.
const blocks = { 'product-card': { label: 'Product card', defaultProps: { name: 'Product', price: '0 €' }, render: (props) => <ProductCard {...props} />, },} <RichTextEditor label="Page" blocks={blocks} />Revisar y corregir en equipo
Lo que se necesita para que varias personas trabajen sobre un mismo texto. Todo se activa con una propiedad, y lo que no se activa no pesa.
Corrector ortográfico
createWordSpellchecker o createLanguageToolSpellchecker.Comentarios
threads y onThreadsChange); en el documento solo queda la marca de a qué texto se refieren.Sugerencias
Historial de versiones
diffDocuments(antes, despues) da la misma comparación fuera del editor.Columnas
Fórmulas
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)La edición simultánea en tiempo real, con varias personas escribiendo a la vez y sus cursores a la vista, no está todavía. Necesita un servidor de sincronización y credenciales por usuario, y llegará más adelante. Mientras tanto, los comentarios, las sugerencias y las versiones funcionan sobre un documento que se guarda y se vuelve a abrir.
Los componentes de AMSET UI dentro del documento
Con amsetBlocks() los componentes de la biblioteca (159 hoy) se convierten en bloques: escribes «/» y el nombre, y se inserta el componente de verdad.
Catálogo generado
Carga bajo demanda
Qué guarda el documento
className, style, ref, id, HTML crudo ni funciones, y los enlaces e imágenes pasan por las mismas comprobaciones de URL que el resto del editor.Componentes con contenido
Acciones y datos de tu aplicación
save). Una gráfica no lleva sus cifras: lleva el nombre de un origen de datos (sales). Tu aplicación los resuelve con amsetBlocks({ actions, data }). En el editor no se ejecuta nada, y la ficha deja elegir de dónde sale cada lista.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": [] }] }]}Un visor de HTML seguro
HtmlViewer pinta HTML que no controlas: una plantilla de correo, el cuerpo de un mensaje, el HTML que devuelve un modelo.
Modo documento
onReport.Modo página
<iframe sandbox> sin allow-same-origin y con una política de seguridad que no permite red, formularios, marcos ni scripts ajenos. Solo se ejecuta un pequeño script nuestro, con nonce, que avisa de la altura y de los clics en enlaces.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" />A código y a agentes
toTsx(doc) devuelve el código de un componente de React con los componentes de la biblioteca, las acciones como actions.save y los datos como data.sales, más una lista de avisos de lo que no se pudo llevar tal cual. Para un asistente, listComponents(), describeComponent(kind) y createComponent(kind, props) dan el catálogo, la ficha de un componente y un bloque ya validado, con los errores explicados en vez de un bloque roto.
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' })Seguridad
- Lo que se pega, se importa y se carga pasa por listas de lo permitido; nada de lo no listado llega al documento.
- El documento nunca contiene funciones, scripts ni estilos arbitrarios. Los bloques propios y los de la biblioteca se validan contra su esquema y con límites de tamaño.
- Los enlaces aceptan
http,https,mailto,tely rutas relativas; las imágenes,http,httpsy rutas propias, nuncadata:. - El visor de HTML se prueba con más de 50 vectores de ataque y 600 documentos mutados al azar.
Accesibilidad y peso
El campo es un role="textbox" con su etiqueta, la barra de herramientas se maneja con teclado, los cambios importantes se anuncian y cada historia pasa axe. El editor completo pesa unos 99 KB comprimidos y RichTextView, que solo lee, unos 21 KB. Los paneles de revisión y de versiones, el conversor de fórmulas, los componentes de AMSET UI y los conversores a Markdown o Word se cargan solo si se usan.
