diff --git a/frontend/app/element/contextmenu.tsx b/frontend/app/element/contextmenu.tsx index bc8eb8190..e06dd11fb 100644 --- a/frontend/app/element/contextmenu.tsx +++ b/frontend/app/element/contextmenu.tsx @@ -4,6 +4,7 @@ import clsx from "clsx"; import React, { memo, useEffect, useLayoutEffect, useRef, useState } from "react"; import ReactDOM from "react-dom"; +import { throttle } from "throttle-debounce"; import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; import "./contextmenu.less"; @@ -269,7 +270,7 @@ const ContextMenu = memo( parentRef: React.RefObject, label: string ) => { - setTimeout(() => { + throttle(0, () => { const subContextMenuRef = subContextMenuRefs.current[key]?.current; if (!subContextMenuRef) return; @@ -296,7 +297,7 @@ const ContextMenu = memo( ...prev, [key]: { top, left, label }, })); - }, 0); + })(); }; const handleMouseEnterItem = ( diff --git a/frontend/app/element/emojipalette.less b/frontend/app/element/emojipalette.less new file mode 100644 index 000000000..98b5f87e9 --- /dev/null +++ b/frontend/app/element/emojipalette.less @@ -0,0 +1,43 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.emoji-palette-content { + padding: 10px; + max-height: 250px; + width: 250px; + display: flex; + flex-direction: column; + + // > input { + // margin-top: 10px; + // } +} + +.emoji-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(30px, 1fr)); + gap: 5px; + justify-content: center; + align-items: center; + margin-top: 5px; +} + +.emoji-button { + font-size: 24px; + padding: 5px; + cursor: pointer; + background: none; + border: none; + transition: background-color 0.3s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + border-radius: 5px; + } +} + +.no-emojis { + font-size: 14px; + color: #888; + text-align: center; +} diff --git a/frontend/app/element/emojipalette.stories.tsx b/frontend/app/element/emojipalette.stories.tsx new file mode 100644 index 000000000..42c4b389f --- /dev/null +++ b/frontend/app/element/emojipalette.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useRef } from "react"; +import { EmojiPalette } from "./emojipalette"; + +const meta: Meta = { + title: "Elements/EmojiPalette", + component: EmojiPalette, + args: { + className: "custom-emoji-palette-class", + }, + argTypes: { + scopeRef: { + description: "Reference to the outer container element for positioning", + }, + className: { + description: "Custom class for emoji palette styling", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const DefaultEmojiPalette: Story = { + render: (args) => { + const scopeRef = useRef(null); + + return ( +
+ +
+ ); + }, + args: { + className: "custom-emoji-palette-class", + }, +}; diff --git a/frontend/app/element/emojipalette.tsx b/frontend/app/element/emojipalette.tsx index e69de29bb..d755f1849 100644 --- a/frontend/app/element/emojipalette.tsx +++ b/frontend/app/element/emojipalette.tsx @@ -0,0 +1,280 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import clsx from "clsx"; +import React, { memo, useEffect, useRef, useState } from "react"; +import { Button } from "./button"; +import { Input } from "./input"; +import { Palette } from "./palette"; + +import "./emojiPalette.less"; + +interface EmojiPaletteProps { + scopeRef: React.RefObject; + className?: string; +} + +const emojiList = [ + // Smileys & Emotion + { emoji: "๐Ÿ˜€", name: "grinning face" }, + { emoji: "๐Ÿ˜", name: "beaming face with smiling eyes" }, + { emoji: "๐Ÿ˜‚", name: "face with tears of joy" }, + { emoji: "๐Ÿคฃ", name: "rolling on the floor laughing" }, + { emoji: "๐Ÿ˜ƒ", name: "grinning face with big eyes" }, + { emoji: "๐Ÿ˜„", name: "grinning face with smiling eyes" }, + { emoji: "๐Ÿ˜…", name: "grinning face with sweat" }, + { emoji: "๐Ÿ˜†", name: "grinning squinting face" }, + { emoji: "๐Ÿ˜‰", name: "winking face" }, + { emoji: "๐Ÿ˜Š", name: "smiling face with smiling eyes" }, + { emoji: "๐Ÿ˜‹", name: "face savoring food" }, + { emoji: "๐Ÿ˜Ž", name: "smiling face with sunglasses" }, + { emoji: "๐Ÿ˜", name: "smiling face with heart-eyes" }, + { emoji: "๐Ÿ˜˜", name: "face blowing a kiss" }, + { emoji: "๐Ÿ˜—", name: "kissing face" }, + { emoji: "๐Ÿ˜™", name: "kissing face with smiling eyes" }, + { emoji: "๐Ÿ˜š", name: "kissing face with closed eyes" }, + { emoji: "๐Ÿ™‚", name: "slightly smiling face" }, + { emoji: "๐Ÿค—", name: "hugging face" }, + { emoji: "๐Ÿค”", name: "thinking face" }, + { emoji: "๐Ÿ˜", name: "neutral face" }, + { emoji: "๐Ÿ˜‘", name: "expressionless face" }, + { emoji: "๐Ÿ˜ถ", name: "face without mouth" }, + { emoji: "๐Ÿ™„", name: "face with rolling eyes" }, + { emoji: "๐Ÿ˜", name: "smirking face" }, + { emoji: "๐Ÿ˜ฃ", name: "persevering face" }, + { emoji: "๐Ÿ˜ฅ", name: "sad but relieved face" }, + { emoji: "๐Ÿ˜ฎ", name: "face with open mouth" }, + { emoji: "๐Ÿค", name: "zipper-mouth face" }, + { emoji: "๐Ÿ˜ฏ", name: "hushed face" }, + { emoji: "๐Ÿ˜ช", name: "sleepy face" }, + { emoji: "๐Ÿ˜ซ", name: "tired face" }, + { emoji: "๐Ÿฅฑ", name: "yawning face" }, + { emoji: "๐Ÿ˜ด", name: "sleeping face" }, + { emoji: "๐Ÿ˜Œ", name: "relieved face" }, + { emoji: "๐Ÿ˜›", name: "face with tongue" }, + { emoji: "๐Ÿ˜œ", name: "winking face with tongue" }, + { emoji: "๐Ÿ˜", name: "squinting face with tongue" }, + { emoji: "๐Ÿคค", name: "drooling face" }, + { emoji: "๐Ÿ˜’", name: "unamused face" }, + { emoji: "๐Ÿ˜“", name: "downcast face with sweat" }, + { emoji: "๐Ÿ˜”", name: "pensive face" }, + { emoji: "๐Ÿ˜•", name: "confused face" }, + { emoji: "๐Ÿ™ƒ", name: "upside-down face" }, + { emoji: "๐Ÿซ ", name: "melting face" }, + { emoji: "๐Ÿ˜ฒ", name: "astonished face" }, + { emoji: "โ˜น๏ธ", name: "frowning face" }, + { emoji: "๐Ÿ™", name: "slightly frowning face" }, + { emoji: "๐Ÿ˜–", name: "confounded face" }, + { emoji: "๐Ÿ˜ž", name: "disappointed face" }, + { emoji: "๐Ÿ˜Ÿ", name: "worried face" }, + { emoji: "๐Ÿ˜ค", name: "face with steam from nose" }, + { emoji: "๐Ÿ˜ข", name: "crying face" }, + { emoji: "๐Ÿ˜ญ", name: "loudly crying face" }, + { emoji: "๐Ÿ˜ฆ", name: "frowning face with open mouth" }, + { emoji: "๐Ÿ˜ง", name: "anguished face" }, + { emoji: "๐Ÿ˜จ", name: "fearful face" }, + { emoji: "๐Ÿ˜ฉ", name: "weary face" }, + { emoji: "๐Ÿคฏ", name: "exploding head" }, + { emoji: "๐Ÿ˜ฌ", name: "grimacing face" }, + { emoji: "๐Ÿ˜ฐ", name: "anxious face with sweat" }, + { emoji: "๐Ÿ˜ฑ", name: "face screaming in fear" }, + { emoji: "๐Ÿฅต", name: "hot face" }, + { emoji: "๐Ÿฅถ", name: "cold face" }, + { emoji: "๐Ÿ˜ณ", name: "flushed face" }, + { emoji: "๐Ÿคช", name: "zany face" }, + { emoji: "๐Ÿ˜ต", name: "dizzy face" }, + { emoji: "๐Ÿฅด", name: "woozy face" }, + { emoji: "๐Ÿ˜ ", name: "angry face" }, + { emoji: "๐Ÿ˜ก", name: "pouting face" }, + { emoji: "๐Ÿคฌ", name: "face with symbols on mouth" }, + { emoji: "๐Ÿคฎ", name: "face vomiting" }, + { emoji: "๐Ÿคข", name: "nauseated face" }, + { emoji: "๐Ÿ˜ท", name: "face with medical mask" }, + + // Gestures & Hand Signs + { emoji: "๐Ÿ‘‹", name: "waving hand" }, + { emoji: "๐Ÿคš", name: "raised back of hand" }, + { emoji: "๐Ÿ–๏ธ", name: "hand with fingers splayed" }, + { emoji: "โœ‹", name: "raised hand" }, + { emoji: "๐Ÿ‘Œ", name: "OK hand" }, + { emoji: "โœŒ๏ธ", name: "victory hand" }, + { emoji: "๐Ÿคž", name: "crossed fingers" }, + { emoji: "๐ŸคŸ", name: "love-you gesture" }, + { emoji: "๐Ÿค˜", name: "sign of the horns" }, + { emoji: "๐Ÿค™", name: "call me hand" }, + { emoji: "๐Ÿ‘ˆ", name: "backhand index pointing left" }, + { emoji: "๐Ÿ‘‰", name: "backhand index pointing right" }, + { emoji: "๐Ÿ‘†", name: "backhand index pointing up" }, + { emoji: "๐Ÿ‘‡", name: "backhand index pointing down" }, + { emoji: "๐Ÿ‘", name: "thumbs up" }, + { emoji: "๐Ÿ‘Ž", name: "thumbs down" }, + { emoji: "๐Ÿ‘", name: "clapping hands" }, + { emoji: "๐Ÿ™Œ", name: "raising hands" }, + { emoji: "๐Ÿ‘", name: "open hands" }, + { emoji: "๐Ÿ™", name: "folded hands" }, + + // Animals & Nature + { emoji: "๐Ÿถ", name: "dog face" }, + { emoji: "๐Ÿฑ", name: "cat face" }, + { emoji: "๐Ÿญ", name: "mouse face" }, + { emoji: "๐Ÿน", name: "hamster face" }, + { emoji: "๐Ÿฐ", name: "rabbit face" }, + { emoji: "๐ŸฆŠ", name: "fox face" }, + { emoji: "๐Ÿป", name: "bear face" }, + { emoji: "๐Ÿผ", name: "panda face" }, + { emoji: "๐Ÿจ", name: "koala" }, + { emoji: "๐Ÿฏ", name: "tiger face" }, + { emoji: "๐Ÿฆ", name: "lion" }, + { emoji: "๐Ÿฎ", name: "cow face" }, + { emoji: "๐Ÿท", name: "pig face" }, + { emoji: "๐Ÿธ", name: "frog face" }, + { emoji: "๐Ÿต", name: "monkey face" }, + { emoji: "๐Ÿฆ„", name: "unicorn face" }, + { emoji: "๐Ÿข", name: "turtle" }, + { emoji: "๐Ÿ", name: "snake" }, + { emoji: "๐Ÿฆ‹", name: "butterfly" }, + { emoji: "๐Ÿ", name: "honeybee" }, + { emoji: "๐Ÿž", name: "lady beetle" }, + { emoji: "๐Ÿฆ€", name: "crab" }, + { emoji: "๐Ÿ ", name: "tropical fish" }, + { emoji: "๐ŸŸ", name: "fish" }, + { emoji: "๐Ÿฌ", name: "dolphin" }, + { emoji: "๐Ÿณ", name: "spouting whale" }, + { emoji: "๐Ÿ‹", name: "whale" }, + { emoji: "๐Ÿฆˆ", name: "shark" }, + + // Food & Drink + { emoji: "๐Ÿ", name: "green apple" }, + { emoji: "๐ŸŽ", name: "red apple" }, + { emoji: "๐Ÿ", name: "pear" }, + { emoji: "๐ŸŠ", name: "tangerine" }, + { emoji: "๐Ÿ‹", name: "lemon" }, + { emoji: "๐ŸŒ", name: "banana" }, + { emoji: "๐Ÿ‰", name: "watermelon" }, + { emoji: "๐Ÿ‡", name: "grapes" }, + { emoji: "๐Ÿ“", name: "strawberry" }, + { emoji: "๐Ÿซ", name: "blueberries" }, + { emoji: "๐Ÿˆ", name: "melon" }, + { emoji: "๐Ÿ’", name: "cherries" }, + { emoji: "๐Ÿ‘", name: "peach" }, + { emoji: "๐Ÿฅญ", name: "mango" }, + { emoji: "๐Ÿ", name: "pineapple" }, + { emoji: "๐Ÿฅฅ", name: "coconut" }, + { emoji: "๐Ÿฅ‘", name: "avocado" }, + { emoji: "๐Ÿฅฆ", name: "broccoli" }, + { emoji: "๐Ÿฅ•", name: "carrot" }, + { emoji: "๐ŸŒฝ", name: "corn" }, + { emoji: "๐ŸŒถ๏ธ", name: "hot pepper" }, + { emoji: "๐Ÿ”", name: "hamburger" }, + { emoji: "๐ŸŸ", name: "french fries" }, + { emoji: "๐Ÿ•", name: "pizza" }, + { emoji: "๐ŸŒญ", name: "hot dog" }, + { emoji: "๐Ÿฅช", name: "sandwich" }, + { emoji: "๐Ÿฟ", name: "popcorn" }, + { emoji: "๐Ÿฅ“", name: "bacon" }, + { emoji: "๐Ÿฅš", name: "egg" }, + { emoji: "๐Ÿฐ", name: "cake" }, + { emoji: "๐ŸŽ‚", name: "birthday cake" }, + { emoji: "๐Ÿฆ", name: "ice cream" }, + { emoji: "๐Ÿฉ", name: "doughnut" }, + { emoji: "๐Ÿช", name: "cookie" }, + { emoji: "๐Ÿซ", name: "chocolate bar" }, + { emoji: "๐Ÿฌ", name: "candy" }, + { emoji: "๐Ÿญ", name: "lollipop" }, + + // Activities + { emoji: "โšฝ", name: "soccer ball" }, + { emoji: "๐Ÿ€", name: "basketball" }, + { emoji: "๐Ÿˆ", name: "american football" }, + { emoji: "โšพ", name: "baseball" }, + { emoji: "๐ŸฅŽ", name: "softball" }, + { emoji: "๐ŸŽพ", name: "tennis" }, + { emoji: "๐Ÿ", name: "volleyball" }, + { emoji: "๐ŸŽณ", name: "bowling" }, + { emoji: "โ›ณ", name: "flag in hole" }, + { emoji: "๐Ÿšด", name: "person biking" }, + { emoji: "๐ŸŽฎ", name: "video game" }, + { emoji: "๐ŸŽฒ", name: "game die" }, + { emoji: "๐ŸŽธ", name: "guitar" }, + { emoji: "๐ŸŽบ", name: "trumpet" }, + + // Miscellaneous + { emoji: "๐Ÿš€", name: "rocket" }, + { emoji: "๐Ÿ’–", name: "sparkling heart" }, + { emoji: "๐ŸŽ‰", name: "party popper" }, + { emoji: "๐Ÿ”ฅ", name: "fire" }, + { emoji: "๐ŸŽ", name: "gift" }, + { emoji: "โค๏ธ", name: "red heart" }, + { emoji: "๐Ÿงก", name: "orange heart" }, + { emoji: "๐Ÿ’›", name: "yellow heart" }, + { emoji: "๐Ÿ’š", name: "green heart" }, + { emoji: "๐Ÿ’™", name: "blue heart" }, + { emoji: "๐Ÿ’œ", name: "purple heart" }, + { emoji: "๐Ÿค", name: "white heart" }, + { emoji: "๐ŸคŽ", name: "brown heart" }, + { emoji: "๐Ÿ’”", name: "broken heart" }, +]; + +const EmojiPalette = memo(({ scopeRef, className }: EmojiPaletteProps) => { + const anchorRef = useRef(null); + const [isPaletteVisible, setIsPaletteVisible] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (anchorRef.current && !anchorRef.current.contains(event.target as Node)) { + setIsPaletteVisible(false); + } + }; + + scopeRef?.current?.addEventListener("mousedown", handleClickOutside); + return () => { + scopeRef?.current?.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const handleAnchorClick = () => { + setIsPaletteVisible((prev) => !prev); + }; + + const handleSearchChange = (val: string) => { + setSearchTerm(val.toLowerCase()); + }; + + const filteredEmojis = emojiList.filter((item) => item.name.includes(searchTerm)); + + return ( +
+ + {isPaletteVisible && ( + + +
+ {filteredEmojis.length > 0 ? ( + filteredEmojis.map((item, index) => ( + + )) + ) : ( +
No emojis found
+ )} +
+
+ )} +
+ ); +}); + +EmojiPalette.displayName = "EmojiPalette"; + +export { EmojiPalette }; diff --git a/frontend/app/element/palette.less b/frontend/app/element/palette.less index e5e7d5ed6..9ecffbe4c 100644 --- a/frontend/app/element/palette.less +++ b/frontend/app/element/palette.less @@ -13,4 +13,5 @@ border: 1px solid rgba(255, 255, 255, 0.15); background: #212121; box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3); + visibility: hidden; } diff --git a/frontend/app/element/palette.stories.tsx b/frontend/app/element/palette.stories.tsx index 1ff3021c1..46a98fe79 100644 --- a/frontend/app/element/palette.stories.tsx +++ b/frontend/app/element/palette.stories.tsx @@ -33,13 +33,13 @@ export const DefaultPalette: Story = { setIsMenuVisible((prev) => !prev); }; - const handleClickOutside = (event: MouseEvent) => { - if (anchorRef.current && !anchorRef.current.contains(event.target as Node)) { - setIsMenuVisible(false); - } - }; - useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (anchorRef.current && !anchorRef.current.contains(event.target as Node)) { + setIsMenuVisible(false); + } + }; + scopeRef?.current?.addEventListener("mousedown", handleClickOutside); return () => { scopeRef?.current?.removeEventListener("mousedown", handleClickOutside); diff --git a/frontend/app/element/palette.tsx b/frontend/app/element/palette.tsx index 07ef93a87..c4f89245e 100644 --- a/frontend/app/element/palette.tsx +++ b/frontend/app/element/palette.tsx @@ -32,7 +32,7 @@ const Palette = memo(({ children, className, anchorRef, scopeRef }: PaletteProps // Check if the palette goes beyond the right edge of the window const rightEdge = left + paletteEl.offsetWidth; if (rightEdge > window.innerWidth) { - left = window.innerWidth - paletteEl.offsetWidth - 15; + left = window.innerWidth - paletteEl.offsetWidth - 10; } // Check if the palette goes beyond the bottom edge of the window @@ -44,12 +44,14 @@ const Palette = memo(({ children, className, anchorRef, scopeRef }: PaletteProps } }, [anchorRef, scopeRef, width, height]); + useEffect(() => { + if (position.top > 0 && paletteRef.current?.style.visibility !== "visible") { + paletteRef.current.style.visibility = "visible"; + } + }, [position.top]); + return createPortal( -
+
{children}
, document.body