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

@wulperstd/ui-react

v2.0.0

Published

Radix and Tailwind v4 React components for @wulperstd/editor-core: block editor, toolbars, menus, node views and theme presets.

Readme

@wulperstd/ui-react

Not published to npm. This package is private and consumed only through the pnpm workspace (currently by apps/cms-integration) — do not npm install or pnpm add it from a registry.

Shadcn-style React components for @wulperstd/editor-core's headless editor extensions: Radix primitives, cva variants, and Tailwind v4 utilities compiled by the consumer. The fastest way in is one of the two ready-to-use editors below — RichTextEditor or BlockEditor — each a complete, working useEditor + EditorContent wiring you can drop straight into an app. Everything else in this package (EditorToolbar, SelectionMenu, BlockGutter, TurnIntoMenu, the slash-command menu, and the individual ui/ primitives) is what those two editors are built from, and remains available on its own for building something different.

Two ready-to-use editors

RichTextEditor and BlockEditor are meant to be copied, not just imported: each is a small, fully-wired file — read it, and take it as the starting point for your own editor once you need something its props don't cover. That is a deliberate design choice, not a gap: these components keep their escape hatches narrow (documented below) rather than growing a prop for every possible customization, so the file stays short enough to actually read in one sitting. They are apps/cms-integration's own RichTextEditor/useNotionEditor + NotionEditorShell ported into this package, minus that app's CMS-specific output shape, curated embed services, and per-node placeholder copy — all choices that stay app-owned.

Required peers: react, react-dom, @tiptap/react, @tiptap/pm, @tiptap/suggestion, and tailwindcss — see package.json's peerDependencies for exact ranges. The consumer compiles Tailwind; this package ships its CSS entries (tailwind.css, content.css, themes/*.css) with @source so the consumer's own Tailwind build picks up this package's utility classes.

RichTextEditor

The simple editor: an optional fixed toolbar above plain document content — no slash menu, no drag handle, no floating selection menu.

import { RichTextEditor, EditorToolbar } from '@wulperstd/ui-react';
import '@wulperstd/ui-react/tailwind.css'; // compiled by your own Tailwind build
import '@wulperstd/ui-react/content.css'; // document typography, optional

function App() {
  return (
    <RichTextEditor
      content="<p>Hello world</p>"
      toolbar={<EditorToolbar />}
      onChange={(editor) => console.log(editor.getHTML())}
    />
  );
}

BlockEditor

The Notion-style editor: a slash-command ("/") menu, a per-block BlockGutter (drag grip, "add block below", and its turn-into/duplicate/ copy/delete/reset-formatting menu), a floating SelectionMenu, and the content itself.

import { BlockEditor } from '@wulperstd/ui-react';
import '@wulperstd/ui-react/tailwind.css'; // compiled by your own Tailwind build
import '@wulperstd/ui-react/content.css'; // document typography, optional

function App() {
  return (
    <BlockEditor
      content="<p>Hello world</p>"
      onChange={(editor) => console.log(editor.getJSON())}
    />
  );
}

The extensions escape hatch

Both components accept an extensions prop that, when given, replaces the built-in bundle outright — it does not merge with it. Each component also exports that built-in bundle as a plain value (richTextEditorExtensions, an array; blockEditorExtensions(options), a function, since its slash-menu catalog is configurable) so you can build your own list explicitly instead of fighting the default one:

// Add one extra extension:
extensions={[...richTextEditorExtensions, MyExtension]}

// Swap or remove one — the exact case a consumer needs and `useNotionEditor`
// used to hide inside itself, hardcoded, with no way for a caller to reach it:
import { embedExtension } from '@wulperstd/editor-core';

const extensions = blockEditorExtensions().map((extension) =>
  extension.name === 'embed'
    ? embedExtension.configure({ services: MY_EMBED_SERVICES })
    : extension,
);

Pass the full result through extensions.

The change callback

Both components take onChange?: (editor: Editor) => void, fired on every document update with the live Editor instance. Neither owns an output shape — call editor.getHTML(), editor.getJSON(), serializeMdx(editor.getJSON()), or anything else you need from it. This mirrors apps/cms-integration's own useNotionEditor, which derives an entire EditorOutput (html/mdx/json/ validation report) because that is what it needs. That choice does not belong in this package.

onChange also fires once on mount, right after the editor is created from the initial content — not only on later edits. Without that, the obvious onChange={(e) => setHtml(e.getHTML())} pattern with an initial content leaves derived state empty until the first keystroke, which defeats the point of a drop-in component. Both components follow the same reference: the callback gets the same live Editor either way, with nothing marking the mount call as different from an update.

The prose class

Both apply 'editor-prose' to the content element via editorProps.attributes.class by default (see content.css below). Pass your own contentClassName to replace it outright — include 'editor-prose' yourself to add to it (contentClassName="editor-prose my-extra-class"), or omit it entirely to use a different prose layer, such as Tailwind Typography's prose class.

The null-editor render

useEditor returns null on the very first render, before the Editor instance exists. Both components handle this internally (they render nothing until it does) — you never need your own if (!editor) return null; guard around them.

Usage

Slash-command menu

import { createSlashCommand } from '@wulperstd/editor-core';
import { createSlashMenuRenderer, DEFAULT_SLASH_MENU_ITEMS } from '@wulperstd/ui-react';

