Components: add plugin (dev) (#585)

* Components: add plugin (dev)

* fix: rename alert component to notice and some fixes
This commit is contained in:
prism 2025-12-21 04:01:32 +01:00 • committed by GitHub
parent 819d43e059
commit 89f93e245e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 5416 additions and 94 deletions

View file

@ -1,59 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { filters, mapMangledModuleLazy } from "@webpack";
import type { ComponentType, CSSProperties, ReactNode } from "react";
const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE="positive', {
HelpMessageTypes: filters.byProps("POSITIVE", "WARNING", "INFO", "ERROR"),
HelpMessage: filters.byCode(".iconDiv")
});
export interface AlertProps {
children: ReactNode;
className?: string;
style?: CSSProperties;
icon?: ComponentType<{ className?: string; color?: string; }>;
}
function Warning({ children, className, style, icon }: AlertProps) {
return (
<HelpMessage messageType={HelpMessageTypes.WARNING} className={className} style={style} icon={icon}>
{children}
</HelpMessage>
);
}
function Info({ children, className, style, icon }: AlertProps) {
return (
<HelpMessage messageType={HelpMessageTypes.INFO} className={className} style={style} icon={icon}>
{children}
</HelpMessage>
);
}
function Error({ children, className, style, icon }: AlertProps) {
return (
<HelpMessage messageType={HelpMessageTypes.ERROR} className={className} style={style} icon={icon}>
{children}
</HelpMessage>
);
}
function Positive({ children, className, style, icon }: AlertProps) {
return (
<HelpMessage messageType={HelpMessageTypes.POSITIVE} className={className} style={style} icon={icon}>
{children}
</HelpMessage>
);
}
export const Alert = {
Warning,
Info,
Error,
Positive
};

72
src/components/Notice.tsx Normal file
View file

