From f5faa8bd3513e0936891557f894c37278b0dca13 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 29 Oct 2025 21:18:23 +0530 Subject: [PATCH 01/15] chore: create logo component in propel package --- .../propel/src/emoji-icon-picker/index.ts | 1 + .../propel/src/emoji-icon-picker/logo.tsx | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/propel/src/emoji-icon-picker/logo.tsx diff --git a/packages/propel/src/emoji-icon-picker/index.ts b/packages/propel/src/emoji-icon-picker/index.ts index 1ace9dfe823..a662c48d7a2 100644 --- a/packages/propel/src/emoji-icon-picker/index.ts +++ b/packages/propel/src/emoji-icon-picker/index.ts @@ -1,4 +1,5 @@ export * from "./emoji-picker"; export * from "./helper"; +export * from "./logo"; export * from "./lucide-icons"; export * from "./material-icons"; diff --git a/packages/propel/src/emoji-icon-picker/logo.tsx b/packages/propel/src/emoji-icon-picker/logo.tsx new file mode 100644 index 00000000000..264dc6f03af --- /dev/null +++ b/packages/propel/src/emoji-icon-picker/logo.tsx @@ -0,0 +1,105 @@ +"use client"; + +import type { FC } from "react"; +// Due to some weird issue with the import order, the import of useFontFaceObserver +// should be after the imported here rather than some below helper functions as it is in the original file +// eslint-disable-next-line import/order +import useFontFaceObserver from "use-font-face-observer"; +// plane imports +import type { TLogoProps } from "@plane/types"; +// local imports +import { getEmojiSize, stringToEmoji } from "./helper"; +import { LUCIDE_ICONS_LIST } from "./lucide-icons"; + +type Props = { + logo: TLogoProps; + size?: number; + type?: "lucide" | "material"; +}; + +export const Logo: FC = (props) => { + const { logo, size = 16, type = "material" } = props; + + // destructuring the logo object + const { in_use, emoji, icon } = logo; + + // derived values + const value = in_use === "emoji" ? emoji?.value : icon?.name; + const color = icon?.color; + const lucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === value); + + const isMaterialSymbolsFontLoaded = useFontFaceObserver([ + { + family: `Material Symbols Rounded`, + style: `normal`, + weight: `normal`, + stretch: `condensed`, + }, + ]); + // if no value, return empty fragment + if (!value) return <>; + + if (!isMaterialSymbolsFontLoaded) { + return ( + + ); + } + + // emoji + if (in_use === "emoji") { + return ( + + {stringToEmoji(emoji?.value || "")} + + ); + } + + // icon + if (in_use === "icon") { + return ( + <> + {type === "lucide" ? ( + <> + {lucideIcon && ( + + )} + + ) : ( + + {value} + + )} + + ); + } + + // if no value, return empty fragment + return <>; +}; From f6bd2173a1dfe70e3b05541966b3f4dc241f2577 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 29 Oct 2025 21:30:57 +0530 Subject: [PATCH 02/15] chore: migrate emoji components from @plane/ui to @plane/propel --- .../web/ce/components/breadcrumbs/project.tsx | 3 +-- .../use-work-item-filters-config.tsx | 3 ++- .../overview/active-project-item.tsx | 4 ++-- .../components/analytics/select/project.tsx | 5 ++--- .../work-items/workitems-insight-table.tsx | 2 +- .../core/components/common/switcher-label.tsx | 2 +- .../list/cycle-list-project-group-header.tsx | 3 +-- .../components/dropdowns/project/base.tsx | 2 +- .../components/home/widgets/recents/page.tsx | 3 +-- .../home/widgets/recents/project.tsx | 2 +- .../filters/applied-filters/project.tsx | 2 +- .../filters/header/filters/project.tsx | 2 +- .../components/issues/issue-layouts/utils.tsx | 3 +-- .../pages/editor/header/logo-picker.tsx | 6 ++---- .../components/pages/editor/header/root.tsx | 4 ++-- apps/web/core/components/pages/list/block.tsx | 2 +- .../components/pages/modals/page-form.tsx | 12 +++++------ .../components/power-k/menus/projects.tsx | 2 +- apps/web/core/components/profile/sidebar.tsx | 4 +--- apps/web/core/components/project/card.tsx | 2 +- .../core/components/project/create/header.tsx | 3 +-- apps/web/core/components/project/form.tsx | 6 ++---- .../components/project/multi-select-modal.tsx | 2 +- .../project/project-feature-update.tsx | 2 +- .../settings/project/sidebar/root.tsx | 2 +- apps/web/core/components/views/form.tsx | 3 +-- .../core/components/views/view-list-item.tsx | 2 +- .../favorite-items/common/helper.tsx | 2 +- .../workspace/sidebar/projects-list-item.tsx | 2 +- apps/web/core/store/pages/base-page.ts | 7 +++---- .../core/extensions/callout/logo-selector.tsx | 20 ++++++++++--------- .../src/core/extensions/callout/utils.ts | 6 +++--- 32 files changed, 57 insertions(+), 68 deletions(-) diff --git a/apps/web/ce/components/breadcrumbs/project.tsx b/apps/web/ce/components/breadcrumbs/project.tsx index 2f6c67bd712..b1137d32990 100644 --- a/apps/web/ce/components/breadcrumbs/project.tsx +++ b/apps/web/ce/components/breadcrumbs/project.tsx @@ -1,12 +1,11 @@ "use client"; import { observer } from "mobx-react"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { ProjectIcon } from "@plane/propel/icons"; // plane imports import type { ICustomSearchSelectOption } from "@plane/types"; import { BreadcrumbNavigationSearchDropdown, Breadcrumbs } from "@plane/ui"; -// components -import { Logo } from "@/components/common/logo"; import { SwitcherLabel } from "@/components/common/switcher-label"; // hooks import { useProject } from "@/hooks/store/use-project"; diff --git a/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx b/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx index 90ad9394dbc..48aee8c7723 100644 --- a/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx +++ b/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo } from "react"; import { AtSign, Briefcase, Calendar } from "lucide-react"; // plane imports +import { Logo } from "@plane/propel/emoji-icon-picker"; import { CycleGroupIcon, CycleIcon, @@ -26,7 +27,7 @@ import type { IProject, TWorkItemFilterProperty, } from "@plane/types"; -import { Avatar, Logo } from "@plane/ui"; +import { Avatar } from "@plane/ui"; import { getAssigneeFilterConfig, getCreatedAtFilterConfig, diff --git a/apps/web/core/components/analytics/overview/active-project-item.tsx b/apps/web/core/components/analytics/overview/active-project-item.tsx index b551f494573..4737a8d8118 100644 --- a/apps/web/core/components/analytics/overview/active-project-item.tsx +++ b/apps/web/core/components/analytics/overview/active-project-item.tsx @@ -1,7 +1,7 @@ -import { ProjectIcon } from "@plane/propel/icons"; // plane package imports +import { Logo } from "@plane/propel/emoji-icon-picker"; +import { ProjectIcon } from "@plane/propel/icons"; import { cn } from "@plane/utils"; -import { Logo } from "@/components/common/logo"; // plane web hooks import { useProject } from "@/hooks/store/use-project"; diff --git a/apps/web/core/components/analytics/select/project.tsx b/apps/web/core/components/analytics/select/project.tsx index aee3090268e..b2f77d4ca25 100644 --- a/apps/web/core/components/analytics/select/project.tsx +++ b/apps/web/core/components/analytics/select/project.tsx @@ -1,11 +1,10 @@ "use client"; import { observer } from "mobx-react"; -import { ProjectIcon } from "@plane/propel/icons"; // plane package imports +import { Logo } from "@plane/propel/emoji-icon-picker"; +import { ProjectIcon } from "@plane/propel/icons"; import { CustomSearchSelect } from "@plane/ui"; -// components -import { Logo } from "@/components/common/logo"; // hooks import { useProject } from "@/hooks/store/use-project"; diff --git a/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx b/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx index 0d0d69cb1bb..5de02726835 100644 --- a/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx +++ b/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx @@ -5,13 +5,13 @@ import { useParams } from "next/navigation"; import useSWR from "swr"; import { UserRound } from "lucide-react"; import { useTranslation } from "@plane/i18n"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { ProjectIcon } from "@plane/propel/icons"; // plane package imports import type { AnalyticsTableDataMap, WorkItemInsightColumns } from "@plane/types"; // plane web components import { Avatar } from "@plane/ui"; import { getFileURL } from "@plane/utils"; -import { Logo } from "@/components/common/logo"; // hooks import { useAnalytics } from "@/hooks/store/use-analytics"; import { useProject } from "@/hooks/store/use-project"; diff --git a/apps/web/core/components/common/switcher-label.tsx b/apps/web/core/components/common/switcher-label.tsx index 7f88c6cdd9d..0e9375da61b 100644 --- a/apps/web/core/components/common/switcher-label.tsx +++ b/apps/web/core/components/common/switcher-label.tsx @@ -1,8 +1,8 @@ import type { FC } from "react"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import type { ISvgIcons } from "@plane/propel/icons"; import type { TLogoProps } from "@plane/types"; import { getFileURL, truncateText } from "@plane/utils"; -import { Logo } from "@/components/common/logo"; type TSwitcherIconProps = { logo_props?: TLogoProps; 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 fdf9b1d42f1..b12ab5786d1 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 @@ -4,12 +4,11 @@ import type { FC } from "react"; import React from "react"; import { observer } from "mobx-react"; import { ChevronRight } from "lucide-react"; +import { Logo } from "@plane/propel/emoji-icon-picker"; // icons import { Row } from "@plane/ui"; // helpers import { cn } from "@plane/utils"; -// components -import { Logo } from "@/components/common/logo"; import { useProject } from "@/hooks/store/use-project"; type Props = { diff --git a/apps/web/core/components/dropdowns/project/base.tsx b/apps/web/core/components/dropdowns/project/base.tsx index 77a2e85fe2e..8a598eea991 100644 --- a/apps/web/core/components/dropdowns/project/base.tsx +++ b/apps/web/core/components/dropdowns/project/base.tsx @@ -6,11 +6,11 @@ import { Check, ChevronDown, Search } from "lucide-react"; import { Combobox } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { ProjectIcon } from "@plane/propel/icons"; import { ComboDropDown } from "@plane/ui"; import { cn } from "@plane/utils"; // components -import { Logo } from "@/components/common/logo"; // hooks import { useDropdown } from "@/hooks/use-dropdown"; // plane web imports diff --git a/apps/web/core/components/home/widgets/recents/page.tsx b/apps/web/core/components/home/widgets/recents/page.tsx index 14a9f0467e1..4c7151e2c59 100644 --- a/apps/web/core/components/home/widgets/recents/page.tsx +++ b/apps/web/core/components/home/widgets/recents/page.tsx @@ -1,11 +1,10 @@ import { useRouter } from "next/navigation"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { PageIcon } from "@plane/propel/icons"; // plane import import type { TActivityEntityData, TPageEntityData } from "@plane/types"; import { Avatar } from "@plane/ui"; import { calculateTimeAgo, getFileURL, getPageName } from "@plane/utils"; -// components -import { Logo } from "@/components/common/logo"; import { ListItem } from "@/components/core/list"; // hooks import { useMember } from "@/hooks/store/use-member"; diff --git a/apps/web/core/components/home/widgets/recents/project.tsx b/apps/web/core/components/home/widgets/recents/project.tsx index e9e68a49f8f..f8a36de128c 100644 --- a/apps/web/core/components/home/widgets/recents/project.tsx +++ b/apps/web/core/components/home/widgets/recents/project.tsx @@ -1,9 +1,9 @@ import { useRouter } from "next/navigation"; // plane types +import { Logo } from "@plane/propel/emoji-icon-picker"; import type { TActivityEntityData, TProjectEntityData } from "@plane/types"; import { calculateTimeAgo } from "@plane/utils"; // components -import { Logo } from "@/components/common/logo"; import { ListItem } from "@/components/core/list"; import { MemberDropdown } from "@/components/dropdowns/member/dropdown"; // helpers 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 04efe576ca9..c50aba35660 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,7 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; // components -import { Logo } from "@/components/common/logo"; +import { Logo } from "@plane/propel/emoji-icon-picker"; // hooks import { useProject } from "@/hooks/store/use-project"; diff --git a/apps/web/core/components/issues/issue-layouts/filters/header/filters/project.tsx b/apps/web/core/components/issues/issue-layouts/filters/header/filters/project.tsx index b04f426204b..f95a7d996e4 100644 --- a/apps/web/core/components/issues/issue-layouts/filters/header/filters/project.tsx +++ b/apps/web/core/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -4,9 +4,9 @@ import React, { useMemo, useState } from "react"; import { sortBy } from "lodash-es"; import { observer } from "mobx-react"; // ui +import { Logo } from "@plane/propel/emoji-icon-picker"; import { Loader } from "@plane/ui"; // components -import { Logo } from "@/components/common/logo"; import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters"; // hooks import { useProject } from "@/hooks/store/use-project"; diff --git a/apps/web/core/components/issues/issue-layouts/utils.tsx b/apps/web/core/components/issues/issue-layouts/utils.tsx index 10ef1875c0f..a7bb471e492 100644 --- a/apps/web/core/components/issues/issue-layouts/utils.tsx +++ b/apps/web/core/components/issues/issue-layouts/utils.tsx @@ -6,6 +6,7 @@ import { clone, isNil, pull, uniq, concat } from "lodash-es"; import scrollIntoView from "smooth-scroll-into-view-if-needed"; // plane types import { EIconSize, ISSUE_PRIORITIES, STATE_GROUPS } from "@plane/constants"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import type { ISvgIcons } from "@plane/propel/icons"; import { CycleGroupIcon, CycleIcon, ModuleIcon, PriorityIcon, StateGroupIcon } from "@plane/propel/icons"; import type { @@ -26,8 +27,6 @@ import { EIssuesStoreType } from "@plane/types"; // plane ui import { Avatar } from "@plane/ui"; import { renderFormattedDate, getFileURL } from "@plane/utils"; -// components -import { Logo } from "@/components/common/logo"; // helpers // store import { store } from "@/lib/store-context"; diff --git a/apps/web/core/components/pages/editor/header/logo-picker.tsx b/apps/web/core/components/pages/editor/header/logo-picker.tsx index 63cb650998c..6ad966e0d4d 100644 --- a/apps/web/core/components/pages/editor/header/logo-picker.tsx +++ b/apps/web/core/components/pages/editor/header/logo-picker.tsx @@ -1,10 +1,8 @@ import { useState } from "react"; import { observer } from "mobx-react"; // plane imports -import { EmojiIconPicker, EmojiIconPickerTypes } from "@plane/ui"; +import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker"; import { cn } from "@plane/utils"; -// components -import { Logo } from "@/components/common/logo"; // store import type { TPageInstance } from "@/store/pages/base-page"; @@ -27,7 +25,7 @@ export const PageEditorHeaderLogoPicker: React.FC = observer((props) => { "max-h-[56px] pointer-events-auto": isLogoSelected, })} > - setIsLogoPickerOpen(val)} className="flex items-center justify-center" diff --git a/apps/web/core/components/pages/editor/header/root.tsx b/apps/web/core/components/pages/editor/header/root.tsx index 85e8546c0fa..36fe1952116 100644 --- a/apps/web/core/components/pages/editor/header/root.tsx +++ b/apps/web/core/components/pages/editor/header/root.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { SmilePlus } from "lucide-react"; // plane imports -import { EmojiIconPicker, EmojiIconPickerTypes } from "@plane/ui"; +import { EmojiPicker, EmojiIconPickerTypes } from "@plane/propel/emoji-icon-picker"; import { cn } from "@plane/utils"; // store import type { TPageInstance } from "@/store/pages/base-page"; @@ -32,7 +32,7 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => { "opacity-100": isTitleEmpty, })} > - setIsLogoPickerOpen(val)} className="flex items-center justify-center" diff --git a/apps/web/core/components/pages/list/block.tsx b/apps/web/core/components/pages/list/block.tsx index 5cf055d2816..c03d349fd48 100644 --- a/apps/web/core/components/pages/list/block.tsx +++ b/apps/web/core/components/pages/list/block.tsx @@ -3,11 +3,11 @@ import type { FC } from "react"; import { useRef } from "react"; import { observer } from "mobx-react"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { PageIcon } from "@plane/propel/icons"; // plane imports import { getPageName } from "@plane/utils"; // components -import { Logo } from "@/components/common/logo"; import { ListItem } from "@/components/core/list"; import { BlockItemAction } from "@/components/pages/list/block-item-action"; // hooks diff --git a/apps/web/core/components/pages/modals/page-form.tsx b/apps/web/core/components/pages/modals/page-form.tsx index 9005e33a89e..5f1d383f84c 100644 --- a/apps/web/core/components/pages/modals/page-form.tsx +++ b/apps/web/core/components/pages/modals/page-form.tsx @@ -8,13 +8,13 @@ import { Globe2, Lock } from "lucide-react"; import { ETabIndices, EPageAccess } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; +import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker"; import { PageIcon } from "@plane/propel/icons"; import type { TPage } from "@plane/types"; -import { EmojiIconPicker, EmojiIconPickerTypes, Input } from "@plane/ui"; -import { convertHexEmojiToDecimal, getTabIndex } from "@plane/utils"; +import { Input } from "@plane/ui"; +import { getTabIndex } from "@plane/utils"; // components import { AccessField } from "@/components/common/access-field"; -import { Logo } from "@/components/common/logo"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -65,7 +65,7 @@ export const PageForm: React.FC = (props) => {

