npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alggtta/editor

v2.1.0

Published

Universal Editor with Tiptap 3.x and DaisyUI 5

Readme

@alggtta/editor

npm version License: MIT TypeScript

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 | Landing Page | GitHub


Table of Contents


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

npm install @alggtta/editor

Peer Dependencies

Required:

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

Optional (for collaboration):

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

Optional (for tables):

npm install @tanstack/react-table

Optional (for math):

npm install katex

Quick Start

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.

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:

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
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:

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:

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

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:

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:

<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:

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

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:

<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:

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:

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:

<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

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

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

EditorLabels Structure

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:

import { useLabels } from '@alggtta/editor'

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

Available Locale Presets

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():

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):

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:

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.

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.

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.

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.

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.

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:

{
  "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

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

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

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:

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:

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

Comments & Threads

The editor includes a complete inline commenting system:

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)
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
<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:

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

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:

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


Live Demo | npm | GitHub