Drop Components

This commit is contained in:
thororen1234 2026-04-24 12:50:30 -04:00
parent b4ec1711c7
commit b6a442ce73
No known key found for this signature in database
89 changed files with 0 additions and 9729 deletions

View file

@ -1,209 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Heading } from "@components/Heading";
import { SettingsTab, wrapTab } from "@components/settings";
import { Margins } from "@utils/margins";
import { TabBar, useMemo, useState } from "@webpack/common";
import { SearchBar } from ".";
import AccordionTab from "./tabs/AccordionTab";
import AlertTab from "./tabs/AlertTab";
import AnchorTab from "./tabs/AnchorTab";
import AnimationsTab from "./tabs/AnimationsTab";
import AvatarTab from "./tabs/AvatarTab";
import BadgeTab from "./tabs/BadgeTab";
import ButtonsTab from "./tabs/ButtonsTab";
import CalendarTab from "./tabs/CalendarTab";
import CardTab from "./tabs/CardTab";
import CheckboxGroupTab from "./tabs/CheckboxGroupTab";
import CheckboxTab from "./tabs/CheckboxTab";
import ChipTab from "./tabs/ChipTab";
import ClickableTab from "./tabs/ClickableTab";
import CodeBlockTab from "./tabs/CodeBlockTab";
import ColorPickerTab from "./tabs/ColorPickerTab";
import ComboboxTab from "./tabs/ComboboxTab";
import DividerTab from "./tabs/DividerTab";
import ErrorBoundaryTab from "./tabs/ErrorBoundaryTab";
import FocusLockTab from "./tabs/FocusLockTab";
import GuildIconTab from "./tabs/GuildIconTab";
import HeadingTab from "./tabs/HeadingTab";
import LayerModalTab from "./tabs/LayerModalTab";
import ListboxTab from "./tabs/ListboxTab";
import MenuTab from "./tabs/MenuTab";
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 ScrollerTab from "./tabs/ScrollerTab";
import SearchBarTab from "./tabs/SearchBarTab";
import SelectTab from "./tabs/SelectTab";
import SkeletonTab from "./tabs/SkeletonTab";
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 TimestampTab from "./tabs/TimestampTab";
import ToastTab from "./tabs/ToastTab";
import TooltipTab from "./tabs/TooltipTab";
import TypographyTab from "./tabs/TypographyTab";
import UserSummaryItemTab from "./tabs/UserSummaryItemTab";
const TABS = [
{ id: "accordion", label: "Accordion" },
{ id: "alert", label: "Alert" },
{ id: "animations", label: "Animations" },
{ id: "anchor", label: "Anchor" },
{ id: "avatar", label: "Avatar" },
{ id: "badge", label: "Badge" },
{ id: "buttons", label: "Buttons" },
{ id: "calendar", label: "Calendar" },
{ id: "card", label: "Card" },
{ id: "chip", label: "Chip" },
{ id: "checkbox", label: "Checkbox" },
{ id: "checkboxgroup", label: "CheckboxGroup" },
{ id: "clickable", label: "Clickable" },
{ id: "codeblock", label: "CodeBlock" },
{ id: "colorpicker", label: "ColorPicker" },
{ id: "combobox", label: "Combobox" },
{ id: "divider", label: "Divider" },
{ id: "errorboundary", label: "ErrorBoundary" },
{ id: "focuslock", label: "FocusLock" },
{ id: "guildicon", label: "GuildIcon" },
{ id: "heading", label: "Heading" },
{ id: "layermodal", label: "LayerModal" },
{ id: "listbox", label: "Listbox" },
{ id: "menu", label: "Menu" },
{ 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: "scroller", label: "Scroller" },
{ id: "searchbar", label: "SearchBar" },
{ id: "select", label: "Select" },
{ id: "skeleton", label: "Skeleton" },
{ 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: "timestamp", label: "Timestamp" },
{ id: "toast", label: "Toast" },
{ id: "tooltip", label: "Tooltip" },
{ id: "typography", label: "Typography" },
{ id: "usersummaryitem", label: "UserSummaryItem" },
].sort((a, b) => a.label.localeCompare(b.label));
type TabId = typeof TABS[number]["id"];
const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
accordion: AccordionTab,
alert: AlertTab,
animations: AnimationsTab,
anchor: AnchorTab,
avatar: AvatarTab,
badge: BadgeTab,
buttons: ButtonsTab,
calendar: CalendarTab,
card: CardTab,
chip: ChipTab,
checkbox: CheckboxTab,
checkboxgroup: CheckboxGroupTab,
clickable: ClickableTab,
codeblock: CodeBlockTab,
colorpicker: ColorPickerTab,
combobox: ComboboxTab,
divider: DividerTab,
errorboundary: ErrorBoundaryTab,
focuslock: FocusLockTab,
guildicon: GuildIconTab,
heading: HeadingTab,
layermodal: LayerModalTab,
listbox: ListboxTab,
menu: MenuTab,
modal: ModalTab,
notice: NoticeTab,
paginator: PaginatorTab,
popover: PopoverTab,
progressbar: ProgressBarTab,
radiogroup: RadioGroupTab,
richtooltip: RichTooltipTab,
scroller: ScrollerTab,
searchbar: SearchBarTab,
select: SelectTab,
skeleton: SkeletonTab,
slider: SliderTab,
spinner: SpinnerTab,
switch: SwitchTab,
tabbar: TabBarTab,
textarea: TextAreaTab,
textbutton: TextButtonTab,
textinput: TextInputTab,
timestamp: TimestampTab,
toast: ToastTab,
tooltip: TooltipTab,
typography: TypographyTab,
usersummaryitem: UserSummaryItemTab,
};
function ComponentsTab() {
const [currentTab, setCurrentTab] = useState<TabId>("avatar");
const [search, setSearch] = useState("");
const TabComponent = TAB_COMPONENTS[currentTab];
const filteredTabs = useMemo(() => {
if (!search.trim()) return TABS;
const query = search.toLowerCase();
return TABS.filter(tab => tab.label.toLowerCase().includes(query));
}, [search]);
return (
<SettingsTab>
<Heading className={Margins.bottom16}>Components</Heading>
<div className="vc-compfinder-search">
<SearchBar
query={search}
onChange={setSearch}
onClear={() => setSearch("")}
placeholder="Search components..."
/>
</div>
<TabBar
type="top"
look="brand"
selectedItem={currentTab}
onItemSelect={setCurrentTab}
className="vc-compfinder-tabbar"
>
{filteredTabs.map(tab => (
<TabBar.Item key={tab.id} id={tab.id} className="vc-compfinder-tab">
{tab.label}
</TabBar.Item>
))}
</TabBar>
<div className="vc-compfinder-content">
<ErrorBoundary message={`Failed to render ${currentTab} tab`}>
<TabComponent />
</ErrorBoundary>
</div>
</SettingsTab>
);
}
export default wrapTab(ComponentsTab, "ComponentsTab");

View file