Create page

- setIsOpen(val)} className="flex items-center justify-center flex-shrink0" @@ -86,8 +86,8 @@ export const PageForm: React.FC = (props) => { if (val?.type === "emoji") logoValue = { - value: convertHexEmojiToDecimal(val.value.unified), - url: val.value.imageUrl, + value: val.value, + url: undefined, }; else if (val?.type === "icon") logoValue = val.value; diff --git a/apps/web/core/components/power-k/menus/projects.tsx b/apps/web/core/components/power-k/menus/projects.tsx index c262a52e848..61ff21e9169 100644 --- a/apps/web/core/components/power-k/menus/projects.tsx +++ b/apps/web/core/components/power-k/menus/projects.tsx @@ -2,7 +2,7 @@ import React from "react"; // components -import { Logo } from "@/components/common/logo"; +import { Logo } from "@plane/propel/emoji-icon-picker"; // plane imports import type { TPartialProject } from "@/plane-web/types"; // local imports diff --git a/apps/web/core/components/profile/sidebar.tsx b/apps/web/core/components/profile/sidebar.tsx index 6db83dc0ec1..8f3559e2160 100644 --- a/apps/web/core/components/profile/sidebar.tsx +++ b/apps/web/core/components/profile/sidebar.tsx @@ -13,14 +13,12 @@ import { Disclosure, Transition } from "@headlessui/react"; import { useOutsideClickDetector } from "@plane/hooks"; // types import { useTranslation } from "@plane/i18n"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { Tooltip } from "@plane/propel/tooltip"; import type { IUserProfileProjectSegregation } from "@plane/types"; // plane ui import { Loader } from "@plane/ui"; import { cn, renderFormattedDate, getFileURL } from "@plane/utils"; -// components -import { Logo } from "@/components/common/logo"; -// helpers // hooks import { useAppTheme } from "@/hooks/store/use-app-theme"; import { useProject } from "@/hooks/store/use-project"; diff --git a/apps/web/core/components/project/card.tsx b/apps/web/core/components/project/card.tsx index d4ba496f0c9..6234b89c4f6 100644 --- a/apps/web/core/components/project/card.tsx +++ b/apps/web/core/components/project/card.tsx @@ -9,6 +9,7 @@ import { ArchiveRestoreIcon, Check, ExternalLink, LinkIcon, Lock, Settings, Tras import { EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; import { Button } from "@plane/propel/button"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { setPromiseToast, setToast, TOAST_TYPE } from "@plane/propel/toast"; import { Tooltip } from "@plane/propel/tooltip"; import type { IProject } from "@plane/types"; @@ -16,7 +17,6 @@ import type { TContextMenuItem } from "@plane/ui"; import { Avatar, AvatarGroup, ContextMenu, FavoriteStar } from "@plane/ui"; import { copyUrlToClipboard, cn, getFileURL, renderFormattedDate } from "@plane/utils"; // components -import { Logo } from "@/components/common/logo"; // hooks import { useMember } from "@/hooks/store/use-member"; import { useProject } from "@/hooks/store/use-project"; diff --git a/apps/web/core/components/project/create/header.tsx b/apps/web/core/components/project/create/header.tsx index e22c1565be6..47ec9295d4c 100644 --- a/apps/web/core/components/project/create/header.tsx +++ b/apps/web/core/components/project/create/header.tsx @@ -4,13 +4,12 @@ import { X } from "lucide-react"; // plane imports import { ETabIndices } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { EmojiPicker, EmojiIconPickerTypes } from "@plane/propel/emoji-icon-picker"; +import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker"; // plane types import type { IProject } from "@plane/types"; // plane ui import { getFileURL, getTabIndex } from "@plane/utils"; // components -import { Logo } from "@/components/common/logo"; import { ImagePickerPopover } from "@/components/core/image-picker-popover"; // plane web imports import { ProjectTemplateSelect } from "@/plane-web/components/projects/create/template-select"; diff --git a/apps/web/core/components/project/form.tsx b/apps/web/core/components/project/form.tsx index 1af37a63639..b534a93d8b3 100644 --- a/apps/web/core/components/project/form.tsx +++ b/apps/web/core/components/project/form.tsx @@ -8,14 +8,12 @@ import { NETWORK_CHOICES, PROJECT_TRACKER_ELEMENTS, PROJECT_TRACKER_EVENTS } fro import { useTranslation } from "@plane/i18n"; // plane imports import { Button } from "@plane/propel/button"; -import { EmojiPicker } from "@plane/propel/emoji-icon-picker"; +import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { Tooltip } from "@plane/propel/tooltip"; import type { IProject, IWorkspace } from "@plane/types"; -import { CustomSelect, Input, TextArea, EmojiIconPickerTypes } from "@plane/ui"; +import { CustomSelect, Input, TextArea } from "@plane/ui"; import { renderFormattedDate, getFileURL } from "@plane/utils"; -// components -import { Logo } from "@/components/common/logo"; import { ImagePickerPopover } from "@/components/core/image-picker-popover"; import { TimezoneSelect } from "@/components/global"; // helpers diff --git a/apps/web/core/components/project/multi-select-modal.tsx b/apps/web/core/components/project/multi-select-modal.tsx index 5a0e7b85b17..9f8934f1a78 100644 --- a/apps/web/core/components/project/multi-select-modal.tsx +++ b/apps/web/core/components/project/multi-select-modal.tsx @@ -6,10 +6,10 @@ import { Combobox } from "@headlessui/react"; // plane ui import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { Checkbox, EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; // components import { cn } from "@plane/utils"; -import { Logo } from "@/components/common/logo"; import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; // helpers // hooks diff --git a/apps/web/core/components/project/project-feature-update.tsx b/apps/web/core/components/project/project-feature-update.tsx index e411e33bae1..f2d7e9b8624 100644 --- a/apps/web/core/components/project/project-feature-update.tsx +++ b/apps/web/core/components/project/project-feature-update.tsx @@ -7,9 +7,9 @@ import Link from "next/link"; import { useTranslation } from "@plane/i18n"; // ui import { Button, getButtonStyling } from "@plane/propel/button"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { Row } from "@plane/ui"; // components -import { Logo } from "@/components/common/logo"; // hooks import { useProject } from "@/hooks/store/use-project"; // plane web imports diff --git a/apps/web/core/components/settings/project/sidebar/root.tsx b/apps/web/core/components/settings/project/sidebar/root.tsx index fe006ae1f5a..51c9ea4a10d 100644 --- a/apps/web/core/components/settings/project/sidebar/root.tsx +++ b/apps/web/core/components/settings/project/sidebar/root.tsx @@ -2,9 +2,9 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports import { PROJECT_SETTINGS_CATEGORIES, PROJECT_SETTINGS_CATEGORY } from "@plane/constants"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { getUserRole } from "@plane/utils"; // components -import { Logo } from "@/components/common/logo"; // hooks import { useProject } from "@/hooks/store/use-project"; // local imports diff --git a/apps/web/core/components/views/form.tsx b/apps/web/core/components/views/form.tsx index 3ad92c769ef..d63611a4579 100644 --- a/apps/web/core/components/views/form.tsx +++ b/apps/web/core/components/views/form.tsx @@ -7,7 +7,7 @@ import { Controller, useForm } from "react-hook-form"; import { ETabIndices, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; -import { EmojiPicker, EmojiIconPickerTypes } from "@plane/propel/emoji-icon-picker"; +import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker"; import { ViewsIcon } from "@plane/propel/icons"; import type { IIssueDisplayFilterOptions, @@ -20,7 +20,6 @@ import { EViewAccess, EIssuesStoreType } from "@plane/types"; import { Input, TextArea } from "@plane/ui"; import { getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex } from "@plane/utils"; // components -import { Logo } from "@/components/common/logo"; import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters"; import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; // hooks diff --git a/apps/web/core/components/views/view-list-item.tsx b/apps/web/core/components/views/view-list-item.tsx index 2ec2db74b49..524739a9271 100644 --- a/apps/web/core/components/views/view-list-item.tsx +++ b/apps/web/core/components/views/view-list-item.tsx @@ -4,11 +4,11 @@ import type { FC } from "react"; import { useRef } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { ViewsIcon } from "@plane/propel/icons"; // types import type { IProjectView } from "@plane/types"; // components -import { Logo } from "@/components/common/logo"; import { ListItem } from "@/components/core/list"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/apps/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx b/apps/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx index 2f1c3a732cf..17c86ea6e21 100644 --- a/apps/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx +++ b/apps/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx @@ -1,10 +1,10 @@ "use client"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { PageIcon } from "@plane/propel/icons"; // plane imports import type { IFavorite, TLogoProps } from "@plane/types"; // components -import { Logo } from "@/components/common/logo"; // plane web constants import { FAVORITE_ITEM_ICONS, FAVORITE_ITEM_LINKS } from "@/plane-web/constants/sidebar-favorites"; diff --git a/apps/web/core/components/workspace/sidebar/projects-list-item.tsx b/apps/web/core/components/workspace/sidebar/projects-list-item.tsx index af60aed6d1f..36d98856069 100644 --- a/apps/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/apps/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -15,12 +15,12 @@ import { Disclosure, Transition } from "@headlessui/react"; import { EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { ArchiveIcon } from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; import { CustomMenu, DropIndicator, DragHandle, ControlLink } from "@plane/ui"; import { cn } from "@plane/utils"; // components -import { Logo } from "@/components/common/logo"; import { LeaveProjectModal } from "@/components/project/leave-project-modal"; import { PublishProjectModal } from "@/components/project/publish-project/modal"; // hooks diff --git a/apps/web/core/store/pages/base-page.ts b/apps/web/core/store/pages/base-page.ts index ca0b0a2266e..b9381f248f8 100644 --- a/apps/web/core/store/pages/base-page.ts +++ b/apps/web/core/store/pages/base-page.ts @@ -2,9 +2,8 @@ import { set } from "lodash-es"; import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx"; // plane imports import { EPageAccess } from "@plane/constants"; +import type { TChangeHandlerProps } from "@plane/propel/emoji-icon-picker"; import type { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types"; -import type { TChangeHandlerProps } from "@plane/ui"; -import { convertHexEmojiToDecimal } from "@plane/utils"; // plane web store import { ExtendedBasePage } from "@/plane-web/store/pages/extended-base-page"; import type { RootStore } from "@/plane-web/store/root.store"; @@ -448,8 +447,8 @@ export class BasePage extends ExtendedBasePage implements TBasePage { let logoValue = {}; if (value?.type === "emoji") logoValue = { - value: convertHexEmojiToDecimal(value.value.unified), - url: value.value.imageUrl, + value: value.value, + url: undefined, }; else if (value?.type === "icon") logoValue = value.value; diff --git a/packages/editor/src/core/extensions/callout/logo-selector.tsx b/packages/editor/src/core/extensions/callout/logo-selector.tsx index 7a552cd16f0..717345e68f1 100644 --- a/packages/editor/src/core/extensions/callout/logo-selector.tsx +++ b/packages/editor/src/core/extensions/callout/logo-selector.tsx @@ -1,6 +1,7 @@ // plane imports -import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui"; -import { cn, convertHexEmojiToDecimal } from "@plane/utils"; +import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker"; +import type { TLogoProps } from "@plane/types"; +import { cn } from "@plane/utils"; // types import { TCalloutBlockAttributes } from "./types"; // utils @@ -17,7 +18,7 @@ type Props = { export const CalloutBlockLogoSelector: React.FC = (props) => { const { blockAttributes, disabled, handleOpen, isOpen, updateAttributes } = props; - const logoValue: TEmojiLogoProps = { + const logoValue: TLogoProps = { in_use: blockAttributes["data-logo-in-use"], icon: { color: blockAttributes["data-icon-color"], @@ -31,7 +32,7 @@ export const CalloutBlockLogoSelector: React.FC = (props) => { return (
- = (props) => { onChange={(val) => { // construct the new logo value based on the type of value let newLogoValue: Partial = {}; - let newLogoValueToStoreInLocalStorage: TEmojiLogoProps = { + let newLogoValueToStoreInLocalStorage: TLogoProps = { in_use: "emoji", emoji: { value: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"], @@ -51,15 +52,16 @@ export const CalloutBlockLogoSelector: React.FC = (props) => { }, }; if (val.type === "emoji") { + // val.value is now a string in decimal format (e.g. "128512") newLogoValue = { - "data-emoji-unicode": convertHexEmojiToDecimal(val.value.unified), - "data-emoji-url": val.value.imageUrl, + "data-emoji-unicode": val.value, + "data-emoji-url": undefined, }; newLogoValueToStoreInLocalStorage = { in_use: "emoji", emoji: { - value: convertHexEmojiToDecimal(val.value.unified), - url: val.value.imageUrl, + value: val.value, + url: undefined, }, }; } else if (val.type === "icon") { diff --git a/packages/editor/src/core/extensions/callout/utils.ts b/packages/editor/src/core/extensions/callout/utils.ts index 8c2cb8f65fc..6d64e0f8a44 100644 --- a/packages/editor/src/core/extensions/callout/utils.ts +++ b/packages/editor/src/core/extensions/callout/utils.ts @@ -1,5 +1,5 @@ // plane imports -import type { TEmojiLogoProps } from "@plane/ui"; +import type { TLogoProps } from "@plane/types"; import { sanitizeHTML } from "@plane/utils"; // types import { @@ -33,7 +33,7 @@ export const getStoredLogo = (): TStoredLogoValue => { if (typeof window !== "undefined") { const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo") ?? ""); if (storedData) { - let parsedData: TEmojiLogoProps; + let parsedData: TLogoProps; try { parsedData = JSON.parse(storedData); } catch (error) { @@ -65,7 +65,7 @@ export const getStoredLogo = (): TStoredLogoValue => { return fallBackValues; }; // function to update the stored logo on local storage -export const updateStoredLogo = (value: TEmojiLogoProps): void => { +export const updateStoredLogo = (value: TLogoProps): void => { if (typeof window === "undefined") return; localStorage.setItem("editor-calloutComponent-logo", JSON.stringify(value)); }; From 41393a919f234db1857a6be9e9af44a62e2f0717 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 29 Oct 2025 21:31:30 +0530 Subject: [PATCH 03/15] chore: remove emoji components and emoji-picker-react dependency --- apps/web/core/components/common/logo.tsx | 103 ------------ packages/ui/package.json | 1 - packages/ui/src/emoji/emoji-icon-helper.tsx | 102 ------------ .../ui/src/emoji/emoji-icon-picker-new.tsx | 140 ---------------- packages/ui/src/emoji/emoji-icon-picker.tsx | 139 ---------------- packages/ui/src/emoji/helpers.ts | 12 -- packages/ui/src/emoji/icons-list.tsx | 153 ------------------ packages/ui/src/emoji/index.ts | 4 - packages/ui/src/emoji/logo.tsx | 104 ------------ packages/ui/src/emoji/lucide-icons-list.tsx | 129 --------------- packages/ui/src/index.ts | 1 - 11 files changed, 888 deletions(-) delete mode 100644 apps/web/core/components/common/logo.tsx delete mode 100644 packages/ui/src/emoji/emoji-icon-helper.tsx delete mode 100644 packages/ui/src/emoji/emoji-icon-picker-new.tsx delete mode 100644 packages/ui/src/emoji/emoji-icon-picker.tsx delete mode 100644 packages/ui/src/emoji/helpers.ts delete mode 100644 packages/ui/src/emoji/icons-list.tsx delete mode 100644 packages/ui/src/emoji/index.ts delete mode 100644 packages/ui/src/emoji/logo.tsx delete mode 100644 packages/ui/src/emoji/lucide-icons-list.tsx diff --git a/apps/web/core/components/common/logo.tsx b/apps/web/core/components/common/logo.tsx deleted file mode 100644 index 12a7ff06bf2..00000000000 --- a/apps/web/core/components/common/logo.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import type { FC } from "react"; -// Due to some weird issue with the import order, the import of useFontFaceObserver -// should be after the imported here rather than some below helper functions as it is in the original file -// eslint-disable-next-line import/order -import useFontFaceObserver from "use-font-face-observer"; -// plane imports -import { getEmojiSize, LUCIDE_ICONS_LIST, stringToEmoji } from "@plane/propel/emoji-icon-picker"; -import type { TLogoProps } from "@plane/types"; - -type Props = { - logo: TLogoProps; - size?: number; - type?: "lucide" | "material"; -}; - -export const Logo: FC = (props) => { - const { logo, size = 16, type = "material" } = props; - - // destructuring the logo object - const { in_use, emoji, icon } = logo; - - // derived values - const value = in_use === "emoji" ? emoji?.value : icon?.name; - const color = icon?.color; - const lucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === value); - - const isMaterialSymbolsFontLoaded = useFontFaceObserver([ - { - family: `Material Symbols Rounded`, - style: `normal`, - weight: `normal`, - stretch: `condensed`, - }, - ]); - // if no value, return empty fragment - if (!value) return <>; - - if (!isMaterialSymbolsFontLoaded) { - return ( - - ); - } - - // emoji - if (in_use === "emoji") { - return ( - - {stringToEmoji(emoji?.value || "")} - - ); - } - - // icon - if (in_use === "icon") { - return ( - <> - {type === "lucide" ? ( - <> - {lucideIcon && ( - - )} - - ) : ( - - {value} - - )} - - ); - } - - // if no value, return empty fragment - return <>; -}; diff --git a/packages/ui/package.json b/packages/ui/package.json index 0f0ce349d4f..5dc758241e0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -47,7 +47,6 @@ "@popperjs/core": "^2.11.8", "@radix-ui/react-scroll-area": "^1.2.3", "clsx": "^2.0.0", - "emoji-picker-react": "^4.5.16", "lodash-es": "catalog:", "lucide-react": "catalog:", "react-color": "^2.19.3", diff --git a/packages/ui/src/emoji/emoji-icon-helper.tsx b/packages/ui/src/emoji/emoji-icon-helper.tsx deleted file mode 100644 index 4643dcb939e..00000000000 --- a/packages/ui/src/emoji/emoji-icon-helper.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Placement } from "@popperjs/core"; -import { EmojiClickData, Theme } from "emoji-picker-react"; - -export enum EmojiIconPickerTypes { - EMOJI = "emoji", - ICON = "icon", -} - -export const TABS_LIST = [ - { - key: EmojiIconPickerTypes.EMOJI, - title: "Emojis", - }, - { - key: EmojiIconPickerTypes.ICON, - title: "Icons", - }, -]; - -export type TChangeHandlerProps = - | { - type: EmojiIconPickerTypes.EMOJI; - value: EmojiClickData; - } - | { - type: EmojiIconPickerTypes.ICON; - value: { - name: string; - color: string; - }; - }; - -export type TCustomEmojiPicker = { - isOpen: boolean; - handleToggle: (value: boolean) => void; - buttonClassName?: string; - className?: string; - closeOnSelect?: boolean; - defaultIconColor?: string; - defaultOpen?: EmojiIconPickerTypes; - disabled?: boolean; - dropdownClassName?: string; - label: React.ReactNode; - onChange: (value: TChangeHandlerProps) => void; - placement?: Placement; - searchDisabled?: boolean; - searchPlaceholder?: string; - theme?: Theme; - iconType?: "material" | "lucide"; -}; - -export const DEFAULT_COLORS = ["#95999f", "#6d7b8a", "#5e6ad2", "#02b5ed", "#02b55c", "#f2be02", "#e57a00", "#f38e82"]; - -export type TIconsListProps = { - defaultColor: string; - onChange: (val: { name: string; color: string }) => void; - searchDisabled?: boolean; -}; - -/** - * Adjusts the given hex color to ensure it has enough contrast. - * @param {string} hex - The hex color code input by the user. - * @returns {string} - The adjusted hex color code. - */ -export const adjustColorForContrast = (hex: string): string => { - // Ensure hex color is valid - if (!/^#([0-9A-F]{3}){1,2}$/i.test(hex)) { - throw new Error("Invalid hex color code"); - } - - // Convert hex to RGB - let r = 0, - g = 0, - b = 0; - if (hex.length === 4) { - r = parseInt(hex[1] + hex[1], 16); - g = parseInt(hex[2] + hex[2], 16); - b = parseInt(hex[3] + hex[3], 16); - } else if (hex.length === 7) { - r = parseInt(hex[1] + hex[2], 16); - g = parseInt(hex[3] + hex[4], 16); - b = parseInt(hex[5] + hex[6], 16); - } - - // Calculate luminance - const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - - // If the color is too light, darken it - if (luminance > 0.5) { - r = Math.max(0, r - 50); - g = Math.max(0, g - 50); - b = Math.max(0, b - 50); - } - - // Convert RGB back to hex - const toHex = (value: number): string => { - const hex = value.toString(16); - return hex.length === 1 ? "0" + hex : hex; - }; - - return `#${toHex(r)}${toHex(g)}${toHex(b)}`; -}; diff --git a/packages/ui/src/emoji/emoji-icon-picker-new.tsx b/packages/ui/src/emoji/emoji-icon-picker-new.tsx deleted file mode 100644 index b8b48102322..00000000000 --- a/packages/ui/src/emoji/emoji-icon-picker-new.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Popover, Tab } from "@headlessui/react"; -import EmojiPicker from "emoji-picker-react"; -import React, { useRef, useState } from "react"; -import { usePopper } from "react-popper"; -// plane helpers -import { useOutsideClickDetector } from "@plane/hooks"; -// helpers -import { cn } from "../utils"; -// hooks -import { EmojiIconPickerTypes, TABS_LIST, TCustomEmojiPicker } from "./emoji-icon-helper"; -import { LucideIconsList } from "./lucide-icons-list"; -// helpers - -export const EmojiIconPicker: React.FC = (props) => { - const { - isOpen, - handleToggle, - buttonClassName, - className, - closeOnSelect = true, - defaultIconColor = "#6d7b8a", - defaultOpen = EmojiIconPickerTypes.EMOJI, - disabled = false, - dropdownClassName, - label, - onChange, - placement = "bottom-start", - searchDisabled = false, - searchPlaceholder = "Search", - theme, - } = props; - // refs - const containerRef = useRef(null); - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // popper-js - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement, - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 20, - }, - }, - ], - }); - - // close dropdown on outside click - useOutsideClickDetector(containerRef, () => handleToggle(false)); - - return ( - - <> - - - - {isOpen && ( - -
- tab.key === defaultOpen)} - > - - {TABS_LIST.map((tab) => ( - - cn("py-1 text-sm rounded border border-custom-border-200", { - "bg-custom-background-80": selected, - "hover:bg-custom-background-90 focus:bg-custom-background-90": !selected, - }) - } - > - {tab.title} - - ))} - - - - { - onChange({ - type: EmojiIconPickerTypes.EMOJI, - value: val, - }); - if (closeOnSelect) handleToggle(false); - }} - height="20rem" - width="100%" - theme={theme} - searchDisabled={searchDisabled} - searchPlaceholder={searchPlaceholder} - previewConfig={{ - showPreview: false, - }} - lazyLoadEmojis - /> - - - { - onChange({ - type: EmojiIconPickerTypes.ICON, - value: val, - }); - if (closeOnSelect) handleToggle(false); - }} - searchDisabled={searchDisabled} - /> - - - -
-
- )} - -
- ); -}; diff --git a/packages/ui/src/emoji/emoji-icon-picker.tsx b/packages/ui/src/emoji/emoji-icon-picker.tsx deleted file mode 100644 index 04f7fd1a3ec..00000000000 --- a/packages/ui/src/emoji/emoji-icon-picker.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Popover, Tab } from "@headlessui/react"; -import EmojiPicker from "emoji-picker-react"; -import React, { useRef, useState } from "react"; -import { usePopper } from "react-popper"; -// plane helpers -import { useOutsideClickDetector } from "@plane/hooks"; -// components -import { cn } from "../utils"; -import { EmojiIconPickerTypes, TABS_LIST, TCustomEmojiPicker } from "./emoji-icon-helper"; -import { IconsList } from "./icons-list"; -// helpers -// hooks - -export const CustomEmojiIconPicker: React.FC = (props) => { - const { - isOpen, - handleToggle, - buttonClassName, - className, - closeOnSelect = true, - defaultIconColor = "#6d7b8a", - defaultOpen = EmojiIconPickerTypes.EMOJI, - disabled = false, - dropdownClassName, - label, - onChange, - placement = "bottom-start", - searchDisabled = false, - searchPlaceholder = "Search", - theme, - } = props; - // refs - const containerRef = useRef(null); - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // popper-js - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement, - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 20, - }, - }, - ], - }); - - // close dropdown on outside click - useOutsideClickDetector(containerRef, () => handleToggle(false)); - - return ( - - <> - - - - {isOpen && ( - -
- tab.key === defaultOpen)} - > - - {TABS_LIST.map((tab) => ( - - cn("py-1 text-sm rounded border border-custom-border-200", { - "bg-custom-background-80": selected, - "hover:bg-custom-background-90 focus:bg-custom-background-90": !selected, - }) - } - > - {tab.title} - - ))} - - - - { - onChange({ - type: EmojiIconPickerTypes.EMOJI, - value: val, - }); - if (closeOnSelect) handleToggle(false); - }} - height="20rem" - width="100%" - theme={theme} - searchDisabled={searchDisabled} - searchPlaceholder={searchPlaceholder} - previewConfig={{ - showPreview: false, - }} - /> - - - { - onChange({ - type: EmojiIconPickerTypes.ICON, - value: val, - }); - if (closeOnSelect) handleToggle(false); - }} - searchDisabled={searchDisabled} - /> - - - -
-
- )} - -
- ); -}; diff --git a/packages/ui/src/emoji/helpers.ts b/packages/ui/src/emoji/helpers.ts deleted file mode 100644 index aa04f86036f..00000000000 --- a/packages/ui/src/emoji/helpers.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const emojiCodeToUnicode = (emoji: string) => { - if (!emoji) return ""; - - // convert emoji code to unicode - const uniCodeEmoji = emoji - .toString() - .split("-") - .map((emoji) => parseInt(emoji, 10).toString(16)) - .join("-"); - - return uniCodeEmoji; -}; diff --git a/packages/ui/src/emoji/icons-list.tsx b/packages/ui/src/emoji/icons-list.tsx deleted file mode 100644 index 6a21f02cf06..00000000000 --- a/packages/ui/src/emoji/icons-list.tsx +++ /dev/null @@ -1,153 +0,0 @@ -"use client"; - -import { Search } from "lucide-react"; -import React, { useEffect, useState } from "react"; -// icons -import useFontFaceObserver from "use-font-face-observer"; -import { InfoIcon } from "@plane/propel/icons"; -import { MATERIAL_ICONS_LIST } from ".."; -import { Input } from "../form-fields"; -import { cn } from "../utils"; -// components -// hooks -// helpers -import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper"; - -export const IconsList: React.FC = (props) => { - const { defaultColor, onChange, searchDisabled = false } = props; - // states - const [activeColor, setActiveColor] = useState(defaultColor); - const [showHexInput, setShowHexInput] = useState(false); - const [hexValue, setHexValue] = useState(""); - const [isInputFocused, setIsInputFocused] = useState(false); - const [query, setQuery] = useState(""); - - useEffect(() => { - if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false); - else { - setHexValue(defaultColor.slice(1, 7)); - setShowHexInput(true); - } - }, [defaultColor]); - - const filteredArray = MATERIAL_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase())); - - const isMaterialSymbolsFontLoaded = useFontFaceObserver([ - { - family: `Material Symbols Rounded`, - style: `normal`, - weight: `normal`, - stretch: `condensed`, - }, - ]); - - return ( - <> -
- {!searchDisabled && ( -
-
setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - > - - setQuery(e.target.value)} - className="text-[1rem] border-none p-0 h-full w-full " - /> -
-
- )} -
- {showHexInput ? ( -
- - HEX - # - { - const value = e.target.value; - setHexValue(value); - if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`)); - }} - className="flex-grow pl-0 text-xs text-custom-text-200" - mode="true-transparent" - autoFocus - /> -
- ) : ( - DEFAULT_COLORS.map((curCol) => ( - - )) - )} - -
-
- -

