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

@naatchaal/editor

v1.0.3

Published

First-party customizable rich-text / WYSIWYG editor by Naatchaal — document model, transactions, toolbar buttons & dropdowns.

Readme

@naatchaal/editor

First-party customizable rich-text / WYSIWYG engine by Naatchaal.
Playground & docs: https://naat.tools/editor

Highly customizable to the deep — but simple. Most apps need a few imports and props. Theme, toolbar chrome, icons, and layout composition are there when you go deeper — without a second package or breaking the simple path.

No TipTap, Lexical, or Quill — own document model, transactions, history, and DOM bridge.


Start here

Install, drop in <NaatEditor />, seed content, persist on change, validate. Defaults cover a full toolbar.

Install

npm install @naatchaal/editor

Peer dependencies: react, react-dom (≥18).

Quick start (Next.js App Router)

NaatEditor is a Client Component — wrap it in a file with "use client" (or import from one).

"use client";

import { NaatEditor } from "@naatchaal/editor";
// Default styles load with the import above.
// Optional: import "@naatchaal/editor/styles.css" for explicit order / non-bundlers.

export function MyEditor() {
  return (
    <NaatEditor
      initialContent="<p>Hello from Naat Editor</p>"
      onChange={({ html, json }) => {
        // persist html or json
      }}
    />
  );
}

config is optional — omit it to use the built-in toolbar, marks, blocks, and theme.

Common wrapper

"use client";

export function ArticleEditor({
  id,
  html,
  onHtmlChange,
}: {
  id: string;
  html?: string;
  onHtmlChange: (html: string) => void;
}) {
  return (
    <NaatEditor
      key={id}
      initialContent={html ?? ""}
      onChange={({ html }) => onHtmlChange(html)}
    />
  );
}

Prefill & edit from the DB

Uncontrolled after mount — seed once, then persist via onChange.

| Prop | Type | Notes | |------|------|--------| | initialContent | DocNode \| string | DocNode, JSON string, HTML, or plain text |

Edit an existing record: remount with a React key so the editor re-seeds.

<NaatEditor
  key={questionId}
  initialContent={savedHtmlOrJson}
  onChange={({ html, json }) => {
    // write to form state / DB
  }}
/>

Forms (RHF / Formik / native): store the string (or DocNode) from onChange. Prefer key={recordId} when switching rows — there is no controlled value prop (remount instead).

Drafts / browser storage: the package does not write localStorage / sessionStorage. Own drafts in the app — read storage into initialContent (pair with key), write from onChange. Only touch storage on the client / after mount (SSR will not have window).

View saved content

Two paths — pick one:

Path A — NaatEditorViewer (recommended for matching create/edit typography):

"use client";

import { NaatEditorViewer } from "@naatchaal/editor";
// Styles load with @naatchaal/editor; optional: import "@naatchaal/editor/styles.css"

export function AnswerView({ html }: { html: string }) {
  return (
    <NaatEditorViewer
      content={html}
      // optional chrome shortcuts → `--ne-viewer-*`
      padding="1.25rem"
      border="1px solid #e2e8f0"
      borderRadius="12px"
      // or className / style / CSS overrides
    />
  );
}

Client Component ("use client"). Same ContentInput as initialContent. No toolbar / provider. Links open in a new tab (target=_blank + rel). Chrome: padding / background / border / borderRadius / boxShadow, or className / style / .naat-editor-viewer / --ne-viewer-*. For async fetch, pass loading={isPending} (optional renderLoading); empty content alone does not imply loading.

Security (view path): NaatEditorViewer / contentToViewerHtml never inject raw HTML. DocNode/JSON/plain go through parseContentdocToHtml (schema tags + escaped text). HTML strings go through sanitizeHtmlForView (strips <script>, event handlers, javascript: / data: URLs, untrusted iframes; allows YouTube/Vimeo embeds). Prefer storing json/DocNode when you can.

Path B — render HTML yourself (e.g. from DB html column):

<div
  className="prose"
  dangerouslySetInnerHTML={{ __html: savedHtml }}
/>

Path B is not hardened by the package. Prefer Path A. If you must inject HTML yourself, run sanitizeHtmlForView(savedHtml) first (or server-side sanitize). For multi-tenant UGC, also sanitize on the server before storage — client view hardening is a last line of defense, not a substitute for trust boundaries.

Editor height + placeholder (like <textarea>)

{/* Default: grows with content from minHeight */}
<NaatEditor
  placeholder="Write something…"
  config={{ theme: { surface: { minHeight: "12rem" } } }}
