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

@comenchi/editor

v1.0.4

Published

Notion-style WYSIWYG editor with AI-powered autocompletions

Readme

@comenchi/editor - Editor WYSIWYG con Estilos Tailwind

Editor de texto enriquecido estilo Notion con estilos Tailwind CSS integrados, basado en TipTap y con funcionalidades avanzadas.

🚀 Características principales

  • Editor WYSIWYG basado en TipTap con funcionalidades modernas
  • Comandos con slash (/) para inserción rápida de contenido
  • Highlighting con IA para resaltar texto automáticamente
  • Soporte para múltiples formatos: Markdown, imágenes, videos, tweets
  • Bubble menu contextual para formateo de texto
  • Extensiones matemáticas con KaTeX
  • Arrastrar y soltar para imágenes y contenido
  • Gestión de estado con Jotai

📦 Instalación

pnpm add @comenchi/editor

Dependencias requeridas

pnpm add react react-hook-form

🔧 Uso básico

Editor con estilos integrados (Recomendado)

import { EditorWithStyles } from "@comenchi/editor";
import { useForm } from "react-hook-form";

function MyForm() {
  const { control } = useForm({
    defaultValues: {
      content: null,
    },
  });

  return (
    <EditorWithStyles
      name="content"
      control={control}
      placeholder="Escribe algo increíble..."
      showCharacterCount
      showSaveStatus
    />
  );
}

Editor con configuración personalizada

import {
  EditorForm,
  defaultStyledExtensions,
  type JSONContent,
} from "@comenchi/editor";

function MyEditor() {
  const initialContent: JSONContent = {
    type: "doc",
    content: [
      {
        type: "paragraph",
        content: [
          {
            type: "text",
            text: "¡Bienvenido al editor! Escribe '/' para ver los comandos disponibles.",
          },
        ],
      },
    ],
  };

  return (
    <EditorRoot>
      <EditorContent
        initialContent={initialContent}
        extensions={[
          StarterKit,
          Placeholder.configure({
            placeholder: "Escribe algo increíble...",
          }),
          TiptapImage,
          TiptapUnderline,
          MarkdownExtension,
          AIHighlight,
          Command.configure({
            suggestion: {
              items: createSuggestionItems([
                {
                  title: "Texto",
                  description: "Empezar a escribir texto plano.",
                  searchTerms: ["p", "paragraph"],
                  icon: "📝",
                  command: ({ editor, range }) => {
                    editor
                      .chain()
                      .focus()
                      .deleteRange(range)
                      .toggleNode("paragraph", "paragraph")
                      .run();
                  },
                },
                {
                  title: "Encabezado 1",
                  description: "Encabezado de sección grande.",
                  searchTerms: ["title", "big", "h1"],
                  icon: "📰",
                  command: ({ editor, range }) => {
                    editor
                      .chain()
                      .focus()
                      .deleteRange(range)
                      .setNode("heading", { level: 1 })
                      .run();
                  },
                },
                // Más comandos...
              ]),
              render: renderItems,
            },
          }),
        ]}
        editorProps={{
          attributes: {
            class: "prose prose-lg dark:prose-invert max-w-full focus:outline-none",
          },
        }}
      >
        <EditorCommand className="z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border border-muted bg-background px-1 py-2 shadow-md transition-all">
          <EditorCommandEmpty className="px-2 text-muted-foreground">
            No se encontraron resultados.
          </EditorCommandEmpty>
          <EditorCommandList>
            {/* Los comandos se renderizan automáticamente aquí */}
          </EditorCommandList>
        </EditorCommand>

        <EditorBubble className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl">
          <EditorBubbleItem
            onSelect={(editor) => editor.chain().focus().toggleBold().run()}
            className="p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
          >
            <strong>B</strong>
          </EditorBubbleItem>
          <EditorBubbleItem
            onSelect={(editor) => editor.chain().focus().toggleItalic().run()}
            className="p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
          >
            <em>I</em>
          </EditorBubbleItem>
          <EditorBubbleItem
            onSelect={(editor) => editor.chain().focus().toggleUnderline().run()}
            className="p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
          >
            <u>U</u>
          </EditorBubbleItem>
        </EditorBubble>
      </EditorContent>
    </EditorRoot>
  );
}

