@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-highlighterOr 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:
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-distThen use the editor with only
onImportFile; noimportFileimplementation needed. Content is mapped into Section blocks (headings → Heading, paragraphs → Text, images → Image, lists, dividers). For better PDF quality, you can setoptions.pdfConvertUrlto your backend base URL; the library will POST PDFs to{pdfConvertUrl}/api/convert-pdf-to-docxand process the returned DOCX.Custom parser: Pass
options.importFileto implement your own parsing. Your function receives the file and an optional progress callback and returns PuckData; 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
- 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-docxand the returned DOCX is processed for best quality. Only used when using the built-in parser (no customimportFile). - onImportFile – When set, shows "Import PDF/Word" in the header and clicking opens the built-in modal (using either your
options.importFileor 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 receiveddatato 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), addpatch-packageand"postinstall": "patch-package"in your app, then runnpx patch-package. You can then setcontentEditable: trueon the block's richtext field in the config if desired.
API
- BlockEditor – Full editor (Puck) with
data,onChange,locale, optionaloptions,config,overrides,onImportFile. WhenonImportFileis set, the "Import PDF/Word" button opens the built-in modal; the library usesoptions.importFileif 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 whenonImportFileis 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 includepdfConvertUrlfor 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.
