Skip to content

Rich Text Editor - Module Specification

1. Overview

1.1 Purpose

Rich Text Editor provides a shared Lexical-based editor component and type definitions for rich text content across all modules. Handles content creation, storage format, and rendering of formatted text with support for headings, lists, links, and media embeds.

1.2 Scope

In Scope: - Lexical editor React component - LexicalEditorState type definition (JSON schema) - Rich text renderer component for public display - Editor configuration and plugins

Out of Scope: - Media upload (→ Media module) - Content storage (handled by consuming modules) - Page/blog management (→ CMS module)


2. Dependencies

2.1 Upstream (What This Module Needs)

Module/Service What We Need Interface
Media Media picker for embedding images/videos React Component

2.2 Downstream (What Needs This Module)

Module/Service What They Need Interface
Inventory Editor for puppy/breed descriptions React Component
Admin Settings Editor for disclaimers, notification banner React Component
CMS Editor for blog posts, page content React Component
Customer Website Renderer for displaying rich text React Component

3. Data Ownership

3.1 Entities This Module Owns

Entity Description Key Fields
This module owns no database entities

Note: Rich Text Editor provides type definitions and UI components only. Content is stored by consuming modules in their own entities.

3.2 Entities This Module Uses (Read-Only)

Entity Owner How We Use It
Media Media Embed images/videos in rich text content

3.3 Type Definitions

import type { Media } from '@/modules/media';

/**
 * Lexical editor serialized state
 * Stored as JSON in database text/jsonb columns
 */
interface LexicalEditorState {
  root: {
    children: LexicalNode[];
    direction: 'ltr' | 'rtl' | null;
    format: string;
    indent: number;
    type: 'root';
    version: number;
  };
}

type LexicalNode =
  | ParagraphNode
  | HeadingNode
  | ListNode
  | ListItemNode
  | LinkNode
  | TextNode
  | ImageNode
  | VideoNode;

interface ParagraphNode {
  type: 'paragraph';
  children: LexicalNode[];
  direction: 'ltr' | 'rtl' | null;
  format: string;
  indent: number;
  version: number;
}

interface HeadingNode {
  type: 'heading';
  tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
  children: LexicalNode[];
  direction: 'ltr' | 'rtl' | null;
  format: string;
  indent: number;
  version: number;
}

interface ListNode {
  type: 'list';
  listType: 'bullet' | 'number';
  children: ListItemNode[];
  direction: 'ltr' | 'rtl' | null;
  format: string;
  indent: number;
  start: number;
  version: number;
}

interface ListItemNode {
  type: 'listitem';
  children: LexicalNode[];
  direction: 'ltr' | 'rtl' | null;
  format: string;
  indent: number;
  value: number;
  version: number;
}

interface LinkNode {
  type: 'link';
  url: string;
  target?: '_blank' | '_self';
  rel?: string;
  children: TextNode[];
  direction: 'ltr' | 'rtl' | null;
  format: string;
  indent: number;
  version: number;
}

interface TextNode {
  type: 'text';
  text: string;
  format: number;                 // Bitmask: bold=1, italic=2, underline=4, etc.
  style: string;
  detail: number;
  mode: 'normal' | 'token' | 'segmented';
  version: number;
}

interface ImageNode {
  type: 'image';
  mediaId: Media['id'];
  altText?: string;
  width?: number;
  height?: number;
  version: number;
}

interface VideoNode {
  type: 'video';
  mediaId: Media['id'];
  version: number;
}

4. Service Level Objectives

Objective Target Measurement
Editor Load Time < 200ms Application monitoring
Typing Latency < 16ms (60fps) Performance profiling
Render Time < 50ms Application monitoring
Bundle Size < 100KB gzipped Build metrics

Note: Editor performance is critical for admin UX. Renderer must be lightweight for customer-facing pages.


5. Interface Contract

5.1 Exported Types

// Primary type exported for use in other modules
export type { LexicalEditorState } from './types';

// Node types for advanced usage
export type {
  LexicalNode,
  ParagraphNode,
  HeadingNode,
  ListNode,
  ListItemNode,
  LinkNode,
  TextNode,
  ImageNode,
  VideoNode,
} from './types';

5.2 React Components

import type { Media } from '@/modules/media';

/**
 * Rich text editor component for admin interfaces
 */
interface RichTextEditorProps {
  value?: LexicalEditorState;
  onChange: (state: LexicalEditorState) => void;
  placeholder?: string;
  disabled?: boolean;

  // Feature toggles
  features?: {
    headings?: boolean;           // h1-h6 support (default: true)
    lists?: boolean;              // bullet/numbered lists (default: true)
    links?: boolean;              // hyperlinks (default: true)
    media?: boolean;              // image/video embeds (default: false)
  };

  // Media picker integration
  onMediaSelect?: () => Promise<Media | null>;

  // Styling
  className?: string;
  minHeight?: number;
}

declare function RichTextEditor(props: RichTextEditorProps): JSX.Element;

