npm package: @its-arun/tiptap-studio
Description: A modern and feature-rich Rich Text Editor component built with TipTap for React
Author: its-arun
Latest version: 0.1.8

# @its-arun/tiptap-studio

A modern and feature-rich Rich Text Editor component built with TipTap for React applications.

![Tiptap Studio Demo](public/screenshot.png)

## Features

- 📝 Rich Text Editor with comprehensive formatting options
- 🌓 Light/Dark theme support
- 📱 Responsive design
- 🎨 Customizable toolbar and styling
- 🔧 TypeScript support
- 📊 Content Rendering (SSR/CSR)
- 🖼️ Image support with captions
- 📋 Table support
- 🎯 Link management
- 📝 Code blocks with syntax highlighting
- 🔍 Source code editing with CodeMirror
- ⚙️ Configurable image upload/fetch endpoints

## Installation

### Basic Installation

For basic editor functionality:

```bash
npm install @its-arun/tiptap-studio @tiptap/react @tiptap/pm @tiptap/starter-kit
```

### Full Feature Installation

For all features including UI components, syntax highlighting, etc.:

```bash
npm install @its-arun/tiptap-studio @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/extension-character-count @tiptap/extension-code-block-lowlight @tiptap/extension-color @tiptap/extension-highlight @tiptap/extension-image @tiptap/extension-link @tiptap/extension-list-keymap @tiptap/extension-placeholder @tiptap/extension-subscript @tiptap/extension-superscript @tiptap/extension-table @tiptap/extension-table-cell @tiptap/extension-table-header @tiptap/extension-table-row @tiptap/extension-text-align @tiptap/extension-text-style @tiptap/extension-underline @radix-ui/react-dropdown-menu @radix-ui/react-popover @radix-ui/react-tooltip react-colorful react-icons lowlight
```

### Peer Dependencies

Make sure you have the required peer dependencies installed:

```bash
npm install react react-dom
```

## Basic Usage

```jsx
import React, { useState } from "react";
import { TipTapEditor } from "@its-arun/tiptap-studio";
import "@its-arun/tiptap-studio/dist/styles.css"; // Import the styles

function MyEditor() {
  const [content, setContent] = useState("<p>Start writing...</p>");

  return (
    <TipTapEditor
      initialContent={content}
      onContentChange={(value) => setContent(value)}
      placeholder={{
        paragraph: "Start writing your content...",
        imageCaption: "Add a caption...",
      }}
    />
  );
}

export default MyEditor;
```

## Advanced Usage

### With Custom Configuration

```jsx
import React, { useRef } from 'react';
import { TipTapEditor, type TiptapEditorRef } from '@its-arun/tiptap-studio';
import '@its-arun/tiptap-studio/dist/styles.css';

function AdvancedEditor() {
  const editorRef = useRef<TiptapEditorRef>(null);

  const handleSave = () => {
    const editor = editorRef.current?.getInstance();
    if (editor) {
      const html = editor.getHTML();
      const json = editor.getJSON();
      console.log({ html, json });
    }
  };

  return (
    <div>
      <TipTapEditor
        ref={editorRef}
        initialContent="<p>Advanced editor example</p>"
        output="html" // or "json"
        contentMinHeight={300}
        contentMaxHeight={600}
        hideMenuBar={false}
        hideStatusBar={false}
        hideBubbleMenu={false}
        onContentChange={(value) => console.log('Content changed:', value)}
      />
      <button onClick={handleSave}>Save Content</button>
    </div>
  );
}
```

### With Custom Image Configuration

Configure your own image upload/fetch endpoints:

```jsx
import React from 'react';
import { TipTapEditor, type ImageConfig } from '@its-arun/tiptap-studio';
import '@its-arun/tiptap-studio/dist/styles.css';

function EditorWithCustomImages() {
  const imageConfig: ImageConfig = {
    upload: {
      url: '/api/my-upload-endpoint',
      headers: {
        'Authorization': 'Bearer your-token',
      },
      formDataKey: 'image', // default is 'file'
    },
    fetch: {
      url: '/api/my-images-endpoint',
      headers: {
        'Authorization': 'Bearer your-token',
      },
    },
  };

  return (
    <TipTapEditor
      imageConfig={imageConfig}
      initialContent="<p>Editor with custom image handling</p>"
      onContentChange={(value) => console.log('Content:', value)}
    />
  );
}
```

