diff --git a/apps/space/core/components/issues/issue-layouts/kanban/block-reactions.tsx b/apps/space/core/components/issues/issue-layouts/kanban/block-reactions.tsx index 897693228bd..504b67dffd6 100644 --- a/apps/space/core/components/issues/issue-layouts/kanban/block-reactions.tsx +++ b/apps/space/core/components/issues/issue-layouts/kanban/block-reactions.tsx @@ -37,7 +37,7 @@ export const BlockReactions = observer((props: Props) => { )} {canReact && (
- +
)} diff --git a/apps/space/core/components/issues/peek-overview/comment/comment-reactions.tsx b/apps/space/core/components/issues/peek-overview/comment/comment-reactions.tsx index 165441108b8..2636554efcf 100644 --- a/apps/space/core/components/issues/peek-overview/comment/comment-reactions.tsx +++ b/apps/space/core/components/issues/peek-overview/comment/comment-reactions.tsx @@ -1,15 +1,14 @@ "use client"; -import React from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { Tooltip } from "@plane/propel/tooltip"; // plane imports -import { cn } from "@plane/utils"; -// ui -import { ReactionSelector } from "@/components/ui"; +import { stringToEmoji } from "@plane/propel/emoji-icon-picker"; +import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; +import type { EmojiReactionType } from "@plane/propel/emoji-reaction"; // helpers -import { groupReactions, renderEmoji } from "@/helpers/emoji.helper"; +import { groupReactions } from "@/helpers/emoji.helper"; import { queryParamGenerator } from "@/helpers/query-param-generator"; // hooks import { useIssueDetails } from "@/hooks/store/use-issue-details"; @@ -23,6 +22,8 @@ type Props = { export const CommentReactions: React.FC = observer((props) => { const { anchor, commentId } = props; + // state + const [isPickerOpen, setIsPickerOpen] = useState(false); const router = useRouter(); const pathName = usePathname(); const searchParams = useSearchParams(); @@ -37,8 +38,18 @@ export const CommentReactions: React.FC = observer((props) => { const { data: user } = useUser(); const isInIframe = useIsInIframe(); - const commentReactions = peekId ? details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions : []; - const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {}; + const commentReactions = useMemo(() => { + if (!peekId) return []; + const peekDetails = details[peekId]; + if (!peekDetails) return []; + const comment = peekDetails.comments?.find((c) => c.id === commentId); + return comment?.comment_reactions ?? []; + }, [peekId, details, commentId]); + + const groupedReactions = useMemo(() => { + if (!peekId) return {}; + return groupReactions(commentReactions ?? [], "reaction"); + }, [peekId, commentReactions]); const userReactions = commentReactions?.filter((r) => r?.actor_detail?.id === user?.id); @@ -62,70 +73,68 @@ export const CommentReactions: React.FC = observer((props) => { // derived values const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels }); + // Transform reactions data to Propel EmojiReactionType format + const propelReactions: EmojiReactionType[] = useMemo(() => { + const REACTIONS_LIMIT = 1000; + + return Object.keys(groupedReactions || {}) + .filter((reaction) => groupedReactions?.[reaction]?.length > 0) + .map((reaction) => { + const reactionList = groupedReactions?.[reaction] ?? []; + const userNames = reactionList + .map((r) => r?.actor_detail?.display_name) + .filter((name): name is string => !!name) + .slice(0, REACTIONS_LIMIT); + + return { + emoji: stringToEmoji(reaction), + count: reactionList.length, + reacted: commentReactions?.some((r) => r?.actor_detail?.id === user?.id && r.reaction === reaction) || false, + users: userNames, + }; + }); + }, [groupedReactions, commentReactions, user?.id]); + + const handleEmojiClick = (emoji: string) => { + if (isInIframe) return; + if (!user) { + router.push(`/?next_path=${pathName}?${queryParam}`); + return; + } + // Convert emoji back to decimal string format for the API + const emojiCodePoints = Array.from(emoji) + .map((char) => char.codePointAt(0)) + .filter((cp): cp is number => cp !== undefined); + const reactionString = emojiCodePoints.join("-"); + handleReactionClick(reactionString); + }; + + const handleEmojiSelect = (emoji: string) => { + if (!user) { + router.push(`/?next_path=${pathName}?${queryParam}`); + return; + } + // emoji is already in decimal string format from EmojiReactionPicker + handleReactionClick(emoji); + }; + return ( -
- {!isInIframe && ( - { - if (user) handleReactionClick(value); - else router.push(`/?next_path=${pathName}?${queryParam}`); - }} - position="top" - selected={userReactions?.map((r) => r.reaction)} - size="md" - /> - )} - - {Object.keys(groupedReactions || {}).map((reaction) => { - const reactions = groupedReactions?.[reaction] ?? []; - const REACTIONS_LIMIT = 1000; - - if (reactions.length > 0) - return ( - - {reactions - .map((r) => r?.actor_detail?.display_name) - .splice(0, REACTIONS_LIMIT) - .join(", ")} - {reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"} -
- } - > - - - ); - })} +
+ setIsPickerOpen(true)} + /> + } + placement="bottom-start" + />
); }); diff --git a/apps/space/core/components/issues/reactions/issue-emoji-reactions.tsx b/apps/space/core/components/issues/reactions/issue-emoji-reactions.tsx index ecf01210ca1..0a3c193d9ff 100644 --- a/apps/space/core/components/issues/reactions/issue-emoji-reactions.tsx +++ b/apps/space/core/components/issues/reactions/issue-emoji-reactions.tsx @@ -1,12 +1,14 @@ "use client"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; // lib -import { Tooltip } from "@plane/propel/tooltip"; -import { ReactionSelector } from "@/components/ui"; +import { stringToEmoji } from "@plane/propel/emoji-icon-picker"; +import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; +import type { EmojiReactionType } from "@plane/propel/emoji-reaction"; // helpers -import { groupReactions, renderEmoji } from "@/helpers/emoji.helper"; +import { groupReactions } from "@/helpers/emoji.helper"; import { queryParamGenerator } from "@/helpers/query-param-generator"; // hooks import { useIssueDetails } from "@/hooks/store/use-issue-details"; @@ -15,11 +17,12 @@ import { useUser } from "@/hooks/store/use-user"; type IssueEmojiReactionsProps = { anchor: string; issueIdFromProps?: string; - size?: "md" | "sm"; }; export const IssueEmojiReactions: React.FC = observer((props) => { - const { anchor, issueIdFromProps, size = "md" } = props; + const { anchor, issueIdFromProps } = props; + // state + const [isPickerOpen, setIsPickerOpen] = useState(false); // router const router = useRouter(); const pathName = usePathname(); @@ -58,62 +61,63 @@ export const IssueEmojiReactions: React.FC = observer( // derived values const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels }); - const reactionDimensions = size === "sm" ? "h-6 px-2 py-1" : "h-full px-2 py-1"; - return ( - <> - { - if (user) handleReactionClick(value); - else router.push(`/?next_path=${pathName}?${queryParam}`); - }} - selected={userReactions?.map((r) => r.reaction)} - size={size} - /> - {Object.keys(groupedReactions || {}).map((reaction) => { - const reactions = groupedReactions?.[reaction] ?? []; - const REACTIONS_LIMIT = 1000; + // Transform reactions data to Propel EmojiReactionType format + const propelReactions: EmojiReactionType[] = useMemo(() => { + const REACTIONS_LIMIT = 1000; + + return Object.keys(groupedReactions || {}) + .filter((reaction) => groupedReactions?.[reaction]?.length > 0) + .map((reaction) => { + const reactionList = groupedReactions?.[reaction] ?? []; + const userNames = reactionList + .map((r) => r?.actor_details?.display_name) + .filter((name): name is string => !!name) + .slice(0, REACTIONS_LIMIT); + + return { + emoji: stringToEmoji(reaction), + count: reactionList.length, + reacted: reactionList.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction), + users: userNames, + }; + }); + }, [groupedReactions, user?.id]); + + const handleEmojiClick = (emoji: string) => { + if (!user) { + router.push(`/?next_path=${pathName}?${queryParam}`); + return; + } + // Convert emoji back to decimal string format for the API + const emojiCodePoints = Array.from(emoji).map((char) => char.codePointAt(0)); + const reactionString = emojiCodePoints.join("-"); + handleReactionClick(reactionString); + }; + + const handleEmojiSelect = (emoji: string) => { + if (!user) { + router.push(`/?next_path=${pathName}?${queryParam}`); + return; + } + // emoji is already in decimal string format from EmojiReactionPicker + handleReactionClick(emoji); + }; - if (reactions.length > 0) - return ( - - {reactions - ?.map((r) => r?.actor_details?.display_name) - ?.splice(0, REACTIONS_LIMIT) - ?.join(", ")} - {reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"} - - } - > - - - ); - })} - + return ( + setIsPickerOpen(true)} + /> + } + placement="bottom-start" + /> ); }); diff --git a/apps/space/core/components/ui/index.ts b/apps/space/core/components/ui/index.ts index ccd2303c4a8..b975409af45 100644 --- a/apps/space/core/components/ui/index.ts +++ b/apps/space/core/components/ui/index.ts @@ -1,2 +1 @@ export * from "./icon"; -export * from "./reaction-selector"; diff --git a/apps/space/core/components/ui/reaction-selector.tsx b/apps/space/core/components/ui/reaction-selector.tsx deleted file mode 100644 index 9b999a618ee..00000000000 --- a/apps/space/core/components/ui/reaction-selector.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Fragment } from "react"; - -// headless ui -import { Popover, Transition } from "@headlessui/react"; - -// helper -import { Icon } from "@/components/ui"; -import { renderEmoji } from "@/helpers/emoji.helper"; - -// icons - -const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"]; - -interface Props { - onSelect: (emoji: string) => void; - position?: "top" | "bottom"; - selected?: string[]; - size?: "sm" | "md" | "lg"; -} - -export const ReactionSelector: React.FC = (props) => { - const { onSelect, position, selected = [], size } = props; - - return ( - - {({ open, close: closePopover }) => ( - <> - - - - - - - -
-
- {reactionEmojis.map((emoji) => ( - - ))} -
-
-
-
- - )} -
- ); -}; diff --git a/apps/web/core/components/comments/comment-reaction.tsx b/apps/web/core/components/comments/comment-reaction.tsx index 8e3e04b94b1..181f7a072a8 100644 --- a/apps/web/core/components/comments/comment-reaction.tsx +++ b/apps/web/core/components/comments/comment-reaction.tsx @@ -1,15 +1,16 @@ "use client"; import type { FC } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react"; // plane imports -import { Tooltip } from "@plane/propel/tooltip"; +import { stringToEmoji } from "@plane/propel/emoji-icon-picker"; +import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; +import type { EmojiReactionType } from "@plane/propel/emoji-reaction"; import type { TCommentsOperations, TIssueComment } from "@plane/types"; import { cn } from "@plane/utils"; // helpers -import { renderEmoji } from "@/helpers/emoji.helper"; // local imports -import { ReactionSelector } from "../issues/issue-detail/reactions"; export type TProps = { comment: TIssueComment; @@ -19,49 +20,66 @@ export type TProps = { export const CommentReactions: FC = observer((props) => { const { comment, activityOperations, disabled = false } = props; + // state + const [isPickerOpen, setIsPickerOpen] = useState(false); const userReactions = activityOperations.userReactions(comment.id); const reactionIds = activityOperations.reactionIds(comment.id); + // Transform reactions data to Propel EmojiReactionType format + const reactions: EmojiReactionType[] = useMemo(() => { + if (!reactionIds) return []; + + return Object.keys(reactionIds) + .filter((reaction) => reactionIds[reaction]?.length > 0) + .map((reaction) => { + // Get user names for this reaction + const tooltipContent = activityOperations.getReactionUsers(reaction, reactionIds); + // Parse the tooltip content string to extract user names + const users = tooltipContent ? tooltipContent.split(", ") : []; + + return { + emoji: stringToEmoji(reaction), + count: reactionIds[reaction].length, + reacted: userReactions?.includes(reaction) || false, + users: users, + }; + }); + }, [reactionIds, userReactions, activityOperations]); + + const handleReactionClick = (emoji: string) => { + if (disabled || !userReactions) return; + // Convert emoji back to decimal string format for the API + const emojiCodePoints = Array.from(emoji).map((char) => char.codePointAt(0)); + const reactionString = emojiCodePoints.join("-"); + activityOperations.react(comment.id, reactionString, userReactions); + }; + + const handleEmojiSelect = (emoji: string) => { + if (!userReactions) return; + // emoji is already in decimal string format from EmojiReactionPicker + activityOperations.react(comment.id, emoji, userReactions); + }; + if (!userReactions) return null; - return ( -
- {!disabled && ( - activityOperations.react(comment.id, reactionEmoji, userReactions)} - /> - )} - {reactionIds && - Object.keys(reactionIds || {}).map( - (reaction: string) => - reactionIds[reaction]?.length > 0 && ( - <> - - - - - ) - )} + return ( +
+ setIsPickerOpen(true)} + /> + } + placement="bottom-start" + />
); }); diff --git a/apps/web/core/components/cycles/list/cycle-list-project-group-header.tsx b/apps/web/core/components/cycles/list/cycle-list-project-group-header.tsx index 22a638a41a2..eddfee58203 100644 --- a/apps/web/core/components/cycles/list/cycle-list-project-group-header.tsx +++ b/apps/web/core/components/cycles/list/cycle-list-project-group-header.tsx @@ -3,8 +3,8 @@ import type { FC } from "react"; import React from "react"; import { observer } from "mobx-react"; -import { ChevronRightIcon } from "@plane/propel/icons"; import { Logo } from "@plane/propel/emoji-icon-picker"; +import { ChevronRightIcon } from "@plane/propel/icons"; // icons import { Row } from "@plane/ui"; // helpers diff --git a/apps/web/core/components/issues/issue-detail/reactions/index.ts b/apps/web/core/components/issues/issue-detail/reactions/index.ts index 8dc6f05bd64..9ab00bd7735 100644 --- a/apps/web/core/components/issues/issue-detail/reactions/index.ts +++ b/apps/web/core/components/issues/issue-detail/reactions/index.ts @@ -1,4 +1,2 @@ -export * from "./reaction-selector"; - export * from "./issue"; -// export * from "./issue-comment"; +export * from "./issue-comment"; diff --git a/apps/web/core/components/issues/issue-detail/reactions/issue-comment.tsx b/apps/web/core/components/issues/issue-detail/reactions/issue-comment.tsx index 4077bcc0a5d..7e7b6d870fb 100644 --- a/apps/web/core/components/issues/issue-detail/reactions/issue-comment.tsx +++ b/apps/web/core/components/issues/issue-detail/reactions/issue-comment.tsx @@ -1,20 +1,20 @@ "use client"; import type { FC } from "react"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import { stringToEmoji } from "@plane/propel/emoji-icon-picker"; +import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; +import type { EmojiReactionType } from "@plane/propel/emoji-reaction"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { Tooltip } from "@plane/propel/tooltip"; import type { IUser } from "@plane/types"; // components import { cn, formatTextList } from "@plane/utils"; // helper -import { renderEmoji } from "@/helpers/emoji.helper"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useMember } from "@/hooks/store/use-member"; // types -import { ReactionSelector } from "./reaction-selector"; export type TIssueCommentReaction = { workspaceSlug: string; @@ -26,7 +26,8 @@ export type TIssueCommentReaction = { export const IssueCommentReaction: FC = observer((props) => { const { workspaceSlug, projectId, commentId, currentUser, disabled = false } = props; - + // state + const [isPickerOpen, setIsPickerOpen] = useState(false); // hooks const { commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser, getCommentReactionById }, @@ -82,7 +83,7 @@ export const IssueCommentReaction: FC = observer((props) [workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions] ); - const getReactionUsers = (reaction: string): string => { + const getReactionUsers = (reaction: string): string[] => { const reactionUsers = (reactionIds?.[reaction] || []) .map((reactionId) => { const reactionDetails = getCommentReactionById(reactionId); @@ -91,48 +92,53 @@ export const IssueCommentReaction: FC = observer((props) : null; }) .filter((displayName): displayName is string => !!displayName); - const formattedUsers = formatTextList(reactionUsers); - return formattedUsers; + return reactionUsers; }; - return ( -
- {!disabled && ( - - )} + // Transform reactions data to Propel EmojiReactionType format + const reactions: EmojiReactionType[] = useMemo(() => { + if (!reactionIds) return []; + + return Object.keys(reactionIds) + .filter((reaction) => reactionIds[reaction]?.length > 0) + .map((reaction) => ({ + emoji: stringToEmoji(reaction), + count: reactionIds[reaction].length, + reacted: userReactions.includes(reaction), + users: getReactionUsers(reaction), + })); + }, [reactionIds, userReactions]); + + const handleReactionClick = (emoji: string) => { + if (disabled) return; + // Convert emoji back to decimal string format for the API + const emojiCodePoints = Array.from(emoji).map((char) => char.codePointAt(0)); + const reactionString = emojiCodePoints.join("-"); + issueCommentReactionOperations.react(reactionString); + }; - {reactionIds && - Object.keys(reactionIds || {}).map( - (reaction) => - reactionIds[reaction]?.length > 0 && ( - <> - - - - - ) - )} + const handleEmojiSelect = (emoji: string) => { + // emoji is already in decimal string format from EmojiReactionPicker + issueCommentReactionOperations.react(emoji); + }; + + return ( +
+ setIsPickerOpen(true)} + /> + } + placement="bottom-start" + />
); }); diff --git a/apps/web/core/components/issues/issue-detail/reactions/issue.tsx b/apps/web/core/components/issues/issue-detail/reactions/issue.tsx index 56b70ba53ce..804e816c251 100644 --- a/apps/web/core/components/issues/issue-detail/reactions/issue.tsx +++ b/apps/web/core/components/issues/issue-detail/reactions/issue.tsx @@ -1,20 +1,20 @@ "use client"; import type { FC } from "react"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import { stringToEmoji } from "@plane/propel/emoji-icon-picker"; +import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; +import type { EmojiReactionType } from "@plane/propel/emoji-reaction"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { Tooltip } from "@plane/propel/tooltip"; import type { IUser } from "@plane/types"; // hooks // ui import { cn, formatTextList } from "@plane/utils"; // helpers -import { renderEmoji } from "@/helpers/emoji.helper"; import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useMember } from "@/hooks/store/use-member"; // types -import { ReactionSelector } from "./reaction-selector"; export type TIssueReaction = { workspaceSlug: string; @@ -27,6 +27,8 @@ export type TIssueReaction = { export const IssueReaction: FC = observer((props) => { const { workspaceSlug, projectId, issueId, currentUser, disabled = false, className = "" } = props; + // state + const [isPickerOpen, setIsPickerOpen] = useState(false); // hooks const { reaction: { getReactionsByIssueId, reactionsByUser, getReactionById }, @@ -82,7 +84,7 @@ export const IssueReaction: FC = observer((props) => { [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions] ); - const getReactionUsers = (reaction: string): string => { + const getReactionUsers = (reaction: string): string[] => { const reactionUsers = (reactionIds?.[reaction] || []) .map((reactionId) => { const reactionDetails = getReactionById(reactionId); @@ -92,42 +94,53 @@ export const IssueReaction: FC = observer((props) => { }) .filter((displayName): displayName is string => !!displayName); - const formattedUsers = formatTextList(reactionUsers); - return formattedUsers; + return reactionUsers; + }; + + // Transform reactions data to Propel EmojiReactionType format + const reactions: EmojiReactionType[] = useMemo(() => { + if (!reactionIds) return []; + + return Object.keys(reactionIds) + .filter((reaction) => reactionIds[reaction]?.length > 0) + .map((reaction) => ({ + emoji: stringToEmoji(reaction), + count: reactionIds[reaction].length, + reacted: userReactions.includes(reaction), + users: getReactionUsers(reaction), + })); + }, [reactionIds, userReactions]); + + const handleReactionClick = (emoji: string) => { + if (disabled) return; + // Convert emoji back to decimal string format for the API + const emojiCodePoints = Array.from(emoji).map((char) => char.codePointAt(0)); + const reactionString = emojiCodePoints.join("-"); + issueReactionOperations.react(reactionString); + }; + + const handleEmojiSelect = (emoji: string) => { + // emoji is already in decimal string format from EmojiReactionPicker + issueReactionOperations.react(emoji); }; return ( -
- {!disabled && ( - - )} - {reactionIds && - Object.keys(reactionIds || {}).map( - (reaction) => - reactionIds[reaction]?.length > 0 && ( - <> - - - - - ) - )} +
+ setIsPickerOpen(true)} + /> + } + placement="bottom-start" + />
); }); diff --git a/apps/web/core/components/issues/issue-detail/reactions/reaction-selector.tsx b/apps/web/core/components/issues/issue-detail/reactions/reaction-selector.tsx deleted file mode 100644 index e3d58b50482..00000000000 --- a/apps/web/core/components/issues/issue-detail/reactions/reaction-selector.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Fragment } from "react"; -import { SmilePlus } from "lucide-react"; -import { Popover, Transition } from "@headlessui/react"; -// helper -import { renderEmoji } from "@/helpers/emoji.helper"; -// icons - -const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"]; - -interface Props { - size?: "sm" | "md" | "lg"; - position?: "top" | "bottom"; - value?: string | string[] | null; - onSelect: (emoji: string) => void; -} - -export const ReactionSelector: React.FC = (props) => { - const { onSelect, position, size } = props; - - return ( - - {({ open, close: closePopover }) => ( - <> - - - - - - - -
-
- {reactionEmojis.map((emoji) => ( - - ))} -
-
-
-
- - )} -
- ); -}; diff --git a/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx b/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx index 1c49a8542e7..fc96163094c 100644 --- a/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -1,7 +1,6 @@ import { observer } from "mobx-react"; -import { CloseIcon } from "@plane/propel/icons"; -// components import { Logo } from "@plane/propel/emoji-icon-picker"; +import { CloseIcon } from "@plane/propel/icons"; // hooks import { useProject } from "@/hooks/store/use-project"; diff --git a/packages/propel/src/animated-counter/animated-counter.tsx b/packages/propel/src/animated-counter/animated-counter.tsx index 09432358a2c..2c377fabce2 100644 --- a/packages/propel/src/animated-counter/animated-counter.tsx +++ b/packages/propel/src/animated-counter/animated-counter.tsx @@ -8,9 +8,9 @@ export interface AnimatedCounterProps { } const sizeClasses = { - sm: "text-xs h-4 w-4", - md: "text-sm h-5 w-5", - lg: "text-base h-6 w-6", + sm: "text-xs", + md: "text-sm", + lg: "text-base", }; export const AnimatedCounter: React.FC = ({ count, className, size = "md" }) => { @@ -44,7 +44,7 @@ export const AnimatedCounter: React.FC = ({ count, classNa const sizeClass = sizeClasses[size]; return ( -
+
{/* Previous number sliding out */} {isAnimating && ( = (props) => { side={finalSide} align={finalAlign} sideOffset={8} + data-prevent-outside-click="true" > diff --git a/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx b/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx index f05c1d52043..c2456b1949f 100644 --- a/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx +++ b/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx @@ -18,6 +18,12 @@ export default meta; type Story = StoryObj; export const Default: Story = { + args: { + isOpen: false, + handleToggle: () => {}, + onChange: () => {}, + label: "Pick Emoji", + }, render() { const [isOpen, setIsOpen] = useState(false); const [selectedEmoji, setSelectedEmoji] = useState(null); @@ -46,6 +52,12 @@ export const Default: Story = { }; export const WithCustomLabel: Story = { + args: { + isOpen: false, + handleToggle: () => {}, + onChange: () => {}, + label: "Add Reaction", + }, render() { const [isOpen, setIsOpen] = useState(false); const [selectedEmoji, setSelectedEmoji] = useState(null); @@ -71,6 +83,12 @@ export const WithCustomLabel: Story = { }; export const InlineReactions: Story = { + args: { + isOpen: false, + handleToggle: () => {}, + onChange: () => {}, + label: "Add", + }, render() { const [isOpen, setIsOpen] = useState(false); const [reactions, setReactions] = useState([ @@ -128,6 +146,12 @@ export const InlineReactions: Story = { }; export const DifferentPlacements: Story = { + args: { + isOpen: false, + handleToggle: () => {}, + onChange: () => {}, + label: "Placements", + }, render() { const [isOpen1, setIsOpen1] = useState(false); const [isOpen2, setIsOpen2] = useState(false); @@ -182,6 +206,12 @@ export const DifferentPlacements: Story = { }; export const SearchDisabled: Story = { + args: { + isOpen: false, + handleToggle: () => {}, + onChange: () => {}, + label: "No Search", + }, render() { const [isOpen, setIsOpen] = useState(false); const [selectedEmoji, setSelectedEmoji] = useState(null); @@ -207,6 +237,12 @@ export const SearchDisabled: Story = { }; export const CustomSearchPlaceholder: Story = { + args: { + isOpen: false, + handleToggle: () => {}, + onChange: () => {}, + label: "Custom Search", + }, render() { const [isOpen, setIsOpen] = useState(false); const [selectedEmoji, setSelectedEmoji] = useState(null); @@ -232,6 +268,12 @@ export const CustomSearchPlaceholder: Story = { }; export const CloseOnSelectDisabled: Story = { + args: { + isOpen: false, + handleToggle: () => {}, + onChange: () => {}, + label: "Select Multiple", + }, render() { const [isOpen, setIsOpen] = useState(false); const [selectedEmojis, setSelectedEmojis] = useState([]); @@ -279,6 +321,12 @@ export const CloseOnSelectDisabled: Story = { }; export const InMessageContext: Story = { + args: { + isOpen: false, + handleToggle: () => {}, + onChange: () => {}, + label: "Message", + }, render() { const [isOpen, setIsOpen] = useState(false); const [reactions, setReactions] = useState([ diff --git a/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx b/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx index 092a388ac60..a2f691f3089 100644 --- a/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx +++ b/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx @@ -70,6 +70,7 @@ export const EmojiReactionPicker: React.FC = (props) = side={finalSide} align={finalAlign} sideOffset={8} + data-prevent-outside-click="true" >
-
- Small - -
-
- Medium (default) - -
-
- Large - -
-
- ); - }, -}; - export const MultipleReactions: Story = { + args: { + emoji: "👍", + count: 0, + }, render() { const [reactions, setReactions] = useState([ { emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] }, @@ -137,6 +124,10 @@ export const MultipleReactions: Story = { }; export const AddButton: Story = { + args: { + emoji: "➕", + count: 0, + }, render() { const handleAdd = () => { alert("Add reaction clicked"); @@ -146,28 +137,11 @@ export const AddButton: Story = { }, }; -export const AddButtonSizes: Story = { - render() { - return ( -
-
- Small - -
-
- Medium - -
-
- Large - -
-
- ); - }, -}; - export const ReactionGroup: Story = { + args: { + emoji: "👍", + count: 0, + }, render() { const [reactions, setReactions] = useState([ { emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] }, @@ -205,34 +179,11 @@ export const ReactionGroup: Story = { }, }; -export const ReactionGroupSizes: Story = { - render() { - const reactions: EmojiReactionType[] = [ - { emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob"] }, - { emoji: "❤️", count: 12, reacted: true, users: ["Charlie", "David"] }, - { emoji: "🎉", count: 3, reacted: false, users: ["Emma"] }, - ]; - - return ( -
-
- Small - -
-
- Medium - -
-
- Large - -
-
- ); - }, -}; - export const InMessageContext: Story = { + args: { + emoji: "👍", + count: 0, + }, render() { const [reactions, setReactions] = useState([ { emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] }, diff --git a/packages/propel/src/emoji-reaction/emoji-reaction.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.tsx index cad773364f7..898fe894b65 100644 --- a/packages/propel/src/emoji-reaction/emoji-reaction.tsx +++ b/packages/propel/src/emoji-reaction/emoji-reaction.tsx @@ -1,7 +1,7 @@ import * as React from "react"; -import { Plus } from "lucide-react"; import { AnimatedCounter } from "../animated-counter"; import { stringToEmoji } from "../emoji-icon-picker"; +import { AddReactionIcon } from "../icons"; import { Tooltip } from "../tooltip"; import { cn } from "../utils"; @@ -20,7 +20,6 @@ export interface EmojiReactionProps extends React.ButtonHTMLAttributes void; className?: string; showCount?: boolean; - size?: "sm" | "md" | "lg"; } export interface EmojiReactionGroupProps extends React.HTMLAttributes { @@ -30,46 +29,15 @@ export interface EmojiReactionGroupProps extends React.HTMLAttributes { onAddReaction?: () => void; className?: string; - size?: "sm" | "md" | "lg"; } -const sizeClasses = { - sm: { - button: "px-2 py-1 text-xs gap-1", - emoji: "text-sm", - count: "text-xs", - addButton: "h-6 w-6", - addIcon: "h-3 w-3", - }, - md: { - button: "px-2.5 py-1.5 text-sm gap-1.5", - emoji: "text-base", - count: "text-sm", - addButton: "h-7 w-7", - addIcon: "h-3.5 w-3.5", - }, - lg: { - button: "px-3 py-2 text-base gap-2", - emoji: "text-lg", - count: "text-base", - addButton: "h-8 w-8", - addIcon: "h-4 w-4", - }, -}; - const EmojiReaction = React.forwardRef( - ( - { emoji, count, reacted = false, users = [], onReactionClick, className, showCount = true, size = "md", ...props }, - ref - ) => { - const sizeClass = sizeClasses[size]; - + ({ emoji, count, reacted = false, users = [], onReactionClick, className, showCount = true, ...props }, ref) => { const handleClick = () => { onReactionClick?.(emoji); }; @@ -96,9 +64,7 @@ const EmojiReaction = React.forwardRef( ref={ref} onClick={handleClick} className={cn( - "inline-flex items-center rounded-full border transition-all duration-200 hover:scale-105", - "focus:outline-none focus:ring-2 focus:ring-custom-primary-100/20 focus:ring-offset-1", - sizeClass.button, + "inline-flex items-center rounded-full border px-1.5 text-xs gap-0.5 transition-all duration-200", reacted ? "bg-custom-primary-100/10 border-custom-primary-100 text-custom-primary-100" : "bg-custom-background-100 border-custom-border-200 text-custom-text-300 hover:border-custom-border-300 hover:bg-custom-background-90", @@ -106,8 +72,8 @@ const EmojiReaction = React.forwardRef( )} {...props} > - {emoji} - {showCount && count > 0 && } + {emoji} + {showCount && count > 0 && } ); @@ -120,42 +86,29 @@ const EmojiReaction = React.forwardRef( ); const EmojiReactionButton = React.forwardRef( - ({ onAddReaction, className, size = "md", ...props }, ref) => { - const sizeClass = sizeClasses[size]; - - return ( - - ); - } + ({ onAddReaction, className, ...props }, ref) => ( + + ) ); const EmojiReactionGroup = React.forwardRef( ( - { - reactions, - onReactionClick, - onAddReaction, - className, - showAddButton = true, - maxDisplayUsers = 5, - size = "md", - ...props - }, + { reactions, onReactionClick, onAddReaction, className, showAddButton = true, maxDisplayUsers = 5, ...props }, ref ) => (
@@ -167,10 +120,9 @@ const EmojiReactionGroup = React.forwardRef ))} - {showAddButton && } + {showAddButton && }
) ); diff --git a/packages/propel/src/icons/actions/add-reaction-icon.tsx b/packages/propel/src/icons/actions/add-reaction-icon.tsx new file mode 100644 index 00000000000..aeee0e4fb82 --- /dev/null +++ b/packages/propel/src/icons/actions/add-reaction-icon.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const AddReactionIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + + + + + ); +}; diff --git a/packages/propel/src/icons/actions/index.ts b/packages/propel/src/icons/actions/index.ts index 0a0b7355dea..16971752fe7 100644 --- a/packages/propel/src/icons/actions/index.ts +++ b/packages/propel/src/icons/actions/index.ts @@ -1,2 +1,3 @@ export * from "./add-icon"; +export * from "./add-reaction-icon"; export * from "./close-icon"; diff --git a/packages/propel/src/icons/constants.tsx b/packages/propel/src/icons/constants.tsx index 556a028878a..cbb8299b8a0 100644 --- a/packages/propel/src/icons/constants.tsx +++ b/packages/propel/src/icons/constants.tsx @@ -1,6 +1,7 @@ import { Icon } from "./icon"; export const ActionsIconsMap = [ { icon: , title: "AddIcon" }, + { icon: , title: "AddReactionIcon" }, { icon: , title: "CloseIcon" }, ]; diff --git a/packages/propel/src/icons/registry.ts b/packages/propel/src/icons/registry.ts index db44b9ea3ef..96f6e2df662 100644 --- a/packages/propel/src/icons/registry.ts +++ b/packages/propel/src/icons/registry.ts @@ -1,3 +1,4 @@ +import { AddReactionIcon } from "./actions"; import { AddIcon } from "./actions/add-icon"; import { CloseIcon } from "./actions/close-icon"; import { ChevronDownIcon } from "./arrows/chevron-down"; @@ -112,6 +113,7 @@ export const ICON_REGISTRY = { // Action icons "action.add": AddIcon, + "action.add-reaction": AddReactionIcon, "action.close": CloseIcon, // Arrow icons