npm package: @neerajup1998/core
Description: Reusable Puck-based block editor with config, blocks, and render.
Latest version: 0.0.9

# @neerajup1998/core

Reusable Puck-based block editor: config, 50+ blocks, and read-only render. Use in any React app.

## Installing in another React app

### Option A: From npm (after publishing)

```bash
npm install @neerajup1998/core react react-dom @puckeditor/core @tiptap/react @tiptap/starter-kit @tiptap/extension-link date-fns recharts react-syntax-highlighter
```

(React and react-dom may already be in your app; the rest are required peer dependencies.)

### Option B: From this repo (local path)

In your other React app’s folder:

```bash
# Install the package from the library repo path (adjust the path to your machine)
npm install /path/to/library/packages/block-editor

# Install peer dependencies if not already present
npm install @puckeditor/core @tiptap/react @tiptap/starter-kit @tiptap/extension-link date-fns recharts react-syntax-highlighter
```

Or add to your app’s `package.json`:

```json
{
  "dependencies": {
    "@neerajup1998/core": "file:../path/to/library/packages/block-editor",
    "@puckeditor/core": "^0.21.0",
    "@tiptap/react": "^3.19.0",
    "@tiptap/starter-kit": "^3.19.0",
    "@tiptap/extension-link": "^3.19.0",
    "date-fns": "^4.0.0",
    "recharts": "^2.0.0",
    "react-syntax-highlighter": "^15.0.0"
  }
}
```

Then run `npm install`. Ensure `packages/block-editor` has been built (`npm run build` inside that folder) so `dist/` exists.

## Usage

Examples are shown in **JavaScript first**, then **TypeScript**; copy the block that matches your project.

### Quick reference (imports)

| Use case | JavaScript | TypeScript |
|----------|------------|------------|
| Editor + styles | `BlockEditor`, `BlockEditorRender` from `@neerajup1998/core`; `import "@neerajup1998/core/style.css"` | Same, plus `import type { Data } from "@neerajup1998/core"` |
| Templates | Add `TemplatePicker`, `getNewsletterTemplates`, etc. | Same, plus `Data` for state and callbacks |

### JavaScript

#### Basic editor (JavaScript)

```jsx
import { useState } from "react";
import { BlockEditor, BlockEditorRender } from "@neerajup1998/core";
import "@neerajup1998/core/style.css";

function MyPage() {
  const [data, setData] = useState({ root: {}, content: [], zones: {} });

  return (
    <div style={{ height: "100vh" }}>
      <BlockEditor
        data={data}
        onChange={setData}
        locale="en"
        options={{
          feedProxyUrl: "https://my-api.com",
          aiService: { generateAltText: async (url) => "..." },
          giphyApiKey: "your-key",
        }}
      />
    </div>
  );
}

// Read-only: <BlockEditorRender data={data} locale="en" />
```

#### Built-in Import PDF/Word (JavaScript)

When you pass **`onImportFile`**, the editor shows an "Import PDF/Word" button. Clicking it opens the **library’s built-in modal** (drag-and-drop for .docx, .doc, .pdf). The library can process files in two ways:

1. **Default parser (no host code):** If you do **not** pass `options.importFile`, the library uses its built-in parser. Install the optional peer dependencies so the modal works out of the box:

   ```bash
   npm install mammoth docx-preview pdfjs-dist
   ```

   Then use the editor with only `onImportFile`; no `importFile` implementation needed. Content is mapped into Section blocks (headings → Heading, paragraphs → Text, images → Image, lists, dividers). For better PDF quality, you can set **`options.pdfConvertUrl`** to your backend base URL; the library will POST PDFs to `{pdfConvertUrl}/api/convert-pdf-to-docx` and process the returned DOCX.

2. **Custom parser:** Pass **`options.importFile`** to implement your own parsing. Your function receives the file and an optional progress callback and returns Puck `Data`; the library merges it into the canvas.

Example with **default parser** (optional deps installed):

```jsx
import { useState } from "react";
import { BlockEditor } from "@neerajup1998/core";
import "@neerajup1998/core/style.css";

function MyPage() {
  const [data, setData] = useState({ root: {}, content: [], zones: {} });

  return (
    <div style={{ height: "100vh" }}>
      <BlockEditor
        data={data}
        onChange={setData}
        locale="en"
        onImportFile={() => {}}
        options={{
          feedProxyUrl: "https://my-api.com",
          giphyApiKey: "your-key",
          // optional: for better PDF quality, set your backend that converts PDF → DOCX
          // pdfConvertUrl: "https://my-api.com",
        }}
      />
    </div>
  );
}
```