### With Custom Provider

```jsx
import React from "react";
import {
  TiptapProvider,
  useTiptapContext,
  ExtensionKit,
} from "@its-arun/tiptap-studio";

function CustomEditor() {
  const editorOptions = {
    extensions: ExtensionKit,
    content: "<p>Custom provider example</p>",
    editable: true,
  };

  return (
    <TiptapProvider editorOptions={editorOptions}>
      <CustomToolbar />
      <EditorContent />
    </TiptapProvider>
  );
}

function CustomToolbar() {
  const { editor } = useTiptapContext();

  return (
    <div>
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        data-active={editor.isActive("bold")}
      >
        Bold
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        data-active={editor.isActive("italic")}
      >
        Italic
      </button>
    </div>
  );
}
```

## Props

### TipTapEditor Props

| Prop               | Type                       | Default       | Description                                  |
| ------------------ | -------------------------- | ------------- | -------------------------------------------- |
| `initialContent`   | `Content`                  | `undefined`   | Initial editor content (HTML string or JSON) |
| `onContentChange`  | `(value: Content) => void` | `undefined`   | Callback when content changes                |
| `output`           | `"html" \| "json"`         | `"html"`      | Output format for content changes            |
| `placeholder`      | `object`                   | `undefined`   | Placeholder text for different elements      |
| `readonly`         | `boolean`                  | `false`       | Make editor read-only                        |
| `disabled`         | `boolean`                  | `false`       | Disable editor interactions                  |
| `hideMenuBar`      | `boolean`                  | `false`       | Hide the top menu bar                        |
| `hideStatusBar`    | `boolean`                  | `false`       | Hide the bottom status bar                   |
| `hideBubbleMenu`   | `boolean`                  | `true`        | Hide the bubble menu                         |
| `contentMinHeight` | `string \| number`         | `200`         | Minimum height of editor content             |
| `contentMaxHeight` | `string \| number`         | `undefined`   | Maximum height of editor content             |
| `ssr`              | `boolean`                  | `auto-detect` | Enable server-side rendering support         |
| `imageConfig`      | `ImageConfig`              | `undefined`   | Configure image upload/fetch endpoints       |

### ImageConfig Interface

```typescript
interface ImageConfig {
  upload?: {
    url: string; // Upload endpoint URL
    headers?: Record<string, string>; // Optional headers
    formDataKey?: string; // Form data key (default: 'file')
  };
  fetch?: {
    url: string; // Fetch images endpoint URL
    headers?: Record<string, string>; // Optional headers
  };
}
```

## Server-Side Rendering (SSR)

The editor automatically detects SSR environments and adjusts accordingly. You can also explicitly control SSR behavior:

```jsx
// Explicitly enable SSR mode
<TipTapEditor ssr={true} />

// Explicitly disable SSR mode
<TipTapEditor ssr={false} />

// Auto-detect (default)
<TipTapEditor />
```

### Next.js App Router Usage

When using the editor in Next.js with the App Router, you may need to import it dynamically to prevent SSR issues:

```jsx
"use client";

import dynamic from "next/dynamic";

const TipTapEditor = dynamic(
  () => import("@its-arun/tiptap-studio").then((mod) => mod.TipTapEditor),
  {
    ssr: false,
    loading: () => <p>Loading editor...</p>,
  }
);

function MyEditor() {
  return (
    <TipTapEditor
      initialContent="<p>Start writing...</p>"
      onContentChange={(value) => console.log(value)}
    />
  );
}
```

### Next.js Pages Router Usage

For the Pages Router, wrap your editor component:

```jsx
import dynamic from "next/dynamic";

const TipTapEditor = dynamic(
  () => import("@its-arun/tiptap-studio").then((mod) => mod.TipTapEditor),
  {
    ssr: false,
  }
);

export default function MyPage() {
  return (
    <div>
      <TipTapEditor ssr={false} initialContent="<p>Start writing...</p>" />
    </div>
  );
}
```

### Troubleshooting SSR Issues

If you encounter hydration errors like "SSR has been detected, and `immediatelyRender` has been set to `true`", try:

