Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion apps/snow-leopard/components/document/text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { EditorView } from "prosemirror-view";
import React, { memo, useEffect, useRef, useCallback, useState } from "react";
import { buildContentFromDocument, buildDocumentFromContent } from "@/lib/editor/functions";
import { setActiveEditorView } from "@/lib/editor/editor-state";

import { EditorToolbar } from "@/components/document/editor-toolbar";
import {
savePluginKey,
Expand Down Expand Up @@ -386,6 +385,76 @@ function PureEditor({
pointer-events: none;
z-index: 1;
}

/* Emoji plugin styles */
.emoji-widget {
display: inline;
font-size: 1.2em;
vertical-align: middle;
line-height: 1;
margin: 0 1px;
}

.emoji-hidden {
display: none;
}

/* Ensure emojis render properly */
.ProseMirror {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

/* Emoji suggestion panel styles */
.emoji-suggestion-panel {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: rgb(0 0 0);
color: rgb(255 255 255);
border-color: rgb(55 65 81);
}

.emoji-suggestion-panel:focus {
outline: 2px solid rgb(59 130 246);
outline-offset: 2px;
}

.emoji-suggestion-panel .emoji-suggestion-header {
color: rgb(255 255 255);
}

.emoji-suggestion-panel .emoji-suggestion-item {
color: rgb(255 255 255);
}

.emoji-suggestion-panel .emoji-suggestion-item:hover {
background-color: rgb(55 65 81);
}

.emoji-suggestion-panel .emoji-suggestion-item.selected {
background-color: rgb(55 65 81);
}

.emoji-suggestion-panel .emoji-suggestion-shortcuts {
color: rgb(156 163 175);
opacity: 0.8;
}

.emoji-suggestion-panel::-webkit-scrollbar {
height: 6px;
}

.emoji-suggestion-panel::-webkit-scrollbar-track {
background: #374151;
border-radius: 3px;
}

.emoji-suggestion-panel::-webkit-scrollbar-thumb {
background: #6b7280;
border-radius: 3px;
}

.emoji-suggestion-panel::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
`}</style>
</>
);
Expand Down
188 changes: 188 additions & 0 deletions apps/snow-leopard/components/emoji-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"use client";

import { useCallback, useEffect, useState, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";

export interface EmojiSuggestion {
emoji: string;
code: string;
score: number;
}

interface EmojiOverlayProps {
isOpen: boolean;
onClose: () => void;
onSelectEmoji: (emojiCode: string) => void;
position?: { x: number; y: number };
suggestions: EmojiSuggestion[];
selectedIndex: number;
onSelectedIndexChange: (index: number) => void;
query: string;
}

export default function EmojiOverlay({
isOpen,
onClose,
onSelectEmoji,
position = { x: 100, y: 100 },
suggestions,
selectedIndex,
onSelectedIndexChange,
query,
}: EmojiOverlayProps) {
const [currentPosition, setCurrentPosition] = useState(position);
// Removed drag functionality for a cleaner, compact overlay
const overlayRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLDivElement>(null);

// Update current position when initial position prop changes
useEffect(() => {
setCurrentPosition(position);
}, [position]);

// Removed drag functionality for a cleaner, compact overlay

const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!isOpen || suggestions.length === 0) return;

switch (e.key) {
case "Escape":
onClose();
break;
case "ArrowRight":
case "ArrowDown":
case "Tab":
e.preventDefault();
onSelectedIndexChange(Math.min(selectedIndex + 1, suggestions.length - 1));
break;
case "ArrowLeft":
case "ArrowUp":
e.preventDefault();
onSelectedIndexChange(Math.max(selectedIndex - 1, 0));
break;
case "Home":
e.preventDefault();
onSelectedIndexChange(0);
break;
case "End":
e.preventDefault();
onSelectedIndexChange(suggestions.length - 1);
break;
case "Enter":
case " ":
e.preventDefault();
if (selectedIndex < suggestions.length) {
onSelectEmoji(suggestions[selectedIndex].code);
}
break;
}
},
[isOpen, suggestions, selectedIndex, onSelectedIndexChange, onSelectEmoji, onClose]
);

const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (
overlayRef.current &&
!overlayRef.current.contains(event.target as Node)
) {
onClose();
}
},
[onClose]
);

useEffect(() => {
if (isOpen) {
window.addEventListener("keydown", handleKeyDown);
document.addEventListener("mousedown", handleClickOutside);
}

return () => {
window.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen, handleKeyDown, handleClickOutside]);

useEffect(() => {
if (isOpen && listRef.current) {
const selectedItem = listRef.current.querySelector(`[data-index="${selectedIndex}"]`) as HTMLElement;
if (selectedItem) {
selectedItem.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center"
});
}
}
}, [selectedIndex, isOpen]);

// Adjust position to ensure overlay is visible within viewport
useEffect(() => {
if (isOpen && overlayRef.current) {
const overlay = overlayRef.current;
const rect = overlay.getBoundingClientRect();

const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

let adjustedX = currentPosition.x;
let adjustedY = currentPosition.y;

if (rect.right > viewportWidth) {
adjustedX = viewportWidth - rect.width - 10;
}

if (rect.bottom > viewportHeight) {
adjustedY = viewportHeight - rect.height - 10;
}

if (adjustedX !== currentPosition.x || adjustedY !== currentPosition.y) {
overlay.style.left = `${adjustedX}px`;
overlay.style.top = `${adjustedY}px`;
}
}
}, [isOpen, currentPosition]);

Comment on lines +122 to +148
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix viewport repositioning: update React state instead of mutating DOM styles

Directly setting overlay.style.left/top will be overwritten on the next render because style is controlled by React via currentPosition. Persist the adjustment by updating state.

Apply this diff:

-      if (adjustedX !== currentPosition.x || adjustedY !== currentPosition.y) {
-        overlay.style.left = `${adjustedX}px`;
-        overlay.style.top = `${adjustedY}px`;
-      }
+      if (adjustedX !== currentPosition.x || adjustedY !== currentPosition.y) {
+        setCurrentPosition({ x: adjustedX, y: adjustedY });
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Adjust position to ensure overlay is visible within viewport
useEffect(() => {
if (isOpen && overlayRef.current) {
const overlay = overlayRef.current;
const rect = overlay.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let adjustedX = currentPosition.x;
let adjustedY = currentPosition.y;
if (rect.right > viewportWidth) {
adjustedX = viewportWidth - rect.width - 10;
}
if (rect.bottom > viewportHeight) {
adjustedY = viewportHeight - rect.height - 10;
}
if (adjustedX !== currentPosition.x || adjustedY !== currentPosition.y) {
overlay.style.left = `${adjustedX}px`;
overlay.style.top = `${adjustedY}px`;
}
}
}, [isOpen, currentPosition]);
// Adjust position to ensure overlay is visible within viewport
useEffect(() => {
if (isOpen && overlayRef.current) {
const overlay = overlayRef.current;
const rect = overlay.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let adjustedX = currentPosition.x;
let adjustedY = currentPosition.y;
if (rect.right > viewportWidth) {
adjustedX = viewportWidth - rect.width - 10;
}
if (rect.bottom > viewportHeight) {
adjustedY = viewportHeight - rect.height - 10;
}
if (adjustedX !== currentPosition.x || adjustedY !== currentPosition.y) {
setCurrentPosition({ x: adjustedX, y: adjustedY });
}
}
}, [isOpen, currentPosition]);
🤖 Prompt for AI Agents
In apps/snow-leopard/components/emoji-overlay.tsx around lines 122 to 148, the
viewport repositioning currently mutates overlay.style.left/top directly which
will be overwritten by React on next render; instead compute adjustedX/adjustedY
as you already do and call the state updater (e.g., setCurrentPosition or the
prop callback you use to control currentPosition) with the new coordinates so
the adjustment persists in React state; only call the updater if values changed,
keep the same rect/viewport logic, and ensure the setter is included in the
effect dependencies (or use a functional updater) to avoid stale closures.

if (!isOpen || suggestions.length === 0) return null;

return (
<AnimatePresence>
{isOpen && (
<motion.div
ref={overlayRef}
className="fixed z-50 bg-background rounded-md shadow-lg border border-border overflow-hidden select-none max-w-[172px]"
style={{
top: `${currentPosition.y}px`,
left: `${currentPosition.x}px`,
}}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.15 }}
>
<div
ref={listRef}
className="flex gap-1 overflow-x-auto p-1 bg-background scrollbar-thin"
>
{suggestions.map((suggestion, index) => (
<button
key={`${suggestion.code}-${index}`}
data-index={index}
onClick={() => onSelectEmoji(suggestion.code)}
className={cn(
"p-1 rounded-md text-xl transition-colors",
index === selectedIndex ? "bg-muted" : "hover:bg-muted/60"
)}
>
{suggestion.emoji}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
);
}
2 changes: 2 additions & 0 deletions apps/snow-leopard/lib/editor/editor-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { synonymsPlugin } from './synonym-plugin';
import { diffPlugin } from './diff-plugin';
import { formatPlugin } from './format-plugin';
import { savePlugin } from './save-plugin';
import { emojiPlugin } from './emoji-plugin';

export interface EditorPluginOptions {
documentId: string;
Expand All @@ -34,6 +35,7 @@ export function createEditorPlugins(opts: EditorPluginOptions): Plugin[] {
synonymsPlugin(),
diffPlugin(opts.documentId),
formatPlugin(opts.setActiveFormats),
emojiPlugin(),
savePlugin({
saveFunction: opts.performSave,
initialLastSaved: opts.initialLastSaved,
Expand Down
Loading