Skip to content

Latest commit

 

History

History
375 lines (289 loc) · 10.4 KB

File metadata and controls

375 lines (289 loc) · 10.4 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Build & Development Commands

# 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/editor

Testing

yarn 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 report

Linting & Formatting

yarn lint              # ESLint
yarn lint:fix          # ESLint with auto-fix
yarn prettier          # Format with Prettier
yarn format            # Run all formatters

Architecture Overview

Yoopta-Editor is a React rich-text editor built on Slate.js with a plugin architecture.

Monorepo Structure (Turborepo + Yarn Berry)

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

YooEditor API

Created via createYooptaEditor({ plugins, marks, value }). Key methods:

Block Operations:

  • insertBlock, updateBlock, deleteBlock, duplicateBlock
  • toggleBlock - Change block type while preserving content
  • moveBlock, focusBlock, mergeBlock, splitBlock
  • increaseBlockDepth, decreaseBlockDepth - Nesting control
  • getBlock

Element Operations:

  • insertElement, updateElement, deleteElement
  • getElement, getElements, getElementEntry, getElementPath
  • isElementEmpty

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

Namespace APIs

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
});

Data Model

// 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', ... }
}

Key Files

  • 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/

Usage Pattern

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>

Plugin Structure

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'

Marks (Text Formatting)

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

Theme Structure

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

Export Formats

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) => '...' }
}

Available Hooks

Must be called within <YooptaEditor> children:

  • useYooptaEditor() - Get editor instance
  • useYooptaReadOnly() - Check read-only state
  • useYooptaFocused() - Check focus state
  • useBlockData(blockId) - Get block data
  • useYooptaPluginOptions(type) - Get plugin options

Important Conventions

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 with type, meta, order, depth
  • Element (SlateElement): Nested structure within block's value array

Plugin Injection:

  • injectElementsFromPlugins ONLY 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

Build System

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 multiple

Key Directories

packages/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