Example with **custom parser**:

```jsx
options={{
  importFile: async (file, onProgress) => {
    // Your parser; return { root: { props: {} }, content: [], zones: {} } with Section(s) and blocks
    return { root: { props: {} }, content: [], zones: {} };
  },
}}
```

#### Editor with custom Import modal (JavaScript)

If you want your **own** Import modal UI (not the library’s), do not use **`onImportFile`**. Add your own button, open your modal, parse the file in your app, then merge using **`appendImportedData`** from the library.

```jsx
import { useState } from "react";
import { BlockEditor, appendImportedData } from "@neerajup1998/core";
import "@neerajup1998/core/style.css";

function MyPage() {
  const [data, setData] = useState({ root: {}, content: [], zones: {} });
  const [showImportModal, setShowImportModal] = useState(false);

  const handleImportSuccess = (importedData) => {
    setData((current) =>
      appendImportedData(current || { root: {}, content: [], zones: {} }, importedData)
    );
    setShowImportModal(false);
  };

  return (
    <div style={{ height: "100vh" }}>
      <button type="button" onClick={() => setShowImportModal(true)}>
        My Import
      </button>
      <BlockEditor
        data={data}
        onChange={setData}
        locale="en"
        options={{ feedProxyUrl: "https://my-api.com", giphyApiKey: "your-key" }}
      />
      {showImportModal && (
        <YourImportModal
          onImport={handleImportSuccess}
          onClose={() => setShowImportModal(false)}
        />
      )}
    </div>
  );
}
```

#### TemplatePicker (JavaScript)

```jsx
import { useState } from "react";
import { BlockEditor, TemplatePicker } from "@neerajup1998/core";
import "@neerajup1998/core/style.css";

function MyPage() {
  const [data, setData] = useState(null);
  const [mode, setMode] = useState("templates");

  return (
    <div style={{ padding: "16px" }}>
      <div style={{ marginBottom: "16px", display: "flex", gap: "8px" }}>
        <button
          type="button"
          onClick={() => setMode("templates")}
          style={{ fontWeight: mode === "templates" ? 600 : 400 }}
        >
          Templates
        </button>
        <button
          type="button"
          onClick={() => setMode("canvas")}
          style={{ fontWeight: mode === "canvas" ? 600 : 400 }}
        >
          Canvas
        </button>
      </div>
      {mode === "templates" && (
        <TemplatePicker
          onSelect={(templateData) => {
            setData(templateData);
            setMode("canvas");
          }}
        />
      )}
      {mode === "canvas" && (
        <div style={{ height: "80vh" }}>
          <BlockEditor
            data={data ?? { root: { props: {} }, content: [], zones: {} }}
            onChange={setData}
            locale="en"
          />
        </div>
      )}
    </div>
  );
}
```

### TypeScript

#### Basic editor (TypeScript)

```tsx
import { useState } from "react";
import { BlockEditor, BlockEditorRender } from "@neerajup1998/core";
import type { Data } from "@neerajup1998/core";
import "@neerajup1998/core/style.css";

function MyPage() {
  const [data, setData] = useState<Data | null>({ root: {}, content: [], zones: {} });

  return (
    <div style={{ height: "100vh" }}>
      <BlockEditor
        data={data}
        onChange={setData}
        locale="en"
        options={{
          feedProxyUrl: "https://my-api.com",
          aiService: { generateAltText: async (url) => "..." },
          giphyApiKey: "your-key",
        }}
      />
    </div>
  );
}

// Read-only: <BlockEditorRender data={data} locale="en" />
```

#### Built-in Import PDF/Word (TypeScript)

Pass **`onImportFile`**; the library’s modal opens on button click. Omit **`options.importFile`** to use the built-in parser (install optional deps: mammoth, docx-preview, pdfjs-dist), or pass **`options.importFile`** for your own parser. Optional **`options.pdfConvertUrl`** improves PDF quality via your backend.