@ -1,258 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CodeBlock } from "@components/CodeBlock";
import { classNameFactory } from "@utils/css";
import { DiscordHeading, DiscordText, Divider, InlineCode, Notice, Paragraph } from ".";
const cl = classNameFactory("vc-compfinder-");
export interface PropDef {
name: string;
type: string;
default?: string;
required?: boolean;
internal?: boolean;
description: string;
}
export interface DocNotice {
type: "info" | "warn" | "danger" | "positive";
children: React.ReactNode;
}
export interface DemoSection {
title: string;
description?: string;
children: React.ReactNode;
code?: string;
relevantProps?: string[];
notice?: DocNotice;
id?: string;
}
export interface PropGroup {
title: string;
props: PropDef[];
}
export interface CodeExample {
label: string;
code: string;
}
export interface DocPageConfig {
overview: string;
importPath: string;
sections?: DemoSection[];
demos?: DemoSection[];
props: PropDef[] | PropGroup[];
examples?: CodeExample[];
componentName?: string;
notices?: DocNotice[];
showToc?: boolean;
}
function isGrouped(props: PropDef[] | PropGroup[]): props is PropGroup[] {
return props.length > 0 && "props" in props[0];
}
function allProps(props: PropDef[] | PropGroup[]): PropDef[] {
return isGrouped(props) ? props.flatMap(g => g.props) : props;
}
function generateId(title: string): string {
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
function PropsTable({ props, showLegend }: { props: PropDef[]; showLegend?: boolean; }) {
const hasRequired = props.some(p => p.required);
const hasInternal = props.some(p => p.internal);
return (
<>
<div className={cl("table-wrapper")}>
<table className={cl("table")}>
<thead>
<tr>
<th>FIELD</th>
<th>TYPE</th>
<th>DEFAULT</th>
<th>DESCRIPTION</th>
</tr>
</thead>
<tbody>
{props.map(p => (
<tr key={p.name} className={p.required ? cl("table-row-required") : p.internal ? cl("table-row-internal") : undefined}>
<td>
<span className={cl("table-field")}>{p.name}</span>
</td>
<td><InlineCode>{p.type}</InlineCode></td>
<td>{p.default ?? <span className={cl("table-dash")}>-</span>}</td>
<td className={cl("table-desc")}>{p.description}</td>
</tr>
))}
</tbody>
</table>
</div>
{showLegend && (hasRequired || hasInternal) && (
<div className={cl("table-legend")}>
{hasRequired && <span className={cl("table-legend-item")}><span className={cl("table-legend-bar", "table-legend-required")} /> Required</span>}
{hasInternal && <span className={cl("table-legend-item")}><span className={cl("table-legend-bar", "table-legend-internal")} /> Internal</span>}
</div>
)}
</>
);
}
function TableOfContents({ entries }: { entries: { id: string; label: string; }[]; }) {
return (
<div className={cl("toc")}>
<DiscordText variant="text-xs/semibold" color="text-muted" className={cl("toc-label")}>
ON THIS PAGE
</DiscordText>
<div className={cl("toc-links")}>
{entries.map(entry => (
<a
key={entry.id}
className={cl("toc-link")}
href={`#${entry.id}`}
onClick={e => {
e.preventDefault();
document.getElementById(entry.id)?.scrollIntoView({ behavior: "smooth", block: "start" });
}}
>
{entry.label}
</a>
))}
</div>
</div>
);
}
function MajorSection({ title, id, children }: { title: string; id: string; children: React.ReactNode; }) {
return (
<div id={id} className={cl("doc-section")}>
<DiscordHeading variant="heading-lg/semibold" color="text-strong">{title}</DiscordHeading>
{children}
</div>
);
}
function DemoSectionBlock({ section, relevantProps }: { section: DemoSection; relevantProps: PropDef[] | null; }) {
const id = section.id ?? generateId(section.title);
return (
<div id={id} className={cl("doc-section")}>
<DiscordHeading variant="heading-md/semibold" color="text-strong">{section.title}</DiscordHeading>
{section.notice && (
<Notice messageType={section.notice.type}>{section.notice.children}</Notice>
)}
{section.description && (
<Paragraph color="text-muted">{section.description}</Paragraph>
)}
<div className={cl("doc-demo")}>
{section.children}
</div>
{section.code && (
<CodeBlock content={section.code} lang="tsx" />
)}
{relevantProps && (
<div className={cl("doc-relevant-props")}>
<PropsTable props={relevantProps} />
</div>
)}
</div>
);
}
export function DocPage({ componentName, overview, importPath, sections, demos, props, examples, notices, showToc }: DocPageConfig) {
const items = sections ?? demos ?? [];
const flat = allProps(props);
const tocEntries = [
{ id: "overview", label: "Overview" },
{ id: "import", label: "Import" },
...items.map(s => ({ id: s.id ?? generateId(s.title), label: s.title })),
{ id: "api-reference", label: "API Reference" },
...(examples?.length ? [{ id: "examples", label: "Examples" }] : []),
];
const shouldShowToc = showToc ?? items.length >= 4;
return (
<div className={cl("doc-page")}>
{componentName && (
<DiscordHeading variant="heading-xl/bold" color="text-strong" className={cl("doc-title")}>
{componentName}
</DiscordHeading>
)}
<div id="overview" className={cl("doc-section")}>
<DiscordHeading variant="heading-lg/semibold" color="text-strong">Overview</DiscordHeading>
<Paragraph color="text-muted">{overview}</Paragraph>
</div>
{notices?.map((notice, i) => (
<Notice key={i} messageType={notice.type}>{notice.children}</Notice>
))}
{shouldShowToc && <TableOfContents entries={tocEntries} />}
<Divider />
<div id="import" className={cl("doc-section")}>
<div className={cl("import-block")}>
<DiscordText variant="text-xs/semibold" color="text-muted" className={cl("import-label")}>
IMPORT
</DiscordText>
<CodeBlock content={importPath} lang="tsx" />
</div>
</div>
{items.map(section => {
const relevant = section.relevantProps?.length
? flat.filter(p => section.relevantProps!.includes(p.name))
: null;
return [
<Divider key={`${section.title}-divider`} />,
<DemoSectionBlock key={section.title} section={section} relevantProps={relevant} />,
];
})}
<Divider />
<MajorSection title="API Reference" id="api-reference">
{isGrouped(props) ? (
props.map(group => (
<div key={group.title} className={cl("doc-prop-group")}>
<DiscordHeading variant="heading-md/medium" color="text-strong">
{group.title}
</DiscordHeading>
<PropsTable props={group.props} showLegend />
</div>
))
) : (
<PropsTable props={props} showLegend />
)}
</MajorSection>
{examples?.length ? (
<>
<Divider />
<MajorSection title="Examples" id="examples">
{examples.map(ex => (
<div key={ex.label} className={cl("doc-example")}>
<Paragraph color="text-muted">{ex.label}</Paragraph>
<CodeBlock content={ex.code} lang="tsx" />
</div>
))}
</MajorSection>
</>
) : null}
</div>
);
}

View file

@ -1,23 +0,0 @@
/*
* 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

@ -1,16 +0,0 @@
/*
* 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

@ -1,350 +0,0 @@
/*
* 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 { Card } from "@components/Card";
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { TooltipContainer } from "@components/TooltipContainer";
import { proxyLazy } from "@utils/lazy";
import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy, findLazy, mapMangledModuleLazy, waitFor, wreq } from "@webpack";
import {
GuildStore,
ListScrollerAuto,
ListScrollerThin,
OAuth2AuthorizeModal,
ScrollerAuto,
ScrollerNone,
ScrollerThin,
SearchableSelect,
TabBar,
useEffect,
useRef,
UserStore,
useState,
} from "@webpack/common";
import { ToastPosition, ToastType } from "../constants";
import type {
AccordionProps,
AlertsType,
AnchorProps,
AvatarProps,
BadgeShapesType,
CircleBadgeProps,
ClickableProps,
ColorPickerWithSwatchesProps,
ColorSwatchProps,
ConfirmModalProps,
ContextMenuApiType,
CustomColorButtonProps,
DefaultColorButtonProps,
DiscordHeadingProps,
DiscordTextProps,
ErrorBoundaryProps,
ExpressiveModalProps,
FocusLockProps,
GuildIconProps,
IconBadgeProps,
LocalErrorBoundaryProps,
ManaButtonProps,
ManaCheckboxGroupProps,
ManaCheckboxProps,
ManaListboxProps,
ManaPopoverProps,
ManaRichTooltipProps,
ManaSelectProps,
ManaTextAreaProps,
ManaTextButtonProps,
ManaTextInputProps,
ManaTooltipProps,
MenuType,
ModalProps,
ModalRenderFn,
NoticeProps,
NumberBadgeProps,
OpenModalOptions,
PaginatorProps,
PremiumBadgeProps,
ProgressBarProps,
SearchBarProps,
SliderProps,
SpinnerComponent,
TextBadgeProps,
TimestampProps,
ToastData,
ToastOptions,
ToastsModule,
ToastTypeValue,
UserSummaryItemProps,
} from "../types";
export * from "../constants";
export type * from "../types";
export {
Button,
Card,
GuildStore,
Heading,
ListScrollerAuto,
ListScrollerThin,
OAuth2AuthorizeModal,
Paragraph,
ScrollerAuto,
ScrollerNone,
ScrollerThin,
SearchableSelect,
TabBar,
TooltipContainer,
useEffect,
useRef,
UserStore,
useState,
};
export const Anchor = findComponentByCodeLazy("getDefaultLinkInterceptor", "useDefaultUnderlineStyles") as React.ComponentType<AnchorProps>;
export const Clickable = findByCodeLazy("ignoreKeyPress", "renderNonInteractive") as React.ComponentType<ClickableProps>;
export const ManaButton = findComponentByCodeLazy('"data-mana-component":"button"') as React.ComponentType<ManaButtonProps>;
export const ManaTextButton = findComponentByCodeLazy('"data-mana-component":"text-button"') as React.ComponentType<ManaTextButtonProps>;
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 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 ManaTooltip = findComponentByCodeLazy("VoidTooltip cannot find DOM node") as React.ComponentType<ManaTooltipProps>;
export const ManaRichTooltip = findComponentByCodeLazy('"data-mana-component":"rich-tooltip"') as React.ComponentType<ManaRichTooltipProps>;
export const ManaPopover = findByCodeLazy("title", "body", "badge", "graphic", "actions", "textLink", "gradientColor", "popoverRef") as React.ComponentType<ManaPopoverProps>;
export const Slider = findComponentByCodeLazy("stickToMarkers", "onMarkerRender", "grabberClassName") as React.ComponentType<SliderProps>;
export const Avatar = findComponentByCodeLazy("statusTooltip", "statusBackdropColor", "isSpeaking") as React.ComponentType<AvatarProps>;
export const ProgressBar = findComponentByCodeLazy("progressContainer", "labelledBy", "aria-valuenow") as React.ComponentType<ProgressBarProps>;
export const Spinner = findComponentByCodeLazy("spinningCircleSimple", "pulsingEllipsis", "wanderingCubes") as unknown as SpinnerComponent;
export const SearchBar = findComponentByCodeLazy("#{intl::SEARCH}", "clearable", "autoComplete") as React.ComponentType<SearchBarProps>;
export const Paginator = findComponentByCodeLazy("#{intl::BACK}", "#{intl::NEXT}", "renderPageWrapper") as React.ComponentType<PaginatorProps>;
export const Notice = findComponentByCodeLazy("messageType", '="text-default"', '="text-sm/medium"') as React.ComponentType<NoticeProps>;
export const Accordion = findComponentByCodeLazy("scrollHeight)}", "onExpandedChange", "defaultExpanded") as React.ComponentType<AccordionProps>;
export const Timestamp = findComponentByCodeLazy("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}", "isVisibleOnlyOnHover") as React.ComponentType<TimestampProps>;
export const GuildIcon = findComponentByCodeLazy("Masks.CLAN_ICON,width", "acronymClassName") as React.ComponentType<GuildIconProps>;
export const FocusLock = findComponentByCodeLazy(".containerRef,{keyboardModeEnabled:") as React.ComponentType<FocusLockProps>;
export const UserSummaryItem = findComponentByCodeLazy("popoutUserId") as React.ComponentType<UserSummaryItemProps>;
export const Animations = findByPropsLazy("useSpring", "animated", "useTransition");
export const SpringConfigs = proxyLazy(() => Animations.config) as {
default: object;
gentle: object;
wobbly: object;
stiff: object;
slow: object;
molasses: object;
};
export const useSpring = proxyLazy(() => Animations.useSpring) as (config: object) => object;
export const useTransition = proxyLazy(() => Animations.useTransition) as <T>(items: T[], config: object) => (callback: (style: object, item: T) => React.ReactNode) => React.ReactNode[];
export const useTrail = proxyLazy(() => Animations.useTrail) as (count: number, config: object) => object[];
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("BADGE_NOTIFICATION_BACKGROUND")) &&
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 PremiumBadge: React.ComponentType<PremiumBadgeProps> = proxyLazy(() =>
getBadgeExport(fn => 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 getBadgeWidthForValue: (count: number) => number = proxyLazy(() => {
for (const value of Object.values(BadgeModule)) {
if (typeof value === "function" && value.toString().includes("e<10?16:e<100")) return value as (count: number) => number;
}
return (count: number) => count < 10 ? 16 : count < 100 ? 22 : 30;
});
export const getBadgeCountString: (count: number) => string = proxyLazy(() => {
for (const value of Object.values(BadgeModule)) {
if (typeof value === "function" && value.toString().includes("e<1e3")) return value as (count: number) => string;
}
return (count: number) => count < 1000 ? `${count}` : `${Math.min(Math.floor(count / 1000), 9)}k+`;
});
const ToastsModule = findByPropsLazy("showToast", "popToast");
export const Toasts: ToastsModule = proxyLazy(() => ({
Type: ToastType,
Position: ToastPosition,
genId: () => Math.random().toString(36).slice(2),
show: (toast: ToastData) => ToastsModule.showToast(toast),
pop: () => ToastsModule.popToast(),
create: (message: string, type: ToastTypeValue, options?: ToastOptions) => ToastsModule.createToast(message, type, options),
}));
export const showToast = (message: string, type: ToastTypeValue = ToastType.MESSAGE) =>
ToastsModule.showToast(ToastsModule.createToast(message, type));
const ModalModule = findByPropsLazy("Modal", "ConfirmModal");
const ModalAPIModule = findByPropsLazy("openModal", "closeModal");
export const Modal: React.ComponentType<ModalProps> = proxyLazy(() => ModalModule.Modal);
export const ConfirmModal: React.ComponentType<ConfirmModalProps> = proxyLazy(() => ModalModule.ConfirmModal);
export const ExpressiveModal: React.ComponentType<ExpressiveModalProps> = proxyLazy(() => ModalModule.ExpressiveModal);
export const openModal = (render: ModalRenderFn, options?: OpenModalOptions): string =>
ModalAPIModule.openModal(render, options);
export const closeModal = (modalKey: string): void =>
ModalAPIModule.closeModal(modalKey);
export const closeAllModals = (): void =>
ModalAPIModule.closeAllModals();
export const hasModalOpen = (modalKey: string): boolean =>
ModalAPIModule.hasModalOpen(modalKey);
export const hasAnyModalOpen = (): boolean =>
ModalAPIModule.hasAnyModalOpen();
const DiscordTextModule = findByPropsLazy("Heading", "Text");
export function DiscordHeading(props: DiscordHeadingProps) {
return <DiscordTextModule.Heading {...props} />;
}
export function DiscordText(props: DiscordTextProps) {
return <DiscordTextModule.Text {...props} />;
}
export function Divider({ className, style, ...restProps }: React.ComponentPropsWithoutRef<"hr">) {
return <hr className={`vc-divider${className ? ` ${className}` : ""}`} style={style} {...restProps} />;
}
const CodeContainerClasses = findCssClassesLazy("markup", "codeContainer");
export function InlineCode({ children }: { children: React.ReactNode; }) {
return (
<span className={CodeContainerClasses.markup}>
<code className="inline">{children}</code>
</span>
);
}
const AlertsModule = findByPropsLazy("show", "close", "confirm");
export const Alerts: AlertsType = {
show: options => AlertsModule.show(options),
close: () => AlertsModule.close(),
confirm: options => AlertsModule.confirm(options),
};
const MenuComponents: Record<string, React.ComponentType<any>> = {};
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
const exports = wreq(id);
for (const key in exports) {
try {
const value = exports[key];
if (typeof value === "function" && value.name?.startsWith("Menu")) {
MenuComponents[value.name] = value;
}
} catch { }
}
});
waitFor(filters.componentByCode('path:["empty"]'), m => { MenuComponents.Menu = m; });
waitFor(filters.componentByCode("SLIDER)", "handleSize:16"), m => { MenuComponents.MenuSliderControl = m; });
waitFor(filters.componentByCode(".SEARCH)", ".focus()", "query:"), m => { MenuComponents.MenuSearchControl = m; });
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
const exports = wreq(id);
for (const key in exports) {
try {
const value = exports[key];
if (typeof value !== "function") continue;
const str = value.toString();
if (str.length === 26 && str.endsWith("(e){return null}") && value.name === "l") {
Object.defineProperty(value, "name", { value: "MenuSwitchItem" });
MenuComponents.MenuSwitchItem = value;
break;
}
} catch { }
}
});
export const Menu: MenuType = proxyLazy(() => ({
Menu: MenuComponents.Menu,
MenuItem: MenuComponents.MenuItem,
MenuCheckboxItem: MenuComponents.MenuCheckboxItem,
MenuRadioItem: MenuComponents.MenuRadioItem,
MenuSwitchItem: MenuComponents.MenuSwitchItem,
MenuGroup: MenuComponents.MenuGroup,
MenuSeparator: MenuComponents.MenuSeparator,
MenuControlItem: MenuComponents.MenuControlItem,
MenuSliderControl: MenuComponents.MenuSliderControl,
MenuSearchControl: MenuComponents.MenuSearchControl,
}));
export const ContextMenuApi: ContextMenuApiType = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', {
closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"),
openContextMenu: filters.byCode("renderLazy:"),
openContextMenuLazy: e => typeof e === "function" && e.toString().length < 100
});
export const ErrorBoundary = findComponentByCodeLazy("this.resetErrorBoundary", "onReset", "FallbackComponent") as React.ComponentType<ErrorBoundaryProps>;
export const LocalErrorBoundary = findComponentByCodeLazy("LocalErrorBoundary", "text-feedback-critical") as React.ComponentType<LocalErrorBoundaryProps>;

View file

@ -1,102 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Accordion, Paragraph, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const ACCORDION_PROPS: PropDef[] = [
{ name: "title", type: "string", required: true, description: "Header title text. Rendered as text-sm/medium." },
{ name: "children", type: "ReactNode", required: true, description: "Content revealed when expanded." },
{ name: "subtitle", type: "string", description: "Optional subtitle below the title. Rendered as text-xs/medium in text-subtle." },
{ name: "icon", type: "ReactNode", description: "Optional icon displayed before the title." },
{ name: "defaultExpanded", type: "boolean", default: "false", description: "Initial expanded state for uncontrolled usage." },
{ name: "isExpanded", type: "boolean", description: "Controlled expanded state. When set, the component becomes controlled." },
{ name: "onExpandedChange", type: "(expanded: boolean) => void", description: "Called when expanded state changes. Works in both controlled and uncontrolled modes." },
{ name: "onOpen", type: "() => void", description: "Called only when the accordion opens, not when it closes." },
{ name: "maxHeight", type: "number | string", description: "Maximum content height when expanded. Numbers are converted to px. Without this, height is based on scrollHeight." },
{ name: "className", type: "string", description: "Additional CSS class name on the root element." },
];
function ControlledDemo() {
const [controlled, setControlled] = useState(false);
return (
<Accordion
title="Controlled Accordion"
isExpanded={controlled}
onExpandedChange={setControlled}
>
<Paragraph>This accordion's state is controlled externally.</Paragraph>
</Accordion>
);
}
export default function AccordionTab() {
return (
<DocPage
componentName="Accordion"
overview="Accordion is a collapsible content container with a clickable header. It supports both controlled and uncontrolled modes, optional subtitle and icon, and configurable max height for overflowing content."
importPath={'import { Accordion } from "../components";'}
sections={[
{
title: "Basic",
description: "Click the header to expand and collapse.",
children: (
<Accordion title="Click to expand">
<Paragraph>This is the accordion content that is revealed when expanded.</Paragraph>
</Accordion>
),
code: '<Accordion title="Details">\n <Paragraph>Hidden content here.</Paragraph>\n</Accordion>',
relevantProps: ["title", "children"]
},
{
title: "With Subtitle",
description: "A subtitle renders below the title in smaller, subtler text.",
children: (
<Accordion title="Settings" subtitle="Configure options">
<Paragraph>Content with a subtitle in the header.</Paragraph>
</Accordion>
),
relevantProps: ["subtitle", "icon"]
},
{
title: "Default Expanded",
description: "Starts open on mount using defaultExpanded.",
children: (
<Accordion title="Already Open" defaultExpanded>
<Paragraph>This accordion starts expanded by default.</Paragraph>
</Accordion>
),
relevantProps: ["defaultExpanded"]
},
{
title: "Controlled",
description: "Use isExpanded and onExpandedChange to manage state externally.",
children: <ControlledDemo />,
code: '<Accordion title="Settings" isExpanded={open} onExpandedChange={setOpen}>\n <Paragraph>Controlled content.</Paragraph>\n</Accordion>',
relevantProps: ["isExpanded", "onExpandedChange"]
},
{
title: "With Max Height",
description: "Constrains expanded content height. Overflowing content is clipped.",
children: (
<Accordion title="Limited Height" maxHeight={100}>
<Paragraph>Line 1</Paragraph>
<Paragraph>Line 2</Paragraph>
<Paragraph>Line 3</Paragraph>
<Paragraph>Line 4</Paragraph>
<Paragraph>Line 5</Paragraph>
<Paragraph>Line 6</Paragraph>
</Accordion>
),
code: '<Accordion title="Advanced" subtitle="Extra options" icon={<GearIcon />} maxHeight={200}>\n <Paragraph>Scrollable content area.</Paragraph>\n</Accordion>',
relevantProps: ["maxHeight"]
},
]}
props={ACCORDION_PROPS}
/>
);
}

View file

@ -1,129 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Alerts, Button, showToast, ToastType } from "..";
import { DocPage, type PropDef } from "../DocPage";
const ALERT_PROPS: PropDef[] = [
{ name: "title", type: "string", required: true, description: "Alert dialog title." },
{ name: "body", type: "ReactNode", required: true, description: "Alert body content. Can be a string or JSX." },
{ name: "confirmText", type: "string", description: "Text for the confirm button. Defaults to a localized \"Okay\" string." },
{ name: "cancelText", type: "string", description: "Text for the cancel button. Only shown when set to a non-empty string." },
{ name: "confirmVariant", type: '"primary" | "critical-primary" | "expressive"', default: '"primary"', description: "Visual variant for the confirm button." },
{ name: "onConfirm", type: "() => void", description: "Called when the confirm button is clicked." },
{ name: "onCancel", type: "() => void", description: "Called when the cancel button is clicked." },
{ name: "onCloseCallback", type: "() => void", description: "Called when the alert closes, regardless of which button was pressed." },
{ name: "contextKey", type: "string", description: "Optional context key passed to the modal system." },
];
export default function AlertTab() {
return (
<DocPage
componentName="Alerts"
overview={"Alerts is a utility API for showing modal alert dialogs. It wraps Discord's ConfirmModal with a simple show/close/confirm interface. The confirm method returns a Promise that resolves to true or false based on the user's choice."}
notices={[
{ type: "warn", children: "Alerts.show() opens a modal dialog that blocks user interaction with the rest of the UI. Avoid chaining multiple alerts or showing them in rapid succession." },
{ type: "info", children: "Prefer Alerts.confirm() over Alerts.show() when you need to act on the user's choice, since it returns a Promise<boolean> that cleanly integrates with async flows." },
]}
importPath={'import { Alerts } from "../components";'}
sections={[
{
title: "Basic Alert",
description: "Simple alert with just a title and body. Only shows a confirm button.",
children: (
<Button onClick={() => {
Alerts.show({
title: "Basic Alert",
body: "This is a basic alert dialog with just an OK button.",
});
}}>
Show Basic Alert
</Button>
),
code: 'Alerts.show({ title: "Done", body: "Operation completed successfully." });',
relevantProps: ["title", "body"]
},
{
title: "Confirm Alert",
description: "Alert with confirm and cancel buttons. Both fire callbacks on click.",
children: (
<Button onClick={() => {
Alerts.show({
title: "Confirm Action",
body: "Are you sure you want to proceed with this action?",
confirmText: "Yes, proceed",
cancelText: "Cancel",
onConfirm: () => showToast("Confirmed!", ToastType.SUCCESS),
onCancel: () => showToast("Cancelled", ToastType.MESSAGE),
});
}}>
Show Confirm Alert
</Button>
),
relevantProps: ["confirmText", "cancelText", "onConfirm", "onCancel"]
},
{
title: "Danger Alert",
description: "Uses critical-primary variant for destructive actions.",
children: (
<Button variant="dangerPrimary" onClick={() => {
Alerts.show({
title: "Delete Item",
body: "This action cannot be undone. Are you sure you want to delete this item?",
confirmText: "Delete",
confirmVariant: "critical-primary",
cancelText: "Keep it",
onConfirm: () => showToast("Deleted!", ToastType.SUCCESS),
});
}}>
Show Danger Alert
</Button>
),
code: 'Alerts.show({\n title: "Delete?",\n body: "This cannot be undone.",\n confirmText: "Delete",\n confirmVariant: "critical-primary",\n cancelText: "Cancel",\n onConfirm: () => deleteItem(),\n});',
relevantProps: ["confirmVariant"]
},
{
title: "Expressive Alert",
description: "Uses the expressive variant for premium or promotional actions.",
children: (
<Button onClick={() => {
Alerts.show({
title: "Premium Feature",
body: "This feature requires a premium subscription.",
confirmText: "Upgrade Now",
confirmVariant: "expressive",
cancelText: "Maybe Later",
onConfirm: () => showToast("Upgrading!", ToastType.SUCCESS),
});
}}>
Show Expressive Alert
</Button>
),
relevantProps: ["confirmVariant"]
},
{
title: "Promise-based Confirm",
description: "Alerts.confirm() returns a Promise<boolean>. Resolves true on confirm, false on cancel.",
children: (
<Button onClick={async () => {
const result = await Alerts.confirm({
title: "Async Confirm",
body: "This uses Alerts.confirm() which returns a promise.",
confirmText: "Accept",
cancelText: "Decline",
});
showToast(result ? "Accepted!" : "Declined", result ? ToastType.SUCCESS : ToastType.MESSAGE);
}}>
Show Promise Confirm
</Button>
),
code: 'const confirmed = await Alerts.confirm({\n title: "Proceed?",\n body: "Are you sure?",\n confirmText: "Yes",\n cancelText: "No",\n});\nif (confirmed) doSomething();'
},
]}
props={ALERT_PROPS}
/>
);
}

View file

@ -1,109 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Anchor, Paragraph } from "..";
import { DocPage, type PropDef } from "../DocPage";
const ANCHOR_PROPS: PropDef[] = [
{ name: "href", type: "string", description: "Link URL. External links automatically get rel=\"noreferrer noopener\" and target=\"_blank\"." },
{ name: "children", type: "ReactNode", description: "Link content." },
{ name: "onClick", type: "MouseEventHandler", description: "Click handler. If omitted and href is set, uses Discord's default link interceptor." },
{ name: "useDefaultUnderlineStyles", type: "boolean", default: "true", description: "Whether to show underline on hover." },
{ name: "title", type: "string", description: "Tooltip text shown on hover." },
{ name: "target", type: "string", description: "Link target. Auto-set to _blank for external URLs." },
{ name: "rel", type: "string", description: "Link relationship. Auto-set to \"noreferrer noopener\" for external URLs." },
{ name: "focusProps", type: "Record<string, unknown>", internal: true, description: "Props forwarded to the focus management wrapper." },
{ name: "ref", type: "Ref<HTMLAnchorElement>", description: "Ref to the underlying anchor element." },
{ name: "className", type: "string", description: "Additional CSS class name." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
];
export default function AnchorTab() {
return (
<DocPage
componentName="Anchor"
overview="Anchor is Discord's styled link component. It handles external link safety (adding noreferrer/noopener, opening in new tabs), untrusted link warning modals, and integrates with Discord's focus management system. Supports underline-on-hover styling by default."
notices={[
{ type: "positive", children: 'Anchor automatically applies rel="noreferrer noopener" and target="_blank" for external URLs, so you don\'t need to set these manually.' },
]}
importPath={'import { Anchor } from "../components";'}
sections={[
{
title: "Basic Link",
description: "A styled link with underline on hover.",
children: (
<Anchor href="https://discord.com">
Discord Homepage
</Anchor>
),
code: '<Anchor href="https://example.com">Visit Example</Anchor>',
relevantProps: ["href", "children"]
},
{
title: "Without Underline",
description: "Disable the default underline-on-hover behavior with useDefaultUnderlineStyles={false}.",
children: (
<Anchor href="https://discord.com" useDefaultUnderlineStyles={false}>
No underline on hover
</Anchor>
),
relevantProps: ["useDefaultUnderlineStyles"]
},
{
title: "With Title Tooltip",
description: "Hover to see the native title tooltip.",
children: (
<Anchor href="https://discord.com" title="Click to visit Discord">
Hover for tooltip
</Anchor>
),
relevantProps: ["title"]
},
{
title: "Custom Styling",
children: (
<Anchor
href="https://discord.com"
style={{ color: "var(--text-positive)", fontWeight: 600 }}
>
Green styled link
</Anchor>
),
relevantProps: ["style", "className"]
},
{
title: "With onClick Handler",
description: "Custom click handler prevents default navigation.",
children: (
<Anchor
href="#"
onClick={e => {
e.preventDefault();
alert("Link clicked!");
}}
>
Click me (shows alert)
</Anchor>
),
code: '<Anchor href="#" onClick={e => { e.preventDefault(); doSomething(); }}>\n Click me\n</Anchor>',
relevantProps: ["onClick"]
},
{
title: "Inline in Text",
description: "Links used naturally within a paragraph.",
children: (
<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>
),
code: '<Paragraph>\n Read the <Anchor href="/docs" useDefaultUnderlineStyles={false}>documentation</Anchor>.\n</Paragraph>'
},
]}
props={ANCHOR_PROPS}
/>
);
}

View file

@ -1,220 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Animations, ManaButton, Paragraph, SpringConfigs, useSpring, useState, useTrail, useTransition } from "..";
import { DocPage, type PropDef } from "../DocPage";
const { animated } = Animations;
const TRAIL_ITEMS = ["Discord", "uses", "react-spring", "for", "animations"];
const SPRING_CONFIG_PROPS: PropDef[] = [
{ name: "default", type: "{ tension: 170, friction: 26 }", description: "Standard spring. Balanced speed and bounce." },
{ name: "gentle", type: "{ tension: 120, friction: 14 }", description: "Soft, slow spring with more bounce." },
{ name: "wobbly", type: "{ tension: 180, friction: 12 }", description: "Bouncy, playful spring with high overshoot." },
{ name: "stiff", type: "{ tension: 210, friction: 20 }", description: "Quick, snappy spring with minimal bounce." },
{ name: "slow", type: "{ tension: 280, friction: 60 }", description: "High tension with heavy damping. Slow and deliberate." },
{ name: "molasses", type: "{ tension: 280, friction: 120 }", description: "Extremely slow and heavily damped. No bounce." },
];
function SpringDemo() {
const [toggled, setToggled] = useState(false);
const [config, setConfig] = useState<keyof typeof SpringConfigs>("default");
const spring = useSpring({
opacity: toggled ? 1 : 0.3,
transform: toggled ? "scale(1) rotate(0deg)" : "scale(0.8) rotate(-10deg)",
backgroundColor: toggled ? "var(--brand-500)" : "var(--background-tertiary)",
config: SpringConfigs[config]
});
return (
<>
<div style={{ display: "flex", gap: 8, marginBottom: 16, flexWrap: "wrap" }}>
{(Object.keys(SpringConfigs) as (keyof typeof SpringConfigs)[]).map(c => (
<ManaButton
key={c}
variant={config === c ? "primary" : "secondary"}
size="sm"
text={c}
onClick={() => setConfig(c)}
/>
))}
</div>
<animated.div
onClick={() => setToggled(!toggled)}
style={{
...spring,
width: 120,
height: 120,
borderRadius: 12,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<Paragraph color="text-strong">Click me</Paragraph>
</animated.div>
</>
);
}
function TransitionDemo() {
const [items, setItems] = useState([1, 2, 3]);
const [nextId, setNextId] = useState(4);
const transitions = useTransition(items, {
from: { opacity: 0, transform: "translateX(-20px) scale(0.9)" },
enter: { opacity: 1, transform: "translateX(0px) scale(1)" },
leave: { opacity: 0, transform: "translateX(20px) scale(0.9)" },
keys: item => item,
config: SpringConfigs.gentle
});
return (
<>
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
<ManaButton variant="primary" size="sm" text="Add Item" onClick={() => {
setItems([...items, nextId]);
setNextId(nextId + 1);
}} />
<ManaButton
variant="secondary"
size="sm"
text="Clear All"
onClick={() => setItems([])}
disabled={items.length === 0}
/>
</div>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", minHeight: 50 }}>
{transitions((style, item) => (
<animated.div
style={{
...style,
padding: "8px 16px",
background: "var(--background-secondary)",
borderRadius: 8,
cursor: "pointer"
}}
onClick={() => setItems(items.filter(i => i !== item))}
>
<Paragraph>Item {item}</Paragraph>
</animated.div>
))}
</div>
</>
);
}
function TrailDemo() {
const [show, setShow] = useState(true);
const trail = useTrail(TRAIL_ITEMS.length, {
opacity: show ? 1 : 0,
transform: show ? "translateY(0px)" : "translateY(20px)",
config: SpringConfigs.gentle
}) as object[];
return (
<>
<div style={{ marginBottom: 16 }}>
<ManaButton
variant="secondary"
size="sm"
text={show ? "Hide" : "Show"}
onClick={() => setShow(!show)}
/>
</div>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", minHeight: 40 }}>
{trail.map((style, i) => (
<animated.div
key={i}
style={{
...style,
padding: "8px 12px",
background: "var(--brand-500)",
borderRadius: 6
}}
>
<Paragraph color="text-strong">{TRAIL_ITEMS[i]}</Paragraph>
</animated.div>
))}
</div>
</>
);
}
function AnimatedElementsDemo() {
const [hover, setHover] = useState(false);
const hoverSpring = useSpring({
scale: hover ? 1.05 : 1,
boxShadow: hover
? "0 8px 24px rgba(0,0,0,0.3)"
: "0 2px 8px rgba(0,0,0,0.1)",
config: SpringConfigs.wobbly
});
return (
<animated.div
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
...hoverSpring,
width: 100,
height: 100,
background: "var(--background-secondary)",
borderRadius: 12,
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer"
}}
>
<Paragraph color="text-muted" style={{ fontSize: 12 }}>Hover me</Paragraph>
</animated.div>
);
}
export default function AnimationsTab() {
return (
<DocPage
componentName="Animations"
overview="Discord uses react-spring for physics-based animations. The Animations module exposes useSpring, useTransition, useTrail, useChain, useSprings hooks plus animated.* element wrappers and preset spring configs. All animations are interruptible and use spring physics rather than duration-based easing."
notices={[
{ type: "info", children: "These are re-exports of react-spring. All animations are interruptible and respect the user's reduced motion preference when used with Discord's built-in motion settings." },
]}
importPath={'import { Animations, useSpring, useTransition, useTrail, SpringConfigs } from "../components";'}
sections={[
{
title: "useSpring",
description: "Animate values with spring physics. Click the box to toggle, and switch between config presets to see different animation feels.",
children: <SpringDemo />,
code: "const spring = useSpring({ opacity: visible ? 1 : 0, config: SpringConfigs.gentle });\nreturn <animated.div style={spring}>Content</animated.div>;"
},
{
title: "useTransition",
description: "Animate items entering and leaving a list. Click items to remove them, or use the buttons to add/clear.",
children: <TransitionDemo />,
code: "const transitions = useTransition(items, {\n from: { opacity: 0 },\n enter: { opacity: 1 },\n leave: { opacity: 0 },\n keys: item => item.id,\n});\nreturn transitions((style, item) => <animated.div style={style}>{item.name}</animated.div>);"
},
{
title: "useTrail",
description: "Staggered animations for multiple items. Each item animates slightly after the previous one.",
children: <TrailDemo />,
code: 'const trail = useTrail(items.length, {\n opacity: show ? 1 : 0,\n transform: show ? "translateY(0)" : "translateY(20px)",\n config: SpringConfigs.gentle,\n});'
},
{
title: "animated Elements",
description: "Use animated.div, animated.span, animated.svg etc. to create spring-animated HTML elements. Hover the box below.",
children: <AnimatedElementsDemo />
},
]}
props={SPRING_CONFIG_PROPS}
/>
);
}

View file

@ -1,157 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Avatar, AvatarSizes, AvatarStatuses, Paragraph, UserStore } from "..";
import { DocPage, type PropDef } from "../DocPage";
const AVATAR_PROPS: PropDef[] = [
{ name: "src", type: "string", description: "Avatar image URL." },
{ name: "size", type: "AvatarSize", description: "Avatar size constant (SIZE_16 through SIZE_152). Controls the SVG viewBox dimensions." },
{ name: "status", type: "AvatarStatus", description: "User presence status. Renders a colored indicator dot." },
{ name: "statusColor", type: "string", description: "Override the status indicator color." },
{ name: "statusBackdropColor", type: "string", description: "Background color behind the status indicator." },
{ name: "isMobile", type: "boolean", default: "false", description: "Shows the mobile phone status indicator instead of a dot." },
{ name: "isVR", type: "boolean", default: "false", description: "Shows the VR headset status indicator." },
{ name: "isTyping", type: "boolean", default: "false", description: "Shows a typing animation in the status area." },
{ name: "isSpeaking", type: "boolean", default: "false", description: "Renders an animated speaking ring around the avatar." },
{ name: "isLatched", type: "boolean", default: "false", description: "Latched speaking state for voice activity." },
{ name: "voiceDb", type: "number", description: "Voice volume level in dB for speaking animation intensity." },
{ name: "speakingStylesConfig", type: "Record<string, any>", description: "Custom styles config for the speaking ring animation." },
{ name: "statusTooltip", type: "boolean", default: "false", description: "Shows a tooltip with the status text on hover." },
{ name: "statusTooltipDelay", type: "number", description: "Delay in ms before the status tooltip appears." },
{ name: "typingIndicatorRef", type: "Ref<any>", description: "Ref for the typing indicator element." },
{ name: "avatarContentRef", type: "Ref<any>", description: "Ref for the avatar image foreignObject content." },
{ name: "CutoutIcon", type: "ComponentType<any>", description: "Custom icon component rendered as a cutout overlay on the avatar." },
{ name: "avatarTooltipAsset", type: "ReactNode", description: "Custom tooltip asset displayed on the avatar." },
{ name: "avatarTooltipText", type: "string", description: "Tooltip text for the avatar overlay." },
{ name: "avatarTooltipTitle", type: "string", description: "Tooltip title for the avatar overlay." },
{ name: "avatarDecoration", type: "string", description: "URL for the avatar decoration overlay image." },
{ name: "aria-hidden", type: "boolean", default: "false", description: "Hides the avatar from screen readers." },
{ name: "aria-label", type: "string", description: "Accessibility label for the avatar." },
{ name: "imageClassName", type: "string", description: "CSS class applied to the avatar image element." },
{ name: "className", type: "string", description: "CSS class applied to the root wrapper." },
{ name: "onClick", type: "MouseEventHandler", description: "Click handler on the avatar wrapper. Makes the avatar interactive." },
{ name: "onMouseDown", type: "MouseEventHandler", description: "Mouse down handler on the avatar wrapper." },
{ name: "onKeyDown", type: "KeyboardEventHandler", description: "Key down handler on the avatar wrapper." },
{ name: "onContextMenu", type: "MouseEventHandler", description: "Context menu handler on the avatar wrapper." },
{ name: "onMouseEnter", type: "MouseEventHandler", description: "Mouse enter handler on the avatar wrapper." },
{ name: "onMouseLeave", type: "MouseEventHandler", description: "Mouse leave handler on the avatar wrapper." },
{ name: "tabIndex", type: "number", description: "Tab index for keyboard navigation on the avatar wrapper." },
{ name: "ref", type: "Ref<SVGSVGElement>", description: "Ref to the root SVG element." },
];
export default function AvatarTab() {
const currentUser = UserStore.getCurrentUser();
const avatarUrl = currentUser?.getAvatarURL?.(undefined, 128);
return (
<DocPage
componentName="Avatar"
overview="Avatar renders a user's profile picture as an SVG with optional status indicators, speaking rings, typing animations, and tooltip overlays. It handles all the visual states used throughout Discord's UI including mobile, VR, and voice activity indicators."
importPath={'import { Avatar } from "../components";'}
sections={[
{
title: "Sizes",
description: "All 13 available sizes from 16px to 152px.",
relevantProps: ["size", "src"],
children: (
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
{AvatarSizes.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>
)
},
{
title: "Statuses",
description: "All status indicator variants.",
code: '<Avatar src={user.getAvatarURL()} size="SIZE_40" status="online" />',
relevantProps: ["status", "statusColor", "statusBackdropColor"],
children: (
<div className="vc-compfinder-grid">
{AvatarStatuses.map(status => (
<div key={status} style={{ textAlign: "center" }}>
<Avatar src={avatarUrl} size="SIZE_40" status={status} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
{status}
</Paragraph>
</div>
))}
</div>
)
},
{
title: "Mobile Status",
description: "Shows the mobile phone icon instead of a status dot when isMobile is true.",
code: '<Avatar src={avatarUrl} size="SIZE_32" status="online" isMobile statusTooltip />',
relevantProps: ["isMobile", "isVR"],
children: (
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center" }}>
<Avatar src={avatarUrl} size="SIZE_40" status="online" isMobile />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Mobile</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<Avatar src={avatarUrl} size="SIZE_40" status="online" isVR />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>VR</Paragraph>
</div>
</div>
)
},
{
title: "Speaking State",
description: "Animated ring indicator for voice activity.",
code: '<Avatar src={avatarUrl} size="SIZE_80" isSpeaking voiceDb={voiceLevel} />',
relevantProps: ["isSpeaking", "isLatched", "voiceDb"],
children: (
<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>
)
},
{
title: "Typing Indicator",
description: "Shows a typing animation in the status area.",
relevantProps: ["isTyping"],
children: (
<div className="vc-compfinder-grid">
<div style={{ textAlign: "center" }}>
<Avatar src={avatarUrl} size="SIZE_40" status="online" isTyping />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Typing</Paragraph>
</div>
</div>
)
},
{
title: "Status Tooltip",
description: "Hover to see the status text tooltip.",
relevantProps: ["statusTooltip", "statusTooltipDelay"],
children: (
<div className="vc-compfinder-grid">
{AvatarStatuses.map(status => (
<div key={status} style={{ textAlign: "center" }}>
<Avatar src={avatarUrl} size="SIZE_40" status={status} statusTooltip />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
{status}
</Paragraph>
</div>
))}
</div>
)
},
]}
props={AVATAR_PROPS}
/>
);
}

View file

@ -1,142 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, PremiumBadge, TextBadge } from "..";
import { DocPage, type PropDef } from "../DocPage";
import { EquicordIcon } from "../icons/EquicordIcon";
const SHARED_PROPS: PropDef[] = [
{ name: "color", type: "string", default: "BADGE_NOTIFICATION_BACKGROUND", description: "Background color. Defaults to the notification red for most badges. CircleBadge defaults to INTERACTIVE_TEXT_ACTIVE instead." },
{ name: "disableColor", type: "boolean", default: "false", description: "Removes the background color entirely when true." },
{ name: "shape", type: "BadgeShapes", default: "ROUND", description: "Controls border radius. ROUND (pill), ROUND_LEFT, ROUND_RIGHT, or SQUARE." },
{ name: "className", type: "string", description: "Additional CSS class name." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
];
const ALL_PROPS: PropDef[] = [
{ name: "count", type: "number", required: true, description: "NumberBadge only. The count to display. Auto-abbreviates large values (1000+ becomes \"1k+\", capped at \"9k+\")." },
{ name: "renderBadgeCount", type: "(count: number) => string", description: "NumberBadge only. Custom formatter for the count display. Defaults to the built-in abbreviation." },
{ name: "text", type: "string", required: true, description: "TextBadge and PremiumBadge. The text content to display." },
{ name: "icon", type: "ComponentType<{ className?: string; color?: string; }>", required: true, description: "IconBadge only. Icon component to render inside the badge." },
...SHARED_PROPS,
];
export default function BadgeTab() {
return (
<DocPage
componentName="Badge"
overview="Discord's badge system provides four visual badge components (NumberBadge, TextBadge, IconBadge, CircleBadge) plus a PremiumBadge variant. All share a common set of styling props for color, shape, and disabled state. NumberBadge auto-sizes its width based on digit count and abbreviates large numbers."
importPath={'import { NumberBadge, TextBadge, IconBadge, CircleBadge, PremiumBadge, BadgeShapes } from "../components";'}
sections={[
{
title: "NumberBadge",
description: "Displays a count. Auto-abbreviates at 1000+ and auto-sizes width based on digit count (16px for <10, 22px for <100, 30px otherwise).",
code: "<NumberBadge count={unreadCount} />",
relevantProps: ["count", "renderBadgeCount"],
children: (
<div className="vc-compfinder-grid">
<NumberBadge count={1} />
<NumberBadge count={9} />
<NumberBadge count={99} />
<NumberBadge count={999} />
<NumberBadge count={9999} />
</div>
)
},
{
title: "TextBadge",
description: "Displays text content inside a badge.",
relevantProps: ["text"],
children: (
<div className="vc-compfinder-grid">
<TextBadge text="NEW" />
<TextBadge text="BETA" />
<TextBadge text="PRO" />
</div>
)
},
{
title: "PremiumBadge",
description: "A TextBadge variant with an additional premium styling class applied.",
relevantProps: ["text"],
children: (
<div className="vc-compfinder-grid">
<PremiumBadge text="PREMIUM" />
<PremiumBadge text="NITRO" />
<PremiumBadge text="BOOST" />
</div>
)
},
{
title: "IconBadge",
description: "Renders an icon component inside a badge. The icon receives color=\"currentColor\".",
code: '<IconBadge icon={StarIcon} color="var(--status-warning)" />',
relevantProps: ["icon"],
children: (
<div className="vc-compfinder-grid">
<IconBadge icon={EquicordIcon} />
<IconBadge icon={EquicordIcon} color="var(--status-positive)" />
<IconBadge icon={EquicordIcon} color="var(--status-warning)" />
</div>
)
},
{
title: "CircleBadge",
description: "A simple circular dot indicator. Defaults to INTERACTIVE_TEXT_ACTIVE color unlike other badges.",
children: (
<div className="vc-compfinder-grid">
<CircleBadge />
<CircleBadge color="var(--status-positive)" />
<CircleBadge color="var(--status-warning)" />
<CircleBadge color="var(--status-danger)" />
</div>
)
},
{
title: "Shapes",
description: "All four shape variants. SQUARE removes border radius entirely.",
code: '<TextBadge text="NEW" shape={BadgeShapes.ROUND_LEFT} color="var(--brand-500)" />',
relevantProps: ["shape"],
children: (
<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>
)
},
{
title: "Custom Colors",
description: "Any CSS color value works. Use disableColor to remove the background entirely.",
relevantProps: ["color", "disableColor"],
children: (
<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>
)
},
{
title: "Custom Render",
description: "NumberBadge accepts a custom renderBadgeCount function to format the display.",
code: '<NumberBadge\n count={messageCount}\n renderBadgeCount={n => n > 99 ? "99+" : String(n)}\n/>',
children: (
<div className="vc-compfinder-grid">
<NumberBadge count={42} renderBadgeCount={n => `#${n}`} />
<NumberBadge count={100} renderBadgeCount={n => `${n}%`} />
<NumberBadge count={5} renderBadgeCount={() => "!"} />
</div>
)
},
]}
props={ALL_PROPS}
/>
);
}

View file

@ -1,142 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaButton, ManaButtonSizes, ManaButtonVariants, Paragraph, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const BUTTON_PROPS: PropDef[] = [
{ name: "text", type: "string", description: "Button label text." },
{ name: "variant", type: "ManaButtonVariant", default: '"primary"', description: "Visual style variant." },
{ name: "size", type: "ManaButtonSize", default: '"md"', description: "Button size. Also controls icon and text variant sizing internally." },
{ name: "disabled", type: "boolean", default: "false", description: "Disables the button. When true, all event handlers are suppressed." },
{ name: "loading", type: "boolean", default: "false", description: "Shows a loading spinner overlay and suppresses event handlers like disabled." },
{ name: "loadingStartedLabel", type: "string", description: "Accessibility label announced via live region when loading begins." },
{ name: "loadingFinishedLabel", type: "string", description: "Accessibility label announced via live region when loading ends." },
{ name: "fullWidth", type: "boolean", default: "false", description: "Expands the button to fill its container width. Can be overridden by a parent ButtonGroup context." },
{ name: "rounded", type: "boolean", default: "false", description: "Applies fully rounded pill-shaped corners." },
{ name: "icon", type: "ManaButtonIconType", description: "Icon to display. Accepts a component, or an object with type \"icon\", \"rive\", or \"sticker\"." },
{ name: "iconPosition", type: '"start" | "end"', default: '"start"', description: "Which side of the text the icon appears on." },
{ name: "iconOpticalOffsetMargin", type: "number", default: "0", description: "Pixel offset margin for optical icon alignment." },
{ name: "minWidth", type: "number | string", description: "Minimum button width. Numbers are converted to px." },
{ name: "role", type: "string", default: '"button"', description: "ARIA role attribute." },
{ name: "type", type: '"button" | "submit" | "reset"', default: '"button"', description: "HTML button type attribute." },
{ name: "rel", type: "string", description: "Relationship attribute for link-style buttons." },
{ name: "onClick", type: "(e: MouseEvent) => void", description: "Click handler. Suppressed when disabled or loading." },
{ name: "onDoubleClick", type: "(e: MouseEvent) => void", description: "Double-click handler. Suppressed when disabled or loading." },
{ name: "onMouseEnter", type: "(e: MouseEvent) => void", description: "Mouse enter handler. Suppressed when disabled or loading." },
{ name: "onMouseLeave", type: "(e: MouseEvent) => void", description: "Mouse leave handler. Suppressed when disabled or loading." },
{ name: "onMouseUp", type: "(e: MouseEvent) => void", description: "Mouse up handler. Suppressed when disabled or loading." },
{ name: "onMouseDown", type: "(e: MouseEvent) => void", description: "Mouse down handler. Suppressed when disabled or loading." },
{ name: "onKeyDown", type: "(e: KeyboardEvent) => void", description: "Keyboard handler. Suppressed when disabled or loading." },
{ name: "focusProps", type: "Record<string, any>", internal: true, description: "Props forwarded to the focus management wrapper." },
{ name: "buttonRef", type: "Ref<HTMLButtonElement>", internal: true, description: "Ref attached to the underlying button element." },
{ name: "className", type: "string", description: "Additional CSS class name." },
{ name: "style", type: "CSSProperties", description: "Inline styles applied to the button." },
];
function StatesDemo() {
const [loading, setLoading] = useState(false);
return (
<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>
);
}
export default function ButtonsTab() {
return (
<DocPage
overview="ManaButton is Discord's primary button component used for actions and form submissions. It supports eight visual variants, three sizes, loading states, icons, and full-width layouts. Renders a native button element wrapped in a focus management provider."
importPath={'import { ManaButton } from "../components";'}
sections={[
{
title: "Variants",
description: "All eight visual styles at the default medium size. The expressive variant includes a Rive animation background on hover.",
children: (
<div className="vc-compfinder-grid">
{ManaButtonVariants.map(variant => (
<ManaButton key={variant} variant={variant} size="md" text={variant} />
))}
</div>
),
code: '<ManaButton variant="primary" size="md" text="Save" onClick={() => save()} />',
relevantProps: ["variant"],
},
{
title: "Sizes",
description: "Each size maps to a specific text variant internally (xs to text-xs/medium, sm to text-sm/medium, md to text-md/medium) and a corresponding icon size.",
children: (
<div className="vc-compfinder-grid">
{ManaButtonSizes.map(size => (
<ManaButton key={size} variant="primary" size={size} text={`Size: ${size}`} />
))}
</div>
),
code: '<ManaButton variant="primary" size="sm" text="Small" />',
relevantProps: ["size"],
},
{
title: "States",
description: "Disabled and loading both suppress all event handlers. Loading shows a spinner overlay with accessibility announcements.",
children: <StatesDemo />,
code: '<ManaButton\n variant="primary"\n text="Save"\n loading={saving}\n loadingStartedLabel="Saving..."\n loadingFinishedLabel="Saved!"\n onClick={handleSave}\n/>',
relevantProps: ["disabled", "loading", "loadingStartedLabel", "loadingFinishedLabel", "rounded", "fullWidth"],
},
{
title: "All Variants Disabled",
description: "How each variant looks in its disabled state.",
children: (
<div className="vc-compfinder-grid">
{ManaButtonVariants.map(variant => (
<ManaButton key={variant} variant={variant} size="md" text={variant} disabled />
))}
</div>
),
},
{
title: "Size \u00d7 Variant Matrix",
description: "Every combination of size and variant for comparison.",
children: (
<>
{ManaButtonSizes.map(size => (
<div key={size} style={{ marginBottom: 8 }}>
<Paragraph color="text-muted" style={{ fontSize: 12, marginBottom: 4 }}>
Size: {size}
</Paragraph>
<div className="vc-compfinder-grid">
{ManaButtonVariants.map(variant => (
<ManaButton key={`${size}-${variant}`} variant={variant} size={size} text={variant} />
))}
</div>
</div>
))}
</>
),
},
]}
props={BUTTON_PROPS}
examples={[
{
label: "Destructive action with critical variant:",
code: '<ManaButton variant="critical-primary" size="sm" text="Delete" onClick={handleDelete} />',
},
]}
/>
);
}

View file

@ -1,194 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaCalendar, ManaDatePicker } from "@equicordplugins/components.dev";
import { useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const CALENDAR_PROPS: PropDef[] = [
{ name: "value", type: "CalendarDate | null", description: "The currently selected date value." },
{ name: "onChange", type: "(value: CalendarDate) => void", description: "Called when a date is selected." },
{ name: "minValue", type: "CalendarDate", description: "Minimum selectable date." },
{ name: "maxValue", type: "CalendarDate", description: "Maximum selectable date." },
{ name: "disabled", type: "boolean", default: "false", description: "Disables the entire calendar." },
{ name: "readOnly", type: "boolean", default: "false", description: "Makes the calendar read-only. Dates are visible but not selectable." },
{ name: "className", type: "string", description: "Additional CSS class on the root element." },
{ name: "aria-label", type: "string", description: "Accessibility label for the calendar." },
];
const DATEPICKER_PROPS: PropDef[] = [
{ name: "value", type: "CalendarDate | CalendarDateTime | null", description: "The currently selected date/time value." },
{ name: "onChange", type: "(value: CalendarDate | CalendarDateTime) => void", description: "Called when the value changes. Also closes the popover." },
{ name: "minValue", type: "CalendarDate", description: "Minimum selectable date." },
{ name: "maxValue", type: "CalendarDate", description: "Maximum selectable date." },
{ name: "placeholderValue", type: "CalendarDate | CalendarDateTime", description: "Placeholder date shown in the input when no value is selected." },
{ name: "granularity", type: '"day" | "hour" | "minute" | "second"', default: '"day"', description: "Time precision level. Day shows only date fields, others add time segments." },
{ name: "hourCycle", type: "12 | 24", description: "Whether to use 12-hour or 24-hour time format." },
{ name: "hideTimeZone", type: "boolean", default: "false", description: "Hides the time zone display when using time granularity." },
{ name: "label", type: "string", description: "Input field label." },
{ name: "hideLabel", type: "boolean", description: "Visually hides the label while keeping it accessible." },
{ name: "description", type: "string", description: "Help text shown below the label." },
{ name: "helperText", type: "string", description: "Additional help text shown below the input." },
{ name: "errorMessage", type: "string", description: "Error text shown below the input. Triggers error styling when non-empty." },
{ name: "successMessage", type: "string", description: "Success text shown below the input." },
{ name: "disabled", type: "boolean", description: "Disables the date picker input and calendar." },
{ name: "required", type: "boolean", default: "false", description: "Marks the field as required." },
{ name: "badge", type: "ReactNode", description: "Badge element displayed next to the label." },
{ name: "icon", type: "ReactNode", description: "Icon displayed in the field." },
{ name: "id", type: "string", description: "Custom ID for the input control." },
{ name: "layout", type: "string", description: "Field layout variant." },
{ name: "layoutConfig", type: "Record<string, unknown>", description: "Configuration for the field layout." },
];
function CalendarDemo() {
const [value, setValue] = useState<any>(null);
return (
<ManaCalendar
value={value}
onChange={setValue}
aria-label="Select a date"
/>
);
}
function DatePickerDemo() {
const [value, setValue] = useState<any>(null);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16, maxWidth: 300 }}>
<ManaDatePicker
value={value}
onChange={setValue}
label="Select Date"
description="Choose a date from the calendar"
granularity="day"
/>
</div>
);
}
function DatePickerTimeDemo() {
const [value, setValue] = useState<any>(null);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16, maxWidth: 300 }}>
<ManaDatePicker
value={value}
onChange={setValue}
label="Select Date & Time"
granularity="minute"
hourCycle={24}
/>
</div>
);
}
function DatePickerStatesDemo() {
const [value, setValue] = useState<any>(null);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16, maxWidth: 300 }}>
<ManaDatePicker
value={value}
onChange={setValue}
label="Disabled"
disabled
/>
<ManaDatePicker
value={value}
onChange={setValue}
label="Required"
required
/>
<ManaDatePicker
value={value}
onChange={setValue}
label="With Error"
errorMessage="Please select a valid date"
/>
<ManaDatePicker
value={value}
onChange={setValue}
label="With Helper Text"
helperText="Pick any date in the future"
/>
</div>
);
}
export default function CalendarTab() {
return (
<DocPage
componentName="Calendar"
overview="ManaCalendar is a standalone calendar grid for date selection. ManaDatePicker wraps it in a text input with a popover, supporting time granularity, validation states, and field layout props. Both use react-aria internationalized date objects internally."
notices={[
{ type: "info", children: "Calendar values use react-aria's CalendarDate and CalendarDateTime types from @internationalized/date, not native JavaScript Date objects. You may need to convert between them." },
]}
importPath={'import { ManaCalendar, ManaDatePicker } from "../components";'}
sections={[
{
title: "Calendar",
description: "Basic calendar grid. Click a date to select it. Navigate months with the arrow buttons.",
code: '<ManaCalendar value={date} onChange={setDate} aria-label="Pick a date" />',
relevantProps: ["value", "onChange", "aria-label"],
children: <CalendarDemo />
},
{
title: "Calendar (Disabled)",
description: "All dates are visible but not interactive.",
relevantProps: ["disabled"],
children: (
<ManaCalendar
value={null}
onChange={() => { }}
disabled
aria-label="Disabled calendar"
/>
)
},
{
title: "Calendar (Read Only)",
description: "Dates are visible with current selection shown but cannot be changed.",
relevantProps: ["readOnly"],
children: (
<ManaCalendar
value={null}
onChange={() => { }}
readOnly
aria-label="Read only calendar"
/>
)
},
{
title: "Date Picker",
description: "Text input with a calendar popover. Click the calendar icon to open.",
code: '<ManaDatePicker\n value={date}\n onChange={setDate}\n label="Event Date"\n description="When should this happen?"\n granularity="day"\n/>',
relevantProps: ["label", "description", "granularity"],
children: <DatePickerDemo />
},
{
title: "Date Picker with Time",
description: "Using minute granularity adds hour and minute segments to the input.",
code: '<ManaDatePicker\n value={date}\n onChange={setDate}\n label="Start Time"\n granularity="minute"\n hourCycle={24}\n required\n errorMessage={!date ? "Required" : ""}\n/>',
relevantProps: ["granularity", "hourCycle"],
children: <DatePickerTimeDemo />
},
{
title: "Date Picker States",
description: "Disabled, required, error, and helper text states.",
relevantProps: ["disabled", "required", "errorMessage", "helperText"],
children: <DatePickerStatesDemo />
},
]}
props={[
...CALENDAR_PROPS.map(p => ({ ...p, name: `[Calendar] ${p.name}` })),
...DATEPICKER_PROPS.map(p => ({ ...p, name: `[DatePicker] ${p.name}` })),
]}
/>
);
}

View file

@ -1,117 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Card, CardVariants, Paragraph } from "..";
import { DocPage, type PropDef } from "../DocPage";
const CARD_PROPS: PropDef[] = [
{ name: "variant", type: '"primary" | "warning" | "danger" | "success" | "brand"', default: '"primary"', description: "Visual style variant controlling background color." },
{ name: "outline", type: "boolean", default: "false", description: "Renders with a border outline instead of filled background." },
{ name: "defaultPadding", type: "boolean", description: "Adds 1em padding to the card. Defaults to true when no className is provided, false otherwise." },
{ name: "children", type: "ReactNode", description: "Card content." },
{ name: "className", type: "string", description: "Additional CSS class. When set, defaultPadding becomes false unless explicitly overridden." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
{ name: "...restProps", type: "HTMLDivAttributes", description: "All other standard div attributes are forwarded to the root element." },
];
export default function CardTab() {
return (
<DocPage
componentName="Card"
overview="Card is a Vencord component that renders a styled container div with variant-based coloring. It supports filled and outline modes, automatic default padding, and all standard div attributes. Useful for grouping content with visual emphasis."
notices={[
{ type: "info", children: "Card is a Vencord component from @components/Card, not a Discord native component. Its API is stable but may differ from Discord's own card patterns." },
]}
importPath={'import { Card } from "@components/Card";'}
sections={[
{
title: "Variants",
description: "Five visual styles for different content contexts.",
code: '<Card variant="primary">\n <Paragraph>Content here</Paragraph>\n</Card>',
relevantProps: ["variant", "children"],
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{CardVariants.map(variant => (
<Card key={variant} variant={variant}>
<Paragraph style={{ fontWeight: 600, marginBottom: 4 }}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</Paragraph>
<Paragraph color="text-muted">
{variant === "primary" ? "General purpose content container." :
variant === "warning" ? "Cautionary or attention-needed messages." :
variant === "danger" ? "Error states or destructive actions." :
variant === "success" ? "Positive feedback or confirmations." :
"Branded or highlighted content."}
</Paragraph>
</Card>
))}
</div>
)
},
{
title: "Outline Mode",
description: "Uses a border instead of a filled background.",
code: '<Card variant="warning" outline>\n <Paragraph>Heads up!</Paragraph>\n</Card>',
relevantProps: ["outline"],
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{CardVariants.map(variant => (
<Card key={variant} variant={variant} outline>
<Paragraph>{variant} outline</Paragraph>
</Card>
))}
</div>
)
},
{
title: "Padding Control",
description: "Cards get 1em padding by default when no className is set. You can override this behavior explicitly.",
code: '<Card variant="brand" className={myClass} defaultPadding>\n <Paragraph>Keeps padding despite custom className</Paragraph>\n</Card>',
relevantProps: ["defaultPadding", "className"],
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<Card defaultPadding>
<Paragraph>defaultPadding=true</Paragraph>
</Card>
<Card defaultPadding={false}>
<Paragraph style={{ padding: 8 }}>defaultPadding=false (content adds own padding)</Paragraph>
</Card>
</div>
)
},
{
title: "Nested Cards",
description: "Cards can nest for complex layouts.",
children: (
<Card variant="primary">
<Paragraph style={{ fontWeight: 600, marginBottom: 8 }}>Outer Card</Paragraph>
<Card variant="warning" style={{ marginBottom: 8 }}>
<Paragraph>Nested warning card</Paragraph>
</Card>
<Card variant="danger">
<Paragraph>Nested danger card</Paragraph>
</Card>
</Card>
)
},
{
title: "Custom Styling",
description: "Cards accept custom styles and all div attributes.",
relevantProps: ["style"],
children: (
<Card
variant="primary"
style={{ borderLeft: "4px solid var(--text-brand)", borderRadius: 4 }}
>
<Paragraph>Card with custom border accent</Paragraph>
</Card>
)
},
]}
props={CARD_PROPS}
/>
);
}

View file

@ -1,133 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CheckboxGroupOption, ManaCheckboxGroup, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
import { EquicordIcon } from "../icons/EquicordIcon";
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" },
];
const GROUP_PROPS: PropDef[] = [
{ name: "options", type: "CheckboxGroupOption[]", required: true, description: "Array of checkbox options to render." },
{ name: "selectedValues", type: "(string | number)[]", required: true, description: "Currently selected option values." },
{ name: "onChange", type: "(values: (string | number)[]) => void", description: "Called with the updated selection array when any checkbox is toggled." },
{ name: "disabled", type: "boolean", default: "false", description: "Disables the entire group. Individual option disabled flags still apply." },
{ name: "label", type: "string", description: "Field label displayed above the group." },
{ name: "hideLabel", type: "boolean", description: "Visually hides the label while keeping it accessible." },
{ name: "badge", type: "ReactNode", description: "Badge element displayed next to the label." },
{ name: "icon", type: "ReactNode", description: "Icon element displayed next to the label." },
{ name: "required", type: "boolean", description: "Marks the field as required." },
{ name: "description", type: "string", description: "Description text displayed below the label." },
{ name: "helperText", type: "string", description: "Helper text displayed below the group." },
{ name: "errorMessage", type: "string", description: "Error message displayed below the group." },
{ name: "successMessage", type: "string", description: "Success message displayed below the group." },
{ name: "id", type: "string", description: "Custom ID for the control element." },
{ name: "layout", type: "string", description: "Field layout variant." },
{ name: "layoutConfig", type: "Record<string, unknown>", description: "Configuration for the field layout." },
];
const OPTION_PROPS: PropDef[] = [
{ name: "value", type: "string | number", required: true, description: "Unique identifier for this option." },
{ name: "label", type: "string", required: true, description: "Label text displayed next to the checkbox." },
{ name: "description", type: "string", description: "Description text below the label." },
{ name: "disabled", type: "boolean", description: "Disables this individual option." },
{ name: "leadingIcon", type: "ComponentType<any>", description: "Icon component displayed before the checkbox." },
];
function BasicDemo() {
const [selected, setSelected] = useState<(string | number)[]>(["option1"]);
return <ManaCheckboxGroup options={BASIC_OPTIONS} selectedValues={selected} onChange={setSelected} />;
}
function DescriptionDemo() {
const [selected, setSelected] = useState<(string | number)[]>(["notifications", "sounds"]);
return <ManaCheckboxGroup options={OPTIONS_WITH_DESCRIPTIONS} selectedValues={selected} onChange={setSelected} />;
}
function IconDemo() {
const [selected, setSelected] = useState<(string | number)[]>(["item1"]);
return <ManaCheckboxGroup options={OPTIONS_WITH_ICONS} selectedValues={selected} onChange={setSelected} />;
}
function MixedDemo() {
const [selected, setSelected] = useState<(string | number)[]>(["enabled"]);
return <ManaCheckboxGroup options={OPTIONS_MIXED} selectedValues={selected} onChange={setSelected} />;
}
export default function CheckboxGroupTab() {
return (
<DocPage
componentName="ManaCheckboxGroup"
overview="ManaCheckboxGroup renders a group of checkboxes from an options array with multi-select state management. Each option supports labels, descriptions, icons, and individual disabled state. The group can also be disabled entirely."
notices={[
{ type: "info", children: "CheckboxGroup manages multi-select state internally by delegating to individual ManaCheckbox instances. Use this instead of manually rendering multiple checkboxes when you need shared state tracking." },
]}
importPath={'import { ManaCheckboxGroup } from "../components";'}
sections={[
{
title: "Basic",
description: "Simple group with three options.",
children: <BasicDemo />,
code: '<ManaCheckboxGroup\n options={[\n { value: "a", label: "Option A" },\n { value: "b", label: "Option B" },\n ]}\n selectedValues={selected}\n onChange={setSelected}\n/>',
relevantProps: ["options", "selectedValues", "onChange"],
},
{
title: "With Descriptions",
description: "Each option can have a description below its label.",
children: <DescriptionDemo />,
},
{
title: "With Leading Icons",
description: "Options can display an icon component before the checkbox.",
children: <IconDemo />,
},
{
title: "With Disabled Option",
description: "Individual options can be disabled while others remain interactive.",
children: <MixedDemo />,
},
{
title: "Entire Group Disabled",
description: "The disabled prop on the group disables all options.",
children: (
<ManaCheckboxGroup
options={BASIC_OPTIONS}
selectedValues={["option1", "option2"]}
disabled
/>
),
relevantProps: ["disabled"],
},
]}
props={[
{ title: "ManaCheckboxGroup", props: GROUP_PROPS },
{ title: "CheckboxGroupOption", props: OPTION_PROPS },
]}
/>
);
}

View file

@ -1,147 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaCheckbox, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
import { EquicordIcon } from "../icons/EquicordIcon";
const CHECKBOX_PROPS: PropDef[] = [
{ name: "checked", type: "boolean", required: true, description: "Current checked state." },
{ name: "onChange", type: "(checked: boolean) => void", description: "Called when the checkbox is toggled." },
{ name: "label", type: "string", description: "Label text displayed next to the checkbox." },
{ name: "description", type: "string", description: "Description text below the label." },
{ name: "labelType", type: '"primary" | "secondary"', default: '"primary"', description: "Label text styling. Primary uses normal weight, secondary uses muted color." },
{ name: "leadingIcon", type: "ComponentType<any>", description: "Icon component displayed before the checkbox." },
{ name: "usageVariant", type: '"single" | "indicator"', default: '"single"', internal: true, description: "Visual style. Single has full padding and spacing, indicator is compact." },
{ name: "disabled", type: "boolean", default: "false", description: "Disables the checkbox interaction." },
{ name: "displayOnly", type: "boolean", internal: true, description: "Renders the checkbox in a display-only state without interaction." },
{ name: "value", type: "string | number", description: "Value identifier used when inside a CheckboxGroup." },
{ name: "labeledBy", type: "string", internal: true, description: "ID of an external element that labels this checkbox for accessibility." },
];
function BasicDemo() {
const [checked, setChecked] = useState(true);
const [unchecked, setUnchecked] = useState(false);
return (
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox label="Checked" checked={checked} onChange={setChecked} />
<ManaCheckbox label="Unchecked" checked={unchecked} onChange={setUnchecked} />
</div>
);
}
function DescriptionDemo() {
const [checked, setChecked] = useState(true);
return (
<ManaCheckbox
label="Option with description"
description="This is a helpful description for the option."
checked={checked}
onChange={setChecked}
/>
);
}
function LabelTypesDemo() {
const [primary, setPrimary] = useState(true);
const [secondary, setSecondary] = useState(false);
return (
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox label="Primary label (default)" labelType="primary" checked={primary} onChange={setPrimary} />
<ManaCheckbox label="Secondary label" labelType="secondary" checked={secondary} onChange={setSecondary} />
</div>
);
}
function IconDemo() {
const [checked, setChecked] = useState(true);
return (
<ManaCheckbox
label="With icon"
description="This checkbox has a leading icon."
leadingIcon={EquicordIcon}
checked={checked}
onChange={setChecked}
/>
);
}
function UsageVariantsDemo() {
const [single, setSingle] = useState(true);
const [indicator, setIndicator] = useState(false);
return (
<div className="vc-compfinder-grid-vertical">
<ManaCheckbox label="Single (default)" usageVariant="single" checked={single} onChange={setSingle} />
<ManaCheckbox label="Indicator" usageVariant="indicator" checked={indicator} onChange={setIndicator} />
</div>
);
}
export default function CheckboxTab() {
return (
<DocPage
componentName="ManaCheckbox"
overview="ManaCheckbox is Discord's checkbox component with label, description, icon support, and two visual variants. Supports controlled state, disabled and display-only modes, and integrates with CheckboxGroup for multi-select patterns."
notices={[
{ type: "info", children: "ManaCheckbox is a controlled component. The checked prop must always be provided, and you must update it via onChange to reflect the new state." },
]}
importPath={'import { ManaCheckbox } from "../components";'}
sections={[
{
title: "Basic",
description: "Toggle checkboxes on and off.",
children: <BasicDemo />,
code: '<ManaCheckbox label="Enable feature" checked={enabled} onChange={setEnabled} />',
relevantProps: ["checked", "onChange", "label"],
},
{
title: "With Description",
description: "A description renders below the label in smaller text.",
children: <DescriptionDemo />,
code: '<ManaCheckbox\n label="Notifications"\n description="Receive email notifications for new messages."\n checked={checked}\n onChange={setChecked}\n/>',
relevantProps: ["description"],
},
{
title: "Label Types",
description: "Primary uses normal text styling, secondary uses a muted, lighter appearance.",
children: <LabelTypesDemo />,
relevantProps: ["labelType"],
},
{
title: "With Leading Icon",
description: "An icon component displayed before the checkbox indicator.",
children: <IconDemo />,
code: '<ManaCheckbox\n label="With icon"\n leadingIcon={BellIcon}\n checked={checked}\n onChange={setChecked}\n/>',
relevantProps: ["leadingIcon"],
},
{
title: "States",
description: "Disabled prevents interaction. Display only renders without any interactive behavior.",
children: (
<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>
),
relevantProps: ["disabled", "displayOnly"],
},
{
title: "Usage Variants",
description: "Single has full padding and spacing for standalone use. Indicator is compact for inline contexts.",
children: <UsageVariantsDemo />,
relevantProps: ["usageVariant"],
},
]}
props={CHECKBOX_PROPS}
/>
);
}

View file

@ -1,71 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Chip } from "@equicordplugins/components.dev";
import { Paragraph } from "..";
import { DocPage, type PropDef } from "../DocPage";
const CHIP_COLORS = ["blurple", "purple", "green", "orange", "yellow", "pink", "red", "gray"] as const;
const CHIP_SHADES = ["Light", "Medium", "Dark"] as const;
const CHIP_PROPS: PropDef[] = [
{ name: "text", type: "string", required: true, description: "Text displayed inside the chip." },
{ name: "variant", type: "ChipVariant", default: '"grayLight"', description: "Color variant in the format {color}{shade}. 8 colors (blurple, purple, green, orange, yellow, pink, red, gray) times 3 shades (Light, Medium, Dark) for 24 total variants." },
];
export default function ChipTab() {
return (
<DocPage
componentName="Chip"
overview="Chip is a small label component used for tags, statuses, and categories. It renders text in an eyebrow style with a colored background. Supports 24 variants across 8 colors and 3 shade levels."
importPath={'import { Chip } from "../components";'}
sections={[
{
title: "All Variants",
description: "All 24 chip variants: 8 colors times 3 shades.",
children: (
<>
{CHIP_SHADES.map(shade => (
<div key={shade} style={{ marginBottom: 16 }}>
<Paragraph color="text-muted" style={{ marginBottom: 8, fontWeight: 600 }}>
{shade}
</Paragraph>
<div className="vc-compfinder-grid">
{CHIP_COLORS.map(color => (
<Chip
key={`${color}${shade}`}
text={color}
variant={`${color}${shade}` as any}
/>
))}
</div>
</div>
))}
</>
),
code: '<Chip text="New" variant="blurpleLight" />',
relevantProps: ["variant"],
},
{
title: "Use Cases",
description: "Common patterns for status labels and tags.",
children: (
<div className="vc-compfinder-grid">
<Chip text="New" variant="blurpleLight" />
<Chip text="Beta" variant="yellowMedium" />
<Chip text="Deprecated" variant="redLight" />
<Chip text="Stable" variant="greenLight" />
<Chip text="Premium" variant="pinkMedium" />
<Chip text="Experimental" variant="orangeLight" />
</div>
),
},
]}
props={CHIP_PROPS}
/>
);
}

View file

@ -1,141 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Clickable, Paragraph, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const CLICKABLE_PROPS: PropDef[] = [
{ name: "tag", type: "keyof JSX.IntrinsicElements", default: '"div"', description: "HTML element to render as." },
{ name: "onClick", type: "MouseEventHandler", description: "Click handler. When absent, renders as a non-interactive element without keyboard handling." },
{ name: "onKeyPress", type: "KeyboardEventHandler", description: "Key press handler called after internal Enter/Space handling." },
{ name: "role", type: "string", default: '"button"', description: "ARIA role attribute." },
{ name: "tabIndex", type: "number", default: "0", description: "Tab index for keyboard navigation." },
{ name: "ignoreKeyPress", type: "boolean", description: "When true, Enter and Space keys will not trigger onClick." },
{ name: "focusProps", type: "Record<string, unknown>", internal: true, description: "Props forwarded to the focus management wrapper." },
{ name: "innerRef", type: "Ref<HTMLElement>", internal: true, description: "Ref to the underlying DOM element." },
{ name: "href", type: "string", description: "Optional href. Affects key handling behavior (Space won't preventDefault when set)." },
{ name: "className", type: "string", description: "Additional CSS class name." },
{ name: "children", type: "ReactNode", description: "Child elements." },
];
const boxStyle: React.CSSProperties = {
padding: "8px 16px",
backgroundColor: "var(--background-secondary)",
borderRadius: 4,
cursor: "pointer",
};
function BasicDemo() {
const [count, setCount] = useState(0);
return (
<>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Click count: {count}</Paragraph>
<Clickable onClick={() => setCount(c => c + 1)} style={boxStyle}>
Click me!
</Clickable>
</>
);
}
function TagDemo() {
const [last, setLast] = useState("None");
return (
<>
<div style={{ display: "flex", gap: 8 }}>
<Clickable tag="div" onClick={() => setLast("div")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)" }}>
tag="div"
</Clickable>
<Clickable tag="span" onClick={() => setLast("span")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)" }}>
tag="span"
</Clickable>
<Clickable tag="button" onClick={() => setLast("button")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)", border: "none", color: "inherit" }}>
tag="button"
</Clickable>
</div>
<Paragraph color="text-muted" style={{ marginTop: 8 }}>Last clicked: {last}</Paragraph>
</>
);
}
function KeyPressDemo() {
const [last, setLast] = useState("None");
return (
<>
<div style={{ display: "flex", gap: 8 }}>
<Clickable tabIndex={0} onClick={() => setLast("Normal (keys work)")} style={boxStyle}>
Normal (try Enter/Space)
</Clickable>
<Clickable tabIndex={0} ignoreKeyPress onClick={() => setLast("Keys blocked")} style={boxStyle}>
ignoreKeyPress (keys blocked)
</Clickable>
</div>
<Paragraph color="text-muted" style={{ marginTop: 8 }}>Last action: {last}</Paragraph>
</>
);
}
export default function ClickableTab() {
return (
<DocPage
componentName="Clickable"
overview="Clickable is a generic interactive wrapper that adds click and keyboard handling to any HTML element. It handles Enter/Space key presses, integrates with Discord's focus management context, and gracefully degrades to a non-interactive element when no onClick is provided. It's a class component with configurable tag, role, and tabIndex defaults."
notices={[
{ type: "positive", children: 'Use Clickable instead of adding onClick to plain divs. It handles keyboard accessibility, focus management, and proper ARIA role="button" automatically.' },
]}
importPath={'import { Clickable } from "../components";'}
sections={[
{
title: "Basic",
description: "Default clickable element. Renders as a div with role=\"button\" and tabIndex=0.",
children: <BasicDemo />,
code: "<Clickable onClick={() => doSomething()} style={myStyles}>\n Click me!\n</Clickable>",
relevantProps: ["onClick", "children"],
},
{
title: "Custom Tag",
description: "Clickable can render as any HTML element using the tag prop.",
children: <TagDemo />,
code: '<Clickable tag="button" onClick={handleClick}>\n I am a button element\n</Clickable>',
relevantProps: ["tag"],
},
{
title: "Role and TabIndex",
description: "Accessibility props for keyboard navigation. Defaults to role=\"button\" and tabIndex=0.",
children: (
<div style={{ display: "flex", gap: 8 }}>
<Clickable role="button" tabIndex={0} onClick={() => { }} style={boxStyle}>
role="button"
</Clickable>
<Clickable role="link" tabIndex={0} onClick={() => { }} style={boxStyle}>
role="link"
</Clickable>
</div>
),
relevantProps: ["role", "tabIndex"],
},
{
title: "Ignore Key Press",
description: "When ignoreKeyPress is true, Enter and Space will not trigger onClick. Only mouse clicks work.",
children: <KeyPressDemo />,
relevantProps: ["ignoreKeyPress"],
},
{
title: "Non-Interactive",
description: "When no onClick is provided, renders without keyboard handling or interactive behavior.",
children: (
<Clickable style={{ padding: "8px 16px", backgroundColor: "var(--background-modifier-accent)", borderRadius: 4 }}>
Non-interactive (no onClick)
</Clickable>
),
},
]}
props={CLICKABLE_PROPS}
/>
);
}

View file

@ -1,122 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CodeBlock } from "@components/CodeBlock";
import { InlineCode, Paragraph } from "..";
import { DocPage, type PropDef } from "../DocPage";
const CODEBLOCK_PROPS: PropDef[] = [
{ name: "content", type: "string", description: "The code text to render inside the block." },
{ name: "lang", type: "string", required: true, description: "Language identifier for syntax highlighting (e.g. \"tsx\", \"js\", \"css\", \"py\"). Use empty string for no highlighting." },
];
const INLINECODE_PROPS: PropDef[] = [
{ name: "children", type: "ReactNode", required: true, description: "Content to render as inline code." },
];
const JS_EXAMPLE = `function greet(name) {
return \`Hello, \${name}!\`;
}
console.log(greet("World"));`;
const TS_EXAMPLE = `interface User {
id: string;
name: string;
email?: string;
}
const getUser = (id: string): User => {
return { id, name: "John" };
};`;
const CSS_EXAMPLE = `.container {
display: flex;
gap: 8px;
padding: 16px;
background: var(--background-secondary);
border-radius: 8px;
}`;
const PYTHON_EXAMPLE = `def fibonacci(n: int) -> list[int]:
if n <= 0:
return []
sequence = [0, 1]
while len(sequence) < n:
sequence.append(sequence[-1] + sequence[-2])
return sequence[:n]
print(fibonacci(10))`;
const JSON_EXAMPLE = `{
"name": "equicord",
"version": "1.0.0",
"plugins": ["example"],
"settings": {
"enabled": true,
"theme": "dark"
}
}`;
export default function CodeBlockTab() {
return (
<DocPage
componentName="CodeBlock"
overview="CodeBlock renders syntax-highlighted code using Discord's built-in markdown parser. InlineCode renders small code snippets inline within text. Both are Vencord components wrapping Discord's markup system."
importPath={'import { CodeBlock } from "@components/CodeBlock";\nimport { InlineCode } from "../components";'}
sections={[
{
title: "Inline Code",
description: "Small code snippets rendered inline within text.",
children: (
<>
<Paragraph>I really like <InlineCode>cats</InlineCode> so yeah</Paragraph>
<Paragraph>Use <InlineCode>console.log()</InlineCode> to debug your code.</Paragraph>
<Paragraph>
Combine <InlineCode>const</InlineCode> with <InlineCode>let</InlineCode> and
use <InlineCode>===</InlineCode> for strict equality.
</Paragraph>
</>
),
code: "<Paragraph>Use <InlineCode>console.log()</InlineCode> to debug.</Paragraph>",
},
{
title: "JavaScript",
children: <CodeBlock content={JS_EXAMPLE} lang="js" />,
code: '<CodeBlock content={jsCode} lang="js" />',
relevantProps: ["content", "lang"],
},
{
title: "TypeScript",
children: <CodeBlock content={TS_EXAMPLE} lang="ts" />,
},
{
title: "CSS",
children: <CodeBlock content={CSS_EXAMPLE} lang="css" />,
},
{
title: "Python",
children: <CodeBlock content={PYTHON_EXAMPLE} lang="py" />,
},
{
title: "JSON",
children: <CodeBlock content={JSON_EXAMPLE} lang="json" />,
},
{
title: "Plain Text",
description: "Use an empty string for lang to render without syntax highlighting.",
children: <CodeBlock content={"This is plain text\nwithout any syntax highlighting."} lang="" />,
code: '<CodeBlock content="plain text" lang="" />',
},
]}
props={[
{ title: "CodeBlock", props: CODEBLOCK_PROPS },
{ title: "InlineCode", props: INLINECODE_PROPS },
]}
/>
);
}

View file

@ -1,243 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ColorPicker } from "@webpack/common";
import { ColorPickerWithSwatches, ColorSwatch, DefaultColorButton, Paragraph, PresetColors, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const DEFAULT_COLOR = 0x5865F2;
function intToHex(color: number): string {
return "#" + color.toString(16).padStart(6, "0");
}
const PICKER_PROPS: PropDef[] = [
{ name: "color", type: "number", required: true, description: "Current color as an integer (e.g. 0x5865F2)." },
{ name: "onChange", type: "(color: number) => void", required: true, description: "Called with the new color integer when changed." },
{ name: "onClose", type: "() => void", description: "Called when the picker popout closes." },
{ name: "suggestedColors", type: "string[]", description: "Array of hex color strings shown as suggestions in the popout." },
{ name: "disabled", type: "boolean", description: "Disables the color picker." },
{ name: "label", type: "ReactNode", description: "Optional label displayed next to the swatch." },
{ name: "colorPickerMiddle", type: "ReactNode", internal: true, description: "Content injected in the middle of the popout." },
{ name: "colorPickerFooter", type: "ReactNode", internal: true, description: "Content injected in the footer of the popout." },
{ name: "showEyeDropper", type: "boolean", description: "Shows the eye dropper tool in the popout." },
];
const SWATCH_PROPS: PropDef[] = [
{ name: "color", type: "number", description: "Color value. Omit for a \"no color\" state." },
{ name: "isDefault", type: "boolean", description: "Renders in the larger default button style." },
{ name: "isCustom", type: "boolean", description: "Shows a dropper icon overlay." },
{ name: "isSelected", type: "boolean", description: "Shows a checkmark overlay." },
{ name: "disabled", type: "boolean", description: "Disables the swatch." },
{ name: "onClick", type: "(color) => void", description: "Click handler receiving the color or gradient object." },
{ name: "isGradient", type: "boolean", description: "Enables gradient rendering mode." },
{ name: "gradientStart", type: "number", description: "Start color for gradients." },
{ name: "gradientEnd", type: "number", description: "End color for gradients." },
{ name: "gradientDegrees", type: "number", default: "180", description: "Angle in degrees for the gradient." },
{ name: "aria-label", type: "string", description: "Accessibility label." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
];
const SWATCHES_PROPS: PropDef[] = [
{ name: "defaultColor", type: "number", required: true, description: "Default/reset color value." },
{ name: "colors", type: "number[] | GradientColor[]", required: true, description: "Array of preset color values or gradient definitions." },
{ name: "value", type: "number", required: true, description: "Currently selected color." },
{ name: "onChange", type: "(color: number) => void", required: true, description: "Called when a color is selected." },
{ name: "customColor", type: "number", description: "Current custom color for the custom button." },
{ name: "secondaryValue", type: "number", description: "Secondary color value for gradient mode." },
{ name: "disabled", type: "boolean", description: "Disables all swatches." },
{ name: "renderDefaultButton", type: "(props) => ReactNode", description: "Render prop for the default/reset button." },
{ name: "renderCustomButton", type: "(props) => ReactNode", description: "Render prop for the custom color button." },
{ name: "isGradient", type: "boolean", description: "Enables gradient mode for all swatches." },
{ name: "gradientDegrees", type: "number", description: "Gradient angle for all swatches." },
{ name: "allowBlackCustomColor", type: "boolean", description: "Allows selecting pure black (#000000)." },
{ name: "className", type: "string", description: "Additional CSS class on the root." },
{ name: "colorContainerClassName", type: "string", description: "CSS class on the swatch grid container." },
];
function BasicPickerDemo() {
const [color, setColor] = useState<number | null>(DEFAULT_COLOR);
return (
<>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Selected: {color !== null ? intToHex(color) : DEFAULT_COLOR}</Paragraph>
<ColorPicker color={color} onChange={setColor} showEyeDropper={false} />
</>
);
}
function EyeDropperDemo() {
const [color, setColor] = useState<number | null>(DEFAULT_COLOR);
return <ColorPicker color={color} onChange={setColor} showEyeDropper />;
}
function SuggestedDemo() {
const [color, setColor] = useState<number | null>(0xE74C3C);
return (
<>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Selected: {color !== null ? intToHex(color) : 0xE74C3C}</Paragraph> <ColorPicker
color={color}
onChange={setColor}
showEyeDropper={false}
suggestedColors={["#1ABC9C", "#2ECC71", "#3498DB", "#9B59B6", "#E91E63", "#F1C40F", "#E67E22", "#E74C3C"]}
/>
</>
);
}
function SwatchSelectDemo() {
const [selected, setSelected] = useState(0x2ECC71);
return (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{PresetColors.slice(0, 10).map(color => (
<ColorSwatch key={color} color={color} isSelected={selected === color} onClick={() => setSelected(color)} />
))}
</div>
);
}
function SwatchesGridDemo() {
const [color, setColor] = useState(0x3498DB);
return (
<>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Selected: {intToHex(color)}</Paragraph>
<ColorPickerWithSwatches
defaultColor={DEFAULT_COLOR}
colors={[...PresetColors]}
value={color}
onChange={setColor}
renderDefaultButton={() => null}
renderCustomButton={() => null}
/>
</>
);
}
function FullPickerDemo() {
const [color, setColor] = useState<number | null>(0x9B59B6);
return (
<>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Selected: {color !== null ? intToHex(color) : 0x9B59B6}</Paragraph>
<ColorPickerWithSwatches
defaultColor={DEFAULT_COLOR}
customColor={color !== null ? color : 0x9B59B6}
colors={[...PresetColors]}
value={color !== null ? color : 0x9B59B6}
onChange={setColor}
renderDefaultButton={props => <DefaultColorButton {...props} />}
renderCustomButton={() => (
<ColorPicker color={color} onChange={setColor} showEyeDropper={false} />
)}
/>
</>
);
}
export default function ColorPickerTab() {
return (
<DocPage
componentName="ColorPicker"
overview="Discord's color picker system includes a full-featured ColorPicker popout, individual ColorSwatch buttons, and a ColorPickerWithSwatches grid. Colors are represented as integers (e.g. 0x5865F2). Convert with parseInt(hex, 16) and color.toString(16)."
notices={[
{ type: "info", children: 'Colors are represented as integers, not hex strings. Convert with parseInt(hex, 16) to get an integer and color.toString(16).padStart(6, "0") to get a hex string.' },
]}
importPath={'import { ColorPicker, ColorPickerWithSwatches, ColorSwatch, DefaultColorButton, CustomColorButton } from "../components";'}
sections={[
{
title: "Basic ColorPicker",
description: "Click the swatch to open a full color picker popout.",
children: <BasicPickerDemo />,
code: "<ColorPicker color={color} onChange={setColor} showEyeDropper={false} />",
relevantProps: ["color", "onChange"],
},
{
title: "With Eye Dropper",
description: "Includes the eye dropper tool for picking colors from screen.",
children: <EyeDropperDemo />,
relevantProps: ["showEyeDropper"],
},
{
title: "With Suggested Colors",
description: "Shows suggested color swatches inside the popout.",
children: <SuggestedDemo />,
code: '<ColorPicker\n color={color}\n onChange={setColor}\n suggestedColors={["#1ABC9C", "#2ECC71", "#3498DB"]}\n/>',
relevantProps: ["suggestedColors"],
},
{
title: "Disabled",
children: <ColorPicker color={0x5865F2} onChange={() => { }} disabled />,
relevantProps: ["disabled"],
},
{
title: "Color Swatches",
description: "Individual swatch buttons. Click to select.",
children: <SwatchSelectDemo />,
code: "<ColorSwatch color={0x5865F2} isSelected={selected === 0x5865F2} onClick={() => setSelected(0x5865F2)} />",
},
{
title: "Swatch States",
description: "Normal, selected, default, custom, disabled, and no-color states.",
children: (
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
{([
["Normal", {}],
["Selected", { isSelected: true }],
["Default", { isDefault: true }],
["Custom", { isCustom: true }],
["Disabled", { disabled: true }],
] as const).map(([label, props]) => (
<div key={label} style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} {...props} />
<Paragraph color="text-muted" style={{ fontSize: 10 }}>{label}</Paragraph>
</div>
))}
<div style={{ textAlign: "center" }}>
<ColorSwatch />
<Paragraph color="text-muted" style={{ fontSize: 10 }}>No Color</Paragraph>
</div>
</div>
),
relevantProps: ["isSelected", "isDefault", "isCustom", "disabled"],
},
{
title: "Gradient Swatches",
description: "Gradient swatches with customizable angle.",
children: (
<div style={{ display: "flex", gap: 8 }}>
<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>
),
relevantProps: ["isGradient", "gradientStart", "gradientEnd", "gradientDegrees"],
},
{
title: "Swatch Grid",
description: "ColorPickerWithSwatches renders a grid of preset color swatches.",
children: <SwatchesGridDemo />,
code: "<ColorPickerWithSwatches\n defaultColor={DEFAULT_COLOR}\n colors={presetColors}\n value={color}\n onChange={setColor}\n renderDefaultButton={() => null}\n renderCustomButton={() => null}\n/>",
},
{
title: "Full Picker with Buttons",
description: "Swatch grid with default reset button and custom color picker using render props.",
children: <FullPickerDemo />,
relevantProps: ["renderDefaultButton", "renderCustomButton"],
},
]}
props={[
{ title: "ColorPicker", props: PICKER_PROPS },
{ title: "ColorSwatch", props: SWATCH_PROPS },
{ title: "ColorPickerWithSwatches", props: SWATCHES_PROPS },
]}
/>
);
}

View file

@ -1,202 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaCombobox } from "@equicordplugins/components.dev";
import { Paragraph, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const COMBOBOX_PROPS: PropDef[] = [
{ name: "placeholder", type: "string", description: "Placeholder text for the search input." },
{ name: "value", type: "T | T[]", description: "Currently selected value or array of values for multi-select." },
{ name: "onChange", type: "(value: T | T[]) => void", description: "Called when the selection changes." },
{ name: "multiSelect", type: "boolean", default: "false", description: "Enable multiple selection mode." },
{ name: "autoFocus", type: "boolean", default: "false", description: "Focus the search input on mount." },
{ name: "maxVisibleItems", type: "number", default: "5", description: "Maximum items visible before scrolling." },
{ name: "itemToString", type: "(item: T) => string", description: "Converts an item to a string for display." },
{ name: "emptyStateHeader", type: "string", description: "Header text shown when no results match the query." },
{ name: "emptyStateText", type: "string", description: "Body text shown when no results match the query." },
{ name: "onQueryChange", type: "(query: string) => void", description: "Called when the search query changes. Useful for external filtering." },
{ name: "aria-label", type: "string", description: "Accessibility label for the combobox." },
{ name: "className", type: "string", description: "CSS class for the combobox container." },
{ name: "listClassName", type: "string", description: "CSS class for the dropdown list." },
{ name: "children", type: "(query: string) => ReactNode[]", required: true, description: "Render function receiving the search query. Return filterable items." },
];
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>
);
}
function filterItems(query: string) {
return SAMPLE_ITEMS.filter(item =>
item.label.toLowerCase().includes(query.toLowerCase())
);
}
function SingleSelectDemo() {
const [value, setValue] = useState<string>("");
return (
<div style={{ maxWidth: 300 }}>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Selected: {SAMPLE_ITEMS.find(i => i.id === value)?.label ?? "None"}
</Paragraph>
<ManaCombobox
placeholder="Search fruits..."
value={value}
onChange={v => setValue(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={value === item.id}
onClick={() => setValue(item.id)}
/>
))}
</ManaCombobox>
</div>
);
}
function MultiSelectDemo() {
const [values, setValues] = useState<string[]>([]);
return (
<div style={{ maxWidth: 300 }}>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Selected: {values.length ? values.map(v => SAMPLE_ITEMS.find(i => i.id === v)?.label).join(", ") : "None"}
</Paragraph>
<ManaCombobox
placeholder="Search fruits..."
value={values}
onChange={v => setValues(v as string[])}
multiSelect
aria-label="Select fruits"
maxVisibleItems={5}
>
{query => filterItems(query).map(item => (
<ComboboxItem
key={item.id}
item={item}
isSelected={values.includes(item.id)}
onClick={() => {
if (values.includes(item.id)) {
setValues(values.filter(v => v !== item.id));
} else {
setValues([...values, item.id]);
}
}}
/>
))}
</ManaCombobox>
</div>
);
}
function EmptyStateDemo() {
return (
<div style={{ maxWidth: 300 }}>
<ManaCombobox
placeholder="Type something that won't match..."
value=""
onChange={() => { }}
aria-label="Empty state demo"
emptyStateHeader="Nothing found"
emptyStateText="We couldn't find what you're looking for."
>
{() => []}
</ManaCombobox>
</div>
);
}
export default function ComboboxTab() {
return (
<DocPage
componentName="ManaCombobox"
overview="ManaCombobox is Discord's searchable dropdown component using a render props pattern. Children is a function that receives the search query and returns filterable items. Supports single and multi-select modes, custom empty states, and external query tracking."
notices={[
{ type: "warn", children: "ManaCombobox uses a render props pattern where children is a function. You are responsible for filtering items based on the query parameter. The component does not filter automatically." },
]}
importPath={'import { ManaCombobox } from "../components";'}
sections={[
{
title: "Single Selection",
description: "Basic combobox with single item selection. Type to filter the list.",
children: <SingleSelectDemo />,
code: `<ManaCombobox
placeholder="Search fruits..."
value={value}
onChange={v => setValue(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} />
))}
</ManaCombobox>`,
relevantProps: ["value", "onChange", "placeholder", "maxVisibleItems"],
},
{
title: "Multi Selection",
description: "Enable multiSelect to allow choosing multiple items at once.",
children: <MultiSelectDemo />,
code: `<ManaCombobox
placeholder="Search fruits..."
value={values}
onChange={v => setValues(v as string[])}
multiSelect
aria-label="Select fruits"
>
{query => filterItems(query).map(item => (
<ComboboxItem key={item.id} item={item} />
))}
</ManaCombobox>`,
relevantProps: ["multiSelect"],
},
{
title: "Empty State",
description: "Custom header and text shown when the children function returns an empty array.",
children: <EmptyStateDemo />,
relevantProps: ["emptyStateHeader", "emptyStateText"],
},
]}
props={COMBOBOX_PROPS}
/>
);
}

View file

@ -1,56 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Divider, Paragraph } from "..";
import { DocPage, type PropDef } from "../DocPage";
const DIVIDER_PROPS: PropDef[] = [
{ name: "className", type: "string", description: "Custom CSS class for the divider." },
{ name: "style", type: "CSSProperties", description: "Inline styles applied to the hr element." },
{ name: "...restProps", type: "HTMLAttributes<hr>", description: "Any other standard hr element attributes." },
];
export default function DividerTab() {
return (
<DocPage
componentName="Divider"
overview="Divider is a simple horizontal rule component from Vencord's @components/Divider. It renders an hr element with the vc-divider class and accepts standard HTML attributes."
importPath={'import { Divider } from "../components";'}
sections={[
{
title: "Basic Divider",
description: "A simple horizontal rule separating content.",
children: (
<div style={{ padding: "8px 0" }}>
<Paragraph>Content above divider</Paragraph>
<Divider />
<Paragraph>Content below divider</Paragraph>
</div>
),
code: "<Divider />",
},
{
title: "Custom Spacing",
description: "Control the spacing around dividers using inline styles.",
children: (
<div style={{ padding: "8px 0" }}>
<Paragraph>Tight spacing (8px)</Paragraph>
<Divider style={{ margin: "8px 0" }} />
<Paragraph>Medium spacing (16px)</Paragraph>
<Divider style={{ margin: "16px 0" }} />
<Paragraph>Large spacing (24px)</Paragraph>
<Divider style={{ margin: "24px 0" }} />
<Paragraph>End</Paragraph>
</div>
),
code: '<Divider style={{ margin: "16px 0" }} />',
relevantProps: ["style"],
},
]}
props={DIVIDER_PROPS}
/>
);
}

View file

@ -1,169 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { SimpleErrorBoundary } from "@equicordplugins/components.dev";
import { Button, ErrorBoundary, LocalErrorBoundary, Paragraph, useState } from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
function ThrowError(): never {
throw new Error("This is a demo error thrown intentionally!");
}
function ErrorTrigger({ label }: { label: string; }) {
const [shouldThrow, setShouldThrow] = useState(false);
if (shouldThrow) {
return <ThrowError />;
}
return (
<Button variant="dangerPrimary" onClick={() => setShouldThrow(true)}>
{label}
</Button>
);
}
function CustomFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void; }) {
return (
<div style={{ padding: 16, background: "var(--background-secondary)", borderRadius: 8 }}>
<Paragraph color="text-danger" style={{ marginBottom: 8 }}>
<strong>Custom Fallback UI</strong>
</Paragraph>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Error: {error.message}
</Paragraph>
<Button onClick={resetErrorBoundary}>Reset</Button>
</div>
);
}
const ERROR_BOUNDARY_PROPS: PropDef[] = [
{ name: "children", type: "ReactNode", required: true, description: "Content to wrap with error handling." },
{ name: "fallback", type: "ReactNode", description: "Static fallback content to show on error." },
{ name: "fallbackRender", type: "(props: { error, resetErrorBoundary }) => ReactNode", description: "Render function for dynamic fallback with error details and reset capability." },
{ name: "FallbackComponent", type: "ComponentType<{ error, resetErrorBoundary }>", description: "Component that receives error and reset props as fallback." },
{ name: "onError", type: "(error: Error, info: ErrorInfo) => void", description: "Callback fired when an error is caught." },
{ name: "onReset", type: "(details) => void", description: "Callback fired when the boundary is reset." },
{ name: "resetKeys", type: "any[]", description: "Array of values that trigger an automatic reset when changed." },
];
const SIMPLE_PROPS: PropDef[] = [
{ name: "children", type: "ReactNode", required: true, description: "Content to wrap with error handling." },
{ name: "fallback", type: "ReactNode", required: true, description: "Static content to display when an error occurs." },
];
const LOCAL_PROPS: PropDef[] = [
{ name: "children", type: "ReactNode", required: true, description: "Content to wrap. Shows inline error text on failure." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "ErrorBoundary", props: ERROR_BOUNDARY_PROPS },
{ title: "SimpleErrorBoundary", props: SIMPLE_PROPS },
{ title: "LocalErrorBoundary", props: LOCAL_PROPS },
];
function ErrorBoundaryDemo() {
const [key, setKey] = useState(0);
return (
<div key={key}>
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<CustomFallback error={error} resetErrorBoundary={resetErrorBoundary} />
)}
onReset={() => setKey(k => k + 1)}
>
<ErrorTrigger label="Trigger Error" />
</ErrorBoundary>
</div>
);
}
function SimpleDemo() {
const [key, setKey] = useState(0);
return (
<div key={key}>
<SimpleErrorBoundary
fallback={
<div style={{ padding: 16, background: "var(--background-secondary)", borderRadius: 8 }}>
<Paragraph color="text-danger">Something went wrong!</Paragraph>
<Button onClick={() => setKey(k => k + 1)} style={{ marginTop: 8 }}>Reset</Button>
</div>
}
>
<ErrorTrigger label="Trigger Error" />
</SimpleErrorBoundary>
</div>
);
}
function LocalDemo() {
const [key, setKey] = useState(0);
return (
<div key={key}>
<LocalErrorBoundary>
<ErrorTrigger label="Trigger Error" />
</LocalErrorBoundary>
<Button variant="secondary" onClick={() => setKey(k => k + 1)} style={{ marginTop: 8 }}>
Reset
</Button>
</div>
);
}
export default function ErrorBoundaryTab() {
return (
<DocPage
componentName="ErrorBoundary"
overview="Discord provides three error boundary components with different levels of control. ErrorBoundary is feature-rich with fallbackRender, FallbackComponent, onError/onReset callbacks, and resetKeys. SimpleErrorBoundary accepts a static fallback ReactNode. LocalErrorBoundary shows inline error text for non-critical failures."
notices={[
{ type: "positive", children: "Wrap plugin UI components with ErrorBoundary to prevent crashes from taking down the entire Discord UI. Use LocalErrorBoundary for non-critical elements." },
{ type: "info", children: "ErrorBoundary only catches errors during rendering, lifecycle methods, and constructors. It does not catch errors in event handlers or async code." },
]}
importPath={'import { ErrorBoundary, SimpleErrorBoundary, LocalErrorBoundary } from "../components";'}
sections={[
{
title: "ErrorBoundary",
description: "Full-featured boundary with fallbackRender for dynamic error UI with reset capability.",
children: <ErrorBoundaryDemo />,
code: `<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Reset</button>
</div>
)}
onError={(error, info) => console.error(error)}
onReset={() => refetch()}
>
<MyComponent />
</ErrorBoundary>`,
relevantProps: ["fallbackRender", "onError", "onReset", "resetKeys"],
},
{
title: "SimpleErrorBoundary",
description: "Lightweight boundary that shows static fallback content on error.",
children: <SimpleDemo />,
code: `<SimpleErrorBoundary fallback={<p>Something went wrong!</p>}>
<MyComponent />
</SimpleErrorBoundary>`,
},
{
title: "LocalErrorBoundary",
description: "Displays an inline error message for non-critical component failures. No fallback prop needed.",
children: <LocalDemo />,
code: `<LocalErrorBoundary>
<MyWidget />
</LocalErrorBoundary>`,
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,101 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { FocusLock, ManaButton, ManaTextInput, Paragraph, useRef, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const FOCUSLOCK_PROPS: PropDef[] = [
{ name: "containerRef", type: "RefObject<HTMLElement | null>", required: true, description: "React ref pointing to the container element that defines the focus trap boundary." },
{ name: "keyboardModeEnabled", type: "boolean", description: "Enable keyboard-based focus trapping. When true, Tab key cycles through focusable elements inside the container." },
{ name: "children", type: "ReactNode", description: "Content rendered inside the focus trap." },
];
function FocusLockDemo() {
const [enabled, setEnabled] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [inputValue, setInputValue] = useState("");
return (
<div>
<ManaButton
variant={enabled ? "primary" : "secondary"}
text={enabled ? "Disable Focus Lock" : "Enable Focus Lock"}
onClick={() => setEnabled(!enabled)}
style={{ marginBottom: 16 }}
/>
<div
ref={containerRef}
style={{
padding: 16,
border: `2px solid ${enabled ? "var(--brand-500)" : "var(--background-modifier-accent)"}`,
borderRadius: 8,
background: "var(--background-secondary)",
}}
>
{enabled ? (
<FocusLock containerRef={containerRef}>
<Paragraph style={{ marginBottom: 12 }}>
Focus is trapped. Try pressing Tab to cycle through these elements:
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<ManaTextInput
value={inputValue}
onChange={setInputValue}
placeholder="First input"
/>
<ManaTextInput
value=""
onChange={() => { }}
placeholder="Second input"
/>
<div style={{ display: "flex", gap: 8 }}>
<ManaButton variant="secondary" text="Button A" onClick={() => { }} />
<ManaButton variant="secondary" text="Button B" onClick={() => { }} />
<ManaButton variant="primary" text="Exit Focus Lock" onClick={() => setEnabled(false)} />
</div>
</div>
</FocusLock>
) : (
<Paragraph color="text-muted">
Click "Enable Focus Lock" to activate the focus trap demo.
</Paragraph>
)}
</div>
</div>
);
}
export default function FocusLockTab() {
return (
<DocPage
componentName="FocusLock"
overview="FocusLock traps keyboard focus within a container element. When active, pressing Tab cycles through focusable elements inside the boundary without escaping. Used in modals, dropdown menus, and anywhere keyboard accessibility requires scoped navigation."
notices={[
{ type: "warn", children: "FocusLock traps all keyboard focus within its container. Always provide a way for the user to exit the focus trap (e.g. a close button or Escape key handler), otherwise they will be unable to navigate away." },
{ type: "info", children: "FocusLock is typically used inside modals and dropdown menus that already handle dismiss behavior. If you use it standalone, you must implement your own dismiss logic." },
]}
importPath={'import { FocusLock } from "../components";'}
sections={[
{
title: "Focus Trap",
description: "Enable the focus lock and press Tab to see focus cycle within the container boundary.",
children: <FocusLockDemo />,
code: `const containerRef = useRef<HTMLDivElement>(null);
<div ref={containerRef}>
<FocusLock containerRef={containerRef}>
<input />
<button>OK</button>
<button>Cancel</button>
</FocusLock>
</div>`,
relevantProps: ["containerRef"],
},
]}
props={FOCUSLOCK_PROPS}
/>
);
}

View file

@ -1,147 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { GuildIcon, GuildIconSizes, GuildStore, ManaButton, Paragraph, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const GUILDICON_PROPS: PropDef[] = [
{ name: "guildId", type: "string", required: true, description: "The guild's snowflake ID." },
{ name: "guildName", type: "string", required: true, description: "Guild name, used for the acronym fallback when no icon is available." },
{ name: "guildIcon", type: "string | null", required: true, description: "Icon hash from the guild object, or null for acronym fallback." },
{ name: "iconSize", type: "number", required: true, description: "Size in pixels. Common values: 16, 20, 24, 32, 40, 48, 56, 80, 96." },
{ name: "animate", type: "boolean", description: "Enable animated icons (animated GIF guild icons)." },
{ name: "className", type: "string", description: "CSS class for the icon container." },
{ name: "acronymClassName", type: "string", description: "CSS class for the acronym text fallback." },
];
function SizesDemo() {
const [animate, setAnimate] = useState(true);
const guilds = Object.values(GuildStore.getGuilds()).slice(0, 6);
const guildWithIcon = guilds.find(g => g.icon);
if (!guildWithIcon) {
return <Paragraph color="text-muted">No guilds with icons found.</Paragraph>;
}
return (
<div>
<ManaButton
variant="secondary"
text={`Animate: ${animate}`}
onClick={() => setAnimate(a => !a)}
style={{ marginBottom: 12 }}
/>
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
{GuildIconSizes.map(size => (
<div key={size} style={{ textAlign: "center" }}>
<GuildIcon
guildId={guildWithIcon.id}
guildName={guildWithIcon.name}
guildIcon={guildWithIcon.icon ?? null}
iconSize={size}
animate={animate}
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
{size}px
</Paragraph>
</div>
))}
</div>
</div>
);
}
function AcronymDemo() {
const guilds = Object.values(GuildStore.getGuilds()).slice(0, 6);
const guildWithoutIcon = guilds.find(g => !g.icon);
return (
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
{[32, 48, 80].map(size => (
<div key={size} style={{ textAlign: "center" }}>
<GuildIcon
guildId={guildWithoutIcon?.id ?? "0"}
guildName={guildWithoutIcon?.name ?? "Test Guild"}
guildIcon={null}
iconSize={size}
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
{size}px (no icon)
</Paragraph>
</div>
))}
</div>
);
}
function YourGuildsDemo() {
const [animate, setAnimate] = useState(true);
const guilds = Object.values(GuildStore.getGuilds()).slice(0, 8);
return (
<div>
<ManaButton
variant="secondary"
text={`Animate: ${animate}`}
onClick={() => setAnimate(a => !a)}
style={{ marginBottom: 12 }}
/>
<div className="vc-compfinder-grid">
{guilds.map(guild => (
<div key={guild.id} style={{ textAlign: "center", maxWidth: 80 }}>
<GuildIcon
guildId={guild.id}
guildName={guild.name}
guildIcon={guild.icon ?? null}
iconSize={48}
animate={animate}
/>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{guild.name.slice(0, 12)}
</Paragraph>
</div>
))}
</div>
</div>
);
}
export default function GuildIconTab() {
return (
<DocPage
componentName="GuildIcon"
overview="GuildIcon renders a server icon from its hash, or falls back to an acronym derived from the guild name. Supports all standard Discord icon sizes and animated GIF icons."
importPath={'import { GuildIcon, GuildIconSizes } from "../components";'}
sections={[
{
title: "Icon Sizes",
description: "All available GuildIconSizes from 16px to 96px.",
children: <SizesDemo />,
code: `<GuildIcon
guildId={guild.id}
guildName={guild.name}
guildIcon={guild.icon}
iconSize={48}
animate={true}
/>`,
relevantProps: ["iconSize", "animate"],
},
{
title: "Acronym Fallback",
description: "When guildIcon is null, the component renders an acronym based on the guild name.",
children: <AcronymDemo />,
relevantProps: ["guildName", "guildIcon"],
},
{
title: "Your Guilds",
description: "Live preview using guilds you're currently in.",
children: <YourGuildsDemo />,
},
]}
props={GUILDICON_PROPS}
/>
);
}

View file

@ -1,118 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DiscordHeading, DiscordText, HeadingVariants, TextColors, TextVariants } from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
const HEADING_PROPS: PropDef[] = [
{ name: "variant", type: "HeadingVariant", required: true, description: 'Heading size and weight, e.g. "heading-xl/bold", "heading-sm/medium".' },
{ name: "color", type: "TextColor", description: "Text color name from Discord's color system." },
{ name: "className", type: "string", description: "Custom CSS class." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
{ name: "children", type: "ReactNode", description: "Heading content." },
];
const TEXT_PROPS: PropDef[] = [
{ name: "variant", type: "TextVariant", required: true, description: 'Text size and weight, e.g. "text-md/medium", "text-sm/normal".' },
{ name: "color", type: "TextColor", description: "Text color name from Discord's color system." },
{ name: "tag", type: "string", default: '"div"', description: "HTML tag to render (div, span, p, etc.)." },
{ name: "selectable", type: "boolean", description: "Allow text selection." },
{ name: "lineClamp", type: "number", description: "Number of lines before truncation with ellipsis." },
{ name: "tabularNumbers", type: "boolean", description: "Use tabular (monospace) number spacing for aligned columns." },
{ name: "scaleFontToUserSetting", type: "boolean", description: "Scale font size based on user's accessibility settings." },
{ name: "className", type: "string", description: "Custom CSS class." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
{ name: "children", type: "ReactNode", description: "Text content." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "DiscordHeading", props: HEADING_PROPS },
{ title: "DiscordText", props: TEXT_PROPS },
];
export default function HeadingTab() {
return (
<DocPage
componentName="DiscordHeading"
overview="Discord's typography system uses two components: DiscordHeading for headings (xxl through sm) and DiscordText for body text (xxl through xs). Both support multiple weight variants and Discord's color system."
importPath={'import { DiscordHeading, DiscordText, HeadingVariants, TextVariants, TextColors } from "../components";'}
sections={[
{
title: "Heading Variants",
description: "All heading size/weight combinations from xxl to sm.",
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{HeadingVariants.map(variant => (
<DiscordHeading key={variant} variant={variant}>
{variant}
</DiscordHeading>
))}
</div>
),
code: '<DiscordHeading variant="heading-xl/bold">Title</DiscordHeading>',
relevantProps: ["variant"],
},
{
title: "Text Variants",
description: "All text size/weight combinations from xxl to xs.",
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{TextVariants.map(variant => (
<DiscordText key={variant} variant={variant}>
{variant}
</DiscordText>
))}
</div>
),
code: '<DiscordText variant="text-md/medium">Body text</DiscordText>',
},
{
title: "Text Colors",
description: "All available color values for the color prop.",
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{TextColors.map(color => (
<DiscordText key={color} variant="text-md/medium" color={color}>
{color}
</DiscordText>
))}
</div>
),
code: '<DiscordText variant="text-md/medium" color="text-brand">Branded</DiscordText>',
relevantProps: ["color"],
},
{
title: "Line Clamp",
description: "Truncate text after a specified number of lines.",
children: (
<div style={{ maxWidth: 300 }}>
<DiscordText variant="text-md/normal" lineClamp={2}>
This is a long paragraph of text that will be clamped to two lines maximum.
Any content beyond the second line will be hidden and replaced with an ellipsis.
This demonstrates the lineClamp prop in action.
</DiscordText>
</div>
),
code: '<DiscordText variant="text-md/normal" lineClamp={2}>\n Long text here...\n</DiscordText>',
relevantProps: ["lineClamp"],
},
{
title: "Tabular Numbers",
description: "Monospaced number spacing for aligned numeric columns.",
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<DiscordText variant="text-md/medium" tabularNumbers>1,234,567</DiscordText>
<DiscordText variant="text-md/medium" tabularNumbers>89,012</DiscordText>
<DiscordText variant="text-md/medium" tabularNumbers>345</DiscordText>
</div>
),
relevantProps: ["tabularNumbers"],
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,135 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { openModal } from "@utils/modal";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Heading, ManaButton, Paragraph, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
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;
};
const LAYERMODAL_PROPS: PropDef[] = [
{ name: "transitionState", type: "number", required: true, description: "Animation state. Use TransitionState.ENTERED for visible." },
{ name: "onClose", type: "() => void", required: true, description: "Called when the modal should close." },
{ name: "children", type: "ReactNode", required: true, description: "Modal content." },
{ name: "animationVariant", type: '"default" | string', default: '"default"', description: "Animation style for enter/exit transitions." },
{ name: "returnRef", type: "Ref<HTMLDivElement>", internal: true, description: "Ref forwarded to the modal root element." },
{ name: "aria-label", type: "string", description: "Accessibility label for the modal." },
];
function ModalContentDemo({ 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 opened via openModal. It supports stacked/layered
modal experiences with built-in animation handling.
</Paragraph>
<ManaButton variant="primary" text="Close" onClick={onClose} />
</div>
</LayerModal>
);
}
function InlineDemo() {
const [visible, setVisible] = useState(false);
return (
<div>
<ManaButton
variant="secondary"
text={visible ? "Hide Inline" : "Show Inline"}
onClick={() => setVisible(!visible)}
/>
{visible && (
<div style={{ marginTop: 16, position: "relative", minHeight: 200 }}>
<LayerModal
transitionState={TransitionState.ENTERED}
onClose={() => setVisible(false)}
aria-label="Inline Demo"
>
<div style={{ padding: 16 }}>
<Paragraph>Inline layer modal content</Paragraph>
<ManaButton variant="primary" text="Close" onClick={() => setVisible(false)} style={{ marginTop: 8 }} />
</div>
</LayerModal>
</div>
)}
</div>
);
}
export default function LayerModalTab() {
return (
<DocPage
componentName="LayerModal"
overview="LayerModal is Discord's component for stacked/layered modal experiences. It has different animation behavior than regular modals and is typically used with openModal() for proper backdrop and positioning. Requires TransitionState for controlling visibility."
notices={[
{ type: "warn", children: "LayerModal must be used with openModal() for proper backdrop, positioning, and transition state management. Rendering it inline without the modal system will result in incorrect behavior." },
{ type: "info", children: "LayerModal is found via findComponentByCodeLazy, not exported directly. The TransitionState constants are also required and must be found separately." },
]}
importPath={`import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
const LayerModal = findComponentByCodeLazy('"data-mana-component":"layer-modal"');
const TransitionState = findByPropsLazy("ENTERING", "ENTERED", "EXITING");`}
sections={[
{
title: "Open via openModal",
description: "The recommended way to use LayerModal is with openModal() which handles backdrop and positioning.",
children: (
<ManaButton
variant="secondary"
text="Open Layer Modal"
onClick={() => openModal(props => (
<ModalContentDemo onClose={props.onClose} />
))}
/>
),
code: `openModal(props => (
<LayerModal
transitionState={TransitionState.ENTERED}
onClose={props.onClose}
aria-label="My Modal"
>
<div style={{ padding: 24 }}>
<p>Modal content here</p>
</div>
</LayerModal>
))`,
relevantProps: ["transitionState", "onClose"],
},
{
title: "Inline Preview",
description: "Rendered inline for preview purposes. Not recommended for production use.",
children: <InlineDemo />,
relevantProps: ["aria-label", "animationVariant"],
},
]}
props={LAYERMODAL_PROPS}
/>
);
}

View file

@ -1,175 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ListboxItem, ManaListbox, useState } from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
const LISTBOX_PROPS: PropDef[] = [
{ name: "items", type: "ListboxItem[]", required: true, description: "Array of items to display in the listbox." },
{ name: "selectedItems", type: "ListboxItem[]", description: "Currently selected items (controlled)." },
{ name: "defaultSelectedItems", type: "ListboxItem[]", description: "Initially selected items (uncontrolled)." },
{ name: "onSelectionChange", type: "(items: ListboxItem[]) => void", description: "Called when the selection changes." },
{ name: "selectionMode", type: '"single" | "multiple"', description: "Whether one or many items can be selected." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable the entire listbox." },
{ name: "required", type: "boolean", description: "Mark the listbox as required for form validation." },
{ name: "loading", type: "boolean", default: "false", description: "Show a loading state." },
{ name: "maxVisibleItems", type: "number", description: "Maximum items visible before scrolling." },
{ name: "shouldFocusWrap", type: "boolean", description: "Wrap keyboard focus when reaching the end." },
{ name: "typeahead", type: "boolean", description: "Enable keyboard search by typing item labels." },
{ name: "renderListItem", type: "(item: ListboxItem) => ReactNode", description: "Custom render function for list items." },
{ name: "renderEmptyState", type: "() => ReactNode", description: "Custom render for the empty state." },
];
const ITEM_PROPS: PropDef[] = [
{ name: "id", type: "string", required: true, description: "Unique identifier for the item." },
{ name: "label", type: "string", required: true, description: "Display label for the item." },
{ name: "disabled", type: "boolean", description: "Disable this specific item." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "ManaListbox", props: LISTBOX_PROPS },
{ title: "ListboxItem", props: ITEM_PROPS },
];
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}`,
}));
function SingleSelectDemo() {
const [selected, setSelected] = useState<ListboxItem[]>([SAMPLE_ITEMS[0]]);
return (
<ManaListbox
items={SAMPLE_ITEMS}
selectedItems={selected}
onSelectionChange={setSelected}
selectionMode="single"
/>
);
}
function MultiSelectDemo() {
const [selected, setSelected] = useState<ListboxItem[]>([SAMPLE_ITEMS[0], SAMPLE_ITEMS[2]]);
return (
<ManaListbox
items={SAMPLE_ITEMS}
selectedItems={selected}
onSelectionChange={setSelected}
selectionMode="multiple"
/>
);
}
function DisabledItemsDemo() {
const [selected, setSelected] = useState<ListboxItem[]>([]);
return (
<ManaListbox
items={ITEMS_WITH_DISABLED}
selectedItems={selected}
onSelectionChange={setSelected}
selectionMode="multiple"
/>
);
}
function MaxVisibleDemo() {
const [selected, setSelected] = useState<ListboxItem[]>([]);
return (
<ManaListbox
items={MANY_ITEMS}
selectedItems={selected}
onSelectionChange={setSelected}
selectionMode="multiple"
maxVisibleItems={5}
/>
);
}
export default function ListboxTab() {
return (
<DocPage
componentName="ManaListbox"
overview="ManaListbox is Discord's listbox component for selecting one or multiple items from a list. Supports single and multiple selection modes, disabled items, loading/empty states, keyboard navigation with typeahead, and custom item rendering."
importPath={'import { ManaListbox, ListboxItem } from "../components";'}
sections={[
{
title: "Single Selection",
description: "Only one item can be selected at a time.",
children: <SingleSelectDemo />,
code: `<ManaListbox
items={items}
selectedItems={selected}
onSelectionChange={setSelected}
selectionMode="single"
/>`,
relevantProps: ["selectionMode", "selectedItems", "onSelectionChange"],
},
{
title: "Multiple Selection",
description: "Multiple items can be selected simultaneously.",
children: <MultiSelectDemo />,
code: `<ManaListbox
items={items}
selectedItems={selected}
onSelectionChange={setSelected}
selectionMode="multiple"
/>`,
},
{
title: "Disabled Items",
description: "Individual items can be disabled while the rest remain interactive.",
children: <DisabledItemsDemo />,
},
{
title: "Max Visible Items",
description: "Limits visible items to 5 with scrolling for the rest.",
children: <MaxVisibleDemo />,
relevantProps: ["maxVisibleItems"],
},
{
title: "Disabled State",
description: "The entire listbox is non-interactive when disabled.",
children: (
<ManaListbox
items={SAMPLE_ITEMS}
selectedItems={[SAMPLE_ITEMS[1]]}
disabled
/>
),
relevantProps: ["disabled"],
},
{
title: "Loading State",
description: "Shows a loading indicator while data is being fetched.",
children: <ManaListbox items={[]} loading />,
relevantProps: ["loading"],
},
{
title: "Empty State",
description: "Default empty appearance when no items are provided.",
children: <ManaListbox items={[]} />,
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,235 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button, ContextMenuApi, Menu, useState } from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
const MENU_PROPS: PropDef[] = [
{ name: "navId", type: "string", required: true, description: "Unique navigation ID for the menu." },
{ name: "onClose", type: "() => void", required: true, description: "Called when the menu should close." },
{ name: "children", type: "ReactNode", required: true, description: "Menu items, groups, and separators." },
];
const MENUITEM_PROPS: PropDef[] = [
{ name: "id", type: "string", required: true, description: "Unique item identifier." },
{ name: "label", type: "ReactNode | ((props) => ReactNode)", required: true, description: "Display text for the item." },
{ name: "action", type: "(e: MouseEvent) => void", description: "Called when the item is clicked." },
{ name: "icon", type: "ComponentType", description: "Icon component displayed after the label." },
{ name: "iconLeft", type: "ComponentType", description: "Icon component displayed before the label." },
{ name: "iconProps", type: "MenuItemIconProps", description: "Props passed to the icon component." },
{ name: "leadingAccessory", type: "ReactNode", description: "Custom leading accessory element." },
{ name: "trailingIndicator", type: "ReactNode", description: "Trailing indicator element." },
{ name: "hint", type: "ReactNode | ((props) => ReactNode)", description: "Keyboard shortcut hint displayed on the right." },
{ name: "shortcut", type: "ReactNode", description: "Keyboard shortcut display." },
{ name: "subtext", type: "ReactNode", description: "Secondary text below the label." },
{ name: "subtextLineClamp", type: "number", description: "Max lines for subtext before truncation." },
{ name: "color", type: '"default" | "brand" | "danger" | "success" | "premium" | "premium-gradient"', description: "Item color variant." },
{ name: "loading", type: "boolean", description: "Show loading state." },
{ name: "badge", type: "ReactNode", description: "Badge element displayed alongside the label." },
{ name: "disabled", type: "boolean", description: "Disable interaction." },
{ name: "onClose", type: "() => void", description: "Called when the menu should close after this item's action." },
{ name: "onFocus", type: "() => void", description: "Called when the item receives focus." },
{ name: "dontCloseOnAction", type: "boolean", description: "Prevent menu from closing when this item is clicked." },
{ name: "dontCloseOnActionIfHoldingShiftKey", type: "boolean", description: "Prevent close on click if shift key is held." },
{ name: "className", type: "string", description: "Custom CSS class name." },
{ name: "focusedClassName", type: "string", description: "CSS class applied when focused." },
{ name: "children", type: "ReactNode", description: "Nested MenuItem elements create a submenu." },
];
const CHECKBOX_PROPS: PropDef[] = [
{ name: "id", type: "string", required: true, description: "Unique item identifier." },
{ name: "label", type: "ReactNode | ((props) => ReactNode)", required: true, description: "Display text." },
{ name: "checked", type: "boolean", required: true, description: "Current checked state." },
{ name: "color", type: "MenuItemColor", default: '"default"', description: "Item color variant." },
{ name: "action", type: "(e: MouseEvent) => void", description: "Called when toggled." },
{ name: "disabled", type: "boolean", description: "Disable interaction." },
{ name: "subtext", type: "ReactNode", description: "Secondary text below the label." },
{ name: "subtextLineClamp", type: "number", description: "Max lines for subtext before truncation." },
{ name: "leftIcon", type: "ComponentType", description: "Icon component displayed on the left." },
{ name: "leadingAccessory", type: "ReactNode", description: "Custom leading accessory element." },
{ name: "className", type: "string", description: "Custom CSS class name." },
{ name: "focusedClassName", type: "string", description: "CSS class applied when focused." },
];
const RADIO_PROPS: PropDef[] = [
{ name: "id", type: "string", required: true, description: "Unique item identifier." },
{ name: "label", type: "ReactNode | ((props) => ReactNode)", required: true, description: "Display text." },
{ name: "group", type: "string", required: true, description: "Radio group name for mutual exclusion." },
{ name: "checked", type: "boolean", required: true, description: "Whether this option is selected." },
{ name: "color", type: "MenuItemColor", default: '"default"', description: "Item color variant." },
{ name: "action", type: "(e: MouseEvent) => void", description: "Called when selected." },
{ name: "disabled", type: "boolean", description: "Disable interaction." },
{ name: "subtext", type: "ReactNode", description: "Secondary text below the label." },
{ name: "subtextLineClamp", type: "number", description: "Max lines for subtext before truncation." },
{ name: "leftIcon", type: "ComponentType", description: "Icon component displayed on the left." },
{ name: "leadingAccessory", type: "ReactNode", description: "Custom leading accessory element." },
];
const SLIDER_PROPS: PropDef[] = [
{ name: "minValue", type: "number", required: true, description: "Minimum slider value." },
{ name: "maxValue", type: "number", required: true, description: "Maximum slider value." },
{ name: "value", type: "number", required: true, description: "Current slider value." },
{ name: "onChange", type: "(value: number) => void", required: true, description: "Called when the value changes." },
{ name: "renderValue", type: "(value: number) => string", description: "Format the value label display." },
];
const SWITCH_PROPS: PropDef[] = [
{ name: "id", type: "string", required: true, description: "Unique item identifier." },
{ name: "label", type: "ReactNode", description: "Display text." },
{ name: "checked", type: "boolean", required: true, description: "Current switch state." },
{ name: "action", type: "(e: MouseEvent) => void", required: true, description: "Called when toggled." },
{ name: "color", type: "MenuItemColor", default: '"default"', description: "Item color variant." },
{ name: "disabled", type: "boolean", description: "Disable interaction." },
{ name: "subtext", type: "ReactNode", description: "Secondary text below the label." },
{ name: "subtextLineClamp", type: "number", description: "Max lines for subtext before truncation." },
{ name: "className", type: "string", description: "Custom CSS class name." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "Menu.Menu", props: MENU_PROPS },
{ title: "Menu.MenuItem", props: MENUITEM_PROPS },
{ title: "Menu.MenuCheckboxItem", props: CHECKBOX_PROPS },
{ title: "Menu.MenuRadioItem", props: RADIO_PROPS },
{ title: "Menu.MenuSwitchItem", props: SWITCH_PROPS },
{ title: "Menu.MenuSliderControl", props: SLIDER_PROPS },
];
function DemoMenu({ onClose }: { onClose: () => void; }) {
const [checkboxValue, setCheckboxValue] = useState(false);
const [switchValue, setSwitchValue] = useState(true);
const [radioValue, setRadioValue] = useState("option1");
const [sliderValue, setSliderValue] = useState(50);
return (
<Menu.Menu navId="demo-menu" onClose={onClose}>
<Menu.MenuItem id="item-1" label="Basic Item" action={() => { }} />
<Menu.MenuItem
id="item-2"
label="Item with Icon"
icon={() => (
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
)}
action={() => { }}
/>
<Menu.MenuItem id="item-hint" label="Item with Hint" hint="Ctrl+H" action={() => { }} />
<Menu.MenuItem id="item-subtext" label="Item with Subtext" subtext="Additional description" action={() => { }} />
<Menu.MenuItem id="item-disabled" label="Disabled Item" disabled />
<Menu.MenuSeparator />
<Menu.MenuGroup label="Colors">
<Menu.MenuItem id="c-default" label="Default" color="default" action={() => { }} />
<Menu.MenuItem id="c-brand" label="Brand" color="brand" action={() => { }} />
<Menu.MenuItem id="c-danger" label="Danger" color="danger" action={() => { }} />
<Menu.MenuItem id="c-success" label="Success" color="success" action={() => { }} />
<Menu.MenuItem id="c-premium" label="Premium" color="premium" action={() => { }} />
<Menu.MenuItem id="c-premium-g" label="Premium Gradient" color="premium-gradient" action={() => { }} />
</Menu.MenuGroup>
<Menu.MenuSeparator />
<Menu.MenuCheckboxItem
id="checkbox"
label="Checkbox Item"
checked={checkboxValue}
action={() => setCheckboxValue(v => !v)}
/>
<Menu.MenuSwitchItem
id="switch"
label="Switch Item"
checked={switchValue}
action={() => setSwitchValue(v => !v)}
/>
<Menu.MenuSeparator />
<Menu.MenuGroup label="Radio Group">
<Menu.MenuRadioItem id="r-1" group="demo" label="Option 1" checked={radioValue === "option1"} action={() => setRadioValue("option1")} />
<Menu.MenuRadioItem id="r-2" group="demo" label="Option 2" checked={radioValue === "option2"} action={() => setRadioValue("option2")} />
<Menu.MenuRadioItem id="r-3" group="demo" label="Option 3" checked={radioValue === "option3"} action={() => setRadioValue("option3")} />
</Menu.MenuGroup>
<Menu.MenuSeparator />
<Menu.MenuItem id="submenu" label="Submenu">
<Menu.MenuItem id="sub-1" label="Sub Item 1" action={() => { }} />
<Menu.MenuItem id="sub-2" label="Sub Item 2" action={() => { }} />
<Menu.MenuItem id="nested" label="Nested Submenu">
<Menu.MenuItem id="nested-1" label="Nested Item" action={() => { }} />
</Menu.MenuItem>
</Menu.MenuItem>
<Menu.MenuSeparator />
<Menu.MenuControlItem
id="slider"
label="Volume"
control={() => (
<Menu.MenuSliderControl
minValue={0}
maxValue={100}
value={sliderValue}
onChange={setSliderValue}
renderValue={v => `${v}%`}
/>
)}
/>
</Menu.Menu>
);
}
export default function MenuTab() {
const openMenu = (e: React.MouseEvent) => {
ContextMenuApi.openContextMenu(e, () => (
<DemoMenu onClose={ContextMenuApi.closeContextMenu} />
));
};
return (
<DocPage
componentName="Menu"
overview="Discord's Menu system provides context menus with items, checkboxes, radio buttons, switches, sliders, submenus, and groups. Open menus with ContextMenuApi.openContextMenu(). The Menu object contains all sub-components: Menu, MenuItem, MenuCheckboxItem, MenuRadioItem, MenuSwitchItem, MenuGroup, MenuSeparator, MenuControlItem, MenuSliderControl, and MenuSearchControl."
notices={[
{ type: "info", children: "Menus must be opened via ContextMenuApi.openContextMenu(). Set the onClose prop to ContextMenuApi.closeContextMenu to ensure proper cleanup." },
]}
importPath={'import { Menu, ContextMenuApi } from "../components";'}
sections={[
{
title: "Full Menu Demo",
description: "Click or right-click to open a menu showcasing all component types: basic items, icons, hints, subtext, colors, checkboxes, switches, radio groups, submenus, and sliders.",
children: (
<div className="vc-compfinder-grid">
<Button onClick={openMenu}>Open Menu (Click)</Button>
<Button variant="secondary" onContextMenu={openMenu}>Open Menu (Right-Click)</Button>
</div>
),
code: `const openMenu = (e: React.MouseEvent) => {
ContextMenuApi.openContextMenu(e, () => (
<Menu.Menu navId="my-menu" onClose={ContextMenuApi.closeContextMenu}>
<Menu.MenuItem id="item" label="Click me" action={() => { }} />
<Menu.MenuSeparator />
<Menu.MenuCheckboxItem id="check" label="Toggle" checked={on} action={() => setOn(!on)} />
</Menu.Menu>
));
};`,
},
{
title: "MenuItem Colors",
description: "Items support color variants: default, brand, danger, success, premium, and premium-gradient. These are visible in the full demo above.",
children: (
<Button variant="secondary" onClick={openMenu}>
Open Menu to See Colors
</Button>
),
relevantProps: ["color"],
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,273 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import {
closeAllModals,
ConfirmModal,
ExpressiveModal,
ManaButton,
ManaTextInput,
Modal,
openModal,
Paragraph,
showToast,
ToastType,
useState,
} from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
const MODAL_PROPS: PropDef[] = [
{ name: "title", type: "string", required: true, description: "Modal title displayed in the header." },
{ name: "onClose", type: "() => void", required: true, description: "Called when the modal should close." },
{ name: "subtitle", type: "string", description: "Subtitle text below the title." },
{ name: "size", type: '"sm" | "md"', description: "Modal width size." },
{ name: "transitionState", type: "number", description: "Animation state from openModal render props." },
{ name: "animationVariant", type: '"default" | "fade" | "slide-up"', default: '"default"', description: "Transition animation style." },
{ name: "paddingSize", type: '"sm" | "lg"', default: '"sm"', description: "Internal padding size." },
{ name: "fullScreenOnMobile", type: "boolean", default: "true", description: "Whether to go full screen on mobile." },
{ name: "maxHeight", type: '"default" | "full"', default: '"default"', description: "Maximum height constraint." },
{ name: "dismissable", type: "boolean", default: "true", description: "Whether the modal can be dismissed by clicking outside." },
{ name: "children", type: "ReactNode", description: "Modal body content." },
{ name: "actions", type: "ModalAction[]", description: "Array of action buttons in the footer." },
{ name: "actionBarInput", type: "ReactNode", description: "Input element in the action bar area." },
{ name: "actionBarInputLayout", type: '"default" | "full-width"', default: '"default"', description: "Layout of the action bar input." },
{ name: "notice", type: '{ message: string; type: "info" | "warn" | "critical" | "positive" }', description: "Notice banner shown at the top of the modal." },
{ name: "input", type: "ReactNode", description: "Input controls rendered in the header area." },
{ name: "preview", type: "ReactNode", description: "Preview content above the modal body." },
{ name: "onScroll", type: "(e: UIEvent) => void", description: "Scroll event handler for the modal body." },
{ name: "scrollerRef", type: "Ref<HTMLDivElement>", description: "Ref to the scroller element." },
{ name: "role", type: "string", default: '"dialog"', description: "ARIA role for the modal." },
{ name: "aria-label", type: "string", description: "ARIA label for the modal." },
{ name: "className", type: "string", description: "Custom CSS class." },
];
const CONFIRM_PROPS: PropDef[] = [
{ name: "title", type: "string", required: true, description: "Modal title." },
{ name: "confirmText", type: "string", required: true, description: "Text for the confirm button." },
{ name: "onClose", type: "() => void", required: true, description: "Called when the modal should close." },
{ name: "cancelText", type: "string", description: "Text for the cancel button." },
{ name: "variant", type: '"critical" | "primary" | "secondary"', description: "Visual style of the confirm button." },
{ name: "onConfirm", type: "() => void | Promise<void>", description: "Called when the confirm button is clicked. Can be async." },
{ name: "onCancel", type: "() => void", description: "Called when the cancel button is clicked." },
{ name: "checkboxProps", type: "{ label, checked, onChange }", description: "Optional checkbox in the modal body." },
{ name: "children", type: "ReactNode", description: "Body content." },
];
const EXPRESSIVE_PROPS: PropDef[] = [
{ name: "title", type: "string", required: true, description: "Modal title." },
{ name: "onClose", type: "() => void", required: true, description: "Called when the modal should close." },
{ name: "subtitle", type: "string", description: "Subtitle text below the title." },
{ name: "size", type: '"sm" | "md"', default: '"md"', description: "Modal width size." },
{ name: "transitionState", type: "number", description: "Animation state." },
{ name: "gradientColor", type: '"purple" | "blue" | "pink" | "green"', default: '"purple"', description: "Background gradient color." },
{ name: "graphic", type: '{ type: "image"; src: string; aspectRatio?: string }', description: "Image graphic shown above the content." },
{ name: "badge", type: "ReactNode", description: "Badge element shown near the title." },
{ name: "actionBarInput", type: "ReactNode", description: "Input element in the action bar area." },
{ name: "actions", type: "ModalAction[]", description: "Action buttons." },
{ name: "children", type: "ReactNode", description: "Modal body content." },
];
const ACTION_PROPS: PropDef[] = [
{ name: "text", type: "string", required: true, description: "Button text." },
{ name: "variant", type: '"primary" | "secondary" | "critical-primary" | "critical-secondary"', required: true, description: "Button style variant." },
{ name: "onClick", type: "() => void", required: true, description: "Click handler." },
{ name: "disabled", type: "boolean", description: "Disable the button." },
{ name: "loading", type: "boolean", description: "Show loading spinner." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "Modal", props: MODAL_PROPS },
{ title: "ConfirmModal", props: CONFIRM_PROPS },
{ title: "ExpressiveModal", props: EXPRESSIVE_PROPS },
{ title: "ModalAction", props: ACTION_PROPS },
];
function openBasicModal() {
openModal(props => (
<Modal
title="Basic Modal"
subtitle="This is a subtitle"
transitionState={props.transitionState}
onClose={props.onClose}
actions={[
{ text: "Cancel", variant: "secondary", onClick: props.onClose },
{ text: "Confirm", variant: "primary", onClick: () => { showToast("Confirmed!", ToastType.SUCCESS); props.onClose(); } },
]}
>
<Paragraph>This is the modal body content. Any React components can go here.</Paragraph>
</Modal>
));
}
function openNoticeModal() {
openModal(props => (
<Modal
title="Modal with Notice"
transitionState={props.transitionState}
onClose={props.onClose}
notice={{ message: "This is an informational notice", type: "info" }}
actions={[{ text: "Got it", variant: "primary", onClick: props.onClose }]}
>
<Paragraph>Modals can display notices at the top for important information.</Paragraph>
</Modal>
));
}
function ModalWithInputContent({ onClose, transitionState }: { onClose: () => void; transitionState: number; }) {
const [inputValue, setInputValue] = useState("");
return (
<Modal
title="Modal with Input"
subtitle="Enter some information"
transitionState={transitionState}
onClose={onClose}
actions={[
{ text: "Cancel", variant: "secondary", onClick: onClose },
{ text: "Submit", variant: "primary", onClick: () => { showToast(`Submitted: ${inputValue}`, ToastType.SUCCESS); onClose(); } },
]}
>
<ManaTextInput value={inputValue} onChange={setInputValue} placeholder="Type something..." />
</Modal>
);
}
function openConfirmCritical() {
openModal(props => (
<ConfirmModal
title="Confirm Action"
subtitle="Are you sure you want to proceed?"
transitionState={props.transitionState}
confirmText="Delete"
cancelText="Cancel"
variant="critical"
onConfirm={() => showToast("Item deleted!", ToastType.SUCCESS)}
onCancel={() => showToast("Cancelled", ToastType.MESSAGE)}
onClose={props.onClose}
>
<Paragraph>This action cannot be undone. The item will be permanently deleted.</Paragraph>
</ConfirmModal>
));
}
function openConfirmPrimary() {
openModal(props => (
<ConfirmModal
title="Save Changes"
transitionState={props.transitionState}
confirmText="Save"
variant="primary"
onConfirm={() => showToast("Changes saved!", ToastType.SUCCESS)}
onClose={props.onClose}
>
<Paragraph>Do you want to save your changes?</Paragraph>
</ConfirmModal>
));
}
function openExpressive() {
openModal(props => (
<ExpressiveModal
title="Welcome!"
subtitle="This is an expressive modal with a more visual design."
transitionState={props.transitionState}
gradientColor="purple"
actions={[{ text: "Get Started", variant: "primary", onClick: props.onClose }]}
onClose={props.onClose}
/>
));
}
export default function ModalTab() {
return (
<DocPage
componentName="Modal"
overview="Discord's modal system includes Modal for general-purpose dialogs, ConfirmModal for confirmation flows, and ExpressiveModal for visual/onboarding experiences. All are opened via openModal() which handles backdrop, positioning, and transition states."
notices={[
{ type: "warn", children: "Always use openModal() to display modals. It handles the backdrop, animation state, stacking context, and cleanup. Rendering modals inline will not work correctly." },
{ type: "info", children: "The transitionState prop from openModal's render callback must be forwarded to the modal component for enter/exit animations to work properly." },
]}
importPath={'import { Modal, ConfirmModal, ExpressiveModal, openModal, closeModal, closeAllModals } from "../components";'}
sections={[
{
title: "Basic Modal",
description: "Simple modal with title, subtitle, body content, and action buttons.",
children: <ManaButton variant="secondary" text="Open Basic Modal" onClick={openBasicModal} />,
code: `openModal(props => (
<Modal
title="My Modal"
subtitle="Subtitle text"
transitionState={props.transitionState}
onClose={props.onClose}
actions={[
{ text: "Cancel", variant: "secondary", onClick: props.onClose },
{ text: "Confirm", variant: "primary", onClick: handleConfirm },
]}
>
<p>Modal content here</p>
</Modal>
))`,
relevantProps: ["title", "subtitle", "actions"],
},
{
title: "Modal with Notice",
description: "A notice banner appears at the top of the modal for important info.",
children: <ManaButton variant="secondary" text="Open Modal with Notice" onClick={openNoticeModal} />,
relevantProps: ["notice"],
},
{
title: "Modal with Input",
description: "Modal with form controls in the body area.",
children: (
<ManaButton
variant="secondary"
text="Open Modal with Input"
onClick={() => openModal(props => (
<ModalWithInputContent onClose={props.onClose} transitionState={props.transitionState} />
))}
/>
),
},
{
title: "ConfirmModal (Critical)",
description: "Confirmation dialog for destructive actions with danger styling.",
children: <ManaButton variant="critical-secondary" text="Open Confirm Modal" onClick={openConfirmCritical} />,
code: `openModal(props => (
<ConfirmModal
title="Delete Item?"
confirmText="Delete"
cancelText="Cancel"
variant="critical"
onConfirm={() => deleteItem()}
onClose={props.onClose}
transitionState={props.transitionState}
>
<p>This cannot be undone.</p>
</ConfirmModal>
))`,
relevantProps: ["confirmText", "variant", "onConfirm"],
},
{
title: "ConfirmModal (Primary)",
description: "Confirmation dialog for non-destructive actions.",
children: <ManaButton variant="secondary" text="Open Save Dialog" onClick={openConfirmPrimary} />,
},
{
title: "ExpressiveModal",
description: "Visual modal for onboarding or promotional experiences.",
children: <ManaButton variant="secondary" text="Open Expressive Modal" onClick={openExpressive} />,
relevantProps: ["subtitle", "gradientColor", "graphic", "badge"],
},
{
title: "Close All Modals",
description: "Utility to close all open modals at once.",
children: <ManaButton variant="critical-secondary" text="Close All Modals" onClick={closeAllModals} />,
code: "closeAllModals();",
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,112 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button, Notice, NoticeTypes, Paragraph } from "..";
import { DocPage, type PropDef } from "../DocPage";
const NOTICE_PROPS: PropDef[] = [
{ name: "children", type: "ReactNode", required: true, description: "Notice content text." },
{ name: "messageType", type: '"info" | "warn" | "danger" | "positive" | "preview"', required: true, description: "Visual severity level of the notice." },
{ name: "action", type: "ReactNode", description: "Action element (typically a button) displayed on the right side." },
{ name: "hidden", type: "boolean", default: "false", description: "Hides the notice without removing it from the DOM." },
{ name: "textColor", type: "string", description: "Custom text color override." },
{ name: "textVariant", type: "string", default: '"text-sm/medium"', description: 'Text size/weight variant, e.g. "text-md/normal".' },
{ name: "icon", type: "ComponentType<{ className?, color? }>", description: "Custom icon component to replace the default." },
{ name: "className", type: "string", description: "Additional CSS class." },
];
export default function NoticeTab() {
return (
<DocPage
componentName="Notice"
overview="Notice displays informational messages with different severity levels. Each type has a distinct color and icon. Supports action buttons, custom icons, text variants, and can be hidden without unmounting."
importPath={'import { Notice, NoticeTypes } from "../components";'}
sections={[
{
title: "Message Types",
description: "All five severity levels with distinct colors and icons.",
children: (
<div className="vc-compfinder-grid-vertical">
{NoticeTypes.map(type => (
<Notice key={type} messageType={type}>
This is a {type} notice message.
</Notice>
))}
</div>
),
code: '<Notice messageType="info">Informational message.</Notice>',
relevantProps: ["messageType"],
},
{
title: "With Action Button",
description: "Notices can include action buttons for user interaction.",
children: (
<div className="vc-compfinder-grid-vertical">
<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>
),
code: `<Notice
messageType="warn"
action={<Button size="small" variant="primary">Fix Now</Button>}
>
Warning that requires attention.
</Notice>`,
relevantProps: ["action"],
},
{
title: "Hidden State",
description: "The hidden prop controls visibility without removing from the DOM.",
children: (
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>hidden=false:</Paragraph>
<Notice messageType="info" hidden={false}>
This notice is visible.
</Notice>
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>hidden=true (invisible below):</Paragraph>
<Notice messageType="info" hidden={true}>
This notice is hidden.
</Notice>
</div>
</div>
),
relevantProps: ["hidden"],
},
{
title: "Text Variants",
description: "Customize text sizing with the textVariant prop.",
children: (
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Default (text-sm/medium):</Paragraph>
<Notice messageType="info">Default text variant.</Notice>
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>text-md/normal:</Paragraph>
<Notice messageType="info" textVariant="text-md/normal">Larger text variant.</Notice>
</div>
</div>
),
relevantProps: ["textVariant"],
},
]}
props={NOTICE_PROPS}
/>
);
}

View file

@ -1,105 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paginator, Paragraph, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const PAGINATOR_PROPS: PropDef[] = [
{ name: "currentPage", type: "number", required: true, description: "Currently active page (1-based)." },
{ name: "totalCount", type: "number", required: true, description: "Total number of items across all pages." },
{ name: "pageSize", type: "number", required: true, description: "Number of items per page." },
{ name: "onPageChange", type: "(page: number) => void", description: "Called when the user navigates to a different page." },
{ name: "maxVisiblePages", type: "number", description: "Maximum number of page buttons visible at once." },
{ name: "hideMaxPage", type: "boolean", default: "false", description: "Hide the last page number." },
{ name: "disablePaginationGap", type: "boolean", default: "false", description: "Remove the ellipsis gaps between page numbers." },
{ name: "className", type: "string", description: "Custom CSS class." },
{ name: "renderPageWrapper", type: "(props, defaultRender) => ReactNode", description: "Custom render wrapper for each page button." },
];
function BasicDemo() {
const [page, setPage] = useState(1);
return (
<div>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Page {page} of 10</Paragraph>
<Paginator currentPage={page} totalCount={100} pageSize={10} onPageChange={setPage} />
</div>
);
}
function MaxVisibleDemo() {
const [page3, setPage3] = useState(1);
const [page7, setPage7] = useState(1);
return (
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>maxVisiblePages=3:</Paragraph>
<Paginator currentPage={page3} totalCount={100} pageSize={10} maxVisiblePages={3} onPageChange={setPage3} />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>maxVisiblePages=7:</Paragraph>
<Paginator currentPage={page7} totalCount={100} pageSize={10} maxVisiblePages={7} onPageChange={setPage7} />
</div>
</div>
);
}
function OptionsDemo() {
const [page, setPage] = useState(1);
return (
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>hideMaxPage:</Paragraph>
<Paginator currentPage={page} totalCount={100} pageSize={10} hideMaxPage onPageChange={setPage} />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>disablePaginationGap:</Paragraph>
<Paginator currentPage={page} totalCount={50} pageSize={10} disablePaginationGap onPageChange={setPage} />
</div>
</div>
);
}
export default function PaginatorTab() {
return (
<DocPage
componentName="Paginator"
overview="Paginator provides page navigation for paginated content. Shows page numbers with back/next buttons, ellipsis gaps for large page ranges, and supports customization of visible page count and gap behavior."
notices={[
{ type: "info", children: "Paginator is 1-based. The currentPage value starts at 1, not 0." },
]}
importPath={'import { Paginator } from "../components";'}
sections={[
{
title: "Basic",
description: "Standard paginator with 10 pages.",
children: <BasicDemo />,
code: `<Paginator
currentPage={page}
totalCount={100}
pageSize={10}
onPageChange={setPage}
/>`,
relevantProps: ["currentPage", "totalCount", "pageSize", "onPageChange"],
},
{
title: "Max Visible Pages",
description: "Control how many page numbers are visible before ellipsis appears.",
children: <MaxVisibleDemo />,
relevantProps: ["maxVisiblePages"],
},
{
title: "Options",
description: "Hide the last page number or remove ellipsis gaps entirely.",
children: <OptionsDemo />,
relevantProps: ["hideMaxPage", "disablePaginationGap"],
},
]}
props={PAGINATOR_PROPS}
/>
);
}

View file

@ -1,142 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaButton, ManaPopover, PopoverAction, useRef, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const POPOVER_PROPS: PropDef[] = [
{ name: "targetElementRef", type: "RefObject<HTMLElement>", required: true, description: "Ref to the element the popover is anchored to." },
{ name: "shouldShow", type: "boolean", required: true, description: "Controls popover visibility." },
{ name: "onRequestClose", type: "(reason: string) => void", description: "Called when the popover wants to close (click outside, escape, etc.)." },
{ name: "title", type: "string", description: "Popover title text." },
{ name: "body", type: "string", description: "Popover body text." },
{ name: "size", type: '"sm" | "md" | "lg"', description: "Popover width size." },
{ name: "position", type: '"top" | "bottom" | "left" | "right"', description: "Position relative to the target element." },
{ name: "actions", type: "PopoverAction[]", description: "Action buttons rendered in the popover footer." },
{ name: "textLink", type: "{ text: string; onClick?: () => void }", description: "Text link displayed below the body." },
{ name: "graphic", type: "{ src: string; aspectRatio?: string }", description: "Image graphic shown in the popover." },
{ name: "badge", type: "ReactNode", description: "Badge element displayed near the title." },
{ name: "gradientColor", type: "string", description: "Background gradient color." },
{ name: "scrollBehavior", type: "string", description: "Scroll behavior for popover content." },
{ name: "hasVideo", type: "boolean", description: "Whether the popover contains video content." },
{ name: "popoverRef", type: "Ref<HTMLDivElement>", description: "Ref forwarded to the popover container." },
{ name: "caretConfig", type: '{ align?: "start" | "center" | "end" }', description: "Configuration for the caret arrow." },
{ name: "alignmentStrategy", type: '"edge" | "trigger-center"', description: "How the popover aligns to the target element." },
{ name: "align", type: '"left" | "center" | "right"', description: 'Alignment when alignmentStrategy is "edge".' },
];
function PopoverDemo({ label, title, body, size, actions, position }: {
label: string;
title: string;
body: string;
size?: "sm" | "md" | "lg";
actions?: PopoverAction[];
position?: "top" | "bottom" | "left" | "right";
}) {
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 (
<DocPage
componentName="ManaPopover"
overview="ManaPopover is Discord's popover component that anchors to a target element. Supports title, body text, action buttons, graphics, text links, and badge elements. Positioned relative to the target with configurable direction and size."
notices={[
{ type: "warn", children: "The targetElementRef must point to a mounted DOM element. If the ref is null when shouldShow becomes true, the popover will not position correctly." },
{ type: "info", children: "Handle onRequestClose to dismiss the popover when the user clicks outside or presses Escape. Without it, the popover will remain visible until shouldShow is manually set to false." },
]}
importPath={'import { ManaPopover } from "../components";'}
sections={[
{
title: "Basic",
description: "Simple popover with title and body text.",
children: (
<PopoverDemo
label="Click me"
title="Popover Title"
body="This is the popover body text with some helpful information."
/>
),
code: `const ref = useRef<HTMLDivElement>(null);
const [show, setShow] = useState(false);
<div ref={ref}>
<Button onClick={() => setShow(!show)}>Click me</Button>
</div>
<ManaPopover
targetElementRef={ref}
shouldShow={show}
onRequestClose={() => setShow(false)}
title="Popover Title"
body="Body text here."
/>`,
relevantProps: ["targetElementRef", "shouldShow", "onRequestClose", "title", "body"],
},
{
title: "Sizes",
description: "Three size variants: sm, md, and lg.",
children: (
<div className="vc-compfinder-grid">
<PopoverDemo label="sm" title="Small" body="Small popover." size="sm" />
<PopoverDemo label="md" title="Medium" body="Medium popover (default)." size="md" />
<PopoverDemo label="lg" title="Large" body="Large popover with more space." size="lg" />
</div>
),
relevantProps: ["size"],
},
{
title: "Positions",
description: "Popover can be positioned on any side of the target element.",
children: (
<div className="vc-compfinder-grid">
<PopoverDemo label="top" title="Top" body="Appears above." position="top" />
<PopoverDemo label="bottom" title="Bottom" body="Appears below." position="bottom" />
<PopoverDemo label="left" title="Left" body="Appears left." position="left" />
<PopoverDemo label="right" title="Right" body="Appears right." position="right" />
</div>
),
relevantProps: ["position"],
},
{
title: "With Actions",
description: "Popover with action buttons in the footer.",
children: (
<PopoverDemo
label="With Actions"
title="Popover with Actions"
body="This popover has action buttons."
actions={[
{ text: "Primary", variant: "primary" },
{ text: "Secondary", variant: "secondary" },
]}
/>
),
relevantProps: ["actions"],
},
]}
props={POPOVER_PROPS}
/>
);
}

View file

@ -1,159 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph, ProgressBar, useEffect, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const PROGRESSBAR_PROPS: PropDef[] = [
{ name: "progress", type: "number", required: true, description: "Current progress value." },
{ name: "minimum", type: "number", default: "0", description: "Minimum range value." },
{ name: "maximum", type: "number", default: "100", description: "Maximum range value." },
{ name: "variant", type: '"blue" | "orange" | "unset"', default: '"blue"', description: 'Color variant. Use "unset" with override for custom colors.' },
{ name: "override", type: "{ default?: { background, gradientStart, gradientEnd } }", description: 'Custom colors when variant is "unset".' },
{ name: "weight", type: '"light" | "medium"', default: '"light"', description: "Visual weight of the progress bar." },
{ name: "glowing", type: "boolean", default: "true", description: "Whether the progress bar has a glow effect." },
{ name: "labelledBy", type: "string", internal: true, description: "ID of the labelling element for accessibility." },
];
function AnimatedDemo() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setProgress(prev => (prev >= 100 ? 0 : prev + 5));
}, 200);
return () => clearInterval(interval);
}, []);
return (
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Progress: {progress}%</Paragraph>
<ProgressBar progress={progress} variant="blue" />
</div>
);
}
export default function ProgressBarTab() {
return (
<DocPage
overview="ProgressBar displays a horizontal progress indicator with support for blue and orange color variants, light and medium weights, a glow effect, custom color overrides via gradients, and configurable min/max ranges."
importPath={'import { ProgressBar } from "../components";'}
sections={[
{
title: "Variants",
description: "Blue (default) and orange color variants.",
children: (
<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>
),
code: '<ProgressBar progress={60} variant="blue" />',
relevantProps: ["variant"],
},
{
title: "Progress Values",
description: "Various progress levels from 0% to 100%.",
children: (
<div className="vc-compfinder-grid-vertical">
{[0, 25, 50, 75, 100].map(v => (
<div key={v}>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>{v}%:</Paragraph>
<ProgressBar progress={v} variant="blue" />
</div>
))}
</div>
),
relevantProps: ["progress"],
},
{
title: "Animated",
description: "Progress bar that increments automatically.",
children: <AnimatedDemo />,
},
{
title: "Weight",
description: "Light (default) and medium weight variants.",
children: (
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Light (default):</Paragraph>
<ProgressBar progress={60} weight="light" />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Medium:</Paragraph>
<ProgressBar progress={60} weight="medium" />
</div>
</div>
),
code: '<ProgressBar progress={60} weight="medium" />',
relevantProps: ["weight"],
},
{
title: "Glowing",
description: "Glow effect is enabled by default. Set glowing={false} to disable.",
children: (
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Glowing (default):</Paragraph>
<ProgressBar progress={60} glowing={true} />
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>No glow:</Paragraph>
<ProgressBar progress={60} glowing={false} />
</div>
</div>
),
code: "<ProgressBar progress={60} glowing={false} />",
relevantProps: ["glowing"],
},
{
title: "Custom Range",
description: "Using minimum=0 and maximum=200 with progress=150 (75% filled).",
children: <ProgressBar progress={150} minimum={0} maximum={200} variant="orange" />,
code: '<ProgressBar progress={150} minimum={0} maximum={200} variant="orange" />',
relevantProps: ["minimum", "maximum"],
},
{
title: "Custom Override",
description: 'Using variant="unset" with custom gradient colors.',
children: (
<ProgressBar
progress={70}
variant="unset"
override={{
default: {
background: "rgba(88, 101, 242, 0.3)",
gradientStart: "#5865F2",
gradientEnd: "#EB459E",
},
}}
/>
),
code: `<ProgressBar
progress={70}
variant="unset"
override={{
default: {
background: "rgba(88, 101, 242, 0.3)",
gradientStart: "#5865F2",
gradientEnd: "#EB459E",
},
}}
/>`,
relevantProps: ["override"],
},
]}
props={PROGRESSBAR_PROPS}
/>
);
}

View file

@ -1,164 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaBaseRadioGroup, StandaloneRadioIndicator } from "@equicordplugins/components.dev";
import { RadioOption, useState } from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
import { EquicordIcon } from "../icons/EquicordIcon";
const RADIOGROUP_PROPS: PropDef[] = [
{ name: "options", type: "RadioOption[]", required: true, description: "Array of radio options to display." },
{ name: "value", type: "string | number | null", description: "Currently selected value." },
{ name: "onChange", type: "(value: string | number) => void", description: "Called when the selection changes." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable the entire radio group." },
{ name: "orientation", type: '"vertical" | "horizontal"', description: "Layout orientation of the radio items." },
{ name: "radioPosition", type: '"left" | "right"', default: '"left"', description: "Position of the radio indicator relative to the label." },
{ name: "withTransparentBackground", type: "boolean", description: "Use transparent background for radio items." },
{ name: "label", type: "string", description: "Field label text for the group." },
{ name: "description", type: "string", description: "Field description text." },
{ name: "required", type: "boolean", description: "Mark the field as required." },
{ name: "errorMessage", type: "string", description: "Error message displayed below the group." },
{ name: "aria-labelledby", type: "string", description: "ID of an external labelling element for accessibility." },
{ name: "className", type: "string", description: "Custom CSS class name." },
];
const OPTION_PROPS: PropDef[] = [
{ name: "value", type: "string | number", required: true, description: "The value for this option." },
{ name: "name", type: "string", required: true, description: "Display label text." },
{ name: "description", type: "string", description: "Description text shown below the label." },
{ name: "disabled", type: "boolean", description: "Disable this individual option." },
{ name: "icon", type: "ComponentType", description: "Icon component displayed alongside the radio option." },
{ name: "color", type: "string", description: "Custom color for the option highlight." },
];
const STANDALONE_PROPS: PropDef[] = [
{ name: "isSelected", type: "boolean", required: true, description: "Whether the indicator shows as selected." },
{ name: "disabled", type: "boolean", description: "Disable the indicator." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "ManaBaseRadioGroup", props: RADIOGROUP_PROPS },
{ title: "RadioOption", props: OPTION_PROPS },
{ title: "StandaloneRadioIndicator", props: STANDALONE_PROPS },
];
const BASIC_OPTIONS: RadioOption[] = [
{ value: "option1", name: "Option 1" },
{ value: "option2", name: "Option 2" },
{ value: "option3", name: "Option 3" },
];
const DESC_OPTIONS: RadioOption[] = [
{ value: "low", name: "Low Quality", description: "Faster loading, uses less data" },
{ value: "medium", name: "Medium Quality", description: "Balanced quality and performance" },
{ value: "high", name: "High Quality", description: "Best quality, uses more data" },
];
const ICON_OPTIONS: RadioOption[] = [
{ value: "item1", name: "Item with icon", description: "This option has an icon", icon: EquicordIcon },
{ value: "item2", name: "Another with icon", icon: EquicordIcon },
{ value: "item3", name: "Third with icon", icon: EquicordIcon },
];
const MIXED_OPTIONS: RadioOption[] = [
{ value: "enabled", name: "Enabled option" },
{ value: "disabled", name: "Disabled option", disabled: true },
{ value: "another", name: "Another enabled" },
];
function BasicDemo() {
const [value, setValue] = useState<string | number>("option1");
return <ManaBaseRadioGroup options={BASIC_OPTIONS} value={value} onChange={setValue} />;
}
function DescDemo() {
const [value, setValue] = useState<string | number>("medium");
return <ManaBaseRadioGroup options={DESC_OPTIONS} value={value} onChange={setValue} />;
}
function IconDemo() {
const [value, setValue] = useState<string | number>("item1");
return <ManaBaseRadioGroup options={ICON_OPTIONS} value={value} onChange={setValue} />;
}
function DisabledItemDemo() {
const [value, setValue] = useState<string | number>("enabled");
return <ManaBaseRadioGroup options={MIXED_OPTIONS} value={value} onChange={setValue} />;
}
function UnselectedDemo() {
const [value, setValue] = useState<string | number | undefined>(undefined);
return <ManaBaseRadioGroup options={BASIC_OPTIONS} value={value} onChange={setValue} />;
}
function StandaloneDemo() {
const [selected, setSelected] = useState(false);
return (
<div onClick={() => setSelected(!selected)} style={{ cursor: "pointer", display: "inline-block" }}>
<StandaloneRadioIndicator isSelected={selected} />
</div>
);
}
export default function RadioGroupTab() {
return (
<DocPage
componentName="ManaBaseRadioGroup"
overview="ManaBaseRadioGroup renders a group of mutually exclusive radio options. Options support descriptions, leading icons, and individual disabled states. StandaloneRadioIndicator is an animated radio circle for custom radio implementations."
importPath={'import { ManaBaseRadioGroup, StandaloneRadioIndicator, RadioOption } from "../components";'}
sections={[
{
title: "Basic",
description: "Simple radio group with text-only options.",
children: <BasicDemo />,
code: `<ManaBaseRadioGroup
options={[
{ value: "a", name: "Option A" },
{ value: "b", name: "Option B" },
]}
value={value}
onChange={setValue}
/>`,
relevantProps: ["options", "value", "onChange"],
},
{
title: "With Descriptions",
description: "Options with descriptive text using the description field.",
children: <DescDemo />,
},
{
title: "With Leading Icons",
description: "Each option can have an icon component before the radio indicator.",
children: <IconDemo />,
},
{
title: "Disabled Option",
description: "Individual options can be disabled while others remain interactive.",
children: <DisabledItemDemo />,
},
{
title: "No Initial Selection",
description: "Radio group with no option initially selected.",
children: <UnselectedDemo />,
},
{
title: "Entire Group Disabled",
description: "The entire group is non-interactive.",
children: <ManaBaseRadioGroup options={BASIC_OPTIONS} value="option2" disabled />,
relevantProps: ["disabled"],
},
{
title: "Standalone Radio Indicator",
description: "An animated radio circle component for custom radio UIs. Click to toggle.",
children: <StandaloneDemo />,
code: "<StandaloneRadioIndicator isSelected={selected} />",
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,126 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaButton, ManaRichTooltip } from "..";
import { DocPage, type PropDef } from "../DocPage";
const RICHTOOLTIP_PROPS: PropDef[] = [
{ name: "body", type: "string", required: true, description: "Tooltip body text." },
{ name: "children", type: "ReactNode | ((props) => ReactNode)", required: true, description: "Trigger element or render function." },
{ name: "title", type: "string", description: "Bold title text above the body." },
{ name: "position", type: '"top" | "bottom" | "left" | "right"', description: "Position relative to the trigger." },
{ name: "align", type: '"start" | "center" | "end"', description: "Alignment along the position axis." },
{ name: "spacing", type: "number", description: "Distance in pixels from the trigger element." },
{ name: "asContainer", type: "boolean", description: "Wrap children in a container element for hover detection." },
{ name: "element", type: '"span" | "div"', description: "HTML element type for the container when asContainer is true." },
{ name: "asset", type: "ReactNode | { src: string; alt?: string }", description: "Image or element displayed in the tooltip." },
{ name: "assetSize", type: "number", description: "Size of the asset in pixels." },
{ name: "targetElementRef", type: "RefObject<HTMLElement>", description: "Ref to an external trigger element instead of wrapping children." },
{ name: "anchorRef", type: "RefObject<HTMLElement>", description: "Ref to an anchor element for positioning." },
{ name: "positionKey", type: "string", internal: true, description: "Unique key for position caching." },
{ name: "caretConfig", type: '{ align?: "start" | "center" | "end" }', description: "Configuration for the caret arrow." },
{ name: "layerContext", type: "any", internal: true, description: "Layer context for stacking." },
{ name: "ariaHidden", type: "boolean", default: "false", description: "Hide the tooltip from screen readers." },
{ name: "shouldShow", type: "boolean", default: "true", description: "Externally control whether the tooltip is shown." },
{ name: "delay", type: "number", description: "Delay in milliseconds before the tooltip appears." },
{ name: "forceOpen", type: "boolean", default: "false", description: "Force the tooltip to stay open." },
{ name: "overflowOnly", type: "boolean", default: "false", description: "Only show when the trigger text is overflowing." },
{ name: "hideOnClick", type: "boolean", default: "true", description: "Hide when the trigger is clicked." },
{ name: "onTooltipShow", type: "() => void", description: "Callback when the tooltip becomes visible." },
{ name: "onTooltipHide", type: "() => void", description: "Callback when the tooltip is hidden." },
];
export default function RichTooltipTab() {
return (
<DocPage
componentName="ManaRichTooltip"
overview="ManaRichTooltip is Discord's rich tooltip component that displays a title, body text, and optional assets on hover. Unlike ManaTooltip which is text-only, RichTooltip supports structured content with headings and descriptions."
notices={[
{ type: "info", children: "Use asContainer when wrapping components that don't forward refs or hover props. Without it, the tooltip may not detect hover events on the trigger element." },
]}
importPath={'import { ManaRichTooltip } from "../components";'}
sections={[
{
title: "Basic",
description: "Rich tooltip with title and body text. Hover to preview.",
children: (
<div className="vc-compfinder-grid">
<ManaRichTooltip title="Tooltip Title" body="This is the body text with more detailed information." asContainer>
<ManaButton variant="secondary" text="Hover me" />
</ManaRichTooltip>
<ManaRichTooltip body="This tooltip only has body text, no title." asContainer>
<ManaButton variant="secondary" text="Body Only" />
</ManaRichTooltip>
</div>
),
code: `<ManaRichTooltip title="Title" body="Body text here." asContainer>
<Button>Hover me</Button>
</ManaRichTooltip>`,
relevantProps: ["title", "body", "asContainer"],
},
{
title: "Positions",
description: "Tooltip can appear on any side of the trigger element.",
children: (
<div className="vc-compfinder-grid">
{(["top", "bottom", "left", "right"] as const).map(pos => (
<ManaRichTooltip key={pos} title={pos} body={`Positioned at ${pos}`} position={pos} asContainer>
<ManaButton variant="secondary" text={pos} />
</ManaRichTooltip>
))}
</div>
),
relevantProps: ["position"],
},
{
title: "Alignment",
description: "Align the tooltip along the position axis.",
children: (
<div className="vc-compfinder-grid">
{(["start", "center", "end"] as const).map(align => (
<ManaRichTooltip key={align} title={`${align} aligned`} body={`Tooltip aligned to ${align}`} align={align} asContainer>
<ManaButton variant="secondary" text={align} />
</ManaRichTooltip>
))}
</div>
),
relevantProps: ["align"],
},
{
title: "Custom Spacing",
description: "Control the distance between tooltip and trigger.",
children: (
<div className="vc-compfinder-grid">
<ManaRichTooltip title="4px spacing" body="Minimal spacing." spacing={4} asContainer>
<ManaButton variant="secondary" text="4px" />
</ManaRichTooltip>
<ManaRichTooltip title="16px spacing" body="Increased spacing." spacing={16} asContainer>
<ManaButton variant="secondary" text="16px" />
</ManaRichTooltip>
</div>
),
relevantProps: ["spacing"],
},
{
title: "Container Element",
description: "Choose the wrapper element type for the trigger.",
children: (
<div className="vc-compfinder-grid">
<ManaRichTooltip title="Span" body="Uses span container (default)" element="span" asContainer>
<ManaButton variant="secondary" text="Span" />
</ManaRichTooltip>
<ManaRichTooltip title="Div" body="Uses div container" element="div" asContainer>
<ManaButton variant="secondary" text="Div" />
</ManaRichTooltip>
</div>
),
relevantProps: ["element"],
},
]}
props={RICHTOOLTIP_PROPS}
/>
);
}

View file

@ -1,90 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaButton, Paragraph, ScrollerAuto, ScrollerNone, ScrollerThin, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const SCROLLER_PROPS: PropDef[] = [
{ name: "children", type: "ReactNode", required: true, description: "Content to render inside the scrollable area." },
{ name: "orientation", type: '"vertical" | "horizontal"', default: '"vertical"', description: "Scroll direction." },
{ name: "paddingFix", type: "boolean", default: "true", description: "Apply padding fix for scroll containers." },
{ name: "fade", type: "boolean", default: "false", description: "Add a fade effect at the edges to indicate more content." },
{ name: "dir", type: '"ltr" | "rtl"', default: '"ltr"', description: "Text direction for horizontal scrolling." },
{ name: "onScroll", type: "(e: UIEvent) => void", description: "Scroll event handler." },
{ name: "className", type: "string", description: "Custom CSS class." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
];
const SAMPLE_ITEMS = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`);
function ScrollerDemo({ Component, label }: { Component: React.ComponentType<any>; label: string; }) {
const [orientation, setOrientation] = useState<"vertical" | "horizontal">("vertical");
const [fade, setFade] = useState(true);
const scrollerStyle = orientation === "vertical"
? { height: 200, width: "100%", border: "1px solid var(--background-modifier-accent)", borderRadius: 8 }
: { height: 60, width: "100%", border: "1px solid var(--background-modifier-accent)", borderRadius: 8 };
const itemStyle = orientation === "vertical"
? { padding: "8px 12px", borderBottom: "1px solid var(--background-modifier-accent)" }
: { padding: "8px 16px", display: "inline-block", whiteSpace: "nowrap" as const };
return (
<div>
<div className="vc-compfinder-grid" style={{ marginBottom: 8 }}>
<ManaButton
variant="secondary"
text={`Orientation: ${orientation}`}
onClick={() => setOrientation(o => o === "vertical" ? "horizontal" : "vertical")}
/>
<ManaButton
variant="secondary"
text={`Fade: ${fade}`}
onClick={() => setFade(f => !f)}
/>
</div>
<Component orientation={orientation} fade={fade} style={scrollerStyle}>
{SAMPLE_ITEMS.map(item => (
<div key={item} style={itemStyle}>
<Paragraph>{item}</Paragraph>
</div>
))}
</Component>
</div>
);
}
export default function ScrollerTab() {
return (
<DocPage
componentName="Scroller"
overview="Discord provides three scroller variants for different scrollbar styles: ScrollerThin (thin scrollbar, most common), ScrollerAuto (standard auto-showing scrollbar), and ScrollerNone (hidden scrollbar, still scrollable). All support vertical/horizontal orientation and edge fade effects."
importPath={'import { ScrollerThin, ScrollerAuto, ScrollerNone } from "../components";'}
sections={[
{
title: "ScrollerThin",
description: "Thin scrollbar, the most commonly used variant. Ideal for sidebars and lists.",
children: <ScrollerDemo Component={ScrollerThin} label="ScrollerThin" />,
code: `<ScrollerThin orientation="vertical" fade style={{ height: 200 }}>
{items.map(item => <div key={item.id}>{item.label}</div>)}
</ScrollerThin>`,
relevantProps: ["orientation", "fade"],
},
{
title: "ScrollerAuto",
description: "Standard scrollbar that appears automatically when content overflows.",
children: <ScrollerDemo Component={ScrollerAuto} label="ScrollerAuto" />,
},
{
title: "ScrollerNone",
description: "No visible scrollbar. Content is still scrollable via mouse wheel or touch.",
children: <ScrollerDemo Component={ScrollerNone} label="ScrollerNone" />,
},
]}
props={SCROLLER_PROPS}
/>
);
}

View file

@ -1,94 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { SearchBar, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const SEARCHBAR_PROPS: PropDef[] = [
{ name: "query", type: "string", required: true, description: "Current search query value." },
{ name: "onChange", type: "(value: string) => void", required: true, description: "Called when the query text changes." },
{ name: "onClear", type: "() => void", description: "Called when the clear button is clicked." },
{ name: "placeholder", type: "string", description: "Placeholder text when empty." },
{ name: "size", type: '"sm" | "md"', description: "Input size variant." },
{ name: "autoFocus", type: "boolean", description: "Focus the input on mount." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable the search bar." },
{ name: "onKeyDown", type: "(e: KeyboardEvent) => void", description: "Keyboard event handler." },
{ name: "onBlur", type: "() => void", description: "Called when the input loses focus." },
{ name: "onFocus", type: "() => void", description: "Called when the input gains focus." },
{ name: "autoComplete", type: "string", description: "HTML autocomplete attribute value." },
{ name: "inputProps", type: "Record<string, any>", description: "Additional props passed to the underlying input element." },
{ name: "className", type: "string", description: "Custom CSS class." },
{ name: "aria-label", type: "string", description: "Accessibility label." },
];
function BasicDemo() {
const [query, setQuery] = useState("");
return (
<SearchBar
query={query}
onChange={setQuery}
onClear={() => setQuery("")}
placeholder="Search..."
/>
);
}
function SizesDemo() {
const [query, setQuery] = useState("");
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<SearchBar query={query} onChange={setQuery} onClear={() => setQuery("")} placeholder="Small size" size="sm" />
<SearchBar query={query} onChange={setQuery} onClear={() => setQuery("")} placeholder="Medium size (default)" size="md" />
</div>
);
}
function StatesDemo() {
const [prefilled, setPrefilled] = useState("prefilled");
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<SearchBar query="" onChange={() => { }} placeholder="Disabled" disabled />
<SearchBar query={prefilled} onChange={setPrefilled} onClear={() => setPrefilled("")} placeholder="Prefilled" />
</div>
);
}
export default function SearchBarTab() {
return (
<DocPage
componentName="SearchBar"
overview="SearchBar is Discord's search input component with a built-in search icon, clearable button, and two size variants. Used throughout Discord for filtering and searching content."
importPath={'import { SearchBar } from "../components";'}
sections={[
{
title: "Basic",
description: "Default search bar with clear button.",
children: <BasicDemo />,
code: `<SearchBar
query={query}
onChange={setQuery}
onClear={() => setQuery("")}
placeholder="Search..."
/>`,
relevantProps: ["query", "onChange", "onClear", "placeholder"],
},
{
title: "Sizes",
description: "Available in sm and md (default) sizes.",
children: <SizesDemo />,
relevantProps: ["size"],
},
{
title: "States",
description: "Disabled and prefilled states.",
children: <StatesDemo />,
relevantProps: ["disabled"],
},
]}
props={SEARCHBAR_PROPS}
/>
);
}

View file

@ -1,155 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaSelect, ManaSelectOption, useState } from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
const SELECT_PROPS: PropDef[] = [
{ name: "options", type: "ManaSelectOption[]", required: true, description: "Array of selectable options." },
{ name: "value", type: "string | string[] | null", description: "Currently selected value(s)." },
{ name: "onSelectionChange", type: "(value: string | string[] | null) => void", description: "Called when the selection changes. Note: NOT onChange." },
{ name: "selectionMode", type: '"single" | "multiple"', description: "Single or multi-select mode." },
{ name: "placeholder", type: "string", description: "Placeholder text when nothing is selected." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable the select." },
{ name: "readOnly", type: "boolean", description: "Make the select read-only." },
{ name: "clearable", type: "boolean", description: "Allow clearing the selection." },
{ name: "fullWidth", type: "boolean", description: "Expand to fill available width." },
{ name: "autoFocus", type: "boolean", description: "Focus on mount." },
{ name: "closeOnSelect", type: "boolean", description: "Close the dropdown after selecting." },
{ name: "shouldFocusWrap", type: "boolean", description: "Wrap keyboard focus at list ends." },
{ name: "maxOptionsVisible", type: "number", description: "Maximum options visible before scrolling." },
{ name: "wrapTags", type: "boolean", description: "Wrap selected tags to multiple lines in multi-select." },
{ name: "formatOption", type: "(option: ManaSelectOption) => ReactNode", description: "Custom render for each option." },
{ name: "label", type: "string", description: "Label text for the select." },
{ name: "required", type: "boolean", description: "Mark as required for form validation." },
];
const OPTION_PROPS: PropDef[] = [
{ name: "id", type: "string", required: true, description: "Unique option identifier." },
{ name: "value", type: "string", required: true, description: "Option value." },
{ name: "label", type: "string", required: true, description: "Display label." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "ManaSelect", props: SELECT_PROPS },
{ title: "ManaSelectOption", props: OPTION_PROPS },
];
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" },
];
function SingleDemo() {
const [value, setValue] = useState<string>("option1");
return (
<ManaSelect
options={SAMPLE_OPTIONS}
value={value}
onSelectionChange={v => setValue(v as string)}
selectionMode="single"
/>
);
}
function MultiDemo() {
const [value, setValue] = useState<string[]>(["option1", "option2"]);
return (
<ManaSelect
options={SAMPLE_OPTIONS}
value={value}
onSelectionChange={v => setValue(v as string[])}
selectionMode="multiple"
/>
);
}
function PlaceholderDemo() {
const [value, setValue] = useState<string>("");
return (
<ManaSelect
options={SAMPLE_OPTIONS}
value={value}
onSelectionChange={v => setValue(v as string)}
placeholder="Select an option..."
/>
);
}
function ClearableDemo() {
const [value, setValue] = useState<string>("option1");
return (
<ManaSelect
options={SAMPLE_OPTIONS}
value={value}
onSelectionChange={v => setValue(v as string)}
clearable
/>
);
}
export default function SelectTab() {
return (
<DocPage
componentName="ManaSelect"
overview="ManaSelect is Discord's dropdown select component supporting single and multi-select modes. Important: uses onSelectionChange (not onChange) for the selection callback. Supports clearable selection, placeholder text, full width, and custom option rendering."
notices={[
{ type: "warn", children: "ManaSelect uses onSelectionChange, not onChange. This is a common source of confusion since most other Discord form components use onChange." },
]}
importPath={'import { ManaSelect, ManaSelectOption } from "../components";'}
sections={[
{
title: "Single Selection",
description: "Default single-select dropdown.",
children: <SingleDemo />,
code: `<ManaSelect
options={options}
value={value}
onSelectionChange={v => setValue(v as string)}
selectionMode="single"
/>`,
relevantProps: ["options", "value", "onSelectionChange", "selectionMode"],
},
{
title: "Multiple Selection",
description: "Multi-select with tag pills for each selected option.",
children: <MultiDemo />,
},
{
title: "With Placeholder",
description: "Shows placeholder text when no option is selected.",
children: <PlaceholderDemo />,
relevantProps: ["placeholder"],
},
{
title: "Clearable",
description: "Adds a clear button to reset the selection.",
children: <ClearableDemo />,
relevantProps: ["clearable"],
},
{
title: "Full Width",
description: "Expands to fill the available width.",
children: (
<ManaSelect options={SAMPLE_OPTIONS} value="option1" fullWidth />
),
relevantProps: ["fullWidth"],
},
{
title: "Disabled",
description: "Non-interactive disabled state.",
children: (
<ManaSelect options={SAMPLE_OPTIONS} value="option2" disabled />
),
relevantProps: ["disabled"],
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,69 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Skeleton } from "@equicordplugins/components.dev";
import { ManaButton, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const SKELETON_PROPS: PropDef[] = [
{ name: "withHeader", type: "boolean", default: "true", description: "Show a header placeholder row." },
{ name: "size", type: "number", default: "15", description: "Number of skeleton rows to display." },
];
function SkeletonDemo() {
const [withHeader, setWithHeader] = useState(true);
const [size, setSize] = useState(5);
return (
<div>
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
<ManaButton variant="secondary" text={withHeader ? "Hide Header" : "Show Header"} onClick={() => setWithHeader(!withHeader)} />
<ManaButton variant="secondary" text="Fewer Items" onClick={() => setSize(Math.max(1, size - 1))} />
<ManaButton variant="secondary" text="More Items" onClick={() => setSize(Math.min(20, size + 1))} />
</div>
<div className="full-motion">
<Skeleton withHeader={withHeader} size={size} />
</div>
</div>
);
}
export default function SkeletonTab() {
return (
<DocPage
componentName="Skeleton"
overview="Skeleton displays a loading placeholder with shimmer animation. Used in Discord's member list and other loading states. Renders rows of animated placeholder content with an optional header."
importPath={'import { Skeleton } from "../components";'}
sections={[
{
title: "Interactive Demo",
description: "Toggle the header and adjust the number of skeleton rows.",
children: <SkeletonDemo />,
code: "<Skeleton withHeader={true} size={5} />",
relevantProps: ["withHeader", "size"],
},
{
title: "Header Only",
children: (
<div className="full-motion">
<Skeleton withHeader={true} size={0} />
</div>
),
},
{
title: "No Header",
children: (
<div className="full-motion">
<Skeleton withHeader={false} size={3} />
</div>
),
},
]}
props={SKELETON_PROPS}
/>
);
}

View file

@ -1,148 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph, Slider, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const SLIDER_PROPS: PropDef[] = [
{ name: "initialValue", type: "number", description: "Initial slider value (uncontrolled)." },
{ name: "value", type: "number", description: "Current slider value (controlled)." },
{ name: "minValue", type: "number", description: "Minimum range value." },
{ name: "maxValue", type: "number", description: "Maximum range value." },
{ name: "onValueChange", type: "(value: number) => void", description: "Called when the value changes (on release)." },
{ name: "asValueChanges", type: "(value: number) => void", description: "Called continuously as the value changes (while dragging)." },
{ name: "onValueRender", type: "(value: number) => string | null", description: "Custom tooltip label for the current value." },
{ name: "markers", type: "number[]", description: "Array of values to show as tick marks." },
{ name: "stickToMarkers", type: "boolean", description: "Snap the slider value to the nearest marker." },
{ name: "equidistant", type: "boolean", description: "Space markers equally regardless of value." },
{ name: "onMarkerRender", type: "(value: number) => string | ReactNode", description: "Custom label for each marker tick." },
{ name: "keyboardStep", type: "number", description: "Step size for keyboard arrow key increments." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable interaction." },
{ name: "mini", type: "boolean", description: "Use compact visual style." },
{ name: "hideBubble", type: "boolean", description: "Hide the value tooltip bubble." },
{ name: "orientation", type: '"horizontal" | "vertical"', default: '"horizontal"', description: "Slider orientation." },
{ name: "className", type: "string", description: "Custom CSS class." },
];
function BasicDemo() {
const [value, setValue] = useState(50);
return (
<div>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Value: {value}</Paragraph>
<Slider initialValue={value} minValue={0} maxValue={100} onValueChange={setValue} />
</div>
);
}
function MarkersDemo() {
const [value, setValue] = useState(50);
return (
<div>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Value: {value}</Paragraph>
<Slider
initialValue={value}
minValue={0}
maxValue={100}
markers={[0, 25, 50, 75, 100]}
onValueChange={setValue}
onMarkerRender={(v: number) => `${v}%`}
/>
</div>
);
}
function MiniDemo() {
const [value, setValue] = useState(30);
return (
<div>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Value: {value}</Paragraph>
<div style={{ width: 200 }}>
<Slider initialValue={value} minValue={0} maxValue={100} mini onValueChange={setValue} />
</div>
</div>
);
}
export default function SliderTab() {
return (
<DocPage
componentName="Slider"
overview="Slider is Discord's range input component with support for markers, snap-to-marker behavior, mini mode, value tooltips, and custom rendering. Supports both controlled and uncontrolled usage."
notices={[
{ type: "info", children: "Slider has two change callbacks: onValueChange fires on release (final value), while asValueChanges fires continuously during dragging. Use onValueChange for state updates to avoid excessive re-renders." },
]}
importPath={'import { Slider } from "../components";'}
sections={[
{
title: "Basic",
description: "Simple slider with min/max range.",
children: <BasicDemo />,
code: `<Slider
initialValue={50}
minValue={0}
maxValue={100}
onValueChange={setValue}
/>`,
relevantProps: ["initialValue", "minValue", "maxValue", "onValueChange"],
},
{
title: "With Markers",
description: "Tick marks at specified values with custom labels.",
children: <MarkersDemo />,
code: `<Slider
initialValue={50}
minValue={0}
maxValue={100}
markers={[0, 25, 50, 75, 100]}
onMarkerRender={v => \`\${v}%\`}
/>`,
relevantProps: ["markers", "onMarkerRender"],
},
{
title: "Stick to Markers",
description: "Value snaps to the nearest marker position.",
children: (
<Slider
initialValue={50}
minValue={0}
maxValue={100}
markers={[0, 25, 50, 75, 100]}
stickToMarkers
onMarkerRender={(v: number) => `${v}%`}
/>
),
relevantProps: ["stickToMarkers"],
},
{
title: "Mini",
description: "Compact visual style for tight layouts.",
children: <MiniDemo />,
relevantProps: ["mini"],
},
{
title: "Disabled",
description: "Non-interactive disabled state.",
children: <Slider initialValue={60} minValue={0} maxValue={100} disabled />,
relevantProps: ["disabled"],
},
{
title: "Custom Range",
description: "Slider with negative to positive range.",
children: (
<Slider
initialValue={0}
minValue={-50}
maxValue={50}
markers={[-50, -25, 0, 25, 50]}
onMarkerRender={(v: number) => v.toString()}
/>
),
},
]}
props={SLIDER_PROPS}
/>
);
}

View file

@ -1,85 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaButton, Paragraph, Spinner, SpinnerTypes, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const SPINNER_PROPS: PropDef[] = [
{ name: "type", type: "SpinnerType", description: 'Spinner variant. One of: "wanderingCubes", "chasingDots", "pulsingEllipsis", "spinningCircle", "spinningCircleSimple", "lowMotion".' },
{ name: "animated", type: "boolean", default: "true", description: "Enable or pause the animation." },
{ name: "className", type: "string", description: "CSS class for the container." },
{ name: "itemClassName", type: "string", description: "CSS class for inner animation items." },
{ name: "aria-label", type: "string", description: "Accessibility label." },
];
function AnimationToggleDemo() {
const [animated, setAnimated] = useState(true);
return (
<div>
<ManaButton
variant="secondary"
text={animated ? "Pause Animation" : "Resume Animation"}
onClick={() => setAnimated(!animated)}
style={{ marginBottom: 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>
);
}
export default function SpinnerTab() {
return (
<DocPage
componentName="Spinner"
overview="Spinner provides animated loading indicators in six visual styles. Automatically switches to the lowMotion variant when the user has reduced motion enabled. The animation can be paused via the animated prop."
notices={[
{ type: "positive", children: "Spinner automatically respects the user's reduced motion setting by switching to the lowMotion variant. You don't need to handle this manually." },
]}
importPath={'import { Spinner, SpinnerTypes } from "../components";'}
sections={[
{
title: "All Types",
description: "All six spinner variants.",
children: (
<div className="vc-compfinder-grid">
{SpinnerTypes.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>
),
code: '<Spinner type="spinningCircle" />',
relevantProps: ["type"],
},
{
title: "Animation Toggle",
description: "Spinners can be paused with animated=false.",
children: <AnimationToggleDemo />,
relevantProps: ["animated"],
},
{
title: "Low Motion",
description: "The lowMotion type is used when the user prefers reduced motion. The component automatically switches to this variant.",
children: (
<div style={{ textAlign: "center", padding: 16 }}>
<Spinner type="lowMotion" />
</div>
),
},
]}
props={SPINNER_PROPS}
/>
);
}

View file

@ -1,151 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaSwitch } from "@equicordplugins/components.dev";
import { findComponentByCodeLazy } from "@webpack";
import { useState } from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
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>;
const SWITCH_PROPS: PropDef[] = [
{ name: "checked", type: "boolean", required: true, description: "Current toggle state." },
{ name: "onChange", type: "(checked: boolean) => void", description: "Called when the switch is toggled." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable interaction." },
{ name: "id", type: "string", description: "HTML id attribute." },
{ name: "hasIcon", type: "boolean", description: "Show a checkmark/x icon inside the toggle." },
{ name: "describedBy", type: "string", internal: true, description: "ID of an element describing this switch." },
{ name: "labelledBy", type: "string", internal: true, description: "ID of an element labelling this switch." },
{ name: "innerRef", type: "Ref<HTMLInputElement>", internal: true, description: "Ref to the underlying input element." },
];
const SWITCH_WITH_LABEL_PROPS: PropDef[] = [
{ name: "checked", type: "boolean", required: true, description: "Current toggle state." },
{ name: "onChange", type: "(checked: boolean) => void", description: "Called when the switch is toggled." },
{ name: "label", type: "string", description: "Label text displayed next to the switch." },
{ name: "description", type: "string", description: "Description text below the label." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable interaction." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "ManaSwitch", props: SWITCH_PROPS },
{ title: "SwitchWithLabel", props: SWITCH_WITH_LABEL_PROPS },
];
function BasicDemo() {
const [on, setOn] = useState(true);
const [off, setOff] = useState(false);
return (
<div className="vc-compfinder-grid">
<div className="vc-compfinder-switch-item">
<span>On:</span>
<ManaSwitch checked={on} onChange={setOn} />
</div>
<div className="vc-compfinder-switch-item">
<span>Off:</span>
<ManaSwitch checked={off} onChange={setOff} />
</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>
);
}
function LabelDemo() {
const [on, setOn] = useState(true);
const [off, setOff] = useState(false);
return (
<div className="vc-compfinder-grid-vertical">
<SwitchWithLabel label="Enabled option" checked={on} onChange={setOn} />
<SwitchWithLabel label="Disabled option" checked={off} onChange={setOff} />
</div>
);
}
function DescriptionDemo() {
const [checked, setChecked] = useState(true);
return (
<SwitchWithLabel
label="Feature toggle"
description="Enable this feature to unlock additional functionality."
checked={checked}
onChange={setChecked}
/>
);
}
export default function SwitchTab() {
return (
<DocPage
componentName="ManaSwitch"
overview="Discord provides two switch components: ManaSwitch is a bare toggle without label (used inline), and SwitchWithLabel wraps it with label and description text for settings-style layouts."
notices={[
{ type: "info", children: "SwitchWithLabel is found via findComponentByCodeLazy, not directly exported. Use it for settings layouts, and ManaSwitch for inline toggle scenarios." },
]}
importPath={`import { ManaSwitch } from "../components";
import { findComponentByCodeLazy } from "@webpack";
const SwitchWithLabel = findComponentByCodeLazy('"data-toggleable-component":"switch"');`}
sections={[
{
title: "Basic Switch",
description: "ManaSwitch in on, off, and disabled states.",
children: <BasicDemo />,
code: "<ManaSwitch checked={enabled} onChange={setEnabled} />",
relevantProps: ["checked", "onChange", "disabled"],
},
{
title: "Switch with Label",
description: "SwitchWithLabel renders a label next to the toggle.",
children: <LabelDemo />,
code: `<SwitchWithLabel
label="Notifications"
checked={enabled}
onChange={setEnabled}
/>`,
},
{
title: "With Description",
description: "Label with description text below.",
children: <DescriptionDemo />,
code: `<SwitchWithLabel
label="Feature toggle"
description="Enable this to unlock additional functionality."
checked={checked}
onChange={setChecked}
/>`,
},
{
title: "Disabled States",
description: "Both checked and unchecked disabled states.",
children: (
<div className="vc-compfinder-grid-vertical">
<SwitchWithLabel label="Disabled (checked)" checked={true} disabled />
<SwitchWithLabel label="Disabled (unchecked)" checked={false} disabled />
</div>
),
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,179 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph, TabBar, useState } from "..";
import { DocPage, type PropDef, type PropGroup } from "../DocPage";
const TABBAR_PROPS: PropDef[] = [
{ name: "type", type: '"side" | "top" | "top-pill"', description: "Tab bar visual style." },
{ name: "look", type: '"brand" | "grey"', description: "Color scheme." },
{ name: "selectedItem", type: "string", description: "ID of the currently selected tab." },
{ name: "onItemSelect", type: "(id: string) => void", description: "Called when a tab is selected." },
{ name: "orientation", type: '"horizontal" | "vertical"', description: "Layout direction." },
{ name: "className", type: "string", description: "Custom CSS class." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
{ name: "aria-label", type: "string", description: "Accessibility label." },
];
const ITEM_PROPS: PropDef[] = [
{ name: "id", type: "string", required: true, description: "Unique tab identifier." },
{ name: "children", type: "ReactNode", required: true, description: "Tab label content." },
{ name: "disabled", type: "boolean", description: "Disable the tab." },
{ name: "color", type: "string", description: "Custom tab color (hex or CSS color)." },
{ name: "variant", type: '"destructive"', description: "Danger styling for actions like logout." },
{ name: "disableItemStyles", type: "boolean", description: "Remove default tab styles." },
{ name: "onClick", type: "MouseEventHandler", description: "Custom click handler." },
];
const PROP_GROUPS: PropGroup[] = [
{ title: "TabBar", props: TABBAR_PROPS },
{ title: "TabBar.Item", props: ITEM_PROPS },
];
function SideDemo() {
const [tab, setTab] = useState("tab1");
return (
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Selected: {tab}</Paragraph>
<TabBar type="side" look="grey" selectedItem={tab} onItemSelect={setTab}>
<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>
</div>
);
}
function TopDemo() {
const [tab, setTab] = useState("tab1");
return (
<TabBar type="top" look="grey" selectedItem={tab} onItemSelect={setTab}>
<TabBar.Item id="tab1">Overview</TabBar.Item>
<TabBar.Item id="tab2">Settings</TabBar.Item>
<TabBar.Item id="tab3">Members</TabBar.Item>
</TabBar>
);
}
function TopPillDemo() {
const [tab, setTab] = useState("tab1");
return (
<TabBar type="top-pill" look="grey" selectedItem={tab} onItemSelect={setTab}>
<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>
);
}
function BrandDemo() {
const [tab, setTab] = useState("tab1");
return (
<TabBar type="top" look="brand" selectedItem={tab} onItemSelect={setTab}>
<TabBar.Item id="tab1">Home</TabBar.Item>
<TabBar.Item id="tab2">Explore</TabBar.Item>
<TabBar.Item id="tab3">Library</TabBar.Item>
</TabBar>
);
}
function HeaderSeparatorDemo() {
const [tab, setTab] = useState("tab1");
return (
<TabBar type="side" look="grey" selectedItem={tab} onItemSelect={setTab}>
<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>
);
}
function FeaturesDemo() {
const [tab, setTab] = useState("tab1");
const [colorTab, setColorTab] = useState("tab1");
return (
<div className="vc-compfinder-grid-vertical">
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Disabled items:</Paragraph>
<TabBar type="top" look="grey" selectedItem={tab} onItemSelect={setTab}>
<TabBar.Item id="tab1">Active</TabBar.Item>
<TabBar.Item id="tab2" disabled>Disabled</TabBar.Item>
<TabBar.Item id="tab3">Another Active</TabBar.Item>
</TabBar>
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Custom colors:</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>
</div>
<div>
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Destructive variant:</Paragraph>
<TabBar type="side" look="grey" selectedItem={tab} onItemSelect={setTab}>
<TabBar.Item id="tab1">Settings</TabBar.Item>
<TabBar.Item id="logout" variant="destructive">Log Out</TabBar.Item>
</TabBar>
</div>
</div>
);
}
export default function TabBarTab() {
return (
<DocPage
componentName="TabBar"
overview="TabBar provides navigation tabs in three styles: side (vertical sidebar), top (horizontal with underline), and top-pill (horizontal pill buttons). Supports brand/grey color schemes, disabled items, custom colors, destructive variants, and section headers with separators."
importPath={'import { TabBar } from "../components";'}
sections={[
{
title: "Side (Default)",
description: "Vertical sidebar-style tabs with grey look.",
children: <SideDemo />,
code: `<TabBar type="side" look="grey" selectedItem={tab} onItemSelect={setTab}>
<TabBar.Item id="tab1">First Tab</TabBar.Item>
<TabBar.Item id="tab2">Second Tab</TabBar.Item>
</TabBar>`,
relevantProps: ["type", "look", "selectedItem", "onItemSelect"],
},
{
title: "Top",
description: "Horizontal tabs with underline indicator.",
children: <TopDemo />,
},
{
title: "Top Pill",
description: "Pill-style horizontal tabs.",
children: <TopPillDemo />,
},
{
title: "Brand Look",
description: "Brand-colored tab styling.",
children: <BrandDemo />,
},
{
title: "Headers and Separators",
description: "TabBar.Header and TabBar.Separator for organizing groups of tabs.",
children: <HeaderSeparatorDemo />,
},
{
title: "Disabled, Colors, Destructive",
description: "Items can be disabled, colored, or styled as destructive.",
children: <FeaturesDemo />,
relevantProps: ["disabled", "color", "variant"],
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,100 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaTextArea, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const TEXTAREA_PROPS: PropDef[] = [
{ name: "value", type: "string", description: "Current textarea value (controlled)." },
{ name: "defaultValue", type: "string", description: "Initial value (uncontrolled)." },
{ name: "onChange", type: "(value: string, name?: string) => void", description: "Called when the text changes." },
{ name: "placeholder", type: "string", description: "Placeholder text when empty." },
{ name: "name", type: "string", description: "HTML name attribute for form submission." },
{ name: "rows", type: "number", description: "Fixed number of visible text rows." },
{ name: "autosize", type: "boolean", description: "Automatically grow height to fit content." },
{ name: "autoFocus", type: "boolean", description: "Focus on mount." },
{ name: "maxLength", type: "number", description: "Maximum character limit." },
{ name: "minLength", type: "number", description: "Minimum character requirement." },
{ name: "showCharacterCount", type: "boolean", description: "Display current character count." },
{ name: "showRemainingCharacterCount", type: "boolean", description: "Display remaining characters instead of current count." },
{ name: "error", type: "string | boolean", description: "Error state. String shows error message, boolean shows red border." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable the textarea." },
{ name: "readOnly", type: "boolean", description: "Make the textarea read-only." },
{ name: "fullWidth", type: "boolean", description: "Expand to full available width." },
{ name: "inputRef", type: "Ref<HTMLTextAreaElement>", internal: true, description: "Ref to the underlying textarea element." },
];
function BasicDemo() {
const [value, setValue] = useState("Hello world\nThis is a textarea");
return <ManaTextArea value={value} onChange={setValue} />;
}
function AutosizeDemo() {
const [value, setValue] = useState("This textarea will grow as you type more content...");
return <ManaTextArea value={value} onChange={setValue} autosize />;
}
function CharCountDemo() {
const [value, setValue] = useState("Count characters");
return <ManaTextArea value={value} onChange={setValue} showCharacterCount maxLength={200} />;
}
export default function TextAreaTab() {
return (
<DocPage
overview="ManaTextArea is Discord's multi-line text input with support for autosize, character counting, fixed row count, error states, and disabled/read-only modes."
importPath={'import { ManaTextArea } from "../components";'}
sections={[
{
title: "Basic",
description: "Default textarea with controlled value.",
children: <BasicDemo />,
code: "<ManaTextArea value={value} onChange={setValue} />",
relevantProps: ["value", "onChange"],
},
{
title: "With Placeholder",
children: (
<ManaTextArea value="" onChange={() => { }} placeholder="Enter your message..." />
),
relevantProps: ["placeholder"],
},
{
title: "Autosize",
description: "Automatically grows to fit content as you type.",
children: <AutosizeDemo />,
relevantProps: ["autosize"],
},
{
title: "Fixed Rows",
description: "Fixed height with 5 visible rows.",
children: (
<ManaTextArea value="Fixed 5 rows" onChange={() => { }} rows={5} />
),
relevantProps: ["rows"],
},
{
title: "Character Count",
description: "Shows character count with a max length of 200.",
children: <CharCountDemo />,
relevantProps: ["showCharacterCount", "maxLength"],
},
{
title: "States",
description: "Disabled and read-only states.",
children: (
<div className="vc-compfinder-grid-vertical">
<ManaTextArea value="Disabled textarea" disabled />
<ManaTextArea value="Read only textarea" readOnly />
</div>
),
relevantProps: ["disabled", "readOnly"],
},
]}
props={TEXTAREA_PROPS}
/>
);
}

View file

@ -1,80 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaTextButton, ManaTextButtonTextVariants, ManaTextButtonVariants } from "..";
import { DocPage, type PropDef } from "../DocPage";
const TEXTBUTTON_PROPS: PropDef[] = [
{ name: "text", type: "string", required: true, description: "Button label text." },
{ name: "variant", type: '"primary" | "secondary" | "always-white" | "critical"', description: "Color variant." },
{ name: "textVariant", type: "string", description: 'Typography variant, e.g. "text-sm/medium", "text-lg/medium".' },
{ name: "lineClamp", type: "number", description: "Max lines before truncation with ellipsis." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable the button." },
{ name: "onClick", type: "(e: MouseEvent) => void", description: "Click handler." },
{ name: "role", type: "string", description: "ARIA role attribute." },
{ name: "type", type: '"button" | "submit" | "reset"', description: "HTML button type." },
{ name: "buttonRef", type: "Ref<HTMLButtonElement>", internal: true, description: "Ref to the underlying button element." },
{ name: "className", type: "string", description: "Custom CSS class." },
{ name: "style", type: "CSSProperties", description: "Inline styles." },
];
export default function TextButtonTab() {
return (
<DocPage
overview="ManaTextButton is Discord's text-only button component (no background or border). Supports four color variants, typography sizing via textVariant, line clamping, and disabled state."
importPath={'import { ManaTextButton, ManaTextButtonVariants, ManaTextButtonTextVariants } from "../components";'}
sections={[
{
title: "Variants",
description: "All four color variants.",
children: (
<div className="vc-compfinder-grid">
{ManaTextButtonVariants.map(variant => (
<ManaTextButton key={variant} text={variant} variant={variant} />
))}
</div>
),
code: '<ManaTextButton text="Click me" variant="primary" />',
relevantProps: ["variant"],
},
{
title: "Text Variants",
description: "Different typography sizes and weights.",
children: (
<div className="vc-compfinder-grid-vertical">
{ManaTextButtonTextVariants.map(textVariant => (
<ManaTextButton key={textVariant} text={textVariant} textVariant={textVariant} />
))}
</div>
),
relevantProps: ["textVariant"],
},
{
title: "States",
description: "Normal and disabled states.",
children: (
<div className="vc-compfinder-grid">
<ManaTextButton text="Normal" />
<ManaTextButton text="Disabled" disabled />
</div>
),
relevantProps: ["disabled"],
},
{
title: "Line Clamp",
description: "Truncate long text to a single line.",
children: (
<div style={{ maxWidth: 200 }}>
<ManaTextButton text="This is a very long text that should be clamped to one line" lineClamp={1} />
</div>
),
relevantProps: ["lineClamp"],
},
]}
props={TEXTBUTTON_PROPS}
/>
);
}

View file

@ -1,121 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaTextInput, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const TEXTINPUT_PROPS: PropDef[] = [
{ name: "value", type: "string", description: "Current input value (controlled)." },
{ name: "defaultValue", type: "string", description: "Initial value (uncontrolled)." },
{ name: "onChange", type: "(value: string, name?: string) => void", description: "Called when the input value changes." },
{ name: "placeholder", type: "string", description: "Placeholder text when empty." },
{ name: "name", type: "string", description: "HTML name attribute for form submission." },
{ name: "type", type: '"text" | "password" | "email" | "number" | "search" | "tel" | "url"', default: '"text"', description: "Input type." },
{ name: "size", type: '"sm" | "md"', default: '"md"', description: "Input size variant." },
{ name: "clearable", type: "boolean | { show: boolean }", description: "Show a clear button when the input has a value." },
{ name: "showCharacterCount", type: "boolean", description: "Display current character count." },
{ name: "maxLength", type: "number", description: "Maximum character limit." },
{ name: "minLength", type: "number", description: "Minimum character requirement." },
{ name: "error", type: "string | boolean", description: "Error state. String shows error message, boolean shows red border." },
{ name: "disabled", type: "boolean", default: "false", description: "Disable the input." },
{ name: "readOnly", type: "boolean", description: "Make the input read-only." },
{ name: "fullWidth", type: "boolean", description: "Expand to full available width." },
{ name: "leading", type: "TextInputLeadingAccessory", description: "Leading accessory (icon, button, tags, or image)." },
{ name: "trailing", type: "TextInputTrailingAccessory", description: "Trailing accessory (icon or button)." },
{ name: "inputRef", type: "Ref<HTMLInputElement>", internal: true, description: "Ref to the underlying input element." },
];
function BasicDemo() {
const [value, setValue] = useState("Hello world");
return <ManaTextInput value={value} onChange={setValue} />;
}
function SizesDemo() {
const [sm, setSm] = useState("Small");
const [md, setMd] = useState("Medium");
return (
<div className="vc-compfinder-grid-vertical">
<ManaTextInput value={sm} onChange={setSm} size="sm" placeholder="Size: sm" />
<ManaTextInput value={md} onChange={setMd} size="md" placeholder="Size: md" />
</div>
);
}
function ClearableDemo() {
const [value, setValue] = useState("Clear me");
return <ManaTextInput value={value} onChange={setValue} clearable />;
}
function CharCountDemo() {
const [value, setValue] = useState("Count chars");
return <ManaTextInput value={value} onChange={setValue} showCharacterCount maxLength={50} />;
}
function ErrorDemo() {
const [value, setValue] = useState("Invalid input");
return <ManaTextInput value={value} onChange={setValue} error />;
}
export default function TextInputTab() {
return (
<DocPage
overview="ManaTextInput is Discord's single-line text input with support for two sizes, clearable button, character counting, error states, leading/trailing accessories, and disabled/read-only modes."
importPath={'import { ManaTextInput } from "../components";'}
sections={[
{
title: "Basic",
description: "Default text input with controlled value.",
children: <BasicDemo />,
code: "<ManaTextInput value={value} onChange={setValue} />",
relevantProps: ["value", "onChange"],
},
{
title: "Sizes",
description: "Available in sm and md (default) sizes.",
children: <SizesDemo />,
relevantProps: ["size"],
},
{
title: "With Placeholder",
children: (
<ManaTextInput value="" onChange={() => { }} placeholder="Enter text here..." />
),
relevantProps: ["placeholder"],
},
{
title: "Clearable",
description: "Shows a clear button when the input has a value.",
children: <ClearableDemo />,
relevantProps: ["clearable"],
},
{
title: "Character Count",
description: "Displays character count with a 50-character max.",
children: <CharCountDemo />,
relevantProps: ["showCharacterCount", "maxLength"],
},
{
title: "Error State",
description: "Red border indicating invalid input.",
children: <ErrorDemo />,
relevantProps: ["error"],
},
{
title: "States",
description: "Disabled and read-only states.",
children: (
<div className="vc-compfinder-grid-vertical">
<ManaTextInput value="Disabled" disabled />
<ManaTextInput value="Read only" readOnly />
</div>
),
relevantProps: ["disabled", "readOnly"],
},
]}
props={TEXTINPUT_PROPS}
/>
);
}

View file

@ -1,158 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph, Timestamp, TimestampDisplayFormats, TooltipPositions } from "..";
import { DocPage, type PropDef } from "../DocPage";
const TIMESTAMP_PROPS: PropDef[] = [
{ name: "timestamp", type: "Date | Moment", required: true, description: "The date or moment object to display." },
{ name: "timestampFormat", type: "TimestampDisplayFormat", description: 'Moment.js format string. One of: "LT", "LTS", "L", "LL", "LLL", "LLLL", "l", "ll", "lll", "llll".' },
{ name: "compact", type: "boolean", default: "false", description: "Use a shorter display style." },
{ name: "cozyAlt", type: "boolean", default: "false", description: "Use the cozy alternative layout." },
{ name: "isInline", type: "boolean", default: "false", description: "Render the timestamp inline." },
{ name: "isVisibleOnlyOnHover", type: "boolean", default: "false", description: "Hide the timestamp until the parent is hovered." },
{ name: "isEdited", type: "boolean", default: "false", description: 'Show an "(edited)" indicator after the timestamp.' },
{ name: "tooltipPosition", type: '"top" | "bottom" | "left" | "right"', description: "Position of the full-date tooltip." },
{ name: "id", type: "string", description: "HTML id attribute." },
{ name: "className", type: "string", description: "Additional CSS class." },
{ name: "children", type: "ReactNode", description: "Custom content to render instead of the formatted timestamp." },
];
export default function TimestampTab() {
const now = new Date();
return (
<DocPage
componentName="Timestamp"
overview="Timestamp renders a formatted date with a tooltip showing the full date. Supports moment.js format strings, compact mode, inline rendering, edited indicators, and hover-only visibility."
importPath={'import { Timestamp, TimestampDisplayFormats } from "../components";'}
sections={[
{
title: "Formats",
description: "All available timestamp display formats using moment.js format strings.",
children: (
<div className="vc-compfinder-grid">
{TimestampDisplayFormats.map(({ format, description }) => (
<div key={format} style={{ padding: 12, display: "flex", flexDirection: "column", gap: 4 }}>
<Timestamp timestamp={now} timestampFormat={format} />
<Paragraph color="text-muted" style={{ fontSize: 10 }}>
{format}: {description}
</Paragraph>
</div>
))}
</div>
),
code: '<Timestamp timestamp={new Date()} timestampFormat="LLL" />',
relevantProps: ["timestamp", "timestampFormat"],
},
{
title: "Compact Mode",
description: "Compact mode shows a shorter version of the timestamp.",
children: (
<div className="vc-compfinder-grid">
<div style={{ padding: 12 }}>
<Timestamp timestamp={now} compact={false} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
compact=false (default)
</Paragraph>
</div>
<div style={{ padding: 12 }}>
<Timestamp timestamp={now} compact={true} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
compact=true
</Paragraph>
</div>
</div>
),
relevantProps: ["compact"],
},
{
title: "Display Variants",
description: "Inline and cozy alternative display modes.",
children: (
<div className="vc-compfinder-grid">
<div style={{ padding: 12 }}>
<Timestamp timestamp={now} isInline={false} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
isInline=false (default)
</Paragraph>
</div>
<div style={{ padding: 12 }}>
<Timestamp timestamp={now} isInline={true} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
isInline=true
</Paragraph>
</div>
<div style={{ padding: 12 }}>
<Timestamp timestamp={now} cozyAlt={true} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
cozyAlt=true
</Paragraph>
</div>
</div>
),
relevantProps: ["isInline", "cozyAlt"],
},
{
title: "Edited Indicator",
description: 'Shows an "(edited)" indicator after the timestamp.',
children: (
<div className="vc-compfinder-grid">
<div style={{ padding: 12 }}>
<Timestamp timestamp={now} isEdited={false} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
isEdited=false (default)
</Paragraph>
</div>
<div style={{ padding: 12 }}>
<Timestamp timestamp={now} isEdited={true} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
isEdited=true
</Paragraph>
</div>
</div>
),
relevantProps: ["isEdited"],
},
{
title: "Visibility on Hover",
description: "Timestamp can be hidden until the parent element is hovered.",
children: (
<div className="vc-compfinder-grid">
<div style={{ padding: 12 }}>
<span style={{ padding: 8, background: "var(--background-secondary)", borderRadius: 4 }}>
Hover me: <Timestamp timestamp={now} isVisibleOnlyOnHover={true} />
</span>
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
isVisibleOnlyOnHover=true
</Paragraph>
</div>
</div>
),
relevantProps: ["isVisibleOnlyOnHover"],
},
{
title: "Tooltip Positions",
description: "The tooltip showing the full date can be positioned on any side.",
children: (
<div className="vc-compfinder-grid">
{TooltipPositions.map(position => (
<div key={position} style={{ padding: 12 }}>
<Timestamp timestamp={now} tooltipPosition={position} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
tooltipPosition="{position}"
</Paragraph>
</div>
))}
</div>
),
relevantProps: ["tooltipPosition"],
},
]}
props={TIMESTAMP_PROPS}
/>
);
}

View file

@ -1,153 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaButton, Paragraph, showToast, Toasts, ToastType, type ToastTypeValue } from "..";
import { DocPage, type PropGroup } from "../DocPage";
const TOAST_TYPES: { type: ToastTypeValue; label: string; description: string; }[] = [
{ type: ToastType.MESSAGE, label: "MESSAGE", description: "Default message toast." },
{ type: ToastType.SUCCESS, label: "SUCCESS", description: "Green success toast." },
{ type: ToastType.FAILURE, label: "FAILURE", description: "Red failure/error toast." },
{ type: ToastType.CLIP, label: "CLIP", description: "Clip/copy toast." },
{ type: ToastType.LINK, label: "LINK", description: "Link copied toast." },
{ type: ToastType.FORWARD, label: "FORWARD", description: "Forward action toast." },
{ type: ToastType.BOOKMARK, label: "BOOKMARK", description: "Bookmark action toast." },
{ type: ToastType.CLOCK, label: "CLOCK", description: "Clock/timer toast." },
];
const PROP_GROUPS: PropGroup[] = [
{
title: "showToast(message, type)",
props: [
{ name: "message", type: "string", required: true, description: "Toast message text." },
{ name: "type", type: "ToastTypeValue", default: '"message"', description: "Toast type from ToastType." },
],
},
{
title: "Toasts.create(message, type, options)",
props: [
{ name: "message", type: "string", required: true, description: "Toast message text." },
{ name: "type", type: "ToastTypeValue", required: true, description: "Toast type from ToastType." },
{ name: "options.duration", type: "number", description: "Display time in milliseconds." },
{ name: "options.position", type: "0 | 1", description: "Toast position. 0 = TOP, 1 = BOTTOM." },
{ name: "options.component", type: "ReactNode", description: "Custom React content for CUSTOM type toasts." },
],
},
{
title: "ToastType",
props: [
{ name: "MESSAGE", type: '"message"', description: "Default message style." },
{ name: "SUCCESS", type: '"success"', description: "Green checkmark icon." },
{ name: "FAILURE", type: '"failure"', description: "Red X icon." },
{ name: "CUSTOM", type: '"custom"', description: "Custom component content." },
{ name: "CLIP", type: '"clip"', description: "Clip/scissors icon." },
{ name: "LINK", type: '"link"', description: "Link icon." },
{ name: "FORWARD", type: '"forward"', description: "Forward arrow icon." },
{ name: "BOOKMARK", type: '"bookmark"', description: "Bookmark icon." },
{ name: "CLOCK", type: '"clock"', description: "Clock icon." },
],
},
];
export default function ToastTab() {
return (
<DocPage
componentName="Toasts"
overview="Toasts are brief notification messages that appear at screen edges. Use showToast for quick one-liners, or Toasts.create + Toasts.show for full control over duration, position, and custom content. Toasts.pop removes the most recent toast."
notices={[
{ type: "info", children: "Use showToast() for quick one-liners. Use Toasts.create() + Toasts.show() when you need custom duration, position, or component content." },
]}
importPath={'import { showToast, Toasts, ToastType } from "../components";'}
sections={[
{
title: "All Types",
description: "Click any button to show that toast type.",
children: (
<div className="vc-compfinder-grid" style={{ gap: 8 }}>
{TOAST_TYPES.map(({ type, label, description }) => (
<div key={label} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<ManaButton
variant="secondary"
text={label}
onClick={() => showToast(`This is a ${label} toast!`, type)}
/>
<Paragraph color="text-muted" style={{ fontSize: 10 }}>
{description}
</Paragraph>
</div>
))}
</div>
),
code: 'showToast("Copied to clipboard!", ToastType.SUCCESS);',
},
{
title: "Simple Usage",
description: "showToast is the quickest way to show a toast. It creates and shows a toast in one call.",
children: (
<ManaButton
variant="primary"
text="Show Simple Toast"
onClick={() => showToast("Hello from showToast!", ToastType.SUCCESS)}
/>
),
code: 'showToast("Hello from showToast!", ToastType.SUCCESS);',
},
{
title: "Advanced Usage",
description: "Use Toasts.create to build a toast with custom duration, position, or component content, then Toasts.show to display it.",
children: (
<div className="vc-compfinder-grid" style={{ gap: 8 }}>
<ManaButton
variant="secondary"
text="5s Duration Toast"
onClick={() => {
const toast = Toasts.create("This lasts 5 seconds", ToastType.SUCCESS, { duration: 5000 });
Toasts.show(toast);
}}
/>
<ManaButton
variant="secondary"
text="Bottom Position Toast"
onClick={() => {
const toast = Toasts.create("Bottom toast", ToastType.MESSAGE, { position: Toasts.Position.BOTTOM });
Toasts.show(toast);
}}
/>
<ManaButton
variant="secondary"
text="Custom Component Toast"
onClick={() => {
const toast = Toasts.create("Custom", ToastType.CUSTOM, {
component: <span style={{ color: "var(--text-brand)" }}>Custom JSX content!</span>,
});
Toasts.show(toast);
}}
/>
</div>
),
code: `const toast = Toasts.create("Done!", ToastType.SUCCESS, {
duration: 5000,
position: Toasts.Position.BOTTOM,
});
Toasts.show(toast);`,
},
{
title: "Pop Toast",
description: "Toasts.pop removes the most recent toast from the stack.",
children: (
<ManaButton
variant="critical-secondary"
text="Pop Last Toast"
onClick={() => Toasts.pop()}
/>
),
code: "Toasts.pop();",
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,157 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaButton, ManaTooltip, Paragraph, TooltipColors, TooltipPositions } from "..";
import { DocPage, type PropDef } from "../DocPage";
const TOOLTIP_PROPS: PropDef[] = [
{ name: "text", type: "string | (() => ReactNode)", required: true, description: "Tooltip content. Can be a string or a render function for rich content." },
{ name: "children", type: "(props) => ReactNode", required: true, description: "Render function that receives props to spread on the trigger element." },
{ name: "position", type: '"top" | "bottom" | "left" | "right"', default: '"top"', description: "Which side of the trigger the tooltip appears on." },
{ name: "align", type: '"start" | "center" | "end"', default: '"center"', description: "Alignment along the tooltip edge." },
{ name: "color", type: '"primary" | "grey" | "brand" | "green" | "red"', default: '"primary"', description: "Tooltip background color." },
{ name: "spacing", type: "number", default: "8", description: "Gap in pixels between the trigger and tooltip." },
{ name: "delay", type: "number", description: "Delay in milliseconds before the tooltip appears." },
{ name: "hideOnClick", type: "boolean", default: "true", description: "Hide the tooltip when the trigger is clicked." },
{ name: "forceOpen", type: "boolean", description: "Force the tooltip to stay open." },
{ name: "shouldShow", type: "boolean", description: "Externally control whether the tooltip is shown." },
{ name: "allowOverflow", type: "boolean", description: "Allow the tooltip to overflow its container." },
{ name: "overflowOnly", type: "boolean", description: "Only show the tooltip when the trigger text is overflowing." },
{ name: "tooltipClassName", type: "string", description: "CSS class for the tooltip container." },
{ name: "tooltipStyle", type: "CSSProperties", description: "Inline styles for the tooltip container." },
{ name: "tooltipContentClassName", type: "string", description: "CSS class for the tooltip content." },
{ name: "tooltipPointerClassName", type: "string", description: "CSS class for the tooltip arrow pointer." },
{ name: "positionKeyStemOverride", type: "string", internal: true, description: "Override the position key stem used for animation keying." },
{ name: "clickableOnMobile", type: "boolean", description: "Make the tooltip trigger clickable on mobile devices." },
{ name: "disableTooltipPointerEvents", type: "boolean", description: "Disable pointer events on the tooltip overlay." },
{ name: "targetElementRef", type: "RefObject<HTMLElement>", description: "Ref to an external element to position the tooltip against." },
{ name: "onTooltipShow", type: "() => void", description: "Callback when the tooltip becomes visible." },
{ name: "onTooltipHide", type: "() => void", description: "Callback when the tooltip is hidden." },
{ name: "onAnimationRest", type: "() => void", description: "Callback when the tooltip show/hide animation completes." },
{ name: "aria-label", type: "string | false", description: "Accessibility label. Set to false to disable." },
];
export default function TooltipTab() {
return (
<DocPage
componentName="ManaTooltip"
overview="ManaTooltip displays contextual information on hover via a render-props pattern. Supports positioning on all four sides, five color variants, configurable delay, and click-to-hide behavior. The children prop receives props that must be spread on the trigger element."
notices={[
{ type: "info", children: "ManaTooltip uses a render-props pattern. The children function receives props that must be spread onto the trigger element for hover detection to work." },
]}
importPath={'import { ManaTooltip, TooltipPositions, TooltipColors } from "../components";'}
sections={[
{
title: "Positions",
description: "Tooltip can appear on any side of the trigger element.",
children: (
<div className="vc-compfinder-grid">
{TooltipPositions.map(position => (
<ManaTooltip key={position} text={`Tooltip on ${position}`} position={position}>
{props => (
<ManaButton {...props} variant="secondary" text={position} />
)}
</ManaTooltip>
))}
</div>
),
code: `<ManaTooltip text="Tooltip on top" position="top">
{props => <ManaButton {...props} variant="secondary" text="Hover me" />}
</ManaTooltip>`,
relevantProps: ["position"],
},
{
title: "Colors",
description: "Five background color variants.",
children: (
<div className="vc-compfinder-grid">
{TooltipColors.map(color => (
<ManaTooltip key={color} text={`${color} tooltip`} color={color}>
{props => (
<ManaButton {...props} variant="secondary" text={color} />
)}
</ManaTooltip>
))}
</div>
),
code: '<ManaTooltip text="Branded" color="brand">\n {props => <ManaButton {...props} variant="secondary" text="Brand" />}\n</ManaTooltip>',
relevantProps: ["color"],
},
{
title: "Delay",
description: "Configure how long to wait before showing the tooltip.",
children: (
<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>
),
relevantProps: ["delay"],
},
{
title: "Hide on Click",
description: "By default tooltips hide when the trigger is clicked. Set hideOnClick=false to keep them visible.",
children: (
<div className="vc-compfinder-grid">
<ManaTooltip text="Hides when clicked" hideOnClick>
{props => (
<ManaButton {...props} variant="secondary" text="hideOnClick=true" />
)}
</ManaTooltip>
<ManaTooltip text="Stays visible when clicked" hideOnClick={false}>
{props => (
<ManaButton {...props} variant="secondary" text="hideOnClick=false" />
)}
</ManaTooltip>
</div>
),
relevantProps: ["hideOnClick"],
},
{
title: "Rich Content",
description: "The text prop accepts a function that returns JSX for rich tooltip content.",
children: (
<div className="vc-compfinder-grid">
<ManaTooltip text={() => (
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<Paragraph color="text-default" style={{ fontWeight: 600 }}>Rich Tooltip</Paragraph>
<Paragraph color="text-muted" style={{ fontSize: 12 }}>With multiple lines of content.</Paragraph>
</div>
)}>
{props => (
<ManaButton {...props} variant="secondary" text="Rich content" />
)}
</ManaTooltip>
</div>
),
code: `<ManaTooltip text={() => (
<div>
<strong>Title</strong>
<p>Description text</p>
</div>
)}>
{props => <ManaButton {...props} text="Hover me" />}
</ManaTooltip>`,
relevantProps: ["text"],
},
]}
props={TOOLTIP_PROPS}
/>
);
}

View file

@ -1,144 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CodeColors, FontSizeMap, Paragraph, TextColors, TextSizes, TextWeights } from "..";
import { DocPage, type PropGroup } from "../DocPage";
const PROP_GROUPS: PropGroup[] = [
{
title: "Text Colors",
props: TextColors.map(color => ({
name: color,
type: "CSS variable",
description: `var(--${color})`,
})),
},
{
title: "Text Sizes",
props: TextSizes.map(({ name, value, pixels }) => ({
name,
type: "font-size",
description: `${value} (${pixels}).`,
})),
},
{
title: "Text Weights",
props: TextWeights.map(({ name, value }) => ({
name,
type: "font-weight",
description: `font-weight: ${value}.`,
})),
},
{
title: "Code Colors",
props: CodeColors.map(color => ({
name: color,
type: "CSS variable",
description: `var(--${color})`,
})),
},
];
export default function TypographyTab() {
return (
<DocPage
componentName="Typography Reference"
overview="Typography reference for Discord's design system CSS variables. Shows all available text colors, sizes, weights, and code syntax colors. Use these with the Paragraph, DiscordHeading, and DiscordText components."
importPath={'import { TextColors, TextSizes, TextWeights, CodeColors, FontSizeMap } from "../components";'}
sections={[
{
title: "Text Colors",
description: "All available text color CSS variables. Use these as the color prop on Paragraph and DiscordText.",
children: (
<div className="vc-compfinder-grid-vertical">
{TextColors.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>
),
code: '<Paragraph color="text-muted">Muted text</Paragraph>',
},
{
title: "Text Sizes",
description: "Font sizes from xxs (10px) to xxl (32px).",
children: (
<div className="vc-compfinder-grid-vertical">
{TextSizes.map(({ name, value, pixels }) => (
<div key={name} style={{ display: "flex", alignItems: "baseline", gap: 16 }}>
<span style={{ fontSize: FontSizeMap[name], minWidth: 200 }}>
{name}
</span>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
{value} ({pixels})
</code>
</div>
))}
</div>
),
},
{
title: "Text Weights",
description: "Font weights from thin (100) to extrabold (800).",
children: (
<div className="vc-compfinder-grid-vertical">
{TextWeights.map(({ name, value }) => (
<div key={name} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<span style={{ fontWeight: Number(value), minWidth: 200 }}>
{name}
</span>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
font-weight: {value}
</code>
</div>
))}
</div>
),
},
{
title: "Code Colors",
description: "Syntax highlighting colors for code blocks.",
children: (
<div className="vc-compfinder-grid-vertical">
{CodeColors.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>
),
},
{
title: "Usage with Components",
description: "These values are used with Paragraph, DiscordHeading, and DiscordText. See those tabs for component-specific documentation.",
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<Paragraph color="text-default">text-default: Primary readable text.</Paragraph>
<Paragraph color="text-muted">text-muted: Secondary, less prominent text.</Paragraph>
<Paragraph color="text-link">text-link: Clickable link text.</Paragraph>
<Paragraph color="text-brand">text-brand: Brand-colored text.</Paragraph>
</div>
),
code: `<Paragraph color="text-muted">Muted text</Paragraph>
<DiscordText variant="text-md/semibold" color="text-brand">Brand text</DiscordText>
<DiscordHeading variant="heading-lg/bold">Bold heading</DiscordHeading>`,
},
]}
props={PROP_GROUPS}
/>
);
}

View file

@ -1,149 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph, UserStore, UserSummaryItem, useState } from "..";
import { DocPage, type PropDef } from "../DocPage";
const USER_SUMMARY_PROPS: PropDef[] = [
{ name: "users", type: "(User | null)[]", required: true, description: "Array of User objects to display as avatars. Null entries render as empty slots." },
{ name: "max", type: "number", default: "10", description: "Maximum number of avatars to show before the overflow indicator." },
{ name: "count", type: "number", description: 'Override the count used in the "+N" overflow display.' },
{ name: "size", type: "16 | 24 | 32 | 56", default: "24", description: "Avatar size in pixels." },
{ name: "guildId", type: "string", description: "Guild context for server-specific avatars." },
{ name: "showUserPopout", type: "boolean", default: "false", description: "Show user popout when an avatar is clicked." },
{ name: "showDefaultAvatarsForNullUsers", type: "boolean", description: "Render default avatars for null entries in the users array." },
{ name: "renderUser", type: "(user, isLast, index) => ReactNode", description: "Custom render function for each user avatar." },
{ name: "renderMoreUsers", type: "(text, count) => ReactNode", description: 'Custom render function for the "+N" overflow indicator.' },
{ name: "renderIcon", type: "boolean", description: "Whether to render an icon." },
{ name: "renderLeadingIcon", type: "(className) => ReactNode", description: "Render a leading icon before the user avatars." },
{ name: "dimEmptyUsers", type: "boolean", description: "Dim null user slots." },
{ name: "hideMoreUsers", type: "boolean", default: "false", description: 'Hide the "+N" overflow count.' },
{ name: "useFallbackUserForPopout", type: "boolean", description: "Use fallback user data for popout when user data is unavailable." },
{ name: "extraDetail", type: "ReactNode", description: "Additional content rendered after the avatars." },
{ name: "className", type: "string", description: "CSS class for the container." },
];
function MaxUsersDemo() {
const currentUser = UserStore.getCurrentUser();
const users = currentUser ? Array(5).fill(currentUser) : [];
return (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{[2, 3, 5].map(max => (
<div key={max} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 80 }}>max={max}:</Paragraph>
<UserSummaryItem users={users} max={max} />
</div>
))}
</div>
);
}
function SizesDemo() {
const currentUser = UserStore.getCurrentUser();
const users = currentUser ? Array(5).fill(currentUser) : [];
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
{([16, 24, 32, 56] as const).map(size => (
<div key={size} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 50 }}>{size}:</Paragraph>
<UserSummaryItem users={users} max={4} size={size} />
</div>
))}
</div>
);
}
function OverrideCountDemo() {
const currentUser = UserStore.getCurrentUser();
const users = currentUser ? Array(3).fill(currentUser) : [];
const [count, setCount] = useState(42);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 120 }}>Default count:</Paragraph>
<UserSummaryItem users={users} max={2} />
</div>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 120 }}>count={count}:</Paragraph>
<UserSummaryItem users={users} max={2} count={count} />
</div>
</div>
);
}
export default function UserSummaryItemTab() {
const currentUser = UserStore.getCurrentUser();
const users = currentUser ? Array(5).fill(currentUser) : [];
return (
<DocPage
componentName="UserSummaryItem"
overview="UserSummaryItem displays a row of user avatars with an overflow indicator. Supports four sizes, configurable max visible count, user popout on click, and custom render functions for both avatars and the overflow indicator."
importPath={'import { UserSummaryItem, UserStore } from "../components";'}
sections={[
{
title: "Basic",
description: "A simple row of user avatars with overflow.",
children: (
<UserSummaryItem users={users} max={3} />
),
code: `const users = [user1, user2, user3, user4, user5];
<UserSummaryItem users={users} max={3} />`,
relevantProps: ["users", "max"],
},
{
title: "Sizes",
description: "Four avatar sizes: 16, 24, 32, and 56 pixels.",
children: <SizesDemo />,
code: "<UserSummaryItem users={users} max={4} size={32} />",
relevantProps: ["size"],
},
{
title: "Max Users",
description: "Control how many avatars are visible before the overflow count.",
children: <MaxUsersDemo />,
relevantProps: ["max"],
},
{
title: "Override Count",
description: 'The count prop overrides the number shown in the "+N" indicator, useful when the total is different from users.length.',
children: <OverrideCountDemo />,
relevantProps: ["count"],
},
{
title: "User Popout",
description: "Enable user popout on avatar click with showUserPopout.",
children: (
<UserSummaryItem users={users} max={3} showUserPopout />
),
code: "<UserSummaryItem users={users} max={3} showUserPopout />",
relevantProps: ["showUserPopout"],
},
{
title: "Hide Overflow",
description: 'Hide the "+N" overflow indicator entirely.',
children: (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 150 }}>hideMoreUsers=false:</Paragraph>
<UserSummaryItem users={users} max={3} />
</div>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<Paragraph color="text-muted" style={{ width: 150 }}>hideMoreUsers=true:</Paragraph>
<UserSummaryItem users={users} max={3} hideMoreUsers />
</div>
</div>
),
relevantProps: ["hideMoreUsers"],
},
]}
props={USER_SUMMARY_PROPS}
/>
);
}

View file

@ -1,184 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const ManaButtonVariants = ["primary", "secondary", "critical-primary", "critical-secondary", "active", "overlay-primary", "overlay-secondary", "expressive"] as const;
export type ManaButtonVariant = typeof ManaButtonVariants[number];
export const ManaButtonSizes = ["xs", "sm", "md"] as const;
export type ManaButtonSize = typeof ManaButtonSizes[number];
export const ManaTextButtonVariants = ["primary", "secondary", "always-white", "critical"] as const;
export type ManaTextButtonVariant = typeof ManaTextButtonVariants[number];
export const AvatarSizes = ["SIZE_16", "SIZE_20", "SIZE_24", "SIZE_32", "SIZE_40", "SIZE_44", "SIZE_48", "SIZE_56", "SIZE_72", "SIZE_80", "SIZE_96", "SIZE_120", "SIZE_152"] as const;
export type AvatarSize = typeof AvatarSizes[number];
export const AvatarStatuses = ["online", "idle", "dnd", "offline", "streaming"] as const;
export type AvatarStatus = typeof AvatarStatuses[number];
export const GuildIconSizes = [16, 20, 24, 32, 40, 48, 56, 80, 96] as const;
export type GuildIconSize = typeof GuildIconSizes[number];
export const CardVariants = ["primary", "warning", "danger", "success", "brand"] as const;
export type CardVariant = typeof CardVariants[number];
export const SpinnerTypes = ["wanderingCubes", "chasingDots", "pulsingEllipsis", "spinningCircle", "spinningCircleSimple", "lowMotion"] as const;
export type SpinnerType = typeof SpinnerTypes[number];
export const TooltipPositions = ["top", "bottom", "left", "right"] as const;
export type TooltipPosition = typeof TooltipPositions[number];
export const TooltipAligns = ["start", "center", "end"] as const;
export type TooltipAlign = typeof TooltipAligns[number];
export const TooltipColors = ["primary", "grey", "brand", "green", "red"] as const;
export type TooltipColor = typeof TooltipColors[number];
export const TextInputSizes = ["sm", "md"] as const;
export type TextInputSize = typeof TextInputSizes[number];
export const TimestampFormats = [
{ format: "R", label: "Relative", example: "2 hours ago" },
{ format: "t", label: "Short Time", example: "9:30 PM" },
{ format: "T", label: "Long Time", example: "9:30:00 PM" },
{ format: "d", label: "Short Date", example: "01/20/2024" },
{ format: "D", label: "Long Date", example: "January 20, 2024" },
{ format: "f", label: "Short DateTime", example: "January 20, 2024 9:30 PM" },
{ format: "F", label: "Long DateTime", example: "Saturday, January 20, 2024 9:30 PM" },
] as const;
export const ModalSizes = [
{ size: "small", width: "440px" },
{ size: "medium", width: "600px" },
{ size: "large", width: "720px" },
{ size: "dynamic", width: "fit-content" },
] as const;
export type ModalSize = typeof ModalSizes[number]["size"];
export const TextColors = [
"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;
export type TextColorType = typeof TextColors[number];
export const TextSizes = [
{ name: "xxs", value: "0.625rem", pixels: "10px" },
{ name: "xs", value: "0.75rem", pixels: "12px" },
{ name: "sm", value: "0.875rem", pixels: "14px" },
{ name: "md", value: "1rem", pixels: "16px" },
{ name: "lg", value: "1.25rem", pixels: "20px" },
{ name: "xl", value: "1.5rem", pixels: "24px" },
{ name: "xxl", value: "2rem", pixels: "32px" },
] as const;
export type TextSizeType = typeof TextSizes[number]["name"];
export const TextWeights = [
{ 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;
export type TextWeightType = typeof TextWeights[number]["name"];
export const TextVariants = [
"text-xxl/bold", "text-xxl/semibold", "text-xxl/medium", "text-xxl/normal",
"text-xl/bold", "text-xl/semibold", "text-xl/medium", "text-xl/normal",
"text-lg/bold", "text-lg/semibold", "text-lg/medium", "text-lg/normal",
"text-md/bold", "text-md/semibold", "text-md/medium", "text-md/normal",
"text-sm/bold", "text-sm/semibold", "text-sm/medium", "text-sm/normal",
"text-xs/bold", "text-xs/semibold", "text-xs/medium", "text-xs/normal",
] as const;
export type TextVariantType = typeof TextVariants[number];
export const HeadingVariants = [
"heading-xxl/extrabold", "heading-xxl/bold", "heading-xxl/semibold", "heading-xxl/medium", "heading-xxl/normal",
"heading-xl/extrabold", "heading-xl/bold", "heading-xl/semibold", "heading-xl/medium",
"heading-lg/extrabold", "heading-lg/bold", "heading-lg/semibold", "heading-lg/medium",
"heading-md/bold", "heading-md/semibold", "heading-md/medium", "heading-md/normal",
"heading-sm/bold", "heading-sm/semibold", "heading-sm/medium",
] as const;
export type HeadingVariantType = typeof HeadingVariants[number];
export const CodeColors = [
"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",
] as const;
export type CodeColorType = typeof CodeColors[number];
export const NoticeTypes = ["info", "warn", "danger", "positive", "preview"] as const;
export type NoticeTypeValue = typeof NoticeTypes[number];
export const PresetColors = [
0x5865F2, 0x3BA55C, 0xFAA61A, 0xED4245, 0xEB459E,
0x9B59B6, 0x3498DB, 0x1ABC9C, 0xE67E22, 0x2ECC71,
] as const;
export const FontSizeMap: Record<string, string> = {
xxs: "0.625rem",
xs: "0.75rem",
sm: "0.875rem",
md: "1rem",
lg: "1.25rem",
xl: "1.5rem",
xxl: "2rem",
};
export const ManaTextButtonTextVariants = [
"text-xs/normal", "text-sm/normal", "text-sm/medium",
"text-md/normal", "text-md/medium", "text-lg/medium",
] as const;
export type ManaTextButtonTextVariant = typeof ManaTextButtonTextVariants[number];
export const TimestampDisplayFormats = [
{ format: "LT", description: "Time (e.g., 8:30 PM)" },
{ format: "LTS", description: "Time with seconds (e.g., 8:30:25 PM)" },
{ format: "L", description: "Date (e.g., 09/04/1986)" },
{ format: "LL", description: "Date with month name (e.g., September 4, 1986)" },
{ format: "LLL", description: "Date and time (e.g., September 4, 1986 8:30 PM)" },
{ format: "LLLL", description: "Full date and time (e.g., Thursday, September 4, 1986 8:30 PM)" },
{ format: "l", description: "Short date (e.g., 9/4/1986)" },
{ format: "ll", description: "Short date with month (e.g., Sep 4, 1986)" },
{ format: "lll", description: "Short date and time (e.g., Sep 4, 1986 8:30 PM)" },
{ format: "llll", description: "Short full date and time (e.g., Thu, Sep 4, 1986 8:30 PM)" },
] as const;
export type TimestampDisplayFormat = typeof TimestampDisplayFormats[number]["format"];
export const CheckboxLabelTypes = ["primary", "secondary"] as const;
export type CheckboxLabelType = typeof CheckboxLabelTypes[number];
export const CheckboxUsageVariants = ["single", "indicator"] as const;
export type CheckboxUsageVariant = typeof CheckboxUsageVariants[number];
export const PopoverSizes = ["sm", "md", "lg"] as const;
export type PopoverSize = typeof PopoverSizes[number];
export const SliderOrientations = ["horizontal", "vertical"] as const;
export type SliderOrientation = typeof SliderOrientations[number];
export const ToastType = {
MESSAGE: "message",
SUCCESS: "success",
FAILURE: "failure",
CUSTOM: "custom",
CLIP: "clip",
LINK: "link",
FORWARD: "forward",
BOOKMARK: "bookmark",
CLOCK: "clock",
} as const;
export const ToastPosition = {
TOP: 0,
BOTTOM: 1,
} as const;

View file

@ -1,164 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { ComponentsIcon } from "@components/Icons";
import SettingsPlugin from "@plugins/_core/settings";
import { Devs } from "@utils/constants";
import { removeFromArray } from "@utils/misc";
import definePlugin, { StartAt } from "@utils/types";
import { SettingsRouter } from "@webpack/common";
import { ChipProps, ColorPickerProps, ManaBaseRadioGroupProps, ManaCalendarProps, ManaComboboxProps, ManaDatePickerProps, ManaSwitchProps, SimpleErrorBoundaryProps, SkeletonProps, StandaloneRadioIndicatorProps } from "./components";
import ComponentsTab from "./components/ComponentsTab";
export let Skeleton: React.ComponentType<SkeletonProps>;
export let Chip: React.ComponentType<ChipProps>;
export let StandaloneRadioIndicator: React.ComponentType<StandaloneRadioIndicatorProps>;
export let ManaCombobox: React.ComponentType<ManaComboboxProps>;
export let ManaBaseRadioGroup: React.ComponentType<ManaBaseRadioGroupProps>;
export let ManaDatePicker: React.ComponentType<ManaDatePickerProps>;
export let ManaCalendar: React.ComponentType<ManaCalendarProps>;
export let ManaSwitch: React.ComponentType<ManaSwitchProps>;
export let SimpleErrorBoundary: React.ComponentType<SimpleErrorBoundaryProps>;
export let ColorPicker: React.ComponentType<ColorPickerProps> = () => null;
export function setColorPicker(component: React.ComponentType<ColorPickerProps>) {
ColorPicker = component;
}
export default definePlugin({
name: "Components",
description: "Adds a new tab to settings to browse Discord components.",
tags: ["Appearance", "Customisation", "Console", "Developers", "Organisation"],
authors: [Devs.prism],
dependencies: ["Settings"],
patches: [
{
find: /withHeader:\i=!0,size:/,
replacement: {
match: /(?=function (\i)\(\i\)\{.{0,50}withHeader:\i=!0)/,
replace: "$self.Skeleton=$1;"
}
},
{
find: '="grayLight"}',
replacement: {
match: /(?=function (\i)\(\i\)\{.{0,25}="grayLight"\})/,
replace: "$self.Chip=$1;"
}
},
{
find: ".standaloneRadioIndicator,",
replacement: {
match: /(?=function (\i)\(\i\)\{.{0,15}disabled:\i,isSelected:\i\})/,
replace: "$self.StandaloneRadioIndicator=$1;"
}
},
{
find: "#{intl::AUTOCOMPLETE_NO_RESULTS_BODY}",
replacement: {
match: /(?<=data-listbox-item-id".*?itemToString:\i\}\);)(?=function (\i)\(\i\)\{)/,
replace: "$self.ManaCombobox=$1;"
}
},
{
find: '"data-mana-component":"BaseRadioGroup"',
replacement: {
match: /(?=function (\i)\(\i\)\{.{0,400}"data-mana-component":"BaseRadioGroup")/,
replace: "$self.ManaBaseRadioGroup=$1;"
}
},
{
find: '"data-mana-component":"date-picker"',
replacement: {
match: /(?=function (\i)\(\i\)\{.{0,60},placeholderValue:\i)/,
replace: "$self.ManaDatePicker=$1;"
}
},
{
find: '"data-mana-component":"calendar"',
replacement: {
match: /(?=function (\i)\(\i\)\{.{0,400}"data-mana-component":"calendar")/,
replace: "$self.ManaCalendar=$1;"
}
},
{
find: '"data-mana-component":"switch"',
replacement: {
match: /(?=function (\i)\(\i\)\{.{0,50}focusProps:\i,hasIcon:)/,
replace: "$self.ManaSwitch=$1;"
}
},
{
find: '"data-mana-component":"switch"',
replacement: {
match: /(?=function (\i)\(\i\)\{.{0,50}focusProps:\i,hasIcon:)/,
replace: "$self.ManaSwitch=$1;"
}
},
{
find: "#{intl::zksHZO::raw}",
replacement: {
match: /(?<=class (\i).{0,90}getDerivedStateFromError.*?this.props.children\}\})/,
replace: "$self.SimpleErrorBoundary=$1;"
}
},
{
find: "#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}),focusProps:",
replacement: {
match: /(?=function (\i)\(\i\)\{let\{onChange:\i,onClose:\i,[^}]+?showEyeDropper:)/,
replace: "$self.setColorPicker($1);"
}
},
],
set Skeleton(value: any) {
Skeleton = value;
},
set Chip(value: any) {
Chip = value;
},
set StandaloneRadioIndicator(value: any) {
StandaloneRadioIndicator = value;
},
set ManaCombobox(value: any) {
ManaCombobox = value;
},
set ManaBaseRadioGroup(value: any) {
ManaBaseRadioGroup = value;
},
set ManaDatePicker(value: any) {
ManaDatePicker = value;
},
set ManaCalendar(value: any) {
ManaCalendar = value;
},
set ManaSwitch(value: any) {
ManaSwitch = value;
},
set SimpleErrorBoundary(value: any) {
SimpleErrorBoundary = value;
},
setColorPicker,
startAt: StartAt.WebpackReady,
toolboxActions: {
"Open Components Tab"() {
SettingsRouter.openUserSettings("equicord_components_panel");
},
},
start() {
SettingsPlugin.customEntries.push({
key: "equicord_components",
title: "Components",
Component: ComponentsTab,
Icon: ComponentsIcon
});
},
stop() {
removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_components");
},
});

View file

@ -1,314 +0,0 @@
/* ── Search & Tab Navigation ── */
.vc-compfinder-search {
margin-bottom: 40px;
}
.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;
}
/* ── Shared Utility Layouts ── */
.vc-compfinder-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.vc-compfinder-section-block > :nth-child(n+3) {
margin-top: 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: rgb(0 0 0 / 15%);
border-radius: 4px;
}
.vc-compfinder-demo-button {
padding: 8px 16px;
background: rgb(0 0 0 / 15%);
border: 1px solid rgb(255 255 255 / 6%);
border-radius: 4px;
color: var(--text-default);
cursor: pointer;
}
.vc-compfinder-demo-button:hover {
background: rgb(0 0 0 / 25%);
}
/* ── Modal Demo Helpers ── */
.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;
}
.vc-alert-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}
/* ── DocPage Layout ── */
.vc-compfinder-doc-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.vc-compfinder-doc-page > .vc-divider {
margin: 8px 0;
}
.vc-compfinder-doc-title {
margin-bottom: -8px;
}
.vc-compfinder-doc-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.vc-compfinder-doc-section [id] {
scroll-margin-top: 16px;
}
.vc-compfinder-doc-demo {
padding: 16px;
}
.vc-compfinder-doc-relevant-props {
margin-top: 8px;
}
.vc-compfinder-doc-prop-group {
margin-bottom: 16px;
}
.vc-compfinder-doc-example {
display: flex;
flex-direction: column;
gap: 4px;
}
.vc-compfinder-doc-example + .vc-compfinder-doc-example {
margin-top: 12px;
}
/* ── Import Block ── */
.vc-compfinder-import-block {
display: flex;
flex-direction: column;
gap: 6px;
}
.vc-compfinder-import-label {
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ── Table of Contents ── */
.vc-compfinder-toc {
display: flex;
flex-direction: column;
gap: 6px;
}
.vc-compfinder-toc-label {
text-transform: uppercase;
letter-spacing: 0.05em;
}
.vc-compfinder-toc-links {
display: flex;
flex-wrap: wrap;
gap: 2px 12px;
}
.vc-compfinder-toc-link {
color: var(--text-link);
font-size: 0.8125rem;
text-decoration: none;
cursor: pointer;
line-height: 1.6;
}
.vc-compfinder-toc-link:hover {
text-decoration: underline;
}
/* ── Props Table ── */
.vc-compfinder-table-wrapper {
overflow-x: auto;
}
.vc-compfinder-table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
}
.vc-compfinder-table th {
text-align: left;
padding: 8px 12px;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
background: rgb(0 0 0 / 20%);
border-bottom: 1px solid rgb(255 255 255 / 6%);
vertical-align: middle;
}
.vc-compfinder-table td {
padding: 8px 12px;
color: var(--text-default);
vertical-align: middle;
}
.vc-compfinder-table tbody tr:nth-child(odd) {
background: rgb(0 0 0 / 8%);
}
.vc-compfinder-table tbody tr:nth-child(even) {
background: rgb(0 0 0 / 15%);
}
.vc-compfinder-table code.inline {
padding: 1px 4px !important;
font-size: 0.625rem !important;
line-height: 1.3;
white-space: nowrap;
}
.vc-compfinder-doc-page code.inline {
padding: 2px 5px;
font-size: 0.75rem;
}
.vc-compfinder-doc-page pre code {
font-size: 0.75rem !important;
}
.vc-compfinder-table td:nth-child(2) {
white-space: nowrap;
}
.vc-compfinder-table-field {
font-weight: 600;
font-size: 0.75rem;
color: var(--text-default);
font-family: var(--font-code);
white-space: nowrap;
}
.vc-compfinder-table-row-required td:first-child {
border-left: 3px solid var(--status-positive);
padding-left: 9px;
}
.vc-compfinder-table-row-internal td:first-child {
border-left: 3px solid var(--status-warning);
padding-left: 9px;
}
.vc-compfinder-table-legend {
display: flex;
gap: 16px;
margin-top: 8px;
}
.vc-compfinder-table-legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.6875rem;
color: var(--text-muted);
}
.vc-compfinder-table-legend-bar {
width: 3px;
height: 12px;
border-radius: 1px;
}
.vc-compfinder-table-legend-required {
background: var(--status-positive);
}
.vc-compfinder-table-legend-internal {
background: var(--status-warning);
}
.vc-compfinder-table-dash {
color: var(--text-faint);
}
.vc-compfinder-table-desc {
color: var(--text-muted);
line-height: 1.4;
}