🧩 Componentes principales

EditorRoot

Componente raíz que proporciona el contexto global del editor usando Jotai para gestión de estado.

<EditorRoot>
  {/* Todos los componentes del editor deben estar aquí */}
</EditorRoot>

EditorContent

Componente principal que renderiza el contenido editable del editor.

<EditorContent
  initialContent={content}
  extensions={extensions}
  editorProps={{
    attributes: {
      class: "prose prose-lg focus:outline-none",
    },
  }}
>
  {/* Componentes adicionales como bubble menu, comandos, etc. */}
</EditorContent>

Props:

  • initialContent: Contenido inicial en formato JSONContent
  • extensions: Array de extensiones de TipTap
  • editorProps: Propiedades pasadas al editor de TipTap
  • className: Clases CSS adicionales

EditorBubble y EditorBubbleItem

Menu contextual que aparece al seleccionar texto.

<EditorBubble className="flex rounded-md border bg-background shadow-xl">
  <EditorBubbleItem
    onSelect={(editor) => editor.chain().focus().toggleBold().run()}
  >
    Negrita
  </EditorBubbleItem>
  <EditorBubbleItem
    onSelect={(editor) => editor.chain().focus().toggleItalic().run()}
  >
    Cursiva
  </EditorBubbleItem>
</EditorBubble>

EditorCommand y EditorCommandItem

Sistema de comandos activado con slash (/).

<EditorCommand className="z-50 max-h-[330px] overflow-y-auto rounded-md border bg-background">
  <EditorCommandEmpty>No hay comandos disponibles.</EditorCommandEmpty>
  <EditorCommandList>
    {suggestionItems.map((item) => (
      <EditorCommandItem
        key={item.title}
        onCommand={item.command}
        className="flex items-center space-x-2 px-2 py-1 hover:bg-accent"
      >
        <span>{item.icon}</span>
        <div>
          <p className="font-medium">{item.title}</p>
          <p className="text-xs text-muted-foreground">{item.description}</p>
        </div>
      </EditorCommandItem>
    ))}
  </EditorCommandList>
</EditorCommand>

🔌 Extensiones disponibles

Extensiones básicas

import {
  StarterKit,          // Funcionalidades básicas (bold, italic, etc.)
  Placeholder,         // Texto placeholder
  TiptapImage,         // Soporte para imágenes
  TiptapUnderline,     // Texto subrayado
  MarkdownExtension,   // Soporte para markdown
  TextStyle,           // Estilos de texto
  Color,               // Colores de texto
  HighlightExtension,  // Highlighting de texto
  HorizontalRule,      // Reglas horizontales
  TaskItem,            // Items de tareas
  TaskList,            // Listas de tareas
  TiptapLink,          // Enlaces
  CharacterCount,      // Conteo de caracteres
  CustomKeymap,        // Atajos de teclado personalizados
} from "novel";

Extensiones multimedia

import {
  Youtube,             // Embeds de YouTube
  Twitter,             // Embeds de Twitter/X
  Mathematics,         // Fórmulas matemáticas con KaTeX
  UpdatedImage,        // Imágenes con funcionalidades mejoradas
  ImageResizer,        // Redimensionado de imágenes
} from "novel";

Extensiones de código

import {
  CodeBlockLowlight,   // Bloques de código con highlighting
} from "novel";

Extensiones de IA

import {
  AIHighlight,         // Highlighting inteligente
  addAIHighlight,      // Función para añadir highlighting
  removeAIHighlight,   // Función para remover highlighting
} from "novel";

// Uso de funciones de IA
const editor = useEditor();

