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

@unknownaiio/editor

v0.2.2

Published

Production-ready Tiptap editor presets and a static React viewer for UnknownAI.

Readme

@unknownaiio/editor

A public React 19/Tiptap package for editing and statically rendering stored JSON documents. It ships compiled ESM, declarations, source maps, and CSS; consumers do not compile package TypeScript.

React 19, react-dom, and the Tiptap core/react/ProseMirror packages are peer dependencies. Keep them on the compatible versions shown by your package manager to guarantee one React and one Tiptap/ProseMirror runtime.

pnpm add @unknownaiio/editor

The package is public on npm (publishConfig.access: "public") under an UNLICENSED license: anyone can install it and read its code, and no one is granted rights to use it. Store Tiptap JSONContent, not HTML. To release a new version, follow PUBLISHING.md.

API

Load the stylesheet once and select an explicit capability tier.

import { Editor, RichTextViewer, type JSONContent } from "@unknownaiio/editor";
import "@unknownaiio/editor/styles.css";

function Document({ initial }: { initial: JSONContent }) {
  const [value, setValue] = React.useState(initial);
  return (
    <>
      <Editor preset="full" value={value} editable onChange={setValue} />
      <RichTextViewer value={value} />
    </>
  );
}

| Preset | Designed for | Included features | | --- | --- | --- | | composer | Quickly submitting AI/chat content | Attachment action, send/loading/error controls, mentions | | minimal | Small general rich-text input | Composer features plus marks, links, code, lists, blockquotes | | standard | Normal rich-text editing | Minimal features plus headings, tasks, highlight, typography, image/video/attachment nodes | | full | Everything supported | Standard features plus tables, colour, alignment, sub/superscript, rules, comments |

The root entry only exports Editor, its types, RichTextViewer, and Tiptap types. Integration helpers live at @unknownaiio/editor/integrations; intentional low-level composition lives at @unknownaiio/editor/custom.

Next.js and security

The interactive entry points are client components (they emit the "use client" directive). Import Editor or RichTextViewer from a Next.js Client Component; keep stored JSON and data loading in your Server Component.

Links are scheme-checked before the editor stores them and again by the viewer. That is defense in depth, not a trust boundary: validate and authorize document JSON and private-media keys on your server before persisting or resolving them.

Bundling note

The package is ESM and unused root exports can be tree-shaken. The selected preset itself is a runtime prop, however, so <Editor preset="minimal" /> necessarily imports all four preset implementations. Splitting each preset behind asynchronous imports would change editor mounting behavior, so this package intentionally does not do that. Use the smallest preset for behavior; consider a separate, preset-specific entrypoint only if a future performance requirement outweighs the single-component API.

Composer request state and attachment action

composer and minimal submit through onSubmit. An async handler clears only after it resolves; throwing preserves the draft. Supply submitButtonStatus to show the built-in loader or error button.

const [status, setStatus] = React.useState<"idle" | "loader" | "error">("idle");

<Editor
  preset="composer"
  onAttach={() => picker.current?.click()}
  submitButtonStatus={status}
  submitErrorTooltip="Request failed — edit and retry"
  onSubmit={async (json, text) => {
    setStatus("loader");
    try {
      await api.send({ content: json, preview: text });
      setStatus("idle");
    } catch (error) {
      setStatus("error");
      throw error;
    }
  }}
/>

onAttach only signals: open your picker there. composer and minimal have no file nodes, so the host keeps the chosen files itself (as chips above the composer, say) and sends them with the message in onSubmit. handleFileDrop(files) works the same way for files dropped on the composer, and attachmentIconType sets the button's icon, as in the other presets (see Images, files and drops).

The composer draws no border or background of its own; put it in the frame your page needs (a bordered box, a chat bar).

The attachment button sits in the left slot of composer, as in a chat input, and at the end of the formatting toolbar in minimal, as in standard and full. leadingAction takes the left slot and replaces the attachment action, and trailingActions adds host controls beside the state button. Pass users and currentUserId to any preset for @ mentions.

Images, files and drops

The package owns no picker, dialog, provider or upload API. Every way a file can reach a document goes through the host, which decides what is allowed, uploads it, and inserts it with the integration helpers:

| Way in | Prop | What the editor does | | --- | --- | --- | | Attachment button (and mod+shift+i) | handleFileInput() | calls it; nothing else | | composer/minimal attachment button | onAttach() | calls it; nothing else | | Dropping files | handleFileDrop(files) | hands it the File objects and puts the caret where they were dropped |

