Rich Text
Overview
Rich text model, serialization, editing and rendering for bridge content.
Rich text is split across two packages:
@workspace/rich-textis pure TypeScript: the domainRichTextmodel, serialization (HTML, plain text, markdown, PBN comments) and test factories. It has no React or DOM dependency.@workspace/ui-rich-textholds the React side: editor and display components, hooks, Plate.js plugins and styles. It wraps Plate.js internally but never exposes Plate types to consumers. All public APIs use the domainRichTexttype.
Architecture
@workspace/rich-text (pure TS)
model/ Domain types & constants (RichText, LocalizedRichText, suit glyphs)
lib/ Serialization: toHTML, fromHTML, toString, fromString, fromPbnComment
testing/ Test factories (makeRichText, makeComment, richText)
@workspace/ui-rich-text (React / DOM)
components/ React components (RichEditor, LocalizedEditor, RichTextDisplay, RichCommentField, CommentPreview)
hooks/ React hooks (useLocalizedRichText)
plugins/ Plate plugin configuration (suit/, magic-var)
plate/ Plate abstraction layer (internal, not exported)
types/ Editor TypeScript types (handles, config, options)
testing/ Playwright helpers for the editor
styles.css Editor stylesDesign Principles
- Domain boundary: consumers work with
RichText(domain type), never Plate/Slate nodes - Uncontrolled editor:
defaultValueon mount, imperativereffor reads/writes (per Plate docs) - Composable:
RichEditoris a compound component (Root + Toolbar + Area + Input), consumers compose the layout - No barrels for components: explicit imports like
@workspace/ui-rich-text/components/rich-editor
Quick Start
Editing
import { RichEditor } from '@workspace/ui-rich-text/components/rich-editor';
import { MarkToolbarButton } from '@workspace/ui-rich-text/components/mark-toolbar-button';
import { SuitSymbolButton } from '@workspace/ui-rich-text/plugins/suit/suit-symbol-button';
import type { RichEditorHandle } from '@workspace/ui-rich-text/types';
const ref = useRef<RichEditorHandle>(null);
<RichEditor.Root config={{ mode: 'default', lists: true }} defaultValue={richText} ref={ref}>
<RichEditor.Toolbar>
<MarkToolbarButton nodeType="bold">B</MarkToolbarButton>
<MarkToolbarButton nodeType="italic">I</MarkToolbarButton>
<SuitSymbolButton suit="S" />
<SuitSymbolButton suit="H" />
</RichEditor.Toolbar>
<RichEditor.Area variant="edge">
<RichEditor.Input placeholder="Write..." variant="edge" />
</RichEditor.Area>
</RichEditor.Root>Read-only Display
import { RichTextDisplay } from '@workspace/ui-rich-text/components/rich-text-display';
<RichTextDisplay value={richText} />Serialization
import { toHTML, fromHTML, toString, fromString } from '@workspace/rich-text/lib';
const html = toHTML(richText); // RichText -> HTML
const rt = fromHTML(html); // HTML -> RichText
const text = toString(richText); // RichText -> plain text
const rt2 = fromString(markdown); // Markdown -> RichTextExports
| Path | Type | Description |
|---|---|---|
@workspace/rich-text | Barrel | Domain types, constants, suit utilities |
@workspace/rich-text/lib | Barrel | Serialization functions (toHTML, fromHTML, toString, fromString, fromPbnComment) |
@workspace/rich-text/testing | Barrel | Test factories (makeRichText, makeComment, richText, fromString) |
@workspace/ui-rich-text/components/* | Explicit | React components (rich-editor, localized-editor, rich-comment-field, comment-preview, etc.) |
@workspace/ui-rich-text/hooks/* | Explicit | React hooks (use-localized-rich-text) |
@workspace/ui-rich-text/plugins/* | Explicit | Plugin config (create-rich-text-plugins, magic-var) |
@workspace/ui-rich-text/plugins/suit/* | Explicit | Suit plugin components (suit-symbol-button, suit-icons, etc.) |
@workspace/ui-rich-text/types | Barrel | Editor TypeScript types (RichEditorHandle, RichEditorConfig, etc.) |
@workspace/ui-rich-text/testing/playwright | Explicit | Playwright helpers for driving the editor in e2e tests |
@workspace/ui-rich-text/styles.css | CSS | Editor styles |