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

dpk-editor

v0.1.1

Published

A React feature-rich HTML rich-text (WYSIWYG) editor built on TipTap v3. Use it anywhere — CMS bodies, comments, documents, marketing pages, or email. Preserves inline styles and the full document shell, and produces clean, portable, paste-anywhere markup

Readme

dpk-editor

A React feature-rich HTML rich-text editor built on TipTap v3. Use it anywhere you need a WYSIWYG editor that produces clean, portable, inline-styled HTML — CMS bodies, comment boxes, document editors, marketing pages, or email composers.

Unlike editors that emit class-based or framework-coupled markup, this one is designed around three constraints that keep the output portable and paste-anywhere safe:

  1. Full documents are supported<!doctype html><html><body style="…">…. A contentEditable/ProseMirror surface cannot host <html>/<head>/<body>; it silently strips them. So the editor edits only the body fragment and preserves the surrounding document shell verbatim (the document-shell bridge). Pass it a full document or a bare fragment — it round-trips either.
  2. Inline styles are preservedstyle="padding:…;background:…" on buttons, colored headings, table layouts. TipTap strips the style attribute by default. The bundled PreserveStyles extension keeps it, so styled content survives the round-trip.
  3. Output is portable, bulletproof markup — call-to-action buttons are <a style="display:inline-block;…"> wrapped in an aligned <p>, not <button> — markup that renders consistently across browsers, CMSes, and even email clients.
  • ✅ Ships ESM + CJS + type declarations
  • ✅ Works in Next.js App Router and plain Vite/CRA (immediatelyRender:false set internally for SSR)
  • ✅ No Tailwind/shadcn requirement — plain CSS you import
  • ✅ TypeScript strict, no any in public types

Install

npm i dpk-editor
# peer deps (you already have these in a React app):
npm i react react-dom
import { EmailEditor } from "dpk-editor";
import "dpk-editor/styles.css"; // required

The main component is exported as EmailEditor (and as the default export) for historical reasons — it is a fully general HTML editor and works for any content, not just email.

Usage

<EmailEditor> — batteries included (default export)

The full editor: toolbar, editable surface, document-shell bridge, optional insertable token chips, and image-upload wiring. value may be a full <!doctype><html><body>… document or a bare fragment; onChange always emits in the same shape it received.

import { useState } from "react";
import { EmailEditor } from "dpk-editor";
import "dpk-editor/styles.css";

export default function Composer() {
  const [html, setHtml] = useState("<h1>Hello, world</h1><p>Start writing…</p>");

  return (
    <EmailEditor
      value={html}
      onChange={setHtml}
      // Optional: clickable chips that insert any text/token at the caret.
      placeholders={[
        { token: "{{FirstName}}", label: "First name" },
        { token: "{{Date}}", label: "Today's date" },
      ]}
      onUploadImage={async (file) => {
        // upload `file` somewhere and return a hosted URL
        return "https://cdn.example.com/x.png";
      }}
      minHeight={288}
    />
  );
}

<RichTextEditor> — the generic body-HTML editor (named export)

The toolbar + editable surface operating on a plain HTML fragment (no <html>/<body>). EmailEditor is a thin wrapper that adds the shell bridge + token chips around this. Use it if you want to build your own wrapper or only ever deal with a body fragment.

import { useRef, useState } from "react";
import { RichTextEditor, type RichTextEditorHandle } from "dpk-editor";
import "dpk-editor/styles.css";

function Body() {
  const [body, setBody] = useState("<p>Body HTML only.</p>");
  const ref = useRef<RichTextEditorHandle>(null);

  return (
    <>
      <button onClick={() => ref.current?.insertAtCaret("{{FirstName}}")}>
        Insert token
      </button>
      <RichTextEditor
        ref={ref}
        value={body}
        onChange={setBody}
        editorStyle={{ minHeight: 240 }}
        toolbar={{ button: false }} // hide the button-builder control
      />
    </>
  );
}

Props

EmailEditorProps

| Prop | Type | Default | Description | | --------------- | ----------------------------------------- | -------------- | ----------- | | value | string | — | Full HTML document or bare body fragment. | | onChange | (value: string) => void | — | Fires with HTML in the same shape as value (shell re-applied). | | placeholders | EmailPlaceholder[] | undefined | Insertable tokens → renders a chip row that inserts each at the caret. | | onUploadImage | (file: File) => Promise<string> | undefined | Resolve an upload to a URL. If omitted, the image button prompts for a URL. | | minHeight | number | 288 | Min height (px) of the editable surface. | | placeholder | string | "Write something…" | Empty-state text. | | toolbar | ToolbarConfig | all on | Which controls to render. | | className | string | undefined | Extra class on the wrapper. |

RichTextEditorProps

| Prop | Type | Default | Description | | --------------- | ----------------------------------- | ------- | ----------- | | value | string | — | Body-level HTML fragment. | | onChange | (value: string) => void | — | Fires with the body-level HTML fragment. | | placeholder | string | "Write something…" | Empty-state text. | | onUploadImage | (file: File) => Promise<string> | undefined | Upload handler (else URL prompt). | | className | string | undefined | Extra class on the wrapper. | | editorStyle | React.CSSProperties | undefined | Applied to the editable surface (e.g. { minHeight }). | | toolbar | ToolbarConfig | all on | Which controls to render. |

