Components: add more types, components and fixes (#591)

* changes

* fixes

* more fixes

* even more changes
This commit is contained in:
prism 2025-12-23 23:28:02 +01:00 • committed by GitHub
parent 74c095975e
commit f3d63106c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 634 additions and 221 deletions

View file

@ -25,6 +25,7 @@ import ClickableTab from "./tabs/ClickableTab";
import ColorPickerTab from "./tabs/ColorPickerTab";
import ComboboxTab from "./tabs/ComboboxTab";
import DividerTab from "./tabs/DividerTab";
import FocusLockTab from "./tabs/FocusLockTab";
import GuildIconTab from "./tabs/GuildIconTab";
import HeadingTab from "./tabs/HeadingTab";
import LayerModalTab from "./tabs/LayerModalTab";
@ -32,6 +33,7 @@ import ListboxTab from "./tabs/ListboxTab";
import ModalTab from "./tabs/ModalTab";
import NoticeTab from "./tabs/NoticeTab";
import PaginatorTab from "./tabs/PaginatorTab";
import PopoutTab from "./tabs/PopoutTab";
import PopoverTab from "./tabs/PopoverTab";
import ProgressBarTab from "./tabs/ProgressBarTab";
import RadioGroupTab from "./tabs/RadioGroupTab";
@ -51,6 +53,7 @@ import TimestampTab from "./tabs/TimestampTab";
import ToastTab from "./tabs/ToastTab";
import TooltipTab from "./tabs/TooltipTab";
import TypographyTab from "./tabs/TypographyTab";
import UserSummaryItemTab from "./tabs/UserSummaryItemTab";
const TABS = [
@ -68,6 +71,7 @@ const TABS = [
{ id: "colorpicker", label: "ColorPicker" },
{ id: "combobox", label: "Combobox" },
{ id: "divider", label: "Divider" },
{ id: "focuslock", label: "FocusLock" },
{ id: "guildicon", label: "GuildIcon" },
{ id: "heading", label: "Heading" },
{ id: "layermodal", label: "LayerModal" },
@ -75,6 +79,7 @@ const TABS = [
{ id: "modal", label: "Modal" },
{ id: "notice", label: "Notice" },
{ id: "paginator", label: "Paginator" },
{ id: "popout", label: "Popout" },
{ id: "popover", label: "Popover" },
{ id: "progressbar", label: "ProgressBar" },
{ id: "radiogroup", label: "RadioGroup" },
@ -94,6 +99,7 @@ const TABS = [
{ id: "toast", label: "Toast" },
{ id: "tooltip", label: "Tooltip" },
{ id: "typography", label: "Typography" },
{ id: "usersummaryitem", label: "UserSummaryItem" },
].sort((a, b) => a.label.localeCompare(b.label));
@ -114,6 +120,7 @@ const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
colorpicker: ColorPickerTab,
combobox: ComboboxTab,
divider: DividerTab,
focuslock: FocusLockTab,
guildicon: GuildIconTab,
heading: HeadingTab,
layermodal: LayerModalTab,
@ -121,6 +128,7 @@ const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
modal: ModalTab,
notice: NoticeTab,
paginator: PaginatorTab,
popout: PopoutTab,
popover: PopoverTab,
progressbar: ProgressBarTab,
radiogroup: RadioGroupTab,
@ -140,6 +148,7 @@ const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
toast: ToastTab,
tooltip: TooltipTab,
typography: TypographyTab,
usersummaryitem: UserSummaryItemTab,
};
function ComponentsTab() {

View file

@ -9,43 +9,31 @@ import { Card } from "@components/Card";
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { proxyLazy } from "@utils/lazy";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findLazy } from "@webpack";
import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findLazy, mapMangledModuleLazy } from "@webpack";
import {
GuildStore,
ListScrollerAuto,
ListScrollerNone,
ListScrollerThin,
OAuth2AuthorizeModal,
ScrollerAuto,
ScrollerNone,
ScrollerThin,
SearchableSelect,
TabBar,
TooltipContainer,
useEffect,
useRef,
UserStore,
useState,
} from "@webpack/common";
export {
Button,
Card,
GuildStore,
Heading,
Paragraph,
ScrollerAuto,
ScrollerNone,
ScrollerThin,
TabBar,
useEffect,
useRef,
UserStore,
useState,
};
export * from "../constants";
import {
import { ToastPosition, ToastType } from "../constants";
import type {
AccordionProps,
AnchorProps,
AvatarProps,
BadgeShapesType,
CheckboxGroupOption,
ChipProps,
CircleBadgeProps,
ClickableProps,
@ -58,11 +46,9 @@ import {
DiscordHeadingProps,
DiscordTextProps,
ExpressiveModalProps,
GradientColor,
FocusLockProps,
GuildIconProps,
HeadingVariant,
IconBadgeProps,
ListboxItem,
ManaBaseRadioGroupProps,
ManaButtonProps,
ManaCalendarProps,
@ -73,137 +59,61 @@ import {
ManaListboxProps,
ManaPopoverProps,
ManaRichTooltipProps,
ManaSelectOption,
ManaSelectProps,
ManaSwitchProps,
ManaTextAreaProps,
ManaTextButtonProps,
ManaTextInputProps,
ManaTooltipProps,
ModalAction,
ModalNotice,
ModalProps,
ModalRenderFn,
NoticeProps,
NoticeType,
NumberBadgeProps,
OpenModalOptions,
PaginatorProps,
PopoverAction,
PopoutComponent,
ProgressBarProps,
RadioOption,
SearchBarProps,
SkeletonProps,
SliderProps,
SpinnerComponent,
SpinnerProps,
StandaloneRadioIndicatorProps,
TabBarComponent,
TabBarHeaderProps,
TabBarItemProps,
TabBarProps,
TabBarSeparatorProps,
TextBadgeProps,
TextColor,
TextVariant,
TimestampProps,
ToastData,
ToastOptions,
ToastPositionValue,
ToastsModule,
ToastTypeValue,
UserSummaryItemProps,
} from "../types";
export type {
AccordionProps,
AnchorProps,
AvatarProps,
BadgeShapesType,
CheckboxGroupOption,
ChipProps,
CircleBadgeProps,
ColorPickerProps,
ColorPickerWithSwatchesProps,
ColorSwatchProps,
ConfirmModalProps,
CustomColorButtonProps,
DefaultColorButtonProps,
DiscordHeadingProps,
DiscordTextProps,
ExpressiveModalProps,
GradientColor,
GuildIconProps,
HeadingVariant,
IconBadgeProps,
ListboxItem,
ManaBaseRadioGroupProps,
ManaButtonProps,
ManaCalendarProps,
ManaCheckboxGroupProps,
ManaCheckboxProps,
ManaComboboxProps,
ManaDatePickerProps,
ManaListboxProps,
ManaPopoverProps,
ManaRichTooltipProps,
ManaSelectOption,
ManaSelectProps,
ManaSwitchProps,
ManaTextAreaProps,
ManaTextButtonProps,
ManaTextInputProps,
ManaTooltipProps,
ModalAction,
ModalNotice,
ModalProps,
ModalRenderFn,
NoticeProps,
NoticeType,
NumberBadgeProps,
OpenModalOptions,
PaginatorProps,
PopoverAction,
ProgressBarProps,
RadioOption,
SearchBarProps,
SkeletonProps,
SliderProps,
SpinnerProps,
StandaloneRadioIndicatorProps,
TabBarComponent,
TabBarHeaderProps,
TabBarItemProps,
TabBarProps,
TabBarSeparatorProps,
TextBadgeProps,
TextColor,
TextVariant,
TimestampProps,
ToastData,
ToastOptions,
ToastPositionValue,
ToastsModule,
ToastTypeValue,
export * from "../constants";
export type * from "../types";
export {
Button,
Card,
GuildStore,
Heading,
ListScrollerAuto,
ListScrollerNone,
ListScrollerThin,
OAuth2AuthorizeModal,
Paragraph,
ScrollerAuto,
ScrollerNone,
ScrollerThin,
SearchableSelect,
TabBar,
TooltipContainer,
useEffect,
useRef,
UserStore,
useState,
};
export const ToastType = {
MESSAGE: "message",
SUCCESS: "success",
FAILURE: "failure",
CUSTOM: "custom",
CLIP: "clip",
LINK: "link",
FORWARD: "forward",
BOOKMARK: "bookmark",
CLOCK: "clock",
} as const;
export const ToastPosition = {
TOP: 0,
BOTTOM: 1,
} as const;
export const Anchor = findComponentByCodeLazy("anchorUnderlineOnHover", "useDefaultUnderlineStyles") as React.ComponentType<AnchorProps>;
export const Clickable = findByCodeLazy("ignoreKeyPress", "renderNonInteractive") as React.ComponentType<ClickableProps>;
export const ManaButton = findComponentByCodeLazy('"data-mana-component":"button"') as React.ComponentType<ManaButtonProps>;
export const ManaTextButton = findComponentByCodeLazy('"data-mana-component":"text-button"') as React.ComponentType<ManaTextButtonProps>;
@ -212,25 +122,38 @@ export const ManaTextInput = findComponentByCodeLazy('"data-mana-component":"tex
export const ManaTextArea = findComponentByCodeLazy('"data-mana-component":"text-area"') as React.ComponentType<ManaTextAreaProps>;
export const ManaCheckbox = findComponentByCodeLazy('"data-mana-component":"checkbox"') as React.ComponentType<ManaCheckboxProps>;
export const ManaSelect = findComponentByCodeLazy('"data-mana-component":"select"') as React.ComponentType<ManaSelectProps>;
export const ManaTooltip = findComponentByCodeLazy("VoidTooltip cannot find DOM node") as React.ComponentType<ManaTooltipProps>;
export const ManaCalendar = findComponentByCodeLazy('"data-mana-component":"calendar"') as React.ComponentType<ManaCalendarProps>;
export const ManaDatePicker = findComponentByCodeLazy('"data-mana-component":"date-picker"') as React.ComponentType<ManaDatePickerProps>;
export const ManaRichTooltip = findComponentByCodeLazy('"data-mana-component":"rich-tooltip"') as React.ComponentType<ManaRichTooltipProps>;
export const ManaCombobox = findComponentByCodeLazy("itemToString", "multiSelect", "maxVisibleItems") as React.ComponentType<ManaComboboxProps>;
export const ManaListbox = findComponentByCodeLazy('"data-mana-component":"listbox"') as React.ComponentType<ManaListboxProps>;
export const ManaCheckboxGroup = findComponentByCodeLazy('"data-mana-component":"checkbox-group"') as React.ComponentType<ManaCheckboxGroupProps>;
export const ManaBaseRadioGroup = findComponentByCodeLazy('"data-mana-component":"BaseRadioGroup"') as React.ComponentType<ManaBaseRadioGroupProps>;
export const ManaCombobox = findComponentByCodeLazy("itemToString", "multiSelect", "maxVisibleItems") as React.ComponentType<ManaComboboxProps>;
export const ManaTooltip = findComponentByCodeLazy("VoidTooltip cannot find DOM node") as React.ComponentType<ManaTooltipProps>;
export const ManaRichTooltip = findComponentByCodeLazy('"data-mana-component":"rich-tooltip"') as React.ComponentType<ManaRichTooltipProps>;
export const ManaPopover = findByCodeLazy("title", "body", "badge", "graphic", "actions", "textLink", "gradientColor", "popoverRef") as React.ComponentType<ManaPopoverProps>;
export const StandaloneRadioIndicator = findComponentByCodeLazy("standaloneRadioIndicator", "animateIn", "animateOut") as React.ComponentType<StandaloneRadioIndicatorProps>;
export const ManaPopover = findComponentByCodeLazy('"data-mana-component":"popover"') as React.ComponentType<ManaPopoverProps>;
export const Slider = findComponentByCodeLazy("stickToMarkers", "onMarkerRender", "grabberClassName") as React.ComponentType<SliderProps>;
export const Avatar = findComponentByCodeLazy("statusTooltip", "statusBackdropColor", "isSpeaking") as React.ComponentType<AvatarProps>;
export const ProgressBar = findComponentByCodeLazy("progressContainer", "labelledBy", "aria-valuenow") as React.ComponentType<ProgressBarProps>;
export const Spinner = findComponentByCodeLazy("spinningCircleSimple", "pulsingEllipsis", "wanderingCubes") as unknown as SpinnerComponent;
export const SearchBar = findComponentByCodeLazy("#{intl::SEARCH}", "clearable", "autoComplete") as React.ComponentType<SearchBarProps>;
export const Paginator2 = findComponentByCodeLazy("#{intl::BACK}", "#{intl::NEXT}", "renderPageWrapper") as React.ComponentType<PaginatorProps>;
export const Paginator = findComponentByCodeLazy("#{intl::BACK}", "#{intl::NEXT}", "renderPageWrapper") as React.ComponentType<PaginatorProps>;
export const Notice = findComponentByCodeLazy("messageType", "iconDiv", "actionContainer") as React.ComponentType<NoticeProps>;
export const Chip = findComponentByCodeLazy('variant:"eyebrow"', "chip,") as React.ComponentType<ChipProps>;
export const Skeleton = findComponentByCodeLazy("productSkeletonCardContainer", "skipPulseAnimation") as React.ComponentType<SkeletonProps>;
export const Accordion = findComponentByCodeLazy("accordionContainer", "onExpandedChange", "defaultExpanded") as React.ComponentType<AccordionProps>;
export const Timestamp = findComponentByCodeLazy("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}", "isVisibleOnlyOnHover") as React.ComponentType<TimestampProps>;
export const GuildIcon = findComponentByCodeLazy("Masks.CLAN_ICON", "guildIconImage") as React.ComponentType<GuildIconProps>;
export const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", "showEyeDropper") as React.ComponentType<ColorPickerProps>;
export const Popout: PopoutComponent = proxyLazy(() => findLazy(m => m?.y?.Animation && m.y.toString?.().includes("renderPopout")).y);
export const FocusLock = findComponentByCodeLazy(".containerRef,{keyboardModeEnabled:") as React.ComponentType<FocusLockProps>;
export const UserSummaryItem = findComponentByCodeLazy("popoutUserId") as React.ComponentType<UserSummaryItemProps>;
export const Animations = mapMangledModuleLazy(".assign({colorNames:", {
Transition: filters.componentByCode('["items","children"]', ",null,"),
animated: filters.byProps("div", "text")
});
const ColorPickerModule = findLazy(m => {
const values = Object.values(m);
@ -273,23 +196,28 @@ function getBadgeExport(check: (fn: Function) => boolean): React.ComponentType<a
return (() => null) as React.ComponentType<any>;
}
export const NumberBadge: React.ComponentType<NumberBadgeProps> = proxyLazy(() => getBadgeExport(fn => fn.toString().includes("numberBadge")));
export const TextBadge: React.ComponentType<TextBadgeProps> = proxyLazy(() => getBadgeExport(fn => fn.toString().includes("textBadge")));
export const IconBadge: React.ComponentType<IconBadgeProps> = proxyLazy(() => getBadgeExport(fn => fn.toString().includes("iconBadge")));
export const CircleBadge: React.ComponentType<CircleBadgeProps> = proxyLazy(() => getBadgeExport(fn => fn.toString().includes("disableColor") && !fn.toString().includes("iconBadge") && !fn.toString().includes("textBadge") && !fn.toString().includes("numberBadge") && !fn.toString().includes("premiumBadge")));
export const BadgeShapes = proxyLazy(() => Object.values(BadgeModule).find(v => typeof v === "object" && (v as BadgeShapesType)?.ROUND) as BadgeShapesType ?? { ROUND: "", ROUND_LEFT: "", ROUND_RIGHT: "", SQUARE: "" });
export const Clickable = findByCodeLazy("ignoreKeyPress", "renderNonInteractive") as React.ComponentType<ClickableProps>;
export const Chip = findComponentByCodeLazy('variant:"eyebrow"', "chip,") as React.ComponentType<ChipProps>;
export const Skeleton = findComponentByCodeLazy("productSkeletonCardContainer", "skipPulseAnimation") as React.ComponentType<SkeletonProps>;
export const Accordion = findComponentByCodeLazy("accordionContainer", "onExpandedChange", "defaultExpanded") as React.ComponentType<AccordionProps>;
export const Timestamp = findComponentByCodeLazy("isVisibleOnlyOnHover", "cozyAlt", "timestampFormat") as React.ComponentType<TimestampProps>;
export const GuildIcon = findComponentByCodeLazy("getGuildIconURL", "acronym") as React.ComponentType<GuildIconProps>;
export const NumberBadge: React.ComponentType<NumberBadgeProps> = proxyLazy(() =>
getBadgeExport(fn => fn.toString().includes("numberBadge"))
);
export const TextBadge: React.ComponentType<TextBadgeProps> = proxyLazy(() =>
getBadgeExport(fn => fn.toString().includes("textBadge"))
);
export const IconBadge: React.ComponentType<IconBadgeProps> = proxyLazy(() =>
getBadgeExport(fn => fn.toString().includes("iconBadge"))
);
export const CircleBadge: React.ComponentType<CircleBadgeProps> = proxyLazy(() =>
getBadgeExport(fn =>
fn.toString().includes("disableColor") &&
!fn.toString().includes("iconBadge") &&
!fn.toString().includes("textBadge") &&
!fn.toString().includes("numberBadge") &&
!fn.toString().includes("premiumBadge")
)
);
export const BadgeShapes = proxyLazy(() =>
Object.values(BadgeModule).find(v => typeof v === "object" && (v as BadgeShapesType)?.ROUND) as BadgeShapesType ??
{ ROUND: "", ROUND_LEFT: "", ROUND_RIGHT: "", SQUARE: "" }
);
const ToastsModule = findByPropsLazy("showToast", "popToast");
export const Toasts: ToastsModule = proxyLazy(() => ({
@ -300,7 +228,9 @@ export const Toasts: ToastsModule = proxyLazy(() => ({
pop: () => ToastsModule.popToast(),
create: (message: string, type: ToastTypeValue, options?: ToastOptions) => ToastsModule.createToast(message, type, options),
}));
export const showToast = (message: string, type: ToastTypeValue = ToastType.MESSAGE) => ToastsModule.showToast(ToastsModule.createToast(message, type));
export const showToast = (message: string, type: ToastTypeValue = ToastType.MESSAGE) =>
ToastsModule.showToast(ToastsModule.createToast(message, type));
const ModalModule = findByPropsLazy("Modal", "ConfirmModal");
const ModalAPIModule = findByPropsLazy("openModal", "closeModal");
@ -309,20 +239,25 @@ export const Modal: React.ComponentType<ModalProps> = proxyLazy(() => ModalModul
export const ConfirmModal: React.ComponentType<ConfirmModalProps> = proxyLazy(() => ModalModule.ConfirmModal);
export const ExpressiveModal: React.ComponentType<ExpressiveModalProps> = proxyLazy(() => ModalModule.ExpressiveModal);
export const openModal = (render: ModalRenderFn, options?: OpenModalOptions): string => ModalAPIModule.openModal(render, options);
export const closeModal = (modalKey: string): void => ModalAPIModule.closeModal(modalKey);
export const closeAllModals = (): void => ModalAPIModule.closeAllModals();
export const hasModalOpen = (modalKey: string): boolean => ModalAPIModule.hasModalOpen(modalKey);
export const hasAnyModalOpen = (): boolean => ModalAPIModule.hasAnyModalOpen();
export const openModal = (render: ModalRenderFn, options?: OpenModalOptions): string =>
ModalAPIModule.openModal(render, options);
export const closeModal = (modalKey: string): void =>
ModalAPIModule.closeModal(modalKey);
export const closeAllModals = (): void =>
ModalAPIModule.closeAllModals();
export const hasModalOpen = (modalKey: string): boolean =>
ModalAPIModule.hasModalOpen(modalKey);
export const hasAnyModalOpen = (): boolean =>
ModalAPIModule.hasAnyModalOpen();
const DiscordTextModule = findByPropsLazy("Heading", "Text");
export function DiscordHeading(props: DiscordHeadingProps) {
const Component = DiscordTextModule.Heading;
return <Component {...props} />;
return <DiscordTextModule.Heading {...props} />;
}
export function DiscordText(props: DiscordTextProps) {
const Component = DiscordTextModule.Text;
return <Component {...props} />;
return <DiscordTextModule.Text {...props} />;
}
export function Divider({ className, style, ...restProps }: React.ComponentPropsWithoutRef<"hr">) {

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, Paragraph,TextBadge } from "..";
import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, Paragraph, TextBadge } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";
@ -84,7 +84,7 @@ export default function BadgeTab() {
<SectionWrapper title="Props">
<Paragraph color="text-muted">
NumberBadge: count, maxDigits?, color?, disableColor?, shape?, className?, style?
NumberBadge: count, color?, disableColor?, shape?, className?, style?, renderBadgeCount?
</Paragraph>
<Paragraph color="text-muted">
TextBadge: text, color?, disableColor?, shape?, className?, style?

View file

@ -4,31 +4,35 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Chip , Paragraph } from "..";
import { Chip, Paragraph } from "..";
import { SectionWrapper } from "../SectionWrapper";
const CHIP_COLORS = ["blurple", "purple", "green", "orange", "yellow", "pink", "red", "gray"] as const;
const CHIP_SHADES = ["Light", "Medium", "Dark"] as const;
export default function ChipTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Colors">
<div className="vc-compfinder-grid">
<Chip text="Blurple" variant="blurpleMedium" />
<Chip text="Purple" variant="purpleMedium" />
<Chip text="Green" variant="greenMedium" />
<Chip text="Orange" variant="orangeMedium" />
<Chip text="Yellow" variant="yellowMedium" />
<Chip text="Pink" variant="pinkMedium" />
<Chip text="Red" variant="redMedium" />
<Chip text="Gray" variant="grayMedium" />
</div>
</SectionWrapper>
<SectionWrapper title="Shades">
<div className="vc-compfinder-grid">
<Chip text="Light" variant="blurpleLight" />
<Chip text="Medium" variant="blurpleMedium" />
<Chip text="Dark" variant="blurpleDark" />
</div>
<SectionWrapper title="All Variants">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
All 24 chip variants: 8 colors × 3 shades.
</Paragraph>
{CHIP_SHADES.map(shade => (
<div key={shade} style={{ marginBottom: 16 }}>
<Paragraph color="text-muted" style={{ marginBottom: 8, fontWeight: 600 }}>
{shade}
</Paragraph>
<div className="vc-compfinder-grid">
{CHIP_COLORS.map(color => (
<Chip
key={`${color}${shade}`}
text={color}
variant={`${color}${shade}` as any}
/>
))}
</div>
</div>
))}
</SectionWrapper>
<SectionWrapper title="Use Cases">
@ -49,6 +53,15 @@ export default function ChipTab() {
<Paragraph color="text-muted">
• variant?: ChipVariant - Color variant (default: "grayLight")
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 8 }}>
<strong>Variant Format:</strong> {"{color}{shade}"} where:
</Paragraph>
<Paragraph color="text-muted">
• Colors: blurple, purple, green, orange, yellow, pink, red, gray
</Paragraph>
<Paragraph color="text-muted">
• Shades: Light, Medium, Dark
</Paragraph>
</SectionWrapper>
</div>
);

View file

@ -0,0 +1,86 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { FocusLock, ManaButton, ManaTextInput, Paragraph, useRef, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function FocusLockTab() {
const [focusLockEnabled, setFocusLockEnabled] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [inputValue, setInputValue] = useState("");
return (
<div className="vc-compfinder-section">
<SectionWrapper title="FocusLock Demo">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
FocusLock traps keyboard focus within a container. Enable it and try tabbing - focus stays inside the box.
</Paragraph>
<ManaButton
variant={focusLockEnabled ? "primary" : "secondary"}
text={focusLockEnabled ? "Disable Focus Lock" : "Enable Focus Lock"}
onClick={() => setFocusLockEnabled(!focusLockEnabled)}
style={{ marginBottom: 16 }}
/>
<div
ref={containerRef}
style={{
padding: 16,
border: `2px solid ${focusLockEnabled ? "var(--brand-500)" : "var(--background-modifier-accent)"}`,
borderRadius: 8,
background: "var(--background-secondary)",
}}
>
{focusLockEnabled && (
<FocusLock containerRef={containerRef}>
<Paragraph style={{ marginBottom: 12 }}>
Focus is now trapped! Tab between these elements:
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<ManaTextInput
value={inputValue}
onChange={setInputValue}
placeholder="First input"
/>
<ManaTextInput
value=""
onChange={() => { }}
placeholder="Second input"
/>
<div style={{ display: "flex", gap: 8 }}>
<ManaButton variant="secondary" text="Button 1" onClick={() => { }} />
<ManaButton variant="secondary" text="Button 2" onClick={() => { }} />
<ManaButton
variant="primary"
text="Exit Focus Lock"
onClick={() => setFocusLockEnabled(false)}
/>
</div>
</div>
</FocusLock>
)}
{!focusLockEnabled && (
<Paragraph color="text-muted">
Click "Enable Focus Lock" to activate the focus trap demo.
</Paragraph>
)}
</div>
</SectionWrapper>
<SectionWrapper title="Use Cases">
<Paragraph color="text-muted">• Modal dialogs - Keep focus within the modal</Paragraph>
<Paragraph color="text-muted">• Dropdown menus - Trap focus while open</Paragraph>
<Paragraph color="text-muted">• Settings panels - Improve keyboard navigation</Paragraph>
<Paragraph color="text-muted">• Accessibility - Ensure screen readers stay in context</Paragraph>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• containerRef - React ref to the container element</Paragraph>
<Paragraph color="text-muted">• keyboardModeEnabled - Enable keyboard focus trapping</Paragraph>
<Paragraph color="text-muted">• children - Content to render inside the focus trap</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paginator2 as Paginator, Paragraph, useState } from "..";
import { Paginator, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function PaginatorTab() {

View file

@ -0,0 +1,124 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Clickable, ManaButton, Paragraph, Popout, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function PopoutTab() {
const [showBasic, setShowBasic] = useState(false);
const [showPositions, setShowPositions] = useState<Record<string, boolean>>({});
const positions = ["top", "bottom", "left", "right"] as const;
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Popout">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Click the button to toggle a popout.
</Paragraph>
<Popout
shouldShow={showBasic}
onRequestClose={() => setShowBasic(false)}
renderPopout={() => (
<div style={{ padding: 16, background: "var(--background-floating)", borderRadius: 8 }}>
<Paragraph>This is a popout!</Paragraph>
<ManaButton
variant="secondary"
text="Close"
onClick={() => setShowBasic(false)}
style={{ marginTop: 8 }}
/>
</div>
)}
>
{(props, { isShown }) => (
<Clickable {...props}>
<ManaButton
variant={isShown ? "primary" : "secondary"}
text={isShown ? "Popout Open" : "Open Popout"}
onClick={() => setShowBasic(!showBasic)}
/>
</Clickable>
)}
</Popout>
</SectionWrapper>
<SectionWrapper title="Positions">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Popout can appear in different positions.
</Paragraph>
<div className="vc-compfinder-grid">
{positions.map(position => (
<Popout
key={position}
position={position}
shouldShow={showPositions[position] ?? false}
onRequestClose={() => setShowPositions(p => ({ ...p, [position]: false }))}
renderPopout={() => (
<div style={{ padding: 12, background: "var(--background-floating)", borderRadius: 8 }}>
<Paragraph>Position: {position}</Paragraph>
</div>
)}
>
{(props, { isShown }) => (
<Clickable {...props}>
<ManaButton
variant={isShown ? "primary" : "secondary"}
text={position}
onClick={() => setShowPositions(p => ({ ...p, [position]: !p[position] }))}
/>
</Clickable>
)}
</Popout>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Animation Types">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Popout supports different animation types via Popout.Animation.
</Paragraph>
<div className="vc-compfinder-grid">
{(["NONE", "TRANSLATE", "SCALE", "FADE"] as const).map(anim => (
<Popout
key={anim}
animation={Popout.Animation[anim]}
shouldShow={showPositions[`anim-${anim}`] ?? false}
onRequestClose={() => setShowPositions(p => ({ ...p, [`anim-${anim}`]: false }))}
renderPopout={() => (
<div style={{ padding: 12, background: "var(--background-floating)", borderRadius: 8 }}>
<Paragraph>Animation: {anim}</Paragraph>
</div>
)}
>
{(props, { isShown }) => (
<Clickable {...props}>
<ManaButton
variant={isShown ? "primary" : "secondary"}
text={anim}
onClick={() => setShowPositions(p => ({ ...p, [`anim-${anim}`]: !p[`anim-${anim}`] }))}
/>
</Clickable>
)}
</Popout>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• children - Render function (props, data) =&gt; ReactNode</Paragraph>
<Paragraph color="text-muted">• renderPopout - Render function for popout content</Paragraph>
<Paragraph color="text-muted">• shouldShow - Control visibility</Paragraph>
<Paragraph color="text-muted">• position - top, bottom, left, right, center, window_center</Paragraph>
<Paragraph color="text-muted">• align - left, right, center, top, bottom</Paragraph>
<Paragraph color="text-muted">• animation - Popout.Animation.NONE/TRANSLATE/SCALE/FADE</Paragraph>
<Paragraph color="text-muted">• spacing - Distance from target (default 8)</Paragraph>
<Paragraph color="text-muted">• onRequestOpen/onRequestClose - Callbacks</Paragraph>
<Paragraph color="text-muted">• autoInvert - Auto-flip if near edge (default true)</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -12,48 +12,23 @@ export default function SkeletonTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Default Skeleton">
<SectionWrapper title="Skeleton">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Loading placeholder with pulse animation. Requires "full-motion" parent class for animation.
</Paragraph>
<div className="vc-compfinder-grid full-motion">
<Skeleton />
<Skeleton />
<Skeleton />
</div>
</SectionWrapper>
<SectionWrapper title="Animation Toggle">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Use skipPulseAnimation to disable the pulse effect.
Loading placeholder with pulse animation. Used in Discord's shop for product cards.
</Paragraph>
<ManaButton
variant="secondary"
text={animated ? "Disable Animation" : "Enable Animation"}
onClick={() => setAnimated(!animated)}
style={{ marginBottom: 16 }}
/>
<div className="vc-compfinder-grid full-motion" style={{ marginTop: 16 }}>
<Skeleton skipPulseAnimation={!animated} />
<div className="full-motion">
<Skeleton skipPulseAnimation={!animated} />
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• skipPulseAnimation?: boolean - Disable pulse animation (default: false)
</Paragraph>
</SectionWrapper>
<SectionWrapper title="Notes">
<Paragraph color="text-muted">
• This is a product card skeleton used in Discord's shop.
</Paragraph>
<Paragraph color="text-muted">
• Animation requires a parent with "full-motion" class (Discord's reduced motion setting).
</Paragraph>
<Paragraph color="text-muted">
• The skeleton has fixed dimensions designed for product cards.
</Paragraph>
<Paragraph color="text-muted">• skipPulseAnimation?: boolean - Disable pulse animation</Paragraph>
</SectionWrapper>
</div>
);

View file

@ -14,8 +14,10 @@ const TOAST_TYPES = [
{ type: ToastType.CLIP, label: "CLIP", description: "Clip/copy toast" },
{ type: ToastType.LINK, label: "LINK", description: "Link copied toast" },
{ type: ToastType.FORWARD, label: "FORWARD", description: "Forward action toast" },
{ type: ToastType.INVITE, label: "INVITE", description: "Invite action toast" },
{ type: ToastType.BOOKMARK, label: "BOOKMARK", description: "Bookmark action toast" },
{ type: ToastType.CLOCK, label: "CLOCK", description: "Clock/timer toast" },
{ type: ToastType.AI, label: "AI", description: "AI action toast" },
] as const;
export default function ToastTab() {
@ -108,8 +110,10 @@ export default function ToastTab() {
<Paragraph color="text-muted">• CLIP - Clip/scissors icon</Paragraph>
<Paragraph color="text-muted">• LINK - Link icon</Paragraph>
<Paragraph color="text-muted">• FORWARD - Forward arrow icon</Paragraph>
<Paragraph color="text-muted">• INVITE - Invite icon</Paragraph>
<Paragraph color="text-muted">• BOOKMARK - Bookmark icon</Paragraph>
<Paragraph color="text-muted">• CLOCK - Clock icon</Paragraph>
<Paragraph color="text-muted">• AI - AI icon</Paragraph>
</SectionWrapper>
<SectionWrapper title="ToastPosition">

View file

@ -0,0 +1,92 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph, UserStore, UserSummaryItem } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function UserSummaryItemTab() {
const currentUser = UserStore.getCurrentUser();
const users = currentUser ? [currentUser, currentUser, currentUser, currentUser, currentUser] : [];
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic UserSummaryItem">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Displays a row of user avatars with overflow indicator.
</Paragraph>
<div className="vc-compfinder-grid">
<UserSummaryItem users={users} max={3} />
</div>
</SectionWrapper>
<SectionWrapper title="Different Sizes">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Available sizes: 16, 24, 32, 56.
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 50 }}>16:</Paragraph>
<UserSummaryItem users={users} max={4} size={16} />
</div>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 50 }}>24:</Paragraph>
<UserSummaryItem users={users} max={4} size={24} />
</div>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 50 }}>32:</Paragraph>
<UserSummaryItem users={users} max={4} size={32} />
</div>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 50 }}>56:</Paragraph>
<UserSummaryItem users={users} max={4} size={56} />
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Max Users">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Control how many avatars to show before overflow.
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 80 }}>max=2:</Paragraph>
<UserSummaryItem users={users} max={2} />
</div>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 80 }}>max=3:</Paragraph>
<UserSummaryItem users={users} max={3} />
</div>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 80 }}>max=5:</Paragraph>
<UserSummaryItem users={users} max={5} />
</div>
</div>
</SectionWrapper>
<SectionWrapper title="With Popout">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Enable user popout on avatar click.
</Paragraph>
<div className="vc-compfinder-grid">
<UserSummaryItem users={users} max={3} showUserPopout />
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• users - Array of User objects (or null for empty slots)</Paragraph>
<Paragraph color="text-muted">• max - Maximum avatars to show (default 10)</Paragraph>
<Paragraph color="text-muted">• count - Override count for "+N" display</Paragraph>
<Paragraph color="text-muted">• size - 16, 24, 32, or 56 (default 24)</Paragraph>
<Paragraph color="text-muted">• guildId - Guild context for avatars</Paragraph>
<Paragraph color="text-muted">• showUserPopout - Enable user popout on click</Paragraph>
<Paragraph color="text-muted">• showDefaultAvatarsForNullUsers - Show default avatar for null</Paragraph>
<Paragraph color="text-muted">• renderUser - Custom render function for each user</Paragraph>
<Paragraph color="text-muted">• renderMoreUsers - Custom render for overflow indicator</Paragraph>
<Paragraph color="text-muted">• hideMoreUsers - Hide the "+N" overflow count</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -165,3 +165,22 @@ export type PopoverSize = typeof PopoverSizes[number];
export const SliderOrientations = ["horizontal", "vertical"] as const;
export type SliderOrientation = typeof SliderOrientations[number];
export const ToastType = {
MESSAGE: "message",
SUCCESS: "success",
FAILURE: "failure",
CUSTOM: "custom",
CLIP: "clip",
LINK: "link",
FORWARD: "forward",
INVITE: "invite",
BOOKMARK: "bookmark",
CLOCK: "clock",
AI: "ai",
} as const;
export const ToastPosition = {
TOP: 0,
BOTTOM: 1,
} as const;

View file

@ -20,15 +20,24 @@ export interface AvatarProps {
size?: AvatarSize;
status?: AvatarStatus;
statusColor?: string;
statusBackdropColor?: string;
isMobile?: boolean;
isTyping?: boolean;
isSpeaking?: boolean;
isLatched?: boolean;
voiceDb?: number;
speakingStylesConfig?: Record<string, any>;
statusTooltip?: boolean;
statusTooltipDelay?: number;
avatarDecoration?: string;
typingIndicatorRef?: React.Ref<any>;
avatarContentRef?: React.Ref<any>;
CutoutIcon?: React.ComponentType<any>;
avatarTooltipAsset?: React.ReactNode;
avatarTooltipText?: string;
avatarTooltipTitle?: string;
"aria-hidden"?: boolean;
"aria-label"?: string;
imageClassName?: string;
className?: string;
onClick?: (e: React.MouseEvent) => void;
ref?: React.Ref<SVGSVGElement>;
}

View file

@ -13,12 +13,12 @@ export interface BadgeShapesType {
export interface NumberBadgeProps {
count: number;
maxDigits?: number;
color?: string;
disableColor?: boolean;
shape?: string;
className?: string;
style?: React.CSSProperties;
renderBadgeCount?: (count: number) => string;
}
export interface TextBadgeProps {

View file

@ -18,6 +18,8 @@ export interface ManaCheckboxProps {
leadingIcon?: React.ComponentType<any>;
labelType?: CheckboxLabelType;
usageVariant?: CheckboxUsageVariant;
value?: string | number;
labeledBy?: string;
}
export interface CheckboxGroupOption {

View file

@ -0,0 +1,11 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface FocusLockProps {
containerRef: React.RefObject<HTMLElement | null>;
keyboardModeEnabled?: boolean;
children?: React.ReactNode;
}

View file

@ -15,12 +15,14 @@ export * from "./checkbox";
export * from "./chip";
export * from "./clickable";
export * from "./colorpicker";
export * from "./focuslock";
export * from "./guildicon.d";
export * from "./heading";
export * from "./input";
export * from "./modal";
export * from "./notice";
export * from "./paginator";
export * from "./popout";
export * from "./progress";
export * from "./radio";
export * from "./scroller";
@ -33,3 +35,4 @@ export * from "./tabbar";
export * from "./timestamp";
export * from "./toast";
export * from "./tooltip";
export * from "./usersummary";

View file

@ -63,11 +63,14 @@ export interface ManaTextAreaProps {
disabled?: boolean;
readOnly?: boolean;
rows?: number;
autoFocus?: boolean;
autosize?: boolean;
maxLength?: number;
minLength?: number;
showCharacterCount?: boolean;
showRemainingCharacterCount?: boolean;
error?: string | boolean;
defaultDirty?: boolean;
fullWidth?: boolean;
focusProps?: Record<string, any>;
inputRef?: React.Ref<HTMLTextAreaElement>;

View file

@ -0,0 +1,72 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type PopoutAnimationType = "1" | "2" | "3" | "4";
export type PopoutPosition = "top" | "bottom" | "left" | "right" | "center" | "window_center";
export type PopoutAlign = "left" | "right" | "center" | "top" | "bottom";
export interface PopoutChildrenProps {
"aria-controls": string;
"aria-expanded": boolean;
onClick: (event: React.MouseEvent<HTMLElement>) => void;
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
onMouseDown: (event: React.MouseEvent<HTMLElement>) => void;
}
export interface PopoutChildrenData {
isShown: boolean;
position: PopoutPosition;
}
export interface PopoutRenderProps {
closePopout: () => void;
isPositioned: boolean;
nudge: number;
position: PopoutPosition;
setPopoutRef: (ref: any) => void;
updatePosition: () => void;
}
export interface PopoutProps {
children: (props: PopoutChildrenProps, data: PopoutChildrenData) => React.ReactNode;
renderPopout: (props: PopoutRenderProps) => React.ReactNode;
shouldShow?: boolean;
targetElementRef?: React.RefObject<HTMLElement>;
onRequestOpen?: () => void;
onRequestClose?: () => void;
align?: PopoutAlign;
animation?: PopoutAnimationType;
animationPosition?: PopoutPosition;
autoInvert?: boolean;
nudgeAlignIntoViewport?: boolean;
position?: PopoutPosition;
positionKey?: string;
popoutKey?: string;
spacing?: number;
preload?: boolean;
fixed?: boolean;
useRawTargetDimensions?: boolean;
onShiftClick?: () => void;
disablePointerEvents?: boolean;
ignoreModalClicks?: boolean;
scrollBehavior?: string;
useMouseEnter?: boolean;
layerContext?: any;
clickTrap?: boolean;
}
export interface PopoutAnimationEnum {
NONE: "1";
TRANSLATE: "2";
SCALE: "3";
FADE: "4";
}
export type PopoutComponent = React.ComponentType<PopoutProps> & {
Animation: PopoutAnimationEnum;
};

View file

@ -17,6 +17,7 @@ export interface ManaBaseRadioGroupProps {
value?: string | number;
onChange?: (value: string | number) => void;
disabled?: boolean;
label?: string;
"aria-labelledby"?: string;
}

View file

@ -25,15 +25,22 @@ export interface ManaSelectProps {
shouldFocusWrap?: boolean;
maxOptionsVisible?: number;
wrapTags?: boolean;
formatOption?: (option: ManaSelectOption) => React.ReactNode;
name?: string;
form?: string;
autoComplete?: string;
label?: string;
required?: boolean;
}
export interface ManaComboboxProps {
export interface ManaComboboxProps<T = string> {
placeholder?: string;
value?: string | string[];
onChange?: (value: string | string[]) => void;
value?: T | T[];
onChange?: (value: T | T[]) => void;
multiSelect?: boolean;
autoFocus?: boolean;
maxVisibleItems?: number;
itemToString?: (item: T) => string;
emptyStateText?: string;
emptyStateHeader?: string;
onQueryChange?: (query: string) => void;

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type ToastTypeValue = "message" | "success" | "failure" | "custom" | "clip" | "link" | "forward" | "bookmark" | "clock";
export type ToastTypeValue = "message" | "success" | "failure" | "custom" | "clip" | "link" | "forward" | "invite" | "bookmark" | "clock" | "ai";
export type ToastPositionValue = 0 | 1;

View file

@ -9,14 +9,29 @@ import type { PopoverSize, TooltipAlign, TooltipColor, TooltipPosition } from ".
export type { PopoverSize, TooltipAlign, TooltipColor, TooltipPosition };
export interface ManaTooltipProps {
text: string;
text: string | (() => React.ReactNode);
position?: TooltipPosition;
align?: TooltipAlign;
color?: TooltipColor;
spacing?: number;
hideOnClick?: boolean;
delay?: number;
children: (props: any) => React.ReactNode;
forceOpen?: boolean;
shouldShow?: boolean;
allowOverflow?: boolean;
overflowOnly?: boolean;
clickableOnMobile?: boolean;
disableTooltipPointerEvents?: boolean;
targetElementRef?: React.RefObject<HTMLElement>;
tooltipClassName?: string;
tooltipStyle?: React.CSSProperties;
tooltipContentClassName?: string;
tooltipPointerClassName?: string;
onTooltipShow?: () => void;
onTooltipHide?: () => void;
onAnimationRest?: () => void;
"aria-label"?: string | false;
children: (props: Record<string, any>) => React.ReactNode;
}
export interface ManaRichTooltipProps {
@ -29,8 +44,13 @@ export interface ManaRichTooltipProps {
position?: TooltipPosition;
align?: TooltipAlign;
spacing?: number;
caretConfig?: { align?: "start" | "center" | "end"; };
layerContext?: any;
targetElementRef?: React.RefObject<HTMLElement>;
anchorRef?: React.RefObject<HTMLElement>;
positionKey?: string;
ariaHidden?: boolean;
children: React.ReactNode | ((props: any) => React.ReactNode);
children: React.ReactNode | ((props: Record<string, any>) => React.ReactNode);
}
export interface PopoverAction {

View file

@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { User } from "@vencord/discord-types";
export type UserSummarySize = 16 | 24 | 32 | 56;
export interface UserSummaryItemProps {
users: (User | null)[];
max?: number;
count?: number;
size?: UserSummarySize;
guildId?: string;
showUserPopout?: boolean;
showDefaultAvatarsForNullUsers?: boolean;
renderUser?: (user: User | null, isLast: boolean, index: number) => React.ReactNode;
renderMoreUsers?: (text: string, count: number) => React.ReactNode;
renderIcon?: boolean;
renderLeadingIcon?: (className: string) => React.ReactNode;
dimEmptyUsers?: boolean;
hideMoreUsers?: boolean;
useFallbackUserForPopout?: boolean;
extraDetail?: React.ReactNode;
className?: string;
}