// Añadir highlighting con IA
addAIHighlight(editor, "#c1ecf970");

// Remover highlighting
removeAIHighlight(editor);

Extensiones de comandos

import {
  Command,                    // Sistema de comandos slash
  renderItems,               // Renderizador de items de comando
  createSuggestionItems,     // Creador de items de sugerencia
  handleCommandNavigation,   // Manejador de navegación
  GlobalDragHandle,          // Manejo de arrastrar elementos
} from "novel";

📤 Plugins de carga de archivos

Plugin de subida de imágenes

import {
  UploadImagesPlugin,
  createImageUpload,
  handleImageDrop,
  handleImagePaste,
  type UploadFn,
  type ImageUploadOptions,
} from "novel";

// Configurar función de subida
const uploadFn: UploadFn = async (file) => {
  // Tu lógica de subida aquí
  const formData = new FormData();
  formData.append("file", file);
  
  const response = await fetch("/api/upload", {
    method: "POST",
    body: formData,
  });
  
  const data = await response.json();
  return data.url;
};

// Usar en extensiones
const extensions = [
  StarterKit,
  UploadImagesPlugin({ imageClass: "uploaded-image" }),
  TiptapImage.configure({
    HTMLAttributes: {
      class: "uploaded-image",
    },
  }),
];

// Configurar eventos de arrastrar y pegar
const editorProps = {
  handleDrop: (view, event, slice, moved) => {
    return handleImageDrop(view, event, moved, uploadFn);
  },
  handlePaste: (view, event, slice) => {
    return handleImagePaste(view, event, uploadFn);
  },
};

🎨 Personalización de comandos slash

import { createSuggestionItems, type SuggestionItem } from "novel";

const customCommands: SuggestionItem[] = [
  {
    title: "Encabezado 1",
    description: "Encabezado principal de la sección.",
    searchTerms: ["h1", "heading", "title"],
    icon: "📰",
    command: ({ editor, range }) => {
      editor
        .chain()
        .focus()
        .deleteRange(range)
        .setNode("heading", { level: 1 })
        .run();
    },
  },
  {
    title: "Lista de tareas",
    description: "Crear una lista de tareas con checkboxes.",
    searchTerms: ["todo", "task", "list", "check"],
    icon: "✅",
    command: ({ editor, range }) => {
      editor
        .chain()
        .focus()
        .deleteRange(range)
        .toggleTaskList()
        .run();
    },
  },
  {
    title: "Imagen",
    description: "Subir una imagen desde tu dispositivo.",
    searchTerms: ["photo", "picture", "media"],
    icon: "🖼️",
    command: ({ editor, range }) => {
      editor.chain().focus().deleteRange(range).run();
      // Lógica para abrir selector de archivos
      const input = document.createElement("input");
      input.type = "file";
      input.accept = "image/*";
      input.onchange = (e) => {
        const file = (e.target as HTMLInputElement).files?.[0];
        if (file) {
          // Procesar archivo de imagen
        }
      };
      input.click();
    },
  },
];

// Usar en la configuración del comando
const extensions = [
  Command.configure({
    suggestion: {
      items: createSuggestionItems(customCommands),
      render: renderItems,
    },
  }),
];

🔗 Hooks y utilidades

useEditor

Hook para obtener la instancia del editor actual.

import { useEditor } from "novel";

function MyComponent() {
  const { editor } = useEditor();
  
  if (!editor) return null;
  
  const saveContent = () => {
    const json = editor.getJSON();
    const html = editor.getHTML();
    // Guardar contenido...
  };
  
  return (
    <button onClick={saveContent}>
      Guardar contenido
    </button>
  );
}

Utilidades de contenido

import {
  getAllContent,    // Obtener todo el contenido en markdown
  getPrevText,      // Obtener texto anterior a una posición
  isValidUrl,       // Validar URL
  getUrlFromString, // Extraer URL de string
} from "novel";

const editor = useEditor();

// Obtener contenido en markdown
const markdownContent = getAllContent(editor);