/>

{/* Cap + scroll inside when content exceeds maxHeight */}
<NaatEditor
  config={{ theme: { surface: { minHeight: "12rem", maxHeight: "20rem" } } }}
/>

Editor height (writing surface only; toolbar uncapped):

| Knob | Behavior | |------|----------| | theme.surface.minHeight | Floor height (CSS default 16rem--ne-surface-min-height) | | theme.surface.maxHeight | Cap; enables --ne-surface-overflow-y: auto so content scrolls inside. Unset → surface grows with content. |

Tailwind / className override on NaatSurface: e.g. min-h-48 max-h-80 overflow-y-auto. Placeholder CSS: --ne-placeholder-color.

Form essentials

import {
  NaatEditor,
  isEmptyContent,
  getTextLength,
  getPlainText,
  validateContent,
  parseContent,
} from "@naatchaal/editor";

// Zod — field stores HTML (or JSON) from onChange
z.string().refine((html) => !isEmptyContent(html), "Required");
z.string().refine((html) => getTextLength(html) >= 20, "Too short");

const { ok, errors } = validateContent(html, {
  required: true,
  minLength: 10,
  maxLength: 2000,
});

const doc = parseContent(savedHtmlOrJson);

| Helper | Use | |--------|-----| | parseContent | Normalize DocNode / JSON / HTML / plain → DocNode | | isEmptyContent | Required-field checks (media counts as content) | | getPlainText / getTextLength | Min/max length | | validateContent | One-liner { ok, errors } for required / min / max |

Validation UI

Presentational only — your app owns the rules (validateContent, Zod, RHF, …). Pass the result into the editor chrome.

Default — when error is set, red shell outline + message under the surface:

const v = validateContent(html, { required: true, minLength: 10 });

<NaatEditor
  error={v.ok ? undefined : v.errors[0]}
  onChange={({ html }) => setHtml(html)}
/>

// invalid without copy
<NaatEditor error />

Off — keep aria-invalid, hide outline + message:

<NaatEditor error="Answer is required" showError={false} />

Customize — CSS vars (--ne-error-border, --ne-error-color, …), theme tokens, renderError, invalidClassName:

<NaatEditor
  error="Max 500 characters"
  invalidClassName="ring-2 ring-red-500"
  renderError={(msg) => <p className="text-sm text-red-600">{msg}</p>}
  config={{ theme: { error: { border: "#b91c1c", color: "#b91c1c" } } }}
/>

Philosophy for every feature: default out of the box → turn on/off → customize deeply.

Content preview (toolbar)

The default toolbar includes a Preview button (after media, before undo/redo). It opens a read-only modal of the current document as HTML (same export as onChange).

Default — on (included in DEFAULT_CONFIG / buildGroupedToolbar when "preview" is listed).

Off — omit "preview" from toolbar.items (or from the action list you pass to buildFlatToolbar / buildGroupedToolbar).

CustomizepreviewTitle="…", or style .naat-editor-preview-* / --ne-preview-*.

<NaatEditor previewTitle="Article preview" />

// without preview:
<NaatEditor
  config={{
    toolbar: {
      items: buildGroupedToolbar([
        "bold", "italic", "link", "heading1", "image", "undo", "redo",
        // no "preview"
      ]),
    },
  }}
/>

Link hover preview

While editing, plain clicks on links do not navigate (caret stays intact). Hover shows a small bubble with the URL and an Open button (target=_blank, rel=noopener noreferrer). Ctrl/Cmd+click also opens immediately.

Default — on (omit the prop or pass linkPreview).

OfflinkPreview={false}.

CustomizerenderLinkPreview, or style .naat-editor-link-preview / --ne-link-preview-*.

<NaatEditor
  // linkPreview // default true
  // linkPreview={false}
  renderLinkPreview={({ href, onOpen, onClose }) => (
    <div className="my-link-chip">
      <button type="button" onClick={onOpen}>{href}</button>
      <button type="button" onClick={onClose}>×</button>
    </div>
  )}
/>

Form helpers (hooks) — optional

You do not need hooks. <NaatEditor /> + validateContent / isEmptyContent is enough.

Optional extras only (no generic UI hooks like useDisclosure — use Mantine / your own):

| Hook | Role | |------|------| | useNaatEditor | Context inside provider (compose path) | | useNaatEditorField | Form state + editorProps (remount via key on setValue / reset) | | useNaatEditorValidation | Thin wrapper: validateContenterror for chrome |