RichTextEditor is a forwardRef exposing RichTextEditorHandle:

type RichTextEditorHandle = { insertAtCaret: (htmlOrText: string) => void };

ToolbarConfig

Every control defaults to on. There are two levels of control:

  • Single-control groups (link, image, button, html) are plain booleans — set to false to hide.
  • Multi-button groups (inline, headings, lists, align, blocks) accept either a boolean (whole group on/off) or an object of per-button booleans for fine-grained control. Unlisted buttons stay on, so you only list what you want to change.
type ToolbarConfig = {
  inline?: boolean | {
    bold?: boolean; italic?: boolean; underline?: boolean;
    strike?: boolean; code?: boolean;
  };
  headings?: boolean | {
    h2?: boolean; h3?: boolean; h4?: boolean; h5?: boolean; h6?: boolean;
  };
  lists?: boolean | { bullet?: boolean; ordered?: boolean; blockquote?: boolean };
  align?: boolean | { left?: boolean; center?: boolean; right?: boolean };
  blocks?: boolean | { paragraph?: boolean; divider?: boolean; footer?: boolean };
  link?: boolean;    // Link control
  image?: boolean;   // Image control
  button?: boolean;  // Button-builder dialog
  html?: boolean;    // Raw HTML source toggle
};

Examples:

// Hide whole groups:
<EmailEditor toolbar={{ button: false, html: false, blocks: false }} … />

// Keep the inline group but drop just Underline and Inline-code:
<EmailEditor toolbar={{ inline: { underline: false, code: false } }} … />

// Only offer H2 and H3 headings:
<EmailEditor toolbar={{ headings: { h4: false, h5: false, h6: false } }} … />

// Mix both levels freely:
<EmailEditor
  toolbar={{
    inline: { code: false },   // group on, hide one button
    align: false,              // whole group off
    image: false,              // single control off
  }}
  …
/>

true and {} are equivalent (group on, all buttons on). false hides the whole group. Hidden groups leave no dangling toolbar dividers.

For advanced use, resolveToolbarConfig(config) (exported) returns the flat, fully-expanded ResolvedToolbarConfig the toolbar renders from.

Building blocks (also exported)

For advanced use, the underlying pieces are exported:

import {
  PreserveStyles,        // the TipTap extension that keeps inline `style`
  createEmailExtensions, // the full extension set as a factory
  buildButtonHtml,       // (config: EmailButtonConfig) => string
  EmailButtonDialog,     // the button-config modal
  splitEmailHtml,        // (html) => { prefix, body, suffix }
  joinEmailHtml,         // (shell, body) => html
  extractEmailBody,      // (html) => body fragment
  escapeHtml,
  presets,               // { paragraph, divider, footer, heading }
} from "dpk-editor";

import type {
  EmailButtonConfig,
  EmailPlaceholder,
  ToolbarConfig,
  EmailHtmlDocument,
} from "dpk-editor";

buildButtonHtml

Builds portable, inline-styled HTML for a clickable button (an <a>, so it works in any HTML context — including email clients that won't render a <button>):

const html = buildButtonHtml({
  text: "Get started",
  href: "https://example.com",
  bgColor: "#2563eb",
  textColor: "#ffffff",
  align: "center",
  radius: 6,
  fullWidth: false,
});
// → <p style="margin:16px 0;text-align:center"><a href="…" style="display:inline-block;…">Get started</a></p>

The anchor is wrapped in a <p> (not a <div>) on purpose: TipTap has no div node and would unwrap a <div>, losing the alignment. A paragraph's text-align is preserved by the TextAlign extension.

Theming

styles.css targets .rte-* classes and the .ProseMirror/.rte-content surface, and exposes CSS custom properties with neutral defaults. Override any of them on a parent element (or :root):

.my-editor {
  --rte-accent: #7c3aed;
  --rte-accent-contrast: #ffffff;
  --rte-border: #e2e8f0;
  --rte-border-strong: #cbd5e1;
  --rte-bg: #ffffff;
  --rte-bg-muted: #f8fafc;
  --rte-fg: #0f172a;
  --rte-fg-muted: #64748b;
  --rte-radius: 10px;
  --rte-active-bg: #f5f3ff;
}
<EmailEditor className="my-editor" value={html} onChange={setHtml} />

SSR / Next.js App Router

  • The component entry is marked "use client", so importing it from a Server Component is fine — render <EmailEditor> inside a client boundary.
  • immediatelyRender: false is set internally, which prevents the hydration mismatch TipTap otherwise throws under SSR. You don't need to configure anything.

Built-in extensions

The editor is configured with TipTap v3:

  • StarterKit (which already bundles Link, Underline, lists, blockquote, code, and headings — do not add @tiptap/extension-link or @tiptap/extension-underline yourself), with Link configured openOnClick:false, autolink:true, rel="noopener noreferrer".
  • @tiptap/extension-image (inline:false, allowBase64:false)
  • @tiptap/extension-text-align (["heading","paragraph"])
  • @tiptap/extension-text-style (TextStyle + Color, re-exported here — no separate @tiptap/extension-color needed)
  • @tiptap/extension-highlight (multicolor:true)
  • @tiptap/extension-placeholder (StarterKit v3 does not bundle it; added explicitly for the empty-state hint, which sets the is-editor-empty/is-empty class and data-placeholder attribute that styles.css renders)
  • PreserveStyles (this package)

License

MIT