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

@neerajup1998/core

v0.1.6

Published

Reusable Puck-based block editor with config, blocks, and render.

Readme

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

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:

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

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

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:

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

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:

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.

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)

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)

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.

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.

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)

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

  • optionsfeedProxyUrl (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 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:

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

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:

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.