This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Install dependencies
yarn install
# Build all packages (cleans dist directories first)
yarn build
# Development - start dev server (web/next-app-example)
yarn dev
# Dev server + watch specific packages (recommended)
yarn dev --filter=@yoopta/editor --filter=@yoopta/paragraph
# Build a single package
yarn build --filter=@yoopta/editoryarn test # Run all tests with Vitest
yarn test:run # Single test run (no watch)
yarn test:watch # Watch mode
yarn test:ui # Vitest UI
yarn test:plugins # Plugin tests only
yarn test:core # Core package tests
yarn test:marks # Mark tests
yarn test:integration # Playwright e2e tests
yarn coverage # Coverage reportyarn lint # ESLint
yarn lint:fix # ESLint with auto-fix
yarn prettier # Format with Prettier
yarn format # Run all formattersYoopta-Editor is a React rich-text editor built on Slate.js with a plugin architecture.
packages/
├── core/
│ ├── editor/ # @yoopta/editor - Main editor component, YooEditor API
│ ├── collaboration/# @yoopta/collaboration - Real-time collaboration (Yjs)
│ ├── ui/ # @yoopta/ui - Toolbar, ActionMenu, BlockOptions, etc.
│ └── exports/ # @yoopta/exports - HTML/Markdown/PlainText serializers
├── plugins/ # Block plugins (see list below)
├── marks/ # @yoopta/marks - Text formatting (Bold, Italic, etc.)
├── themes/ # Theme packages (base, material, shadcn)
web/
└── next-app-example/ # Development playground and examples
Available Plugins: accordion, blockquote, callout, carousel, code, divider, embed, emoji, file, headings, image, link, lists, math, mention, paragraph, steps, table, table-of-contents, tabs, video
Created via createYooptaEditor({ plugins, marks, value }). Key methods:
Block Operations:
insertBlock,updateBlock,deleteBlock,duplicateBlocktoggleBlock- Change block type while preserving contentmoveBlock,focusBlock,mergeBlock,splitBlockincreaseBlockDepth,decreaseBlockDepth- Nesting controlgetBlock
Element Operations:
insertElement,updateElement,deleteElementgetElement,getElements,getElementEntry,getElementPathisElementEmpty
Element Builder (editor.y):
// Create block element
editor.y('paragraph', { props: {...}, children: [...] })
// Create text node with marks
editor.y.text('Hello', { bold: true, italic: true })
// Create inline element (e.g., link)
editor.y.inline('link', { props: { url: '...' }, children: [...] })Events: on, off, once, emit for: change, focus, blur, block:copy, path-change
Parsers: getHTML, getMarkdown, getPlainText, getEmail
History: undo, redo, batchOperations
Import from @yoopta/editor:
import { Blocks, Elements, Marks, Selection } from '@yoopta/editor';Blocks API - block-level operations:
Blocks.insertBlock(editor, { ... })
Blocks.deleteBlock(editor, { ... })
Blocks.updateBlock(editor, { ... })
Blocks.moveBlock(editor, { ... })
Blocks.duplicateBlock(editor, { ... })
Blocks.toggleBlock(editor, { ... })
Blocks.focusBlock(editor, { ... })
Blocks.splitBlock(editor, { ... })
Blocks.mergeBlock(editor, { ... })
Blocks.increaseBlockDepth(editor, { ... })
Blocks.decreaseBlockDepth(editor, { ... })
Blocks.getBlock(editor, { ... })
Blocks.getBlockSlate(editor, { ... })
Blocks.buildBlockData(editor, { ... })Elements API - element-level operations within blocks:
Elements.insertElement(editor, { ... })
Elements.updateElement(editor, { ... })
Elements.deleteElement(editor, { ... })
Elements.getElement(editor, { ... })
Elements.getElements(editor, { ... })
Elements.getElementEntry(editor, { ... })
Elements.getElementPath(editor, { ... })
Elements.getParentElementPath(editor, { ... })
Elements.getElementChildren(editor, { ... })
Elements.getRootElement(editor, { ... })
Elements.isElementEmpty(editor, { ... })Marks API - text formatting:
Marks.update(editor, {
type: 'highlight',
value: { color: 'red', backgroundColor: '#ffff00' },
at: [0, 1, 2], // block indices
});// Content structure
YooptaContentValue = Record<blockId, YooptaBlockData>
YooptaBlockData = {
id: string;
type: string; // PascalCase: "Paragraph", "HeadingOne"
value: SlateElement[]; // Slate elements with kebab-case types
meta: { order, depth, align }
}
SlateElement = {
id: string;
type: string; // kebab-case: "paragraph", "heading-one"
children: Descendant[];
props?: { nodeType: 'block' | 'inline' | 'void', ... }
}- Main component:
packages/core/editor/src/yoopta-editor.tsx - Editor types:
packages/core/editor/src/editor/types.ts - Block operations:
packages/core/editor/src/editor/blocks/ - Element operations:
packages/core/editor/src/editor/elements/ - Plugin types:
packages/core/editor/src/plugins/types.ts - Dev playground:
web/next-app-example/
const editor = useMemo(() => createYooptaEditor({
plugins: PLUGINS,
marks: MARKS,
value: initialValue,
}), []);
<YooptaEditor
editor={editor}
onChange={(value, { operations }) => { ... }}
placeholder="Type / to open menu"
style={{ width: 750 }}
>
<YooptaToolbar />
<YooptaFloatingBlockActions />
<YooptaSlashCommandMenu />
<YooptaActionMenuList />
</YooptaEditor>Plugins are instances of YooptaPlugin<TElementMap, TOptions>. Each plugin follows this structure:
packages/plugins/{plugin-name}/
├── src/
│ ├── index.ts # Main export
│ ├── types.ts # ElementMap, ElementProps types
│ ├── commands/ # Plugin commands (e.g., ParagraphCommands)
│ ├── extensions/ # Slate editor extensions (with* functions)
│ ├── plugin/ # Main plugin definition
│ └── utils/ # Helper functions
├── package.json
├── rollup.config.js
└── tsconfig.json
Creating a Plugin:
const MyPlugin = new YooptaPlugin<MyElementMap>({
type: 'MyPlugin', // PascalCase
elements: {
'my-element': { // kebab-case
render: (props) => <div>{props.children}</div>,
props: { nodeType: 'block' }
}
},
parsers: { html: {...}, markdown: {...} },
commands: MyPluginCommands,
extensions: [withMyPlugin],
});Extending Plugins:
const CustomImage = Image.extend({
options: { upload: customUploadFn },
injectElementsFromPlugins: [Paragraph, Lists.BulletedList], // Only for leaf elements
});NodeType Values: 'block' | 'inline' | 'void' | 'inlineVoid'
import { createYooptaMark } from '@yoopta/editor';
export const Bold = createYooptaMark<BoldMarkProps>({
type: 'bold', // lowercase
hotkey: 'mod+b',
render: (props) => <strong>{props.children}</strong>,
});Available marks: Bold, Italic, Underline, Strike, CodeMark, Highlight
Themes provide styled versions of plugin renderers:
packages/themes/{theme-name}/
├── src/
│ ├── {plugin-name}/
│ │ ├── elements/ # Custom render components
│ │ └── styles.css
│ └── index.ts
Available themes: base, material, shadcn
packages/core/exports/src/
├── html/ # serialize.ts, deserialize.ts
├── markdown/ # serialize.ts, deserialize.ts
├── email/ # serialize.ts
└── text/ # serialize.ts, deserialize.ts
Plugin Parsers:
parsers: {
html: {
deserialize: { nodeNames: ['P'], parse: (el, editor) => SlateElement },
serialize: (element, text, blockMeta) => '<p>...</p>'
},
markdown: { serialize: (element) => '...' },
email: { serialize: (element, text, blockMeta) => '...' }
}Must be called within <YooptaEditor> children:
useYooptaEditor()- Get editor instanceuseYooptaReadOnly()- Check read-only stateuseYooptaFocused()- Check focus stateuseBlockData(blockId)- Get block datauseYooptaPluginOptions(type)- Get plugin options
Type Case Sensitivity (Critical!):
- Block types: PascalCase (
"Paragraph","HeadingOne","Image") - Element types: kebab-case (
"paragraph","heading-one","image")
Block vs Element:
- Block (
YooptaBlockData): Top-level content container withtype,meta,order,depth - Element (
SlateElement): Nested structure within block'svaluearray
Plugin Injection:
injectElementsFromPluginsONLY works on leaf elements (elements without children)- Throws error if applied to parent elements
- Used by: Accordion, Carousel, Tabs, table-of-contents
Slate Extensions:
- Named
with*(e.g.,withParagraph,withParagraphNormalize) - Signature:
(slate: SlateEditor, editor: YooEditor) => SlateEditor
Rollup (via config/rollup.js):
- ES modules with TypeScript declarations
- CSS extraction with PostCSS + Tailwind support
- CSS class prefixes per plugin:
yoo-p-(paragraph),yoo-code-(code)
Workspace Commands:
yarn dev --filter=@yoopta/editor # Dev server + watch one package
yarn build --filter=@yoopta/editor # Build single package
yarn dev --filter=@yoopta/editor --filter=@yoopta/paragraph # Watch multiplepackages/core/editor/src/
├── editor/
│ ├── blocks/ # Block operations (insert, delete, move, merge, split)
│ ├── elements/ # Element operations within blocks
│ ├── textFormats/ # Mark operations
│ ├── selection/ # Selection utilities
│ └── core/ # applyTransforms, setEditorValue, history
├── utils/
│ ├── generateId.ts
│ ├── editor-builders.ts
│ └── hotkeys.ts
└── plugins/
├── create-yoopta-plugin.tsx # YooptaPlugin class
└── types.ts