/**
 * Read-only renderer for displaying rich text content
 */
interface RichTextRendererProps {
  content: LexicalEditorState;
  className?: string;

  // Media URL transformer for responsive images
  transformMediaUrl?: (mediaId: string) => string;
}

declare function RichTextRenderer(props: RichTextRendererProps): JSX.Element;

5.3 Utility Functions

/**
 * Create empty editor state
 */
function createEmptyEditorState(): LexicalEditorState;

/**
 * Convert plain text to editor state
 */
function textToEditorState(text: string): LexicalEditorState;

/**
 * Extract plain text from editor state (for search indexing)
 */
function editorStateToText(state: LexicalEditorState): string;

/**
 * Validate editor state structure
 */
function isValidEditorState(value: unknown): value is LexicalEditorState;

6. Error Handling Strategy

Error Code Condition User Message
RTE_INVALID_STATE Malformed editor state JSON "Content could not be loaded"
RTE_RENDER_FAILED Error during content render "Content could not be displayed"
RTE_MEDIA_LOAD_FAILED Embedded media failed to load "Media could not be loaded"
RTE_PASTE_FAILED Paste operation failed "Could not paste content"

Error Handling Approach

// Editor gracefully handles invalid state by resetting to empty
<RichTextEditor
  value={maybeCorruptedState}
  onError={(error) => console.error('Editor error:', error)}
/>

// Renderer shows fallback for invalid content
<RichTextRenderer
  content={content}
  fallback={<p>Content unavailable</p>}
/>

7. Observability

7.1 Logging

Event Level Fields to Include
Editor initialized DEBUG editor_id, features_enabled
Content saved DEBUG editor_id, content_size_bytes
Invalid state recovered WARN editor_id, error_type
Render error ERROR content_hash, error_message

7.2 Metrics

Metric Type Description
rte_editor_load_ms Histogram Editor initialization time
rte_render_duration_ms Histogram Content render time
rte_content_size_bytes Histogram Saved content size
rte_errors_total Counter Editor/render errors by type

8. Testing Strategy

Test Type Coverage Target Tools Focus Areas
Unit 80%+ Vitest Utility functions, state validation, text extraction
Component Critical paths Vitest + Testing Library Editor interactions, renderer output
E2E Happy paths Playwright Full editing workflows, media embeds
Visual Key states Playwright Editor UI, rendered content appearance

9. Feature Inventory

Feature ID Name Status Priority
FEATURE-052 Rich Text Editor Draft Must Have
FEATURE-053 Rich Text Renderer Draft Must Have

FEATURE-052 Note: Includes editor component, LexicalEditorState type definition, utility functions, and media embed support.

FEATURE-053 Note: Read-only renderer for customer-facing display with media URL transformation.


10. Access Control

Operation Permissions

Operation Public Sales Kennel Staff Photographer Admin
View rendered content Yes Yes Yes Yes Yes
Use editor component No No No No Yes

Note: Editor access is controlled by the consuming module's permissions. The editor component itself is only available in admin interfaces.


11. Decisions

D1: Lexical as Editor Framework (2026-01-20)

Status: Accepted Context: Need a free, open-source rich text editor that supports React components as custom blocks Decision: Use Lexical (MIT, React-first, best support for custom React nodes/components) Rationale: - Do not use TipTap paid extensions; use Lexical core + @lexical/react plugins - There is no official pre-built UI kit for Lexical. Use the Lexical Playground as a reference and port toolbar, floating link editor, and block type dropdown as needed - Use Payload CMS's implementation only as a behavioral reference, not as a UI kit Implementation Notes: - Prefer Lexical plugins (RichTextPlugin, HistoryPlugin, ListPlugin, LinkPlugin, etc.) - Build custom blocks via Lexical nodes and React components - Keep the solution fully open source and React-native Consequences: Requires wiring our own toolbar/plugins, but offers example UI code in the Playground; modern architecture, good React integration, active development

D2: JSON Storage Format (2026-01-20)

Status: Accepted Context: Need to store rich text content in database Decision: Store Lexical's native JSON state directly in database jsonb columns Consequences: No conversion needed, preserves full fidelity, easy to query/index text content

D3: Separate Editor and Renderer (2026-01-20)

Status: Accepted Context: Customer website needs lightweight rendering without editor overhead Decision: Provide separate RichTextRenderer component that doesn't load Lexical editor code Consequences: Smaller bundle for customer pages, faster load times, editor only loaded in admin


12. References

  • Architecture: ARCH-001
  • Inventory Module: MODULE-002
  • Media Module: MODULE-003
  • Admin Settings Module: MODULE-004
  • Lexical Documentation: https://lexical.dev/
  • Lexical Playground: https://playground.lexical.dev/
  • Payload CMS Lexical Implementation: https://github.com/payloadcms/payload/tree/main/packages/richtext-lexical

Change Log

Version Date Author Changes
1.0 2026-01-20 Claude Initial module spec - Draft