npm package: @alggtta/editor
Description: Universal Editor with Tiptap 3.x and DaisyUI 5
Latest version: 2.1.0

# @alggtta/editor

[![npm version](https://img.shields.io/npm/v/@alggtta/editor?color=blue)](https://www.npmjs.com/package/@alggtta/editor)
[![License: MIT](https://img.shields.io/npm/l/@alggtta/editor)](https://github.com/hjkwon-github/alggtta-ed/blob/main/LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)

A powerful, extensible rich text editor built on **Tiptap 3.x** and **React** with AI integration, real-time collaboration, and 25+ features out of the box.

**[Live Demo](https://demo.alggtta.com)** | **[Landing Page](https://alggtta.com)** | **[GitHub](https://github.com/hjkwon-github/alggtta-ed)**

---

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Component API](#component-api)
  - [AlggttaEditor](#alggttaeditor-)
  - [ReadOnlyViewer](#readonlyviewer-)
- [Ref API (External Access)](#ref-api-external-access)
- [Custom Extensions](#custom-extensions)
  - [React NodeView Extensions](#react-nodeview-extensions)
  - [Wrapper Blocks](#wrapper-blocks-content-block)
  - [Built-in GuideFlow Extensions](#built-in-guideflow-extensions)
- [Custom Slash Commands](#custom-slash-commands)
  - [SlashCommand Interface](#slashcommand-interface)
  - [External Callbacks in Commands](#external-callbacks-in-commands)
- [Image Upload](#image-upload)
- [AI Integration](#ai-integration)
  - [AIRequest Interface](#airequest-interface)
  - [Streaming Responses](#streaming-responses)
  - [AI Diff Review](#ai-diff-review)
  - [External AI Panel Integration](#external-ai-panel-integration)
- [i18n (Internationalization)](#i18n-internationalization)
  - [Using locale Presets](#using-locale-presets)
  - [Custom Label Overrides](#custom-label-overrides)
  - [useLabels Hook](#uselabels-hook)
- [ReadOnlyViewer Advanced](#readonlyviewer-advanced)
  - [Custom Extensions in Viewer](#custom-extensions-in-viewer)
  - [Content Filtering](#content-filtering)
  - [filterNodeTypes Utility](#filternodetypes-utility)
- [Content Compatibility](#content-compatibility)
- [Markdown Import/Export](#markdown-importexport)
- [Theming](#theming)
- [Collaboration (Yjs)](#collaboration-yjs)
- [Comments & Threads](#comments--threads)
- [Database Tables](#database-tables)
- [Code Blocks](#code-blocks)
- [Edge Runtime & Next.js Compatibility](#edge-runtime--nextjs-compatibility)
- [CSS Isolation](#css-isolation)
- [Bundle Size](#bundle-size)
- [TypeScript Exports](#typescript-exports)
- [Browser Support](#browser-support)
- [License](#license)

---

## Features

- **Rich Text Editing** — Headings, lists, task lists, quotes, code blocks, tables, images, and more
- **Slash Commands** — Type `/` to quickly insert any block type with fuzzy search
- **Bubble Menu** — Context-aware floating toolbar for inline formatting
- **Drag & Drop** — Drag handles to reorder any block
- **Custom Extensions** — Add domain-specific blocks with React NodeView support
- **AI Writing Assistant** — Streaming text generation with diff-based accept/reject
- **AI Autocompletion** — Inline suggestions as you type, powered by any LLM
- **AI Image Generation** — Generate images from text prompts
- **Real-time Collaboration** — Multi-user editing via Yjs with presence cursors
- **Comments & Threads** — Inline comment threads with resolve/unresolve workflow
- **Offline Support** — Continue editing offline with automatic sync on reconnect
- **Database Tables** — Full-featured tables with sorting, column resizing, and click-to-edit cells (TanStack Table v8)
- **Code Blocks** — 190+ languages with syntax highlighting and execution support
- **Markdown Import/Export** — Seamless conversion between Markdown and editor content
- **6 Built-in Themes** — Neuroscience-inspired themes using OKLCH color space
- **i18n Support** — Built-in English and Korean locales with full label override API
- **Version History** — Checkpoint-based document snapshots
- **Read-Only Viewer** — Lightweight component with custom extension and content filtering support
- **Ref API** — Programmatic editor access via React ref (`insertContent`, `getJSON`, etc.)
- **TypeScript First** — Full type safety with comprehensive type exports

---

## Installation

```bash
npm install @alggtta/editor
```

### Peer Dependencies

Required:

```bash
npm install react react-dom @tiptap/core @tiptap/react @tiptap/starter-kit @tiptap/pm @heroicons/react
```

Optional (for collaboration):

```bash
npm install yjs y-prosemirror y-protocols y-indexeddb @tiptap/extension-collaboration @tiptap/y-tiptap
```

Optional (for tables):

```bash
npm install @tanstack/react-table
```

Optional (for math):

```bash
npm install katex
```

---

## Quick Start

```tsx
import { AlggttaEditor } from '@alggtta/editor'
import '@alggtta/editor/styles.css'

function App() {
  return (
    <AlggttaEditor
      token="your-license-token"
      content="<p>Start writing...</p>"
      onUpdate={(editor) => {
        const json = editor.getJSON()
        console.log(json)
      }}
    />
  )
}
```

---

## Component API

### `<AlggttaEditor />`

The main editor component with built-in slash menu, bubble menu, and drag handle. Supports `React.forwardRef` for programmatic access.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `token` | `string` | **(required)** | License token for production use |
| `content` | `string \| JSONContent` | `''` | Initial content (HTML string or Tiptap JSON) |
| `placeholder` | `string` | `"Type '/' for commands..."` | Placeholder text when editor is empty |
| `editable` | `boolean` | `true` | Whether the editor is editable |
| `autofocus` | `boolean \| 'start' \| 'end'` | `false` | Auto-focus the editor on mount |
| `theme` | `string` | `undefined` | DaisyUI theme name (sets `data-theme` on wrapper) |
| `className` | `string` | `''` | Additional CSS classes on the editor wrapper |
| `locale` | `'en' \| 'ko'` | `'en'` | Built-in locale preset for UI labels |
| `labels` | `Partial<EditorLabels>` | `undefined` | Override individual UI labels (merged on top of locale) |
| `onUpdate` | `(editor: Editor) => void` | `undefined` | Called on every document change |
| `onCreate` | `(editor: Editor) => void` | `undefined` | Called when editor is initialized |
| `onSelectionUpdate` | `(editor: Editor) => void` | `undefined` | Called when selection changes |
| `onBlur` | `(editor: Editor) => void` | `undefined` | Called when editor loses focus |
| `onFocus` | `(editor: Editor) => void` | `undefined` | Called when editor gains focus |
| `onImageUpload` | `(file: File) => Promise<string>` | `undefined` | Custom image upload handler (returns URL) |
| `onFileUpload` | `(file: File) => Promise<string>` | `undefined` | Custom file upload handler (returns URL) |
| `onAIRequest` | `OnAIRequest` | `undefined` | AI text generation handler (enables AI features) |
| `onExecuteCode` | `(req: CodeExecutionRequest) => Promise<CodeExecutionResult>` | `undefined` | Code execution handler for code blocks |
| `verifyEndpoint` | `string` | `'https://cdn.alggtta.com'` | CDN gateway URL for license verification |
| `extensionConfig` | `ExtensionConfig` | `undefined` | Extension configuration (see below) |

#### ExtensionConfig

Passed via the `extensionConfig` prop:

| Field | Type | Description |
|-------|------|-------------|
| `customExtensions` | `AnyExtension[]` | Additional Tiptap extensions to register |
| `customSlashCommands` | `SlashCommand[]` | Additional slash commands (merged with defaults) |
| `collaboration` | `{ document: YDoc, field?: string }` | Yjs collaboration config |
| `onAISuggest` | `(context: string, signal: AbortSignal) => Promise<string>` | AI autocomplete handler |
| `aiAutocompletionDebounceMs` | `number` | Autocomplete debounce (default: 600ms) |
| `enableCounter` | `boolean` | Enable heading/figure auto-numbering |

### `<ReadOnlyViewer />`

Lightweight viewer for displaying editor content without editing capabilities. No ProseMirror state — renders pure static HTML.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `content` | `JSONContent \| null` | **(required)** | Tiptap JSON document to render |
| `className` | `string` | `''` | Additional CSS classes |
| `extensions` | `AnyExtension[]` | `[StarterKit]` | Custom extensions for rendering custom node types |
| `contentFilter` | `(doc: JSONContent) => JSONContent` | `undefined` | Pre-process document before rendering |

---

## Ref API (External Access)

Use `React.useRef` to access the editor instance and convenience methods from parent components.

```tsx
import { useRef } from 'react'
import { AlggttaEditor } from '@alggtta/editor'
import type { AlggttaEditorRef } from '@alggtta/editor'

function MyPage() {
  const editorRef = useRef<AlggttaEditorRef>(null)

  const handleAIResult = (html: string) => {
    editorRef.current?.insertContent(html)
  }

  const handleSave = () => {
    const json = editorRef.current?.getJSON()
    fetch('/api/save', { method: 'POST', body: JSON.stringify(json) })
  }

  const handleGetSelection = () => {
    const selected = editorRef.current?.getSelectedText()
    console.log('Selected:', selected)
  }

  return (
    <>
      <AlggttaEditor ref={editorRef} token="..." />
      <button onClick={handleSave}>Save</button>
      <button onClick={handleGetSelection}>Get Selection</button>
    </>
  )
}
```

### AlggttaEditorRef Methods

| Method | Signature | Description |
|--------|-----------|-------------|
| `editor` | `Editor \| null` | The underlying Tiptap Editor instance |
| `insertImage` | `(url: string, attrs?: { alt?, title?, width? }) => void` | Insert an image at cursor |
| `insertContent` | `(content: string \| JSONContent) => void` | Insert HTML or JSON at cursor |
| `getSelectedText` | `() => string` | Get the currently selected text |
| `getJSON` | `() => JSONContent` | Get full document as Tiptap JSON |
| `getHTML` | `() => string` | Get full document as HTML |
| `focus` | `(position?: 'start' \| 'end' \| number \| boolean) => void` | Focus the editor |

### Alternative: onCreate callback

If you prefer callbacks over refs:

```tsx
const [editor, setEditor] = useState<Editor | null>(null)

<AlggttaEditor
  token="..."
  onCreate={(editor) => setEditor(editor)}
/>

// Then use editor?.getJSON(), editor?.commands.insertContent(), etc.
```

---

## Custom Extensions

### React NodeView Extensions

Pass custom Tiptap extensions with React NodeViews via `extensionConfig.customExtensions`. The editor fully supports:

- `atom: true` blocks (no internal editing, attribute-only)
- `content: "block+"` wrapper blocks (contain other blocks)
- `ReactNodeViewRenderer` for custom React components
- `addCommands()` for programmatic insertion

```tsx
import { Node, mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'

// Example: MapBlock Extension
const MapBlockExtension = Node.create({
  name: 'mapBlock',
  group: 'block',
  atom: true,

  addAttributes() {
    return {
      location: { default: '' },
      lat: { default: null },
      lng: { default: null },
    }
  },

  addNodeView() {
    return ReactNodeViewRenderer(MapBlockComponent)
  },

  parseHTML() {
    return [{ tag: 'div[data-type="map-block"]' }]
  },

  renderHTML({ HTMLAttributes }) {
    return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'map-block' }), 0]
  },
})

// Usage
<AlggttaEditor
  token="..."
  extensionConfig={{
    customExtensions: [MapBlockExtension, HotelBlockExtension],
  }}
/>
```

### Wrapper Blocks (`content: "block+"`)

Blocks that wrap other blocks are fully supported:

```tsx
const StaffOnlyBlock = Node.create({
  name: 'staffOnlyBlock',
  group: 'block',
  content: 'block+',    // Contains child blocks
  defining: true,
  draggable: true,

  addNodeView() {
    return ReactNodeViewRenderer(StaffOnlyBlockNodeView)
  },

  addCommands() {
    return {
      setStaffOnlyBlock: () => ({ commands }) => commands.wrapIn('staffOnlyBlock'),
      toggleStaffOnlyBlock: () => ({ commands }) => commands.toggleWrap('staffOnlyBlock'),
    }
  },
})
```

### Built-in GuideFlow Extensions

Pre-built domain extensions for travel guide apps:

```tsx
import {
  guideFlowExtensions,
  guideFlowSlashCommands,
  // Individual imports:
  MapBlockExtension,
  HotelBlockExtension,
  ContactBlockExtension,
  ScheduleBlockExtension,
  StaffOnlyBlockExtension,
  DividerBlockExtension,
} from '@alggtta/editor'

<AlggttaEditor
  token="..."
  extensionConfig={{
    customExtensions: guideFlowExtensions,
    customSlashCommands: guideFlowSlashCommands,
  }}
/>
```

---

## Custom Slash Commands

### SlashCommand Interface

```typescript
interface SlashCommand {
  title: string                                      // Display name
  description?: string                               // Shown below title
  icon?: React.ComponentType<{ className?: string }> // Heroicon component
  command: (editor: Editor) => void                  // Executed when selected
  keywords?: string[]                                // Fuzzy search terms
  category?: string                                  // Group header in menu
  shortcut?: string                                  // Keyboard shortcut hint
  requiresAI?: boolean                               // Only show when AI is enabled
}
```

Custom commands are **merged (appended)** with the built-in defaults — they do not replace them. Commands are grouped by `category` with section headers in the slash menu.

> **Note:** The `command` function receives only `(editor: Editor)`. The `/` trigger character is automatically deleted before your command runs — you don't need to handle `range` deletion.

### External Callbacks in Commands

Since `customSlashCommands` is defined in your component scope, you can capture external functions via closures:

```tsx
function MyEditor({ onOpenAIPanel, guideId }) {
  const customCommands = useMemo(() => [
    {
      title: 'AI Write',
      description: 'Open AI writing panel',
      icon: SparklesIcon,
      category: 'AI',
      keywords: ['ai', 'write', 'generate'],
      command: (editor) => {
        // Access external state/callbacks via closure
        onOpenAIPanel('write')
      },
    },
    {
      title: 'Map',
      description: 'Insert a map block',
      icon: MapPinIcon,
      category: 'Blocks',
      keywords: ['map', 'location', 'place'],
      command: (editor) => {
        editor.commands.setMapBlock({ location: '' })
      },
    },
  ], [onOpenAIPanel])

  return (
    <AlggttaEditor
      token="..."
      extensionConfig={{ customSlashCommands: customCommands }}
    />
  )
}
```

---

## Image Upload

The `onImageUpload` callback handles all image insertion scenarios through a single handler:

```tsx
<AlggttaEditor
  token="..."
  onImageUpload={async (file: File) => {
    const formData = new FormData()
    formData.append('file', file)
    const res = await fetch('/api/upload/image', { method: 'POST', body: formData })
    const { url } = await res.json()
    return url // Editor inserts the image with this URL
  }}
/>
```

### Covered Scenarios

| Scenario | Handled |
|----------|---------|
| Toolbar image button / slash command `/image` | Yes |
| Drag and drop image files onto editor | Yes |
| Ctrl+V / Cmd+V clipboard image paste | Yes |
| File input dialog | Yes |

### Upload UX

During upload, the editor shows:
- `previewUrl` — local blob URL for instant preview
- `uploadProgress` — 0-100 progress indicator
- `uploadError` — error state with retry option

Once your handler resolves, the `src` attribute is replaced with the returned URL.

### File Uploads

Non-image files are handled separately via `onFileUpload`:

```tsx
onFileUpload={async (file) => {
  // Upload to your storage
  return downloadUrl
}}
```

---

## AI Integration

Enable AI features by providing the `onAIRequest` callback. When set, the editor shows:
- AI slash commands (`/ai-write`, `/ai-translate`, `/ai-summarize`)
- AI button in the bubble menu
- Floating AI prompt input
- AI side panel with Write, Translate, Image, and Ingest tabs

### AIRequest Interface

```typescript
interface AIRequest {
  type?: 'chat' | 'translate' | 'summarize' | 'write' | 'ingest' | 'image' | string
  prompt: string              // User's input prompt
  selectedText?: string       // Currently selected text (if any)
  context?: string            // Surrounding text context
  signal?: AbortSignal        // For cancellation support
}

type OnAIRequest = (request: AIRequest) =>
  | Promise<ReadableStream<string> | string>   // Async streaming or one-shot
  | ReadableStream<string>                      // Sync streaming
  | string                                       // Sync one-shot
```

### Streaming Responses

Return a `ReadableStream<string>` for real-time streaming into the editor:

```tsx
<AlggttaEditor
  token="..."
  onAIRequest={async (request) => {
    const response = await fetch('/api/ai/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        prompt: request.prompt,
        context: request.context,
        selectedText: request.selectedText,
      }),
      signal: request.signal, // Pass abort signal for cancellation
    })
    return response.body! // ReadableStream — editor inserts chunks in real-time
  }}
/>
```

**Streaming behavior:**
1. An AI skeleton placeholder appears at the cursor
2. First chunk arrives — skeleton is replaced with real content
3. Subsequent chunks are appended in real-time
4. All streaming changes are batched into a single undo entry
5. HTML is sanitized via DOMPurify before insertion

### AI Diff Review

After AI generates content, users can review changes with accept/reject:

```typescript
import type { UseAIDiffOptions, UseAIDiffReturn, AIDiffChange } from '@alggtta/editor'
```

The diff system uses LCS (Longest Common Subsequence) for block-level change detection with individual and batch accept/reject.

### External AI Panel Integration

You can use slash commands to trigger external AI panels instead of (or in addition to) the built-in AI:

```tsx
const customCommands = [
  {
    title: 'AI Write',
    category: 'AI',
    command: (editor) => {
      // Open your external AI panel
      onOpenExternalAIPanel('write')
    },
  },
  {
    title: 'AI Image',
    category: 'AI',
    command: (editor) => {
      onOpenExternalAIPanel('image')
    },
  },
]
```

### AI Autocompletion (Copilot-style)

Enable inline ghost-text suggestions:

```tsx
<AlggttaEditor
  token="..."
  extensionConfig={{
    onAISuggest: async (context, signal) => {
      const res = await fetch('/api/ai/complete', {
        method: 'POST',
        body: JSON.stringify({ context }),
        signal,
      })
      return res.text()
    },
    aiAutocompletionDebounceMs: 600,
  }}
/>
```

Tab to accept, Escape to dismiss.

---

## i18n (Internationalization)

The editor supports full UI text localization with built-in English (`en`) and Korean (`ko`) presets.

### Using locale Presets

```tsx
// Korean UI
<AlggttaEditor token="..." locale="ko" />

// English UI (default)
<AlggttaEditor token="..." locale="en" />
```

### Custom Label Overrides

Override specific labels while keeping the rest from the locale preset:

```tsx
<AlggttaEditor
  token="..."
  locale="ko"
  labels={{
    placeholder: '여기에 입력하세요...',
    bubbleMenu: {
      bold: '굵게 (Ctrl+B)',
    },
    ai: {
      promptPlaceholder: 'AI에게 물어보세요...',
    },
  }}
/>
```

### EditorLabels Structure

```typescript
interface EditorLabels {
  placeholder: string
  bubbleMenu: { bold, italic, underline, strike, highlight, aiAssistant }
  dragHandle: { addBlock, blockActions, delete, duplicate, turnInto, text, heading1-3, bulletList, numberedList, taskList, quote, codeBlock }
  slashMenu: { noResults }
  ai: { assistant, generating, promptPlaceholder, stopGenerating, sendPrompt, close, enterToSend, escToClose, write, translate, image, ingest }
  mobileToolbar: { addBlock, bold, italic, underline, strike, highlight, moreOptions, heading1-3, quote, bulletList, numberedList, taskList, codeBlock, moveUp, moveDown, duplicate, delete, link, undo, redo, linkPlaceholder, applyLink, cancel }
  mobileBlockMenu: { title, close, text, textDesc, heading1, heading1Desc, ... }
  comments: { title, noComments, noCommentsHint, placeholder, reply, edit, save, cancel, delete, resolve, unresolve, resolved, confirmDelete, justNow, minutesAgo(m), hoursAgo(h), daysAgo(d), ... }
  database: { untitled, newRow, addColumn, deleteRow, dragToReorder, filter, new, uncategorized, boardViewHint }
}
```

### useLabels Hook

Access labels from within custom child components:

```tsx
import { useLabels } from '@alggtta/editor'

function MyCustomComponent() {
  const labels = useLabels()
  return <span>{labels.comments.title}</span>
}
```

### Available Locale Presets

```tsx
import { labelsEn, labelsKo } from '@alggtta/editor'
import type { EditorLabels } from '@alggtta/editor'

// Create a custom locale based on English
const myLabels: EditorLabels = {
  ...labelsEn,
  placeholder: 'Write something amazing...',
}
```

---

## ReadOnlyViewer Advanced

### Custom Extensions in Viewer

When your documents contain custom node types, pass the same extensions to the viewer so it can render them via `renderHTML()`:

```tsx
import { ReadOnlyViewer } from '@alggtta/editor'
import StarterKit from '@tiptap/starter-kit'

<ReadOnlyViewer
  content={document}
  extensions={[StarterKit, MapBlockExtension, HotelBlockExtension]}
/>
```

Without `extensions`, only StarterKit nodes are rendered. Unknown node types are silently skipped.

### Content Filtering

Filter out specific nodes before rendering (e.g., hide staff-only content in public views):

```tsx
import { ReadOnlyViewer, filterNodeTypes } from '@alggtta/editor'

// Method A: Using the built-in filterNodeTypes utility
<ReadOnlyViewer
  content={document}
  extensions={[StarterKit, StaffOnlyBlockExtension]}
  contentFilter={(doc) => filterNodeTypes(doc, ['staffOnlyBlock'])}
/>

// Method B: Custom filter function
<ReadOnlyViewer
  content={document}
  contentFilter={(doc) => {
    // Your custom filtering logic
    return transformedDoc
  }}
/>
```

### filterNodeTypes Utility

Recursively removes specific node types from a Tiptap JSON document:

```typescript
import { filterNodeTypes } from '@alggtta/editor'

const publicDoc = filterNodeTypes(document, ['staffOnlyBlock', 'internalNote'])
// All staffOnlyBlock and internalNote nodes (and their children) are removed
```

---

## External Integration Hooks

These hooks operate **outside** the editor (in side panels, dialogs, etc.) but can optionally interact with the editor via `AlggttaEditorRef`.

### Edit Lock (`useEditLock`)

Manages document-level edit locks with automatic heartbeat renewal.

```tsx
import { useEditLock } from '@alggtta/editor'

function DocumentEditor({ docId }: { docId: string }) {
  const { isMyLock, editable, lockOwner, acquire, release, error, isPending } = useEditLock({
    documentId: docId,
    autoAcquire: true,
    heartbeatInterval: 30_000,
    callbacks: {
      acquireLock: async (signal) => {
        const res = await fetch(`/api/docs/${docId}/lock`, { method: 'POST', signal })
        return res.json() // { lockOwner, isMyLock, expiresAt }
      },
      releaseLock: async (signal) => {
        await fetch(`/api/docs/${docId}/lock`, { method: 'DELETE', signal })
      },
      heartbeat: async (signal) => {
        const res = await fetch(`/api/docs/${docId}/lock/heartbeat`, { method: 'POST', signal })
        return res.json()
      },
    },
  })

  if (!editable) return <p>Locked by {lockOwner}</p>
  return <AlggttaEditor token="..." editable={editable} />
}
```

### AI Document Parsing (`useAIIngest`)

Parses uploaded files (PDF, DOCX, etc.) via an AI backend and optionally inserts the result.

```tsx
import { useRef } from 'react'
import { useAIIngest, AlggttaEditor } from '@alggtta/editor'
import type { AlggttaEditorRef } from '@alggtta/editor'

function IngestPanel() {
  const editorRef = useRef<AlggttaEditorRef>(null)
  const { ingest, isProcessing, result, insertToEditor } = useAIIngest({
    editorRef,
    onAIIngest: async ({ file, prompt, signal }) => {
      const form = new FormData()
      form.append('file', file)
      if (prompt) form.append('prompt', prompt)
      const res = await fetch('/api/ai/ingest', { method: 'POST', body: form, signal })
      return res.text()
    },
  })

  return (
    <>
      <input type="file" onChange={(e) => e.target.files?.[0] && ingest(e.target.files[0])} />
      {isProcessing && <p>Processing...</p>}
      {result && <button onClick={insertToEditor}>Insert to Editor</button>}
      <AlggttaEditor ref={editorRef} token="..." />
    </>
  )
}
```

### AI Full Translation (`useAITranslate`)

Translates the full document or selected text via an AI backend.

```tsx
import { useRef } from 'react'
import { useAITranslate, AlggttaEditor } from '@alggtta/editor'
import type { AlggttaEditorRef } from '@alggtta/editor'

function TranslatePanel() {
  const editorRef = useRef<AlggttaEditorRef>(null)
  const { translate, isTranslating, result, replaceInEditor } = useAITranslate({
    editorRef,
    onAITranslate: async ({ text, targetLang, signal }) => {
      const res = await fetch('/api/ai/translate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text, targetLang }),
        signal,
      })
      return res.text()
    },
  })

  return (
    <>
      <button onClick={() => translate({ targetLang: 'ko' })} disabled={isTranslating}>
        Translate to Korean
      </button>
      {result && <button onClick={replaceInEditor}>Replace Content</button>}
      <AlggttaEditor ref={editorRef} token="..." />
    </>
  )
}
```

### AI Multi-turn Image Editor (`useAIImageEditor`)

Multi-turn conversational image generation with iterative refinement.

```tsx
import { useRef } from 'react'
import { useAIImageEditor, AlggttaEditor } from '@alggtta/editor'
import type { AlggttaEditorRef } from '@alggtta/editor'

function ImageEditorPanel() {
  const editorRef = useRef<AlggttaEditorRef>(null)
  const { messages, sendMessage, isGenerating, currentImage, insertToEditor, reset } = useAIImageEditor({
    editorRef,
    maxHistory: 20,
    onAIImageEdit: async ({ prompt, history, currentImageUrl, style, signal }) => {
      const res = await fetch('/api/ai/image-edit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt, history, currentImageUrl, style }),
        signal,
      })
      const { imageUrl } = await res.json()
      return imageUrl
    },
  })

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
          {msg.imageUrl && <img src={msg.imageUrl} alt={msg.content} />}
        </div>
      ))}
      <input onKeyDown={(e) => {
        if (e.key === 'Enter') sendMessage(e.currentTarget.value)
      }} />
      {currentImage && <button onClick={() => insertToEditor()}>Insert to Editor</button>}
      <button onClick={reset}>New Conversation</button>
    </div>
  )
}
```

### Editor Preview Frame (`PreviewFrame`)

Device-shaped preview frame for responsive content viewing.

```tsx
import { PreviewFrame } from '@alggtta/editor'

function PreviewPanel({ content }) {
  return (
    <div className="flex gap-4">
      <PreviewFrame content={content} device="mobile" />
      <PreviewFrame content={content} device="tablet" scale={0.5} />
      <PreviewFrame content={content} device="desktop" scale={0.4} />
    </div>
  )
}
```

Props: `content`, `device` (`'mobile' | 'tablet' | 'desktop'`), `extensions`, `contentFilter`, `className`, `showChrome`, `scale`.

---

## Content Compatibility

### Tiptap 2.x → 3.x JSON

Tiptap 3.x is **backward compatible** with Tiptap 2.x JSON format. Documents saved with `editor.getJSON()` in Tiptap 2.x can be loaded directly:

```json
{
  "type": "doc",
  "content": [
    { "type": "paragraph", "content": [{ "type": "text", "text": "Hello" }] },
    { "type": "mapBlock", "attrs": { "location": "Seoul", "lat": 37.55, "lng": 126.97 } }
  ]
}
```

**Unknown node types:** If a node type (e.g., `mapBlock`) is not registered via `customExtensions`, ProseMirror silently skips it during rendering. However, the node **will be lost on save** if the extension is not registered — always register extensions for all node types in your documents.

### Recommended Save Format

Use `editor.getJSON()` (not `getHTML()`) for persistence:
- JSON is lossless and compact
- HTML requires DOMPurify sanitization and may lose attributes on round-trip

---

## Markdown Import/Export

```tsx
import { markdownToHtml, htmlToMarkdown } from '@alggtta/editor'

// Import Markdown → HTML (for editor content)
const html = markdownToHtml('# Hello\n\nThis is **bold** text.')

// Export HTML → Markdown
const markdown = htmlToMarkdown('<h1>Hello</h1><p>This is <strong>bold</strong> text.</p>')
```

---

## Theming

The editor ships with 6 neuroscience-inspired themes using OKLCH color space, plus all 30+ built-in DaisyUI themes:

| Theme | Type | Description |
|-------|------|-------------|
| `neuro-light` | Light | Solarized-inspired for sustained focus |
| `neuro-dark` | Dark | Warm tones to reduce melatonin suppression |
| `neuro-sepia` | Light | Optimized for dyslexia and Irlen Syndrome |
| `midnight` | Dark | Deep blue/purple for calm environments |
| `matcha` | Light | Soft green, nature-inspired |
| `sunset` | Light | Warm gradient tones |

### Applying a theme

```tsx
// Built-in theme
<AlggttaEditor token="..." theme="neuro-dark" />

// Any DaisyUI theme
<AlggttaEditor token="..." theme="dracula" />

// Custom theme — define in your CSS, then pass the name
<AlggttaEditor token="..." theme="my-custom-theme" />
```

Theming works via the `data-theme` attribute on the editor wrapper. Any DaisyUI-compatible theme defined in your CSS will work.

### Theme Toggle Components

```tsx
import { ThemeToggle, SimpleThemeToggle } from '@alggtta/editor'

<ThemeToggle />         // Full dropdown with all themes
<SimpleThemeToggle />   // Minimal light/dark toggle
```

---

## Collaboration (Yjs)

Enable real-time collaboration with Yjs:

```tsx
import * as Y from 'yjs'
import { AlggttaEditor } from '@alggtta/editor'

const ydoc = new Y.Doc()

<AlggttaEditor
  token="..."
  extensionConfig={{
    collaboration: {
      document: ydoc,
      field: 'default',  // Y.XmlFragment field name
    },
  }}
/>
```

You must provide your own WebSocket server for syncing Yjs documents. The editor also provides offline support utilities:

```tsx
import { createOfflineProvider, createNetworkMonitor } from '@alggtta/editor'
```

---

## Comments & Threads

The editor includes a complete inline commenting system:

```tsx
import type { UseCommentsOptions, UseCommentsReturn, Comment, CommentThread } from '@alggtta/editor'
```

Features:
- Select text → add comment thread
- Reply to threads
- Resolve / unresolve threads
- Edit and delete comments
- Draft mode for unsaved comments
- Thread navigation with scroll + flash animation

Comments are stored locally (no built-in server sync). Implement your own persistence layer using the hook's callbacks.

---

## Database Tables

Full-featured database tables powered by TanStack Table v8:

- 4 view types: Table, Board (Kanban), Gallery, List
- Column types: text, number, select, multi-select, date, checkbox
- Column resizing, sorting, global search
- Click-to-edit cells
- Row reordering via drag-and-drop
- Data stored in Tiptap node `attrs` (single source of truth)

```tsx
import type { DatabaseColumn, DatabaseRow, ViewType, ColumnType } from '@alggtta/editor'
```

---

## Code Blocks

- 190+ languages with syntax highlighting via Lowlight (highlight.js)
- Language selector with search
- Optional code execution via `onExecuteCode` callback
- Copy button, line numbers

```tsx
<AlggttaEditor
  token="..."
  onExecuteCode={async ({ language, code, signal }) => {
    const result = await executeInSandbox(language, code, signal)
    return { stdout: result.output, stderr: result.error }
  }}
/>
```

---

## Edge Runtime & Next.js Compatibility

### Cloudflare Workers / Edge Runtime

The editor package is fully compatible with edge environments:
- No Node.js-only APIs (`fs`, `path`, `crypto`, `buffer`) used
- All APIs are browser-compatible (DOM, Web APIs, fetch)
- Safe for Cloudflare Workers, Vercel Edge Functions, Deno Deploy

### Next.js App Router

The library includes `"use client"` directives automatically (added at build time). For Next.js App Router:

```tsx
// app/editor/page.tsx
import dynamic from 'next/dynamic'

const Editor = dynamic(
  () => import('@alggtta/editor').then((m) => m.AlggttaEditor),
  { ssr: false }
)

export default function EditorPage() {
  return <Editor token="..." />
}
```

### SSR Safety

- `immediatelyRender: false` is set by default to prevent SSR hydration mismatches
- The editor only initializes in the browser
- `ReadOnlyViewer` renders static HTML and is SSR-safe (but DOMPurify requires a browser environment — use `createStaticRenderer()` for server-side rendering)

---

## CSS Isolation

The editor's styles are scoped to prevent conflicts with your app:

- All styles scoped under `.ue-editor-root` class
- DaisyUI classes use `ue-` prefix (`ue-btn`, `ue-card`, `ue-dropdown`, etc.)
- CSS variables defined under `.ue-editor-root` (plus `:root` for font stacks)
- Single bundled CSS file — no CSS-in-JS runtime

### Import styles

```tsx
import '@alggtta/editor/styles.css'
```

### Potential Conflicts

If your project also uses Tailwind CSS / DaisyUI:
- The editor's prefixed classes (`ue-btn`, etc.) won't conflict with your `btn` classes
- Some Tailwind utility classes (e.g., `prose`, `flex`, `p-4`) are shared — this is intentional and generally safe
- CSS variables under `.ue-editor-root` are scoped and won't affect your global theme

---

## Bundle Size

| Output | Size | Description |
|--------|------|-------------|
| Entry (ESM) | ~4KB | Re-export entry point |
| Main chunk (ESM) | ~1.4MB | Core editor + all features (pre-gzip) |
| Main chunk (CJS) | ~956KB | CommonJS equivalent |
| CSS | ~273KB | All styles (pre-gzip) |

**Tree-shaking:** ESM format with code splitting. Heavy features are lazy-loaded:
- Mermaid diagrams — loaded on first use
- Code highlighting languages — loaded per language
- html2pdf — loaded on export

**Externalized (not in bundle):** `react`, `@tiptap/*`, `prosemirror-*`, `@heroicons/react`, `yjs`, `@tanstack/react-table`, `katex`

---

## TypeScript Exports

All props, hooks, utilities, and types are fully typed and exported:

```typescript
// Components
import { AlggttaEditor, ReadOnlyViewer, ThemeToggle } from '@alggtta/editor'

// Types
import type {
  AlggttaEditorRef,
  AlggttaEditorProps,
  ReadOnlyViewerProps,
  EditorLabels,
  SlashCommand,
  AIRequest,
  OnAIRequest,
  AIImageRequest,
  AIDiffChange,
  CodeExecutionRequest,
  CodeExecutionResult,
  ExtensionConfig,
  CollaborationConfig,
  DatabaseColumn,
  DatabaseRow,
  Comment,
  CommentThread,
  DaisyUITheme,
} from '@alggtta/editor'

// Utilities
import {
  filterNodeTypes,
  markdownToHtml,
  htmlToMarkdown,
  renderToStaticHTML,
  createStaticRenderer,
  createExtensions,
  useLabels,
  labelsEn,
  labelsKo,
} from '@alggtta/editor'

// GuideFlow extensions
import {
  guideFlowExtensions,
  guideFlowSlashCommands,
  MapBlockExtension,
  HotelBlockExtension,
  ContactBlockExtension,
  ScheduleBlockExtension,
  StaffOnlyBlockExtension,
} from '@alggtta/editor'
```

---

## Browser Support

- Chrome / Edge 90+
- Firefox 90+
- Safari 15+

---

## License

[MIT](https://github.com/hjkwon-github/alggtta-ed/blob/main/LICENSE)

---

**[Live Demo](https://demo.alggtta.com)** | **[npm](https://www.npmjs.com/package/@alggtta/editor)** | **[GitHub](https://github.com/hjkwon-github/alggtta-ed)**