feat: add rich emoji preview in editor

Emoji inserted via the autocomplete now display as actual images/characters
instead of :shortcode: text:

- Custom emoji: renders as inline <img> with proper sizing
- Unicode emoji: renders as text with emoji font sizing
- Both show :shortcode: on hover via title attribute

CSS styles ensure proper vertical alignment with surrounding text.
This commit is contained in:
Claude
2026-01-12 10:17:22 +00:00
parent 3154efa635
commit 8f9bf76656
2 changed files with 60 additions and 3 deletions

View File

@@ -5,8 +5,7 @@ import {
useMemo,
useCallback,
} from "react";
import { useEditor, EditorContent } from "@tiptap/react";
import { ReactRenderer } from "@tiptap/react";
import { useEditor, EditorContent, ReactRenderer } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Mention from "@tiptap/extension-mention";
import Placeholder from "@tiptap/extension-placeholder";
@@ -62,9 +61,47 @@ export interface MentionEditorHandle {
submit: () => void;
}
// Create emoji extension by extending Mention with a different name
// Create emoji extension by extending Mention with a different name and custom node view
const EmojiMention = Mention.extend({
name: "emoji",
addNodeView() {
return ({ node, HTMLAttributes }) => {
// Create wrapper span
const dom = document.createElement("span");
dom.className = "emoji-node";
Object.entries(HTMLAttributes).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
dom.setAttribute(key, String(value));
}
});
const { url, source, id } = node.attrs;
const isUnicode = source === "unicode";
if (isUnicode) {
// Unicode emoji - render as text span
const span = document.createElement("span");
span.className = "emoji-unicode";
span.textContent = url;
span.title = `:${id}:`;
dom.appendChild(span);
} else {
// Custom emoji - render as image
const img = document.createElement("img");
img.src = url;
img.alt = `:${id}:`;
img.title = `:${id}:`;
img.className = "emoji-image";
img.draggable = false;
dom.appendChild(img);
}
return {
dom,
};
};
},
});
export const MentionEditor = forwardRef<

View File

@@ -314,3 +314,23 @@ body.animating-layout
.ProseMirror .mention:hover {
background-color: hsl(var(--primary) / 0.2);
}
/* Emoji styles */
.ProseMirror .emoji-node {
display: inline-flex;
align-items: center;
vertical-align: middle;
}
.ProseMirror .emoji-image {
height: 1.2em;
width: auto;
vertical-align: middle;
object-fit: contain;
}
.ProseMirror .emoji-unicode {
font-size: 1.1em;
line-height: 1;
vertical-align: middle;
}