Components: improvements and changes (#586)

* Components: add Timestamp, Chip, Skeleton, Accordion tabs and fix Spinner

* yeet deprecated backwards-compatibility code

* remove .

* t

* bunch of fixes and new components

* expand types

* add missing

* this can be undefined?

* t

* Update MediaEngineStore.d.ts

* Update ComponentsTab.tsx

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
prism 2025-12-22 01:40:58 +01:00 • committed by GitHub
parent daca068537
commit ea972836d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 2293 additions and 557 deletions

View file

@ -1,18 +1,47 @@
import { FluxStore } from "..";
type VideoDevice = {
containerId: string;
disabled: boolean;
effects: any;
facing: string;
guid: string;
hardwareId: string;
export interface AudioDevice {
id: string;
index: number;
name: string;
};
disabled: boolean;
facing?: string;
guid: string;
hardwareId: string;
containerId: string;
effects?: unknown;
}
type GoLiveSource = {
export interface VideoDevice {
id: string;
index: number;
name: string;
disabled: boolean;
facing?: string;
guid: string;
hardwareId?: string;
containerId?: string;
effects?: unknown;
}
export interface ClipsSource {
quality: {
frameRate: number;
resolution: number;
};
desktopDescription: {
id: string;
soundshareId: number;
useLoopback: boolean;
useVideoHook: boolean;
useGraphicsCapture: boolean;
useQuartzCapturer: boolean;
allowScreenCaptureKit: boolean;
hdrCaptureMode: string;
};
}
export interface GoLiveSource {
desktopSource: {
id: string;
sourcePid: number | null;
@ -23,38 +52,169 @@ type GoLiveSource = {
resolution: number;
frameRate: number;
};
};
}
type CodecCapabilities = (codecs: string) => void;
export interface VideoStreamParameter {
rid: string;
type: string;
quality: number;
}
interface MediaEngine {
export interface LocalPan {
left: number;
right: number;
}
export interface ModeOptions {
threshold: number;
autoThreshold: boolean;
vadUseKrisp: boolean;
vadKrispActivationThreshold: number;
vadLeading: number;
vadTrailing: number;
delay: number;
shortcut: string[];
vadDuringPreProcess?: boolean;
}
export interface LoopbackOptions {
echoCancellation: boolean;
noiseSuppression: boolean;
automaticGainControl: boolean;
noiseCancellation: boolean;
}
export type MediaEngineEvent =
| "Destroy"
| "Silence"
| "Connection"
| "DeviceChange"
| "VolumeChange"
| "VoiceActivity"
| "WatchdogTimeout"
| "AudioPermission"
| "VideoPermission"
| "DesktopSourceEnd"
| "ConnectionStats"
| "VideoInputInitialized"
| "AudioInputInitialized"
| "ClipsRecordingRestartNeeded"
| "ClipsInitFailure"
| "ClipsRecordingEnded"
| "NativeScreenSharePickerUpdate"
| "NativeScreenSharePickerCancel"
| "NativeScreenSharePickerError"
| "AudioDeviceModuleError"
| "VoiceFiltersFailed"
| "VideoCodecError"
| "VoiceQueueMetrics"
| "SystemMicrophoneModeChange"
| "SelectedDeviceChange";
export type MediaEngineContext = "default" | "stream";
export interface MediaEngine {
Camera: React.ComponentType<{ disabled?: boolean; deviceId?: string; width?: number; height?: number; }>;
Video: React.ComponentType & { onContainerResized: () => void; };
on(event: MediaEngineEvent, listener: (...args: unknown[]) => void): this;
once(event: MediaEngineEvent, listener: (...args: unknown[]) => void): this;
off(event: MediaEngineEvent, listener: (...args: unknown[]) => void): this;
emit(event: MediaEngineEvent, ...args: unknown[]): boolean;
removeAllListeners(event?: MediaEngineEvent): this;
listenerCount(event: MediaEngineEvent): number;
applyMediaFilterSettings(settings: unknown): Promise<void>;
connect(userId: string, channelId: string, options: unknown): unknown;
connectionsEmpty(): boolean;
getAudioLayer(): boolean;
getCodecCapabilities(callback: (CodecCapabilities: string) => void): string;
createReplayConnection(userId: string, options: unknown): unknown;
destroy(): void;
eachConnection(callback: (connection: unknown) => void, context?: string): void;
enable(): Promise<void>;
exportClip(clipId: string, userId: string): Promise<Blob>;
fetchAsyncResources(options: { fetchDave?: boolean; }): Promise<void>;
getAudioInputDevices(): AudioDevice[];
getAudioLayer(): string;
getAudioOutputDevices(): AudioDevice[];
getAudioSubsystem(): string;
getCodecCapabilities(callback: (capabilities: string) => void): void;
getCodecSurvey(): Promise<unknown>;
getDebugLogging(): boolean;
getDesktopSource(): Promise<unknown>;
getLoopback(): boolean;
getMLSSigningKey(userId: string, guildId: string): Promise<{ key: unknown; signature: unknown; }>;
getNoiseCancellationStats(): Promise<unknown>;
getScreenPreviews(width: number, height: number): Promise<unknown[]>;
getSupportedBandwidthEstimationExperiments(callback: (experiments: unknown) => void): void;
getSupportedSecureFramesProtocolVersion(): number;
getSupportedVideoCodecs(callback: (codecs: string[]) => void): void;
getSystemMicrophoneMode(): Promise<string>;
getVideoInputDeviceId(): string;
getVideoInputDevices(): VideoDevice[];
getWindowPreviews(width: number, height: number): Promise<unknown[]>;
interact(): void;
presentNativeScreenSharePicker(options?: string): void;
queueAudioSubsystem(subsystem: string): void;
rankRtcRegions(regions: string[]): Promise<string[]>;
releaseNativeDesktopVideoSourcePickerStream(): void;
saveClip(clipId: string, userId: string): Promise<unknown>;
saveClipForUser(clipId: string, userId: string, options: unknown): Promise<unknown>;
saveScreenshot(channelId: string, userId: string, width: number | null, height: number | null, options: unknown): Promise<unknown>;
setAecDump(value: boolean): void;
setAsyncClipsSourceDeinit(callback: () => void): void;
setAsyncVideoInputDeviceInit(callback: () => void): void;
setAudioInputBypassSystemProcessing(value: boolean): void;
setAudioInputDevice(deviceId: string): void;
setAudioOutputDevice(deviceId: string): void;
setAudioSubsystem(subsystem: string): void;
setAv1Enabled(value: boolean): void;
setClipBufferLength(length: number): void;
setClipsMLPipelineEnabled(value: boolean): void;
setClipsMLPipelineTypeEnabled(type: string, enabled: boolean): void;
setClipsQualitySettings(resolution: number, frameRate: number, hdr: boolean): boolean;
setClipsSource(source: ClipsSource | null): void;
setDebugLogging(value: boolean): void;
setH265Enabled(value: boolean): void;
setGoLiveSource(source: GoLiveSource | null, context?: string): void;
setH264Enabled(value: boolean): void;
setH265Enabled(value: boolean): void;
setHasFullbandPerformance(value: boolean): void;
setInputVolume(volume: number): void;
setLoopback(reason: string, enabled: boolean): void;
setLoopback(reason: string, options: LoopbackOptions): void;
setMaxSyncDelayOverride(delay: number): void;
setMaybePreprocessMute(value: boolean): void;
setNativeDesktopVideoSourcePickerActive(value: boolean): void;
setNoiseCancellationEnableStats(value: boolean): void;
setOffloadAdmControls(value: boolean): void;
setOnVideoContainerResized(callback: () => void): void;
setOutputVolume(volume: number): void;
setSidechainCompression(value: boolean): void;
setSidechainCompressionStrength(strength: number): void;
setVideoInputDevice(deviceId: string): void;
setSoundshareSource(soundshareId: number, enabled: boolean, context?: string): void;
setVideoInputDevice(deviceId: string): Promise<void>;
shouldConnectionBroadcastVideo(connection: unknown): boolean;
showSystemCaptureConfigurationUI(options: unknown): void;
startAecDump(): void;
startLocalAudioRecording(options: unknown): Promise<void>;
startRecordingRawSamples(options: unknown): void;
stopAecDump(): void;
stopLocalAudioRecording(): void;
stopLocalAudioRecording(callback: (success: boolean, data: unknown) => void): void;
stopRecordingRawSamples(): void;
supported(): boolean;
supports(value: string): boolean;
writeAudioDebugState(): Promise<never>;
supports(feature: string): boolean;
updateClipMetadata(clipId: string, metadata: unknown): Promise<void>;
watchdogTick(): void;
writeAudioDebugState(): Promise<void>;
}
export class MediaEngineStore extends FluxStore {
fetchAsyncResources(): void;
getActiveInputProfile(): string;
getActiveVoiceFilter(): string | null;
getActiveVoiceFilterAppliedAt(): Date | null;
@ -65,7 +225,7 @@ export class MediaEngineStore extends FluxStore {
getAudioSubsystem(): string;
getAutomaticGainControl(): boolean;
getBypassSystemInputProcessing(): boolean;
getCameraComponent(): (e: any) => any;
getCameraComponent(): React.ComponentType;
getDebugLogging(): boolean;
getEchoCancellation(): boolean;
getEnableSilenceWarning(): boolean;
@ -78,89 +238,99 @@ export class MediaEngineStore extends FluxStore {
getHardwareEncoding(): boolean;
getInputDetected(): boolean | null;
getInputDeviceId(): string;
getInputDevices(): Record<string, VideoDevice>;
getInputDevices(): Record<string, AudioDevice>;
getInputVolume(): number;
getKrispEnableStats(): boolean;
getKrispModelOverride(): string;
getKrispModelOverride(): string | undefined;
getKrispModels(): string[];
getKrispSuppressionLevel(): number;
getKrispVadActivationThreshold(): number;
getLastAudioInputDeviceChangeTimestamp(): number;
getLocalPan(key: string): Record<string, number>;
getLocalVolume(): number;
getLocalPan(userId: string, context?: string): LocalPan;
getLocalVolume(userId: string, context?: string): number;
getLoopback(): boolean;
getLoopbackReasons(): Set<string>;
getMediaEngine(): MediaEngine;
getMode(): string;
getModeOptions(): Record<string, number | boolean>;
getMLSSigningKey(userId: string, guildId: string): Promise<unknown>;
getMode(context?: string): string;
getModeOptions(context?: string): ModeOptions;
getMostRecentlyRequestedVoiceFilter(): string | null;
getNoInputDetectedNotice(): boolean;
getNoiseCancellation(): boolean;
getNoiseSuppression(): boolean;
getOutputDeviceId(): string;
getOutputDevices(): Record<string, VideoDevice>;
getOutputDevices(): Record<string, AudioDevice>;
getOutputVolume(): number;
getPacketDelay(): number;
getPreviousVoiceFilter(): string | null;
getPreviousVoiceFilterAppliedAt(): Date | null;
getQoS(): boolean;
getSettings(): Record<string, any>;
getShortcuts(): Record<string, any>;
getSettings(context?: string): Record<string, unknown>;
getShortcuts(): Record<string, unknown>;
getSidechainCompression(): boolean;
getSidechainCompressionStrength(): number;
getSpeakingWhileMuted(): boolean;
getSupportedSecureFramesProtocolVersion(): boolean;
getSystemMicrophoneMode(): boolean;
getState(): {
settingsByContext: Record<string, unknown>;
inputDevices: Record<string, AudioDevice>;
outputDevices: Record<string, AudioDevice>;
appSupported: Record<string, boolean>;
krispModuleLoaded: boolean;
krispVersion: string | undefined;
krispSuppressionLevel: number | undefined;
goLiveSource: GoLiveSource | undefined;
goLiveContext: string;
};
getSupportedSecureFramesProtocolVersion(): number;
getSystemMicrophoneMode(): string | undefined;
getUseGamescopeCapture(): boolean;
getUseSystemScreensharePicker(): boolean;
getUseVaapiEncoder(): boolean;
getVideoComponent(): (e: any) => any;
getVideoComponent(): React.ComponentType;
getVideoDeviceId(): string;
getVideoDevices(): Record<string, VideoDevice>;
getVideoHook(): boolean;
getVideoStreamParameters(): Record<string, string | number>;
getVideoToggleState(key: string): string;
getVideoStreamParameters(context?: string): VideoStreamParameter[];
getVideoToggleState(userId: string, context?: string): string;
getVoiceFilterPlaybackEnabled(): boolean;
goLiveSimulcastEnabled(): boolean;
hasActiveCallKitCall(): boolean;
hasClipsSource(): boolean;
hasContext(key: string): boolean;
hasContext(context: string): boolean;
hasH265HardwareDecode(): boolean;
isAdvancedVoiceActivitySupported(): boolean;
isAecDumpSupported(): boolean;
isAnyLocalVideoAutoDisabled(): boolean;
isAnyLocalVideoAutoDisabled(context?: string): boolean;
isAutomaticGainControlSupported(): boolean;
isDeaf(): boolean;
isEnableHardwareMuteNotice(): boolean;
isEnabled(): boolean;
isHardwareMute(): boolean;
isHardwareMute(context?: string): boolean;
isInputProfileCustom(): boolean;
isInteractionRequired(): boolean;
isLocalMute(): boolean;
isLocalMute(userId: string): boolean;
isLocalVideoAutoDisabled(userId: string): boolean;
isLocalVideoDisabled(userId: string): boolean;
isLocalVideoDisabled(): boolean;
isLocalMute(userId: string, context?: string): boolean;
isLocalVideoAutoDisabled(userId: string, context?: string): boolean;
isLocalVideoDisabled(userId: string, context?: string): boolean;
isMediaFilterSettingLoading(): boolean;
isMute(): boolean;
isNativeAudioPermissionReady(): boolean;
isNoiseCancellationError(): boolean;
isNoiseCancellationSupported(): boolean;
isNoiseSuppressionSupported(): boolean;
isScreenSharing(): boolean;
isSelfDeaf(): boolean;
isSelfMute(): boolean;
isSelfMutedTemporarily(): boolean;
isScreenSharing(context?: string): boolean;
isSelfDeaf(context?: string): boolean;
isSelfMute(context?: string): boolean;
isSelfMutedTemporarily(context?: string): boolean;
isSimulcastSupported(): boolean;
isSoundSharing(): boolean;
isSoundSharing(context?: string): boolean;
isSupported(): boolean;
isVideoAvailable(): boolean;
isVideoEnabled(): boolean;
notifyMuteUnmuteSoundWasSkipped(): boolean;
notifyMuteUnmuteSoundWasSkipped(): void;
setCanHavePriority(userId: string, value: boolean): void;
setHasActiveCallKitCall(active: boolean): void;
shouldOfferManualSubsystemSelection(): boolean;
@ -169,7 +339,7 @@ export class MediaEngineStore extends FluxStore {
startDavePreload(): void;
supports(e: string): boolean;
supports(feature: string): boolean;
supportsDisableLocalVideo(): boolean;
supportsExperimentalSoundshare(): boolean;
supportsHookSoundshare(): boolean;

View file

@ -8,8 +8,8 @@ type State = {
export class OverridePremiumTypeStore extends FluxStore {
getState(): State;
getCreatedAtOverride(): Date;
getCreatedAtOverride(): Date | undefined;
getPremiumTypeActual(): number;
getPremiumTypeOverride(): number;
getPremiumTypeOverride(): number | undefined;
get premiumType(): number;
}

View file

@ -11,16 +11,22 @@ import { SettingsTab, wrapTab } from "@components/settings";
import { Margins } from "@utils/margins";
import { TabBar, useState } from "@webpack/common";
import AccordionTab from "./tabs/AccordionTab";
import AnchorTab from "./tabs/AnchorTab";
import AvatarTab from "./tabs/AvatarTab";
import BadgeTab from "./tabs/BadgeTab";
import ButtonsTab from "./tabs/ButtonsTab";
import CalendarTab from "./tabs/CalendarTab";
import 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 ColorPickerTab from "./tabs/ColorPickerTab";
import ComboboxTab from "./tabs/ComboboxTab";
import DividerTab from "./tabs/DividerTab";
import GuildIconTab from "./tabs/GuildIconTab";
import HeadingTab from "./tabs/HeadingTab";
import LayerModalTab from "./tabs/LayerModalTab";
import ListboxTab from "./tabs/ListboxTab";
import ModalTab from "./tabs/ModalTab";
@ -30,8 +36,10 @@ 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";
@ -39,20 +47,29 @@ 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";
const TABS = [
{ id: "accordion", label: "Accordion" },
{ 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: "colorpicker", label: "ColorPicker" },
{ id: "combobox", label: "Combobox" },
{ id: "divider", label: "Divider" },
{ id: "guildicon", label: "GuildIcon" },
{ id: "heading", label: "Heading" },
{ id: "layermodal", label: "LayerModal" },
{ id: "listbox", label: "Listbox" },
{ id: "modal", label: "Modal" },
@ -62,8 +79,10 @@ const TABS = [
{ 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" },
@ -71,23 +90,32 @@ const TABS = [
{ 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" },
].sort((a, b) => a.label.localeCompare(b.label));
type TabId = typeof TABS[number]["id"];
const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
accordion: AccordionTab,
anchor: AnchorTab,
avatar: AvatarTab,
badge: BadgeTab,
buttons: ButtonsTab,
calendar: CalendarTab,
card: CardTab,
chip: ChipTab,
checkbox: CheckboxTab,
checkboxgroup: CheckboxGroupTab,
clickable: ClickableTab,
colorpicker: ColorPickerTab,
combobox: ComboboxTab,
divider: DividerTab,
guildicon: GuildIconTab,
heading: HeadingTab,
layermodal: LayerModalTab,
listbox: ListboxTab,
modal: ModalTab,
@ -97,8 +125,10 @@ const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
progressbar: ProgressBarTab,
radiogroup: RadioGroupTab,
richtooltip: RichTooltipTab,
scroller: ScrollerTab,
searchbar: SearchBarTab,
select: SelectTab,
skeleton: SkeletonTab,
slider: SliderTab,
spinner: SpinnerTab,
switch: SwitchTab,
@ -106,6 +136,8 @@ const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
textarea: TextAreaTab,
textbutton: TextButtonTab,
textinput: TextInputTab,
timestamp: TimestampTab,
toast: ToastTab,
tooltip: TooltipTab,
typography: TypographyTab,
};

View file

@ -4,22 +4,63 @@
* 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 { proxyLazy } from "@utils/lazy";
import { findByCodeLazy, findComponentByCodeLazy, findLazy } from "@webpack";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findLazy } from "@webpack";
import {
GuildStore,
ScrollerAuto,
ScrollerNone,
ScrollerThin,
TabBar,
useEffect,
useRef,
UserStore,
useState,
} from "@webpack/common";
export {
Button,
Card,
GuildStore,
Heading,
Paragraph,
ScrollerAuto,
ScrollerNone,
ScrollerThin,
TabBar,
useEffect,
useRef,
UserStore,
useState,
};
export * from "../constants";
import {
AccordionProps,
AnchorProps,
AvatarProps,
BadgeShapesType,
CheckboxGroupOption,
ChipProps,
CircleBadgeProps,
ClickableProps,
ColorPickerProps,
ColorPickerWithSwatchesProps,
ColorSwatchProps,
ConfirmModalProps,
CustomColorButtonProps,
DefaultColorButtonProps,
DiscordHeadingProps,
DiscordTextProps,
ExpressiveModalProps,
GradientColor,
GuildIconProps,
HeadingVariant,
IconBadgeProps,
ListboxItem,
ManaBaseRadioGroupProps,
@ -39,15 +80,22 @@ import {
ManaTextButtonProps,
ManaTextInputProps,
ManaTooltipProps,
ModalAction,
ModalNotice,
ModalProps,
ModalRenderFn,
NoticeProps,
NoticeType,
NumberBadgeProps,
OpenModalOptions,
PaginatorProps,
PopoverAction,
ProgressBarProps,
RadioOption,
SearchBarProps,
SkeletonProps,
SliderProps,
SpinnerComponent,
SpinnerProps,
StandaloneRadioIndicatorProps,
TabBarComponent,
@ -55,21 +103,37 @@ import {
TabBarItemProps,
TabBarProps,
TabBarSeparatorProps,
TextBadgeProps
TextBadgeProps,
TextColor,
TextVariant,
TimestampProps,
ToastData,
ToastOptions,
ToastPositionValue,
ToastsModule,
ToastTypeValue,
} from "../types";
export type {
AccordionProps,
AnchorProps,
AvatarProps,
BadgeShapesType,
CheckboxGroupOption,
ChipProps,
CircleBadgeProps,
ColorPickerProps,
ColorPickerWithSwatchesProps,
ColorSwatchProps,
ConfirmModalProps,
CustomColorButtonProps,
DefaultColorButtonProps,
DiscordHeadingProps,
DiscordTextProps,
ExpressiveModalProps,
GradientColor,
GuildIconProps,
HeadingVariant,
IconBadgeProps,
ListboxItem,
ManaBaseRadioGroupProps,
@ -89,14 +153,20 @@ export type {
ManaTextButtonProps,
ManaTextInputProps,
ManaTooltipProps,
ModalAction,
ModalNotice,
ModalProps,
ModalRenderFn,
NoticeProps,
NoticeType,
NumberBadgeProps,
OpenModalOptions,
PaginatorProps,
PopoverAction,
ProgressBarProps,
RadioOption,
SearchBarProps,
SkeletonProps,
SliderProps,
SpinnerProps,
StandaloneRadioIndicatorProps,
@ -105,9 +175,34 @@ export type {
TabBarItemProps,
TabBarProps,
TabBarSeparatorProps,
TextBadgeProps
TextBadgeProps,
TextColor,
TextVariant,
TimestampProps,
ToastData,
ToastOptions,
ToastPositionValue,
ToastsModule,
ToastTypeValue,
};
export const ToastType = {
MESSAGE: "message",
SUCCESS: "success",
FAILURE: "failure",
CUSTOM: "custom",
CLIP: "clip",
LINK: "link",
FORWARD: "forward",
BOOKMARK: "bookmark",
CLOCK: "clock",
} as const;
export const ToastPosition = {
TOP: 0,
BOTTOM: 1,
} as const;
export const Anchor = findComponentByCodeLazy("anchorUnderlineOnHover", "useDefaultUnderlineStyles") as React.ComponentType<AnchorProps>;
export const ManaButton = findComponentByCodeLazy('"data-mana-component":"button"') as React.ComponentType<ManaButtonProps>;
@ -117,7 +212,7 @@ export const ManaTextInput = findComponentByCodeLazy('"data-mana-component":"tex
export const ManaTextArea = findComponentByCodeLazy('"data-mana-component":"text-area"') as React.ComponentType<ManaTextAreaProps>;
export const ManaCheckbox = findComponentByCodeLazy('"data-mana-component":"checkbox"') as React.ComponentType<ManaCheckboxProps>;
export const ManaSelect = findComponentByCodeLazy('"data-mana-component":"select"') as React.ComponentType<ManaSelectProps>;
export const ManaTooltip = findComponentByCodeLazy('"data-mana-component":"tooltip"') as React.ComponentType<ManaTooltipProps>;
export const ManaTooltip = findComponentByCodeLazy("VoidTooltip cannot find DOM node") as React.ComponentType<ManaTooltipProps>;
export const ManaCalendar = findComponentByCodeLazy('"data-mana-component":"calendar"') as React.ComponentType<ManaCalendarProps>;
export const ManaDatePicker = findComponentByCodeLazy('"data-mana-component":"date-picker"') as React.ComponentType<ManaDatePickerProps>;
export const ManaRichTooltip = findComponentByCodeLazy('"data-mana-component":"rich-tooltip"') as React.ComponentType<ManaRichTooltipProps>;
@ -131,7 +226,7 @@ export const ManaPopover = findComponentByCodeLazy('"data-mana-component":"popov
export const Slider = findComponentByCodeLazy("stickToMarkers", "onMarkerRender", "grabberClassName") as React.ComponentType<SliderProps>;
export const Avatar = findComponentByCodeLazy("statusTooltip", "statusBackdropColor", "isSpeaking") as React.ComponentType<AvatarProps>;
export const ProgressBar = findComponentByCodeLazy("progressContainer", "labelledBy", "aria-valuenow") as React.ComponentType<ProgressBarProps>;
export const Spinner = findComponentByCodeLazy("spinningCircleSimple", "pulsingEllipsis", "wanderingCubes") as React.ComponentType<SpinnerProps>;
export const Spinner = findComponentByCodeLazy("spinningCircleSimple", "pulsingEllipsis", "wanderingCubes") as unknown as SpinnerComponent;
export const SearchBar = findComponentByCodeLazy("#{intl::SEARCH}", "clearable", "autoComplete") as React.ComponentType<SearchBarProps>;
export const Paginator2 = findComponentByCodeLazy("#{intl::BACK}", "#{intl::NEXT}", "renderPageWrapper") as React.ComponentType<PaginatorProps>;
export const Notice = findComponentByCodeLazy("messageType", "iconDiv", "actionContainer") as React.ComponentType<NoticeProps>;
@ -184,6 +279,52 @@ export const IconBadge: React.ComponentType<IconBadgeProps> = proxyLazy(() => ge
export const CircleBadge: React.ComponentType<CircleBadgeProps> = proxyLazy(() => getBadgeExport(fn => fn.toString().includes("disableColor") && !fn.toString().includes("iconBadge") && !fn.toString().includes("textBadge") && !fn.toString().includes("numberBadge") && !fn.toString().includes("premiumBadge")));
export const BadgeShapes = proxyLazy(() => Object.values(BadgeModule).find(v => typeof v === "object" && (v as BadgeShapesType)?.ROUND) as BadgeShapesType ?? { ROUND: "", ROUND_LEFT: "", ROUND_RIGHT: "", SQUARE: "" });
export const TabBar = findByCodeLazy("this.tabBarRef", "renderChildren") as TabBarComponent;
export const Clickable = findByCodeLazy("ignoreKeyPress", "renderNonInteractive") as React.ComponentType<ClickableProps>;
export const Chip = findComponentByCodeLazy('variant:"eyebrow"', "chip,") as React.ComponentType<ChipProps>;
export const Skeleton = findComponentByCodeLazy("productSkeletonCardContainer", "skipPulseAnimation") as React.ComponentType<SkeletonProps>;
export const Accordion = findComponentByCodeLazy("accordionContainer", "onExpandedChange", "defaultExpanded") as React.ComponentType<AccordionProps>;
export const Timestamp = findComponentByCodeLazy("isVisibleOnlyOnHover", "cozyAlt", "timestampFormat") as React.ComponentType<TimestampProps>;
export const GuildIcon = findComponentByCodeLazy("getGuildIconURL", "acronym") as React.ComponentType<GuildIconProps>;
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) {
const Component = DiscordTextModule.Heading;
return <Component {...props} />;
}
export function DiscordText(props: DiscordTextProps) {
const Component = DiscordTextModule.Text;
return <Component {...props} />;
}
export function Divider({ className, style, ...restProps }: React.ComponentPropsWithoutRef<"hr">) {
return <hr className={`vc-divider${className ? ` ${className}` : ""}`} style={style} {...restProps} />;
}

View file

@ -0,0 +1,73 @@
/*
* 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 { SectionWrapper } from "../SectionWrapper";
export default function AccordionTab() {
const [controlled, setControlled] = useState(false);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Accordion">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Click to expand/collapse content.
</Paragraph>
<Accordion title="Click to expand">
<Paragraph>This is the accordion content that is revealed when expanded.</Paragraph>
</Accordion>
</SectionWrapper>
<SectionWrapper title="With Subtitle">
<Accordion title="Settings" subtitle="Configure options">
<Paragraph>Content with a subtitle in the header.</Paragraph>
</Accordion>
</SectionWrapper>
<SectionWrapper title="Default Expanded">
<Accordion title="Already Open" defaultExpanded>
<Paragraph>This accordion starts expanded by default.</Paragraph>
</Accordion>
</SectionWrapper>
<SectionWrapper title="Controlled">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Use isExpanded and onExpandedChange for controlled state.
</Paragraph>
<Accordion
title="Controlled Accordion"
isExpanded={controlled}
onExpandedChange={setControlled}
>
<Paragraph>This accordion's state is controlled externally.</Paragraph>
</Accordion>
</SectionWrapper>
<SectionWrapper title="With Max Height">
<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>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• title: string - Header title</Paragraph>
<Paragraph color="text-muted">• subtitle?: string - Optional subtitle</Paragraph>
<Paragraph color="text-muted">• icon?: ReactNode - Optional icon</Paragraph>
<Paragraph color="text-muted">• children: ReactNode - Content</Paragraph>
<Paragraph color="text-muted">• defaultExpanded?: boolean - Initial state (default: false)</Paragraph>
<Paragraph color="text-muted">• isExpanded?: boolean - Controlled expanded state</Paragraph>
<Paragraph color="text-muted">• onExpandedChange?: (expanded: boolean) =&gt; void</Paragraph>
<Paragraph color="text-muted">• onOpen?: () =&gt; void - Called when opened</Paragraph>
<Paragraph color="text-muted">• maxHeight?: number | string - Max content height</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,9 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { Anchor } from "..";
import { Anchor , Paragraph } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function AnchorTab() {

View file

@ -4,14 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { UserStore } from "@webpack/common";
import { Avatar } from "..";
import { Avatar, AvatarSizes, Paragraph, UserStore } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SIZES = ["SIZE_16", "SIZE_20", "SIZE_24", "SIZE_32", "SIZE_40", "SIZE_48", "SIZE_56", "SIZE_80", "SIZE_120"] as const;
export default function AvatarTab() {
const currentUser = UserStore.getCurrentUser();
const avatarUrl = currentUser?.getAvatarURL?.(undefined, 128);
@ -23,7 +18,7 @@ export default function AvatarTab() {
Avatar component from @webpack/common with various sizes.
</Paragraph>
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
{SIZES.map(size => (
{AvatarSizes.map(size => (
<div key={size} style={{ textAlign: "center" }}>
<Avatar
src={avatarUrl}
@ -109,7 +104,7 @@ export default function AvatarTab() {
<SectionWrapper title="Props">
<Paragraph color="text-muted">• src - Avatar image URL</Paragraph>
<Paragraph color="text-muted">• size - SIZE_16 to SIZE_120</Paragraph>
<Paragraph color="text-muted">• size - 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</Paragraph>
<Paragraph color="text-muted">• status - online, idle, dnd, offline</Paragraph>
<Paragraph color="text-muted">• isMobile - Show mobile indicator</Paragraph>
<Paragraph color="text-muted">• isTyping - Show typing indicator</Paragraph>

View file

@ -4,9 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, TextBadge } from "..";
import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, Paragraph,TextBadge } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";

View file

@ -4,15 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaButton } from "..";
import { ManaButton, ManaButtonSizes, ManaButtonVariants, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
const VARIANTS = ["primary", "secondary", "critical-primary", "critical-secondary", "overlay-primary", "overlay-secondary", "expressive"] as const;
const SIZES = ["xs", "sm", "md"] as const;
export default function ButtonsTab() {
const [loading, setLoading] = useState(false);
@ -20,7 +14,7 @@ export default function ButtonsTab() {
<div className="vc-compfinder-section">
<SectionWrapper title="Variants (size: md)">
<div className="vc-compfinder-grid">
{VARIANTS.map(variant => (
{ManaButtonVariants.map(variant => (
<ManaButton key={variant} variant={variant} size="md" text={variant} />
))}
</div>
@ -28,7 +22,7 @@ export default function ButtonsTab() {
<SectionWrapper title="Sizes (variant: primary)">
<div className="vc-compfinder-grid">
{SIZES.map(size => (
{ManaButtonSizes.map(size => (
<ManaButton key={size} variant="primary" size={size} text={`Size: ${size}`} />
))}
</div>
@ -54,7 +48,7 @@ export default function ButtonsTab() {
<SectionWrapper title="All Variants Disabled">
<div className="vc-compfinder-grid">
{VARIANTS.map(variant => (
{ManaButtonVariants.map(variant => (
<ManaButton key={variant} variant={variant} size="md" text={variant} disabled />
))}
</div>

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaCalendar, ManaDatePicker } from "..";
import { ManaCalendar, ManaDatePicker, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function CalendarTab() {

View file

@ -0,0 +1,105 @@
/*
* 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 { SectionWrapper } from "../SectionWrapper";
export default function CardTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Card Variants">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Card component from @components/Card with different severity styles.
</Paragraph>
<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)} Card
</Paragraph>
<Paragraph color="text-muted">
This is a {variant} card variant. Use for {
variant === "normal" ? "general content" :
variant === "warning" ? "cautionary messages" :
"error or destructive actions"
}.
</Paragraph>
</Card>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Default Padding">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Cards have 1em padding by default when no className is passed.
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div>
<Paragraph color="text-muted" style={{ fontSize: 10, marginBottom: 4 }}>
defaultPadding=true (or no className)
</Paragraph>
<Card defaultPadding>
<Paragraph>Card with default padding</Paragraph>
</Card>
</div>
<div>
<Paragraph color="text-muted" style={{ fontSize: 10, marginBottom: 4 }}>
defaultPadding=false
</Paragraph>
<Card defaultPadding={false}>
<Paragraph style={{ padding: 8 }}>Card without default padding (content adds own)</Paragraph>
</Card>
</div>
</div>
</SectionWrapper>
<SectionWrapper title="Nested Cards">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Cards can be nested for complex layouts.
</Paragraph>
<Card variant="normal">
<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>
</SectionWrapper>
<SectionWrapper title="Custom Styling">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Cards accept custom className and style props.
</Paragraph>
<Card
variant="normal"
style={{
borderLeft: "4px solid var(--text-brand)",
borderRadius: 4,
}}
>
<Paragraph>Card with custom border accent</Paragraph>
</Card>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• variant - "normal" | "warning" | "danger" (default: "normal")</Paragraph>
<Paragraph color="text-muted">• defaultPadding - Add 1em padding (default: true if no className)</Paragraph>
<Paragraph color="text-muted">• className - Custom CSS class</Paragraph>
<Paragraph color="text-muted">• style - Inline styles</Paragraph>
<Paragraph color="text-muted">• children - Card content</Paragraph>
<Paragraph color="text-muted">• ...restProps - Any other div attributes</Paragraph>
</SectionWrapper>
<SectionWrapper title="Import">
<Paragraph color="text-muted">
{"import { Card } from \"@components/Card\";"}
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { CheckboxGroupOption, ManaCheckboxGroup } from "..";
import { CheckboxGroupOption, ManaCheckboxGroup, Paragraph, useState } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaCheckbox } from "..";
import { ManaCheckbox, Paragraph, useState } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";

View file

@ -0,0 +1,55 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Chip , Paragraph } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function ChipTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Colors">
<div className="vc-compfinder-grid">
<Chip text="Blurple" variant="blurpleMedium" />
<Chip text="Purple" variant="purpleMedium" />
<Chip text="Green" variant="greenMedium" />
<Chip text="Orange" variant="orangeMedium" />
<Chip text="Yellow" variant="yellowMedium" />
<Chip text="Pink" variant="pinkMedium" />
<Chip text="Red" variant="redMedium" />
<Chip text="Gray" variant="grayMedium" />
</div>
</SectionWrapper>
<SectionWrapper title="Shades">
<div className="vc-compfinder-grid">
<Chip text="Light" variant="blurpleLight" />
<Chip text="Medium" variant="blurpleMedium" />
<Chip text="Dark" variant="blurpleDark" />
</div>
</SectionWrapper>
<SectionWrapper title="Use Cases">
<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>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• text: string - Text to display inside the chip
</Paragraph>
<Paragraph color="text-muted">
• variant?: ChipVariant - Color variant (default: "grayLight")
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { Clickable } from "..";
import { Clickable, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function ClickableTab() {

View file

@ -4,21 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ColorPicker, ColorPickerWithSwatches, ColorSwatch, CustomColorButton, DefaultColorButton } from "..";
import { ColorPicker, ColorPickerWithSwatches, ColorSwatch, CustomColorButton, DefaultColorButton, Paragraph, PresetColors, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
const DEFAULT_COLOR = 0x5865F2;
const PRESET_COLORS = [
0x1ABC9C, 0x2ECC71, 0x3498DB, 0x9B59B6, 0xE91E63,
0xF1C40F, 0xE67E22, 0xE74C3C, 0x95A5A6, 0x607D8B,
0x11806A, 0x1F8B4C, 0x206694, 0x71368A, 0xAD1457,
0xC27C0E, 0xA84300, 0x992D22, 0x979C9F, 0x546E7A,
];
function intToHex(color: number): string {
return "#" + color.toString(16).padStart(6, "0");
}
@ -84,7 +74,7 @@ export default function ColorPickerTab() {
Individual color swatch buttons. Click to select.
</Paragraph>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{PRESET_COLORS.slice(0, 10).map(color => (
{PresetColors.slice(0, 10).map(color => (
<ColorSwatch
key={color}
color={color}
@ -102,27 +92,27 @@ export default function ColorPickerTab() {
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} />
<Paragraph color="text-muted" size="xs">Normal</Paragraph>
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Normal</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} isSelected />
<Paragraph color="text-muted" size="xs">Selected</Paragraph>
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Selected</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} isDefault />
<Paragraph color="text-muted" size="xs">Default</Paragraph>
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Default</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} isCustom />
<Paragraph color="text-muted" size="xs">Custom</Paragraph>
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Custom</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch color={0x5865F2} disabled />
<Paragraph color="text-muted" size="xs">Disabled</Paragraph>
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Disabled</Paragraph>
</div>
<div style={{ textAlign: "center" }}>
<ColorSwatch />
<Paragraph color="text-muted" size="xs">No Color</Paragraph>
<Paragraph color="text-muted" style={{ fontSize: 10 }}>No Color</Paragraph>
</div>
</div>
</SectionWrapper>
@ -174,7 +164,7 @@ export default function ColorPickerTab() {
<CustomColorButton
customColor={customBtnColor}
value={customBtnColor}
presets={PRESET_COLORS}
presets={[...PresetColors]}
/>
</SectionWrapper>
@ -184,7 +174,7 @@ export default function ColorPickerTab() {
</Paragraph>
<ColorPickerWithSwatches
defaultColor={DEFAULT_COLOR}
colors={PRESET_COLORS}
colors={[...PresetColors]}
value={swatchColor}
onChange={setSwatchColor}
renderDefaultButton={() => null}
@ -199,7 +189,7 @@ export default function ColorPickerTab() {
<ColorPickerWithSwatches
defaultColor={DEFAULT_COLOR}
customColor={customSwatchColor}
colors={PRESET_COLORS}
colors={[...PresetColors]}
value={customSwatchColor}
onChange={setCustomSwatchColor}
renderDefaultButton={props => (

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaButton, ManaCombobox } from "..";
import { ManaButton, ManaCombobox, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SAMPLE_ITEMS = [

View file

@ -0,0 +1,52 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Divider, Paragraph } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function DividerTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Vencord Divider">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Simple horizontal rule from @components/Divider.
</Paragraph>
<div style={{ padding: "16px 0" }}>
<Paragraph>Content above divider</Paragraph>
<Divider />
<Paragraph>Content below divider</Paragraph>
</div>
</SectionWrapper>
<SectionWrapper title="Custom Styled Dividers">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Dividers with custom margins and styles.
</Paragraph>
<div style={{ padding: "16px 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>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• className - Custom CSS class</Paragraph>
<Paragraph color="text-muted">• style - Inline styles</Paragraph>
<Paragraph color="text-muted">• ...restProps - Any other hr attributes</Paragraph>
</SectionWrapper>
<SectionWrapper title="Import">
<Paragraph color="text-muted">
{"import { Divider } from \"@components/Divider\";"}
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,111 @@
/*
* 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 { SectionWrapper } from "../SectionWrapper";
export default function GuildIconTab() {
const [animate, setAnimate] = useState(true);
const guilds = Object.values(GuildStore.getGuilds()).slice(0, 6);
const guildWithIcon = guilds.find(g => g.icon);
const guildWithoutIcon = guilds.find(g => !g.icon) ?? guilds[0];
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Controls">
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text={`Animate: ${animate}`}
onClick={() => setAnimate(a => !a)}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Icon Sizes">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
GuildIcon component with various sizes. Shows server icon or acronym fallback.
</Paragraph>
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
{guildWithIcon && 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>
</SectionWrapper>
<SectionWrapper title="Acronym Fallback">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
When a guild has no icon, displays an acronym based on the guild name.
</Paragraph>
<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>
</SectionWrapper>
<SectionWrapper title="Your Guilds">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Showing icons from guilds you're in.
</Paragraph>
<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>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• guildId - The guild's snowflake ID</Paragraph>
<Paragraph color="text-muted">• guildName - Guild name (used for acronym fallback)</Paragraph>
<Paragraph color="text-muted">• guildIcon - Icon hash or null</Paragraph>
<Paragraph color="text-muted">• iconSize - Size in pixels</Paragraph>
<Paragraph color="text-muted">• animate - Enable animated icons (GIF)</Paragraph>
<Paragraph color="text-muted">• className - Custom CSS class</Paragraph>
<Paragraph color="text-muted">• acronymClassName - Class for acronym text</Paragraph>
</SectionWrapper>
<SectionWrapper title="Import">
<Paragraph color="text-muted">
{"import { GuildIcon } from \"@equicordplugins/components.dev/components\";"}
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,94 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DiscordHeading, DiscordText, HeadingVariants, Paragraph,TextColors, TextVariants } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function HeadingTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Discord Heading">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Discord's Heading component with variant prop. Auto-sets h1-h6 tag based on nesting level.
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{HeadingVariants.slice(0, 10).map(variant => (
<DiscordHeading key={variant} variant={variant}>
{variant}
</DiscordHeading>
))}
</div>
</SectionWrapper>
<SectionWrapper title="More Heading Sizes">
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{HeadingVariants.slice(10).map(variant => (
<DiscordHeading key={variant} variant={variant}>
{variant}
</DiscordHeading>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Discord Text">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Discord's Text component for body text with various sizes and weights.
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{TextVariants.slice(0, 12).map(variant => (
<DiscordText key={variant} variant={variant}>
{variant}
</DiscordText>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Smaller Text Sizes">
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{TextVariants.slice(12).map(variant => (
<DiscordText key={variant} variant={variant}>
{variant}
</DiscordText>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Text Colors">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Available color props for Text component.
</Paragraph>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{TextColors.map(color => (
<DiscordText key={color} variant="text-md/medium" color={color}>
{color}
</DiscordText>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Props - DiscordHeading">
<Paragraph color="text-muted">• variant - "heading-SIZE/WEIGHT" (xxl, xl, lg, md, sm)</Paragraph>
<Paragraph color="text-muted">• className - Custom CSS class</Paragraph>
<Paragraph color="text-muted">• color - Text color (same as Text colors)</Paragraph>
</SectionWrapper>
<SectionWrapper title="Props - DiscordText">
<Paragraph color="text-muted">• variant - "text-SIZE/WEIGHT" (xxl, xl, lg, md, sm, xs)</Paragraph>
<Paragraph color="text-muted">• color - Text color name</Paragraph>
<Paragraph color="text-muted">• tag - HTML tag to render (default: "div")</Paragraph>
<Paragraph color="text-muted">• selectable - Allow text selection</Paragraph>
<Paragraph color="text-muted">• lineClamp - Number of lines before truncation</Paragraph>
<Paragraph color="text-muted">• tabularNumbers - Use tabular number spacing</Paragraph>
</SectionWrapper>
<SectionWrapper title="Import">
<Paragraph color="text-muted">
{"import { DiscordHeading, DiscordText, HeadingVariants, TextVariants, TextColors } from \"../components\";"}
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,13 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { openModal } from "@utils/modal";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { useState } from "@webpack/common";
import { ManaButton } from "..";
import { Heading, ManaButton, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
const LayerModal = findComponentByCodeLazy('"data-mana-component":"layer-modal"') as React.ComponentType<{

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ListboxItem, ManaListbox } from "..";
import { ListboxItem, ManaListbox, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SAMPLE_ITEMS: ListboxItem[] = [

View file

@ -4,156 +4,229 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { classNameFactory } from "@utils/css";
import {
ModalContent,
ModalFooter,
ModalHeader,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { findComponentByCodeLazy } from "@webpack";
import { ManaButton } from "..";
closeAllModals,
ConfirmModal,
ExpressiveModal,
ManaButton,
ManaTextInput,
Modal,
openModal,
Paragraph,
showToast,
ToastType,
useState,
} from "..";
import { SectionWrapper } from "../SectionWrapper";
const cl = classNameFactory("vc-compfinder-modal-");
const ModalCloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL") as React.ComponentType<{
onClick: () => void;
className?: string;
withCircleBackground?: boolean;
hideOnFullscreen?: boolean;
}>;
const SIZES = [
{ size: ModalSize.SMALL, label: "Small" },
{ size: ModalSize.MEDIUM, label: "Medium" },
{ size: ModalSize.LARGE, label: "Large" },
{ size: ModalSize.DYNAMIC, label: "Dynamic" },
] as const;
function openDemoModal(size: ModalSize, label: string, showCloseButton = true, showFooter = true) {
function openBasicModal() {
openModal(props => (
<ModalRoot {...props} size={size}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<Heading tag="h2">
{label} Modal
</Heading>
<Paragraph color="text-muted">
This is a {label.toLowerCase()} modal using Discord's native modal system.
</Paragraph>
</div>
{showCloseButton && (
<ModalCloseButton
onClick={props.onClose}
className={cl("close")}
/>
)}
</ModalHeader>
<ModalContent className={cl("content")}>
<Paragraph color="text-muted">
Size: {size}
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 8 }}>
The modal header, content, and footer use Discord's native components
from the webpack modules.
</Paragraph>
</ModalContent>
{showFooter && (
<ModalFooter className={cl("footer")}>
<ManaButton variant="primary" text="Confirm" onClick={props.onClose} />
<ManaButton variant="secondary" text="Cancel" onClick={props.onClose} />
</ModalFooter>
)}
</ModalRoot>
<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 content. You can put any React components here.</Paragraph>
</Modal>
));
}
function openModalWithNotice() {
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..."
/>
<Paragraph color="text-muted" style={{ marginTop: 8 }}>
Using ManaTextInput inside the modal content.
</Paragraph>
</Modal>
);
}
function openModalWithInput() {
openModal(props => (
<ModalWithInputContent onClose={props.onClose} transitionState={props.transitionState} />
));
}
function openConfirmModal() {
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 openConfirmModalPrimary() {
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 openExpressiveModal() {
openModal(props => (
<ExpressiveModal
title="Welcome!"
description="This is an expressive modal with a more visual design."
transitionState={props.transitionState}
actions={[
{ text: "Get Started", variant: "primary", onClick: props.onClose },
]}
onClose={props.onClose}
/>
));
}
export default function ModalTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Modal Sizes">
<SectionWrapper title="Basic Modal">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Uses ModalRoot, ModalHeader, ModalContent, ModalFooter from @utils/modal
and native ModalCloseButton via findComponentByCodeLazy("CLOSE_BUTTON_LABEL")
Simple modal with title, subtitle, content, and action buttons.
</Paragraph>
<div className="vc-compfinder-grid">
{SIZES.map(({ size, label }) => (
<ManaButton
key={size}
variant="secondary"
text={label}
onClick={() => openDemoModal(size, label)}
/>
))}
</div>
<ManaButton variant="secondary" text="Open Basic Modal" onClick={openBasicModal} />
</SectionWrapper>
<SectionWrapper title="Without Close Button">
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text="Open Modal"
onClick={() => openDemoModal(ModalSize.SMALL, "No Close Button", false, true)}
/>
</div>
<SectionWrapper title="Modal with Notice">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Modal with a notice banner (info, warn, critical, positive).
</Paragraph>
<ManaButton variant="secondary" text="Open Modal with Notice" onClick={openModalWithNotice} />
</SectionWrapper>
<SectionWrapper title="Without Footer">
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text="Open Modal"
onClick={() => openDemoModal(ModalSize.SMALL, "No Footer", true, false)}
/>
</div>
<SectionWrapper title="Modal with Input">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Modal with input controls in the header area.
</Paragraph>
<ManaButton variant="secondary" text="Open Modal with Input" onClick={openModalWithInput} />
</SectionWrapper>
<SectionWrapper title="Minimal (No Close Button or Footer)">
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text="Open Modal"
onClick={() => openDemoModal(ModalSize.DYNAMIC, "Minimal", false, false)}
/>
</div>
<SectionWrapper title="Confirm Modal (Critical)">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Confirmation dialog for destructive actions.
</Paragraph>
<ManaButton variant="critical-secondary" text="Open Confirm Modal" onClick={openConfirmModal} />
</SectionWrapper>
<SectionWrapper title="Props">
<SectionWrapper title="Confirm Modal (Primary)">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Confirmation dialog for non-destructive actions.
</Paragraph>
<ManaButton variant="secondary" text="Open Save Dialog" onClick={openConfirmModalPrimary} />
</SectionWrapper>
<SectionWrapper title="Expressive Modal">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Visual modal with graphic/badge support.
</Paragraph>
<ManaButton variant="secondary" text="Open Expressive Modal" onClick={openExpressiveModal} />
</SectionWrapper>
<SectionWrapper title="Close All Modals">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Utility to close all open modals at once.
</Paragraph>
<ManaButton variant="critical-secondary" text="Close All Modals" onClick={closeAllModals} />
</SectionWrapper>
<SectionWrapper title="Modal Props">
<Paragraph color="text-muted">• title - Modal title (required)</Paragraph>
<Paragraph color="text-muted">• subtitle - Optional subtitle</Paragraph>
<Paragraph color="text-muted">• size - "sm" or "md"</Paragraph>
<Paragraph color="text-muted">• children - Modal content</Paragraph>
<Paragraph color="text-muted">• actions - Array of action buttons</Paragraph>
<Paragraph color="text-muted">• notice - {"{ message, type }"} for notices</Paragraph>
<Paragraph color="text-muted">• input - Controls in header area</Paragraph>
<Paragraph color="text-muted">• onClose - Close handler (required)</Paragraph>
</SectionWrapper>
<SectionWrapper title="ConfirmModal Props">
<Paragraph color="text-muted">• confirmText - Confirm button text</Paragraph>
<Paragraph color="text-muted">• cancelText - Cancel button text</Paragraph>
<Paragraph color="text-muted">• variant - "critical" | "primary" | "secondary"</Paragraph>
<Paragraph color="text-muted">• onConfirm - Confirm callback (can be async)</Paragraph>
<Paragraph color="text-muted">• onCancel - Cancel callback</Paragraph>
<Paragraph color="text-muted">• checkboxProps - Optional checkbox</Paragraph>
</SectionWrapper>
<SectionWrapper title="Action Button">
<Paragraph color="text-muted">• text - Button text</Paragraph>
<Paragraph color="text-muted">• variant - "primary" | "secondary" | "critical-primary"</Paragraph>
<Paragraph color="text-muted">• onClick - Click handler</Paragraph>
<Paragraph color="text-muted">• disabled - Disable button</Paragraph>
<Paragraph color="text-muted">• loading - Show loading state</Paragraph>
</SectionWrapper>
<SectionWrapper title="openModal Options">
<Paragraph color="text-muted">• modalKey - Unique key for the modal</Paragraph>
<Paragraph color="text-muted">• dismissable - Allow clicking backdrop to close</Paragraph>
<Paragraph color="text-muted">• instant - Skip animation</Paragraph>
<Paragraph color="text-muted">• onCloseCallback - Called when modal closes</Paragraph>
</SectionWrapper>
<SectionWrapper title="Import">
<Paragraph color="text-muted">
<strong>ModalRoot</strong> - from @utils/modal
</Paragraph>
<Paragraph color="text-muted">
• size: ModalSize - SMALL, MEDIUM, LARGE, or DYNAMIC
</Paragraph>
<Paragraph color="text-muted">
• ...modalProps - Props from openModal callback
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ModalHeader</strong>
</Paragraph>
<Paragraph color="text-muted">
• separator?: boolean - Show separator line (default: true)
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>ModalCloseButton</strong> - findComponentByCodeLazy("CLOSE_BUTTON_LABEL")
</Paragraph>
<Paragraph color="text-muted">
• onClick: () =&gt; void - Close handler
</Paragraph>
<Paragraph color="text-muted">
• withCircleBackground?: boolean - Circle background style
</Paragraph>
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
<strong>openModal</strong> - from @utils/modal
</Paragraph>
<Paragraph color="text-muted">
• (render: (props) =&gt; ReactNode) =&gt; string - Returns modal key
{"import { Modal, ConfirmModal, openModal, closeModal } from \"../components\";"}
</Paragraph>
</SectionWrapper>
</div>

View file

@ -4,14 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button } from "@components/Button";
import { Paragraph } from "@components/Paragraph";
import { Notice, NoticeType } from "..";
import { Button, Notice, NoticeTypes, Paragraph } from "..";
import { SectionWrapper } from "../SectionWrapper";
const NOTICE_TYPES: NoticeType[] = ["info", "warn", "danger", "positive", "preview"];
export default function NoticeTab() {
return (
<div className="vc-compfinder-section">
@ -20,7 +15,7 @@ export default function NoticeTab() {
Notice component for displaying informational messages with different severity levels.
</Paragraph>
<div className="vc-compfinder-grid">
{NOTICE_TYPES.map(type => (
{NoticeTypes.map(type => (
<div key={type} style={{ textAlign: "center" }}>
<Notice messageType={type}>
This is a {type} notice message.

View file

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

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useRef, useState } from "@webpack/common";
import { ManaButton, ManaPopover, PopoverAction } from "..";
import { ManaButton, ManaPopover, Paragraph, PopoverAction, useRef, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
function PopoverDemo({ title, body, size, actions, position, label }: { title: string; body: string; size?: "sm" | "md" | "lg"; actions?: PopoverAction[]; position?: "top" | "bottom" | "left" | "right"; label: string; }) {

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useEffect, useState } from "@webpack/common";
import { ProgressBar } from "..";
import { Paragraph, ProgressBar, useEffect, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function ProgressBarTab() {

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaBaseRadioGroup, RadioOption, StandaloneRadioIndicator } from "..";
import { ManaBaseRadioGroup, Paragraph, RadioOption, StandaloneRadioIndicator, useState } from "..";
import { EquicordIcon } from "../icons/EquicordIcon";
import { SectionWrapper } from "../SectionWrapper";

View file

@ -4,9 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { ManaButton, ManaRichTooltip } from "..";
import { ManaButton, ManaRichTooltip , Paragraph } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function RichTooltipTab() {

View file

@ -0,0 +1,105 @@
/*
* 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 { SectionWrapper } from "../SectionWrapper";
const SAMPLE_ITEMS = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`);
export default function ScrollerTab() {
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 className="vc-compfinder-section">
<SectionWrapper title="Controls">
<div className="vc-compfinder-grid">
<ManaButton
variant="secondary"
text={`Orientation: ${orientation}`}
onClick={() => setOrientation(o => o === "vertical" ? "horizontal" : "vertical")}
/>
<ManaButton
variant="secondary"
text={`Fade: ${fade}`}
onClick={() => setFade(f => !f)}
/>
</div>
</SectionWrapper>
<SectionWrapper title="ScrollerThin">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Thin scrollbar, most commonly used. Ideal for sidebars and lists.
</Paragraph>
<ScrollerThin orientation={orientation} fade={fade} style={scrollerStyle}>
{SAMPLE_ITEMS.map(item => (
<div key={item} style={itemStyle}>
<Paragraph>{item}</Paragraph>
</div>
))}
</ScrollerThin>
</SectionWrapper>
<SectionWrapper title="ScrollerAuto">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Standard scrollbar that appears automatically when content overflows.
</Paragraph>
<ScrollerAuto orientation={orientation} fade={fade} style={scrollerStyle}>
{SAMPLE_ITEMS.map(item => (
<div key={item} style={itemStyle}>
<Paragraph>{item}</Paragraph>
</div>
))}
</ScrollerAuto>
</SectionWrapper>
<SectionWrapper title="ScrollerNone">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
No visible scrollbar, content still scrollable via mouse wheel/touch.
</Paragraph>
<ScrollerNone orientation={orientation} fade={fade} style={scrollerStyle}>
{SAMPLE_ITEMS.map(item => (
<div key={item} style={itemStyle}>
<Paragraph>{item}</Paragraph>
</div>
))}
</ScrollerNone>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• orientation - "vertical" | "horizontal" (default: "vertical")</Paragraph>
<Paragraph color="text-muted">• fade - boolean, adds fade effect at edges</Paragraph>
<Paragraph color="text-muted">• className - custom CSS class</Paragraph>
<Paragraph color="text-muted">• style - inline styles</Paragraph>
<Paragraph color="text-muted">• children - content to scroll</Paragraph>
<Paragraph color="text-muted">• dir - text direction ("ltr" | "rtl")</Paragraph>
</SectionWrapper>
<SectionWrapper title="Variants">
<Paragraph color="text-muted">• ScrollerThin - Thin scrollbar (most common)</Paragraph>
<Paragraph color="text-muted">• ScrollerAuto - Standard auto-showing scrollbar</Paragraph>
<Paragraph color="text-muted">• ScrollerNone - Hidden scrollbar</Paragraph>
<Paragraph color="text-muted">• ListScrollerThin - Virtualized list with thin scrollbar</Paragraph>
<Paragraph color="text-muted">• ListScrollerAuto - Virtualized list with auto scrollbar</Paragraph>
<Paragraph color="text-muted">• ListScrollerNone - Virtualized list with hidden scrollbar</Paragraph>
</SectionWrapper>
<SectionWrapper title="Import">
<Paragraph color="text-muted">
{"import { ScrollerThin, ScrollerAuto, ScrollerNone } from \"@webpack/common\";"}
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { SearchBar } from "..";
import { Paragraph, SearchBar, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function SearchBarTab() {

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaSelect, ManaSelectOption } from "..";
import { ManaSelect, ManaSelectOption, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SAMPLE_OPTIONS: ManaSelectOption[] = [

View file

@ -0,0 +1,60 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ManaButton, Paragraph, Skeleton, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function SkeletonTab() {
const [animated, setAnimated] = useState(true);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Default Skeleton">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Loading placeholder with pulse animation. Requires "full-motion" parent class for animation.
</Paragraph>
<div className="vc-compfinder-grid full-motion">
<Skeleton />
<Skeleton />
<Skeleton />
</div>
</SectionWrapper>
<SectionWrapper title="Animation Toggle">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Use skipPulseAnimation to disable the pulse effect.
</Paragraph>
<ManaButton
variant="secondary"
text={animated ? "Disable Animation" : "Enable Animation"}
onClick={() => setAnimated(!animated)}
/>
<div className="vc-compfinder-grid full-motion" style={{ marginTop: 16 }}>
<Skeleton skipPulseAnimation={!animated} />
<Skeleton skipPulseAnimation={!animated} />
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• skipPulseAnimation?: boolean - Disable pulse animation (default: false)
</Paragraph>
</SectionWrapper>
<SectionWrapper title="Notes">
<Paragraph color="text-muted">
• This is a product card skeleton used in Discord's shop.
</Paragraph>
<Paragraph color="text-muted">
• Animation requires a parent with "full-motion" class (Discord's reduced motion setting).
</Paragraph>
<Paragraph color="text-muted">
• The skeleton has fixed dimensions designed for product cards.
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { Slider } from "..";
import { Paragraph, Slider, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function SliderTab() {

View file

@ -4,21 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaButton, Spinner } from "..";
import { ManaButton, Paragraph, Spinner, SpinnerTypes, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
const SPINNER_TYPES = [
"wanderingCubes",
"chasingDots",
"pulsingEllipsis",
"spinningCircle",
"spinningCircleSimple",
"lowMotion"
] as const;
export default function SpinnerTab() {
const [animated, setAnimated] = useState(true);
@ -29,7 +17,7 @@ export default function SpinnerTab() {
All available spinner variants.
</Paragraph>
<div className="vc-compfinder-grid">
{SPINNER_TYPES.map(type => (
{SpinnerTypes.map(type => (
<div key={type} style={{ textAlign: "center", padding: 16 }}>
<Spinner type={type} />
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 8 }}>
@ -75,7 +63,7 @@ export default function SpinnerTab() {
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• type?: "wanderingCubes" | "chasingDots" | "pulsingEllipsis" | "spinningCircle" | "spinningCircleSimple" | "lowMotion"
• type?: SpinnerType - Spinner variant
</Paragraph>
<Paragraph color="text-muted">
• animated?: boolean - Enable/disable animation (default: true)
@ -90,6 +78,15 @@ export default function SpinnerTab() {
• aria-label?: string - Accessibility label
</Paragraph>
</SectionWrapper>
<SectionWrapper title="Type Values">
<Paragraph color="text-muted">• "wanderingCubes"</Paragraph>
<Paragraph color="text-muted">• "chasingDots"</Paragraph>
<Paragraph color="text-muted">• "pulsingEllipsis"</Paragraph>
<Paragraph color="text-muted">• "spinningCircle"</Paragraph>
<Paragraph color="text-muted">• "spinningCircleSimple"</Paragraph>
<Paragraph color="text-muted">• "lowMotion"</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,11 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { findComponentByCodeLazy } from "@webpack";
import { useState } from "@webpack/common";
import { ManaSwitch } from "..";
import { ManaSwitch, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
interface SwitchWithLabelProps {

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { TabBar } from "..";
import { Paragraph, TabBar, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function TabBarTab() {

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaTextArea } from "..";
import { ManaTextArea, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function TextAreaTab() {

View file

@ -4,28 +4,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { ManaTextButton } from "..";
import { ManaTextButton, ManaTextButtonTextVariants, ManaTextButtonVariants, Paragraph } from "..";
import { SectionWrapper } from "../SectionWrapper";
const VARIANTS = ["primary", "secondary", "always-white", "critical"] as const;
const TEXT_VARIANTS = [
"text-xs/normal",
"text-sm/normal",
"text-sm/medium",
"text-md/normal",
"text-md/medium",
"text-lg/medium",
] as const;
export default function TextButtonTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Variants">
<div className="vc-compfinder-grid">
{VARIANTS.map(variant => (
{ManaTextButtonVariants.map(variant => (
<ManaTextButton
key={variant}
text={variant}
@ -37,7 +24,7 @@ export default function TextButtonTab() {
<SectionWrapper title="Text Variants">
<div className="vc-compfinder-grid-vertical">
{TEXT_VARIANTS.map(textVariant => (
{ManaTextButtonTextVariants.map(textVariant => (
<ManaTextButton
key={textVariant}
text={textVariant}

View file

@ -4,10 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { useState } from "@webpack/common";
import { ManaTextInput } from "..";
import { ManaTextInput, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function TextInputTab() {

View file

@ -0,0 +1,166 @@
/*
* 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 { SectionWrapper } from "../SectionWrapper";
export default function TimestampTab() {
const now = new Date();
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Timestamp Formats">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
All available timestamp format options using moment.js format strings.
</Paragraph>
<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>
</SectionWrapper>
<SectionWrapper title="Compact Mode">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Compact mode displays a shorter version of the timestamp.
</Paragraph>
<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>
</SectionWrapper>
<SectionWrapper title="Display Variants">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Different display modes for timestamps.
</Paragraph>
<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>
</SectionWrapper>
<SectionWrapper title="Edited Indicator">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Shows an "(edited)" indicator after the timestamp.
</Paragraph>
<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>
</SectionWrapper>
<SectionWrapper title="Visibility on Hover">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Timestamp can be hidden until hovered.
</Paragraph>
<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>
</SectionWrapper>
<SectionWrapper title="Tooltip Positions">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
The tooltip showing full date can be positioned differently.
</Paragraph>
<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>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
• timestamp: Date | Moment - The timestamp to display (required)
</Paragraph>
<Paragraph color="text-muted">
• timestampFormat?: TimestampFormat - Format string (LT, LTS, L, LL, LLL, LLLL, l, ll, lll, llll)
</Paragraph>
<Paragraph color="text-muted">
• compact?: boolean - Use compact display mode
</Paragraph>
<Paragraph color="text-muted">
• cozyAlt?: boolean - Use cozy alternative display
</Paragraph>
<Paragraph color="text-muted">
• isInline?: boolean - Display inline
</Paragraph>
<Paragraph color="text-muted">
• isVisibleOnlyOnHover?: boolean - Only show on hover
</Paragraph>
<Paragraph color="text-muted">
• isEdited?: boolean - Show "(edited)" indicator
</Paragraph>
<Paragraph color="text-muted">
• tooltipPosition?: "top" | "bottom" | "left" | "right" - Tooltip position
</Paragraph>
<Paragraph color="text-muted">
• id?: string - Element ID
</Paragraph>
<Paragraph color="text-muted">
• className?: string - Additional CSS class
</Paragraph>
<Paragraph color="text-muted">
• children?: ReactNode - Custom content
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,136 @@
/*
* 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, ToastTypeValue } from "..";
import { SectionWrapper } from "../SectionWrapper";
const TOAST_TYPES = [
{ 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" },
] as const;
export default function ToastTab() {
const showSimpleToast = (type: ToastTypeValue, label: string) => {
showToast(`This is a ${label} toast!`, type);
};
const showAdvancedToast = () => {
const toast = Toasts.create("Advanced toast with options", ToastType.SUCCESS, {
duration: 5000,
position: Toasts.Position.BOTTOM,
});
Toasts.show(toast);
};
const showCustomToast = () => {
const toast = Toasts.create("Custom component toast", ToastType.CUSTOM, {
component: <span style={{ color: "var(--text-brand)" }}>Custom JSX content!</span>,
});
Toasts.show(toast);
};
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Toast Types">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Click buttons to show different toast types. Toasts appear at screen edges.
</Paragraph>
<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={() => showSimpleToast(type, label)}
/>
<Paragraph color="text-muted" style={{ fontSize: 10 }}>
{description}
</Paragraph>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Simple Usage">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
showToast(message, type) - Quick way to show a toast.
</Paragraph>
<ManaButton
variant="primary"
text="Show Simple Toast"
onClick={() => showToast("Hello from showToast!", ToastType.SUCCESS)}
/>
</SectionWrapper>
<SectionWrapper title="Advanced Usage">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Toasts.show() with full options object.
</Paragraph>
<div className="vc-compfinder-grid" style={{ gap: 8 }}>
<ManaButton
variant="secondary"
text="Advanced Toast (5s duration)"
onClick={showAdvancedToast}
/>
<ManaButton
variant="secondary"
text="Custom Component Toast"
onClick={showCustomToast}
/>
</div>
</SectionWrapper>
<SectionWrapper title="Pop Toast">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Toasts.pop() removes the most recent toast.
</Paragraph>
<ManaButton
variant="critical-secondary"
text="Pop Last Toast"
onClick={() => Toasts.pop()}
/>
</SectionWrapper>
<SectionWrapper title="ToastType">
<Paragraph color="text-muted">• MESSAGE - Default message style</Paragraph>
<Paragraph color="text-muted">• SUCCESS - Green checkmark icon</Paragraph>
<Paragraph color="text-muted">• FAILURE - Red X icon</Paragraph>
<Paragraph color="text-muted">• CUSTOM - Custom component content</Paragraph>
<Paragraph color="text-muted">• CLIP - Clip/scissors icon</Paragraph>
<Paragraph color="text-muted">• LINK - Link icon</Paragraph>
<Paragraph color="text-muted">• FORWARD - Forward arrow icon</Paragraph>
<Paragraph color="text-muted">• BOOKMARK - Bookmark icon</Paragraph>
<Paragraph color="text-muted">• CLOCK - Clock icon</Paragraph>
</SectionWrapper>
<SectionWrapper title="ToastPosition">
<Paragraph color="text-muted">• TOP (0) - Toast appears at top</Paragraph>
<Paragraph color="text-muted">• BOTTOM (1) - Toast appears at bottom</Paragraph>
</SectionWrapper>
<SectionWrapper title="Toasts.show() Options">
<Paragraph color="text-muted">• message - Toast message text</Paragraph>
<Paragraph color="text-muted">• type - Toast type from ToastType</Paragraph>
<Paragraph color="text-muted">• id - Unique ID (use Toasts.genId())</Paragraph>
<Paragraph color="text-muted">• options.duration - Display time in ms</Paragraph>
<Paragraph color="text-muted">• options.position - ToastPosition value</Paragraph>
<Paragraph color="text-muted">• options.component - Custom React component</Paragraph>
</SectionWrapper>
<SectionWrapper title="Import">
<Paragraph color="text-muted">
{"import { showToast, Toasts, ToastType, ToastPosition } from \"../components\";"}
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -4,20 +4,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { ManaButton, ManaTooltip } from "..";
import { ManaButton, ManaTooltip, Paragraph, TooltipColors, TooltipPositions } from "..";
import { SectionWrapper } from "../SectionWrapper";
const POSITIONS = ["top", "bottom", "left", "right"] as const;
const COLORS = ["primary", "black", "grey", "brand", "green", "yellow", "red"] as const;
export default function TooltipTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Positions">
<div className="vc-compfinder-grid">
{POSITIONS.map(position => (
{TooltipPositions.map(position => (
<ManaTooltip key={position} text={`Tooltip on ${position}`} position={position}>
{props => (
<ManaButton {...props} variant="secondary" text={position} />
@ -29,7 +24,7 @@ export default function TooltipTab() {
<SectionWrapper title="Colors">
<div className="vc-compfinder-grid">
{COLORS.map(color => (
{TooltipColors.map(color => (
<ManaTooltip key={color} text={`${color} tooltip`} color={color}>
{props => (
<ManaButton {...props} variant="secondary" text={color} />

View file

@ -4,69 +4,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Paragraph } from "@components/Paragraph";
import { CodeColors, FontSizeMap, Paragraph, TextColors, TextSizes, TextWeights } from "..";
import { SectionWrapper } from "../SectionWrapper";
const TEXT_COLORS = [
"text-default",
"text-muted",
"text-subtle",
"text-strong",
"text-normal",
"text-link",
"text-brand",
"text-disabled",
"text-invert",
"text-feedback-critical",
"text-feedback-warning",
"text-feedback-info",
"text-feedback-positive",
"text-status-online",
"text-status-idle",
"text-status-dnd",
"text-status-offline",
"text-overlay-dark",
"text-overlay-light",
] as const;
const TEXT_SIZES = [
{ name: "xxs", value: "0.625rem (10px)" },
{ name: "xs", value: "0.75rem (12px)" },
{ name: "sm", value: "0.875rem (14px)" },
{ name: "md", value: "1rem (16px)" },
{ name: "lg", value: "1.25rem (20px)" },
{ name: "xl", value: "1.5rem (24px)" },
{ name: "xxl", value: "2rem (32px)" },
] as const;
const TEXT_WEIGHTS = [
{ name: "thin", value: "100" },
{ name: "extralight", value: "200" },
{ name: "light", value: "300" },
{ name: "normal", value: "400" },
{ name: "medium", value: "500" },
{ name: "semibold", value: "600" },
{ name: "bold", value: "700" },
{ name: "extrabold", value: "800" },
] as const;
const FONT_SIZE_MAP: Record<string, string> = {
xxs: "0.625rem",
xs: "0.75rem",
sm: "0.875rem",
md: "1rem",
lg: "1.25rem",
xl: "1.5rem",
xxl: "2rem",
};
export default function TypographyTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Text Colors (CSS Variables)">
<div className="vc-compfinder-grid-vertical">
{TEXT_COLORS.map(color => (
{TextColors.map(color => (
<div key={color} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<span style={{ color: `var(--${color})`, minWidth: 200 }}>
{color}
@ -81,13 +27,13 @@ export default function TypographyTab() {
<SectionWrapper title="Text Sizes">
<div className="vc-compfinder-grid-vertical">
{TEXT_SIZES.map(({ name, value }) => (
{TextSizes.map(({ name, value, pixels }) => (
<div key={name} style={{ display: "flex", alignItems: "baseline", gap: 16 }}>
<span style={{ fontSize: FONT_SIZE_MAP[name], minWidth: 200 }}>
<span style={{ fontSize: FontSizeMap[name], minWidth: 200 }}>
{name}
</span>
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
{value}
{value} ({pixels})
</code>
</div>
))}
@ -96,7 +42,7 @@ export default function TypographyTab() {
<SectionWrapper title="Text Weights">
<div className="vc-compfinder-grid-vertical">
{TEXT_WEIGHTS.map(({ name, value }) => (
{TextWeights.map(({ name, value }) => (
<div key={name} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<span style={{ fontWeight: value, minWidth: 200 }}>
{name}
@ -122,20 +68,7 @@ export default function TypographyTab() {
<SectionWrapper title="Code Colors">
<div className="vc-compfinder-grid-vertical">
{[
"text-code",
"text-code-keyword",
"text-code-string",
"text-code-comment",
"text-code-builtin",
"text-code-variable",
"text-code-tag",
"text-code-title",
"text-code-section",
"text-code-bullet",
"text-code-addition",
"text-code-deletion",
].map(color => (
{CodeColors.map(color => (
<div key={color} style={{ display: "flex", alignItems: "center", gap: 16 }}>
<span style={{ color: `var(--${color})`, minWidth: 200, fontFamily: "monospace" }}>
{color}

View file

@ -0,0 +1,167 @@
/*
* 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", "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 = ["normal", "warning", "danger"] 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", "black", "grey", "brand", "green", "yellow", "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];

View file

@ -0,0 +1,18 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export 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

@ -4,9 +4,16 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type AvatarSize = "SIZE_16" | "SIZE_20" | "SIZE_24" | "SIZE_32" | "SIZE_40" | "SIZE_48" | "SIZE_56" | "SIZE_80" | "SIZE_120";
import type { AvatarSize, AvatarStatus } from "../constants";
export type AvatarStatus = "online" | "idle" | "dnd" | "offline" | "streaming";
export type { AvatarSize, AvatarStatus };
export interface AvatarSizeConfig {
size: number;
status: number;
stroke: number;
offset: number;
}
export interface AvatarProps {
src?: string;
@ -18,8 +25,10 @@ export interface AvatarProps {
isSpeaking?: boolean;
statusTooltip?: boolean;
statusTooltipDelay?: number;
avatarDecoration?: string;
"aria-hidden"?: boolean;
"aria-label"?: string;
imageClassName?: string;
className?: string;
onClick?: (e: React.MouseEvent) => void;
}

View file

@ -4,26 +4,57 @@
* 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?: "primary" | "secondary" | "critical-primary" | "critical-secondary" | "overlay-primary" | "overlay-secondary" | "expressive";
size?: "xs" | "sm" | "md";
variant?: ManaButtonVariant;
size?: ManaButtonSize;
disabled?: boolean;
loading?: boolean;
loadingStartedLabel?: string;
loadingFinishedLabel?: string;
fullWidth?: boolean;
rounded?: boolean;
icon?: React.ComponentType<any>;
icon?: ManaButtonIconType;
iconPosition?: "start" | "end";
onClick?: () => void;
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?: "primary" | "secondary" | "always-white" | "critical";
variant?: ManaTextButtonVariant;
textVariant?: string;
lineClamp?: number;
onClick?: () => void;
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

@ -4,6 +4,10 @@
* 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;
@ -12,8 +16,8 @@ export interface ManaCheckboxProps {
label?: string;
description?: string;
leadingIcon?: React.ComponentType<any>;
labelType?: "primary" | "secondary";
usageVariant?: "single" | "indicator";
labelType?: CheckboxLabelType;
usageVariant?: CheckboxUsageVariant;
}
export interface CheckboxGroupOption {

View file

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

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

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

@ -4,22 +4,32 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "../constants";
export * from "./accordion";
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 "./guildicon.d";
export * from "./heading";
export * from "./input";
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";

View file

@ -4,29 +4,76 @@
* 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;
onChange?: (value: string) => void;
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;
size?: "sm" | "md";
error?: boolean;
clearable?: 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>;
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
}
export interface ManaTextAreaProps {
value: string;
onChange?: (value: string) => void;
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;
autosize?: boolean;
maxLength?: number;
minLength?: number;
showCharacterCount?: boolean;
error?: string | boolean;
fullWidth?: boolean;
focusProps?: Record<string, any>;
inputRef?: React.Ref<HTMLTextAreaElement>;
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
}
export interface SearchBarProps {
@ -34,7 +81,7 @@ export interface SearchBarProps {
onChange: (value: string) => void;
onClear?: () => void;
placeholder?: string;
size?: "sm" | "md";
size?: TextInputSize;
autoFocus?: boolean;
disabled?: boolean;
onKeyDown?: (e: React.KeyboardEvent) => void;

View file

@ -0,0 +1,94 @@
/*
* 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 interface ModalProps {
title: string;
subtitle?: string;
size?: NativeModalSize;
transitionState?: number;
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;
role?: 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 interface ExpressiveModalProps {
title: string;
description?: string;
transitionState?: number;
graphic?: { src: string; aspectRatio?: string };
badge?: React.ReactNode;
actions?: ModalAction[];
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

@ -4,7 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type NoticeType = "warn" | "info" | "danger" | "positive" | "preview";
import type { NoticeTypeValue } from "../constants";
export type NoticeType = NoticeTypeValue;
export interface NoticeProps {
children: React.ReactNode;

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface ScrollerProps {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
orientation?: "vertical" | "horizontal";
fade?: boolean;
dir?: "ltr" | "rtl";
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
}
export interface ListScrollerProps extends ScrollerProps {
rowHeight: number;
sections: number[];
sectionHeight: number;
renderSection: (section: number) => React.ReactNode;
renderRow: (row: { section: number; row: number }) => React.ReactNode;
role?: string;
}

View file

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

View file

@ -4,25 +4,38 @@
* 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;
onValueRender?: (value: number) => string;
asValueChanges?: (value: number) => void;
onValueRender?: (value: number) => string | null;
markers?: number[];
stickToMarkers?: boolean;
onMarkerRender?: (value: number) => string;
equidistant?: boolean;
onMarkerRender?: (value: number) => string | React.ReactNode | null;
renderMarker?: (value: number) => React.ReactNode;
getAriaValueText?: (value: number) => string;
keyboardStep?: number;
disabled?: boolean;
mini?: boolean;
hideBubble?: boolean;
defaultValue?: number;
orientation?: "horizontal" | "vertical";
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;

View file

@ -4,7 +4,18 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type SpinnerType = "wanderingCubes" | "chasingDots" | "pulsingEllipsis" | "spinningCircle" | "spinningCircleSimple" | "lowMotion";
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;
@ -13,3 +24,7 @@ export interface SpinnerProps {
itemClassName?: string;
"aria-label"?: string;
}
export interface SpinnerComponent extends React.FC<SpinnerProps> {
Type: SpinnerTypeEnum;
}

View file

@ -8,4 +8,10 @@ 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

@ -0,0 +1,27 @@
/*
* 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;
id?: string;
className?: string;
children?: React.ReactNode;
tooltipPosition?: TooltipPosition;
}

View file

@ -0,0 +1,32 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export 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

@ -4,11 +4,15 @@
* 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;
position?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
color?: "primary" | "black" | "grey" | "brand" | "green" | "yellow" | "red";
position?: TooltipPosition;
align?: TooltipAlign;
color?: TooltipColor;
spacing?: number;
hideOnClick?: boolean;
delay?: number;
@ -22,8 +26,8 @@ export interface ManaRichTooltipProps {
assetSize?: number;
asContainer?: boolean;
element?: "span" | "div";
position?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
position?: TooltipPosition;
align?: TooltipAlign;
spacing?: number;
ariaHidden?: boolean;
children: React.ReactNode | ((props: any) => React.ReactNode);
@ -40,14 +44,14 @@ export interface ManaPopoverProps {
body?: string;
badge?: React.ReactNode;
graphic?: { src: string; aspectRatio?: string; };
size?: "sm" | "md" | "lg";
size?: PopoverSize;
actions?: PopoverAction[];
textLink?: { text: string; onClick?: () => void; };
gradientColor?: string;
onRequestClose?: (reason: string) => void;
targetElementRef: React.RefObject<HTMLElement | null>;
shouldShow: boolean;
position?: "top" | "bottom" | "left" | "right";
position?: TooltipPosition;
alignmentStrategy?: "edge" | "trigger-center";
align?: "left" | "center" | "right";
}

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { BackupRestoreIcon, CloudIcon, LogIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons";
import { BackupRestoreIcon, CloudIcon, LogIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PluginsIcon, UpdaterIcon } from "@components/Icons";
import {
BackupAndRestoreTab,
ChangelogTab,
@ -21,24 +21,39 @@ import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import { isTruthy } from "@utils/guards";
import definePlugin, { IconProps, OptionType } from "@utils/types";
import { waitFor } from "@webpack";
import { findByPropsLazy } from "@webpack";
import { React } from "@webpack/common";
import type { ComponentType, PropsWithChildren, ReactNode } from "react";
let LayoutTypes = {
SECTION: 1,
SIDEBAR_ITEM: 2,
PANEL: 3,
PANE: 4
};
waitFor(["SECTION", "SIDEBAR_ITEM", "PANEL"], v => LayoutTypes = v);
const enum LayoutType {
ROOT = 0,
SECTION = 1,
SIDEBAR_ITEM = 2,
PANEL = 3,
SPLIT = 4,
CATEGORY = 5,
ACCORDION = 6,
LIST = 7,
RELATED = 8,
FIELD_SET = 9,
TAB_ITEM = 10,
STATIC = 11,
BUTTON = 12,
TOGGLE = 13,
SLIDER = 14,
SELECT = 15,
RADIO = 16,
NAVIGATOR = 17,
CUSTOM = 18
}
const FallbackSectionTypes = {
HEADER: "HEADER",
DIVIDER: "DIVIDER",
CUSTOM: "CUSTOM"
};
type SectionTypes = typeof FallbackSectionTypes;
const LayoutTypes: typeof LayoutType = findByPropsLazy("SECTION", "SIDEBAR_ITEM", "PANEL");
const enum SectionType {
HEADER = "HEADER",
DIVIDER = "DIVIDER",
CUSTOM = "CUSTOM"
}
type SettingsLocation =
| "top"
@ -49,7 +64,7 @@ type SettingsLocation =
| "bottom";
interface SettingsLayoutNode {
type: number;
type: LayoutType;
key?: string;
legacySearchKey?: string;
getLegacySearchKey?(): string;
@ -62,12 +77,13 @@ interface SettingsLayoutNode {
}
interface EntryOptions {
key: string,
title: string,
panelTitle?: string,
Component: ComponentType<{}>,
key: string;
title: string;
panelTitle?: string;
Component: ComponentType<{}>;
Icon: ComponentType<IconProps>;
}
interface SettingsLayoutBuilder {
key?: string;
buildLayout(): SettingsLayoutNode[];
@ -100,7 +116,6 @@ export default definePlugin({
{
find: ".versionHash",
replacement: [
// for whatever reason the first letter of RELEASE_CHANNEL is now lowercase, so we fix that here because it looks better imo
{
match: /\.RELEASE_CHANNEL/,
replace: "$&.replace(/^./, c => c.toUpperCase())"
@ -130,13 +145,7 @@ export default definePlugin({
replacement: [
{
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
replace: (
_,
sectionTypes,
commaOrSemi,
elements,
element,
) =>
replace: (_, sectionTypes, commaOrSemi, elements, element) =>
`${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`,
},
{
@ -146,12 +155,10 @@ export default definePlugin({
},
],
},
// Fix the settings cog context menu to work properly
{
find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",
replacement: {
// Skip the check Discord performs to make sure the section being selected in the user settings context menu is valid
match: /(?<=function\((\i),(\i),\i\)\{)(?=let \i=Object.values\(\i\.\i\).+?(\(0,\i\.openUserSettings\))\()/,
match: /(?<=function\((\i),(\i),\i\)\{)(?=let \i=Object\.values\(\i\.\i\).+?(\(0,\i\.openUserSettings\))\()/,
replace: (_, settingsPanel, section, openUserSettings) => `${openUserSettings}(${settingsPanel},{section:${section}});return;`
}
},
@ -178,40 +185,20 @@ export default definePlugin({
key: key + "_panel",
type: LayoutTypes.PANEL,
useTitle: () => panelTitle,
};
const render = {
// FIXME
buildLayout: () => [],
StronglyDiscouragedCustomComponent: () => <Component />,
render: () => <Component />,
};
// FIXME
if (LayoutTypes.PANE) {
panel.buildLayout = () => [
{
key: key + "_pane",
type: LayoutTypes.PANE,
useTitle: () => panelTitle,
buildLayout: () => [],
...render
}
];
} else {
Object.assign(panel, render);
panel.buildLayout = () => [];
}
return ({
return {
key,
type: LayoutTypes.SIDEBAR_ITEM,
// FIXME
legacySearchKey: title.toUpperCase(),
getLegacySearchKey: () => title.toUpperCase(),
useTitle: () => title,
icon: () => <Icon width={20} height={20} />,
buildLayout: () => [panel]
});
};
},
getSettingsSectionMappings() {
@ -235,7 +222,6 @@ export default definePlugin({
const layout = originalLayoutBuilder.buildLayout();
if (originalLayoutBuilder.key !== "$Root") return layout;
if (!Array.isArray(layout)) return layout;
if (layout.some(s => s?.key === "equicord_section")) return layout;
const { buildEntry } = this;
@ -292,19 +278,7 @@ export default definePlugin({
Component: PatchHelperTab,
Icon: PatchHelperIcon
}),
...this.customEntries.map(buildEntry),
// TODO: Remove deprecated customSections in a future update
...this.customSections.map((func, i) => {
const { section, element, label } = func(FallbackSectionTypes);
if (section !== "Equibop") return null;
return buildEntry({
key: `equicord_deprecated_custom_${section}`,
title: label,
Component: element,
Icon: VesktopSettingsIcon
});
})
...this.customEntries.map(buildEntry)
].filter(isTruthy);
const equicordSection: SettingsLayoutNode = {
@ -339,11 +313,10 @@ export default definePlugin({
return layout;
},
/** @deprecated Use customEntries */
customSections: [] as ((SectionTypes: SectionTypes) => any)[],
customSections: [] as ((SectionTypes: Record<string, string>) => { section: string; element: ComponentType; label: string; id?: string; })[],
customEntries: [] as EntryOptions[],
makeSettingsCategories(SectionTypes: SectionTypes) {
makeSettingsCategories(SectionTypes: Record<string, string>) {
return [
{
section: SectionTypes.HEADER,
@ -414,15 +387,12 @@ export default definePlugin({
isRightSpot({ header, settings: s }: { header?: string; settings?: string[]; }) {
const firstChild = s?.[0];
// lowest two elements... sanity backup
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS")
return true;
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
const { settingsLocation } = settings.store;
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
if (settingsLocation === "belowActivity")
return firstChild === "CHANGELOG";
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
if (!header) return;
@ -434,10 +404,7 @@ export default definePlugin({
aboveActivity: getIntlMessage("ACTIVITY_SETTINGS"),
};
if (
!names[settingsLocation] ||
names[settingsLocation].endsWith("_SETTINGS")
)
if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS"))
return firstChild === "PREMIUM";
return header === names[settingsLocation];
@ -451,49 +418,39 @@ export default definePlugin({
addSettings(
elements: any[],
element: { header?: string; settings: string[]; },
sectionTypes: SectionTypes,
SectionTypes: Record<string, string>,
) {
if (this.patchedSettings.has(elements) || !this.isRightSpot(element))
return;
if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return;
this.patchedSettings.add(elements);
elements.push(...this.makeSettingsCategories(sectionTypes));
elements.push(...this.makeSettingsCategories(SectionTypes));
},
wrapSettingsHook(
originalHook: (...args: any[]) => Record<string, unknown>[],
) {
wrapSettingsHook(originalHook: (...args: any[]) => Record<string, unknown>[]) {
return (...args: any[]) => {
const elements = originalHook(...args);
if (!this.patchedSettings.has(elements))
elements.unshift(...this.makeSettingsCategories(FallbackSectionTypes));
elements.unshift(...this.makeSettingsCategories({ HEADER: SectionType.HEADER, DIVIDER: SectionType.DIVIDER, CUSTOM: SectionType.CUSTOM }) as Record<string, unknown>[]);
return elements;
};
},
get electronVersion() {
return (
VencordNative.native.getVersions().electron ||
window.legcord?.electron ||
null
);
return VencordNative.native.getVersions().electron ?? window.legcord?.electron ?? null;
},
get chromiumVersion() {
try {
return (
VencordNative.native.getVersions().chrome ||
// @ts-expect-error Typescript will add userAgentData IMMEDIATELY
VencordNative.native.getVersions().chrome ??
// @ts-expect-error userAgentData types
navigator.userAgentData?.brands?.find(
b =>
b.brand === "Chromium" || b.brand === "Google Chrome",
)?.version ||
(b: { brand: string; }) => b.brand === "Chromium" || b.brand === "Google Chrome",
)?.version ??
null
);
} catch {
// inb4 some stupid browser throws unsupported error for navigator.userAgentData, it's only in chromium
return null;
}
},