```tsx
import { useState } from "react";
import { BlockEditor } from "@neerajup1998/core";
import type { Data } from "@neerajup1998/core";
import "@neerajup1998/core/style.css";

function MyPage() {
  const [data, setData] = useState<Data | null>({ root: {}, content: [], zones: {} });

  return (
    <div style={{ height: "100vh" }}>
      <BlockEditor
        data={data}
        onChange={setData}
        locale="en"
        onImportFile={() => {}}
        options={{
          feedProxyUrl: "https://my-api.com",
          giphyApiKey: "your-key",
          // pdfConvertUrl: "https://my-api.com", // optional: for better PDF quality
        }}
      />
    </div>
  );
}
```

#### Editor with custom Import modal (TypeScript)

To use your **own** Import modal, do not use **`onImportFile`**. Add your own button, open your modal, then merge with **`appendImportedData`**.

```tsx
import { useState } from "react";
import { BlockEditor, appendImportedData } from "@neerajup1998/core";
import type { Data } from "@neerajup1998/core";
import "@neerajup1998/core/style.css";

function MyPage() {
  const [data, setData] = useState<Data | null>({ root: {}, content: [], zones: {} });
  const [showImportModal, setShowImportModal] = useState(false);

  const handleImportSuccess = (importedData: Data) => {
    setData((current) =>
      appendImportedData(current ?? { root: {}, content: [], zones: {} }, importedData)
    );
    setShowImportModal(false);
  };

  return (
    <div style={{ height: "100vh" }}>
      <button type="button" onClick={() => setShowImportModal(true)}>
        My Import
      </button>
      <BlockEditor
        data={data}
        onChange={setData}
        locale="en"
        options={{ feedProxyUrl: "https://my-api.com", giphyApiKey: "your-key" }}
      />
      {showImportModal && (
        <YourImportModal
          onImport={handleImportSuccess}
          onClose={() => setShowImportModal(false)}
        />
      )}
    </div>
  );
}
```

#### TemplatePicker (TypeScript)

```tsx
import { useState } from "react";
import { BlockEditor, TemplatePicker } from "@neerajup1998/core";
import type { Data } from "@neerajup1998/core";
import "@neerajup1998/core/style.css";

function MyPage() {
  const [data, setData] = useState<Data | null>(null);
  const [mode, setMode] = useState<"templates" | "canvas">("templates");

  return (
    <div style={{ padding: "16px" }}>
      <div style={{ marginBottom: "16px", display: "flex", gap: "8px" }}>
        <button
          type="button"
          onClick={() => setMode("templates")}
          style={{ fontWeight: mode === "templates" ? 600 : 400 }}
        >
          Templates
        </button>
        <button
          type="button"
          onClick={() => setMode("canvas")}
          style={{ fontWeight: mode === "canvas" ? 600 : 400 }}
        >
          Canvas
        </button>
      </div>
      {mode === "templates" && (
        <TemplatePicker
          onSelect={(templateData: Data) => {
            setData(templateData);
            setMode("canvas");
          }}
        />
      )}
      {mode === "canvas" && (
        <div style={{ height: "80vh" }}>
          <BlockEditor
            data={data ?? { root: { props: {} }, content: [], zones: {} }}
            onChange={setData}
            locale="en"
          />
        </div>
      )}
    </div>
  );
}
```

### Options and advanced

- **options** – `feedProxyUrl` (RssFeed block), `aiService: { generateAltText }` (Image block), `giphyApiKey` (Gif/Sticker search). See the examples above.
- **options.importFile** – `(file: File, onProgress?: (percent, step) => void) => Promise<Data>`. Optional. When set with **onImportFile**, the built-in Import modal uses this to parse PDF/Word. If not set, the library uses its built-in parser (requires optional peer deps: **mammoth**, **docx-preview**, **pdfjs-dist**).
- **options.pdfConvertUrl** – Optional. Base URL for PDF→DOCX conversion (e.g. your backend). When set, PDFs are sent to `{pdfConvertUrl}/api/convert-pdf-to-docx` and the returned DOCX is processed for best quality. Only used when using the built-in parser (no custom `importFile`).
- **onImportFile** – When set, shows "Import PDF/Word" in the header and clicking opens the built-in modal (using either your `options.importFile` or the library’s default parser).
- **headerTitle**, **headerPath** – Optional strings for the editor header.
- **appendImportedData(currentData, importedData)** – Exported helper to merge imported section data into current editor data (unique IDs). Use this when you implement your own Import modal.

## Templates