@ -0,0 +1,72 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { filters, mapMangledModuleLazy } from "@webpack";
import type { ComponentType, CSSProperties, ReactNode } from "react";
export type NoticeType = "warn" | "info" | "danger" | "positive" | "preview";
export const NoticeTypes: Record<"WARNING" | "INFO" | "ERROR" | "POSITIVE" | "PREVIEW", NoticeType> = {
WARNING: "warn",
INFO: "info",
ERROR: "danger",
POSITIVE: "positive",
PREVIEW: "preview"
};
const { HelpMessage } = mapMangledModuleLazy('POSITIVE="positive"', {
HelpMessage: filters.byCode(".iconDiv")
});
export interface NoticeProps {
children: ReactNode;
className?: string;
style?: CSSProperties;
icon?: ComponentType<{ className?: string; color?: string; }>;
action?: ReactNode;
textColor?: string;
textVariant?: string;
hidden?: boolean;
}
function Warning({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) {
return (
<HelpMessage messageType={NoticeTypes.WARNING} className={className} style={style} icon={icon} action={action} textColor={textColor} textVariant={textVariant} hidden={hidden}>
{children}
</HelpMessage>
);
}
function Info({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) {
return (
<HelpMessage messageType={NoticeTypes.INFO} className={className} style={style} icon={icon} action={action} textColor={textColor} textVariant={textVariant} hidden={hidden}>
{children}
</HelpMessage>
);
}
function Error({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) {
return (
<HelpMessage messageType={NoticeTypes.ERROR} className={className} style={style} icon={icon} action={action} textColor={textColor} textVariant={textVariant} hidden={hidden}>
{children}
</HelpMessage>
);
}
function Positive({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) {
return (
<HelpMessage messageType={NoticeTypes.POSITIVE} className={className} style={style} icon={icon} action={action} textColor={textColor} textVariant={textVariant} hidden={hidden}>
{children}
</HelpMessage>
);
}
export const Notice = {
Warning,
Info,
Error,
Positive
};

View file

@ -20,7 +20,6 @@ import "./themesStyles.css";
import { isPluginEnabled } from "@api/PluginManager";
import { Settings, useSettings } from "@api/Settings";
import { Alert } from "@components/Alert";
import { Button } from "@components/Button";
import { Divider } from "@components/Divider";
import { ErrorCard } from "@components/ErrorCard";
@ -29,6 +28,7 @@ import { FormSwitch } from "@components/FormSwitch";
import { Heading } from "@components/Heading";
import { CogWheel, DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PluginIcon, PlusIcon, RestartIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { Notice } from "@components/Notice";
import { Paragraph } from "@components/Paragraph";
import { AddonCard, openPluginModal, QuickAction, QuickActionCard, SettingsTab, wrapTab } from "@components/settings";
import { OnlineThemeCard } from "@components/settings/OnlineThemeCard";
@ -727,9 +727,9 @@ function ThemesTab() {
}}
/>
<Alert.Info className={Margins.bottom16} style={{ width: "100%" }}>
<Notice.Info className={Margins.bottom16} style={{ width: "100%" }}>
Looking for themes? Check out <Link href="https://betterdiscord.app/themes">BetterDiscord Themes</Link> or search on <Link href="https://github.com/search?q=discord+theme">GitHub</Link>. When downloading from BetterDiscord, click "Download" and place the .theme.css file into your themes folder.
</Alert.Info>
</Notice.Info>
<div className={cl("link-row")}>
<TextInput

View file

@ -4,7 +4,6 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "./Alert";
export * from "./BaseText";
export * from "./Button";
export * from "./Card";
@ -21,6 +20,7 @@ export * from "./Heart";
export * from "./Icons";
export * from "./Link";
export * from "./margins";
export * from "./Notice";
export * from "./Paragraph";
export * from "./settings";
export * from "./Span";

View file

@ -17,11 +17,11 @@
*/
import { downloadSettingsBackup, uploadSettingsBackup } from "@api/SettingsSync/offline";
import { Alert } from "@components/Alert";
import { Button } from "@components/Button";
import { Divider } from "@components/Divider";
import { Flex } from "@components/Flex";
import { Heading } from "@components/Heading";
import { Notice } from "@components/Notice";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { Margins } from "@utils/margins";
@ -34,9 +34,9 @@ function BackupAndRestoreTab() {
Import and export your Equicord settings as a JSON file. This allows you to easily transfer your settings to another device, or recover them after reinstalling Equicord or Discord.
</Paragraph>
<Alert.Warning className={Margins.bottom20}>
<Notice.Warning className={Margins.bottom20}>
Importing a settings file will overwrite your current settings. Make sure to export a backup first if you want to keep your current configuration.
</Alert.Warning>
</Notice.Warning>
<Heading>What's included in a backup</Heading>
<Paragraph className={Margins.bottom20}>

View file

@ -19,7 +19,6 @@
import { useSettings } from "@api/Settings";
import { authorizeCloud, deauthorizeCloud } from "@api/SettingsSync/cloudSetup";
import { deleteCloudSettings, eraseAllCloudData, getCloudSettings, putCloudSettings } from "@api/SettingsSync/cloudSync";
import { Alert } from "@components/Alert";
import { Button } from "@components/Button";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Divider } from "@components/Divider";
@ -28,6 +27,7 @@ import { FormSwitch } from "@components/FormSwitch";
import { Heading } from "@components/Heading";
import { CloudDownloadIcon, CloudUploadIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { Notice } from "@components/Notice";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { localStorage } from "@utils/localStorage";
@ -97,11 +97,11 @@ function CloudTab() {
Equicord's cloud integration allows you to sync your settings across multiple devices and Discord installations. Your data is securely stored and can be easily restored at any time.
</Paragraph>
<Alert.Info className={Margins.bottom16}>
<Notice.Info className={Margins.bottom16}>
We use our own <Link href="https://github.com/Equicord/Equicloud">Equicloud backend</Link> with enhanced features.
View our <Link href="https://equicord.org/cloud/policy">privacy policy</Link> to see what we store and how we use your data.
Equicloud is BSD 3.0 licensed, so you can self-host if preferred.
</Alert.Info>
</Notice.Info>
<FormSwitch
title="Enable Cloud Integration"
@ -218,9 +218,9 @@ function CloudTab() {
</Flex>
{!isAuthenticated && (
<Alert.Warning className={Margins.top8}>
<Notice.Warning className={Margins.top8}>
Enable cloud integration above to use settings sync features.
</Alert.Warning>
</Notice.Warning>
)}
<Divider className={Margins.top20} />

View file

@ -8,13 +8,13 @@ import "./VencordTab.css";
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/Settings";
import { Alert } from "@components/Alert";
import { Button } from "@components/Button";
import { Divider } from "@components/Divider";
import { Flex } from "@components/Flex";
import { FormSwitch } from "@components/FormSwitch";
import { Heading } from "@components/Heading";
import { FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "@components/Icons";
import { Notice } from "@components/Notice";
import { Paragraph } from "@components/Paragraph";
import { openContributorModal, openPluginModal, SettingsTab, wrapTab } from "@components/settings";
import { QuickAction, QuickActionCard } from "@components/settings/QuickAction";
@ -224,7 +224,7 @@ function EquicordSettings() {
<Paragraph className={Margins.bottom16}>
Configure how Equicord behaves and integrates with Discord. These settings affect the Discord client's appearance and behavior.
</Paragraph>
<Alert.Info className={Margins.bottom20} style={{ width: "100%" }}>
<Notice.Info className={Margins.bottom20} style={{ width: "100%" }}>
You can customize where this settings section appears in Discord's settings menu by configuring the{" "}
<a
role="button"
@ -233,7 +233,7 @@ function EquicordSettings() {
>
Settings Plugin
</a>.
</Alert.Info>
</Notice.Info>
{Switches.filter((s): s is Exclude<typeof s, false> => !!s).map(
s => (
@ -246,9 +246,9 @@ function EquicordSettings() {
s.warning.enabled ? (
<>
{s.description}
<Alert.Warning className={Margins.top8} style={{ width: "100%" }}>
<Notice.Warning className={Margins.top8} style={{ width: "100%" }}>
{s.warning.message}
</Alert.Warning>
</Notice.Warning>
</>
) : (
s.description

View file

@ -0,0 +1,140 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "../styles.css";
import { Heading } from "@components/Heading";
import { SettingsTab, wrapTab } from "@components/settings";
import { Margins } from "@utils/margins";
import { TabBar, useState } from "@webpack/common";
import AnchorTab from "./tabs/AnchorTab";
import AvatarTab from "./tabs/AvatarTab";
import BadgeTab from "./tabs/BadgeTab";
import ButtonsTab from "./tabs/ButtonsTab";
import CalendarTab from "./tabs/CalendarTab";
import CheckboxGroupTab from "./tabs/CheckboxGroupTab";
import CheckboxTab from "./tabs/CheckboxTab";
import ClickableTab from "./tabs/ClickableTab";
import ColorPickerTab from "./tabs/ColorPickerTab";
import ComboboxTab from "./tabs/ComboboxTab";
import LayerModalTab from "./tabs/LayerModalTab";
import ListboxTab from "./tabs/ListboxTab";
import ModalTab from "./tabs/ModalTab";
import NoticeTab from "./tabs/NoticeTab";
import PaginatorTab from "./tabs/PaginatorTab";
import PopoverTab from "./tabs/PopoverTab";
import ProgressBarTab from "./tabs/ProgressBarTab";
import RadioGroupTab from "./tabs/RadioGroupTab";
import RichTooltipTab from "./tabs/RichTooltipTab";
import SearchBarTab from "./tabs/SearchBarTab";
import SelectTab from "./tabs/SelectTab";
import SliderTab from "./tabs/SliderTab";
import SpinnerTab from "./tabs/SpinnerTab";
import SwitchTab from "./tabs/SwitchTab";
import TabBarTab from "./tabs/TabBarTab";
import TextAreaTab from "./tabs/TextAreaTab";
import TextButtonTab from "./tabs/TextButtonTab";
import TextInputTab from "./tabs/TextInputTab";
import TooltipTab from "./tabs/TooltipTab";
import TypographyTab from "./tabs/TypographyTab";
const TABS = [
{ id: "anchor", label: "Anchor" },
{ id: "avatar", label: "Avatar" },
{ id: "badge", label: "Badge" },
{ id: "buttons", label: "Buttons" },
{ id: "calendar", label: "Calendar" },
{ id: "checkbox", label: "Checkbox" },
{ id: "checkboxgroup", label: "CheckboxGroup" },
{ id: "clickable", label: "Clickable" },
{ id: "colorpicker", label: "ColorPicker" },
{ id: "combobox", label: "Combobox" },
{ id: "layermodal", label: "LayerModal" },
{ id: "listbox", label: "Listbox" },
{ id: "modal", label: "Modal" },
{ id: "notice", label: "Notice" },
{ id: "paginator", label: "Paginator" },
{ id: "popover", label: "Popover" },
{ id: "progressbar", label: "ProgressBar" },
{ id: "radiogroup", label: "RadioGroup" },
{ id: "richtooltip", label: "RichTooltip" },
{ id: "searchbar", label: "SearchBar" },
{ id: "select", label: "Select" },
{ id: "slider", label: "Slider" },
{ id: "spinner", label: "Spinner" },
{ id: "switch", label: "Switch" },
{ id: "tabbar", label: "TabBar" },
{ id: "textarea", label: "TextArea" },
{ id: "textbutton", label: "TextButton" },
{ id: "textinput", label: "TextInput" },
{ id: "tooltip", label: "Tooltip" },
{ id: "typography", label: "Typography" },
].sort((a, b) => a.label.localeCompare(b.label));
type TabId = typeof TABS[number]["id"];
const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
anchor: AnchorTab,
avatar: AvatarTab,
badge: BadgeTab,
buttons: ButtonsTab,
calendar: CalendarTab,
checkbox: CheckboxTab,
checkboxgroup: CheckboxGroupTab,
clickable: ClickableTab,
colorpicker: ColorPickerTab,
combobox: ComboboxTab,
layermodal: LayerModalTab,
listbox: ListboxTab,
modal: ModalTab,
notice: NoticeTab,
paginator: PaginatorTab,
popover: PopoverTab,
progressbar: ProgressBarTab,
radiogroup: RadioGroupTab,
richtooltip: RichTooltipTab,
searchbar: SearchBarTab,
select: SelectTab,
slider: SliderTab,
spinner: SpinnerTab,
switch: SwitchTab,
tabbar: TabBarTab,
textarea: TextAreaTab,
textbutton: TextButtonTab,
textinput: TextInputTab,
tooltip: TooltipTab,
typography: TypographyTab,
};
function ComponentsTab() {
const [currentTab, setCurrentTab] = useState<TabId>("avatar");
const TabComponent = TAB_COMPONENTS[currentTab];
return (
<SettingsTab>
<Heading className={Margins.bottom16}>Components</Heading>
<TabBar
type="top"
look="brand"
selectedItem={currentTab}
onItemSelect={setCurrentTab}
className="vc-compfinder-tabbar"
>
{TABS.map(tab => (
<TabBar.Item key={tab.id} id={tab.id} className="vc-compfinder-tab">
{tab.label}
</TabBar.Item>
))}
</TabBar>
<div className="vc-compfinder-content">
<TabComponent />
</div>
</SettingsTab>
);
}
export default wrapTab(ComponentsTab, "ComponentsTab");

View file

@ -0,0 +1,23 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Heading } from "@components/Heading";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
interface SectionWrapperProps {
title: string;
children: React.ReactNode;
}
export function SectionWrapper({ title, children }: SectionWrapperProps) {
return (
<div className={classes("vc-compfinder-section-block", Margins.bottom16)}>
<Heading tag="h3" className={Margins.bottom8}>{title}</Heading>
{children}
</div>
);
}

View file

@ -0,0 +1,16 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function EquicordIcon({ className, size, color }: { className?: string; size?: string; color?: string; }) {
const dimension = size === "xs" ? 12 : size === "sm" ? 16 : size === "lg" ? 24 : 20;
return (
<svg className={className} viewBox="0 0 443 443" width={dimension} height={dimension}>
<path fill={color || "currentColor"} d="M221.5.6C99.2.6,0,99.7,0,222.1s99.2,221.5,221.5,221.5,221.5-99.2,221.5-221.5S343.8.6,221.5.6ZM221.5,363.3c-78.3,0-141.8-63.5-141.8-141.8s63.5-141.8,141.8-141.8,141.8,63.5,141.8,141.8-63.5,141.8-141.8,141.8Z" />
<path fill={color || "currentColor"} d="M438.3,175C421,90.1,354.7,23.1,269.9,5.1,145-21.3,33.1,57.6,6.2,169.5c-3.4,14.1,4.6,28.5,18.4,33.1l2.7.9c24.7,8.2,51.8-4.7,60.4-29.3,19.4-55.6,72.5-95.4,134.9-95,44.3.3,84.1,21.2,109.9,53.7l-63.3,29.5c-7.9-7-17.5-12.5-28.5-15.7-35.6-10.3-73.8,7.7-88.5,41.7-17.2,39.8,3.3,85.4,43.9,99.4,37.9,13,79.7-6.9,93.5-44.5,2.6-7.2,4.1-14.5,4.4-21.8l66-30.8c1.8,8.2,2.9,16.6,3.2,25.2,1.2,34-9.6,65.5-28.5,90.5-17.8,23.6-10.5,57.4,15.7,71.1l1.3.7c10.4,5.5,23.3,3.4,31.4-5.2,46.8-49.9,70.8-121.4,55.2-198Z" />
<path fill={color || "currentColor"} transform="translate(319.7 -16.5) rotate(65)" d="M172.8,152.7h0c17.7,0,32,14.3,32,32v148h-64v-148c0-17.7,14.3-32,32-32Z" />
</svg>
);
}

View file

@ -0,0 +1,189 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { proxyLazy } from "@utils/lazy";
import { findByCodeLazy, findComponentByCodeLazy, findLazy } from "@webpack";
import {
AnchorProps,
AvatarProps,
BadgeShapesType,
CheckboxGroupOption,
CircleBadgeProps,
ClickableProps,
ColorPickerProps,
ColorPickerWithSwatchesProps,
ColorSwatchProps,
CustomColorButtonProps,
DefaultColorButtonProps,
GradientColor,
IconBadgeProps,
ListboxItem,
ManaBaseRadioGroupProps,
ManaButtonProps,
ManaCalendarProps,
ManaCheckboxGroupProps,
ManaCheckboxProps,
ManaComboboxProps,
ManaDatePickerProps,
ManaListboxProps,
ManaPopoverProps,
ManaRichTooltipProps,
ManaSelectOption,
ManaSelectProps,
ManaSwitchProps,
ManaTextAreaProps,
ManaTextButtonProps,
ManaTextInputProps,
ManaTooltipProps,
NoticeProps,
NoticeType,
NumberBadgeProps,
PaginatorProps,
PopoverAction,
ProgressBarProps,
RadioOption,
SearchBarProps,
SliderProps,
SpinnerProps,
StandaloneRadioIndicatorProps,
TabBarComponent,
TabBarHeaderProps,
TabBarItemProps,
TabBarProps,
TabBarSeparatorProps,
TextBadgeProps
} from "../types";
export type {
AnchorProps,
AvatarProps,
BadgeShapesType,
CheckboxGroupOption,
CircleBadgeProps,
ColorPickerProps,
ColorPickerWithSwatchesProps,
ColorSwatchProps,
CustomColorButtonProps,
DefaultColorButtonProps,
GradientColor,
IconBadgeProps,
ListboxItem,
ManaBaseRadioGroupProps,
ManaButtonProps,
ManaCalendarProps,
ManaCheckboxGroupProps,
ManaCheckboxProps,
ManaComboboxProps,
ManaDatePickerProps,
ManaListboxProps,
ManaPopoverProps,
ManaRichTooltipProps,
ManaSelectOption,
ManaSelectProps,
ManaSwitchProps,
ManaTextAreaProps,
ManaTextButtonProps,
ManaTextInputProps,
ManaTooltipProps,
NoticeProps,
NoticeType,
NumberBadgeProps,
PaginatorProps,
PopoverAction,
ProgressBarProps,
RadioOption,
SearchBarProps,
SliderProps,
SpinnerProps,
StandaloneRadioIndicatorProps,
TabBarComponent,
TabBarHeaderProps,
TabBarItemProps,
TabBarProps,
TabBarSeparatorProps,
TextBadgeProps
};
export const Anchor = findComponentByCodeLazy("anchorUnderlineOnHover", "useDefaultUnderlineStyles") as React.ComponentType<AnchorProps>;
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>;
export const ManaSwitch = findComponentByCodeLazy('"data-mana-component":"switch"') as React.ComponentType<ManaSwitchProps>;
export const ManaTextInput = findComponentByCodeLazy('"data-mana-component":"text-input"') as React.ComponentType<ManaTextInputProps>;
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('"data-mana-component":"tooltip"') 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 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 React.ComponentType<SpinnerProps>;
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 Notice = findComponentByCodeLazy("messageType", "iconDiv", "actionContainer") as React.ComponentType<NoticeProps>;
export const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", "showEyeDropper") as React.ComponentType<ColorPickerProps>;
const ColorPickerModule = findLazy(m => {
const values = Object.values(m);
return values.length === 4 &&
values.some(v => typeof v === "function" && v.toString?.().includes('id:"color-picker"')) &&
values.some(v => typeof v === "function" && v.toString?.().includes("isDefault:!0"));
});
function getColorPickerExport(check: (fn: Function) => boolean): React.ComponentType<any> {
for (const value of Object.values(ColorPickerModule)) {
if (typeof value === "function" && check(value)) return value as React.ComponentType<any>;
}
return (() => null) as React.ComponentType<any>;
}
export const ColorPickerWithSwatches: React.ComponentType<ColorPickerWithSwatchesProps> = proxyLazy(() =>
getColorPickerExport(fn => fn.toString().includes('id:"color-picker"'))
);
export const ColorSwatch: React.ComponentType<ColorSwatchProps> = proxyLazy(() =>
getColorPickerExport(fn => fn.toString().includes("isDefault") && fn.toString().includes("isCustom") && fn.toString().includes("colorPickerSwatch"))
);
export const DefaultColorButton: React.ComponentType<DefaultColorButtonProps> = proxyLazy(() =>
getColorPickerExport(fn => fn.toString().includes("isDefault:!0") && fn.toString().includes("allowBlackCustomColor"))
);
export const CustomColorButton: React.ComponentType<CustomColorButtonProps> = proxyLazy(() =>
getColorPickerExport(fn => fn.toString().includes("isCustom:!0") && fn.toString().includes("presets"))
);
const BadgeModule = findLazy(m => {
const values = Object.values(m);
return values.length < 15 &&
values.some(v => typeof v === "function" && v.toString?.().includes("numberBadge")) &&
values.some(v => typeof v === "object" && (v as BadgeShapesType)?.ROUND && (v as BadgeShapesType)?.ROUND_LEFT);
});
function getBadgeExport(check: (fn: Function) => boolean): React.ComponentType<any> {
for (const value of Object.values(BadgeModule)) {
if (typeof value === "function" && check(value)) return value as React.ComponentType<any>;
}
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 TabBar = findByCodeLazy("this.tabBarRef", "renderChildren") as TabBarComponent;
export const Clickable = findByCodeLazy("ignoreKeyPress", "renderNonInteractive") as React.ComponentType<ClickableProps>;

View file

@ -0,0 +1,112 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { Anchor } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function AnchorTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Anchor">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
A styled anchor/link component with underline on hover.
</Paragraph>
<Anchor href="https://discord.com">
Discord Homepage
</Anchor>
</SectionWrapper>
<SectionWrapper title="Without Underline Styles">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Disable the default underline-on-hover behavior.
</Paragraph>
<Anchor href="https://discord.com" useDefaultUnderlineStyles={false}>
No underline on hover
</Anchor>
</SectionWrapper>
<SectionWrapper title="With Custom Target">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Open in same tab instead of new tab.
</Paragraph>
<Anchor href="https://discord.com" target="_self">
Opens in same tab
</Anchor>
</SectionWrapper>
<SectionWrapper title="With Title Tooltip">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Hover to see the title tooltip.
</Paragraph>
<Anchor href="https://discord.com" title="Click to visit Discord">
Hover for tooltip
</Anchor>
</SectionWrapper>
<SectionWrapper title="With Custom Styling">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Custom styles applied via style prop.
</Paragraph>
<Anchor
href="https://discord.com"
style={{ color: "var(--text-positive)", fontWeight: 600 }}
>
Green styled link
</Anchor>
</SectionWrapper>
<SectionWrapper title="With onClick Handler">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Custom click handler (prevents default navigation).
</Paragraph>
<Anchor
href="#"
onClick={e => {
e.preventDefault();
alert("Link clicked!");
}}
>
Click me (shows alert)
</Anchor>
</SectionWrapper>
<SectionWrapper title="Multiple Links">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Links in a sentence.
</Paragraph>
<Paragraph>
Check out the <Anchor href="https://discord.com/terms">Terms of Service</Anchor> and{" "}
<Anchor href="https://discord.com/privacy">Privacy Policy</Anchor> for more info.
</Paragraph>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>Anchor</strong> - Styled link component (also known as MaskedLink)
</Paragraph>
<Paragraph color="text-muted">• href?: string - Link URL</Paragraph>
<Paragraph color="text-muted">• onClick?: MouseEventHandler - Click handler</Paragraph>
<Paragraph color="text-muted">• className?: string - Additional CSS classes</Paragraph>
<Paragraph color="text-muted">• children?: ReactNode - Link content</Paragraph>
<Paragraph color="text-muted">• rel?: string - Link relationship (auto-set for external)</Paragraph>
<Paragraph color="text-muted">• target?: string - Link target (auto-set to _blank for external)</Paragraph>
<Paragraph color="text-muted">• useDefaultUnderlineStyles?: boolean - Show underline on hover (default: true)</Paragraph>
<Paragraph color="text-muted">• title?: string - Tooltip text on hover</Paragraph>
<Paragraph color="text-muted">• style?: CSSProperties - Inline styles</Paragraph>
<Paragraph color="text-muted">• focusProps?: object - Focus ring configuration</Paragraph>
<Paragraph color="text-muted">• ref?: Ref - Forward ref to anchor element</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>Behavior</strong>
</Paragraph>
<Paragraph color="text-muted">• External links automatically get rel="noreferrer noopener" and target="_blank"</Paragraph>
<Paragraph color="text-muted">• If no onClick is provided but href is set, uses Discord's default link interceptor</Paragraph>
<Paragraph color="text-muted">• Untrusted external links show a warning modal before navigating</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,122 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { UserStore } from "@webpack/common";
import { Avatar } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SIZES = ["SIZE_16", "SIZE_20", "SIZE_24", "SIZE_32", "SIZE_40", "SIZE_48", "SIZE_56", "SIZE_80", "SIZE_120"] as const;
export default function AvatarTab() {
const currentUser = UserStore.getCurrentUser();
const avatarUrl = currentUser?.getAvatarURL?.(undefined, 128);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Avatar Sizes">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Avatar component from @webpack/common with various sizes.
</Paragraph>
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
{SIZES.map(size => (
<div key={size} style={{ textAlign: "center" }}>
<Avatar
src={avatarUrl}
size={size}
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
{size.replace("SIZE_", "")}
</Paragraph>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title="With Status">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Avatar with status indicator.
</Paragraph>
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center" }}>
<Avatar
src={avatarUrl}
size="SIZE_40"
status="online"
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Online</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<Avatar
src={avatarUrl}
size="SIZE_40"
status="idle"
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Idle</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<Avatar
src={avatarUrl}
size="SIZE_40"
status="dnd"
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>DND</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<Avatar
src={avatarUrl}
size="SIZE_40"
status="offline"
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Offline</Paragraph>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Mobile Status">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Avatar with mobile indicator.
</Paragraph>
<div className="vc-compfinder-grid">
<Avatar
src={avatarUrl}
size="SIZE_40"
status="online"
isMobile
/>
</div>
</SectionWrapper>
<SectionWrapper title="Speaking State">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Avatar with speaking ring indicator.
</Paragraph>
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center" }}>
<Avatar
src={avatarUrl}
size="SIZE_40"
isSpeaking
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Speaking</Paragraph>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• src - Avatar image URL</Paragraph>
<Paragraph color="text-muted">• size - SIZE_16 to SIZE_120</Paragraph>
<Paragraph color="text-muted">• status - online, idle, dnd, offline</Paragraph>
<Paragraph color="text-muted">• isMobile - Show mobile indicator</Paragraph>
<Paragraph color="text-muted">• isTyping - Show typing indicator</Paragraph>
<Paragraph color="text-muted">• isSpeaking - Show speaking ring</Paragraph>
<Paragraph color="text-muted">• statusTooltip - Show status on hover</Paragraph>
<Paragraph color="text-muted">• aria-label - Accessibility label</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,106 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, TextBadge } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";
export default function BadgeTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="NumberBadge">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Displays a count value. Automatically abbreviates large numbers.
</Paragraph>
<div className="vc-compfinder-grid">
<NumberBadge count={1} />
<NumberBadge count={9} />
<NumberBadge count={99} />
<NumberBadge count={999} />
<NumberBadge count={9999} />
</div>
</SectionWrapper>
<SectionWrapper title="TextBadge">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Displays text content.
</Paragraph>
<div className="vc-compfinder-grid">
<TextBadge text="NEW" />
<TextBadge text="BETA" />
<TextBadge text="PRO" />
</div>
</SectionWrapper>
<SectionWrapper title="IconBadge">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Displays an icon inside a badge.
</Paragraph>
<div className="vc-compfinder-grid">
<IconBadge icon={EquicordIcon} />
<IconBadge icon={EquicordIcon} color="var(--status-positive)" />
<IconBadge icon={EquicordIcon} color="var(--status-warning)" />
</div>
</SectionWrapper>
<SectionWrapper title="CircleBadge">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
A simple circular badge indicator.
</Paragraph>
<div className="vc-compfinder-grid">
<CircleBadge />
<CircleBadge color="var(--status-positive)" />
<CircleBadge color="var(--status-warning)" />
</div>
</SectionWrapper>
<SectionWrapper title="Shapes">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Badges support different shapes via the shape prop.
</Paragraph>
<div className="vc-compfinder-grid">
<TextBadge text="ROUND" shape={BadgeShapes.ROUND} />
<TextBadge text="LEFT" shape={BadgeShapes.ROUND_LEFT} />
<TextBadge text="RIGHT" shape={BadgeShapes.ROUND_RIGHT} />
<TextBadge text="SQUARE" shape={BadgeShapes.SQUARE} />
</div>
</SectionWrapper>
<SectionWrapper title="Colors">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Custom colors can be applied. Use disableColor for no background.
</Paragraph>
<div className="vc-compfinder-grid">
<NumberBadge count={5} color="var(--status-danger)" />
<NumberBadge count={5} color="var(--status-positive)" />
<NumberBadge count={5} color="var(--status-warning)" />
<NumberBadge count={5} color="var(--brand-500)" />
<NumberBadge count={5} disableColor />
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
NumberBadge: count, maxDigits?, color?, disableColor?, shape?, className?, style?
</Paragraph>
<Paragraph color="text-muted">
TextBadge: text, color?, disableColor?, shape?, className?, style?
</Paragraph>
<Paragraph color="text-muted">
IconBadge: icon, color?, disableColor?, shape?, className?, style?
</Paragraph>
<Paragraph color="text-muted">
CircleBadge: color?, disableColor?, shape?, className?, style?
</Paragraph>
<Paragraph color="text-muted">
BadgeShapes: ROUND, ROUND_LEFT, ROUND_RIGHT, SQUARE
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,77 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaButton } from "..";
import { SectionWrapper } from "../SectionWrapper";
const VARIANTS = ["primary", "secondary", "critical-primary", "critical-secondary", "overlay-primary", "overlay-secondary", "expressive"] as const;
const SIZES = ["xs", "sm", "md"] as const;
export default function ButtonsTab() {
const [loading, setLoading] = useState(false);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Variants (size: md)">
<div className="vc-compfinder-grid">
{VARIANTS.map(variant => (
<ManaButton key={variant} variant={variant} size="md" text={variant} />
))}
</div>
</SectionWrapper>
<SectionWrapper title="Sizes (variant: primary)">
<div className="vc-compfinder-grid">
{SIZES.map(size => (
<ManaButton key={size} variant="primary" size={size} text={`Size: ${size}`} />
))}
</div>
</SectionWrapper>
<SectionWrapper title="States">
<div className="vc-compfinder-grid">
<ManaButton variant="primary" size="md" text="Disabled" disabled />
<ManaButton
variant="primary"
size="md"
text={loading ? "Loading..." : "Click to Load"}
loading={loading}
onClick={() => {
setLoading(true);
setTimeout(() => setLoading(false), 2000);
}}
/>
<ManaButton variant="primary" size="md" text="Rounded" rounded />
<ManaButton variant="primary" size="md" text="Full Width" fullWidth />
</div>
</SectionWrapper>
<SectionWrapper title="All Variants Disabled">
<div className="vc-compfinder-grid">
{VARIANTS.map(variant => (
<ManaButton key={variant} variant={variant} size="md" text={variant} disabled />
))}
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• text - Button label text</Paragraph>
<Paragraph color="text-muted">• variant - primary, secondary, critical-primary, critical-secondary, overlay-primary, overlay-secondary, expressive</Paragraph>
<Paragraph color="text-muted">• size - xs, sm, md</Paragraph>
<Paragraph color="text-muted">• disabled - Disable button</Paragraph>
<Paragraph color="text-muted">• loading - Show loading spinner</Paragraph>
<Paragraph color="text-muted">• fullWidth - Expand to container width</Paragraph>
<Paragraph color="text-muted">• rounded - Rounded corners</Paragraph>
<Paragraph color="text-muted">• icon - Icon component</Paragraph>
<Paragraph color="text-muted">• iconPosition - start or end</Paragraph>
<Paragraph color="text-muted">• onClick - Click handler</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,159 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaCalendar, ManaDatePicker } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function CalendarTab() {
const [calendarValue, setCalendarValue] = useState<any>(null);
const [datePickerValue, setDatePickerValue] = useState<any>(null);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Calendar">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Basic calendar component for date selection.
</Paragraph>
<div className="vc-compfinder-grid">
<ManaCalendar
value={calendarValue}
onChange={setCalendarValue}
aria-label="Select a date"
/>
</div>
</SectionWrapper>
<SectionWrapper title="Calendar (Disabled)">
<div className="vc-compfinder-grid">
<ManaCalendar
value={calendarValue}
onChange={setCalendarValue}
disabled
aria-label="Disabled calendar"
/>
</div>
</SectionWrapper>
<SectionWrapper title="Calendar (Read Only)">
<div className="vc-compfinder-grid">
<ManaCalendar
value={calendarValue}
onChange={setCalendarValue}
readOnly
aria-label="Read only calendar"
/>
</div>
</SectionWrapper>
<SectionWrapper title="Date Picker">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Date picker with input field and calendar popover.
</Paragraph>
<div className="vc-compfinder-grid-vertical">
<ManaDatePicker
value={datePickerValue}
onChange={setDatePickerValue}
label="Select Date"
description="Choose a date from the calendar"
granularity="day"
/>
</div>
</SectionWrapper>
<SectionWrapper title="Date Picker with Time">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Date picker with hour/minute granularity.
</Paragraph>
<div className="vc-compfinder-grid-vertical">
<ManaDatePicker
value={datePickerValue}
onChange={setDatePickerValue}
label="Select Date & Time"
granularity="minute"
hourCycle={24}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Date Picker States">
<div className="vc-compfinder-grid-vertical">
<ManaDatePicker
value={datePickerValue}
onChange={setDatePickerValue}
label="Disabled"
disabled
/>
<ManaDatePicker
value={datePickerValue}
onChange={setDatePickerValue}
label="Required"
required
/>
<ManaDatePicker
value={datePickerValue}
onChange={setDatePickerValue}
label="With Error"
errorMessage="Please select a valid date"
/>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>ManaCalendar</strong>
</Paragraph>
<Paragraph color="text-muted">
• value: any - Selected date value
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (value: any) =&gt; void - Selection callback
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable calendar
</Paragraph>
<Paragraph color="text-muted">
• readOnly?: boolean - Read-only mode
</Paragraph>
<Paragraph color="text-muted">
• aria-label?: string - Accessibility label
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ManaDatePicker</strong>
</Paragraph>
<Paragraph color="text-muted">
• value: any - Selected date value
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (value: any) =&gt; void - Selection callback
</Paragraph>
<Paragraph color="text-muted">
• label?: string - Input label
</Paragraph>
<Paragraph color="text-muted">
• description?: string - Description text
</Paragraph>
<Paragraph color="text-muted">
• granularity?: "day" | "hour" | "minute" - Time precision
</Paragraph>
<Paragraph color="text-muted">
• hourCycle?: 12 | 24 - Hour format
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable picker
</Paragraph>
<Paragraph color="text-muted">
• required?: boolean - Mark as required
</Paragraph>
<Paragraph color="text-muted">
• errorMessage?: string - Error text to display
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,123 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { CheckboxGroupOption, ManaCheckboxGroup } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";
const BASIC_OPTIONS: CheckboxGroupOption[] = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
const OPTIONS_WITH_DESCRIPTIONS: CheckboxGroupOption[] = [
{ value: "notifications", label: "Notifications", description: "Receive push notifications" },
{ value: "sounds", label: "Sounds", description: "Play sounds for events" },
{ value: "badges", label: "Badges", description: "Show unread badges" },
];
const OPTIONS_WITH_ICONS: CheckboxGroupOption[] = [
{ value: "item1", label: "Item with icon", description: "This option has an icon", leadingIcon: EquicordIcon },
{ value: "item2", label: "Another with icon", leadingIcon: EquicordIcon },
{ value: "item3", label: "Third with icon", leadingIcon: EquicordIcon },
];
const OPTIONS_MIXED: CheckboxGroupOption[] = [
{ value: "enabled", label: "Enabled option" },
{ value: "disabled", label: "Disabled option", disabled: true },
{ value: "another", label: "Another enabled" },
];
export default function CheckboxGroupTab() {
const [basicSelected, setBasicSelected] = useState<(string | number)[]>(["option1"]);
const [descSelected, setDescSelected] = useState<(string | number)[]>(["notifications", "sounds"]);
const [iconSelected, setIconSelected] = useState<(string | number)[]>(["item1"]);
const [mixedSelected, setMixedSelected] = useState<(string | number)[]>(["enabled"]);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic">
<ManaCheckboxGroup
options={BASIC_OPTIONS}
selectedValues={basicSelected}
onChange={setBasicSelected}
/>
</SectionWrapper>
<SectionWrapper title="With Descriptions">
<ManaCheckboxGroup
options={OPTIONS_WITH_DESCRIPTIONS}
selectedValues={descSelected}
onChange={setDescSelected}
/>
</SectionWrapper>
<SectionWrapper title="With Leading Icons">
<ManaCheckboxGroup
options={OPTIONS_WITH_ICONS}
selectedValues={iconSelected}
onChange={setIconSelected}
/>
</SectionWrapper>
<SectionWrapper title="With Disabled Option">
<ManaCheckboxGroup
options={OPTIONS_MIXED}
selectedValues={mixedSelected}
onChange={setMixedSelected}
/>
</SectionWrapper>
<SectionWrapper title="Entire Group Disabled">
<ManaCheckboxGroup
options={BASIC_OPTIONS}
selectedValues={["option1", "option2"]}
disabled
/>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>CheckboxGroup</strong>
</Paragraph>
<Paragraph color="text-muted">
• options: CheckboxGroupOption[] - Array of options
</Paragraph>
<Paragraph color="text-muted">
• selectedValues: (string | number)[] - Currently selected values
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (values: (string | number)[]) =&gt; void - Called when selection changes
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable entire group
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>CheckboxGroupOption</strong>
</Paragraph>
<Paragraph color="text-muted">
• value: string | number - Option value
</Paragraph>
<Paragraph color="text-muted">
• label: string - Option label
</Paragraph>
<Paragraph color="text-muted">
• description?: string - Optional description
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable individual option
</Paragraph>
<Paragraph color="text-muted">
• leadingIcon?: React.ComponentType - Icon before checkbox
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,153 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaCheckbox } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";
export default function CheckboxTab() {
const [basicChecked, setBasicChecked] = useState(true);
const [basicUnchecked, setBasicUnchecked] = useState(false);
const [withDesc, setWithDesc] = useState(true);
const [labelPrimary, setLabelPrimary] = useState(true);
const [labelSecondary, setLabelSecondary] = useState(false);
const [withIcon, setWithIcon] = useState(true);
const [usageSingle, setUsageSingle] = useState(true);
const [usageIndicator, setUsageIndicator] = useState(false);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic">
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox
label="Checked"
checked={basicChecked}
onChange={setBasicChecked}
/>
<ManaCheckbox
label="Unchecked"
checked={basicUnchecked}
onChange={setBasicUnchecked}
/>
</div>
</SectionWrapper>
<SectionWrapper title="With Description">
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox
label="Option with description"
description="This is a helpful description for the option."
checked={withDesc}
onChange={setWithDesc}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Label Types">
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox
label="Primary label (default)"
labelType="primary"
checked={labelPrimary}
onChange={setLabelPrimary}
/>
<ManaCheckbox
label="Secondary label"
labelType="secondary"
checked={labelSecondary}
onChange={setLabelSecondary}
/>
</div>
</SectionWrapper>
<SectionWrapper title="With Leading Icon">
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox
label="With icon"
description="This checkbox has a leading icon."
leadingIcon={EquicordIcon}
checked={withIcon}
onChange={setWithIcon}
/>
</div>
</SectionWrapper>
<SectionWrapper title="States">
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox
label="Disabled (checked)"
checked={true}
disabled
/>
<ManaCheckbox
label="Disabled (unchecked)"
checked={false}
disabled
/>
<ManaCheckbox
label="Display only"
checked={true}
displayOnly
/>
</div>
</SectionWrapper>
<SectionWrapper title="Usage Variants">
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox
label="Single (default)"
usageVariant="single"
checked={usageSingle}
onChange={setUsageSingle}
/>
<ManaCheckbox
label="Indicator"
usageVariant="indicator"
checked={usageIndicator}
onChange={setUsageIndicator}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• checked: boolean - Current checked state
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (checked: boolean) =&gt; void - Called when toggled
</Paragraph>
<Paragraph color="text-muted">
• label?: string - Label text
</Paragraph>
<Paragraph color="text-muted">
• description?: string - Description below label
</Paragraph>
<Paragraph color="text-muted">
• labelType?: "primary" | "secondary" - Label styling
</Paragraph>
<Paragraph color="text-muted">
• leadingIcon?: React.ComponentType - Icon before checkbox
</Paragraph>
<Paragraph color="text-muted">
• usageVariant?: "single" | "indicator" - Visual style
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable interaction
</Paragraph>
<Paragraph color="text-muted">
• displayOnly?: boolean - Display without interaction
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,230 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { Clickable } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function ClickableTab() {
const [clickCount, setClickCount] = useState(0);
const [lastAction, setLastAction] = useState("None");
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Clickable">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Default clickable element. Click count: {clickCount}
</Paragraph>
<Clickable
onClick={() => setClickCount(c => c + 1)}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-secondary)",
borderRadius: 4,
cursor: "pointer"
}}
>
Click me!
</Clickable>
</SectionWrapper>
<SectionWrapper title="With Custom Tag">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Clickable can render as different HTML elements using the tag prop.
</Paragraph>
<div style={{ display: "flex", gap: 8 }}>
<Clickable
tag="div"
onClick={() => setLastAction("Clicked div")}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-tertiary)",
borderRadius: 4,
cursor: "pointer"
}}
>
tag="div"
</Clickable>
<Clickable
tag="span"
onClick={() => setLastAction("Clicked span")}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-tertiary)",
borderRadius: 4,
cursor: "pointer"
}}
>
tag="span"
</Clickable>
<Clickable
tag="button"
onClick={() => setLastAction("Clicked button")}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-tertiary)",
borderRadius: 4,
cursor: "pointer",
border: "none",
color: "inherit"
}}
>
tag="button"
</Clickable>
</div>
<Paragraph color="text-muted" style={{ marginTop: 8 }}>
Last action: {lastAction}
</Paragraph>
</SectionWrapper>
<SectionWrapper title="With Role and TabIndex">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Accessibility props for keyboard navigation.
</Paragraph>
<div style={{ display: "flex", gap: 8 }}>
<Clickable
role="button"
tabIndex={0}
onClick={() => setLastAction("Role: button")}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-secondary)",
borderRadius: 4,
cursor: "pointer"
}}
>
role="button" tabIndex=0
</Clickable>
<Clickable
role="link"
tabIndex={0}
onClick={() => setLastAction("Role: link")}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-secondary)",
borderRadius: 4,
cursor: "pointer"
}}
>
role="link" tabIndex=0
</Clickable>
</div>
</SectionWrapper>
<SectionWrapper title="Ignore Key Press">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
When ignoreKeyPress is true, Enter/Space won't trigger onClick.
</Paragraph>
<div style={{ display: "flex", gap: 8 }}>
<Clickable
tabIndex={0}
onClick={() => setLastAction("Normal (keys work)")}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-secondary)",
borderRadius: 4,
cursor: "pointer"
}}
>
Normal (try Enter/Space)
</Clickable>
<Clickable
tabIndex={0}
ignoreKeyPress
onClick={() => setLastAction("Ignored (keys blocked)")}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-secondary)",
borderRadius: 4,
cursor: "pointer"
}}
>
ignoreKeyPress (keys blocked)
</Clickable>
</div>
</SectionWrapper>
<SectionWrapper title="Without onClick">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
When no onClick is provided, renders as non-interactive element.
</Paragraph>
<Clickable
style={{
padding: "8px 16px",
backgroundColor: "var(--background-modifier-accent)",
borderRadius: 4
}}
>
Non-interactive (no onClick)
</Clickable>
</SectionWrapper>
<SectionWrapper title="Styled Examples">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Clickable with different visual styles.
</Paragraph>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<Clickable
onClick={() => setLastAction("Card clicked")}
style={{
padding: 16,
backgroundColor: "var(--background-secondary)",
borderRadius: 8,
cursor: "pointer",
width: 120,
textAlign: "center"
}}
>
<div style={{ fontSize: 24, marginBottom: 4 }}>📁</div>
<div>Card Style</div>
</Clickable>
<Clickable
onClick={() => setLastAction("Chip clicked")}
style={{
padding: "4px 12px",
backgroundColor: "var(--brand-experiment)",
borderRadius: 16,
cursor: "pointer",
color: "white"
}}
>
Chip Style
</Clickable>
<Clickable
onClick={() => setLastAction("List item clicked")}
style={{
padding: "8px 16px",
backgroundColor: "var(--background-secondary)",
cursor: "pointer",
width: 200
}}
>
List Item Style →
</Clickable>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>Clickable</strong> - Interactive element wrapper
</Paragraph>
<Paragraph color="text-muted">• tag?: keyof JSX.IntrinsicElements - HTML element to render (default: div)</Paragraph>
<Paragraph color="text-muted">• onClick?: MouseEventHandler - Click handler</Paragraph>
<Paragraph color="text-muted">• onKeyPress?: KeyboardEventHandler - Key press handler</Paragraph>
<Paragraph color="text-muted">• focusProps?: object - Props for FocusLock wrapper</Paragraph>
<Paragraph color="text-muted">• innerRef?: Ref - Ref to the inner element</Paragraph>
<Paragraph color="text-muted">• role?: string - ARIA role attribute</Paragraph>
<Paragraph color="text-muted">• tabIndex?: number - Tab index for keyboard navigation</Paragraph>
<Paragraph color="text-muted">• ignoreKeyPress?: boolean - Disable Enter/Space triggering onClick</Paragraph>
<Paragraph color="text-muted">• href?: string - Optional href (affects key handling)</Paragraph>
<Paragraph color="text-muted">• className?: string - CSS class name</Paragraph>
<Paragraph color="text-muted">• children?: ReactNode - Child elements</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,267 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ColorPicker, ColorPickerWithSwatches, ColorSwatch, CustomColorButton, DefaultColorButton } from "..";
import { SectionWrapper } from "../SectionWrapper";
const DEFAULT_COLOR = 0x5865F2;
const PRESET_COLORS = [
0x1ABC9C, 0x2ECC71, 0x3498DB, 0x9B59B6, 0xE91E63,
0xF1C40F, 0xE67E22, 0xE74C3C, 0x95A5A6, 0x607D8B,
0x11806A, 0x1F8B4C, 0x206694, 0x71368A, 0xAD1457,
0xC27C0E, 0xA84300, 0x992D22, 0x979C9F, 0x546E7A,
];
function intToHex(color: number): string {
return "#" + color.toString(16).padStart(6, "0");
}
export default function ColorPickerTab() {
const [basicColor, setBasicColor] = useState(DEFAULT_COLOR);
const [suggestedColor, setSuggestedColor] = useState(0xE74C3C);
const [swatchColor, setSwatchColor] = useState(0x3498DB);
const [swatchSelected, setSwatchSelected] = useState(0x2ECC71);
const [customSwatchColor, setCustomSwatchColor] = useState(0x9B59B6);
const [defaultBtnColor, setDefaultBtnColor] = useState(DEFAULT_COLOR);
const [customBtnColor, setCustomBtnColor] = useState(0xE91E63);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic ColorPicker">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Click the swatch to open a full color picker popout. Selected: {intToHex(basicColor)}
</Paragraph>
<ColorPicker
color={basicColor}
onChange={setBasicColor}
showEyeDropper={false}
/>
</SectionWrapper>
<SectionWrapper title="With Eye Dropper">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Includes the eye dropper tool for picking colors from screen.
</Paragraph>
<ColorPicker
color={basicColor}
onChange={setBasicColor}
showEyeDropper={true}
/>
</SectionWrapper>
<SectionWrapper title="With Suggested Colors">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Popout shows suggested color swatches. Selected: {intToHex(suggestedColor)}
</Paragraph>
<ColorPicker
color={suggestedColor}
onChange={setSuggestedColor}
showEyeDropper={false}
suggestedColors={[
"#1ABC9C", "#2ECC71", "#3498DB", "#9B59B6", "#E91E63",
"#F1C40F", "#E67E22", "#E74C3C", "#95A5A6", "#607D8B",
]}
/>
</SectionWrapper>
<SectionWrapper title="Disabled ColorPicker">
<ColorPicker
color={0x5865F2}
onChange={() => { }}
disabled
/>
</SectionWrapper>
<SectionWrapper title="ColorSwatch - Basic">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Individual color swatch buttons. Click to select.
</Paragraph>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{PRESET_COLORS.slice(0, 10).map(color => (
<ColorSwatch
key={color}
color={color}
isSelected={swatchSelected === color}
onClick={() => setSwatchSelected(color)}
/>
))}
</div>
</SectionWrapper>
<SectionWrapper title="ColorSwatch - States">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Different swatch states: default, custom, selected, disabled, no color.
</Paragraph>
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} />
<Paragraph color="text-muted" size="xs">Normal</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} isSelected />
<Paragraph color="text-muted" size="xs">Selected</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} isDefault />
<Paragraph color="text-muted" size="xs">Default</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} isCustom />
<Paragraph color="text-muted" size="xs">Custom</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} disabled />
<Paragraph color="text-muted" size="xs">Disabled</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch />
<Paragraph color="text-muted" size="xs">No Color</Paragraph>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="ColorSwatch - Gradient">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Gradient swatches with customizable angle.
</Paragraph>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<ColorSwatch
isGradient
gradientStart={0xE91E63}
gradientEnd={0x9B59B6}
gradientDegrees={90}
/>
<ColorSwatch
isGradient
gradientStart={0x3498DB}
gradientEnd={0x2ECC71}
gradientDegrees={135}
/>
<ColorSwatch
isGradient
gradientStart={0xF1C40F}
gradientEnd={0xE74C3C}
gradientDegrees={180}
/>
</div>
</SectionWrapper>
<SectionWrapper title="DefaultColorButton">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
The "default" button used in ColorPickerWithSwatches. Shows checkmark when selected.
</Paragraph>
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<DefaultColorButton
color={DEFAULT_COLOR}
value={defaultBtnColor}
onChange={setDefaultBtnColor}
/>
<Paragraph color="text-muted">Selected: {defaultBtnColor === DEFAULT_COLOR ? "Yes" : "No"}</Paragraph>
</div>
</SectionWrapper>
<SectionWrapper title="CustomColorButton">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
The "custom" button for custom color selection. Shows dropper icon.
</Paragraph>
<CustomColorButton
customColor={customBtnColor}
value={customBtnColor}
presets={PRESET_COLORS}
/>
</SectionWrapper>
<SectionWrapper title="ColorPickerWithSwatches">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Full swatch grid with preset colors. Selected: {intToHex(swatchColor)}
</Paragraph>
<ColorPickerWithSwatches
defaultColor={DEFAULT_COLOR}
colors={PRESET_COLORS}
value={swatchColor}
onChange={setSwatchColor}
renderDefaultButton={() => null}
renderCustomButton={() => null}
/>
</SectionWrapper>
<SectionWrapper title="With Default & Custom Buttons">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Full picker with default reset button and custom color picker. Selected: {intToHex(customSwatchColor)}
</Paragraph>
<ColorPickerWithSwatches
defaultColor={DEFAULT_COLOR}
customColor={customSwatchColor}
colors={PRESET_COLORS}
value={customSwatchColor}
onChange={setCustomSwatchColor}
renderDefaultButton={props => (
<DefaultColorButton {...props} />
)}
renderCustomButton={props => (
<ColorPicker
color={props.customColor ?? DEFAULT_COLOR}
onChange={setCustomSwatchColor}
showEyeDropper={false}
/>
)}
/>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>ColorPicker</strong> - Full color picker with popout
</Paragraph>
<Paragraph color="text-muted">• color: number - Current color as integer</Paragraph>
<Paragraph color="text-muted">• onChange: (color: number) ={">"} void - Change callback</Paragraph>
<Paragraph color="text-muted">• onClose?: () ={">"} void - Called when picker closes</Paragraph>
<Paragraph color="text-muted">• suggestedColors?: string[] - Hex color strings for suggestions</Paragraph>
<Paragraph color="text-muted">• disabled?: boolean - Disable interaction</Paragraph>
<Paragraph color="text-muted">• label?: ReactNode - Optional label next to swatch</Paragraph>
<Paragraph color="text-muted">• colorPickerMiddle?: ReactNode - Content in middle of popout</Paragraph>
<Paragraph color="text-muted">• colorPickerFooter?: ReactNode - Content in footer of popout</Paragraph>
<Paragraph color="text-muted">• showEyeDropper?: boolean - Show eye dropper tool</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ColorSwatch</strong> - Individual color swatch button
</Paragraph>
<Paragraph color="text-muted">• color?: number - Color value (null for "no color")</Paragraph>
<Paragraph color="text-muted">• isDefault?: boolean - Larger "default" style</Paragraph>
<Paragraph color="text-muted">• isCustom?: boolean - Shows dropper icon</Paragraph>
<Paragraph color="text-muted">• isSelected?: boolean - Shows checkmark</Paragraph>
<Paragraph color="text-muted">• disabled?: boolean - Disable interaction</Paragraph>
<Paragraph color="text-muted">• onClick?: (color) ={">"} void - Click callback</Paragraph>
<Paragraph color="text-muted">• isGradient?: boolean - Gradient mode</Paragraph>
<Paragraph color="text-muted">• gradientStart/End?: number - Gradient colors</Paragraph>
<Paragraph color="text-muted">• gradientDegrees?: number - Gradient angle (default 180)</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ColorPickerWithSwatches</strong> - Swatch grid with render props
</Paragraph>
<Paragraph color="text-muted">• defaultColor: number - Default/reset color</Paragraph>
<Paragraph color="text-muted">• customColor?: number - Current custom color</Paragraph>
<Paragraph color="text-muted">• colors: number[] - Array of preset colors</Paragraph>
<Paragraph color="text-muted">• value: number - Selected color</Paragraph>
<Paragraph color="text-muted">• onChange: (color: number) ={">"} void - Change callback</Paragraph>
<Paragraph color="text-muted">• renderDefaultButton?: (props) ={">"} ReactNode</Paragraph>
<Paragraph color="text-muted">• renderCustomButton?: (props) ={">"} ReactNode</Paragraph>
<Paragraph color="text-muted">• isGradient?: boolean - Gradient mode</Paragraph>
<Paragraph color="text-muted">• gradientDegrees?: number - Gradient angle</Paragraph>
<Paragraph color="text-muted">• allowBlackCustomColor?: boolean - Allow #000000</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>Color Conversion</strong>
</Paragraph>
<Paragraph color="text-muted">• Hex to int: parseInt(hex.replace("#", ""), 16)</Paragraph>
<Paragraph color="text-muted">• Int to hex: "#" + color.toString(16).padStart(6, "0")</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,140 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaButton, ManaCombobox } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SAMPLE_ITEMS = [
{ id: "1", label: "Apple" },
{ id: "2", label: "Banana" },
{ id: "3", label: "Cherry" },
{ id: "4", label: "Date" },
{ id: "5", label: "Elderberry" },
{ id: "6", label: "Fig" },
{ id: "7", label: "Grape" },
{ id: "8", label: "Honeydew" },
];
function ComboboxItem({ item, isSelected, onClick }: { item: { id: string; label: string; }; isSelected: boolean; onClick: () => void; }) {
return (
<div
onClick={onClick}
style={{
padding: "8px 12px",
cursor: "pointer",
background: isSelected ? "var(--background-modifier-selected)" : "transparent",
borderRadius: 4,
}}
onMouseEnter={e => (e.currentTarget.style.background = "var(--background-modifier-hover)")}
onMouseLeave={e => (e.currentTarget.style.background = isSelected ? "var(--background-modifier-selected)" : "transparent")}
>
{item.label}
</div>
);
}
export default function ComboboxTab() {
const [singleValue, setSingleValue] = useState<string>("");
const [multiValue, setMultiValue] = useState<string[]>([]);
const [showDemo, setShowDemo] = useState(false);
const filterItems = (query: string) =>
SAMPLE_ITEMS.filter(item =>
item.label.toLowerCase().includes(query.toLowerCase())
);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Combobox">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Searchable dropdown with render props pattern. Children is a function
that receives the search query and returns filterable items.
</Paragraph>
<ManaButton
variant="secondary"
text={showDemo ? "Hide Demo" : "Show Demo"}
onClick={() => setShowDemo(!showDemo)}
/>
</SectionWrapper>
{showDemo && (
<>
<SectionWrapper title="Single Selection">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Selected: {singleValue || "None"}
</Paragraph>
<div style={{ maxWidth: 300 }}>
<ManaCombobox
placeholder="Search fruits..."
value={singleValue}
onChange={v => setSingleValue(v as string)}
aria-label="Select a fruit"
maxVisibleItems={5}
emptyStateHeader="No results"
emptyStateText="Try a different search term"
>
{query => filterItems(query).map(item => (
<ComboboxItem
key={item.id}
item={item}
isSelected={singleValue === item.id}
onClick={() => setSingleValue(item.id)}
/>
))}
</ManaCombobox>
</div>
</SectionWrapper>
<SectionWrapper title="Multi Selection">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Selected: {multiValue.length ? multiValue.join(", ") : "None"}
</Paragraph>
<div style={{ maxWidth: 300 }}>
<ManaCombobox
placeholder="Search fruits..."
value={multiValue}
onChange={v => setMultiValue(v as string[])}
multiSelect
aria-label="Select fruits"
maxVisibleItems={5}
>
{query => filterItems(query).map(item => (
<ComboboxItem
key={item.id}
item={item}
isSelected={multiValue.includes(item.id)}
onClick={() => {
if (multiValue.includes(item.id)) {
setMultiValue(multiValue.filter(v => v !== item.id));
} else {
setMultiValue([...multiValue, item.id]);
}
}}
/>
))}
</ManaCombobox>
</div>
</SectionWrapper>
</>
)}
<SectionWrapper title="Props">
<Paragraph color="text-muted">• placeholder - Input placeholder text</Paragraph>
<Paragraph color="text-muted">• value - Selected value(s)</Paragraph>
<Paragraph color="text-muted">• onChange - Selection change callback</Paragraph>
<Paragraph color="text-muted">• multiSelect - Enable multiple selection</Paragraph>
<Paragraph color="text-muted">• autoFocus - Focus input on mount</Paragraph>
<Paragraph color="text-muted">• maxVisibleItems - Max items before scroll (default 5)</Paragraph>
<Paragraph color="text-muted">• emptyStateHeader - Header when no results</Paragraph>
<Paragraph color="text-muted">• emptyStateText - Text when no results</Paragraph>
<Paragraph color="text-muted">• children - Function: (query: string) =&gt; ReactNode[]</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,140 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { openModal } from "@utils/modal";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { useState } from "@webpack/common";
import { ManaButton } from "..";
import { SectionWrapper } from "../SectionWrapper";
const LayerModal = findComponentByCodeLazy('"data-mana-component":"layer-modal"') as React.ComponentType<{
transitionState: number;
animationVariant?: "default" | string;
returnRef?: React.Ref<HTMLDivElement>;
"aria-label"?: string;
onClose: () => void;
children: React.ReactNode;
}>;
const TransitionState = findByPropsLazy("ENTERING", "ENTERED", "EXITING") as {
ENTERING: number;
ENTERED: number;
EXITING: number;
EXITED: number;
HIDDEN: number;
};
function LayerModalDemo({ onClose }: { onClose: () => void; }) {
return (
<LayerModal
transitionState={TransitionState.ENTERED}
onClose={onClose}
aria-label="Demo Layer Modal"
>
<div style={{ padding: 24, minWidth: 300 }}>
<Heading tag="h2" style={{ marginBottom: 8 }}>Layer Modal</Heading>
<Paragraph color="text-muted" style={{ marginBottom: 16 }}>
This is a layer modal. It's used for layered/stacked modals
with different animation behavior than regular modals.
</Paragraph>
<ManaButton variant="primary" text="Close" onClick={onClose} />
</div>
</LayerModal>
);
}
export default function LayerModalTab() {
const [showInline, setShowInline] = useState(false);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Layer Modal">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Layer modals are used for stacked/layered modal experiences.
They have different animation variants and are typically used
when you need to show a modal on top of another modal.
</Paragraph>
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text="Open via openModal"
onClick={() => openModal(props => (
<LayerModalDemo onClose={props.onClose} />
))}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Inline Preview">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Toggle to see the layer modal rendered inline (not recommended for production).
</Paragraph>
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text={showInline ? "Hide Inline" : "Show Inline"}
onClick={() => setShowInline(!showInline)}
/>
</div>
{showInline && (
<div style={{ marginTop: 16, position: "relative", minHeight: 200 }}>
<LayerModal
transitionState={TransitionState.ENTERED}
onClose={() => setShowInline(false)}
aria-label="Inline Demo"
>
<div style={{ padding: 16 }}>
<Paragraph>Inline layer modal content</Paragraph>
</div>
</LayerModal>
</div>
)}
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>LayerModal</strong>
</Paragraph>
<Paragraph color="text-muted">
• transitionState: number - Use TransitionState.ENTERED for visible
</Paragraph>
<Paragraph color="text-muted">
• onClose: () =&gt; void - Close handler
</Paragraph>
<Paragraph color="text-muted">
• children: ReactNode - Modal content
</Paragraph>
<Paragraph color="text-muted">
• animationVariant?: "default" | string - Animation style
</Paragraph>
<Paragraph color="text-muted">
• returnRef?: React.Ref - Ref for the modal element
</Paragraph>
<Paragraph color="text-muted">
• aria-label?: string - Accessibility label
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>TransitionState</strong> - findByPropsLazy("ENTERING", "ENTERED", "EXITING")
</Paragraph>
<Paragraph color="text-muted">
• ENTERING, ENTERED, EXITING, EXITED, HIDDEN
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>Usage Notes</strong>
</Paragraph>
<Paragraph color="text-muted">
• Use with openModal() for proper backdrop and positioning
</Paragraph>
<Paragraph color="text-muted">
• Has built-in animation handling based on transitionState
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,146 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ListboxItem, ManaListbox } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SAMPLE_ITEMS: ListboxItem[] = [
{ id: "1", label: "Apple" },
{ id: "2", label: "Banana" },
{ id: "3", label: "Cherry" },
{ id: "4", label: "Date" },
{ id: "5", label: "Elderberry" },
];
const ITEMS_WITH_DISABLED: ListboxItem[] = [
{ id: "1", label: "Available option" },
{ id: "2", label: "Disabled option", disabled: true },
{ id: "3", label: "Another available" },
{ id: "4", label: "Also disabled", disabled: true },
{ id: "5", label: "Last option" },
];
const MANY_ITEMS: ListboxItem[] = Array.from({ length: 10 }, (_, i) => ({
id: String(i + 1),
label: `Item ${i + 1}`,
}));
export default function ListboxTab() {
const [singleSelected, setSingleSelected] = useState<ListboxItem[]>([SAMPLE_ITEMS[0]]);
const [multiSelected, setMultiSelected] = useState<ListboxItem[]>([SAMPLE_ITEMS[0], SAMPLE_ITEMS[2]]);
const [disabledSelected, setDisabledSelected] = useState<ListboxItem[]>([]);
const [manySelected, setManySelected] = useState<ListboxItem[]>([]);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Single Selection">
<ManaListbox
items={SAMPLE_ITEMS}
selectedItems={singleSelected}
onSelectionChange={setSingleSelected}
selectionMode="single"
/>
</SectionWrapper>
<SectionWrapper title="Multiple Selection">
<ManaListbox
items={SAMPLE_ITEMS}
selectedItems={multiSelected}
onSelectionChange={setMultiSelected}
selectionMode="multiple"
/>
</SectionWrapper>
<SectionWrapper title="With Disabled Items">
<ManaListbox
items={ITEMS_WITH_DISABLED}
selectedItems={disabledSelected}
onSelectionChange={setDisabledSelected}
selectionMode="multiple"
/>
</SectionWrapper>
<SectionWrapper title="Max Visible Items (5)">
<ManaListbox
items={MANY_ITEMS}
selectedItems={manySelected}
onSelectionChange={setManySelected}
selectionMode="multiple"
maxVisibleItems={5}
/>
</SectionWrapper>
<SectionWrapper title="Disabled">
<ManaListbox
items={SAMPLE_ITEMS}
selectedItems={[SAMPLE_ITEMS[1]]}
disabled
/>
</SectionWrapper>
<SectionWrapper title="Loading">
<ManaListbox
items={[]}
loading
/>
</SectionWrapper>
<SectionWrapper title="Empty State">
<ManaListbox
items={[]}
/>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>Listbox</strong>
</Paragraph>
<Paragraph color="text-muted">
• items: ListboxItem[] - Array of items
</Paragraph>
<Paragraph color="text-muted">
• selectedItems?: ListboxItem[] - Currently selected items
</Paragraph>
<Paragraph color="text-muted">
• onSelectionChange?: (items: ListboxItem[]) =&gt; void - Selection callback
</Paragraph>
<Paragraph color="text-muted">
• selectionMode?: "single" | "multiple" - Selection mode
</Paragraph>
<Paragraph color="text-muted">
• maxVisibleItems?: number - Max visible before scroll
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable entire listbox
</Paragraph>
<Paragraph color="text-muted">
• loading?: boolean - Show loading state
</Paragraph>
<Paragraph color="text-muted">
• typeahead?: boolean - Enable keyboard search
</Paragraph>
<Paragraph color="text-muted">
• shouldFocusWrap?: boolean - Wrap focus at ends
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ListboxItem</strong>
</Paragraph>
<Paragraph color="text-muted">
• id: string - Unique item ID
</Paragraph>
<Paragraph color="text-muted">
• label: string - Display label
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable individual item
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,161 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { classNameFactory } from "@utils/css";
import {
ModalContent,
ModalFooter,
ModalHeader,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { findComponentByCodeLazy } from "@webpack";
import { ManaButton } from "..";
import { SectionWrapper } from "../SectionWrapper";
const cl = classNameFactory("vc-compfinder-modal-");
const ModalCloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL") as React.ComponentType<{
onClick: () => void;
className?: string;
withCircleBackground?: boolean;
hideOnFullscreen?: boolean;
}>;
const SIZES = [
{ size: ModalSize.SMALL, label: "Small" },
{ size: ModalSize.MEDIUM, label: "Medium" },
{ size: ModalSize.LARGE, label: "Large" },
{ size: ModalSize.DYNAMIC, label: "Dynamic" },
] as const;
function openDemoModal(size: ModalSize, label: string, showCloseButton = true, showFooter = true) {
openModal(props => (
<ModalRoot {...props} size={size}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<Heading tag="h2">
{label} Modal
</Heading>
<Paragraph color="text-muted">
This is a {label.toLowerCase()} modal using Discord's native modal system.
</Paragraph>
</div>
{showCloseButton && (
<ModalCloseButton
onClick={props.onClose}
className={cl("close")}
/>
)}
</ModalHeader>
<ModalContent className={cl("content")}>
<Paragraph color="text-muted">
Size: {size}
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 8 }}>
The modal header, content, and footer use Discord's native components
from the webpack modules.
</Paragraph>
</ModalContent>
{showFooter && (
<ModalFooter className={cl("footer")}>
<ManaButton variant="primary" text="Confirm" onClick={props.onClose} />
<ManaButton variant="secondary" text="Cancel" onClick={props.onClose} />
</ModalFooter>
)}
</ModalRoot>
));
}
export default function ModalTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Modal Sizes">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Uses ModalRoot, ModalHeader, ModalContent, ModalFooter from @utils/modal
and native ModalCloseButton via findComponentByCodeLazy("CLOSE_BUTTON_LABEL")
</Paragraph>
<div className="vc-compfinder-grid">
{SIZES.map(({ size, label }) => (
<ManaButton
key={size}
variant="secondary"
text={label}
onClick={() => openDemoModal(size, label)}
/>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Without Close Button">
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text="Open Modal"
onClick={() => openDemoModal(ModalSize.SMALL, "No Close Button", false, true)}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Without Footer">
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text="Open Modal"
onClick={() => openDemoModal(ModalSize.SMALL, "No Footer", true, false)}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Minimal (No Close Button or Footer)">
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text="Open Modal"
onClick={() => openDemoModal(ModalSize.DYNAMIC, "Minimal", false, false)}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>ModalRoot</strong> - from @utils/modal
</Paragraph>
<Paragraph color="text-muted">
• size: ModalSize - SMALL, MEDIUM, LARGE, or DYNAMIC
</Paragraph>
<Paragraph color="text-muted">
• ...modalProps - Props from openModal callback
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ModalHeader</strong>
</Paragraph>
<Paragraph color="text-muted">
• separator?: boolean - Show separator line (default: true)
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ModalCloseButton</strong> - findComponentByCodeLazy("CLOSE_BUTTON_LABEL")
</Paragraph>
<Paragraph color="text-muted">
• onClick: () =&gt; void - Close handler
</Paragraph>
<Paragraph color="text-muted">
• withCircleBackground?: boolean - Circle background style
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>openModal</strong> - from @utils/modal
</Paragraph>
<Paragraph color="text-muted">
• (render: (props) =&gt; ReactNode) =&gt; string - Returns modal key
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,132 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button } from "@components/Button";
import { Paragraph } from "@components/Paragraph";
import { Notice, NoticeType } from "..";
import { SectionWrapper } from "../SectionWrapper";
const NOTICE_TYPES: NoticeType[] = ["info", "warn", "danger", "positive", "preview"];
export default function NoticeTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Message Types">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Notice component for displaying informational messages with different severity levels.
</Paragraph>
<div className="vc-compfinder-grid">
{NOTICE_TYPES.map(type => (
<div key={type} style={{ textAlign: "center" }}>
<Notice messageType={type}>
This is a {type} notice message.
</Notice>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
{type}
</Paragraph>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title="With Action">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Notices can include action buttons for user interaction.
</Paragraph>
<div className="vc-compfinder-grid">
<Notice
messageType="info"
action={<Button size="small" variant="secondary">Action</Button>}
>
Notice with an action button.
</Notice>
<Notice
messageType="warn"
action={<Button size="small" variant="primary">Fix Now</Button>}
>
Warning that requires attention.
</Notice>
</div>
</SectionWrapper>
<SectionWrapper title="Hidden State">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
The hidden prop controls visibility without removing from DOM.
</Paragraph>
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center" }}>
<Notice messageType="info" hidden={false}>
This notice is visible.
</Notice>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
hidden=false
</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<Notice messageType="info" hidden={true}>
This notice is hidden.
</Notice>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
hidden=true
</Paragraph>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Text Variants">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Custom text styling via the textVariant prop.
</Paragraph>
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center" }}>
<Notice messageType="info">
Default text variant.
</Notice>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
text-sm/medium (default)
</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<Notice messageType="info" textVariant="text-md/normal">
Larger text variant.
</Notice>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
text-md/normal
</Paragraph>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• children: ReactNode - Notice content
</Paragraph>
<Paragraph color="text-muted">
• messageType: "info" | "warn" | "danger" | "positive" | "preview"
</Paragraph>
<Paragraph color="text-muted">
• action?: ReactNode - Action button or element
</Paragraph>
<Paragraph color="text-muted">
• hidden?: boolean - Hide the notice
</Paragraph>
<Paragraph color="text-muted">
• textColor?: string - Custom text color
</Paragraph>
<Paragraph color="text-muted">
• textVariant?: string - Text size/weight variant
</Paragraph>
<Paragraph color="text-muted">
• icon?: ComponentType - Custom icon component
</Paragraph>
<Paragraph color="text-muted">
• className?: string - Additional CSS class
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,125 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { Paginator2 as Paginator } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function PaginatorTab() {
const [page1, setPage1] = useState(1);
const [page2, setPage2] = useState(1);
const [page3, setPage3] = useState(1);
const [page4, setPage4] = useState(1);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Pagination component for navigating through pages of content.
</Paragraph>
<Paginator
currentPage={page1}
totalCount={100}
pageSize={10}
onPageChange={setPage1}
/>
</SectionWrapper>
<SectionWrapper title="Max Visible Pages">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Control how many page numbers are visible at once.
</Paragraph>
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center" }}>
<Paginator
currentPage={page2}
totalCount={100}
pageSize={10}
maxVisiblePages={3}
onPageChange={setPage2}
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
3 visible
</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<Paginator
currentPage={page3}
totalCount={100}
pageSize={10}
maxVisiblePages={7}
onPageChange={setPage3}
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
7 visible
</Paragraph>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Options">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Additional configuration options for the paginator.
</Paragraph>
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center" }}>
<Paginator
currentPage={page4}
totalCount={100}
pageSize={10}
hideMaxPage
onPageChange={setPage4}
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
hideMaxPage
</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<Paginator
currentPage={page4}
totalCount={50}
pageSize={10}
disablePaginationGap
onPageChange={setPage4}
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
disablePaginationGap
</Paragraph>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• currentPage: number - Current active page
</Paragraph>
<Paragraph color="text-muted">
• totalCount: number - Total number of items
</Paragraph>
<Paragraph color="text-muted">
• pageSize: number - Items per page
</Paragraph>
<Paragraph color="text-muted">
• onPageChange?: (page: number) =&gt; void - Page change callback
</Paragraph>
<Paragraph color="text-muted">
• maxVisiblePages?: number - Max page numbers shown
</Paragraph>
<Paragraph color="text-muted">
• hideMaxPage?: boolean - Hide last page number
</Paragraph>
<Paragraph color="text-muted">
• disablePaginationGap?: boolean - Remove ellipsis gaps
</Paragraph>
<Paragraph color="text-muted">
• className?: string - Additional CSS class
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,153 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useRef, useState } from "@webpack/common";
import { ManaButton, ManaPopover, PopoverAction } from "..";
import { SectionWrapper } from "../SectionWrapper";
function PopoverDemo({ title, body, size, actions, position, label }: { title: string; body: string; size?: "sm" | "md" | "lg"; actions?: PopoverAction[]; position?: "top" | "bottom" | "left" | "right"; label: string; }) {
const wrapperRef = useRef<HTMLDivElement>(null);
const [show, setShow] = useState(false);
return (
<>
<div ref={wrapperRef} style={{ display: "inline-block" }}>
<ManaButton
variant="secondary"
text={label}
onClick={() => setShow(!show)}
/>
</div>
<ManaPopover
targetElementRef={wrapperRef}
shouldShow={show}
onRequestClose={() => setShow(false)}
title={title}
body={body}
size={size}
actions={actions}
position={position}
/>
</>
);
}
export default function PopoverTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Popover">
<div className="vc-compfinder-grid">
<PopoverDemo
label="Click me"
title="Popover Title"
body="This is the popover body text with some helpful information."
/>
</div>
</SectionWrapper>
<SectionWrapper title="Sizes">
<div className="vc-compfinder-grid">
<PopoverDemo
label="sm"
title="Small Popover"
body="This is a small popover."
size="sm"
/>
<PopoverDemo
label="md"
title="Medium Popover"
body="This is a medium popover (default)."
size="md"
/>
<PopoverDemo
label="lg"
title="Large Popover"
body="This is a large popover with more space."
size="lg"
/>
</div>
</SectionWrapper>
<SectionWrapper title="Positions">
<div className="vc-compfinder-grid">
<PopoverDemo
label="top"
title="Top Position"
body="Popover appears above."
position="top"
/>
<PopoverDemo
label="bottom"
title="Bottom Position"
body="Popover appears below."
position="bottom"
/>
<PopoverDemo
label="left"
title="Left Position"
body="Popover appears to the left."
position="left"
/>
<PopoverDemo
label="right"
title="Right Position"
body="Popover appears to the right."
position="right"
/>
</div>
</SectionWrapper>
<SectionWrapper title="With Actions">
<div className="vc-compfinder-grid">
<PopoverDemo
label="With Actions"
title="Popover with Actions"
body="This popover has action buttons."
actions={[
{ text: "Primary", variant: "primary" },
{ text: "Secondary", variant: "secondary" },
]}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• targetElementRef: React.RefObject - Ref to trigger element
</Paragraph>
<Paragraph color="text-muted">
• shouldShow: boolean - Controls visibility
</Paragraph>
<Paragraph color="text-muted">
• onRequestClose?: (reason: string) =&gt; void - Close callback
</Paragraph>
<Paragraph color="text-muted">
• title?: string - Popover title
</Paragraph>
<Paragraph color="text-muted">
• body?: string - Popover body text
</Paragraph>
<Paragraph color="text-muted">
• size?: "sm" | "md" | "lg" - Popover size
</Paragraph>
<Paragraph color="text-muted">
• position?: "top" | "bottom" | "left" | "right" - Position
</Paragraph>
<Paragraph color="text-muted">
• actions?: PopoverAction[] - Action buttons
</Paragraph>
<Paragraph color="text-muted">
• textLink?: {"{"} text: string, onClick?: () =&gt; void {"}"} - Link
</Paragraph>
<Paragraph color="text-muted">
• graphic?: {"{"} src: string, aspectRatio?: string {"}"} - Image
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,119 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useEffect, useState } from "@webpack/common";
import { ProgressBar } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function ProgressBarTab() {
const [animatedProgress, setAnimatedProgress] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setAnimatedProgress(prev => (prev >= 100 ? 0 : prev + 5));
}, 200);
return () => clearInterval(interval);
}, []);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Variants">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Blue (default) and Orange variants.
</Paragraph>
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Blue:</Paragraph>
<ProgressBar progress={60} variant="blue" />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Orange:</Paragraph>
<ProgressBar progress={60} variant="orange" />
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Progress Values">
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>0%:</Paragraph>
<ProgressBar progress={0} variant="blue" />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>25%:</Paragraph>
<ProgressBar progress={25} variant="blue" />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>50%:</Paragraph>
<ProgressBar progress={50} variant="blue" />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>75%:</Paragraph>
<ProgressBar progress={75} variant="blue" />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>100%:</Paragraph>
<ProgressBar progress={100} variant="blue" />
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Animated">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Progress: {animatedProgress}%
</Paragraph>
<ProgressBar progress={animatedProgress} variant="blue" />
</SectionWrapper>
<SectionWrapper title="Custom Range">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Using minimum=0 and maximum=200, progress=150 (75% filled).
</Paragraph>
<ProgressBar progress={150} minimum={0} maximum={200} variant="orange" />
</SectionWrapper>
<SectionWrapper title="Custom Override">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Using variant="unset" with custom gradient colors.
</Paragraph>
<ProgressBar
progress={70}
variant="unset"
override={{
default: {
background: "rgba(88, 101, 242, 0.3)",
gradientStart: "#5865F2",
gradientEnd: "#EB459E"
}
}}
/>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• progress: number - Current progress value
</Paragraph>
<Paragraph color="text-muted">
• minimum?: number - Minimum value (default: 0)
</Paragraph>
<Paragraph color="text-muted">
• maximum?: number - Maximum value (default: 100)
</Paragraph>
<Paragraph color="text-muted">
• variant?: "blue" | "orange" | "unset" - Color variant
</Paragraph>
<Paragraph color="text-muted">
• override?: object - Custom colors when variant="unset"
</Paragraph>
<Paragraph color="text-muted">
• labelledBy?: string - ID of labelling element for a11y
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,193 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaBaseRadioGroup, RadioOption, StandaloneRadioIndicator } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";
const BASIC_OPTIONS: RadioOption[] = [
{ value: "option1", name: "Option 1" },
{ value: "option2", name: "Option 2" },
{ value: "option3", name: "Option 3" },
];
const OPTIONS_WITH_DESCRIPTIONS: RadioOption[] = [
{ value: "low", name: "Low Quality", desc: "Faster loading, uses less data" },
{ value: "medium", name: "Medium Quality", desc: "Balanced quality and performance" },
{ value: "high", name: "High Quality", desc: "Best quality, uses more data" },
];
const OPTIONS_WITH_ICONS: RadioOption[] = [
{ value: "item1", name: "Item with icon", desc: "This option has an icon", leadingIcon: EquicordIcon },
{ value: "item2", name: "Another with icon", leadingIcon: EquicordIcon },
{ value: "item3", name: "Third with icon", leadingIcon: EquicordIcon },
];
const OPTIONS_MIXED: RadioOption[] = [
{ value: "enabled", name: "Enabled option" },
{ value: "disabled", name: "Disabled option", disabled: true },
{ value: "another", name: "Another enabled" },
];
const NUMBERED_OPTIONS: RadioOption[] = [
{ value: 1, name: "First choice" },
{ value: 2, name: "Second choice" },
{ value: 3, name: "Third choice" },
];
export default function RadioGroupTab() {
const [basicValue, setBasicValue] = useState<string | number>("option1");
const [descValue, setDescValue] = useState<string | number>("medium");
const [iconValue, setIconValue] = useState<string | number>("item1");
const [mixedValue, setMixedValue] = useState<string | number>("enabled");
const [numberedValue, setNumberedValue] = useState<number>(1);
const [unselectedValue, setUnselectedValue] = useState<string | number | undefined>(undefined);
const [standaloneSelected, setStandaloneSelected] = useState(false);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Simple radio group with text-only options.
</Paragraph>
<ManaBaseRadioGroup
options={BASIC_OPTIONS}
value={basicValue}
onChange={setBasicValue}
/>
</SectionWrapper>
<SectionWrapper title="With Descriptions">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Radio options with descriptive text using the desc prop.
</Paragraph>
<ManaBaseRadioGroup
options={OPTIONS_WITH_DESCRIPTIONS}
value={descValue}
onChange={setDescValue}
/>
</SectionWrapper>
<SectionWrapper title="With Leading Icons">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Radio options with icons using leadingIcon prop.
</Paragraph>
<ManaBaseRadioGroup
options={OPTIONS_WITH_ICONS}
value={iconValue}
onChange={setIconValue}
/>
</SectionWrapper>
<SectionWrapper title="With Disabled Option">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Individual options can be disabled with disabled: true.
</Paragraph>
<ManaBaseRadioGroup
options={OPTIONS_MIXED}
value={mixedValue}
onChange={setMixedValue}
/>
</SectionWrapper>
<SectionWrapper title="With Numeric Values">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Radio options can use numeric values.
</Paragraph>
<ManaBaseRadioGroup
options={NUMBERED_OPTIONS}
value={numberedValue}
onChange={v => setNumberedValue(v as number)}
/>
</SectionWrapper>
<SectionWrapper title="No Initial Selection">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Radio group with no initial selection (value undefined).
</Paragraph>
<ManaBaseRadioGroup
options={BASIC_OPTIONS}
value={unselectedValue}
onChange={setUnselectedValue}
/>
</SectionWrapper>
<SectionWrapper title="Entire Group Disabled">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
The entire group can be disabled with disabled prop.
</Paragraph>
<ManaBaseRadioGroup
options={BASIC_OPTIONS}
value="option2"
disabled
/>
</SectionWrapper>
<SectionWrapper title="Standalone Radio Indicator">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
A standalone animated radio indicator component. Click to toggle.
</Paragraph>
<div
onClick={() => setStandaloneSelected(!standaloneSelected)}
style={{ cursor: "pointer", display: "inline-block" }}
>
<StandaloneRadioIndicator isSelected={standaloneSelected} />
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>BaseRadioGroup</strong>
</Paragraph>
<Paragraph color="text-muted">
• options: RadioOption[] - Array of radio options
</Paragraph>
<Paragraph color="text-muted">
• value?: string | number - Currently selected value
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (value: string | number) =&gt; void - Called when selection changes
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable entire group
</Paragraph>
<Paragraph color="text-muted">
• aria-labelledby?: string - Accessibility label reference
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>RadioOption</strong>
</Paragraph>
<Paragraph color="text-muted">
• value: string | number - Option value
</Paragraph>
<Paragraph color="text-muted">
• name: string - Option label text
</Paragraph>
<Paragraph color="text-muted">
• desc?: string - Optional description text
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable individual option
</Paragraph>
<Paragraph color="text-muted">
• leadingIcon?: React.ComponentType - Icon before radio
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>StandaloneRadioIndicator</strong>
</Paragraph>
<Paragraph color="text-muted">
• isSelected: boolean - Whether the indicator is selected
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable the indicator
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,180 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { ManaButton, ManaRichTooltip } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function RichTooltipTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Rich Tooltip - Basic">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Rich tooltip with title and body text.
</Paragraph>
<div className="vc-compfinder-grid">
<ManaRichTooltip
title="Tooltip Title"
body="This is the body text of the rich tooltip with more detailed information."
asContainer
>
<ManaButton variant="secondary" text="Hover me" />
</ManaRichTooltip>
</div>
</SectionWrapper>
<SectionWrapper title="Rich Tooltip - Body Only">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Rich tooltip without title.
</Paragraph>
<div className="vc-compfinder-grid">
<ManaRichTooltip
body="This tooltip only has body text, no title."
asContainer
>
<ManaButton variant="secondary" text="Body Only" />
</ManaRichTooltip>
</div>
</SectionWrapper>
<SectionWrapper title="Positions">
<div className="vc-compfinder-grid">
<ManaRichTooltip
title="Top"
body="Positioned at top"
position="top"
asContainer
>
<ManaButton variant="secondary" text="Top" />
</ManaRichTooltip>
<ManaRichTooltip
title="Bottom"
body="Positioned at bottom"
position="bottom"
asContainer
>
<ManaButton variant="secondary" text="Bottom" />
</ManaRichTooltip>
<ManaRichTooltip
title="Left"
body="Positioned at left"
position="left"
asContainer
>
<ManaButton variant="secondary" text="Left" />
</ManaRichTooltip>
<ManaRichTooltip
title="Right"
body="Positioned at right"
position="right"
asContainer
>
<ManaButton variant="secondary" text="Right" />
</ManaRichTooltip>
</div>
</SectionWrapper>
<SectionWrapper title="Alignment">
<div className="vc-compfinder-grid">
<ManaRichTooltip
title="Start Aligned"
body="Tooltip aligned to start"
align="start"
asContainer
>
<ManaButton variant="secondary" text="Start" />
</ManaRichTooltip>
<ManaRichTooltip
title="Center Aligned"
body="Tooltip aligned to center"
align="center"
asContainer
>
<ManaButton variant="secondary" text="Center" />
</ManaRichTooltip>
<ManaRichTooltip
title="End Aligned"
body="Tooltip aligned to end"
align="end"
asContainer
>
<ManaButton variant="secondary" text="End" />
</ManaRichTooltip>
</div>
</SectionWrapper>
<SectionWrapper title="With Custom Spacing">
<div className="vc-compfinder-grid">
<ManaRichTooltip
title="More Spacing"
body="This tooltip has increased spacing from the target element."
spacing={16}
asContainer
>
<ManaButton variant="secondary" text="16px Spacing" />
</ManaRichTooltip>
<ManaRichTooltip
title="Less Spacing"
body="This tooltip has minimal spacing."
spacing={4}
asContainer
>
<ManaButton variant="secondary" text="4px Spacing" />
</ManaRichTooltip>
</div>
</SectionWrapper>
<SectionWrapper title="Container Element Types">
<div className="vc-compfinder-grid">
<ManaRichTooltip
title="Span Container"
body="Uses span as container element (default)"
element="span"
asContainer
>
<ManaButton variant="secondary" text="Span" />
</ManaRichTooltip>
<ManaRichTooltip
title="Div Container"
body="Uses div as container element"
element="div"
asContainer
>
<ManaButton variant="secondary" text="Div" />
</ManaRichTooltip>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• title?: string - Tooltip title (bold)
</Paragraph>
<Paragraph color="text-muted">
• body?: string - Tooltip body text
</Paragraph>
<Paragraph color="text-muted">
• children: ReactNode - Trigger element
</Paragraph>
<Paragraph color="text-muted">
• position?: "top" | "bottom" | "left" | "right" - Position
</Paragraph>
<Paragraph color="text-muted">
• align?: "start" | "center" | "end" - Alignment
</Paragraph>
<Paragraph color="text-muted">
• spacing?: number - Distance from trigger (px)
</Paragraph>
<Paragraph color="text-muted">
• asContainer?: boolean - Wrap children in container
</Paragraph>
<Paragraph color="text-muted">
• element?: "span" | "div" - Container element type
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,81 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { SearchBar } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function SearchBarTab() {
const [query1, setQuery1] = useState("");
const [query2, setQuery2] = useState("");
const [query3, setQuery3] = useState("prefilled");
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Default search bar with clear button.
</Paragraph>
<SearchBar
query={query1}
onChange={setQuery1}
onClear={() => setQuery1("")}
placeholder="Search..."
/>
</SectionWrapper>
<SectionWrapper title="Sizes">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Available sizes: sm, md (default).
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<SearchBar
query={query2}
onChange={setQuery2}
onClear={() => setQuery2("")}
placeholder="Small size"
size="sm"
/>
<SearchBar
query={query2}
onChange={setQuery2}
onClear={() => setQuery2("")}
placeholder="Medium size"
size="md"
/>
</div>
</SectionWrapper>
<SectionWrapper title="States">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Disabled and prefilled states.
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<SearchBar
query=""
onChange={() => { }}
placeholder="Disabled"
disabled
/>
<SearchBar
query={query3}
onChange={setQuery3}
onClear={() => setQuery3("")}
placeholder="Prefilled"
/>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
query, onChange, onClear?, placeholder?, size?, autoFocus?, disabled?, onKeyDown?, onBlur?, onFocus?, autoComplete?, inputProps?, aria-label?
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,123 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaSelect, ManaSelectOption } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SAMPLE_OPTIONS: ManaSelectOption[] = [
{ id: "1", value: "option1", label: "Option 1" },
{ id: "2", value: "option2", label: "Option 2" },
{ id: "3", value: "option3", label: "Option 3" },
{ id: "4", value: "option4", label: "Option 4" },
];
export default function SelectTab() {
const [singleValue, setSingleValue] = useState<string>("option1");
const [multiValue, setMultiValue] = useState<string[]>(["option1", "option2"]);
const [clearableValue, setClearableValue] = useState<string>("option1");
const [placeholderValue, setPlaceholderValue] = useState<string>("");
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Single Selection">
<ManaSelect
options={SAMPLE_OPTIONS}
value={singleValue}
onSelectionChange={v => setSingleValue(v as string)}
selectionMode="single"
/>
</SectionWrapper>
<SectionWrapper title="Multiple Selection">
<ManaSelect
options={SAMPLE_OPTIONS}
value={multiValue}
onSelectionChange={v => setMultiValue(v as string[])}
selectionMode="multiple"
/>
</SectionWrapper>
<SectionWrapper title="With Placeholder">
<ManaSelect
options={SAMPLE_OPTIONS}
value={placeholderValue}
onSelectionChange={v => setPlaceholderValue(v as string)}
placeholder="Select an option..."
/>
</SectionWrapper>
<SectionWrapper title="Clearable">
<ManaSelect
options={SAMPLE_OPTIONS}
value={clearableValue}
onSelectionChange={v => setClearableValue(v as string)}
clearable
/>
</SectionWrapper>
<SectionWrapper title="Full Width">
<ManaSelect
options={SAMPLE_OPTIONS}
value="option1"
fullWidth
/>
</SectionWrapper>
<SectionWrapper title="Disabled">
<ManaSelect
options={SAMPLE_OPTIONS}
value="option2"
disabled
/>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>ManaSelect</strong> - Note: Uses onSelectionChange, NOT onChange.
</Paragraph>
<Paragraph color="text-muted">
• options: ManaSelectOption[] - Array of options
</Paragraph>
<Paragraph color="text-muted">
• value?: string | string[] | null - Selected value(s)
</Paragraph>
<Paragraph color="text-muted">
• onSelectionChange?: (value) =&gt; void - Called when selection changes
</Paragraph>
<Paragraph color="text-muted">
• selectionMode?: "single" | "multiple" - Selection mode
</Paragraph>
<Paragraph color="text-muted">
• placeholder?: string - Placeholder text
</Paragraph>
<Paragraph color="text-muted">
• clearable?: boolean - Allow clearing selection
</Paragraph>
<Paragraph color="text-muted">
• fullWidth?: boolean - Expand to full width
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable select
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ManaSelectOption</strong>
</Paragraph>
<Paragraph color="text-muted">
• id: string - Unique option ID
</Paragraph>
<Paragraph color="text-muted">
• value: string - Option value
</Paragraph>
<Paragraph color="text-muted">
• label: string - Display label
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,110 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { Slider } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function SliderTab() {
const [basicValue, setBasicValue] = useState(50);
const [markerValue, setMarkerValue] = useState(50);
const [miniValue, setMiniValue] = useState(30);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Slider">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Value: {basicValue}
</Paragraph>
<Slider
initialValue={basicValue}
minValue={0}
maxValue={100}
onValueChange={setBasicValue}
/>
</SectionWrapper>
<SectionWrapper title="With Markers">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Value: {markerValue}
</Paragraph>
<Slider
initialValue={markerValue}
minValue={0}
maxValue={100}
markers={[0, 25, 50, 75, 100]}
onValueChange={setMarkerValue}
onMarkerRender={(v: number) => `${v}%`}
/>
</SectionWrapper>
<SectionWrapper title="Stick to Markers">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Value snaps to markers.
</Paragraph>
<Slider
initialValue={50}
minValue={0}
maxValue={100}
markers={[0, 25, 50, 75, 100]}
stickToMarkers
onMarkerRender={(v: number) => `${v}%`}
/>
</SectionWrapper>
<SectionWrapper title="Mini Slider">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Value: {miniValue}
</Paragraph>
<div style={{ width: 200 }}>
<Slider
initialValue={miniValue}
minValue={0}
maxValue={100}
mini
onValueChange={setMiniValue}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Disabled">
<Slider
initialValue={60}
minValue={0}
maxValue={100}
disabled
/>
</SectionWrapper>
<SectionWrapper title="Custom Range">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Range: -50 to 50
</Paragraph>
<Slider
initialValue={0}
minValue={-50}
maxValue={50}
markers={[-50, -25, 0, 25, 50]}
onMarkerRender={(v: number) => v.toString()}
/>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• initialValue / value - Current value</Paragraph>
<Paragraph color="text-muted">• minValue / maxValue - Range bounds</Paragraph>
<Paragraph color="text-muted">• onValueChange - Change callback</Paragraph>
<Paragraph color="text-muted">• markers - Array of marker values</Paragraph>
<Paragraph color="text-muted">• stickToMarkers - Snap to markers</Paragraph>
<Paragraph color="text-muted">• onMarkerRender - Custom marker labels</Paragraph>
<Paragraph color="text-muted">• mini - Compact style</Paragraph>
<Paragraph color="text-muted">• hideBubble - Hide value tooltip</Paragraph>
<Paragraph color="text-muted">• disabled - Disable interaction</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,95 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaButton, Spinner } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SPINNER_TYPES = [
"wanderingCubes",
"chasingDots",
"pulsingEllipsis",
"spinningCircle",
"spinningCircleSimple",
"lowMotion"
] as const;
export default function SpinnerTab() {
const [animated, setAnimated] = useState(true);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Spinner Types">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
All available spinner variants.
</Paragraph>
<div className="vc-compfinder-grid">
{SPINNER_TYPES.map(type => (
<div key={type} style={{ textAlign: "center", padding: 16 }}>
<Spinner type={type} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 8 }}>
{type}
</Paragraph>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Animation Toggle">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Spinners can be paused with animated=false.
</Paragraph>
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text={animated ? "Pause Animation" : "Resume Animation"}
onClick={() => setAnimated(!animated)}
/>
</div>
<div className="vc-compfinder-grid" style={{ marginTop: 16 }}>
<div style={{ textAlign: "center", padding: 16 }}>
<Spinner type="spinningCircle" animated={animated} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 8 }}>
animated={animated.toString()}
</Paragraph>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Reduced Motion">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
lowMotion type is used when user prefers reduced motion.
The component automatically switches to this when reducedMotion is enabled.
</Paragraph>
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center", padding: 16 }}>
<Spinner type="lowMotion" />
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• type?: "wanderingCubes" | "chasingDots" | "pulsingEllipsis" | "spinningCircle" | "spinningCircleSimple" | "lowMotion"
</Paragraph>
<Paragraph color="text-muted">
• animated?: boolean - Enable/disable animation (default: true)
</Paragraph>
<Paragraph color="text-muted">
• className?: string - Container class
</Paragraph>
<Paragraph color="text-muted">
• itemClassName?: string - Inner item class
</Paragraph>
<Paragraph color="text-muted">
• aria-label?: string - Accessibility label
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,131 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { findComponentByCodeLazy } from "@webpack";
import { useState } from "@webpack/common";
import { ManaSwitch } from "..";
import { SectionWrapper } from "../SectionWrapper";
interface SwitchWithLabelProps {
checked: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
label?: string;
description?: string;
}
const SwitchWithLabel = findComponentByCodeLazy('"data-toggleable-component":"switch"') as React.ComponentType<SwitchWithLabelProps>;
export default function SwitchTab() {
const [basicOn, setBasicOn] = useState(true);
const [basicOff, setBasicOff] = useState(false);
const [labelOn, setLabelOn] = useState(true);
const [labelOff, setLabelOff] = useState(false);
const [withDesc, setWithDesc] = useState(true);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Switch (ManaSwitch)">
<div className="vc-compfinder-grid">
<div className="vc-compfinder-switch-item">
<span>On:</span>
<ManaSwitch checked={basicOn} onChange={setBasicOn} />
</div>
<div className="vc-compfinder-switch-item">
<span>Off:</span>
<ManaSwitch checked={basicOff} onChange={setBasicOff} />
</div>
<div className="vc-compfinder-switch-item">
<span>Disabled On:</span>
<ManaSwitch checked={true} disabled />
</div>
<div className="vc-compfinder-switch-item">
<span>Disabled Off:</span>
<ManaSwitch checked={false} disabled />
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Switch with Label">
<div className="vc-compfinder-grid-vertical">
<SwitchWithLabel
label="Enabled option"
checked={labelOn}
onChange={setLabelOn}
/>
<SwitchWithLabel
label="Disabled option"
checked={labelOff}
onChange={setLabelOff}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Switch with Description">
<div className="vc-compfinder-grid-vertical">
<SwitchWithLabel
label="Feature toggle"
description="Enable this feature to unlock additional functionality."
checked={withDesc}
onChange={setWithDesc}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Disabled States">
<div className="vc-compfinder-grid-vertical">
<SwitchWithLabel
label="Disabled (checked)"
checked={true}
disabled
/>
<SwitchWithLabel
label="Disabled (unchecked)"
checked={false}
disabled
/>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>ManaSwitch</strong> - Basic toggle switch without label.
</Paragraph>
<Paragraph color="text-muted">
• checked: boolean - Current switch state
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (checked: boolean) =&gt; void - Called when toggled
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable interaction
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>SwitchWithLabel</strong> - Switch with label and description.
</Paragraph>
<Paragraph color="text-muted">
• label?: string - Label text displayed next to switch
</Paragraph>
<Paragraph color="text-muted">
• description?: string - Description text below label
</Paragraph>
<Paragraph color="text-muted">
• checked: boolean - Current switch state
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (checked: boolean) =&gt; void - Called when toggled
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable interaction
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,191 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { TabBar } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function TabBarTab() {
const [sideTab, setSideTab] = useState("tab1");
const [topTab, setTopTab] = useState("tab1");
const [topPillTab, setTopPillTab] = useState("tab1");
const [brandTab, setBrandTab] = useState("tab1");
const [colorTab, setColorTab] = useState("tab1");
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Side TabBar (Default)">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Default side-style tab bar with grey look. Selected: {sideTab}
</Paragraph>
<TabBar
type="side"
look="grey"
selectedItem={sideTab}
onItemSelect={setSideTab}
>
<TabBar.Item id="tab1">First Tab</TabBar.Item>
<TabBar.Item id="tab2">Second Tab</TabBar.Item>
<TabBar.Item id="tab3">Third Tab</TabBar.Item>
</TabBar>
</SectionWrapper>
<SectionWrapper title="Top TabBar">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Top-style tab bar with underline indicator. Selected: {topTab}
</Paragraph>
<TabBar
type="top"
look="grey"
selectedItem={topTab}
onItemSelect={setTopTab}
>
<TabBar.Item id="tab1">Overview</TabBar.Item>
<TabBar.Item id="tab2">Settings</TabBar.Item>
<TabBar.Item id="tab3">Members</TabBar.Item>
</TabBar>
</SectionWrapper>
<SectionWrapper title="Top Pill TabBar">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Pill-style tabs for top navigation. Selected: {topPillTab}
</Paragraph>
<TabBar
type="top-pill"
look="grey"
selectedItem={topPillTab}
onItemSelect={setTopPillTab}
>
<TabBar.Item id="tab1">All</TabBar.Item>
<TabBar.Item id="tab2">Online</TabBar.Item>
<TabBar.Item id="tab3">Pending</TabBar.Item>
<TabBar.Item id="tab4">Blocked</TabBar.Item>
</TabBar>
</SectionWrapper>
<SectionWrapper title="Brand Look">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Brand-colored tab bar style. Selected: {brandTab}
</Paragraph>
<TabBar
type="top"
look="brand"
selectedItem={brandTab}
onItemSelect={setBrandTab}
>
<TabBar.Item id="tab1">Home</TabBar.Item>
<TabBar.Item id="tab2">Explore</TabBar.Item>
<TabBar.Item id="tab3">Library</TabBar.Item>
</TabBar>
</SectionWrapper>
<SectionWrapper title="With Header and Separator">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Tab bar with header section and separator.
</Paragraph>
<TabBar
type="side"
look="grey"
selectedItem={sideTab}
onItemSelect={setSideTab}
>
<TabBar.Header>User Settings</TabBar.Header>
<TabBar.Item id="tab1">My Account</TabBar.Item>
<TabBar.Item id="tab2">Privacy</TabBar.Item>
<TabBar.Separator />
<TabBar.Header>App Settings</TabBar.Header>
<TabBar.Item id="tab3">Appearance</TabBar.Item>
<TabBar.Item id="tab4">Notifications</TabBar.Item>
</TabBar>
</SectionWrapper>
<SectionWrapper title="With Disabled Items">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Some tabs can be disabled.
</Paragraph>
<TabBar
type="top"
look="grey"
selectedItem={topTab}
onItemSelect={setTopTab}
>
<TabBar.Item id="tab1">Active</TabBar.Item>
<TabBar.Item id="tab2" disabled>Disabled</TabBar.Item>
<TabBar.Item id="tab3">Another Active</TabBar.Item>
</TabBar>
</SectionWrapper>
<SectionWrapper title="With Custom Colors">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Individual tabs can have custom colors. Selected: {colorTab}
</Paragraph>
<TabBar
type="top"
look="grey"
selectedItem={colorTab}
onItemSelect={setColorTab}
>
<TabBar.Item id="tab1" color="#43b581">Green</TabBar.Item>
<TabBar.Item id="tab2" color="#faa61a">Yellow</TabBar.Item>
<TabBar.Item id="tab3" color="#f04747">Red</TabBar.Item>
</TabBar>
</SectionWrapper>
<SectionWrapper title="Destructive Variant">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Tab with destructive/danger styling.
</Paragraph>
<TabBar
type="side"
look="grey"
selectedItem={sideTab}
onItemSelect={setSideTab}
>
<TabBar.Item id="tab1">Settings</TabBar.Item>
<TabBar.Item id="tab2">Privacy</TabBar.Item>
<TabBar.Item id="logout" variant="destructive">Log Out</TabBar.Item>
</TabBar>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>TabBar</strong> - Container component
</Paragraph>
<Paragraph color="text-muted">• type?: "side" | "top" | "top-pill" - Tab bar style</Paragraph>
<Paragraph color="text-muted">• look?: "brand" | "grey" - Color scheme</Paragraph>
<Paragraph color="text-muted">• selectedItem?: string - Currently selected tab ID</Paragraph>
<Paragraph color="text-muted">• onItemSelect?: (id) ={">"} void - Selection callback</Paragraph>
<Paragraph color="text-muted">• orientation?: "horizontal" | "vertical" - Layout direction</Paragraph>
<Paragraph color="text-muted">• className?: string - Additional CSS classes</Paragraph>
<Paragraph color="text-muted">• style?: CSSProperties - Inline styles</Paragraph>
<Paragraph color="text-muted">• aria-label?: string - Accessibility label</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>TabBar.Item</strong> - Individual tab
</Paragraph>
<Paragraph color="text-muted">• id: string - Unique tab identifier (required)</Paragraph>
<Paragraph color="text-muted">• disabled?: boolean - Disable the tab</Paragraph>
<Paragraph color="text-muted">• color?: string - Custom tab color</Paragraph>
<Paragraph color="text-muted">• variant?: "destructive" - Danger styling</Paragraph>
<Paragraph color="text-muted">• disableItemStyles?: boolean - Remove default styles</Paragraph>
<Paragraph color="text-muted">• onClick?: MouseEventHandler - Custom click handler</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>TabBar.Header</strong> - Section header
</Paragraph>
<Paragraph color="text-muted">• children: ReactNode - Header text</Paragraph>
<Paragraph color="text-muted">• onClick?: MouseEventHandler - Click handler</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>TabBar.Separator</strong> - Visual divider
</Paragraph>
<Paragraph color="text-muted">• style?: CSSProperties - Custom styles</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,80 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaTextArea } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function TextAreaTab() {
const [basic, setBasic] = useState("Hello world\nThis is a textarea");
const [withPlaceholder, setWithPlaceholder] = useState("");
const [autosize, setAutosize] = useState("This textarea will grow as you type more content...");
const [fixedRows, setFixedRows] = useState("Fixed 5 rows");
const [charCount, setCharCount] = useState("Count characters");
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic">
<ManaTextArea value={basic} onChange={setBasic} />
</SectionWrapper>
<SectionWrapper title="With Placeholder">
<ManaTextArea value={withPlaceholder} onChange={setWithPlaceholder} placeholder="Enter your message..." />
</SectionWrapper>
<SectionWrapper title="Autosize">
<ManaTextArea value={autosize} onChange={setAutosize} autosize />
</SectionWrapper>
<SectionWrapper title="Fixed Rows (5)">
<ManaTextArea value={fixedRows} onChange={setFixedRows} rows={5} />
</SectionWrapper>
<SectionWrapper title="Character Count">
<ManaTextArea value={charCount} onChange={setCharCount} showCharacterCount maxLength={200} />
</SectionWrapper>
<SectionWrapper title="States">
<div className="vc-compfinder-grid-vertical">
<ManaTextArea value="Disabled textarea" disabled />
<ManaTextArea value="Read only textarea" readOnly />
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• value: string - Textarea value
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (value: string) =&gt; void - Called on input change
</Paragraph>
<Paragraph color="text-muted">
• placeholder?: string - Placeholder text
</Paragraph>
<Paragraph color="text-muted">
• rows?: number - Fixed number of rows
</Paragraph>
<Paragraph color="text-muted">
• autosize?: boolean - Auto-grow with content
</Paragraph>
<Paragraph color="text-muted">
• showCharacterCount?: boolean - Display character count
</Paragraph>
<Paragraph color="text-muted">
• maxLength?: number - Maximum character limit
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable textarea
</Paragraph>
<Paragraph color="text-muted">
• readOnly?: boolean - Make textarea read-only
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,76 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { ManaTextButton } from "..";
import { SectionWrapper } from "../SectionWrapper";
const VARIANTS = ["primary", "secondary", "always-white", "critical"] as const;
const TEXT_VARIANTS = [
"text-xs/normal",
"text-sm/normal",
"text-sm/medium",
"text-md/normal",
"text-md/medium",
"text-lg/medium",
] as const;
export default function TextButtonTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Variants">
<div className="vc-compfinder-grid">
{VARIANTS.map(variant => (
<ManaTextButton
key={variant}
text={variant}
variant={variant}
/>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Text Variants">
<div className="vc-compfinder-grid-vertical">
{TEXT_VARIANTS.map(textVariant => (
<ManaTextButton
key={textVariant}
text={textVariant}
textVariant={textVariant}
/>
))}
</div>
</SectionWrapper>
<SectionWrapper title="States">
<div className="vc-compfinder-grid">
<ManaTextButton text="Normal" />
<ManaTextButton text="Disabled" disabled />
</div>
</SectionWrapper>
<SectionWrapper title="Line Clamp">
<div className="vc-compfinder-grid" style={{ maxWidth: 200 }}>
<ManaTextButton
text="This is a very long text that should be clamped to one line"
lineClamp={1}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• text - Button label text</Paragraph>
<Paragraph color="text-muted">• variant - primary, secondary, always-white, critical</Paragraph>
<Paragraph color="text-muted">• textVariant - Typography variant (e.g., text-sm/medium)</Paragraph>
<Paragraph color="text-muted">• lineClamp - Max lines before truncation</Paragraph>
<Paragraph color="text-muted">• disabled - Disable button</Paragraph>
<Paragraph color="text-muted">• onClick - Click handler</Paragraph>
</SectionWrapper>
</div>
);
}

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 } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaTextInput } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function TextInputTab() {
const [basic, setBasic] = useState("Hello world");
const [sizeSm, setSizeSm] = useState("Small input");
const [sizeMd, setSizeMd] = useState("Medium input");
const [withPlaceholder, setWithPlaceholder] = useState("");
const [clearable, setClearable] = useState("Clear me");
const [charCount, setCharCount] = useState("Count chars");
const [error, setError] = useState("Invalid input");
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic">
<ManaTextInput value={basic} onChange={setBasic} />
</SectionWrapper>
<SectionWrapper title="Sizes">
<div className="vc-compfinder-grid-vertical">
<ManaTextInput value={sizeSm} onChange={setSizeSm} size="sm" placeholder="Size: sm" />
<ManaTextInput value={sizeMd} onChange={setSizeMd} size="md" placeholder="Size: md" />
</div>
</SectionWrapper>
<SectionWrapper title="With Placeholder">
<ManaTextInput value={withPlaceholder} onChange={setWithPlaceholder} placeholder="Enter text here..." />
</SectionWrapper>
<SectionWrapper title="Clearable">
<ManaTextInput value={clearable} onChange={setClearable} clearable />
</SectionWrapper>
<SectionWrapper title="Character Count">
<ManaTextInput value={charCount} onChange={setCharCount} showCharacterCount maxLength={50} />
</SectionWrapper>
<SectionWrapper title="Error State">
<ManaTextInput value={error} onChange={setError} error />
</SectionWrapper>
<SectionWrapper title="States">
<div className="vc-compfinder-grid-vertical">
<ManaTextInput value="Disabled" disabled />
<ManaTextInput value="Read only" readOnly />
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• value: string - Input value
</Paragraph>
<Paragraph color="text-muted">
• onChange?: (value: string) =&gt; void - Called on input change
</Paragraph>
<Paragraph color="text-muted">
• placeholder?: string - Placeholder text
</Paragraph>
<Paragraph color="text-muted">
• size?: "sm" | "md" - Input size (default: "md")
</Paragraph>
<Paragraph color="text-muted">
• clearable?: boolean - Show clear button
</Paragraph>
<Paragraph color="text-muted">
• showCharacterCount?: boolean - Display character count
</Paragraph>
<Paragraph color="text-muted">
• maxLength?: number - Maximum character limit
</Paragraph>
<Paragraph color="text-muted">
• error?: boolean - Show error state styling
</Paragraph>
<Paragraph color="text-muted">
• disabled?: boolean - Disable input
</Paragraph>
<Paragraph color="text-muted">
• readOnly?: boolean - Make input read-only
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,99 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { ManaButton, ManaTooltip } from "..";
import { SectionWrapper } from "../SectionWrapper";
const POSITIONS = ["top", "bottom", "left", "right"] as const;
const COLORS = ["primary", "black", "grey", "brand", "green", "yellow", "red"] as const;
export default function TooltipTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Positions">
<div className="vc-compfinder-grid">
{POSITIONS.map(position => (
<ManaTooltip key={position} text={`Tooltip on ${position}`} position={position}>
{props => (
<ManaButton {...props} variant="secondary" text={position} />
)}
</ManaTooltip>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Colors">
<div className="vc-compfinder-grid">
{COLORS.map(color => (
<ManaTooltip key={color} text={`${color} tooltip`} color={color}>
{props => (
<ManaButton {...props} variant="secondary" text={color} />
)}
</ManaTooltip>
))}
</div>
</SectionWrapper>
<SectionWrapper title="With Delay">
<div className="vc-compfinder-grid">
<ManaTooltip text="No delay" delay={0}>
{props => (
<ManaButton {...props} variant="secondary" text="No delay" />
)}
</ManaTooltip>
<ManaTooltip text="500ms delay" delay={500}>
{props => (
<ManaButton {...props} variant="secondary" text="500ms delay" />
)}
</ManaTooltip>
<ManaTooltip text="1000ms delay" delay={1000}>
{props => (
<ManaButton {...props} variant="secondary" text="1s delay" />
)}
</ManaTooltip>
</div>
</SectionWrapper>
<SectionWrapper title="Hide on Click">
<div className="vc-compfinder-grid">
<ManaTooltip text="Hides when clicked" hideOnClick>
{props => (
<ManaButton {...props} variant="secondary" text="Click me" />
)}
</ManaTooltip>
<ManaTooltip text="Stays visible when clicked" hideOnClick={false}>
{props => (
<ManaButton {...props} variant="secondary" text="Click me" />
)}
</ManaTooltip>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• text: string - Tooltip content
</Paragraph>
<Paragraph color="text-muted">
• children: (props) =&gt; ReactNode - Render function for trigger element
</Paragraph>
<Paragraph color="text-muted">
• position?: "top" | "bottom" | "left" | "right" - Tooltip position
</Paragraph>
<Paragraph color="text-muted">
• color?: "primary" | "black" | "grey" | "brand" | "green" | "yellow" | "red"
</Paragraph>
<Paragraph color="text-muted">
• delay?: number - Show delay in milliseconds
</Paragraph>
<Paragraph color="text-muted">
• hideOnClick?: boolean - Hide tooltip when trigger is clicked
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,188 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { SectionWrapper } from "../SectionWrapper";
const TEXT_COLORS = [
"text-default",
"text-muted",
"text-subtle",
"text-strong",
"text-normal",
"text-link",
"text-brand",
"text-disabled",
"text-invert",
"text-feedback-critical",
"text-feedback-warning",
"text-feedback-info",
"text-feedback-positive",
"text-status-online",
"text-status-idle",
"text-status-dnd",
"text-status-offline",
"text-overlay-dark",
"text-overlay-light",
] as const;
const TEXT_SIZES = [
{ name: "xxs", value: "0.625rem (10px)" },
{ name: "xs", value: "0.75rem (12px)" },
{ name: "sm", value: "0.875rem (14px)" },
{ name: "md", value: "1rem (16px)" },
{ name: "lg", value: "1.25rem (20px)" },
{ name: "xl", value: "1.5rem (24px)" },
{ name: "xxl", value: "2rem (32px)" },
] as const;
const TEXT_WEIGHTS = [
{ name: "thin", value: "100" },
{ name: "extralight", value: "200" },
{ name: "light", value: "300" },
{ name: "normal", value: "400" },
{ name: "medium", value: "500" },
{ name: "semibold", value: "600" },
{ name: "bold", value: "700" },
{ name: "extrabold", value: "800" },
] as const;
const FONT_SIZE_MAP: Record<string, string> = {
xxs: "0.625rem",
xs: "0.75rem",
sm: "0.875rem",
md: "1rem",
lg: "1.25rem",
xl: "1.5rem",
xxl: "2rem",
};
export default function TypographyTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Text Colors (CSS Variables)">
<div className="vc-compfinder-grid-vertical">
{TEXT_COLORS.map(color => (
<div key={color} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<span style={{ color: `var(--${color})`, minWidth: 200 }}>
{color}
</span>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
var(--{color})
</code>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Text Sizes">
<div className="vc-compfinder-grid-vertical">
{TEXT_SIZES.map(({ name, value }) => (
<div key={name} style={{ display: "flex", alignItems: "baseline", gap: 16 }}>
<span style={{ fontSize: FONT_SIZE_MAP[name], minWidth: 200 }}>
{name}
</span>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
{value}
</code>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Text Weights">
<div className="vc-compfinder-grid-vertical">
{TEXT_WEIGHTS.map(({ name, value }) => (
<div key={name} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<span style={{ fontWeight: value, minWidth: 200 }}>
{name}
</span>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
font-weight: {value}
</code>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Usage with BaseText">
<div className="vc-compfinder-grid-vertical" style={{ gap: 4 }}>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
{"import { BaseText } from \"@components/BaseText\";"}
</code>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
{"<BaseText size=\"md\" weight=\"semibold\" color=\"text-default\">Text</BaseText>"}
</code>
</div>
</SectionWrapper>
<SectionWrapper title="Code Colors">
<div className="vc-compfinder-grid-vertical">
{[
"text-code",
"text-code-keyword",
"text-code-string",
"text-code-comment",
"text-code-builtin",
"text-code-variable",
"text-code-tag",
"text-code-title",
"text-code-section",
"text-code-bullet",
"text-code-addition",
"text-code-deletion",
].map(color => (
<div key={color} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<span style={{ color: `var(--${color})`, minWidth: 200, fontFamily: "monospace" }}>
{color}
</span>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
var(--{color})
</code>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>BaseText</strong> - from @components/BaseText
</Paragraph>
<Paragraph color="text-muted">
• size?: "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl"
</Paragraph>
<Paragraph color="text-muted">
• weight?: "thin" | "extralight" | "light" | "normal" | "medium" | "semibold" | "bold" | "extrabold"
</Paragraph>
<Paragraph color="text-muted">
• color?: string - CSS variable name (e.g., "text-muted")
</Paragraph>
<Paragraph color="text-muted">
• tag?: "span" | "p" | "div" | "h1" - etc.
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>Heading</strong> - from @components/Heading
</Paragraph>
<Paragraph color="text-muted">
• tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" (default: h5)
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>Paragraph</strong> - from @components/Paragraph
</Paragraph>
<Paragraph color="text-muted">
• color?: string - CSS variable name
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>Span</strong> - from @components/Span
</Paragraph>
<Paragraph color="text-muted">
• Inline text with default styling
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,74 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import SettingsPlugin from "@plugins/_core/settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
import { openUserSettingsPanel } from "@webpack/common";
import ComponentsTab from "./components/ComponentsTab";
function ComponentsIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M3 15.5V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v.5a.5.5 0 0 1-.5.5H17a4 4 0 0 0-4 4v4.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5ZM12.5 18H2a1 1 0 1 0 0 2h10.48c.33 0 .57-.3.54-.63A4.08 4.08 0 0 1 13 19v-.5a.5.5 0 0 0-.5-.5Z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M15 11c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-8Zm2 1a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z"
clipRule="evenodd"
/>
</svg>
);
}
export default definePlugin({
name: "Components",
description: "Adds a new tab to settings to browse Discord components.",
authors: [EquicordDevs.Prism],
dependencies: ["Settings"],
startAt: StartAt.WebpackReady,
toolboxActions: {
"Open Components Tab"() {
openUserSettingsPanel("equicord_components");
},
},
start() {
const { customEntries, customSections } = SettingsPlugin;
customEntries.push({
key: "equicord_components",
title: "Components",
Component: ComponentsTab,
Icon: ComponentsIcon
});
customSections.push(() => ({
section: "EquicordDiscordComponents",
label: "Components",
element: ComponentsTab,
className: "vc-discord-components",
id: "Components"
}));
},
stop() {
const { customEntries, customSections } = SettingsPlugin;
const entryIdx = customEntries.findIndex(e => e.key === "equicord_components");
const sectionIdx = customSections.findIndex(s => s({} as any).id === "Components");
if (entryIdx !== -1) customEntries.splice(entryIdx, 1);
if (sectionIdx !== -1) customSections.splice(sectionIdx, 1);
},
});

View file

@ -0,0 +1,89 @@
.vc-compfinder-tabbar {
margin-bottom: 16px;
flex-wrap: wrap !important;
gap: 8px 16px;
}
.vc-compfinder-tabbar > div {
flex-wrap: wrap !important;
gap: 8px 16px;
}
.vc-compfinder-tab {
padding: 8px 12px;
margin: 0 !important;
}
.vc-compfinder-content {
padding-top: 16px;
}
.vc-compfinder-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.vc-compfinder-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.vc-compfinder-grid-vertical {
display: flex;
flex-direction: column;
gap: 8px;
}
.vc-compfinder-switch-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--background-secondary);
border-radius: 4px;
}
.vc-compfinder-demo-button {
padding: 8px 16px;
background: var(--background-secondary);
border: 1px solid var(--border-subtle);
border-radius: 4px;
color: var(--text-normal);
cursor: pointer;
}
.vc-compfinder-demo-button:hover {
background: var(--background-secondary-alt);
}
.vc-compfinder-modal-header {
display: flex;
align-items: flex-start;
position: relative;
}
.vc-compfinder-modal-header-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.vc-compfinder-modal-close {
position: absolute;
top: 0;
right: 0;
}
.vc-compfinder-modal-content {
padding-bottom: var(--space-16);
}
.vc-compfinder-modal-footer {
display: flex;
flex-direction: row-reverse;
gap: 8px;
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface AnchorProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href?: string;
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
className?: string;
children?: React.ReactNode;
rel?: string;
target?: string;
useDefaultUnderlineStyles?: boolean;
title?: string;
style?: React.CSSProperties;
focusProps?: Record<string, unknown>;
ref?: React.Ref<HTMLAnchorElement>;
}

View file

@ -0,0 +1,25 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type AvatarSize = "SIZE_16" | "SIZE_20" | "SIZE_24" | "SIZE_32" | "SIZE_40" | "SIZE_48" | "SIZE_56" | "SIZE_80" | "SIZE_120";
export type AvatarStatus = "online" | "idle" | "dnd" | "offline" | "streaming";
export interface AvatarProps {
src?: string;
size?: AvatarSize;
status?: AvatarStatus;
statusColor?: string;
isMobile?: boolean;
isTyping?: boolean;
isSpeaking?: boolean;
statusTooltip?: boolean;
statusTooltipDelay?: number;
"aria-hidden"?: boolean;
"aria-label"?: string;
imageClassName?: string;
className?: string;
}

View file

@ -0,0 +1,48 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface BadgeShapesType {
ROUND: string;
ROUND_LEFT: string;
ROUND_RIGHT: string;
SQUARE: string;
}
export interface NumberBadgeProps {
count: number;
maxDigits?: number;
color?: string;
disableColor?: boolean;
shape?: string;
className?: string;
style?: React.CSSProperties;
}
export interface TextBadgeProps {
text: string;
color?: string;
disableColor?: boolean;
shape?: string;
className?: string;
style?: React.CSSProperties;
}
export interface IconBadgeProps {
icon: React.ComponentType<{ className?: string; color?: string; }>;
color?: string;
disableColor?: boolean;
shape?: string;
className?: string;
style?: React.CSSProperties;
}
export interface CircleBadgeProps {
color?: string;
disableColor?: boolean;
shape?: string;
className?: string;
style?: React.CSSProperties;
}

View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ManaButtonProps {
text?: string;
variant?: "primary" | "secondary" | "critical-primary" | "critical-secondary" | "overlay-primary" | "overlay-secondary" | "expressive";
size?: "xs" | "sm" | "md";
disabled?: boolean;
loading?: boolean;
fullWidth?: boolean;
rounded?: boolean;
icon?: React.ComponentType<any>;
iconPosition?: "start" | "end";
onClick?: () => void;
className?: string;
style?: React.CSSProperties;
}
export interface ManaTextButtonProps {
text: string;
variant?: "primary" | "secondary" | "always-white" | "critical";
textVariant?: string;
lineClamp?: number;
onClick?: () => void;
disabled?: boolean;
}

View file

@ -0,0 +1,32 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ManaCalendarProps {
value?: any;
onChange?: (value: any) => void;
minValue?: any;
maxValue?: any;
disabled?: boolean;
readOnly?: boolean;
className?: string;
"aria-label"?: string;
}
export interface ManaDatePickerProps {
value?: any;
onChange?: (value: any) => void;
minValue?: any;
maxValue?: any;
placeholderValue?: any;
granularity?: "day" | "hour" | "minute" | "second";
hourCycle?: 12 | 24;
hideTimeZone?: boolean;
disabled?: boolean;
required?: boolean;
label?: string;
description?: string;
errorMessage?: string;
}

View file

@ -0,0 +1,32 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ManaCheckboxProps {
checked: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
displayOnly?: boolean;
label?: string;
description?: string;
leadingIcon?: React.ComponentType<any>;
labelType?: "primary" | "secondary";
usageVariant?: "single" | "indicator";
}
export interface CheckboxGroupOption {
value: string | number;
label: string;
description?: string;
disabled?: boolean;
leadingIcon?: React.ComponentType<any>;
}
export interface ManaCheckboxGroupProps {
options: CheckboxGroupOption[];
selectedValues: (string | number)[];
onChange?: (values: (string | number)[]) => void;
disabled?: boolean;
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ClickableProps extends React.HTMLAttributes<HTMLElement> {
tag?: keyof React.JSX.IntrinsicElements;
focusProps?: Record<string, unknown>;
innerRef?: React.Ref<HTMLElement>;
onClick?: React.MouseEventHandler<HTMLElement>;
role?: string;
tabIndex?: number;
ignoreKeyPress?: boolean;
className?: string;
children?: React.ReactNode;
href?: string;
onKeyPress?: React.KeyboardEventHandler<HTMLElement>;
}

View file

@ -0,0 +1,77 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ColorPickerProps {
color: number;
onChange: (color: number) => void;
onClose?: () => void;
suggestedColors?: string[];
disabled?: boolean;
label?: React.ReactNode;
colorPickerMiddle?: React.ReactNode;
colorPickerFooter?: React.ReactNode;
showEyeDropper?: boolean;
}
export interface ColorSwatchProps {
color?: number;
isDefault?: boolean;
isCustom?: boolean;
isSelected?: boolean;
disabled?: boolean;
style?: React.CSSProperties;
onClick?: (colorOrGradient: number | { start: number; end: number; }) => void;
isGradient?: boolean;
"aria-label"?: string;
gradientStart?: number;
gradientEnd?: number;
gradientDegrees?: number;
}
export interface DefaultColorButtonProps {
color: number;
onChange: (color: number) => void;
value: number;
disabled?: boolean;
allowBlackCustomColor?: boolean;
}
export interface CustomColorButtonProps {
customColor?: number;
value: number;
disabled?: boolean;
"aria-label"?: string;
presets: number[];
}
export interface GradientColor {
start: number;
end: number;
name?: string;
}
export interface ColorPickerWithSwatchesProps {
className?: string;
defaultColor: number;
customColor?: number;
colors: number[] | GradientColor[];
value: number;
secondaryValue?: number;
disabled?: boolean;
onChange: (color: number, secondaryColor?: number) => void;
renderDefaultButton?: (props: DefaultColorButtonProps) => React.ReactNode;
renderCustomButton?: (props: CustomColorButtonProps) => React.ReactNode;
colorContainerClassName?: string;
isGradient?: boolean;
renderGradientCustomButton?: (props: {
value: number;
startColor: number;
endColor: number;
disabled?: boolean;
}) => React.ReactNode;
gradientDegrees?: number;
allowBlackCustomColor?: boolean;
}

View file

@ -0,0 +1,25 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "./anchor";
export * from "./avatar";
export * from "./badge";
export * from "./button";
export * from "./calendar";
export * from "./checkbox";
export * from "./clickable";
export * from "./colorpicker";
export * from "./input";
export * from "./notice";
export * from "./paginator";
export * from "./progress";
export * from "./radio";
export * from "./select";
export * from "./slider";
export * from "./spinner";
export * from "./switch";
export * from "./tabbar";
export * from "./tooltip";

View file

@ -0,0 +1,46 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ManaTextInputProps {
value: string;
onChange?: (value: string) => void;
placeholder?: string;
disabled?: boolean;
readOnly?: boolean;
size?: "sm" | "md";
error?: boolean;
clearable?: boolean;
showCharacterCount?: boolean;
maxLength?: number;
}
export interface ManaTextAreaProps {
value: string;
onChange?: (value: string) => void;
placeholder?: string;
disabled?: boolean;
readOnly?: boolean;
rows?: number;
autosize?: boolean;
maxLength?: number;
showCharacterCount?: boolean;
}
export interface SearchBarProps {
query: string;
onChange: (value: string) => void;
onClear?: () => void;
placeholder?: string;
size?: "sm" | "md";
autoFocus?: boolean;
disabled?: boolean;
onKeyDown?: (e: React.KeyboardEvent) => void;
onBlur?: () => void;
onFocus?: () => void;
autoComplete?: string;
inputProps?: Record<string, any>;
"aria-label"?: string;
}

View file

@ -0,0 +1,18 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type NoticeType = "warn" | "info" | "danger" | "positive" | "preview";
export interface NoticeProps {
children: React.ReactNode;
messageType: NoticeType;
action?: React.ReactNode;
className?: string;
textColor?: string;
textVariant?: string;
icon?: React.ComponentType<{ className?: string; color?: string; }>;
hidden?: boolean;
}

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface PaginatorPageWrapperProps {
key: string;
targetPage: number;
selected: boolean;
navigateToPage: () => void;
}
export interface PaginatorProps {
currentPage: number;
totalCount: number;
pageSize: number;
maxVisiblePages?: number;
disablePaginationGap?: boolean;
onPageChange?: (page: number) => void;
hideMaxPage?: boolean;
className?: string;
renderPageWrapper?: (props: PaginatorPageWrapperProps, defaultRender: React.ReactNode) => React.ReactNode;
}

View file

@ -0,0 +1,25 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type ProgressBarVariant = "blue" | "orange" | "unset";
export interface ProgressBarOverride {
background?: string;
gradientStart?: string;
gradientEnd?: string;
}
export interface ProgressBarProps {
progress: number;
minimum?: number;
maximum?: number;
variant?: ProgressBarVariant;
override?: {
default?: ProgressBarOverride;
[key: string]: ProgressBarOverride | undefined;
};
labelledBy?: string;
}

View file

@ -0,0 +1,26 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface RadioOption {
value: string | number;
name: string;
desc?: string;
disabled?: boolean;
leadingIcon?: React.ComponentType<{ className?: string; size?: string; color?: string; }>;
}
export interface ManaBaseRadioGroupProps {
options: RadioOption[];
value?: string | number;
onChange?: (value: string | number) => void;
disabled?: boolean;
"aria-labelledby"?: string;
}
export interface StandaloneRadioIndicatorProps {
isSelected: boolean;
disabled?: boolean;
}

View file

@ -0,0 +1,66 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ManaSelectOption {
id: string;
value: string;
label: string;
}
export interface ManaSelectProps {
options: ManaSelectOption[];
value?: string | string[] | null;
onSelectionChange?: (value: string | string[] | null) => void;
selectionMode?: "single" | "multiple";
placeholder?: string;
disabled?: boolean;
readOnly?: boolean;
clearable?: boolean;
fullWidth?: boolean;
autoFocus?: boolean;
closeOnSelect?: boolean;
shouldFocusWrap?: boolean;
maxOptionsVisible?: number;
wrapTags?: boolean;
}
export interface ManaComboboxProps {
placeholder?: string;
value?: string | string[];
onChange?: (value: string | string[]) => void;
multiSelect?: boolean;
autoFocus?: boolean;
maxVisibleItems?: number;
emptyStateText?: string;
emptyStateHeader?: string;
onQueryChange?: (query: string) => void;
"aria-label"?: string;
className?: string;
listClassName?: string;
children: (query: string) => React.ReactNode[];
}
export interface ListboxItem {
id: string;
label: string;
disabled?: boolean;
}
export interface ManaListboxProps {
items: ListboxItem[];
selectedItems?: ListboxItem[];
defaultSelectedItems?: ListboxItem[];
onSelectionChange?: (items: ListboxItem[]) => void;
selectionMode?: "single" | "multiple";
disabled?: boolean;
required?: boolean;
loading?: boolean;
maxVisibleItems?: number;
shouldFocusWrap?: boolean;
typeahead?: boolean;
renderListItem?: (item: ListboxItem) => React.ReactNode;
renderEmptyState?: () => React.ReactNode;
}

View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface SliderProps {
initialValue?: number;
value?: number;
minValue?: number;
maxValue?: number;
onValueChange?: (value: number) => void;
onValueRender?: (value: number) => string;
markers?: number[];
stickToMarkers?: boolean;
onMarkerRender?: (value: number) => string;
renderMarker?: (value: number) => React.ReactNode;
disabled?: boolean;
mini?: boolean;
hideBubble?: boolean;
defaultValue?: number;
orientation?: "horizontal" | "vertical";
barClassName?: string;
grabberClassName?: string;
className?: string;
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
}

View file

@ -0,0 +1,15 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type SpinnerType = "wanderingCubes" | "chasingDots" | "pulsingEllipsis" | "spinningCircle" | "spinningCircleSimple" | "lowMotion";
export interface SpinnerProps {
type?: SpinnerType;
animated?: boolean;
className?: string;
itemClassName?: string;
"aria-label"?: string;
}

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 ManaSwitchProps {
checked: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
}

View file

@ -0,0 +1,55 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface TabBarProps {
className?: string;
children?: React.ReactNode;
type?: "side" | "top" | "top-pill";
look?: "brand" | "grey";
style?: React.CSSProperties;
selectedItem?: string;
onItemSelect?: (id: string) => void;
orientation?: "horizontal" | "vertical";
"aria-label"?: string;
}
export interface TabBarItemProps {
id: string;
className?: string;
children?: React.ReactNode;
selectedItem?: string;
onItemSelect?: (id: string) => void;
itemType?: "side" | "top" | "top-pill";
look?: "brand" | "grey";
color?: string;
variant?: "destructive";
disabled?: boolean;
disableItemStyles?: boolean;
onClick?: React.MouseEventHandler;
onContextMenu?: React.MouseEventHandler;
clickableRef?: React.Ref<HTMLElement>;
clickableInnerRef?: React.Ref<HTMLElement>;
"aria-label"?: string;
}
export interface TabBarHeaderProps {
className?: string;
children?: React.ReactNode;
onClick?: React.MouseEventHandler;
"aria-expanded"?: boolean;
"aria-controls"?: string;
}
export interface TabBarSeparatorProps {
style?: React.CSSProperties;
}
export type TabBarComponent = React.ComponentType<TabBarProps> & {
Item: React.ComponentType<TabBarItemProps>;
Header: React.ComponentType<TabBarHeaderProps>;
Separator: React.ComponentType<TabBarSeparatorProps>;
Panel: React.ComponentType<{ children?: React.ReactNode; }>;
};

View file

@ -0,0 +1,53 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ManaTooltipProps {
text: string;
position?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
color?: "primary" | "black" | "grey" | "brand" | "green" | "yellow" | "red";
spacing?: number;
hideOnClick?: boolean;
delay?: number;
children: (props: any) => React.ReactNode;
}
export interface ManaRichTooltipProps {
title?: string;
body: string;
asset?: React.ReactNode | { src: string; alt?: string; };
assetSize?: number;
asContainer?: boolean;
element?: "span" | "div";
position?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
spacing?: number;
ariaHidden?: boolean;
children: React.ReactNode | ((props: any) => React.ReactNode);
}
export interface PopoverAction {
text: string;
onClick?: () => void;
variant?: "primary" | "secondary";
}
export interface ManaPopoverProps {
title?: string;
body?: string;
badge?: React.ReactNode;
graphic?: { src: string; aspectRatio?: string; };
size?: "sm" | "md" | "lg";
actions?: PopoverAction[];
textLink?: { text: string; onClick?: () => void; };
gradientColor?: string;
onRequestClose?: (reason: string) => void;
targetElementRef: React.RefObject<HTMLElement | null>;
shouldShow: boolean;
position?: "top" | "bottom" | "left" | "right";
alignmentStrategy?: "edge" | "trigger-center";
align?: "left" | "center" | "right";
}

View file

@ -6,7 +6,7 @@
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Alert } from "@components/Alert";
import { Notice } from "@components/Notice";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Channel, User, VoiceState } from "@vencord/discord-types";
@ -78,9 +78,9 @@ export default definePlugin({
authors: [EquicordDevs.TheArmagan],
settings,
settingsAboutComponent: () => (
<Alert.Info>
<Notice.Info>
This Plugin is used to follow a Friend/Friends into voice chat(s).
</Alert.Info>
</Notice.Info>
),
flux: {
async VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Alert } from "@components/Alert";
import { Notice } from "@components/Notice";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -25,9 +25,9 @@ export default definePlugin({
description: "Prevents in-call/PiP previews (screenshare, streams, etc) from pausing even if the client loses focus",
authors: [EquicordDevs.vappstar],
settingsAboutComponent: () => (
<Alert.Warning>
<Notice.Warning>
This plugin will cause discord to use more resources than normal
</Alert.Warning>
</Notice.Warning>
),
patches: [
{

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { Alert } from "@components/Alert";
import { Notice } from "@components/Notice";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { UserStore } from "@webpack/common";
@ -50,9 +50,9 @@ export default definePlugin({
description: "Spoof what platform or device you're on",
authors: [EquicordDevs.Drag],
settingsAboutComponent: () => (
<Alert.Warning>
<Notice.Warning>
We can't guarantee this plugin won't get you warned or banned.
</Alert.Warning>
</Notice.Warning>
),
settings: settings,
patches: [

View file

@ -21,7 +21,7 @@ import "./style.css";
import { HeaderBarButton } from "@api/HeaderBar";
import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings";
import { Alert } from "@components/Alert";
import { Notice } from "@components/Notice";
import { Devs } from "@utils/constants";
import { getTheme, Theme } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
@ -220,9 +220,9 @@ export default definePlugin({
authors: [Devs.amia],
settings,
settingsAboutComponent: () => (
<Alert.Info>
<Notice.Info>
You must manually accept the quest first before clicking the button.
</Alert.Info>
</Notice.Info>
),
headerBarButton: {
icon: QuestIcon,

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Alert } from "@components/Alert";
import { Notice } from "@components/Notice";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -12,9 +12,9 @@ export default definePlugin({
name: "QuestFocused",
description: "Prevent the quests player from pausing and possibly skip it all together.",
settingsAboutComponent: () => (
<Alert.Warning>
<Notice.Warning>
You might need to spam left mouse button on the video to skip it.
</Alert.Warning>
</Notice.Warning>
),
authors: [EquicordDevs.secp192k1],
patches: [

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { Alert } from "@components/Alert";
import { Notice } from "@components/Notice";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
@ -41,9 +41,9 @@ export default definePlugin({
description: "Mention any user, regardless of channel access.",
settings,
settingsAboutComponent: () => (
<Alert.Warning>
<Notice.Warning>
Using Global Mention can cause performance issues and show an absurd amount of users in the autocomplete.
</Alert.Warning>
</Notice.Warning>
),
patches: [