Colors will be adjusted to ensure sufficient contrast.

-
-
-
- {filteredArray.map((icon) => ( - - ))} -
- - ); -}; diff --git a/packages/ui/src/emoji/index.ts b/packages/ui/src/emoji/index.ts deleted file mode 100644 index c87b6cd238c..00000000000 --- a/packages/ui/src/emoji/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./emoji-icon-picker-new"; -export * from "./emoji-icon-picker"; -export * from "./emoji-icon-helper"; -export * from "./logo"; diff --git a/packages/ui/src/emoji/logo.tsx b/packages/ui/src/emoji/logo.tsx deleted file mode 100644 index 1d09991c108..00000000000 --- a/packages/ui/src/emoji/logo.tsx +++ /dev/null @@ -1,104 +0,0 @@ -"use client"; - -import { Emoji } from "emoji-picker-react"; -import React, { FC } from "react"; -import useFontFaceObserver from "use-font-face-observer"; -// local imports -import { LUCIDE_ICONS_LIST } from ".."; -import { emojiCodeToUnicode } from "./helpers"; - -export type TEmojiLogoProps = { - in_use: "emoji" | "icon"; - emoji?: { - value?: string; - url?: string; - }; - icon?: { - name?: string; - color?: string; - }; -}; - -type Props = { - logo: TEmojiLogoProps; - size?: number; - type?: "lucide" | "material"; -}; - -export const Logo: FC = (props) => { - const { logo, size = 16, type = "material" } = props; - - // destructuring the logo object - const { in_use, emoji, icon } = logo; - - // if no in_use value, return empty fragment - if (!in_use) return <>; - - // derived values - const value = in_use === "emoji" ? emoji?.value : icon?.name; - const color = icon?.color; - const lucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === value); - - const isMaterialSymbolsFontLoaded = useFontFaceObserver([ - { - family: `Material Symbols Rounded`, - style: `normal`, - weight: `normal`, - stretch: `condensed`, - }, - ]); - // if no value, return empty fragment - if (!value) return <>; - - if (!isMaterialSymbolsFontLoaded) { - return ( - - ); - } - - // emoji - if (in_use === "emoji") { - return ; - } - - // icon - if (in_use === "icon") { - return ( - <> - {type === "lucide" ? ( - <> - {lucideIcon && ( - - )} - - ) : ( - - {value} - - )} - - ); - } - - // if no value, return empty fragment - return <>; -}; diff --git a/packages/ui/src/emoji/lucide-icons-list.tsx b/packages/ui/src/emoji/lucide-icons-list.tsx deleted file mode 100644 index e9f0517ee76..00000000000 --- a/packages/ui/src/emoji/lucide-icons-list.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { Search } from "lucide-react"; -import React, { useEffect, useState } from "react"; -// local imports -import { InfoIcon } from "@plane/propel/icons"; -import { LUCIDE_ICONS_LIST } from ".."; -import { Input } from "../form-fields"; -import { cn } from "../utils"; -import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper"; - -export const LucideIconsList: React.FC = (props) => { - const { defaultColor, onChange, searchDisabled = false } = props; - // states - const [activeColor, setActiveColor] = useState(defaultColor); - const [showHexInput, setShowHexInput] = useState(false); - const [hexValue, setHexValue] = useState(""); - const [isInputFocused, setIsInputFocused] = useState(false); - const [query, setQuery] = useState(""); - - useEffect(() => { - if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false); - else { - setHexValue(defaultColor.slice(1, 7)); - setShowHexInput(true); - } - }, [defaultColor]); - - const filteredArray = LUCIDE_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase())); - - return ( - <> -
- {!searchDisabled && ( -
-
setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - > - - setQuery(e.target.value)} - className="text-[1rem] border-none p-0 h-full w-full " - /> -
-
- )} -
- {showHexInput ? ( -
- - HEX - # - { - const value = e.target.value; - setHexValue(value); - if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`)); - }} - className="flex-grow pl-0 text-xs text-custom-text-200" - mode="true-transparent" - autoFocus - /> -
- ) : ( - DEFAULT_COLORS.map((curCol) => ( - - )) - )} - -
-
- -

