From 8f9bf76656bee444b960bf785e4e6a3548d9dc90 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 10:17:22 +0000 Subject: [PATCH] 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 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. --- src/components/editor/MentionEditor.tsx | 43 +++++++++++++++++++++++-- src/index.css | 20 ++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/components/editor/MentionEditor.tsx b/src/components/editor/MentionEditor.tsx index 56d9995..09ab5b5 100644 --- a/src/components/editor/MentionEditor.tsx +++ b/src/components/editor/MentionEditor.tsx @@ -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< diff --git a/src/index.css b/src/index.css index d7971cb..9684d61 100644 --- a/src/index.css +++ b/src/index.css @@ -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; +}