const field = useNaatEditorField({ initialContent: savedHtml });
const v = useNaatEditorValidation({
  content: field.value,
  required: true,
  minLength: 10,
});

<NaatEditor {...field.editorProps} error={v.error} />

Customize deeply

Same package. Theme tokens, toolbar display/icons, custom items, and full layout control — without changing how the simple path works.

Theme + layout

import {
  NaatEditor,
  NaatEditorProvider,
  NaatToolbar,
  NaatSurface,
} from "@naatchaal/editor";

// Theme tokens + own chrome placement
<NaatEditor
  config={{
    theme: {
      accent: "#16a34a",
      radius: "16px",
      toolbar: { background: "#0B1F3A", padding: "0.75rem" },
      surface: { padding: "1.5rem", minHeight: "22rem" },
    },
  }}
  renderLayout={({ toolbar, surface }) => (
    <>
      {surface}
      {toolbar}
    </>
  )}
/>

// Or full composition
<NaatEditorProvider>
  <div className="naat-editor">
    <NaatToolbar />
    <NaatSurface />
  </div>
</NaatEditorProvider>

Chrome with CSS / Tailwind

Editor chrome (shell, toolbar, surface) — not content marks — is styled three ways:

  1. config.theme → CSS vars (--ne-accent, --ne-toolbar-bg, --ne-btn-*, --ne-surface-min-height, --ne-surface-max-height, …) on .naat-editor
  2. Plain CSS — override those vars or target .naat-editor, .naat-editor-toolbar, .naat-editor-toolbtn, .naat-editor-surface
  3. className on NaatEditor / NaatToolbar / NaatSurface (merged onto the real chrome nodes)
<NaatEditor
  className="rounded-2xl border shadow-sm [&_.naat-editor-toolbtn]:rounded-lg"
  style={{ ["--ne-accent" as string]: "#0ea5e9" }}
/>

// Compose path — wrap with .naat-editor; height via theme or Tailwind on surface
<NaatEditorProvider
  config={{ theme: { surface: { minHeight: "12rem", maxHeight: "20rem" } } }}
>
  <div className="naat-editor rounded-2xl">
    <NaatToolbar className="bg-slate-900 text-white" />
    <NaatSurface className="p-6 min-h-48 max-h-80 overflow-y-auto" placeholder="Write…" />
  </div>
</NaatEditorProvider>

Height knobs: theme.surface.minHeight / maxHeight (or Tailwind min-h / max-h / overflow-y-auto on the surface). Use className / CSS for other chrome (radius, padding, colors).

Toolbar items, display, fonts

toolbar.items may be button | dropdown (custom option objects) | separator | custom (React render).

toolbar: {
  display: "icon", // or "label" | "icon-label"
  items: [
    { type: "button", id: "bold" },
    { type: "button", id: "italic", display: "icon-label", label: "Italic" },
    { type: "separator" },
    {
      type: "dropdown",
      id: "heading",
      label: "Heading",
      items: ["heading1", "heading2", "heading3"],
    },
    { type: "custom", id: "emoji", render: () => <button type="button">…</button> },
  ],
}

theme: {
  toolbar: { fontFamily: "Georgia, serif", fontSize: "0.9rem", fontWeight: "600" },
  button: { fontFamily: "Inter, sans-serif" },
}

Helpers: buildFlatToolbar, buildGroupedToolbar, createEditorConfig.

Custom icons

Built-in SVGs are the default (no icon library required). Override per action:

import { NaatEditor, DEFAULT_TOOLBAR_ICONS } from "@naatchaal/editor";

<NaatEditor icons={{ bold: MyBold, image: MyImage }} />

Or renderIcon={(id, props) => …} (return null to keep the default).

Built-ins

Marks: bold, italic, underline, strike, code, link, fontSize, textColor, highlight
Blocks: paragraph, heading, bulletList, orderedList, codeBlock, blockquote, image, video, horizontalRule
Actions: unlink, clearFormatting, preview (plus mark/block toggles and undo/redo)


AI assistants (Cursor / Claude)

| Resource | URL / path | |----------|------------| | Short AI index | llms.txt · https://naat.tools/editor/llms.txt | | Full AI docs | llms-full.txt · https://naat.tools/editor/llms-full.txt | | Agent rules | AGENTS.md | | Cursor skill | skills/naat-editor/SKILL.md | | MCP server | @naatchaal/editor-mcp |

{
  "mcpServers": {
    "naat-editor": {
      "command": "npx",
      "args": ["-y", "@naatchaal/editor-mcp"]
    }
  }
}

License

MIT — Naatchaal