From f201c63b54e204723642eb84de7a9954619a7c0d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 09:03:12 +0530 Subject: [PATCH 01/10] dev: animated counter added to propel --- .../src/animated-counter/animated-counter.tsx | 95 +++++++++++++++++++ packages/propel/src/animated-counter/index.ts | 2 + 2 files changed, 97 insertions(+) create mode 100644 packages/propel/src/animated-counter/animated-counter.tsx create mode 100644 packages/propel/src/animated-counter/index.ts diff --git a/packages/propel/src/animated-counter/animated-counter.tsx b/packages/propel/src/animated-counter/animated-counter.tsx new file mode 100644 index 00000000000..d5a9042bca3 --- /dev/null +++ b/packages/propel/src/animated-counter/animated-counter.tsx @@ -0,0 +1,95 @@ +import { useState, useEffect } from "react"; +import { cn } from "../utils"; + +export interface AnimatedCounterProps { + count: number; + className?: string; + size?: "sm" | "md" | "lg"; +} + +const sizeClasses = { + sm: "text-xs h-4 w-4", + md: "text-sm h-5 w-5", + lg: "text-base h-6 w-6", +}; + +export const AnimatedCounter: React.FC = ({ count, className, size = "md" }) => { + // states + const [displayCount, setDisplayCount] = useState(count); + const [prevCount, setPrevCount] = useState(count); + const [isAnimating, setIsAnimating] = useState(false); + const [direction, setDirection] = useState<"up" | "down" | null>(null); + const [animationKey, setAnimationKey] = useState(0); + + useEffect(() => { + if (count !== prevCount) { + setDirection(count > prevCount ? "up" : "down"); + setIsAnimating(true); + setAnimationKey((prev) => prev + 1); + + // Update the display count immediately, animation will show the transition + setDisplayCount(count); + + // End animation after CSS transition + const timer = setTimeout(() => { + setIsAnimating(false); + setDirection(null); + setPrevCount(count); + }, 250); + + return () => clearTimeout(timer); + } + }, [count, prevCount]); + + const sizeClass = sizeClasses[size]; + + return ( +
+ {/* Previous number sliding out */} + {isAnimating && ( + + {prevCount} + + )} + + {/* New number sliding in */} + + {displayCount} + +
+ ); +}; diff --git a/packages/propel/src/animated-counter/index.ts b/packages/propel/src/animated-counter/index.ts new file mode 100644 index 00000000000..32442f627cf --- /dev/null +++ b/packages/propel/src/animated-counter/index.ts @@ -0,0 +1,2 @@ +export { AnimatedCounter } from "./animated-counter"; +export type { AnimatedCounterProps } from "./animated-counter"; \ No newline at end of file From 26c1ab604b426870bbbdd6538c2b2c8994192ba8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 09:05:25 +0530 Subject: [PATCH 02/10] chore: animated counter story added --- .../animated-counter.stories.tsx | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/propel/src/animated-counter/animated-counter.stories.tsx diff --git a/packages/propel/src/animated-counter/animated-counter.stories.tsx b/packages/propel/src/animated-counter/animated-counter.stories.tsx new file mode 100644 index 00000000000..fd93fe32595 --- /dev/null +++ b/packages/propel/src/animated-counter/animated-counter.stories.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { AnimatedCounter } from "./animated-counter"; + +const meta: Meta = { + title: "AnimatedCounter", + component: AnimatedCounter, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + size: { + control: { type: "select" }, + options: ["sm", "md", "lg"], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const AnimatedCounterDemo = (args: React.ComponentProps) => { + const [count, setCount] = useState(args.count || 0); + + return ( +
+
+ +
+ +
+ +
+
+ ); +}; + +export const Default: Story = { + render: (args) => , + args: { + count: 5, + size: "md", + }, +}; From 60b9a0ce99d681d7f3c353b17bd670b6b8b8ca4f Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 09:09:51 +0530 Subject: [PATCH 03/10] chore: propel config updated --- packages/propel/package.json | 1 + packages/propel/tsdown.config.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/propel/package.json b/packages/propel/package.json index 15933f2af5a..78d38174554 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -17,6 +17,7 @@ }, "exports": { "./accordion": "./dist/accordion/index.js", + "./animated-counter": "./dist/animated-counter/index.js", "./avatar": "./dist/avatar/index.js", "./card": "./dist/card/index.js", "./charts/*": "./dist/charts/*/index.js", diff --git a/packages/propel/tsdown.config.ts b/packages/propel/tsdown.config.ts index 8f23c301cb8..e0698f8d1a3 100644 --- a/packages/propel/tsdown.config.ts +++ b/packages/propel/tsdown.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "tsdown"; export default defineConfig({ entry: [ "src/accordion/index.ts", + "src/animated-counter/index.ts", "src/avatar/index.ts", "src/card/index.ts", "src/charts/*/index.ts", From e38e76475d3791085f21020a67849b1211d0da4c Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 09:14:58 +0530 Subject: [PATCH 04/10] chore: code refactor --- packages/propel/src/animated-counter/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/propel/src/animated-counter/index.ts b/packages/propel/src/animated-counter/index.ts index 32442f627cf..86b4c39b8a4 100644 --- a/packages/propel/src/animated-counter/index.ts +++ b/packages/propel/src/animated-counter/index.ts @@ -1,2 +1,2 @@ export { AnimatedCounter } from "./animated-counter"; -export type { AnimatedCounterProps } from "./animated-counter"; \ No newline at end of file +export type { AnimatedCounterProps } from "./animated-counter"; From 6d3f9345daa3d2ddecec8fe2e49e58ee3a09a5a0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 09:42:29 +0530 Subject: [PATCH 05/10] dev: emoji reaction and renderer component added to propel --- .../emoji-reaction/emoji-reaction-picker.tsx | 84 ++++++++ .../src/emoji-reaction/emoji-reaction.tsx | 182 ++++++++++++++++++ packages/propel/src/emoji-reaction/index.ts | 10 + 3 files changed, 276 insertions(+) create mode 100644 packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx create mode 100644 packages/propel/src/emoji-reaction/emoji-reaction.tsx create mode 100644 packages/propel/src/emoji-reaction/index.ts diff --git a/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx b/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx new file mode 100644 index 00000000000..092a388ac60 --- /dev/null +++ b/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx @@ -0,0 +1,84 @@ +import React, { useMemo, useCallback } from "react"; +import { EmojiRoot } from "../emoji-icon-picker/emoji/emoji"; +import { emojiToString } from "../emoji-icon-picker/helper"; +import { Popover } from "../popover"; +import { cn } from "../utils/classname"; +import { convertPlacementToSideAndAlign, type TPlacement, type TSide, type TAlign } from "../utils/placement"; + +export interface EmojiReactionPickerProps { + isOpen: boolean; + handleToggle: (value: boolean) => void; + buttonClassName?: string; + closeOnSelect?: boolean; + disabled?: boolean; + dropdownClassName?: string; + label: React.ReactNode; + onChange: (emoji: string) => void; + placement?: TPlacement; + searchDisabled?: boolean; + searchPlaceholder?: string; + side?: TSide; + align?: TAlign; +} + +export const EmojiReactionPicker: React.FC = (props) => { + const { + isOpen, + handleToggle, + buttonClassName, + closeOnSelect = true, + disabled = false, + dropdownClassName, + label, + onChange, + placement = "bottom-start", + searchDisabled = false, + searchPlaceholder = "Search", + side = "bottom", + align = "start", + } = props; + + // side and align calculations + const { finalSide, finalAlign } = useMemo(() => { + if (placement) { + const converted = convertPlacementToSideAndAlign(placement); + return { finalSide: converted.side, finalAlign: converted.align }; + } + return { finalSide: side, finalAlign: align }; + }, [placement, side, align]); + + const handleEmojiChange = useCallback( + (value: string) => { + const emoji = emojiToString(value); + onChange(emoji); + if (closeOnSelect) handleToggle(false); + }, + [onChange, closeOnSelect, handleToggle] + ); + + return ( + + + {label} + + +
+ +
+
+
+ ); +}; diff --git a/packages/propel/src/emoji-reaction/emoji-reaction.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.tsx new file mode 100644 index 00000000000..cad773364f7 --- /dev/null +++ b/packages/propel/src/emoji-reaction/emoji-reaction.tsx @@ -0,0 +1,182 @@ +import * as React from "react"; +import { Plus } from "lucide-react"; +import { AnimatedCounter } from "../animated-counter"; +import { stringToEmoji } from "../emoji-icon-picker"; +import { Tooltip } from "../tooltip"; +import { cn } from "../utils"; + +export interface EmojiReactionType { + emoji: string; + count: number; + reacted?: boolean; + users?: string[]; +} + +export interface EmojiReactionProps extends React.ButtonHTMLAttributes { + emoji: string; + count: number; + reacted?: boolean; + users?: string[]; + onReactionClick?: (emoji: string) => void; + className?: string; + showCount?: boolean; + size?: "sm" | "md" | "lg"; +} + +export interface EmojiReactionGroupProps extends React.HTMLAttributes { + reactions: EmojiReactionType[]; + onReactionClick?: (emoji: string) => void; + onAddReaction?: () => void; + className?: string; + showAddButton?: boolean; + maxDisplayUsers?: number; + size?: "sm" | "md" | "lg"; +} + +export interface EmojiReactionButtonProps extends React.ButtonHTMLAttributes { + 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]; + + const handleClick = () => { + onReactionClick?.(emoji); + }; + + const tooltipContent = React.useMemo(() => { + if (!users.length) return null; + + const displayUsers = users.slice(0, 5); + const remainingCount = users.length - displayUsers.length; + + return ( +
+
{stringToEmoji(emoji)}
+
+ {displayUsers.join(", ")} + {remainingCount > 0 && ` and ${remainingCount} more`} +
+
+ ); + }, [emoji, users]); + + const button = ( + + ); + + if (tooltipContent && users.length > 0) { + return {button}; + } + + return button; + } +); + +const EmojiReactionButton = React.forwardRef( + ({ onAddReaction, className, size = "md", ...props }, ref) => { + const sizeClass = sizeClasses[size]; + + return ( + + ); + } +); + +const EmojiReactionGroup = React.forwardRef( + ( + { + reactions, + onReactionClick, + onAddReaction, + className, + showAddButton = true, + maxDisplayUsers = 5, + size = "md", + ...props + }, + ref + ) => ( +
+ {reactions.map((reaction, index) => ( + + ))} + {showAddButton && } +
+ ) +); + +EmojiReaction.displayName = "EmojiReaction"; +EmojiReactionButton.displayName = "EmojiReactionButton"; +EmojiReactionGroup.displayName = "EmojiReactionGroup"; + +export { EmojiReaction, EmojiReactionButton, EmojiReactionGroup }; diff --git a/packages/propel/src/emoji-reaction/index.ts b/packages/propel/src/emoji-reaction/index.ts new file mode 100644 index 00000000000..151af7bb271 --- /dev/null +++ b/packages/propel/src/emoji-reaction/index.ts @@ -0,0 +1,10 @@ +export { EmojiReaction, EmojiReactionGroup, EmojiReactionButton } from "./emoji-reaction"; +export type { + EmojiReactionProps, + EmojiReactionGroupProps, + EmojiReactionButtonProps, + EmojiReactionType, +} from "./emoji-reaction"; + +export { EmojiReactionPicker } from "./emoji-reaction-picker"; +export type { EmojiReactionPickerProps } from "./emoji-reaction-picker"; From 722b026fee4a1b67db15639c4e8dcd3b1f74e515 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 09:42:58 +0530 Subject: [PATCH 06/10] dev: emoji reaction story added --- .../emoji-reaction-picker.stories.tsx | 49 ++++++++++++++ .../emoji-reaction/emoji-reaction.stories.tsx | 65 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx create mode 100644 packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx diff --git a/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx b/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx new file mode 100644 index 00000000000..558190fdf5b --- /dev/null +++ b/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx @@ -0,0 +1,49 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { SmilePlus } from "lucide-react"; +import { stringToEmoji } from "../emoji-icon-picker"; +import { EmojiReactionPicker } from "./emoji-reaction-picker"; + +const meta: Meta = { + title: "EmojiReactionPicker", + component: EmojiReactionPicker, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +}; + +export default meta; +type Story = StoryObj; + +const EmojiPickerDemo = (args: React.ComponentProps) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedEmoji, setSelectedEmoji] = useState(null); + + return ( +
+ { + setSelectedEmoji(emoji); + console.log("Selected emoji:", emoji); + }} + label={ + + {selectedEmoji ? stringToEmoji(selectedEmoji) : } + + } + /> +
+ ); +}; + +export const Default: Story = { + render: (args) => , + args: { + closeOnSelect: true, + searchPlaceholder: "Search emojis...", + }, +}; diff --git a/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx new file mode 100644 index 00000000000..4d7f2a2c522 --- /dev/null +++ b/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { EmojiReaction, EmojiReactionGroup, EmojiReactionType } from "./emoji-reaction"; + +const meta: Meta = { + title: "EmojiReaction", + component: EmojiReaction, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +}; + +export default meta; +type Story = StoryObj; + +export const Single: Story = { + args: { + emoji: "👍", + count: 5, + reacted: false, + users: ["User 1", "User 2", "User 3"], + }, +}; + +export const Reacted: Story = { + args: { + emoji: "❤️", + count: 12, + reacted: true, + users: ["User 1", "User 2", "User 3", "User 4", "User 5", "User 6"], + }, +}; + +const EmojiReactionGroupDemo = () => { + const [reactions, setReactions] = useState([]); + + const handleReaction = (emoji: string) => { + setReactions((prev) => + prev.map((reaction) => + reaction.emoji === emoji + ? { + ...reaction, + count: reaction.reacted ? reaction.count - 1 : reaction.count + 1, + reacted: !reaction.reacted, + } + : reaction + ) + ); + }; + + return ( +
+ console.log("Add reaction clicked")} + /> +
+ ); +}; + +export const Group: Story = { + render: () => , +}; From d4cdb1fd7115e16f646975cef746b403dce72a5d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 09:46:25 +0530 Subject: [PATCH 07/10] chore: propel config updated --- packages/propel/package.json | 2 ++ packages/propel/tsdown.config.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/propel/package.json b/packages/propel/package.json index 78d38174554..37f5e1e1b12 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -25,6 +25,8 @@ "./command": "./dist/command/index.js", "./dialog": "./dist/dialog/index.js", "./emoji-icon-picker": "./dist/emoji-icon-picker/index.js", + "./emoji-reaction": "./dist/emoji-reaction/index.js", + "./emoji-reaction-picker": "./dist/emoji-reaction-picker/index.js", "./icons": "./dist/icons/index.js", "./menu": "./dist/menu/index.js", "./popover": "./dist/popover/index.js", diff --git a/packages/propel/tsdown.config.ts b/packages/propel/tsdown.config.ts index e0698f8d1a3..c7c45f5d0cc 100644 --- a/packages/propel/tsdown.config.ts +++ b/packages/propel/tsdown.config.ts @@ -11,6 +11,8 @@ export default defineConfig({ "src/command/index.ts", "src/dialog/index.ts", "src/emoji-icon-picker/index.ts", + "src/emoji-reaction/index.ts", + "src/emoji-reaction-picker/index.ts", "src/icons/index.ts", "src/menu/index.ts", "src/popover/index.ts", From 2887cb15a685a9122672cf62c36537fbaed591d0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 09:57:37 +0530 Subject: [PATCH 08/10] chore: code refactor --- .../emoji-reaction/emoji-reaction.stories.tsx | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx index 4d7f2a2c522..4bdedf53c93 100644 --- a/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx +++ b/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx @@ -31,35 +31,3 @@ export const Reacted: Story = { users: ["User 1", "User 2", "User 3", "User 4", "User 5", "User 6"], }, }; - -const EmojiReactionGroupDemo = () => { - const [reactions, setReactions] = useState([]); - - const handleReaction = (emoji: string) => { - setReactions((prev) => - prev.map((reaction) => - reaction.emoji === emoji - ? { - ...reaction, - count: reaction.reacted ? reaction.count - 1 : reaction.count + 1, - reacted: !reaction.reacted, - } - : reaction - ) - ); - }; - - return ( -
- console.log("Add reaction clicked")} - /> -
- ); -}; - -export const Group: Story = { - render: () => , -}; From 5a883edc8c199f44d55c348fd64472ab037a3bda Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 9 Sep 2025 12:47:53 +0530 Subject: [PATCH 09/10] fix: format error --- packages/propel/src/animated-counter/animated-counter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/propel/src/animated-counter/animated-counter.tsx b/packages/propel/src/animated-counter/animated-counter.tsx index d5a9042bca3..09432358a2c 100644 --- a/packages/propel/src/animated-counter/animated-counter.tsx +++ b/packages/propel/src/animated-counter/animated-counter.tsx @@ -54,7 +54,7 @@ export const AnimatedCounter: React.FC = ({ count, classNa "animate-[slideOut_0.25s_ease-out_forwards]", direction === "up" && "[--slide-out-dir:-100%]", direction === "down" && "[--slide-out-dir:100%]", - sizeClass, + sizeClass )} style={{ animation: From fe570f2b5f92d3e660cb1ec8c89abc30aac501b8 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 10 Sep 2025 00:42:41 +0530 Subject: [PATCH 10/10] chore: lint error resolved --- packages/i18n/src/locales/ru/translations.json | 2 +- packages/propel/src/command/command.tsx | 4 ++-- packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index c6c84ed8ee2..ac1db05017c 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -2532,4 +2532,4 @@ "close_button": "Закрыть панель навигации", "outline_floating_button": "Открыть структуру" } -} \ No newline at end of file +} diff --git a/packages/propel/src/command/command.tsx b/packages/propel/src/command/command.tsx index 25c977c3e98..e462a7f2d79 100644 --- a/packages/propel/src/command/command.tsx +++ b/packages/propel/src/command/command.tsx @@ -19,7 +19,7 @@ function CommandInput({ className, ...props }: React.ComponentProps) { +function CommandList({ ...props }: React.ComponentProps) { return ; } @@ -27,7 +27,7 @@ function CommandEmpty({ ...props }: React.ComponentProps; } -function CommandItem({ className, ...props }: React.ComponentProps) { +function CommandItem({ ...props }: React.ComponentProps) { return ; } diff --git a/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx index 4bdedf53c93..55a4aa3b517 100644 --- a/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx +++ b/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx @@ -1,6 +1,5 @@ -import { useState } from "react"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { EmojiReaction, EmojiReactionGroup, EmojiReactionType } from "./emoji-reaction"; +import { EmojiReaction } from "./emoji-reaction"; const meta: Meta = { title: "EmojiReaction",