There is one attachment button. attachmentIconType ("file", the default, "image" or "video") changes only its icon and label ("Add files", "Add image", "Add video"); it calls the same handler whatever it shows, and the host decides what its picker accepts. An app that takes images passes "image" and opens an image/* picker, or its own search dialog or media library, and inserts what is chosen.

The button appears only when its handler is passed; without one it is not rendered at all, because pressing it could do nothing the host knows about. Without handleFileDrop, a file dragged over the editor shows the not-allowed cursor and a drop is ignored (with a console warning in development), and the browser is kept from opening the file. Dragging text inside the editor is unaffected.

A complete upload recipe

One routine serves every way in: the button opens a picker, the picker and a drop both hand their files to uploadAndInsert. insertLocalFiles shows each file at once from a local preview, the upload runs, and updateFileNode swaps the preview for the durable key:

import { Editor, type AttachmentIconType, type TiptapEditor } from "@unknownaiio/editor";
import {
  insertLocalFiles,
  setFileUploadState,
  updateFileNode,
} from "@unknownaiio/editor/integrations";

const ACCEPT: Record<AttachmentIconType, string> = { file: "", image: "image/*", video: "video/*" };

function NoteEditor({ iconType = "file" }: { iconType?: AttachmentIconType }) {
  const editor = React.useRef<TiptapEditor | null>(null);
  const picker = React.useRef<HTMLInputElement>(null);

  async function uploadAndInsert(files: File[]) {
    const instance = editor.current;
    if (!instance || files.length === 0) return;
    // Check type, size and count here: the editor accepts whatever you insert.
    const ids = insertLocalFiles(instance, files);
    await Promise.all(files.map(async (file, index) => {
      const id = ids[index]!;
      setFileUploadState(instance, id, { status: "uploading" });
      try {
        const stored = await api.upload(file);
        updateFileNode(instance, id, { src: stored.key, mimeType: file.type });
        setFileUploadState(instance, id, { status: "uploaded" });
      } catch {
        setFileUploadState(instance, id, { status: "error" });
      }
    }));
  }

  return (
    <>
      <input
        ref={picker}
        type="file"
        hidden
        multiple
        accept={ACCEPT[iconType]}
        onChange={(event) => void uploadAndInsert(Array.from(event.target.files ?? []))}
      />
      <Editor
        ref={editor}
        preset="standard"
        editable
        attachmentIconType={iconType}
        handleFileInput={() => picker.current?.click()}
        handleFileDrop={(files) => void uploadAndInsert(files)}
      />
    </>
  );
}

Leave out handleFileDrop to refuse drops, or handleFileInput to have no button. Whether a file becomes an image, a video or a download card follows its MIME type, not the icon.

Inserting a file that is already stored

A file chosen from a media library or an image search needs no upload and no preview. Insert it by its durable key with insertFileNodes:

import { insertFileNodes } from "@unknownaiio/editor/integrations";

<Editor
  ref={editor}
  preset="standard"
  editable
  attachmentIconType="image"
  handleFileInput={() => setLibraryOpen(true)} // your own dialog
/>;

function onLibraryPick(item: LibraryItem) {
  insertFileNodes(editor.current!, [
    { src: item.key, mimeType: item.mimeType, title: item.name, alt: item.description, size: item.bytes },
  ]);
  setLibraryOpen(false);
}

The node is inserted at the caret. The editor keeps its selection while your dialog has focus, so the image lands where the user was writing.

Clicking a file

handleFileClick(file) receives a file's metadata (id, src, title, type, size, category) when its name is clicked in a read-only editor (editable={false}), for example to open a download or a viewer.

A document saved while an upload is in flight still holds the local blob: URL, which is dead after a reload. Refuse such documents on the server.

Render private media and attachments

Use one resolver for editing and viewing when stored src values are private keys rather than public URLs. It may be synchronous or asynchronous.

const resolveFile = async (fileId: string, key: string) =>
  (await api.files.downloadUrl({ fileId, key })).url;

<Editor preset="full" editable getFile={resolveFile} />
<RichTextViewer value={storedDocument} getFile={resolveFile} />

The viewer renders code, tasks, tables, mentions, images, video, and attachment download cards without mounting an editor. It scheme-checks URL-bearing output: links and attachments allow http(s):, mailto: and relative URLs; an image or video also allows a data: URL of its own kind and a blob: URL, as the editor does. Anything else renders as #.

Styling

The package is styled by its host, not on its own. It needs three things, and a missing one fails silently as unstyled output, never as an error.

  1. The stylesheet, imported once: @unknownaiio/editor/styles.css. It styles the document (headings, lists, code, tables, mentions) in both the editor and the viewer.
  2. Tailwind v4 scanning the built package. The toolbar, menus, tooltips and popovers use Tailwind classes that the package does not compile. Without the @source line they render unstyled while the document still looks right.
  3. The shadcn theme variables, defined by the host, with the usual @theme inline mapping to Tailwind's --color-* and --radius-*: --background, --foreground, --card, --card-foreground, --popover, --popover-foreground, --primary, --primary-foreground, --secondary, --secondary-foreground, --muted, --muted-foreground, --accent, --accent-foreground, --destructive, --border, --input, --ring and --radius. Without them borders and colours disappear. Define them on :root (not only on an app wrapper), because menus and popovers are portalled to body.
@import "tailwindcss";
@import "@unknownaiio/editor/styles.css";
@source "../node_modules/@unknownaiio/editor/dist/**/*.{js,mjs}";

A host that is not built on shadcn maps its own palette onto those names. Optional variables, each with a default: --tt-editor-font-size (1rem), --comment-highlight-bg, --comment-highlight-bg-active and --comment-highlight-border.

The document styles are wrapped in @scope (.unknown-editor-surface), so they never restyle another Tiptap editor in the host. The editor and the viewer each render that surface with .tiptap.ProseMirror inside it; a selector in @scope matches only below its root, so the two must stay separate elements.

Check the preview

pnpm playground

One tab per preset (the tab is kept in the URL, #full). Each shows the editor in one card and a RichTextViewer in another, with nothing else inside them, so both look as they will in a host page. Edit in one and compare with the other: the viewer should look the same as the editor, including the placeholder image in the full sample. If headings and code blocks look like plain text in one place only, the stylesheet's scope does not reach that element.

Above the cards, a switch passes or withholds the host handlers (onAttach, handleFileInput, handleFileDrop), so you can see the button appear and disappear, a select sets attachmentIconType, and a log shows each handler call. The playground plays the host: a native file picker that accepts what the icon promises (image/* for "image"), a simulated 1.5 s upload, and the same routine for dropped files. Turn the switch off and drop a file to see the refusal.

No preset draws a frame of its own, the composer included: the host puts the editor in the border, card or chat bar its page needs. The playground's cards are that host frame.

Scripts

pnpm build
pnpm check-types
pnpm lint
pnpm test
pnpm playground
pnpm build:playground

See features.md for the complete feature matrix and recipes.