// Obtener texto previo
const prevText = getPrevText(editor, 100);

// Validar URL
const isValid = isValidUrl("https://example.com");

// Extraer URL
const url = getUrlFromString("example.com");

Gestión de estado (Jotai)

import { queryAtom, rangeAtom } from "novel";
import { useAtom } from "jotai";

function CommandPanel() {
  const [query] = useAtom(queryAtom);
  const [range] = useAtom(rangeAtom);
  
  return (
    <div>
      <p>Query actual: {query}</p>
      <p>Rango seleccionado: {range?.from} - {range?.to}</p>
    </div>
  );
}

🛠️ Tecnologías utilizadas

Framework base

  • TipTap - Editor de texto enriquecido modular
  • React - Biblioteca de interfaz de usuario
  • TypeScript - Tipado estático

Gestión de estado

  • Jotai - Gestión de estado atómico y reactivo

UI y componentes

  • Radix UI - Componentes primitivos accesibles
  • cmdk - Sistema de comandos
  • Tippy.js - Tooltips y popovers

Funcionalidades especiales

  • KaTeX - Renderizado de matemáticas
  • React Tweet - Embeds de Twitter/X
  • React Moveable - Elementos movibles y redimensionables
  • React Markdown - Renderizado de markdown
  • Tunnel Rat - Portal de componentes
  • Lowlight - Highlighting de código

Extensiones de TipTap

  • Starter Kit, Image, Link, Placeholder, Task Lists
  • Character Count, Code Block, Color, Highlight
  • Horizontal Rule, Text Style, Underline, YouTube
  • Global Drag Handle, Markdown, Suggestion

📁 Estructura del proyecto

packages/editor/
├── src/
│   ├── components/           # Componentes React principales
│   │   ├── editor.tsx       # EditorRoot y EditorContent
│   │   ├── editor-bubble.tsx # Menu contextual
│   │   ├── editor-command.tsx # Sistema de comandos
│   │   └── ...
│   ├── extensions/          # Extensiones de TipTap
│   │   ├── ai-highlight.ts  # Highlighting con IA
│   │   ├── mathematics.ts   # Soporte matemático
│   │   ├── twitter.tsx      # Embeds de Twitter
│   │   └── ...
│   ├── plugins/             # Plugins del editor
│   │   └── upload-images.tsx # Plugin de subida de imágenes
│   ├── utils/               # Utilidades y helpers
│   │   ├── atoms.ts         # Atoms de Jotai
│   │   ├── store.ts         # Configuración del store
│   │   └── index.ts         # Utilidades generales
│   └── index.ts             # Exportaciones principales
├── package.json
└── README.md

🎯 Casos de uso comunes

Editor de blog básico

function BlogEditor() {
  return (
    <EditorRoot>
      <EditorContent
        extensions={[
          StarterKit,
          Placeholder.configure({
            placeholder: "Escribe tu artículo...",
          }),
          TiptapImage,
          MarkdownExtension,
        ]}
        className="prose max-w-none"
      />
    </EditorRoot>
  );
}

Editor con funcionalidades avanzadas

function AdvancedEditor() {
  return (
    <EditorRoot>
      <EditorContent
        extensions={[
          StarterKit,
          AIHighlight,
          Mathematics,
          Youtube,
          Twitter,
          TaskList,
          TaskItem,
          GlobalDragHandle,
          Command.configure({
            suggestion: {
              items: createSuggestionItems(allCommands),
              render: renderItems,
            },
          }),
        ]}
      >
        <EditorBubble>
          {/* Bubble menu items */}
        </EditorBubble>
        
        <EditorCommand>
          {/* Command items */}
        </EditorCommand>
      </EditorContent>
    </EditorRoot>
  );
}

📚 Recursos adicionales

🤝 Contribución

Este paquete está basado en Novel y ha sido adaptado para el ecosistema Comenchi.

📄 Licencia

Apache-2.0 License