@seolhun/firstage-editor
v0.1.9
Published
A modern, extensible rich text editor built with Lexical and React, featuring a **colocation architecture** for maximum maintainability and developer experience.
Readme
Editor Package
A modern, extensible rich text editor built with Lexical and React, featuring a colocation architecture for maximum maintainability and developer experience.
🎯 PlainText vs RichText Architecture
This editor supports two distinct modes optimized for different use cases:
PlainText Editor (소셜 미디어 간단 글쓰기)
- Purpose: Quick social media posts, comments, simple messages
- Features: Mentions (@user), Emojis (😀), Images, Hashtags (#tag), Auto-links
- UI: Clean, minimal interface focused on speed
- Nodes: Limited to essential formatting only
RichText Editor (블로그 고도화 에디터)
- Purpose: Blog posts, articles, documentation, long-form content
- Features: All PlainText features plus tables, code blocks, headings, lists, embeds
- UI: Full toolbar with advanced formatting options
- Nodes: Complete feature set for professional content creation
Dynamic Configuration
The editor automatically loads appropriate nodes and plugins based on the isRichText setting:
// PlainText mode - minimal features
<RootEditor initialSettings={{ isRichText: false }} />
// RichText mode - full features
<RootEditor initialSettings={{ isRichText: true }} />🎯 Colocation Architecture
This editor follows a feature-first colocation approach where all related files for a feature are grouped together in a single directory. This improves code organization, reduces cognitive load, and makes features more maintainable.
Perfect Colocation Example: Emoji System ✨
packages/editor/src/features/emoji/
├── components/ # Components used by Node/Renderer/Plugin
├── emoji.node.ts # Lexical node definition
├── emoji.plugin.tsx # Main plugin with FloatingPortal
├── emoji.typeahead.tsx # Typeahead functionality
├── emoji.renderer.tsx # EmojiPicker renderer (@emoji-mart/react)
├── emoji.store.ts # Zustand state management
├── emoji.types.ts # TypeScript type definitions
├── emoji.utils.ts # Search and filtering utilities
├── emoji.item.tsx # Individual emoji item component
├── emoji.{feature-name}.tsx # Additional feature-specific files
└── index.ts # Exports (export * from pattern)Naming Convention
Pattern: {feature}.{type}.{ext}
- Node:
{feature}.node.ts(e.g.,emoji.node.ts) - Plugin:
{feature}.plugin.tsx(e.g.,emoji.plugin.tsx) - Typeahead:
{feature}.typeahead.tsx(e.g.,emoji.typeahead.tsx) - Renderer:
{feature}.renderer.tsx(e.g.,emoji.renderer.tsx) - Store:
{feature}.store.ts(e.g.,emoji.store.ts) - Types:
{feature}.types.ts(e.g.,emoji.types.ts) - Utils:
{feature}.utils.ts(e.g.,emoji.utils.ts) - Components:
{feature}.{component}.tsx(e.g.,emoji.item.tsx)
📁 Project Structure
packages/editor/src/
├── features/ # Feature-first colocation
│ ├── emoji/ # ✅ Perfect colocation example
│ │ ├── emoji.node.ts # EmojiNode definition
│ │ ├── emoji.plugin.tsx # Main plugin
│ │ ├── emoji.typeahead.tsx # Typeahead functionality
│ │ ├── emoji.renderer.tsx # EmojiPicker renderer
│ │ ├── emoji.store.ts # State management
│ │ ├── emoji.types.ts # Type definitions
│ │ ├── emoji.utils.ts # Utilities
│ │ ├── emoji.item.tsx # Item component
│ │ ├── emoji.{feature-name}.ts # What you need for the feature
│ │ └── index.ts # Exports
│ ├── image/ # 🔄 Improved colocation
│ │ ├── image.node.tsx # ImageNode definition
│ │ ├── image.plugin.tsx # Main plugin
│ │ ├── image.component.tsx # Image component
│ │ ├── image.types.ts # Type definitions
│ │ ├── image.{feature-name}.ts # What you need for the feature
│ │ ├── index.ts # Exports
│ │ └── [legacy folders] # Backward compatibility
│ ├── mention/ # 🔄 New colocation structure
│ │ ├── mention.node.ts # MentionNode definition
│ │ ├── mention.plugin.tsx # Main plugin
│ │ ├── mention.item.tsx # Mention item component
│ │ ├── mention.types.ts # Type definitions
│ │ ├── mention.{feature-name}.ts # What you need for the feature
│ │ └── index.ts # Exports
│ ├── .../ # 🔄 New colocation structure
│ │ ├── ...node.ts # ...Node definition
│ │ ├── ...plugin.tsx # Main plugin
│ │ ├── ...types.ts # Type definitions
│ │ ├── ....{feature-name}.ts # What you need for the feature
│ │ └── index.ts # Exports
│ └── editors/ # Core editor components
├── plugins/ # Legacy plugins (migrating to features/)
├── nodes/ # Legacy nodes (migrating to features/)
├── ui/ # Shared UI components
├── context/ # React contexts
├── hooks/ # Custom hooks
└── utils/ # Utility functions🚀 Usage
Emoji Feature
import { EmojiPlugin, EmojiTypeaheadPlugin } from '@/features/emoji';
function MyEditor() {
return (
<LexicalComposer>
<EmojiPlugin floatingAnchor={anchorElement} />
<EmojiTypeaheadPlugin floatingAnchor={anchorElement} />
</LexicalComposer>
);
}Image Feature
import { ImagePlugin, ImageNode, INSERT_IMAGE_COMMAND } from '@/features/image';
function MyEditor() {
const [editor] = useLexicalComposerContext();
const insertImage = (payload: ImagePayload) => {
editor.dispatchCommand(INSERT_IMAGE_COMMAND, payload);
};
return (
<LexicalComposer initialConfig={{ nodes: [ImageNode] }}>
<ImagePlugin activeEditor={editor} />
</LexicalComposer>
);
}Mention Feature
import { MentionPlugin, MentionNode } from '@/features/mention';
function MyEditor() {
const fetchMentionOptions = async (query: string) => {
// Fetch users, teams, channels, etc.
return await api.searchMentions(query);
};
return (
<LexicalComposer initialConfig={{ nodes: [MentionNode] }}>
<MentionPlugin
floatingAnchor={anchorElement}
fetchMentionOptions={fetchMentionOptions}
minLength={1}
maxResults={10}
/>
</LexicalComposer>
);
}🎨 Styling
Each feature includes its own SCSS file with scoped styles:
// features/emoji/emoji.scss
.__RootEditor__ {
.emoji-container {
// Emoji-specific styles
}
}
// features/image/image.scss
.__RootEditor__ {
.image-container {
// Image-specific styles
}
}
// features/mention/mention.scss
.__RootEditor__ {
.mention {
// Mention-specific styles
}
}🔧 Development Guidelines
Adding a New Feature
- Create feature directory:
features/{feature-name}/ - Follow naming convention:
{feature}.{type}.{ext} - Include all related files:
- Node definition (
{feature}.node.ts) - Plugin (
{feature}.plugin.tsx) - Types (
{feature}.types.ts) - Styles (
{feature}.scss) - Components as needed
- Barrel export (
index.ts)
- Node definition (
Example: Creating a Table Feature
features/table/
├── table.node.ts # TableNode, TableRowNode, TableCellNode
├── table.plugin.tsx # TablePlugin with commands
├── table.toolbar.tsx # Table toolbar component
├── table.types.ts # Table-related types
├── table.utils.ts # Table utilities
├── table.scss # Table styling
└── index.ts # Exports🧪 Testing
Each feature should include its own test files:
features/emoji/
├── emoji.node.test.ts
├── emoji.plugin.test.tsx
└── emoji.utils.test.ts📦 Benefits of Colocation
- High Cohesion: Related functionality is grouped together
- Low Coupling: Features are independent and reusable
- Easy Maintenance: Changes are localized to one directory
- Clear Responsibility: File names indicate exact purpose
- Better DX: Developers can focus on one feature at a time
- Scalability: Easy to add, remove, or modify features
🔄 Migration Status
✅ Core Text Formatting (Internalized)
- ✅ Paragraph: Custom paragraph node with text alignment and indentation support
- ✅ Heading: H1-H6 headings with markdown syntax (## heading) and styling
- ✅ Quote: Blockquotes, pullquotes, and citations with author/source metadata
✅ Feature Nodes (Colocation Pattern)
- ✅ Emoji: Complete colocation implementation
- ✅ Image: Improved with colocation (legacy support maintained)
- ✅ Mention: Migrated from plugins/ to features/
- ✅ Hashtag: Auto-detection and styling with colocation pattern
- ✅ Link: Auto-link detection with URL validation and colocation pattern
📋 Remaining Migrations
- 📋 Tables: Migrate to features/ with colocation
- 📋 Code blocks: Migrate to features/ with colocation
- 📋 Lists: Migrate to features/ with colocation
- 📋 Legacy plugins: Complete migration to features/
🤝 Contributing
When contributing to this editor:
- Follow the colocation pattern for new features
- Use the established naming conventions
- Include comprehensive TypeScript types
- Add feature-specific styling in SCSS files
- Create exports using
export * frompattern inindex.ts - Maintain backward compatibility when refactoring
This architecture ensures our editor remains maintainable, scalable, and developer-friendly as it grows in complexity and features.
