Bridge Training
Rich Text

Overview

Rich text model, serialization, editing and rendering for bridge content.

Rich text is split across two packages:

  • @workspace/rich-text is pure TypeScript: the domain RichText model, serialization (HTML, plain text, markdown, PBN comments) and test factories. It has no React or DOM dependency.
  • @workspace/ui-rich-text holds 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 domain RichText type.

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 styles

Design Principles

  • Domain boundary: consumers work with RichText (domain type), never Plate/Slate nodes
  • Uncontrolled editor: defaultValue on mount, imperative ref for reads/writes (per Plate docs)
  • Composable: RichEditor is 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 -> RichText

Exports

PathTypeDescription
@workspace/rich-textBarrelDomain types, constants, suit utilities
@workspace/rich-text/libBarrelSerialization functions (toHTML, fromHTML, toString, fromString, fromPbnComment)
@workspace/rich-text/testingBarrelTest factories (makeRichText, makeComment, richText, fromString)
@workspace/ui-rich-text/components/*ExplicitReact components (rich-editor, localized-editor, rich-comment-field, comment-preview, etc.)
@workspace/ui-rich-text/hooks/*ExplicitReact hooks (use-localized-rich-text)
@workspace/ui-rich-text/plugins/*ExplicitPlugin config (create-rich-text-plugins, magic-var)
@workspace/ui-rich-text/plugins/suit/*ExplicitSuit plugin components (suit-symbol-button, suit-icons, etc.)
@workspace/ui-rich-text/typesBarrelEditor TypeScript types (RichEditorHandle, RichEditorConfig, etc.)
@workspace/ui-rich-text/testing/playwrightExplicitPlaywright helpers for driving the editor in e2e tests
@workspace/ui-rich-text/styles.cssCSSEditor styles