Colors will be adjusted to ensure sufficient contrast.

-
-
-
- {filteredArray.map((icon) => ( - - ))} -
- - ); -}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 8e86dc635cd..36aeaa97678 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -12,7 +12,6 @@ export * from "./drag-handle"; export * from "./drop-indicator"; export * from "./dropdown"; export * from "./dropdowns"; -export * from "./emoji"; export * from "./favorite-star"; export * from "./form-fields"; export * from "./header"; From b8c9594609c7631350baab16d740725d756cdaa1 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 29 Oct 2025 21:31:53 +0530 Subject: [PATCH 04/15] chore: update pnpm lockfile after emoji migration --- packages/editor/package.json | 1 + pnpm-lock.yaml | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/editor/package.json b/packages/editor/package.json index 985a3f4405c..bf0457fd897 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -43,6 +43,7 @@ "@plane/hooks": "workspace:*", "@plane/types": "workspace:*", "@plane/ui": "workspace:*", + "@plane/propel": "workspace:*", "@plane/utils": "workspace:*", "@tiptap/core": "catalog:", "@tiptap/extension-blockquote": "^2.22.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bca8100f89d..03498ebc752 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -715,6 +715,9 @@ importers: '@plane/hooks': specifier: workspace:* version: link:../hooks + '@plane/propel': + specifier: workspace:* + version: link:../propel '@plane/types': specifier: workspace:* version: link:../types @@ -1241,9 +1244,6 @@ importers: clsx: specifier: ^2.0.0 version: 2.1.1 - emoji-picker-react: - specifier: ^4.5.16 - version: 4.12.2(react@18.3.1) lodash-es: specifier: 'catalog:' version: 4.17.21 @@ -2170,8 +2170,8 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.0.5': - resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} + '@napi-rs/wasm-runtime@1.0.3': + resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} '@next/env@14.2.32': resolution: {integrity: sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==} @@ -9047,7 +9047,7 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.0.5': + '@napi-rs/wasm-runtime@1.0.3': dependencies: '@emnapi/core': 1.5.0 '@emnapi/runtime': 1.5.0 @@ -9734,7 +9734,7 @@ snapshots: '@rolldown/binding-wasm32-wasi@1.0.0-beta.34': dependencies: - '@napi-rs/wasm-runtime': 1.0.5 + '@napi-rs/wasm-runtime': 1.0.3 optional: true '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.34': From 21f9f02a4a4ee0dd3b0e30a0f68f2f0ec6c38ad6 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 15:14:41 +0530 Subject: [PATCH 05/15] chore: add AddReactionIcon to icon library --- .../src/icons/actions/add-reaction-icon.tsx | 21 +++++++++++++++++++ packages/propel/src/icons/actions/index.ts | 1 + packages/propel/src/icons/constants.tsx | 1 + packages/propel/src/icons/registry.ts | 2 ++ 4 files changed, 25 insertions(+) create mode 100644 packages/propel/src/icons/actions/add-reaction-icon.tsx 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 From 506798d488e6c533625c2fbb6057ab20793ce7a9 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 15:16:12 +0530 Subject: [PATCH 06/15] chore: use relative imports and update emoji reaction button --- packages/editor/package.json | 1 - packages/propel/src/calendar/root.tsx | 2 +- packages/propel/src/emoji-icon-picker/lucide-icons.tsx | 2 +- packages/propel/src/emoji-reaction/emoji-reaction.tsx | 6 +++--- packages/propel/src/menu/menu.tsx | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/editor/package.json b/packages/editor/package.json index 5b7108a3be4..bf0457fd897 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -45,7 +45,6 @@ "@plane/ui": "workspace:*", "@plane/propel": "workspace:*", "@plane/utils": "workspace:*", - "@plane/propel": "workspace:*", "@tiptap/core": "catalog:", "@tiptap/extension-blockquote": "^2.22.3", "@tiptap/extension-character-count": "^2.22.3", diff --git a/packages/propel/src/calendar/root.tsx b/packages/propel/src/calendar/root.tsx index e9703f44c21..e29bfd4a87b 100644 --- a/packages/propel/src/calendar/root.tsx +++ b/packages/propel/src/calendar/root.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { DayPicker } from "react-day-picker"; -import { ChevronLeftIcon } from "@plane/propel/icons"; +import { ChevronLeftIcon } from "../icons"; import { cn } from "../utils"; diff --git a/packages/propel/src/emoji-icon-picker/lucide-icons.tsx b/packages/propel/src/emoji-icon-picker/lucide-icons.tsx index 10d4104de9c..f40f583b751 100644 --- a/packages/propel/src/emoji-icon-picker/lucide-icons.tsx +++ b/packages/propel/src/emoji-icon-picker/lucide-icons.tsx @@ -151,7 +151,7 @@ import { Search, User, } from "lucide-react"; -import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon } from "@plane/propel/icons"; +import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon } from "../icons"; export const LUCIDE_ICONS_LIST = [ { name: "Activity", element: Activity }, diff --git a/packages/propel/src/emoji-reaction/emoji-reaction.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.tsx index cad773364f7..711ce005c61 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"; @@ -96,7 +96,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", + "inline-flex items-center rounded-full border transition-all duration-200", "focus:outline-none focus:ring-2 focus:ring-custom-primary-100/20 focus:ring-offset-1", sizeClass.button, reacted @@ -138,7 +138,7 @@ const EmojiReactionButton = React.forwardRef - + ); } diff --git a/packages/propel/src/menu/menu.tsx b/packages/propel/src/menu/menu.tsx index af14ff261a8..92ee70adb3b 100644 --- a/packages/propel/src/menu/menu.tsx +++ b/packages/propel/src/menu/menu.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { Menu as BaseMenu } from "@base-ui-components/react/menu"; import { MoreHorizontal } from "lucide-react"; -import { ChevronDownIcon, ChevronRightIcon } from "@plane/propel/icons"; +import { ChevronDownIcon, ChevronRightIcon } from "../icons"; import { cn } from "../utils/classname"; import { TMenuProps, TSubMenuProps, TMenuItemProps } from "./types"; From 585b0103eab06c46e62bd9f20bf7eeb8f2b10df2 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 15:17:20 +0530 Subject: [PATCH 07/15] chore: migrate web app emoji reactions to propel components --- .../components/comments/comment-reaction.tsx | 99 +++++++++++-------- .../issues/issue-detail/reactions/index.ts | 4 +- .../issue-detail/reactions/issue-comment.tsx | 97 +++++++++--------- .../issues/issue-detail/reactions/issue.tsx | 90 ++++++++++------- .../reactions/reaction-selector.tsx | 74 -------------- 5 files changed, 164 insertions(+), 200 deletions(-) delete mode 100644 apps/web/core/components/issues/issue-detail/reactions/reaction-selector.tsx diff --git a/apps/web/core/components/comments/comment-reaction.tsx b/apps/web/core/components/comments/comment-reaction.tsx index 8e3e04b94b1..f4b2aef9454 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,67 @@ 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)} + size="sm" + /> + } + placement="bottom-start" + />
); }); 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..542271fb155 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,54 @@ 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)} + size="sm" + /> + } + 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..948926eeb96 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,54 @@ 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)} + size="sm" + /> + } + 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) => ( - - ))} -
-
-
-
- - )} -
- ); -}; From 15fced318b4af4ed58b83f7c70748495f7a645e0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 15:17:56 +0530 Subject: [PATCH 08/15] chore: migrate space app emoji reactions to Propel components --- .../comment/comment-reactions.tsx | 136 +++++++++--------- .../reactions/issue-emoji-reactions.tsx | 124 ++++++++-------- apps/space/core/components/ui/index.ts | 1 - .../core/components/ui/reaction-selector.tsx | 80 ----------- 4 files changed, 132 insertions(+), 209 deletions(-) delete mode 100644 apps/space/core/components/ui/reaction-selector.tsx 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..593f7020208 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(); @@ -62,70 +63,67 @@ 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)); + 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)} + size="sm" + /> + } + 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..c7be344fb42 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"; @@ -19,7 +21,9 @@ type IssueEmojiReactionsProps = { }; export const IssueEmojiReactions: React.FC = observer((props) => { - const { anchor, issueIdFromProps, size = "md" } = props; + const { anchor, issueIdFromProps, size = "sm" } = props; + // state + const [isPickerOpen, setIsPickerOpen] = useState(false); // router const router = useRouter(); const pathName = usePathname(); @@ -58,62 +62,64 @@ 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)} + size={size} + /> + } + 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) => ( - - ))} -
-
-
-
- - )} -
- ); -}; From 986951c16e4b08e8e971a48cfe333492a1fd94fb Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 16:19:41 +0530 Subject: [PATCH 09/15] fix: lint and format errors --- .../peek-overview/comment/comment-reactions.tsx | 11 +++++++++-- .../cycles/list/cycle-list-project-group-header.tsx | 2 +- .../issue-layouts/filters/applied-filters/project.tsx | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) 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 593f7020208..8c207fff145 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 @@ -38,8 +38,15 @@ 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 []; + return details[peekId].comments.find((c) => c.id === commentId)?.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); 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-layouts/filters/applied-filters/project.tsx b/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx index 1c49a8542e7..0c7ca22ec15 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,7 @@ import { observer } from "mobx-react"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { CloseIcon } from "@plane/propel/icons"; // components -import { Logo } from "@plane/propel/emoji-icon-picker"; // hooks import { useProject } from "@/hooks/store/use-project"; From d1e746f1daf56ad3a3ac47c645c1ee54f324980b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 17:01:12 +0530 Subject: [PATCH 10/15] chore: code refactor --- packages/propel/src/emoji-reaction/emoji-reaction.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/propel/src/emoji-reaction/emoji-reaction.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.tsx index 711ce005c61..5c12ccb1ed3 100644 --- a/packages/propel/src/emoji-reaction/emoji-reaction.tsx +++ b/packages/propel/src/emoji-reaction/emoji-reaction.tsx @@ -41,7 +41,7 @@ export interface EmojiReactionButtonProps extends React.ButtonHTMLAttributes( ref={ref} onClick={handleClick} className={cn( - "inline-flex items-center rounded-full border transition-all duration-200", - "focus:outline-none focus:ring-2 focus:ring-custom-primary-100/20 focus:ring-offset-1", + "inline-flex items-center rounded-md border transition-all duration-200", sizeClass.button, reacted ? "bg-custom-primary-100/10 border-custom-primary-100 text-custom-primary-100" From e9d081b6b6aff96f23ac44bf8038e2b87a31bda8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 19:58:34 +0530 Subject: [PATCH 11/15] chore: emoji picker, emoji reaction and animated counter propel component updated --- .../src/animated-counter/animated-counter.tsx | 8 +- .../emoji-reaction-picker.stories.tsx | 48 ++++++++++ .../emoji-reaction/emoji-reaction.stories.tsx | 89 ++++------------- .../src/emoji-reaction/emoji-reaction.tsx | 95 +++++-------------- 4 files changed, 96 insertions(+), 144 deletions(-) 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 && ( ; 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.stories.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx index d0117c81d33..f0b6a2a94d7 100644 --- a/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx +++ b/packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx @@ -33,6 +33,10 @@ export const Reacted: Story = { }; export const Interactive: Story = { + args: { + emoji: "👍", + count: 0, + }, render() { const [reacted, setReacted] = useState(false); const [count, setCount] = useState(5); @@ -75,28 +79,11 @@ export const WithoutCount: Story = { }, }; -export const Sizes: Story = { - render() { - return ( -
-
- 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 5c12ccb1ed3..898fe894b65 100644 --- a/packages/propel/src/emoji-reaction/emoji-reaction.tsx +++ b/packages/propel/src/emoji-reaction/emoji-reaction.tsx @@ -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-1.5 py-1 text-xs gap-0.5", - 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,8 +64,7 @@ const EmojiReaction = React.forwardRef( ref={ref} onClick={handleClick} className={cn( - "inline-flex items-center rounded-md border transition-all duration-200", - 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", @@ -105,8 +72,8 @@ const EmojiReaction = React.forwardRef( )} {...props} > - {emoji} - {showCount && count > 0 && } + {emoji} + {showCount && count > 0 && } ); @@ -119,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 ) => (
@@ -166,10 +120,9 @@ const EmojiReactionGroup = React.forwardRef ))} - {showAddButton && } + {showAddButton && }
) ); From e6fecef9e15b96cbbc21b4d87c419e635f5318c5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 20:00:07 +0530 Subject: [PATCH 12/15] chore: emoji picker and reaction component code refactor --- .../issues/issue-layouts/kanban/block-reactions.tsx | 2 +- .../issues/peek-overview/comment/comment-reactions.tsx | 1 - .../components/issues/reactions/issue-emoji-reactions.tsx | 4 +--- apps/web/core/components/comments/comment-reaction.tsx | 1 - .../issues/issue-detail/reactions/issue-comment.tsx | 1 - .../core/components/issues/issue-detail/reactions/issue.tsx | 1 - 6 files changed, 2 insertions(+), 8 deletions(-) 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 8c207fff145..471d6f75fe6 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 @@ -126,7 +126,6 @@ export const CommentReactions: React.FC = observer((props) => { onReactionClick={handleEmojiClick} showAddButton={!isInIframe} onAddReaction={() => setIsPickerOpen(true)} - size="sm" /> } 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 c7be344fb42..0a3c193d9ff 100644 --- a/apps/space/core/components/issues/reactions/issue-emoji-reactions.tsx +++ b/apps/space/core/components/issues/reactions/issue-emoji-reactions.tsx @@ -17,11 +17,10 @@ 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 = "sm" } = props; + const { anchor, issueIdFromProps } = props; // state const [isPickerOpen, setIsPickerOpen] = useState(false); // router @@ -116,7 +115,6 @@ export const IssueEmojiReactions: React.FC = observer( onReactionClick={handleEmojiClick} showAddButton onAddReaction={() => setIsPickerOpen(true)} - size={size} /> } placement="bottom-start" diff --git a/apps/web/core/components/comments/comment-reaction.tsx b/apps/web/core/components/comments/comment-reaction.tsx index f4b2aef9454..181f7a072a8 100644 --- a/apps/web/core/components/comments/comment-reaction.tsx +++ b/apps/web/core/components/comments/comment-reaction.tsx @@ -76,7 +76,6 @@ export const CommentReactions: FC = observer((props) => { onReactionClick={handleReactionClick} showAddButton={!disabled} onAddReaction={() => setIsPickerOpen(true)} - size="sm" /> } placement="bottom-start" 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 542271fb155..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 @@ -135,7 +135,6 @@ export const IssueCommentReaction: FC = observer((props) onReactionClick={handleReactionClick} showAddButton={!disabled} onAddReaction={() => setIsPickerOpen(true)} - size="sm" /> } 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 948926eeb96..804e816c251 100644 --- a/apps/web/core/components/issues/issue-detail/reactions/issue.tsx +++ b/apps/web/core/components/issues/issue-detail/reactions/issue.tsx @@ -137,7 +137,6 @@ export const IssueReaction: FC = observer((props) => { onReactionClick={handleReactionClick} showAddButton={!disabled} onAddReaction={() => setIsPickerOpen(true)} - size="sm" /> } placement="bottom-start" From 93c54187df18ce7fa8626b9d1c13e41f483cbf5d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 30 Oct 2025 20:02:36 +0530 Subject: [PATCH 13/15] fix: peekview outside click event propagation issue --- packages/propel/src/emoji-icon-picker/emoji-picker.tsx | 1 + packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/propel/src/emoji-icon-picker/emoji-picker.tsx b/packages/propel/src/emoji-icon-picker/emoji-picker.tsx index 30bd2c0b406..759fda1440a 100644 --- a/packages/propel/src/emoji-icon-picker/emoji-picker.tsx +++ b/packages/propel/src/emoji-icon-picker/emoji-picker.tsx @@ -106,6 +106,7 @@ export const EmojiPicker: React.FC = (props) => { side={finalSide} align={finalAlign} sideOffset={8} + data-prevent-outside-click="true" > 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" >
Date: Mon, 3 Nov 2025 15:36:47 +0530 Subject: [PATCH 14/15] chore: code refactor --- .../issues/peek-overview/comment/comment-reactions.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 471d6f75fe6..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 @@ -40,7 +40,10 @@ export const CommentReactions: React.FC = observer((props) => { const commentReactions = useMemo(() => { if (!peekId) return []; - return details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions ?? []; + 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(() => { @@ -99,7 +102,9 @@ export const CommentReactions: React.FC = observer((props) => { return; } // Convert emoji back to decimal string format for the API - const emojiCodePoints = Array.from(emoji).map((char) => char.codePointAt(0)); + const emojiCodePoints = Array.from(emoji) + .map((char) => char.codePointAt(0)) + .filter((cp): cp is number => cp !== undefined); const reactionString = emojiCodePoints.join("-"); handleReactionClick(reactionString); }; From c78c809097b1d57eddd8cbf96470a1635cdbede0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 6 Nov 2025 18:17:42 +0530 Subject: [PATCH 15/15] chore: code refactor --- .../components/cycles/list/cycle-list-project-group-header.tsx | 1 - 1 file changed, 1 deletion(-) 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 1126ed8db91..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 @@ -5,7 +5,6 @@ import React from "react"; import { observer } from "mobx-react"; import { Logo } from "@plane/propel/emoji-icon-picker"; import { ChevronRightIcon } from "@plane/propel/icons"; -import { Logo } from "@plane/propel/emoji-icon-picker"; // icons import { Row } from "@plane/ui"; // helpers