1. **Use dynamic imports**: Import the editor dynamically with `ssr: false`
2. **Explicit SSR prop**: Set `ssr={false}` on the TipTapEditor component
3. **Client-side only rendering**: Wrap your editor in a component with `'use client'` directive

## Image Upload/Fetch API

### Default API Endpoints

By default, the editor expects these endpoints:

- **Upload**: `POST /api/images` - Upload a single image file
- **Fetch**: `GET /api/images` - Get list of available images

### API Response Format

Both endpoints must return responses in the following standardized format:

#### Upload Response Format

```typescript
{
  "success": boolean,
  "data": {
    "id": string,
    "url": string,
    "filename": string,
    "mimeType": string,
    "size": number,
    "existing": boolean
  },
  "error"?: string
}
```

#### Fetch Response Format

```typescript
{
  "success": boolean,
  "data": [
    {
      "id": number,
      "url": string,
      "filename": string,
      "mimeType": string,
      "size": number
    }
  ],
  "error"?: string
}
```

### Custom API Implementation

You can implement your own image handling endpoints. Here are examples:

#### Upload Endpoint Example

```javascript
// pages/api/images/route.js or app/api/images/route.js
export async function POST(request) {
  try {
    const formData = await request.formData();
    const file = formData.get("file"); // default formDataKey is 'file'

    if (!file) {
      return Response.json(
        {
          success: false,
          error: "No file provided",
        },
        { status: 400 }
      );
    }

    // Your upload logic here
    const result = await uploadToYourService(file);

    return Response.json({
      success: true,
      data: {
        id: result.id,
        url: result.url,
        filename: file.name,
        mimeType: file.type,
        size: file.size,
        existing: false,
      },
    });
  } catch (error) {
    return Response.json(
      {
        success: false,
        error: error.message || "Upload failed",
      },
      { status: 500 }
    );
  }
}
```

#### Fetch Endpoint Example

```javascript
// pages/api/images/route.js or app/api/images/route.js
export async function GET(request) {
  try {
    // Your fetch logic here
    const images = await getImagesFromYourService();

    return Response.json({
      success: true,
      data: images.map((img) => ({
        id: img.id,
        url: img.url,
        filename: img.filename,
        mimeType: img.mimeType,
        size: img.size,
      })),
    });
  } catch (error) {
    return Response.json(
      {
        success: false,
        data: [],
        error: error.message || "Failed to fetch images",
      },
      { status: 500 }
    );
  }
}
```

### Error Handling

The editor automatically handles API errors and displays user-friendly messages. Make sure your API returns:

- `success: false` for any errors
- Descriptive `error` messages for debugging
- Appropriate HTTP status codes (400 for client errors, 500 for server errors)

## Available Exports

```jsx
// Main components
import {
  TipTapEditor,
  TiptapProvider,
  SourceEditor,
  TipTapClientRenderer,
  TipTapServerRenderer
} from '@its-arun/tiptap-studio';

// Hooks
import {
  useTiptapEditor,
  useTiptapContext
} from '@its-arun/tiptap-studio';

// Utils
import {
  isNodeSelected,
  isTextSelected,
  getNodeContainer,
  ExtensionKit
} from '@its-arun/tiptap-studio';

// UI Components
import {
  BubbleMenu,
  Tooltip,
  Input
} from '@its-arun/tiptap-studio';

// Types
import type {
  TiptapEditorProps,
  TiptapEditorRef,
  UseTiptapEditorOptions,
  ImageConfig,
  Editor,
  Content
} from '@its-arun/tiptap-studio';
```

## Styling

The package includes default styles that you need to import:

```jsx
import "@its-arun/tiptap-studio/dist/styles.css";
```

You can also customize the appearance using CSS variables:

```css
:root {
  --rte-editor-font: "Inter", sans-serif;
  --rte-editor-font-size: 16px;
  --rte-editor-line-height: 1.6;
  --rte-editor-min-height: 200px;
  --rte-editor-max-height: 600px;
}
```

## Tech Stack

- React 18+
- TipTap
- CodeMirror (for source editing)
- TypeScript
- Radix UI (for UI components)
- SCSS + CSS Variables

## Credits

This package is based on the excellent work from [next-tiptap](https://github.com/ndtrung341/next-tiptap) by [@ndtrung341](https://github.com/ndtrung341). We've transformed their Next.js TipTap editor implementation into a reusable React component package.

## License

MIT