The library includes **15 pre-built templates** (5 newsletters, 5 email templates, 5 web page examples) that users can pick and load into the editor. Full **TemplatePicker** examples (JavaScript and TypeScript) are in the [Usage](#javascript) section above.

### TemplatePicker props

- **onSelect** `(data: Data) => void` – Called when the user clicks a template; pass the received `data` to your editor state.
- **categories?** `("newsletter" | "email" | "web")[]` – Which sections to show (default: all three).
- **className?** – Optional CSS class for the wrapper.
- **locale?** – Reserved for future i18n of section titles.

### Using template data programmatically

Load a template by id without the picker UI. JavaScript:

```jsx
import {
  getNewsletterTemplates,
  getEmailTemplates,
  getWebPageTemplates,
  getTemplatesByCategory,
  getAllTemplates,
} from "@neerajup1998/core";

const newsletters = getNewsletterTemplates();
const first = newsletters[0];
setData(first.data);

// Or by category
const webTemplates = getTemplatesByCategory("web");
const all = getAllTemplates();
```

TypeScript (same imports; use `Data` for state if needed):

```tsx
import {
  getNewsletterTemplates,
  getEmailTemplates,
  getWebPageTemplates,
  getTemplatesByCategory,
  getAllTemplates,
} from "@neerajup1998/core";

const newsletters = getNewsletterTemplates();
const first = newsletters[0];
setData(first.data);

// Or by category
const webTemplates = getTemplatesByCategory("web");
const all = getAllTemplates();
```

## Troubleshooting

**Invalid hook call / "Cannot read properties of null (reading 'useContext')"** — This usually means the app is loading more than one copy of React (e.g. when using `file:` links or monorepos). Fix it by forcing a single React instance. In a **Vite** app, add to `vite.config.js`:

```js
resolve: {
  dedupe: ['react', 'react-dom', 'react/jsx-runtime'],
},
```

Restart the dev server after changing the config.

**Heading/Text editing** — Heading and Text blocks use **sidebar-only** editing by default: edit in the right panel ("Heading Text" or "Content"); the canvas shows the content but does not allow inline typing. This avoids the focus-loss bug in @puckeditor/core.

- To try **in-canvas inline editing** again, apply the shipped patch (see `patches/@puckeditor+core+0.21.1.patch`), add `patch-package` and `"postinstall": "patch-package"` in your app, then run `npx patch-package`. You can then set `contentEditable: true` on the block's richtext field in the config if desired.

## API

- **BlockEditor** – Full editor (Puck) with `data`, `onChange`, `locale`, optional `options`, `config`, `overrides`, `onImportFile`. When `onImportFile` is set, the "Import PDF/Word" button opens the built-in modal; the library uses `options.importFile` if provided, otherwise its built-in parser (requires optional deps: mammoth, docx-preview, pdfjs-dist).
- **BlockEditorRender** – Read-only render of the same design.
- **ImportFileModal** – Built-in drag-and-drop modal for PDF/Word; pass `importFile`, `onImport`, `onClose`. Used internally when `onImportFile` is set; also exported for custom flows.
- **appendImportedData(currentData, importedData)** – Merges imported `Data` (Section blocks and zones) into current editor data with unique IDs. Use when implementing your own Import modal.
- **importFile(file, onProgress?, config?)** – Built-in PDF/Word parser. Returns `Promise<Data>`. Config can include `pdfConvertUrl` for PDF→DOCX. Exported for use outside the modal.
- **detectFileType(file)** – Returns `"docx" | "doc" | "pdf" | null`.
- **TemplatePicker** – UI to choose from 15 templates (newsletter, email, web); `onSelect(data)` loads the template into your editor state.
- **getNewsletterTemplates()**, **getEmailTemplates()**, **getWebPageTemplates()** – Arrays of template items (each has `id`, `name`, `description?`, `data`).
- **getAllTemplates()**, **getTemplatesByCategory(category)** – All 15 templates or filtered by `"newsletter" | "email" | "web"`.
- **getEditorConfig(locale, options?)** – Config with translated labels; optional custom `t(locale, key)`.
- **editorConfig** – Static config for types (e.g. `createUsePuck<typeof editorConfig>()`).
- **EditorDataProvider**, **HeadingIdProvider**, **BlockEditorOptionsProvider** – Use if you compose Puck yourself.
- **RichTextEditor**, **collectHeadings** – Exported for custom blocks or tooling.

Bundle size is kept under ~500 KB by externalizing React, Puck, Tiptap, date-fns, recharts, and react-syntax-highlighter; the host app must install these as dependencies.