const SlashCommand = createSlashCommand({
  items: DEFAULT_SLASH_MENU_ITEMS,
  render: createSlashMenuRenderer({ emptyMessage: 'No results' }),
});

Pass SlashCommand alongside your other Tiptap extensions. createSlashCommand handles matching, filtering, and keyboard-event routing (all framework-neutral, from @wulperstd/editor-core); createSlashMenuRenderer mounts this package's Radix-based SlashMenu through the Suggestion plugin's own managed mounting (props.mount) and wires onStart/onUpdate/onKeyDown/onExit for you. DEFAULT_SLASH_MENU_ITEMS covers generic block-type conversions and insertions; build your own SlashMenuItem[] (each declaring a group) to add app-specific entries such as embeds or uploads.

EditorToolbar

The default, ready-to-use formatting toolbar — Bold/Italic/Strikethrough/ Inline code, Heading 1/2, bullet/numbered list, Quote, Code block, a divider insert action, and Undo/Redo:

import { EditorToolbar } from '@wulperstd/ui-react';

function Editor({ editor }: { editor: TiptapEditor }) {
  return <EditorToolbar editor={editor} />;
}

English labels only (design.md D7: "composition-based localization... the default compositions are English"). A consumer that needs different copy composes its own toolbar from the individual controls/* building blocks (MarkToggle, BlockToggle, HistoryButtons, TextAlignGroup, ResetFormattingButton) with their own label/tooltip props instead of configuring this component — see apps/cms-integration's BasicToolbar for that pattern.

SelectionMenu

A floating bar (via @tiptap/react/menus' BubbleMenu) that appears over the current text selection, Notion-style. With no children, it renders the default SelectionMenuContent — mark toggles, LinkPopover, ColorPicker, script toggles, TextAlignGroup, ResetFormattingButton, and TurnIntoMenu:

import { SelectionMenu } from '@wulperstd/ui-react';

function Editor({ editor }: { editor: TiptapEditor }) {
  return <SelectionMenu editor={editor} />;
}

children REPLACES the default composition entirely (D7) — a consumer wanting different copy or extra controls renders its own children built from MarkToggle/LinkPopover/ResetFormattingButton/etc, the same mechanism RichTextEditor's toolbar prop uses. See apps/cms-integration's Spanish NotionSelectionMenu for that pattern.

BlockGutter

A per-block drag grip plus an "add block below" button and a block menu (turn into, reset formatting, duplicate, copy, delete), Notion-style, positioned over the currently hovered or selected block:

import { BlockGutter } from '@wulperstd/ui-react';

function Editor({ editor }: { editor: TiptapEditor }) {
  return (
    <div style={{ position: 'relative' }}>
      <BlockGutter editor={editor} />
      {/* <EditorContent editor={editor} /> */}
    </div>
  );
}

BlockGutter is a hand-rolled overlay on native ProseMirror APIs (posAtCoords, NodeSelection, view.dragging, Selection.near) — it does not use @tiptap/extension-drag-handle-react or any other drag-handle library, and requires no extra peer dependency. It renders as an absolutely positioned sibling, never a body portal (design.md D10), so it MUST be rendered inside the same position: relative wrapper that also holds <EditorContent>.

Composition-based localization

Every control in this package takes its own label/tooltip props (or, for node-view factories that cannot receive props from the React tree, a labels object captured once) instead of a global classNames/labels prop on a bundle component (design.md D7). BlockEditor and RichTextEditor intentionally have NO classNames/labels props: a consumer wanting different copy composes its own tree from BlockGutter/SelectionMenu/ EditorToolbar/individual controls/*, the same way apps/cms-integration's NotionEditorShell and BasicToolbar do. Visual restyling goes through Tailwind utility classes and this package's CSS custom-property theme contract (tailwind.css's @theme inline tokens, themes/*.css presets), not per-slot classNames props.

Seams

These integration points are intentionally NOT built-in components — they are documented here so a consumer can add them without this package growing speculative props for features it does not itself implement (design.md D5, "Seams"):

  • Collaboration: pass Tiptap's Collaboration and CollaborationCaret extensions through BlockEditor's or RichTextEditor's full-replacement extensions prop (extensions={[...blockEditorExtensions(), Collaboration.configure(...), CollaborationCaret.configure(...)]}). Upload placeholders (node-views/image-upload) are local plugin decorations, not schema nodes, so they never sync over a collaborative session.
  • A future <CollaborationCursors/> or <AiMenu/> can mount as a sibling inside the same EditorScope this package's own components wrap their tree in — useEditorInstance()/useCurrentEditor() resolves the live editor with no editor prop threading needed (design.md D5). Compose it directly into your own copy of BlockEditor/RichTextEditor (or NotionEditorShell), next to BlockGutter/SelectionMenu.
  • AI slash items: SlashMenuItem[] is plain data (title, keywords, run, group, an optional icon), so an "Ask AI" entry can simply be appended to DEFAULT_SLASH_MENU_ITEMS (or your own catalog) and passed to blockEditorExtensions({ slashItems }).
  • AI in the selection menu: SelectionMenu's children slot can host an "Ask AI" control alongside (or instead of) the default SelectionMenuContent composition — see the SelectionMenu section above.