From ea972836d3f0a07f58cd03cd3855fd45111cf1da Mon Sep 17 00:00:00 2001 From: prism Date: Mon, 22 Dec 2025 01:40:58 +0100 Subject: [PATCH] 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> --- .../src/stores/MediaEngineStore.d.ts | 270 +++++++++++--- .../src/stores/OverridePremiumTypeStore.d.ts | 4 +- .../components/ComponentsTab.tsx | 32 ++ .../components.dev/components/index.tsx | 155 +++++++- .../components/tabs/AccordionTab.tsx | 73 ++++ .../components/tabs/AnchorTab.tsx | 4 +- .../components/tabs/AvatarTab.tsx | 11 +- .../components/tabs/BadgeTab.tsx | 4 +- .../components/tabs/ButtonsTab.tsx | 14 +- .../components/tabs/CalendarTab.tsx | 5 +- .../components/tabs/CardTab.tsx | 105 ++++++ .../components/tabs/CheckboxGroupTab.tsx | 5 +- .../components/tabs/CheckboxTab.tsx | 5 +- .../components/tabs/ChipTab.tsx | 55 +++ .../components/tabs/ClickableTab.tsx | 5 +- .../components/tabs/ColorPickerTab.tsx | 32 +- .../components/tabs/ComboboxTab.tsx | 5 +- .../components/tabs/DividerTab.tsx | 52 +++ .../components/tabs/GuildIconTab.tsx | 111 ++++++ .../components/tabs/HeadingTab.tsx | 94 +++++ .../components/tabs/LayerModalTab.tsx | 5 +- .../components/tabs/ListboxTab.tsx | 5 +- .../components/tabs/ModalTab.tsx | 331 +++++++++++------- .../components/tabs/NoticeTab.tsx | 9 +- .../components/tabs/PaginatorTab.tsx | 5 +- .../components/tabs/PopoverTab.tsx | 5 +- .../components/tabs/ProgressBarTab.tsx | 5 +- .../components/tabs/RadioGroupTab.tsx | 5 +- .../components/tabs/RichTooltipTab.tsx | 4 +- .../components/tabs/ScrollerTab.tsx | 105 ++++++ .../components/tabs/SearchBarTab.tsx | 5 +- .../components/tabs/SelectTab.tsx | 5 +- .../components/tabs/SkeletonTab.tsx | 60 ++++ .../components/tabs/SliderTab.tsx | 5 +- .../components/tabs/SpinnerTab.tsx | 27 +- .../components/tabs/SwitchTab.tsx | 4 +- .../components/tabs/TabBarTab.tsx | 5 +- .../components/tabs/TextAreaTab.tsx | 5 +- .../components/tabs/TextButtonTab.tsx | 19 +- .../components/tabs/TextInputTab.tsx | 5 +- .../components/tabs/TimestampTab.tsx | 166 +++++++++ .../components/tabs/ToastTab.tsx | 136 +++++++ .../components/tabs/TooltipTab.tsx | 11 +- .../components/tabs/TypographyTab.tsx | 81 +---- .../components.dev/constants.ts | 167 +++++++++ .../components.dev/types/accordion.d.ts | 18 + .../components.dev/types/avatar.d.ts | 13 +- .../components.dev/types/button.d.ts | 43 ++- .../components.dev/types/checkbox.d.ts | 8 +- .../components.dev/types/chip.d.ts | 20 ++ .../components.dev/types/guildicon.d.ts | 15 + .../components.dev/types/heading.d.ts | 32 ++ .../components.dev/types/index.ts | 10 + .../components.dev/types/input.d.ts | 63 +++- .../components.dev/types/modal.d.ts | 94 +++++ .../components.dev/types/notice.d.ts | 4 +- .../components.dev/types/scroller.d.ts | 24 ++ .../components.dev/types/skeleton.d.ts | 9 + .../components.dev/types/slider.d.ts | 19 +- .../components.dev/types/spinner.d.ts | 17 +- .../components.dev/types/switch.d.ts | 6 + .../components.dev/types/timestamp.d.ts | 27 ++ .../components.dev/types/toast.d.ts | 32 ++ .../components.dev/types/tooltip.d.ts | 18 +- src/plugins/_core/settings.tsx | 157 +++------ 65 files changed, 2293 insertions(+), 557 deletions(-) create mode 100644 src/equicordplugins/components.dev/components/tabs/AccordionTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/CardTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ChipTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/DividerTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/GuildIconTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/HeadingTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ScrollerTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/TimestampTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ToastTab.tsx create mode 100644 src/equicordplugins/components.dev/constants.ts create mode 100644 src/equicordplugins/components.dev/types/accordion.d.ts create mode 100644 src/equicordplugins/components.dev/types/chip.d.ts create mode 100644 src/equicordplugins/components.dev/types/guildicon.d.ts create mode 100644 src/equicordplugins/components.dev/types/heading.d.ts create mode 100644 src/equicordplugins/components.dev/types/modal.d.ts create mode 100644 src/equicordplugins/components.dev/types/scroller.d.ts create mode 100644 src/equicordplugins/components.dev/types/skeleton.d.ts create mode 100644 src/equicordplugins/components.dev/types/timestamp.d.ts create mode 100644 src/equicordplugins/components.dev/types/toast.d.ts diff --git a/packages/discord-types/src/stores/MediaEngineStore.d.ts b/packages/discord-types/src/stores/MediaEngineStore.d.ts index ee48febff..97fe00e2c 100644 --- a/packages/discord-types/src/stores/MediaEngineStore.d.ts +++ b/packages/discord-types/src/stores/MediaEngineStore.d.ts @@ -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; + 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; + exportClip(clipId: string, userId: string): Promise; + fetchAsyncResources(options: { fetchDave?: boolean; }): Promise; + + getAudioInputDevices(): AudioDevice[]; + getAudioLayer(): string; + getAudioOutputDevices(): AudioDevice[]; + getAudioSubsystem(): string; + getCodecCapabilities(callback: (capabilities: string) => void): void; + getCodecSurvey(): Promise; getDebugLogging(): boolean; + getDesktopSource(): Promise; + getLoopback(): boolean; + getMLSSigningKey(userId: string, guildId: string): Promise<{ key: unknown; signature: unknown; }>; + getNoiseCancellationStats(): Promise; + getScreenPreviews(width: number, height: number): Promise; + getSupportedBandwidthEstimationExperiments(callback: (experiments: unknown) => void): void; + getSupportedSecureFramesProtocolVersion(): number; + getSupportedVideoCodecs(callback: (codecs: string[]) => void): void; + getSystemMicrophoneMode(): Promise; + getVideoInputDeviceId(): string; + getVideoInputDevices(): VideoDevice[]; + getWindowPreviews(width: number, height: number): Promise; + + interact(): void; + presentNativeScreenSharePicker(options?: string): void; + queueAudioSubsystem(subsystem: string): void; + rankRtcRegions(regions: string[]): Promise; + releaseNativeDesktopVideoSourcePickerStream(): void; + + saveClip(clipId: string, userId: string): Promise; + saveClipForUser(clipId: string, userId: string, options: unknown): Promise; + saveScreenshot(channelId: string, userId: string, width: number | null, height: number | null, options: unknown): Promise; 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; + + shouldConnectionBroadcastVideo(connection: unknown): boolean; + showSystemCaptureConfigurationUI(options: unknown): void; + startAecDump(): void; + startLocalAudioRecording(options: unknown): Promise; + 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; + supports(feature: string): boolean; + updateClipMetadata(clipId: string, metadata: unknown): Promise; + watchdogTick(): void; + writeAudioDebugState(): Promise; } 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; + getInputDevices(): Record; getInputVolume(): number; getKrispEnableStats(): boolean; - getKrispModelOverride(): string; + getKrispModelOverride(): string | undefined; getKrispModels(): string[]; getKrispSuppressionLevel(): number; getKrispVadActivationThreshold(): number; getLastAudioInputDeviceChangeTimestamp(): number; - getLocalPan(key: string): Record; - getLocalVolume(): number; + getLocalPan(userId: string, context?: string): LocalPan; + getLocalVolume(userId: string, context?: string): number; getLoopback(): boolean; getLoopbackReasons(): Set; getMediaEngine(): MediaEngine; - getMode(): string; - getModeOptions(): Record; + getMLSSigningKey(userId: string, guildId: string): Promise; + getMode(context?: string): string; + getModeOptions(context?: string): ModeOptions; getMostRecentlyRequestedVoiceFilter(): string | null; getNoInputDetectedNotice(): boolean; getNoiseCancellation(): boolean; getNoiseSuppression(): boolean; getOutputDeviceId(): string; - getOutputDevices(): Record; + getOutputDevices(): Record; getOutputVolume(): number; getPacketDelay(): number; getPreviousVoiceFilter(): string | null; getPreviousVoiceFilterAppliedAt(): Date | null; getQoS(): boolean; - getSettings(): Record; - getShortcuts(): Record; + getSettings(context?: string): Record; + getShortcuts(): Record; getSidechainCompression(): boolean; getSidechainCompressionStrength(): number; getSpeakingWhileMuted(): boolean; - getSupportedSecureFramesProtocolVersion(): boolean; - getSystemMicrophoneMode(): boolean; + getState(): { + settingsByContext: Record; + inputDevices: Record; + outputDevices: Record; + appSupported: Record; + 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; getVideoHook(): boolean; - getVideoStreamParameters(): Record; - 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; diff --git a/packages/discord-types/src/stores/OverridePremiumTypeStore.d.ts b/packages/discord-types/src/stores/OverridePremiumTypeStore.d.ts index 1e3409304..bbfda9c6e 100644 --- a/packages/discord-types/src/stores/OverridePremiumTypeStore.d.ts +++ b/packages/discord-types/src/stores/OverridePremiumTypeStore.d.ts @@ -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; } diff --git a/src/equicordplugins/components.dev/components/ComponentsTab.tsx b/src/equicordplugins/components.dev/components/ComponentsTab.tsx index 0387abf1d..fc47d5df4 100644 --- a/src/equicordplugins/components.dev/components/ComponentsTab.tsx +++ b/src/equicordplugins/components.dev/components/ComponentsTab.tsx @@ -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 = { + 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 = { 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 = { textarea: TextAreaTab, textbutton: TextButtonTab, textinput: TextInputTab, + timestamp: TimestampTab, + toast: ToastTab, tooltip: TooltipTab, typography: TypographyTab, }; diff --git a/src/equicordplugins/components.dev/components/index.tsx b/src/equicordplugins/components.dev/components/index.tsx index 2fe042eec..9f81cf3f8 100644 --- a/src/equicordplugins/components.dev/components/index.tsx +++ b/src/equicordplugins/components.dev/components/index.tsx @@ -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; export const ManaButton = findComponentByCodeLazy('"data-mana-component":"button"') as React.ComponentType; @@ -117,7 +212,7 @@ export const ManaTextInput = findComponentByCodeLazy('"data-mana-component":"tex export const ManaTextArea = findComponentByCodeLazy('"data-mana-component":"text-area"') as React.ComponentType; export const ManaCheckbox = findComponentByCodeLazy('"data-mana-component":"checkbox"') as React.ComponentType; export const ManaSelect = findComponentByCodeLazy('"data-mana-component":"select"') as React.ComponentType; -export const ManaTooltip = findComponentByCodeLazy('"data-mana-component":"tooltip"') as React.ComponentType; +export const ManaTooltip = findComponentByCodeLazy("VoidTooltip cannot find DOM node") as React.ComponentType; export const ManaCalendar = findComponentByCodeLazy('"data-mana-component":"calendar"') as React.ComponentType; export const ManaDatePicker = findComponentByCodeLazy('"data-mana-component":"date-picker"') as React.ComponentType; export const ManaRichTooltip = findComponentByCodeLazy('"data-mana-component":"rich-tooltip"') as React.ComponentType; @@ -131,7 +226,7 @@ export const ManaPopover = findComponentByCodeLazy('"data-mana-component":"popov export const Slider = findComponentByCodeLazy("stickToMarkers", "onMarkerRender", "grabberClassName") as React.ComponentType; export const Avatar = findComponentByCodeLazy("statusTooltip", "statusBackdropColor", "isSpeaking") as React.ComponentType; export const ProgressBar = findComponentByCodeLazy("progressContainer", "labelledBy", "aria-valuenow") as React.ComponentType; -export const Spinner = findComponentByCodeLazy("spinningCircleSimple", "pulsingEllipsis", "wanderingCubes") as React.ComponentType; +export const Spinner = findComponentByCodeLazy("spinningCircleSimple", "pulsingEllipsis", "wanderingCubes") as unknown as SpinnerComponent; export const SearchBar = findComponentByCodeLazy("#{intl::SEARCH}", "clearable", "autoComplete") as React.ComponentType; export const Paginator2 = findComponentByCodeLazy("#{intl::BACK}", "#{intl::NEXT}", "renderPageWrapper") as React.ComponentType; export const Notice = findComponentByCodeLazy("messageType", "iconDiv", "actionContainer") as React.ComponentType; @@ -184,6 +279,52 @@ export const IconBadge: React.ComponentType = proxyLazy(() => ge export const CircleBadge: React.ComponentType = 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; + +export const Chip = findComponentByCodeLazy('variant:"eyebrow"', "chip,") as React.ComponentType; + +export const Skeleton = findComponentByCodeLazy("productSkeletonCardContainer", "skipPulseAnimation") as React.ComponentType; + +export const Accordion = findComponentByCodeLazy("accordionContainer", "onExpandedChange", "defaultExpanded") as React.ComponentType; + +export const Timestamp = findComponentByCodeLazy("isVisibleOnlyOnHover", "cozyAlt", "timestampFormat") as React.ComponentType; + +export const GuildIcon = findComponentByCodeLazy("getGuildIconURL", "acronym") as React.ComponentType; + +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 = proxyLazy(() => ModalModule.Modal); +export const ConfirmModal: React.ComponentType = proxyLazy(() => ModalModule.ConfirmModal); +export const ExpressiveModal: React.ComponentType = 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 ; +} +export function DiscordText(props: DiscordTextProps) { + const Component = DiscordTextModule.Text; + return ; +} + +export function Divider({ className, style, ...restProps }: React.ComponentPropsWithoutRef<"hr">) { + return
; +} diff --git a/src/equicordplugins/components.dev/components/tabs/AccordionTab.tsx b/src/equicordplugins/components.dev/components/tabs/AccordionTab.tsx new file mode 100644 index 000000000..177fde0c5 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/AccordionTab.tsx @@ -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 ( +
+ + + Click to expand/collapse content. + + + This is the accordion content that is revealed when expanded. + + + + + + Content with a subtitle in the header. + + + + + + This accordion starts expanded by default. + + + + + + Use isExpanded and onExpandedChange for controlled state. + + + This accordion's state is controlled externally. + + + + + + Line 1 + Line 2 + Line 3 + Line 4 + Line 5 + Line 6 + + + + + • title: string - Header title + • subtitle?: string - Optional subtitle + • icon?: ReactNode - Optional icon + • children: ReactNode - Content + • defaultExpanded?: boolean - Initial state (default: false) + • isExpanded?: boolean - Controlled expanded state + • onExpandedChange?: (expanded: boolean) => void + • onOpen?: () => void - Called when opened + • maxHeight?: number | string - Max content height + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx b/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx index 6a2208eec..cb258fffa 100644 --- a/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx b/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx index d96ac1ad1..f6bdd076b 100644 --- a/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx @@ -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.
- {SIZES.map(size => ( + {AvatarSizes.map(size => (
• src - Avatar image URL - • size - SIZE_16 to SIZE_120 + • 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 • status - online, idle, dnd, offline • isMobile - Show mobile indicator • isTyping - Show typing indicator diff --git a/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx b/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx index 45dd21b98..3bc72adfc 100644 --- a/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx @@ -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"; diff --git a/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx b/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx index 61ed9d174..2e95636a8 100644 --- a/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx @@ -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() {
- {VARIANTS.map(variant => ( + {ManaButtonVariants.map(variant => ( ))}
@@ -28,7 +22,7 @@ export default function ButtonsTab() {
- {SIZES.map(size => ( + {ManaButtonSizes.map(size => ( ))}
@@ -54,7 +48,7 @@ export default function ButtonsTab() {
- {VARIANTS.map(variant => ( + {ManaButtonVariants.map(variant => ( ))}
diff --git a/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx b/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx index 72b3a092a..7319826a5 100644 --- a/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/CardTab.tsx b/src/equicordplugins/components.dev/components/tabs/CardTab.tsx new file mode 100644 index 000000000..8ff0485db --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/CardTab.tsx @@ -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 ( +
+ + + Card component from @components/Card with different severity styles. + +
+ {CardVariants.map(variant => ( + + + {variant.charAt(0).toUpperCase() + variant.slice(1)} Card + + + This is a {variant} card variant. Use for { + variant === "normal" ? "general content" : + variant === "warning" ? "cautionary messages" : + "error or destructive actions" + }. + + + ))} +
+
+ + + + Cards have 1em padding by default when no className is passed. + +
+
+ + defaultPadding=true (or no className) + + + Card with default padding + +
+
+ + defaultPadding=false + + + Card without default padding (content adds own) + +
+
+
+ + + + Cards can be nested for complex layouts. + + + Outer Card + + Nested warning card + + + Nested danger card + + + + + + + Cards accept custom className and style props. + + + Card with custom border accent + + + + + • variant - "normal" | "warning" | "danger" (default: "normal") + • defaultPadding - Add 1em padding (default: true if no className) + • className - Custom CSS class + • style - Inline styles + • children - Card content + • ...restProps - Any other div attributes + + + + + {"import { Card } from \"@components/Card\";"} + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx b/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx index 67daa050e..d44b102d6 100644 --- a/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx @@ -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"; diff --git a/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx index f69a81698..3d96ca843 100644 --- a/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx @@ -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"; diff --git a/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx b/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx new file mode 100644 index 000000000..ee4b0c5d6 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx @@ -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 ( +
+ +
+ + + + + + + + +
+
+ + +
+ + + +
+
+ + +
+ + + + + + +
+
+ + + + • text: string - Text to display inside the chip + + + • variant?: ChipVariant - Color variant (default: "grayLight") + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx b/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx index b3b79cf4b..6caf42699 100644 --- a/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx b/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx index 4e09667e2..597a3fdb6 100644 --- a/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx @@ -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.
- {PRESET_COLORS.slice(0, 10).map(color => ( + {PresetColors.slice(0, 10).map(color => (
- Normal + Normal
- Selected + Selected
- Default + Default
- Custom + Custom
- Disabled + Disabled
- No Color + No Color
@@ -174,7 +164,7 @@ export default function ColorPickerTab() {
@@ -184,7 +174,7 @@ export default function ColorPickerTab() { null} @@ -199,7 +189,7 @@ export default function ColorPickerTab() { ( diff --git a/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx index 9ee345d98..125a140e0 100644 --- a/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx @@ -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 = [ diff --git a/src/equicordplugins/components.dev/components/tabs/DividerTab.tsx b/src/equicordplugins/components.dev/components/tabs/DividerTab.tsx new file mode 100644 index 000000000..509c57ebd --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/DividerTab.tsx @@ -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 ( +
+ + + Simple horizontal rule from @components/Divider. + +
+ Content above divider + + Content below divider +
+
+ + + + Dividers with custom margins and styles. + +
+ Tight spacing (8px) + + Medium spacing (16px) + + Large spacing (24px) + + End +
+
+ + + • className - Custom CSS class + • style - Inline styles + • ...restProps - Any other hr attributes + + + + + {"import { Divider } from \"@components/Divider\";"} + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/GuildIconTab.tsx b/src/equicordplugins/components.dev/components/tabs/GuildIconTab.tsx new file mode 100644 index 000000000..3615d5854 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/GuildIconTab.tsx @@ -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 ( +
+ +
+ setAnimate(a => !a)} + /> +
+
+ + + + GuildIcon component with various sizes. Shows server icon or acronym fallback. + +
+ {guildWithIcon && GuildIconSizes.map(size => ( +
+ + + {size}px + +
+ ))} +
+
+ + + + When a guild has no icon, displays an acronym based on the guild name. + +
+ {[32, 48, 80].map(size => ( +
+ + + {size}px (no icon) + +
+ ))} +
+
+ + + + Showing icons from guilds you're in. + +
+ {guilds.map(guild => ( +
+ + + {guild.name.slice(0, 12)} + +
+ ))} +
+
+ + + • guildId - The guild's snowflake ID + • guildName - Guild name (used for acronym fallback) + • guildIcon - Icon hash or null + • iconSize - Size in pixels + • animate - Enable animated icons (GIF) + • className - Custom CSS class + • acronymClassName - Class for acronym text + + + + + {"import { GuildIcon } from \"@equicordplugins/components.dev/components\";"} + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/HeadingTab.tsx b/src/equicordplugins/components.dev/components/tabs/HeadingTab.tsx new file mode 100644 index 000000000..928b22a03 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/HeadingTab.tsx @@ -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 ( +
+ + + Discord's Heading component with variant prop. Auto-sets h1-h6 tag based on nesting level. + +
+ {HeadingVariants.slice(0, 10).map(variant => ( + + {variant} + + ))} +
+
+ + +
+ {HeadingVariants.slice(10).map(variant => ( + + {variant} + + ))} +
+
+ + + + Discord's Text component for body text with various sizes and weights. + +
+ {TextVariants.slice(0, 12).map(variant => ( + + {variant} + + ))} +
+
+ + +
+ {TextVariants.slice(12).map(variant => ( + + {variant} + + ))} +
+
+ + + + Available color props for Text component. + +
+ {TextColors.map(color => ( + + {color} + + ))} +
+
+ + + • variant - "heading-SIZE/WEIGHT" (xxl, xl, lg, md, sm) + • className - Custom CSS class + • color - Text color (same as Text colors) + + + + • variant - "text-SIZE/WEIGHT" (xxl, xl, lg, md, sm, xs) + • color - Text color name + • tag - HTML tag to render (default: "div") + • selectable - Allow text selection + • lineClamp - Number of lines before truncation + • tabularNumbers - Use tabular number spacing + + + + + {"import { DiscordHeading, DiscordText, HeadingVariants, TextVariants, TextColors } from \"../components\";"} + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx b/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx index 2f5bcab5f..31a2a4c33 100644 --- a/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx @@ -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<{ diff --git a/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx index d6e175cd9..dd19b4182 100644 --- a/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx @@ -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[] = [ diff --git a/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx b/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx index fc7795ba5..da80d56b6 100644 --- a/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx @@ -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 => ( - - -
- - {label} Modal - - - This is a {label.toLowerCase()} modal using Discord's native modal system. - -
- {showCloseButton && ( - - )} -
- - - Size: {size} - - - The modal header, content, and footer use Discord's native components - from the webpack modules. - - - {showFooter && ( - - - - - )} -
+ { showToast("Confirmed!", ToastType.SUCCESS); props.onClose(); } }, + ]} + > + This is the modal content. You can put any React components here. + + )); +} + +function openModalWithNotice() { + openModal(props => ( + + Modals can display notices at the top for important information. + + )); +} + +function ModalWithInputContent({ onClose, transitionState }: { onClose: () => void; transitionState: number; }) { + const [inputValue, setInputValue] = useState(""); + + return ( + { showToast(`Submitted: ${inputValue}`, ToastType.SUCCESS); onClose(); } }, + ]} + > + + + Using ManaTextInput inside the modal content. + + + ); +} + +function openModalWithInput() { + openModal(props => ( + + )); +} + +function openConfirmModal() { + openModal(props => ( + { + showToast("Item deleted!", ToastType.SUCCESS); + }} + onCancel={() => { + showToast("Cancelled", ToastType.MESSAGE); + }} + onClose={props.onClose} + > + This action cannot be undone. The item will be permanently deleted. + + )); +} + +function openConfirmModalPrimary() { + openModal(props => ( + { + showToast("Changes saved!", ToastType.SUCCESS); + }} + onClose={props.onClose} + > + Do you want to save your changes? + + )); +} + +function openExpressiveModal() { + openModal(props => ( + )); } export default function ModalTab() { return (
- + - 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. -
- {SIZES.map(({ size, label }) => ( - openDemoModal(size, label)} - /> - ))} -
+
- -
- openDemoModal(ModalSize.SMALL, "No Close Button", false, true)} - /> -
+ + + Modal with a notice banner (info, warn, critical, positive). + + - -
- openDemoModal(ModalSize.SMALL, "No Footer", true, false)} - /> -
+ + + Modal with input controls in the header area. + + - -
- openDemoModal(ModalSize.DYNAMIC, "Minimal", false, false)} - /> -
+ + + Confirmation dialog for destructive actions. + + - + + + Confirmation dialog for non-destructive actions. + + + + + + + Visual modal with graphic/badge support. + + + + + + + Utility to close all open modals at once. + + + + + + • title - Modal title (required) + • subtitle - Optional subtitle + • size - "sm" or "md" + • children - Modal content + • actions - Array of action buttons + • notice - {"{ message, type }"} for notices + • input - Controls in header area + • onClose - Close handler (required) + + + + • confirmText - Confirm button text + • cancelText - Cancel button text + • variant - "critical" | "primary" | "secondary" + • onConfirm - Confirm callback (can be async) + • onCancel - Cancel callback + • checkboxProps - Optional checkbox + + + + • text - Button text + • variant - "primary" | "secondary" | "critical-primary" + • onClick - Click handler + • disabled - Disable button + • loading - Show loading state + + + + • modalKey - Unique key for the modal + • dismissable - Allow clicking backdrop to close + • instant - Skip animation + • onCloseCallback - Called when modal closes + + + - ModalRoot - from @utils/modal - - - • size: ModalSize - SMALL, MEDIUM, LARGE, or DYNAMIC - - - • ...modalProps - Props from openModal callback - - - ModalHeader - - - • separator?: boolean - Show separator line (default: true) - - - ModalCloseButton - findComponentByCodeLazy("CLOSE_BUTTON_LABEL") - - - • onClick: () => void - Close handler - - - • withCircleBackground?: boolean - Circle background style - - - openModal - from @utils/modal - - - • (render: (props) => ReactNode) => string - Returns modal key + {"import { Modal, ConfirmModal, openModal, closeModal } from \"../components\";"}
diff --git a/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx b/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx index ae5778324..cea1d5590 100644 --- a/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx @@ -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 (
@@ -20,7 +15,7 @@ export default function NoticeTab() { Notice component for displaying informational messages with different severity levels.
- {NOTICE_TYPES.map(type => ( + {NoticeTypes.map(type => (
This is a {type} notice message. diff --git a/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx b/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx index 58fa5ecac..f019a5547 100644 --- a/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx b/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx index ed5271da8..cd7bc6c19 100644 --- a/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx @@ -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; }) { diff --git a/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx index 90b28593d..605d08eb1 100644 --- a/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx b/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx index 1d85bb22e..c4385ce6a 100644 --- a/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx @@ -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"; diff --git a/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx b/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx index 74ea02343..f2bede8bf 100644 --- a/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/ScrollerTab.tsx b/src/equicordplugins/components.dev/components/tabs/ScrollerTab.tsx new file mode 100644 index 000000000..ae59ae8f1 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ScrollerTab.tsx @@ -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 ( +
+ +
+ setOrientation(o => o === "vertical" ? "horizontal" : "vertical")} + /> + setFade(f => !f)} + /> +
+
+ + + + Thin scrollbar, most commonly used. Ideal for sidebars and lists. + + + {SAMPLE_ITEMS.map(item => ( +
+ {item} +
+ ))} +
+
+ + + + Standard scrollbar that appears automatically when content overflows. + + + {SAMPLE_ITEMS.map(item => ( +
+ {item} +
+ ))} +
+
+ + + + No visible scrollbar, content still scrollable via mouse wheel/touch. + + + {SAMPLE_ITEMS.map(item => ( +
+ {item} +
+ ))} +
+
+ + + • orientation - "vertical" | "horizontal" (default: "vertical") + • fade - boolean, adds fade effect at edges + • className - custom CSS class + • style - inline styles + • children - content to scroll + • dir - text direction ("ltr" | "rtl") + + + + • ScrollerThin - Thin scrollbar (most common) + • ScrollerAuto - Standard auto-showing scrollbar + • ScrollerNone - Hidden scrollbar + • ListScrollerThin - Virtualized list with thin scrollbar + • ListScrollerAuto - Virtualized list with auto scrollbar + • ListScrollerNone - Virtualized list with hidden scrollbar + + + + + {"import { ScrollerThin, ScrollerAuto, ScrollerNone } from \"@webpack/common\";"} + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx index 48287ee13..6a7b87094 100644 --- a/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx b/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx index 7b4ec098b..b50f84a1d 100644 --- a/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx @@ -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[] = [ diff --git a/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx b/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx new file mode 100644 index 000000000..95473b914 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx @@ -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 ( +
+ + + Loading placeholder with pulse animation. Requires "full-motion" parent class for animation. + +
+ + + +
+
+ + + + Use skipPulseAnimation to disable the pulse effect. + + setAnimated(!animated)} + /> +
+ + +
+
+ + + + • skipPulseAnimation?: boolean - Disable pulse animation (default: false) + + + + + + • This is a product card skeleton used in Discord's shop. + + + • Animation requires a parent with "full-motion" class (Discord's reduced motion setting). + + + • The skeleton has fixed dimensions designed for product cards. + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx b/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx index 0ccfea90e..4b7432020 100644 --- a/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx b/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx index d2312488f..45e607d75 100644 --- a/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx @@ -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.
- {SPINNER_TYPES.map(type => ( + {SpinnerTypes.map(type => (
@@ -75,7 +63,7 @@ export default function SpinnerTab() { - • type?: "wanderingCubes" | "chasingDots" | "pulsingEllipsis" | "spinningCircle" | "spinningCircleSimple" | "lowMotion" + • type?: SpinnerType - Spinner variant • animated?: boolean - Enable/disable animation (default: true) @@ -90,6 +78,15 @@ export default function SpinnerTab() { • aria-label?: string - Accessibility label + + + • "wanderingCubes" + • "chasingDots" + • "pulsingEllipsis" + • "spinningCircle" + • "spinningCircleSimple" + • "lowMotion" +
); } diff --git a/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx b/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx index fd495e786..766161626 100644 --- a/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx @@ -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 { diff --git a/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx index 72e3bd856..cc3180e56 100644 --- a/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx b/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx index 8527658a4..ed854a248 100644 --- a/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx @@ -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() { diff --git a/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx b/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx index 0434ce029..92d040dcb 100644 --- a/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx @@ -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 (
- {VARIANTS.map(variant => ( + {ManaTextButtonVariants.map(variant => (
- {TEXT_VARIANTS.map(textVariant => ( + {ManaTextButtonTextVariants.map(textVariant => ( + + + All available timestamp format options using moment.js format strings. + +
+ {TimestampDisplayFormats.map(({ format, description }) => ( +
+ + + {format}: {description} + +
+ ))} +
+
+ + + + Compact mode displays a shorter version of the timestamp. + +
+
+ + + compact=false (default) + +
+
+ + + compact=true + +
+
+
+ + + + Different display modes for timestamps. + +
+
+ + + isInline=false (default) + +
+
+ + + isInline=true + +
+
+ + + cozyAlt=true + +
+
+
+ + + + Shows an "(edited)" indicator after the timestamp. + +
+
+ + + isEdited=false (default) + +
+
+ + + isEdited=true + +
+
+
+ + + + Timestamp can be hidden until hovered. + +
+
+ + Hover me: + + + isVisibleOnlyOnHover=true + +
+
+
+ + + + The tooltip showing full date can be positioned differently. + +
+ {TooltipPositions.map(position => ( +
+ + + tooltipPosition="{position}" + +
+ ))} +
+
+ + + + • timestamp: Date | Moment - The timestamp to display (required) + + + • timestampFormat?: TimestampFormat - Format string (LT, LTS, L, LL, LLL, LLLL, l, ll, lll, llll) + + + • compact?: boolean - Use compact display mode + + + • cozyAlt?: boolean - Use cozy alternative display + + + • isInline?: boolean - Display inline + + + • isVisibleOnlyOnHover?: boolean - Only show on hover + + + • isEdited?: boolean - Show "(edited)" indicator + + + • tooltipPosition?: "top" | "bottom" | "left" | "right" - Tooltip position + + + • id?: string - Element ID + + + • className?: string - Additional CSS class + + + • children?: ReactNode - Custom content + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx b/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx new file mode 100644 index 000000000..edb4f5eb0 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx @@ -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: Custom JSX content!, + }); + Toasts.show(toast); + }; + + return ( +
+ + + Click buttons to show different toast types. Toasts appear at screen edges. + +
+ {TOAST_TYPES.map(({ type, label, description }) => ( +
+ showSimpleToast(type, label)} + /> + + {description} + +
+ ))} +
+
+ + + + showToast(message, type) - Quick way to show a toast. + + showToast("Hello from showToast!", ToastType.SUCCESS)} + /> + + + + + Toasts.show() with full options object. + +
+ + +
+
+ + + + Toasts.pop() removes the most recent toast. + + Toasts.pop()} + /> + + + + • MESSAGE - Default message style + • SUCCESS - Green checkmark icon + • FAILURE - Red X icon + • CUSTOM - Custom component content + • CLIP - Clip/scissors icon + • LINK - Link icon + • FORWARD - Forward arrow icon + • BOOKMARK - Bookmark icon + • CLOCK - Clock icon + + + + • TOP (0) - Toast appears at top + • BOTTOM (1) - Toast appears at bottom + + + + • message - Toast message text + • type - Toast type from ToastType + • id - Unique ID (use Toasts.genId()) + • options.duration - Display time in ms + • options.position - ToastPosition value + • options.component - Custom React component + + + + + {"import { showToast, Toasts, ToastType, ToastPosition } from \"../components\";"} + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx b/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx index 961b753a4..b25c4aa56 100644 --- a/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx @@ -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 (
- {POSITIONS.map(position => ( + {TooltipPositions.map(position => ( {props => ( @@ -29,7 +24,7 @@ export default function TooltipTab() {
- {COLORS.map(color => ( + {TooltipColors.map(color => ( {props => ( diff --git a/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx b/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx index f13cddd9e..675fd894e 100644 --- a/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx @@ -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 = { - xxs: "0.625rem", - xs: "0.75rem", - sm: "0.875rem", - md: "1rem", - lg: "1.25rem", - xl: "1.5rem", - xxl: "2rem", -}; - export default function TypographyTab() { return (
- {TEXT_COLORS.map(color => ( + {TextColors.map(color => (
{color} @@ -81,13 +27,13 @@ export default function TypographyTab() {
- {TEXT_SIZES.map(({ name, value }) => ( + {TextSizes.map(({ name, value, pixels }) => (
- + {name} - {value} + {value} ({pixels})
))} @@ -96,7 +42,7 @@ export default function TypographyTab() {
- {TEXT_WEIGHTS.map(({ name, value }) => ( + {TextWeights.map(({ name, value }) => (
{name} @@ -122,20 +68,7 @@ export default function TypographyTab() {
- {[ - "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 => (
{color} diff --git a/src/equicordplugins/components.dev/constants.ts b/src/equicordplugins/components.dev/constants.ts new file mode 100644 index 000000000..3d9bcf18d --- /dev/null +++ b/src/equicordplugins/components.dev/constants.ts @@ -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 = { + 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]; diff --git a/src/equicordplugins/components.dev/types/accordion.d.ts b/src/equicordplugins/components.dev/types/accordion.d.ts new file mode 100644 index 000000000..6a8690965 --- /dev/null +++ b/src/equicordplugins/components.dev/types/accordion.d.ts @@ -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; +} diff --git a/src/equicordplugins/components.dev/types/avatar.d.ts b/src/equicordplugins/components.dev/types/avatar.d.ts index da8c17e18..7a19d364b 100644 --- a/src/equicordplugins/components.dev/types/avatar.d.ts +++ b/src/equicordplugins/components.dev/types/avatar.d.ts @@ -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; } diff --git a/src/equicordplugins/components.dev/types/button.d.ts b/src/equicordplugins/components.dev/types/button.d.ts index 536bcf776..c8a4caf27 100644 --- a/src/equicordplugins/components.dev/types/button.d.ts +++ b/src/equicordplugins/components.dev/types/button.d.ts @@ -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 + | { type: "icon"; asset: React.ComponentType; } + | { type: "rive"; asset: React.ComponentType; riveProps?: Record; } + | { type: "sticker"; asset: any; component: React.ComponentType; }; + 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; + 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; + buttonRef?: React.Ref; 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; + buttonRef?: React.Ref; + className?: string; + style?: React.CSSProperties; } diff --git a/src/equicordplugins/components.dev/types/checkbox.d.ts b/src/equicordplugins/components.dev/types/checkbox.d.ts index 12483e45b..fbc0d31c7 100644 --- a/src/equicordplugins/components.dev/types/checkbox.d.ts +++ b/src/equicordplugins/components.dev/types/checkbox.d.ts @@ -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; - labelType?: "primary" | "secondary"; - usageVariant?: "single" | "indicator"; + labelType?: CheckboxLabelType; + usageVariant?: CheckboxUsageVariant; } export interface CheckboxGroupOption { diff --git a/src/equicordplugins/components.dev/types/chip.d.ts b/src/equicordplugins/components.dev/types/chip.d.ts new file mode 100644 index 000000000..7f931d72f --- /dev/null +++ b/src/equicordplugins/components.dev/types/chip.d.ts @@ -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; +} diff --git a/src/equicordplugins/components.dev/types/guildicon.d.ts b/src/equicordplugins/components.dev/types/guildicon.d.ts new file mode 100644 index 000000000..2439f187c --- /dev/null +++ b/src/equicordplugins/components.dev/types/guildicon.d.ts @@ -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; +} diff --git a/src/equicordplugins/components.dev/types/heading.d.ts b/src/equicordplugins/components.dev/types/heading.d.ts new file mode 100644 index 000000000..cd30c5a82 --- /dev/null +++ b/src/equicordplugins/components.dev/types/heading.d.ts @@ -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; +} diff --git a/src/equicordplugins/components.dev/types/index.ts b/src/equicordplugins/components.dev/types/index.ts index 85033ddb5..3277087a3 100644 --- a/src/equicordplugins/components.dev/types/index.ts +++ b/src/equicordplugins/components.dev/types/index.ts @@ -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"; diff --git a/src/equicordplugins/components.dev/types/input.d.ts b/src/equicordplugins/components.dev/types/input.d.ts index a55a38de2..221660b69 100644 --- a/src/equicordplugins/components.dev/types/input.d.ts +++ b/src/equicordplugins/components.dev/types/input.d.ts @@ -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 + | { icon: React.ComponentType; tooltip?: string; } + | { button: React.ReactNode; } + | { type: "tags"; tags: string[]; onRemove?: (tag: string) => void; } + | { type: "image"; src: string; }; + +export type TextInputTrailingAccessory = + | React.ComponentType + | { icon: React.ComponentType; 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; + inputRef?: React.Ref; + "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; + inputRef?: React.Ref; + "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; diff --git a/src/equicordplugins/components.dev/types/modal.d.ts b/src/equicordplugins/components.dev/types/modal.d.ts new file mode 100644 index 000000000..6568064c0 --- /dev/null +++ b/src/equicordplugins/components.dev/types/modal.d.ts @@ -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; + notice?: ModalNotice; + onClose: () => void; + role?: string; + className?: string; +} + +export interface ConfirmModalProps extends Omit { + transitionState?: number; + confirmText: string; + cancelText?: string; + onConfirm?: (setError?: (error: string) => void) => void | Promise; + 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 }>, options?: OpenModalOptions) => Promise; + closeModal: (modalKey: string) => void; + closeAllModals: () => void; + hasModalOpen: (modalKey: string) => boolean; + hasAnyModalOpen: () => boolean; +} + +export interface ModalComponents { + Modal: React.ComponentType; + ConfirmModal: React.ComponentType; + ExpressiveModal: React.ComponentType; +} diff --git a/src/equicordplugins/components.dev/types/notice.d.ts b/src/equicordplugins/components.dev/types/notice.d.ts index 9ff2e5a94..5d6ced3d9 100644 --- a/src/equicordplugins/components.dev/types/notice.d.ts +++ b/src/equicordplugins/components.dev/types/notice.d.ts @@ -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; diff --git a/src/equicordplugins/components.dev/types/scroller.d.ts b/src/equicordplugins/components.dev/types/scroller.d.ts new file mode 100644 index 000000000..e9170b559 --- /dev/null +++ b/src/equicordplugins/components.dev/types/scroller.d.ts @@ -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) => 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; +} diff --git a/src/equicordplugins/components.dev/types/skeleton.d.ts b/src/equicordplugins/components.dev/types/skeleton.d.ts new file mode 100644 index 000000000..a724aba69 --- /dev/null +++ b/src/equicordplugins/components.dev/types/skeleton.d.ts @@ -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; +} diff --git a/src/equicordplugins/components.dev/types/slider.d.ts b/src/equicordplugins/components.dev/types/slider.d.ts index ec02f942d..83abcec26 100644 --- a/src/equicordplugins/components.dev/types/slider.d.ts +++ b/src/equicordplugins/components.dev/types/slider.d.ts @@ -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; diff --git a/src/equicordplugins/components.dev/types/spinner.d.ts b/src/equicordplugins/components.dev/types/spinner.d.ts index a35708b4f..3352257f5 100644 --- a/src/equicordplugins/components.dev/types/spinner.d.ts +++ b/src/equicordplugins/components.dev/types/spinner.d.ts @@ -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 { + Type: SpinnerTypeEnum; +} diff --git a/src/equicordplugins/components.dev/types/switch.d.ts b/src/equicordplugins/components.dev/types/switch.d.ts index 5283de252..68e98d488 100644 --- a/src/equicordplugins/components.dev/types/switch.d.ts +++ b/src/equicordplugins/components.dev/types/switch.d.ts @@ -8,4 +8,10 @@ export interface ManaSwitchProps { checked: boolean; onChange?: (checked: boolean) => void; disabled?: boolean; + id?: string; + hasIcon?: boolean; + focusProps?: Record; + describedBy?: string; + labelledBy?: string; + innerRef?: React.Ref; } diff --git a/src/equicordplugins/components.dev/types/timestamp.d.ts b/src/equicordplugins/components.dev/types/timestamp.d.ts new file mode 100644 index 000000000..4725c184f --- /dev/null +++ b/src/equicordplugins/components.dev/types/timestamp.d.ts @@ -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; +} diff --git a/src/equicordplugins/components.dev/types/toast.d.ts b/src/equicordplugins/components.dev/types/toast.d.ts new file mode 100644 index 000000000..27527ca90 --- /dev/null +++ b/src/equicordplugins/components.dev/types/toast.d.ts @@ -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; + Position: Record; + genId: () => string; + show: (data: ToastData) => void; + pop: () => void; + create: (message: string, type: ToastTypeValue, options?: ToastOptions) => ToastData; +} diff --git a/src/equicordplugins/components.dev/types/tooltip.d.ts b/src/equicordplugins/components.dev/types/tooltip.d.ts index fe276427e..ed7a372f5 100644 --- a/src/equicordplugins/components.dev/types/tooltip.d.ts +++ b/src/equicordplugins/components.dev/types/tooltip.d.ts @@ -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; shouldShow: boolean; - position?: "top" | "bottom" | "left" | "right"; + position?: TooltipPosition; alignmentStrategy?: "edge" | "trigger-center"; align?: "left" | "center" | "right"; } diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 16952be95..e5dab1d12 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -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; } + 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: () => , render: () => , }; - // 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: () => , 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) => { section: string; element: ComponentType; label: string; id?: string; })[], customEntries: [] as EntryOptions[], - makeSettingsCategories(SectionTypes: SectionTypes) { + makeSettingsCategories(SectionTypes: Record) { 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, ) { - 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[], - ) { + wrapSettingsHook(originalHook: (...args: any[]) => Record[]) { 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[]); 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; } },