View file

@ -1,18 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface AccordionProps {
icon?: React.ReactNode;
title: string;
subtitle?: string;
children: React.ReactNode;
className?: string;
isExpanded?: boolean;
onExpandedChange?: (expanded: boolean) => void;
defaultExpanded?: boolean;
onOpen?: () => void;
maxHeight?: number | string;
}

View file

@ -1,23 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface AlertOptions {
title: string;
body: React.ReactNode;
confirmText?: string;
cancelText?: string;
confirmVariant?: "primary" | "critical-primary" | "expressive";
onConfirm?: () => void;
onCancel?: () => void;
onCloseCallback?: () => void;
contextKey?: string;
}
export interface AlertsType {
show: (options: AlertOptions) => void;
close: () => void;
confirm: (options: AlertOptions) => Promise<boolean>;
}

View file

@ -1,19 +0,0 @@
/*
* 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

@ -1,52 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { AvatarSize, AvatarStatus } from "../constants";
export type { AvatarSize, AvatarStatus };
export interface AvatarSizeConfig {
size: number;
status: number;
stroke: number;
offset: number;
}
export interface AvatarProps {
src?: string;
size?: AvatarSize;
status?: AvatarStatus;
statusColor?: string;
statusBackdropColor?: string;
isMobile?: boolean;
isVR?: boolean;
isTyping?: boolean;
isSpeaking?: boolean;
isLatched?: boolean;
voiceDb?: number;
speakingStylesConfig?: Record<string, any>;
statusTooltip?: boolean;
statusTooltipDelay?: number;
typingIndicatorRef?: React.Ref<any>;
avatarContentRef?: React.Ref<any>;
CutoutIcon?: React.ComponentType<any>;
avatarTooltipAsset?: React.ReactNode;
avatarTooltipText?: string;
avatarTooltipTitle?: string;
avatarDecoration?: string;
"aria-hidden"?: boolean;
"aria-label"?: string;
imageClassName?: string;
className?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
onMouseDown?: React.MouseEventHandler<HTMLElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLElement>;
onContextMenu?: React.MouseEventHandler<HTMLElement>;
onMouseEnter?: React.MouseEventHandler<HTMLElement>;
onMouseLeave?: React.MouseEventHandler<HTMLElement>;
tabIndex?: number;
ref?: React.Ref<SVGSVGElement>;
}

View file

@ -1,57 +0,0 @@
/*
* 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 extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
count: number;
color?: string;
disableColor?: boolean;
shape?: string;
className?: string;
style?: React.CSSProperties;
renderBadgeCount?: (count: number) => string;
}
export interface TextBadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
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 extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
color?: string;
disableColor?: boolean;
shape?: string;
className?: string;
style?: React.CSSProperties;
}
export interface PremiumBadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
text: string;
className?: string;
color?: string;
disableColor?: boolean;
shape?: string;
style?: React.CSSProperties;
}

View file

@ -1,60 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { ManaButtonSize, ManaButtonVariant, ManaTextButtonVariant } from "../constants";
export type { ManaButtonSize, ManaButtonVariant, ManaTextButtonVariant };
export type ManaButtonIconType =
| React.ComponentType<any>
| { type: "icon"; asset: React.ComponentType<any>; }
| { type: "rive"; asset: React.ComponentType<any>; riveProps?: Record<string, any>; }
| { type: "sticker"; asset: any; component: React.ComponentType<any>; };
export interface ManaButtonProps {
text?: string;
variant?: ManaButtonVariant;
size?: ManaButtonSize;
disabled?: boolean;
loading?: boolean;
loadingStartedLabel?: string;
loadingFinishedLabel?: string;
fullWidth?: boolean;
rounded?: boolean;
icon?: ManaButtonIconType;
iconPosition?: "start" | "end";
iconOpticalOffsetMargin?: number;
minWidth?: number | string;
role?: string;
type?: "button" | "submit" | "reset";
rel?: string;
onClick?: (e: React.MouseEvent) => void;
onDoubleClick?: (e: React.MouseEvent) => void;
onMouseEnter?: (e: React.MouseEvent) => void;
onMouseLeave?: (e: React.MouseEvent) => void;
onMouseUp?: (e: React.MouseEvent) => void;
onMouseDown?: (e: React.MouseEvent) => void;
onKeyDown?: (e: React.KeyboardEvent) => void;
focusProps?: Record<string, any>;
buttonRef?: React.Ref<HTMLButtonElement>;
className?: string;
style?: React.CSSProperties;
}
export interface ManaTextButtonProps {
text: string;
variant?: ManaTextButtonVariant;
textVariant?: string;
lineClamp?: number;
role?: string;
type?: "button" | "submit" | "reset";
onClick?: (e: React.MouseEvent) => void;
disabled?: boolean;
focusProps?: Record<string, any>;
buttonRef?: React.Ref<HTMLButtonElement>;
className?: string;
style?: React.CSSProperties;
}

View file

@ -1,40 +0,0 @@
/*
* 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;
hideLabel?: boolean;
badge?: React.ReactNode;
icon?: React.ReactNode;
description?: string;
helperText?: string;
errorMessage?: string;
successMessage?: string;
id?: string;
layout?: string;
layoutConfig?: Record<string, unknown>;
}

View file

@ -1,50 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { CheckboxLabelType, CheckboxUsageVariant } from "../constants";
export type { CheckboxLabelType, CheckboxUsageVariant };
export interface ManaCheckboxProps {
checked: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
displayOnly?: boolean;
label?: string;
description?: string;
leadingIcon?: React.ComponentType<any>;
labelType?: CheckboxLabelType;
usageVariant?: CheckboxUsageVariant;
value?: string | number;
labeledBy?: string;
}
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;
label?: string;
hideLabel?: boolean;
badge?: React.ReactNode;
icon?: React.ReactNode;
required?: boolean;
description?: string;
helperText?: string;
errorMessage?: string;
successMessage?: string;
id?: string;
layout?: string;
layoutConfig?: Record<string, unknown>;
}

View file

@ -1,20 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type ChipVariant =
| "blurpleLight" | "blurpleMedium" | "blurpleDark"
| "purpleLight" | "purpleMedium" | "purpleDark"
| "greenLight" | "greenMedium" | "greenDark"
| "orangeLight" | "orangeMedium" | "orangeDark"
| "yellowLight" | "yellowMedium" | "yellowDark"
| "pinkLight" | "pinkMedium" | "pinkDark"
| "redLight" | "redMedium" | "redDark"
| "grayLight" | "grayMedium" | "grayDark";
export interface ChipProps {
text: string;
variant?: ChipVariant;
}

View file

@ -1,19 +0,0 @@
/*
* 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

@ -1,77 +0,0 @@
/*
* 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

@ -1,31 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { ComponentType, ErrorInfo, ReactNode } from "react";
export interface ErrorBoundaryFallbackProps {
error: Error;
resetErrorBoundary: (...args: any[]) => void;
}
export interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
fallbackRender?: (props: ErrorBoundaryFallbackProps) => ReactNode;
FallbackComponent?: ComponentType<ErrorBoundaryFallbackProps>;
onError?: (error: Error, info: ErrorInfo) => void;
onReset?: (details: { reason: "imperative-api" | "keys"; args?: any[]; prev?: any[]; next?: any[]; }) => void;
resetKeys?: any[];
}
export interface SimpleErrorBoundaryProps {
children: ReactNode;
fallback: ReactNode;
}
export interface LocalErrorBoundaryProps {
children: ReactNode;
}

View file

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

View file

@ -1,15 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface GuildIconProps {
guildId: string;
guildName: string;
guildIcon: string | null;
iconSize: number;
className?: string;
acronymClassName?: string;
animate?: boolean;
}

View file

@ -1,32 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { HeadingVariantType, TextColorType, TextVariantType } from "../constants";
export type HeadingVariant = HeadingVariantType;
export type TextVariant = TextVariantType;
export type TextColor = TextColorType | "currentColor" | "none" | "always-white";
export interface DiscordHeadingProps {
variant: HeadingVariant;
className?: string;
color?: TextColor;
style?: React.CSSProperties;
children?: React.ReactNode;
}
export interface DiscordTextProps {
variant: TextVariant;
color?: TextColor;
tag?: string;
selectable?: boolean;
lineClamp?: number;
tabularNumbers?: boolean;
scaleFontToUserSetting?: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}

View file

@ -1,40 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "../constants";
export * from "./accordion";
export * from "./alert";
export * from "./anchor";
export * from "./avatar";
export * from "./badge";
export * from "./button";
export * from "./calendar";
export * from "./checkbox";
export * from "./chip";
export * from "./clickable";
export * from "./colorpicker";
export * from "./errorboundary";
export * from "./focuslock";
export * from "./guildicon.d";
export * from "./heading";
export * from "./input";
export * from "./menu";
export * from "./modal";
export * from "./notice";
export * from "./paginator";
export * from "./progress";
export * from "./radio";
export * from "./scroller";
export * from "./select";
export * from "./skeleton";
export * from "./slider";
export * from "./spinner";
export * from "./switch";
export * from "./tabbar";
export * from "./timestamp";
export * from "./toast";
export * from "./tooltip";
export * from "./usersummary";

View file

@ -1,121 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { TextInputSize } from "../constants";
export type { TextInputSize };
export type TextInputLeadingAccessory =
| string
| React.ComponentType<any>
| { icon: React.ComponentType<any>; tooltip?: string; }
| { button: React.ReactNode; }
| { type: "tags"; tags: string[]; onRemove?: (tag: string) => void; }
| { type: "image"; src: string; };
export type TextInputTrailingAccessory =
| React.ComponentType<any>
| { icon: React.ComponentType<any>; tooltip?: string; }
| { button: React.ReactNode; };
export interface ManaTextInputProps {
value?: string;
defaultValue?: string;
onChange?: (value: string, name?: string) => void;
onClear?: (e: React.MouseEvent) => void;
onFocus?: (e: React.FocusEvent) => void;
onBlur?: (e: React.FocusEvent) => void;
placeholder?: string;
name?: string;
type?: "text" | "password" | "email" | "number" | "search" | "tel" | "url";
disabled?: boolean;
readOnly?: boolean;
editable?: boolean;
size?: TextInputSize;
error?: string | boolean;
clearable?: boolean | { show: boolean; };
showCharacterCount?: boolean;
maxLength?: number;
minLength?: number;
fullWidth?: boolean;
leading?: TextInputLeadingAccessory;
trailing?: TextInputTrailingAccessory;
validateOn?: "change" | "blur";
defaultDirty?: boolean;
focusProps?: Record<string, any>;
inputRef?: React.Ref<HTMLInputElement>;
label?: string;
hideLabel?: boolean;
badge?: React.ReactNode;
icon?: React.ReactNode;
required?: boolean;
description?: string;
helperText?: string;
errorMessage?: string;
successMessage?: string;
id?: string;
layout?: string;
layoutConfig?: Record<string, unknown>;
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
}
export interface ManaTextAreaProps {
value?: string;
defaultValue?: string;
onChange?: (value: string, name?: string) => void;
onFocus?: (e: React.FocusEvent) => void;
onBlur?: (e: React.FocusEvent) => void;
placeholder?: string;
name?: string;
disabled?: boolean;
readOnly?: boolean;
rows?: number;
autoFocus?: boolean;
autosize?: boolean;
maxLength?: number;
minLength?: number;
showCharacterCount?: boolean;
showRemainingCharacterCount?: boolean;
error?: string | boolean;
defaultDirty?: boolean;
fullWidth?: boolean;
focusProps?: Record<string, any>;
inputRef?: React.Ref<HTMLTextAreaElement>;
label?: string;
hideLabel?: boolean;
badge?: React.ReactNode;
icon?: React.ReactNode;
required?: boolean;
description?: string;
helperText?: string;
errorMessage?: string;
successMessage?: string;
id?: string;
layout?: string;
layoutConfig?: Record<string, unknown>;
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
}
export interface SearchBarProps {
query: string;
onChange: (value: string) => void;
onClear?: () => void;
placeholder?: string;
size?: TextInputSize;
autoFocus?: boolean;
disabled?: boolean;
onKeyDown?: (e: React.KeyboardEvent) => void;
onBlur?: () => void;
onFocus?: () => void;
autoComplete?: string;
inputProps?: Record<string, any>;
ref?: React.Ref<HTMLInputElement>;
"aria-label"?: string;
}

View file

@ -1,211 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { ComponentType, CSSProperties, MouseEvent, ReactNode, Ref, UIEvent } from "react";
export type MenuItemColor = "default" | "brand" | "danger" | "premium" | "premium-gradient" | "success";
export type MenuIconSize = "sm" | "md" | "lg";
export interface MenuProps {
navId: string;
onClose: () => void;
onSelect?: () => void;
onInteraction?: (data: { type: number; }) => void;
variant?: "flexible" | "fixed";
hideScroller?: boolean;
className?: string;
style?: CSSProperties;
"aria-label"?: string;
children: ReactNode;
}
export interface MenuItemIconProps {
color?: string;
className?: string;
}
export interface MenuItemProps {
id: string;
label: ReactNode | ((props: MenuItemProps) => ReactNode);
color?: MenuItemColor;
icon?: ComponentType<MenuItemIconProps>;
iconLeft?: ComponentType<MenuItemIconProps>;
iconLeftSize?: MenuIconSize;
iconProps?: MenuItemIconProps;
leadingAccessory?: ReactNode;
trailingIndicator?: ReactNode;
hint?: ReactNode | ((props: MenuItemProps) => ReactNode);
shortcut?: ReactNode;
subtext?: ReactNode;
subtextLineClamp?: number;
hasSubmenu?: boolean;
loading?: boolean;
badge?: ReactNode;
disabled?: boolean;
action?: (e: MouseEvent) => void;
onClose?: () => void;
onFocus?: () => void;
dontCloseOnAction?: boolean;
dontCloseOnActionIfHoldingShiftKey?: boolean;
className?: string;
focusedClassName?: string;
subMenuIconClassName?: string;
render?: (props: MenuItemProps) => ReactNode;
navigable?: boolean;
onChildrenScroll?: () => void;
childRowHeight?: number;
listClassName?: string;
subMenuClassName?: string;
children?: ReactNode;
}
export interface MenuCheckboxItemProps {
id: string;
label: ReactNode | ((props: MenuCheckboxItemProps) => ReactNode);
color?: MenuItemColor;
checked: boolean;
action?: (e: MouseEvent) => void;
disabled?: boolean;
subtext?: ReactNode;
subtextLineClamp?: number;
leftIcon?: ComponentType<MenuItemIconProps>;
leadingAccessory?: ReactNode;
className?: string;
focusedClassName?: string;
}
export interface MenuRadioItemProps {
id: string;
group: string;
label: ReactNode | ((props: MenuRadioItemProps) => ReactNode);
color?: MenuItemColor;
checked: boolean;
action?: (e: MouseEvent) => void;
disabled?: boolean;
subtext?: ReactNode;
subtextLineClamp?: number;
leftIcon?: ComponentType<MenuItemIconProps>;
leadingAccessory?: ReactNode;
}
export interface MenuSwitchItemProps {
id: string;
label?: ReactNode;
color?: MenuItemColor;
checked: boolean;
action: (e: MouseEvent) => void;
disabled?: boolean;
subtext?: ReactNode;
subtextLineClamp?: number;
className?: string;
}
export interface MenuGroupProps {
label?: ReactNode;
color?: MenuItemColor;
className?: string;
children: ReactNode;
}
export interface MenuCustomItemRenderProps {
color: MenuItemColor;
disabled: boolean;
isFocused: boolean;
}
export interface MenuCustomItemProps {
id: string;
color?: MenuItemColor;
disabled?: boolean;
keepItemStyles?: boolean;
action?: (e: MouseEvent) => void;
dontCloseOnAction?: boolean;
dontCloseOnActionIfHoldingShiftKey?: boolean;
navigable?: boolean;
children: (props: MenuCustomItemRenderProps) => ReactNode;
}
export interface MenuControlItemControlProps {
onClose: () => void;
disabled?: boolean;
isFocused: boolean;
onInteraction: (type?: number) => void;
}
export interface MenuControlItemProps {
id: string;
label?: ReactNode;
color?: MenuItemColor;
control: (props: MenuControlItemControlProps, ref: Ref<MenuControlRef>) => ReactNode;
disabled?: boolean;
showDefaultFocus?: boolean;
}
export interface MenuCompositeControlItemProps {
id: string;
color?: MenuItemColor;
disabled?: boolean;
showDefaultFocus?: boolean;
interactive?: boolean;
children: ReactNode;
}
export interface MenuControlRef {
focus: () => void;
blur: () => void;
activate: () => boolean;
}
export interface MenuSliderControlProps {
value: number;
minValue?: number;
maxValue?: number;
onChange: (value: number) => void;
onInteraction?: (type?: number) => void;
renderValue?: (value: number) => string;
"aria-label"?: string;
}
export interface MenuSearchControlProps {
query: string;
onChange: (query: string) => void;
disabled?: boolean;
placeholder?: string;
onInteraction?: (type?: number) => void;
"aria-label"?: string;
}
export interface MenuType {
Menu: ComponentType<MenuProps>;
MenuItem: ComponentType<MenuItemProps>;
MenuCheckboxItem: ComponentType<MenuCheckboxItemProps>;
MenuRadioItem: ComponentType<MenuRadioItemProps>;
MenuSwitchItem: ComponentType<MenuSwitchItemProps>;
MenuGroup: ComponentType<MenuGroupProps>;
MenuSeparator: ComponentType<Record<string, never>>;
MenuControlItem: ComponentType<MenuControlItemProps>;
MenuSliderControl: ComponentType<MenuSliderControlProps>;
MenuSearchControl: ComponentType<MenuSearchControlProps>;
}
export interface ContextMenuOpenOptions {
enableSpellCheck?: boolean;
}
export interface ContextMenuApiType {
openContextMenu: (
event: UIEvent,
render: (props: { onClose: () => void; }) => ReactNode,
options?: ContextMenuOpenOptions
) => void;
openContextMenuLazy: (
event: UIEvent,
renderLazy: () => Promise<(props: { onClose: () => void; }) => ReactNode>,
options?: ContextMenuOpenOptions
) => void;
closeContextMenu: (callback?: () => void) => void;
}

View file

@ -1,119 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type NativeModalSize = "sm" | "md";
export type ModalActionVariant = "primary" | "secondary" | "critical-primary" | "critical-secondary";
export interface ModalAction {
text: string;
variant: ModalActionVariant;
onClick: () => void;
disabled?: boolean;
loading?: boolean;
}
export interface ModalNotice {
message: string;
type: "info" | "warn" | "critical" | "positive";
}
export type ModalAnimationVariant = "default" | "fade" | "slide-up";
export type ModalMaxHeight = "default" | "full";
export interface ModalProps {
title: string;
subtitle?: string;
size?: NativeModalSize;
transitionState?: number;
animationVariant?: ModalAnimationVariant;
paddingSize?: "sm" | "lg";
fullScreenOnMobile?: boolean;
maxHeight?: ModalMaxHeight;
dismissable?: boolean;
children?: React.ReactNode;
actions?: ModalAction[];
actionBarInput?: React.ReactNode;
actionBarInputLayout?: "default" | "full-width";
input?: React.ReactNode;
preview?: React.ReactNode;
listProps?: Record<string, any>;
notice?: ModalNotice;
onClose: () => void;
onScroll?: (e: React.UIEvent) => void;
scrollerRef?: React.Ref<HTMLDivElement>;
returnRef?: React.Ref<HTMLElement>;
role?: string;
"aria-label"?: string;
className?: string;
}
export interface ConfirmModalProps extends Omit<ModalProps, "actions" | "transitionState"> {
transitionState?: number;
confirmText: string;
cancelText?: string;
onConfirm?: (setError?: (error: string) => void) => void | Promise<void>;
onCancel?: () => void;
onCloseCallback?: () => void;
variant?: "critical" | "primary" | "secondary";
checkboxProps?: {
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
};
}
export type ExpressiveGradientColor = "purple" | "blue" | "pink" | "green";
export interface ModalGraphic {
type: "image";
src: string;
aspectRatio?: string;
}
export interface ExpressiveModalProps {
title: string;
subtitle?: string;
size?: NativeModalSize;
transitionState?: number;
gradientColor?: ExpressiveGradientColor;
graphic?: ModalGraphic;
badge?: React.ReactNode;
actionBarInput?: React.ReactNode;
actions?: ModalAction[];
children?: React.ReactNode;
onClose: () => void;
}
export interface OpenModalOptions {
modalKey?: string;
dismissable?: boolean;
instant?: boolean;
onCloseRequest?: () => void;
onCloseCallback?: () => void;
backdropStyle?: "default" | "dark" | "transparent";
stackingBehavior?: "stack" | "replace";
stackNextByDefault?: boolean;
allowsNavigation?: boolean;
}
export type ModalRenderFn = (props: { onClose: () => void; transitionState: number }) => React.ReactNode;
export interface ModalAPI {
openModal: (render: ModalRenderFn, options?: OpenModalOptions) => string;
openModalLazy: (renderFactory: () => Promise<{ default: React.ComponentType<any> }>, options?: OpenModalOptions) => Promise<string>;
closeModal: (modalKey: string) => void;
closeAllModals: () => void;
hasModalOpen: (modalKey: string) => boolean;
hasAnyModalOpen: () => boolean;
}
export interface ModalComponents {
Modal: React.ComponentType<ModalProps>;
ConfirmModal: React.ComponentType<ConfirmModalProps>;
ExpressiveModal: React.ComponentType<ExpressiveModalProps>;
}

View file

@ -1,20 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { NoticeTypeValue } from "../constants";
export type NoticeType = NoticeTypeValue;
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

@ -1,24 +0,0 @@
/*
* 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

@ -1,29 +0,0 @@
/*
* 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 type ProgressBarWeight = "light" | "medium";
export interface ProgressBarProps {
progress: number;
minimum?: number;
maximum?: number;
weight?: ProgressBarWeight;
variant?: ProgressBarVariant;
override?: {
default?: ProgressBarOverride;
[key: string]: ProgressBarOverride | undefined;
};
glowing?: boolean;
labelledBy?: string;
}

View file

@ -1,44 +0,0 @@
/*
* 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;
description?: string;
disabled?: boolean;
icon?: React.ComponentType<{ className?: string; size?: string; color?: string; }>;
color?: string;
radioItemIconClassName?: string;
radioBarClassName?: string;
}
export type RadioPosition = "left" | "right";
export interface ManaBaseRadioGroupProps {
options: RadioOption[];
value?: string | number | null;
onChange?: (value: string | number) => void;
disabled?: boolean;
size?: string;
orientation?: "vertical" | "horizontal";
radioPosition?: RadioPosition;
withTransparentBackground?: boolean;
label?: string;
description?: string;
required?: boolean;
errorMessage?: string;
"aria-labelledby"?: string;
className?: string;
itemInfoClassName?: string;
itemTitleClassName?: string;
radioItemClassName?: string;
collapsibleClassName?: string;
}
export interface StandaloneRadioIndicatorProps {
isSelected: boolean;
disabled?: boolean;
}

View file

@ -1,46 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ScrollerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
orientation?: "vertical" | "horizontal";
paddingFix?: boolean;
fade?: boolean;
dir?: "ltr" | "rtl";
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
}
export interface ListScrollerProps {
className?: string;
style?: React.CSSProperties;
dir?: "ltr" | "rtl";
fade?: boolean;
customTheme?: boolean;
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
onResize?: ((entries: ResizeObserverEntry[]) => void) | null;
onContentResize?: ((entries: ResizeObserverEntry[]) => void) | null;
sections: number[];
sectionHeight: number;
rowHeight: number;
footerHeight?: number;
sidebarHeight?: number;
listHeaderHeight?: number;
renderSection?: (section: { type: string; section: number; }) => React.ReactNode;
renderRow: (row: { type: string; section: number; row: number; }) => React.ReactNode;
renderFooter?: (footer: { type: string; section: number; }) => React.ReactNode;
renderSidebar?: (isListVisible: boolean, isSidebarVisible: boolean) => React.ReactNode;
renderListHeader?: () => React.ReactNode;
stickyListHeader?: boolean;
wrapSection?: (section: number, children: React.ReactNode[]) => React.ReactNode;
paddingTop?: number;
paddingBottom?: number;
chunkSize?: number;
innerId?: string;
innerRole?: string;
innerAriaLabel?: string;
}

View file

@ -1,73 +0,0 @@
/*
* 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;
formatOption?: (option: ManaSelectOption) => React.ReactNode;
name?: string;
form?: string;
autoComplete?: string;
label?: string;
required?: boolean;
}
export interface ManaComboboxProps<T = string> {
placeholder?: string;
value?: T | T[];
onChange?: (value: T | T[]) => void;
multiSelect?: boolean;
autoFocus?: boolean;
maxVisibleItems?: number;
itemToString?: (item: T) => string;
emptyStateText?: string;
emptyStateHeader?: string;
onQueryChange?: (query: string) => void;
"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

@ -1,10 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface SkeletonProps {
withHeader?: boolean;
size?: number;
}

View file

@ -1,50 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { SliderOrientation } from "../constants";
export type { SliderOrientation };
export interface SliderProps {
initialValue?: number;
value?: number;
minValue?: number;
maxValue?: number;
onValueChange?: (value: number) => void;
asValueChanges?: (value: number) => void;
onValueRender?: (value: number) => string | null;
markers?: number[];
stickToMarkers?: boolean;
equidistant?: boolean;
onMarkerRender?: (value: number) => string | React.ReactNode | null;
renderMarker?: (value: number) => React.ReactNode;
getAriaValueText?: (value: number) => string;
keyboardStep?: number;
handleSize?: number;
disabled?: boolean;
mini?: boolean;
hideBubble?: boolean;
defaultValue?: number;
orientation?: SliderOrientation;
markerPosition?: 0 | 1;
barStyles?: React.CSSProperties;
fillStyles?: React.CSSProperties;
grabberStyles?: React.CSSProperties;
barClassName?: string;
grabberClassName?: string;
className?: string;
"aria-hidden"?: boolean;
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
label?: string;
hideLabel?: boolean;
required?: boolean;
description?: string;
helperText?: string;
errorMessage?: string;
successMessage?: string;
}

View file

@ -1,30 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { SpinnerType } from "../constants";
export type { SpinnerType };
export interface SpinnerTypeEnum {
WANDERING_CUBES: "wanderingCubes";
CHASING_DOTS: "chasingDots";
PULSING_ELLIPSIS: "pulsingEllipsis";
SPINNING_CIRCLE: "spinningCircle";
SPINNING_CIRCLE_SIMPLE: "spinningCircleSimple";
LOW_MOTION: "lowMotion";
}
export interface SpinnerProps {
type?: SpinnerType;
animated?: boolean;
className?: string;
itemClassName?: string;
"aria-label"?: string;
}
export interface SpinnerComponent extends React.FC<SpinnerProps> {
Type: SpinnerTypeEnum;
}

View file

@ -1,17 +0,0 @@
/*
* 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;
id?: string;
hasIcon?: boolean;
focusProps?: Record<string, any>;
describedBy?: string;
labelledBy?: string;
innerRef?: React.Ref<HTMLInputElement>;
}

View file

@ -1,55 +0,0 @@
/*
* 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

@ -1,28 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { Moment } from "moment";
import type { TimestampDisplayFormat, TooltipPosition } from "../constants";
export type TimestampFormat = TimestampDisplayFormat;
export type { TooltipPosition };
export interface TimestampProps {
timestamp: Date | Moment;
timestampFormat?: TimestampFormat;
compact?: boolean;
cozyAlt?: boolean;
isInline?: boolean;
isVisibleOnlyOnHover?: boolean;
isEdited?: boolean;
application?: any;
id?: string;
className?: string;
children?: React.ReactNode;
tooltipPosition?: TooltipPosition;
}

View file

@ -1,32 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type ToastTypeValue = "message" | "success" | "failure" | "custom" | "clip" | "link" | "forward" | "bookmark" | "clock";
export type ToastPositionValue = 0 | 1;
export interface ToastOptions {
duration?: number;
position?: ToastPositionValue;
component?: React.ReactNode;
appContext?: number;
}
export interface ToastData {
message: string;
type: ToastTypeValue;
id: string;
options?: ToastOptions;
}
export interface ToastsModule {
Type: Record<string, ToastTypeValue>;
Position: Record<string, ToastPositionValue>;
genId: () => string;
show: (data: ToastData) => void;
pop: () => void;
create: (message: string, type: ToastTypeValue, options?: ToastOptions) => ToastData;
}

View file

@ -1,89 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { PopoverSize, TooltipAlign, TooltipColor, TooltipPosition } from "../constants";
export type { PopoverSize, TooltipAlign, TooltipColor, TooltipPosition };
export interface ManaTooltipProps {
text: string | (() => React.ReactNode);
position?: TooltipPosition;
align?: TooltipAlign;
color?: TooltipColor;
spacing?: number;
hideOnClick?: boolean;
delay?: number;
forceOpen?: boolean;
shouldShow?: boolean;
allowOverflow?: boolean;
overflowOnly?: boolean;
clickableOnMobile?: boolean;
disableTooltipPointerEvents?: boolean;
targetElementRef?: React.RefObject<HTMLElement>;
tooltipClassName?: string;
tooltipStyle?: React.CSSProperties;
tooltipContentClassName?: string;
tooltipPointerClassName?: string;
positionKeyStemOverride?: string;
onTooltipShow?: () => void;
onTooltipHide?: () => void;
onAnimationRest?: () => void;
"aria-label"?: string | false;
children: (props: Record<string, 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?: TooltipPosition;
align?: TooltipAlign;
spacing?: number;
caretConfig?: { align?: "start" | "center" | "end"; };
layerContext?: any;
targetElementRef?: React.RefObject<HTMLElement>;
anchorRef?: React.RefObject<HTMLElement>;
positionKey?: string;
ariaHidden?: boolean;
shouldShow?: boolean;
delay?: number;
onTooltipShow?: () => void;
onTooltipHide?: () => void;
forceOpen?: boolean;
overflowOnly?: boolean;
hideOnClick?: boolean;
children: React.ReactNode | ((props: Record<string, 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?: PopoverSize;
actions?: PopoverAction[];
textLink?: { text: string; onClick?: () => void; };
gradientColor?: string;
scrollBehavior?: string;
hasVideo?: boolean;
onRequestClose?: (reason: string) => void;
popoverRef?: React.Ref<HTMLDivElement>;
targetElementRef: React.RefObject<HTMLElement | null>;
shouldShow: boolean;
position?: TooltipPosition;
caretConfig?: { align?: "start" | "center" | "end"; };
alignmentStrategy?: "edge" | "trigger-center";
align?: "left" | "center" | "right";
}

View file

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