@comenchi/editor
v1.0.4
Published
Notion-style WYSIWYG editor with AI-powered autocompletions
Maintainers
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/editorDependencias 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 JSONContentextensions: Array de extensiones de TipTapeditorProps: Propiedades pasadas al editor de TipTapclassName: 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
