[WEB-4882]feat: suspended users (#7844)

This commit is contained in:
Vamsi Krishna 2025-09-30 15:31:56 +05:30 committed by GitHub
parent 726529044e
commit c45151d5e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 176 additions and 37 deletions

View File

@ -27,6 +27,7 @@ export const useMemberColumns = () => {
// derived values // derived values
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const isSuspended = (rowData: RowData) => rowData.is_active === false;
// handlers // handlers
const handleDisplayFilterUpdate = (filterUpdates: Partial<IMemberFilters>) => { const handleDisplayFilterUpdate = (filterUpdates: Partial<IMemberFilters>) => {
updateFilters(filterUpdates); updateFilters(filterUpdates);
@ -58,6 +59,11 @@ export const useMemberColumns = () => {
{ {
key: "Display name", key: "Display name",
content: t("workspace_settings.settings.members.details.display_name"), content: t("workspace_settings.settings.members.details.display_name"),
tdRender: (rowData: RowData) => (
<div className={`w-32 ${isSuspended(rowData) ? "text-custom-text-400" : ""}`}>
{rowData.member.display_name}
</div>
),
thRender: () => ( thRender: () => (
<MemberHeaderColumn <MemberHeaderColumn
property="display_name" property="display_name"
@ -65,12 +71,16 @@ export const useMemberColumns = () => {
handleDisplayFilterUpdate={handleDisplayFilterUpdate} handleDisplayFilterUpdate={handleDisplayFilterUpdate}
/> />
), ),
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
}, },
{ {
key: "Email address", key: "Email address",
content: t("workspace_settings.settings.members.details.email_address"), content: t("workspace_settings.settings.members.details.email_address"),
tdRender: (rowData: RowData) => (
<div className={`w-48 truncate ${isSuspended(rowData) ? "text-custom-text-400" : ""}`}>
{rowData.member.email}
</div>
),
thRender: () => ( thRender: () => (
<MemberHeaderColumn <MemberHeaderColumn
property="email" property="email"
@ -78,7 +88,6 @@ export const useMemberColumns = () => {
handleDisplayFilterUpdate={handleDisplayFilterUpdate} handleDisplayFilterUpdate={handleDisplayFilterUpdate}
/> />
), ),
tdRender: (rowData: RowData) => <div className="w-48 truncate">{rowData.member.email}</div>,
}, },
{ {
@ -97,14 +106,17 @@ export const useMemberColumns = () => {
{ {
key: "Authentication", key: "Authentication",
content: t("workspace_settings.settings.members.details.authentication"), content: t("workspace_settings.settings.members.details.authentication"),
tdRender: (rowData: RowData) => ( tdRender: (rowData: RowData) =>
<div className="capitalize">{rowData.member.last_login_medium?.replace("-", " ")}</div> isSuspended(rowData) ? null : (
), <div className="capitalize">{rowData.member.last_login_medium?.replace("-", " ")}</div>
),
}, },
{ {
key: "Joining date", key: "Joining date",
content: t("workspace_settings.settings.members.details.joining_date"), content: t("workspace_settings.settings.members.details.joining_date"),
tdRender: (rowData: RowData) =>
isSuspended(rowData) ? null : <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
thRender: () => ( thRender: () => (
<MemberHeaderColumn <MemberHeaderColumn
property="joining_date" property="joining_date"
@ -112,7 +124,6 @@ export const useMemberColumns = () => {
handleDisplayFilterUpdate={handleDisplayFilterUpdate} handleDisplayFilterUpdate={handleDisplayFilterUpdate}
/> />
), ),
tdRender: (rowData: RowData) => <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
}, },
]; ];
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal }; return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };

View File

@ -3,16 +3,20 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Placement } from "@popperjs/core"; import { Placement } from "@popperjs/core";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react"; import { Check, Search } from "lucide-react";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { SuspendedUserIcon } from "@plane/propel/icons";
import { EPillSize, EPillVariant, Pill } from "@plane/propel/pill";
import { IUserLite } from "@plane/types"; import { IUserLite } from "@plane/types";
import { Avatar } from "@plane/ui"; import { Avatar } from "@plane/ui";
import { cn, getFileURL } from "@plane/utils"; import { cn, getFileURL } from "@plane/utils";
// hooks // hooks
import { useMember } from "@/hooks/store/use-member";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
@ -37,6 +41,8 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
placement, placement,
referenceElement, referenceElement,
} = props; } = props;
// router
const { workspaceSlug } = useParams();
// refs // refs
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
// states // states
@ -46,6 +52,9 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
// store hooks // store hooks
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const {
workspace: { isUserSuspended },
} = useMember();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
// popper-js init // popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, { const { styles, attributes } = usePopper(referenceElement, popperElement, {
@ -84,8 +93,19 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} /> <div className="w-4">
<span className="flex-grow truncate"> {isUserSuspended(userId, workspaceSlug?.toString()) ? (
<SuspendedUserIcon className="h-3.5 w-3.5 text-custom-text-400" />
) : (
<Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
)}
</div>
<span
className={cn(
"flex-grow truncate",
isUserSuspended(userId, workspaceSlug?.toString()) ? "text-custom-text-400" : ""
)}
>
{currentUser?.id === userId ? t("you") : userDetails?.display_name} {currentUser?.id === userId ? t("you") : userDetails?.display_name}
</span> </span>
</div> </div>
@ -133,15 +153,26 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
key={option.value} key={option.value}
value={option.value} value={option.value}
className={({ active, selected }) => className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ cn(
active ? "bg-custom-background-80" : "" "flex w-full select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` active && "bg-custom-background-80",
selected ? "text-custom-text-100" : "text-custom-text-200",
isUserSuspended(option.value, workspaceSlug?.toString())
? "cursor-not-allowed"
: "cursor-pointer"
)
} }
disabled={isUserSuspended(option.value, workspaceSlug?.toString())}
> >
{({ selected }) => ( {({ selected }) => (
<> <>
<span className="flex-grow truncate">{option.content}</span> <span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />} {selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
{isUserSuspended(option.value, workspaceSlug?.toString()) && (
<Pill variant={EPillVariant.DEFAULT} size={EPillSize.XS} className="border-none">
Suspended
</Pill>
)}
</> </>
)} )}
</Combobox.Option> </Combobox.Option>

View File

@ -30,6 +30,7 @@ const WORKSPACE_ROLE_OPTIONS: IRoleOption[] = [
{ value: String(EUserWorkspaceRoles.ADMIN), label: "Admin" }, { value: String(EUserWorkspaceRoles.ADMIN), label: "Admin" },
{ value: String(EUserWorkspaceRoles.MEMBER), label: "Member" }, { value: String(EUserWorkspaceRoles.MEMBER), label: "Member" },
{ value: String(EUserWorkspaceRoles.GUEST), label: "Guest" }, { value: String(EUserWorkspaceRoles.GUEST), label: "Guest" },
{ value: "suspended", label: "Suspended" },
]; ];
// Role filter group component // Role filter group component

View File

@ -5,9 +5,11 @@ import { Trash2 } from "lucide-react";
import { Disclosure } from "@headlessui/react"; import { Disclosure } from "@headlessui/react";
// plane imports // plane imports
import { ROLE, EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants"; import { ROLE, EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { SuspendedUserIcon } from "@plane/propel/icons";
import { Pill, EPillVariant, EPillSize } from "@plane/propel/pill";
import { IUser, IWorkspaceMember } from "@plane/types"; import { IUser, IWorkspaceMember } from "@plane/types";
// plane ui // plane ui
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { CustomSelect, PopoverMenu, TOAST_TYPE, cn, setToast } from "@plane/ui";
// constants // constants
// helpers // helpers
import { getFileURL } from "@plane/utils"; import { getFileURL } from "@plane/utils";
@ -19,6 +21,7 @@ import { useUser, useUserPermissions } from "@/hooks/store/user";
export interface RowData { export interface RowData {
member: IWorkspaceMember; member: IWorkspaceMember;
role: EUserPermissions; role: EUserPermissions;
is_active: boolean;
} }
type NameProps = { type NameProps = {
@ -38,6 +41,7 @@ export const NameColumn: React.FC<NameProps> = (props) => {
const { rowData, workspaceSlug, isAdmin, currentUser, setRemoveMemberModal } = props; const { rowData, workspaceSlug, isAdmin, currentUser, setRemoveMemberModal } = props;
// derived values // derived values
const { avatar_url, display_name, email, first_name, id, last_name } = rowData.member; const { avatar_url, display_name, email, first_name, id, last_name } = rowData.member;
const isSuspended = rowData.is_active === false;
return ( return (
<Disclosure> <Disclosure>
@ -48,24 +52,39 @@ export const NameColumn: React.FC<NameProps> = (props) => {
{avatar_url && avatar_url.trim() !== "" ? ( {avatar_url && avatar_url.trim() !== "" ? (
<Link href={`/${workspaceSlug}/profile/${id}`}> <Link href={`/${workspaceSlug}/profile/${id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full capitalize text-white"> <span className="relative flex h-6 w-6 items-center justify-center rounded-full capitalize text-white">
<img {isSuspended ? (
src={getFileURL(avatar_url)} <SuspendedUserIcon className="h-4 w-4 text-custom-text-400" />
className="absolute left-0 top-0 h-full w-full rounded-full object-cover" ) : (
alt={display_name || email} <img
/> src={getFileURL(avatar_url)}
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
alt={display_name || email}
/>
)}
</span> </span>
</Link> </Link>
) : ( ) : (
<Link href={`/${workspaceSlug}/profile/${id}`}> <Link href={`/${workspaceSlug}/profile/${id}`}>
<span className="relative flex h-4 w-4 text-xs items-center justify-center rounded-full bg-gray-700 capitalize text-white"> <span
{(email ?? display_name ?? "?")[0]} className={cn(
"relative flex h-4 w-4 text-xs items-center justify-center rounded-full capitalize text-white",
isSuspended ? "bg-custom-background-80" : "bg-gray-700"
)}
>
{isSuspended ? (
<SuspendedUserIcon className="h-4 w-4 text-custom-text-400" />
) : (
(email ?? display_name ?? "?")[0]
)}
</span> </span>
</Link> </Link>
)} )}
{first_name} {last_name} <span className={isSuspended ? "text-custom-text-400" : ""}>
{first_name} {last_name}
</span>
</div> </div>
{(isAdmin || id === currentUser?.id) && ( {!isSuspended && (isAdmin || id === currentUser?.id) && (
<PopoverMenu <PopoverMenu
data={[""]} data={[""]}
keyExtractor={(item) => item} keyExtractor={(item) => item}
@ -108,10 +127,17 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
const isCurrentUser = currentUser?.id === rowData.member.id; const isCurrentUser = currentUser?.id === rowData.member.id;
const isAdminRole = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const isAdminRole = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const isRoleNonEditable = isCurrentUser || !isAdminRole; const isRoleNonEditable = isCurrentUser || !isAdminRole;
const isSuspended = rowData.is_active === false;
return ( return (
<> <>
{isRoleNonEditable ? ( {isSuspended ? (
<div className="w-32 flex ">
<Pill variant={EPillVariant.DEFAULT} size={EPillSize.SM} className="border-none">
Suspended
</Pill>
</div>
) : isRoleNonEditable ? (
<div className="w-32 flex "> <div className="w-32 flex ">
<span>{ROLE[rowData.role]}</span> <span>{ROLE[rowData.role]}</span>
</div> </div>

View File

@ -53,7 +53,13 @@ export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }>
const filteredMemberIds = workspaceSlug ? getFilteredWorkspaceMemberIds(workspaceSlug.toString()) : []; const filteredMemberIds = workspaceSlug ? getFilteredWorkspaceMemberIds(workspaceSlug.toString()) : [];
const searchedMemberIds = searchQuery ? getSearchedWorkspaceMemberIds(searchQuery) : filteredMemberIds; const searchedMemberIds = searchQuery ? getSearchedWorkspaceMemberIds(searchQuery) : filteredMemberIds;
const searchedInvitationsIds = getSearchedWorkspaceInvitationIds(searchQuery); const searchedInvitationsIds = getSearchedWorkspaceInvitationIds(searchQuery);
const memberDetails = searchedMemberIds?.map((memberId) => getWorkspaceMemberDetails(memberId)); const memberDetails = searchedMemberIds
?.map((memberId) => getWorkspaceMemberDetails(memberId))
.sort((a, b) => {
if (a?.is_active && !b?.is_active) return -1;
if (!a?.is_active && b?.is_active) return 1;
return 0;
});
return ( return (
<> <>

View File

@ -37,8 +37,15 @@ export const getMemberSortKey = (memberDetails: IUserLite, field: string, member
} }
case "email": case "email":
return memberDetails.email?.toLowerCase() || ""; return memberDetails.email?.toLowerCase() || "";
case "joining_date": case "joining_date": {
return memberDetails.joining_date ? new Date(memberDetails.joining_date) : new Date(NaN); if (!memberDetails.joining_date) {
// Return a very old date for missing dates to sort them last
return new Date(0);
}
const date = new Date(memberDetails.joining_date);
// Return a very old date for invalid dates to sort them last
return isNaN(date.getTime()) ? new Date(0) : date;
}
case "role": case "role":
return (memberRole ?? "").toString().toLowerCase(); return (memberRole ?? "").toString().toLowerCase();
default: default:
@ -59,7 +66,7 @@ export const filterProjectMembersByRole = (
}); });
}; };
export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPermissions }>( export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPermissions; is_active?: boolean }>(
members: T[], members: T[],
roleFilters: string[] roleFilters: string[]
): T[] => { ): T[] => {
@ -67,7 +74,20 @@ export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPer
return members.filter((member) => { return members.filter((member) => {
const memberRole = String(member.role ?? ""); const memberRole = String(member.role ?? "");
return roleFilters.includes(memberRole); const isSuspended = member.is_active === false;
// Check if suspended is in the role filters
const hasSuspendedFilter = roleFilters.includes("suspended");
// Get non-suspended role filters
const activeRoleFilters = roleFilters.filter((role) => role !== "suspended");
// For suspended users, include them only if suspended filter is selected
if (isSuspended) {
return hasSuspendedFilter;
}
// For active users, include them only if their role matches any active role filter
return activeRoleFilters.includes(memberRole);
}); });
}; };
@ -100,10 +120,15 @@ export const sortMembers = <T>(
let comparison = 0; let comparison = 0;
if (field === "joining_date") { if (field === "joining_date") {
// For dates, we need to handle Date objects // For dates, we need to handle Date objects and ensure they're valid
const aDate = aValue instanceof Date ? aValue : new Date(aValue); const aDate = aValue instanceof Date ? aValue : new Date(aValue);
const bDate = bValue instanceof Date ? bValue : new Date(bValue); const bDate = bValue instanceof Date ? bValue : new Date(bValue);
comparison = aDate.getTime() - bDate.getTime();
// Handle invalid dates by treating them as very old dates
const aTime = isNaN(aDate.getTime()) ? 0 : aDate.getTime();
const bTime = isNaN(bDate.getTime()) ? 0 : bDate.getTime();
comparison = aTime - bTime;
} else { } else {
// For strings, use localeCompare for proper alphabetical sorting // For strings, use localeCompare for proper alphabetical sorting
const aStr = String(aValue); const aStr = String(aValue);
@ -139,13 +164,12 @@ export const sortProjectMembers = (
); );
}; };
export const sortWorkspaceMembers = <T extends { role: string | EUserPermissions }>( export const sortWorkspaceMembers = <T extends { role: string | EUserPermissions; is_active?: boolean }>(
members: T[], members: T[],
memberDetailsMap: Record<string, IUserLite>, memberDetailsMap: Record<string, IUserLite>,
getMemberKey: (member: T) => string, getMemberKey: (member: T) => string,
filters?: IMemberFilters filters?: IMemberFilters
): T[] => { ): T[] => {
// Apply role filtering first
const filteredMembers = const filteredMembers =
filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members; filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members;

View File

@ -53,6 +53,7 @@ export interface IWorkspaceMemberStore {
data: Partial<IWorkspaceMemberInvitation> data: Partial<IWorkspaceMemberInvitation>
) => Promise<void>; ) => Promise<void>;
deleteMemberInvitation: (workspaceSlug: string, invitationId: string) => Promise<void>; deleteMemberInvitation: (workspaceSlug: string, invitationId: string) => Promise<void>;
isUserSuspended: (userId: string, workspaceSlug: string) => boolean;
} }
export class WorkspaceMemberStore implements IWorkspaceMemberStore { export class WorkspaceMemberStore implements IWorkspaceMemberStore {
@ -126,9 +127,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
(m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(), (m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(),
]); ]);
//filter out bots //filter out bots
const memberIds = members const memberIds = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot).map((m) => m.member);
.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot)
.map((m) => m.member);
return memberIds; return memberIds;
}); });
@ -139,7 +138,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => { getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {}); let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
//filter out bots and inactive members //filter out bots and inactive members
members = members.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot); members = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot);
// Use filters store to get filtered member ids // Use filters store to get filtered member ids
const memberIds = this.filtersStore.getFilteredMemberIds( const memberIds = this.filtersStore.getFilteredMemberIds(
@ -350,4 +349,10 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
); );
}); });
}); });
isUserSuspended = computedFn((userId: string, workspaceSlug: string) => {
if (!workspaceSlug) return false;
const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[userId];
return workspaceMember?.is_active === false;
});
} }

View File

@ -56,3 +56,4 @@ export * from "./ai-icon";
export * from "./plane-icon"; export * from "./plane-icon";
export * from "./wiki-icon"; export * from "./wiki-icon";
export * from "./brand"; export * from "./brand";
export * from "./suspended-user";

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const SuspendedUserIcon: React.FC<ISvgIcons> = ({ className, ...rest }) => (
<svg viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...rest}>
<g clipPath="url(#clip0_806_120890)">
<path
d="M3 13C3 12.304 3.18158 11.6201 3.52681 11.0158C3.87204 10.4115 4.36897 9.90774 4.9685 9.55428C5.56802 9.20082 6.24939 9.00989 6.94529 9.00037C7.64119 8.99086 8.32753 9.16307 8.9365 9.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7 9C8.38071 9 9.5 7.88071 9.5 6.5C9.5 5.11929 8.38071 4 7 4C5.61929 4 4.5 5.11929 4.5 6.5C4.5 7.88071 5.61929 9 7 9Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M10.5 11L13 13.5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13 11L10.5 13.5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_806_120890">
<path
d="M2 4.5C2 3.39543 2.89543 2.5 4 2.5H12C13.1046 2.5 14 3.39543 14 4.5V12.5C14 13.6046 13.1046 14.5 12 14.5H4C2.89543 14.5 2 13.6046 2 12.5V4.5Z"
fill="white"
/>
</clipPath>
</defs>
</svg>
);

View File

@ -1,2 +1,2 @@
export { Pill } from "./pill"; export { Pill, EPillVariant, EPillSize } from "./pill";
export type { PillProps } from "./pill"; export type { PillProps } from "./pill";

View File

@ -14,6 +14,7 @@ export enum EPillSize {
SM = "sm", SM = "sm",
MD = "md", MD = "md",
LG = "lg", LG = "lg",
XS = "xs",
} }
export type TPillVariant = export type TPillVariant =
@ -23,7 +24,7 @@ export type TPillVariant =
| EPillVariant.WARNING | EPillVariant.WARNING
| EPillVariant.ERROR | EPillVariant.ERROR
| EPillVariant.INFO; | EPillVariant.INFO;
export type TPillSize = EPillSize.SM | EPillSize.MD | EPillSize.LG; export type TPillSize = EPillSize.SM | EPillSize.MD | EPillSize.LG | EPillSize.XS;
export interface PillProps extends React.HTMLAttributes<HTMLSpanElement> { export interface PillProps extends React.HTMLAttributes<HTMLSpanElement> {
variant?: TPillVariant; variant?: TPillVariant;
@ -42,6 +43,7 @@ const pillVariants = {
}; };
const pillSizes = { const pillSizes = {
[EPillSize.XS]: "px-1.5 py-0.5 text-xs",
[EPillSize.SM]: "px-2 py-0.5 text-xs", [EPillSize.SM]: "px-2 py-0.5 text-xs",
[EPillSize.MD]: "px-2.5 py-1 text-sm", [EPillSize.MD]: "px-2.5 py-1 text-sm",
[EPillSize.LG]: "px-3 py-1.5 text-base", [EPillSize.LG]: "px-3 py-1.5 text-base",