From 89f93e245ebd288dfc4ce492d70136b58a7d9234 Mon Sep 17 00:00:00 2001 From: prism Date: Sun, 21 Dec 2025 04:01:32 +0100 Subject: [PATCH] Components: add plugin (dev) (#585) * Components: add plugin (dev) * fix: rename alert component to notice and some fixes --- src/components/Alert.tsx | 59 ---- src/components/Notice.tsx | 72 +++++ src/components/ThemeSettings/ThemesTab.tsx | 6 +- src/components/index.ts | 2 +- .../tabs/sync/BackupAndRestoreTab.tsx | 6 +- .../settings/tabs/sync/CloudTab.tsx | 10 +- .../settings/tabs/vencord/index.tsx | 10 +- .../components/ComponentsTab.tsx | 140 +++++++++ .../components/SectionWrapper.tsx | 23 ++ .../components/icons/EquicordIcon.tsx | 16 ++ .../components.dev/components/index.tsx | 189 +++++++++++++ .../components/tabs/AnchorTab.tsx | 112 ++++++++ .../components/tabs/AvatarTab.tsx | 122 ++++++++ .../components/tabs/BadgeTab.tsx | 106 +++++++ .../components/tabs/ButtonsTab.tsx | 77 +++++ .../components/tabs/CalendarTab.tsx | 159 +++++++++++ .../components/tabs/CheckboxGroupTab.tsx | 123 ++++++++ .../components/tabs/CheckboxTab.tsx | 153 ++++++++++ .../components/tabs/ClickableTab.tsx | 230 +++++++++++++++ .../components/tabs/ColorPickerTab.tsx | 267 ++++++++++++++++++ .../components/tabs/ComboboxTab.tsx | 140 +++++++++ .../components/tabs/LayerModalTab.tsx | 140 +++++++++ .../components/tabs/ListboxTab.tsx | 146 ++++++++++ .../components/tabs/ModalTab.tsx | 161 +++++++++++ .../components/tabs/NoticeTab.tsx | 132 +++++++++ .../components/tabs/PaginatorTab.tsx | 125 ++++++++ .../components/tabs/PopoverTab.tsx | 153 ++++++++++ .../components/tabs/ProgressBarTab.tsx | 119 ++++++++ .../components/tabs/RadioGroupTab.tsx | 193 +++++++++++++ .../components/tabs/RichTooltipTab.tsx | 180 ++++++++++++ .../components/tabs/SearchBarTab.tsx | 81 ++++++ .../components/tabs/SelectTab.tsx | 123 ++++++++ .../components/tabs/SliderTab.tsx | 110 ++++++++ .../components/tabs/SpinnerTab.tsx | 95 +++++++ .../components/tabs/SwitchTab.tsx | 131 +++++++++ .../components/tabs/TabBarTab.tsx | 191 +++++++++++++ .../components/tabs/TextAreaTab.tsx | 80 ++++++ .../components/tabs/TextButtonTab.tsx | 76 +++++ .../components/tabs/TextInputTab.tsx | 92 ++++++ .../components/tabs/TooltipTab.tsx | 99 +++++++ .../components/tabs/TypographyTab.tsx | 188 ++++++++++++ src/equicordplugins/components.dev/index.tsx | 74 +++++ src/equicordplugins/components.dev/styles.css | 89 ++++++ .../components.dev/types/anchor.d.ts | 19 ++ .../components.dev/types/avatar.d.ts | 25 ++ .../components.dev/types/badge.d.ts | 48 ++++ .../components.dev/types/button.d.ts | 29 ++ .../components.dev/types/calendar.d.ts | 32 +++ .../components.dev/types/checkbox.d.ts | 32 +++ .../components.dev/types/clickable.d.ts | 19 ++ .../components.dev/types/colorpicker.d.ts | 77 +++++ .../components.dev/types/index.ts | 25 ++ .../components.dev/types/input.d.ts | 46 +++ .../components.dev/types/notice.d.ts | 18 ++ .../components.dev/types/paginator.d.ts | 24 ++ .../components.dev/types/progress.d.ts | 25 ++ .../components.dev/types/radio.d.ts | 26 ++ .../components.dev/types/select.d.ts | 66 +++++ .../components.dev/types/slider.d.ts | 29 ++ .../components.dev/types/spinner.d.ts | 15 + .../components.dev/types/switch.d.ts | 11 + .../components.dev/types/tabbar.d.ts | 55 ++++ .../components.dev/types/tooltip.d.ts | 53 ++++ src/equicordplugins/followVoiceUser/index.tsx | 6 +- .../neverPausePreviews/index.tsx | 6 +- src/equicordplugins/platformSpoofer/index.tsx | 6 +- .../questCompleter.discordDesktop/index.tsx | 6 +- src/equicordplugins/questFocused/index.tsx | 6 +- .../universalMention/index.tsx | 6 +- 69 files changed, 5416 insertions(+), 94 deletions(-) delete mode 100644 src/components/Alert.tsx create mode 100644 src/components/Notice.tsx create mode 100644 src/equicordplugins/components.dev/components/ComponentsTab.tsx create mode 100644 src/equicordplugins/components.dev/components/SectionWrapper.tsx create mode 100644 src/equicordplugins/components.dev/components/icons/EquicordIcon.tsx create mode 100644 src/equicordplugins/components.dev/components/index.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ModalTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/SelectTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/SliderTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/TextInputTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx create mode 100644 src/equicordplugins/components.dev/index.tsx create mode 100644 src/equicordplugins/components.dev/styles.css create mode 100644 src/equicordplugins/components.dev/types/anchor.d.ts create mode 100644 src/equicordplugins/components.dev/types/avatar.d.ts create mode 100644 src/equicordplugins/components.dev/types/badge.d.ts create mode 100644 src/equicordplugins/components.dev/types/button.d.ts create mode 100644 src/equicordplugins/components.dev/types/calendar.d.ts create mode 100644 src/equicordplugins/components.dev/types/checkbox.d.ts create mode 100644 src/equicordplugins/components.dev/types/clickable.d.ts create mode 100644 src/equicordplugins/components.dev/types/colorpicker.d.ts create mode 100644 src/equicordplugins/components.dev/types/index.ts create mode 100644 src/equicordplugins/components.dev/types/input.d.ts create mode 100644 src/equicordplugins/components.dev/types/notice.d.ts create mode 100644 src/equicordplugins/components.dev/types/paginator.d.ts create mode 100644 src/equicordplugins/components.dev/types/progress.d.ts create mode 100644 src/equicordplugins/components.dev/types/radio.d.ts create mode 100644 src/equicordplugins/components.dev/types/select.d.ts create mode 100644 src/equicordplugins/components.dev/types/slider.d.ts create mode 100644 src/equicordplugins/components.dev/types/spinner.d.ts create mode 100644 src/equicordplugins/components.dev/types/switch.d.ts create mode 100644 src/equicordplugins/components.dev/types/tabbar.d.ts create mode 100644 src/equicordplugins/components.dev/types/tooltip.d.ts diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx deleted file mode 100644 index 8e83f5738..000000000 --- a/src/components/Alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { filters, mapMangledModuleLazy } from "@webpack"; -import type { ComponentType, CSSProperties, ReactNode } from "react"; - -const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE="positive', { - HelpMessageTypes: filters.byProps("POSITIVE", "WARNING", "INFO", "ERROR"), - HelpMessage: filters.byCode(".iconDiv") -}); - -export interface AlertProps { - children: ReactNode; - className?: string; - style?: CSSProperties; - icon?: ComponentType<{ className?: string; color?: string; }>; -} - -function Warning({ children, className, style, icon }: AlertProps) { - return ( - - {children} - - ); -} - -function Info({ children, className, style, icon }: AlertProps) { - return ( - - {children} - - ); -} - -function Error({ children, className, style, icon }: AlertProps) { - return ( - - {children} - - ); -} - -function Positive({ children, className, style, icon }: AlertProps) { - return ( - - {children} - - ); -} - -export const Alert = { - Warning, - Info, - Error, - Positive -}; diff --git a/src/components/Notice.tsx b/src/components/Notice.tsx new file mode 100644 index 000000000..e03a068dd --- /dev/null +++ b/src/components/Notice.tsx @@ -0,0 +1,72 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { filters, mapMangledModuleLazy } from "@webpack"; +import type { ComponentType, CSSProperties, ReactNode } from "react"; + +export type NoticeType = "warn" | "info" | "danger" | "positive" | "preview"; + +export const NoticeTypes: Record<"WARNING" | "INFO" | "ERROR" | "POSITIVE" | "PREVIEW", NoticeType> = { + WARNING: "warn", + INFO: "info", + ERROR: "danger", + POSITIVE: "positive", + PREVIEW: "preview" +}; + +const { HelpMessage } = mapMangledModuleLazy('POSITIVE="positive"', { + HelpMessage: filters.byCode(".iconDiv") +}); + +export interface NoticeProps { + children: ReactNode; + className?: string; + style?: CSSProperties; + icon?: ComponentType<{ className?: string; color?: string; }>; + action?: ReactNode; + textColor?: string; + textVariant?: string; + hidden?: boolean; +} + +function Warning({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) { + return ( + + ); +} + +function Info({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) { + return ( + + ); +} + +function Error({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) { + return ( + + ); +} + +function Positive({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) { + return ( + + ); +} + +export const Notice = { + Warning, + Info, + Error, + Positive +}; diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index 31ca471ac..61522cac7 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -20,7 +20,6 @@ import "./themesStyles.css"; import { isPluginEnabled } from "@api/PluginManager"; import { Settings, useSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; import { Button } from "@components/Button"; import { Divider } from "@components/Divider"; import { ErrorCard } from "@components/ErrorCard"; @@ -29,6 +28,7 @@ import { FormSwitch } from "@components/FormSwitch"; import { Heading } from "@components/Heading"; import { CogWheel, DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PluginIcon, PlusIcon, RestartIcon } from "@components/Icons"; import { Link } from "@components/Link"; +import { Notice } from "@components/Notice"; import { Paragraph } from "@components/Paragraph"; import { AddonCard, openPluginModal, QuickAction, QuickActionCard, SettingsTab, wrapTab } from "@components/settings"; import { OnlineThemeCard } from "@components/settings/OnlineThemeCard"; @@ -727,9 +727,9 @@ function ThemesTab() { }} /> - + Looking for themes? Check out BetterDiscord Themes or search on GitHub. When downloading from BetterDiscord, click "Download" and place the .theme.css file into your themes folder. - +
- + Importing a settings file will overwrite your current settings. Make sure to export a backup first if you want to keep your current configuration. - + What's included in a backup diff --git a/src/components/settings/tabs/sync/CloudTab.tsx b/src/components/settings/tabs/sync/CloudTab.tsx index 3f2f5a0ab..117ea2ba6 100644 --- a/src/components/settings/tabs/sync/CloudTab.tsx +++ b/src/components/settings/tabs/sync/CloudTab.tsx @@ -19,7 +19,6 @@ import { useSettings } from "@api/Settings"; import { authorizeCloud, deauthorizeCloud } from "@api/SettingsSync/cloudSetup"; import { deleteCloudSettings, eraseAllCloudData, getCloudSettings, putCloudSettings } from "@api/SettingsSync/cloudSync"; -import { Alert } from "@components/Alert"; import { Button } from "@components/Button"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Divider } from "@components/Divider"; @@ -28,6 +27,7 @@ import { FormSwitch } from "@components/FormSwitch"; import { Heading } from "@components/Heading"; import { CloudDownloadIcon, CloudUploadIcon } from "@components/Icons"; import { Link } from "@components/Link"; +import { Notice } from "@components/Notice"; import { Paragraph } from "@components/Paragraph"; import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab"; import { localStorage } from "@utils/localStorage"; @@ -97,11 +97,11 @@ function CloudTab() { Equicord's cloud integration allows you to sync your settings across multiple devices and Discord installations. Your data is securely stored and can be easily restored at any time. - + We use our own Equicloud backend with enhanced features. View our privacy policy to see what we store and how we use your data. Equicloud is BSD 3.0 licensed, so you can self-host if preferred. - + {!isAuthenticated && ( - + Enable cloud integration above to use settings sync features. - + )} diff --git a/src/components/settings/tabs/vencord/index.tsx b/src/components/settings/tabs/vencord/index.tsx index ed116cee4..11047ca05 100644 --- a/src/components/settings/tabs/vencord/index.tsx +++ b/src/components/settings/tabs/vencord/index.tsx @@ -8,13 +8,13 @@ import "./VencordTab.css"; import { openNotificationLogModal } from "@api/Notifications/notificationLog"; import { useSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; import { Button } from "@components/Button"; import { Divider } from "@components/Divider"; import { Flex } from "@components/Flex"; import { FormSwitch } from "@components/FormSwitch"; import { Heading } from "@components/Heading"; import { FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "@components/Icons"; +import { Notice } from "@components/Notice"; import { Paragraph } from "@components/Paragraph"; import { openContributorModal, openPluginModal, SettingsTab, wrapTab } from "@components/settings"; import { QuickAction, QuickActionCard } from "@components/settings/QuickAction"; @@ -224,7 +224,7 @@ function EquicordSettings() { Configure how Equicord behaves and integrates with Discord. These settings affect the Discord client's appearance and behavior. - + You can customize where this settings section appears in Discord's settings menu by configuring the{" "} Settings Plugin . - + {Switches.filter((s): s is Exclude => !!s).map( s => ( @@ -246,9 +246,9 @@ function EquicordSettings() { s.warning.enabled ? ( <> {s.description} - + {s.warning.message} - + ) : ( s.description diff --git a/src/equicordplugins/components.dev/components/ComponentsTab.tsx b/src/equicordplugins/components.dev/components/ComponentsTab.tsx new file mode 100644 index 000000000..0387abf1d --- /dev/null +++ b/src/equicordplugins/components.dev/components/ComponentsTab.tsx @@ -0,0 +1,140 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "../styles.css"; + +import { Heading } from "@components/Heading"; +import { SettingsTab, wrapTab } from "@components/settings"; +import { Margins } from "@utils/margins"; +import { TabBar, useState } from "@webpack/common"; + +import AnchorTab from "./tabs/AnchorTab"; +import AvatarTab from "./tabs/AvatarTab"; +import BadgeTab from "./tabs/BadgeTab"; +import ButtonsTab from "./tabs/ButtonsTab"; +import CalendarTab from "./tabs/CalendarTab"; +import CheckboxGroupTab from "./tabs/CheckboxGroupTab"; +import CheckboxTab from "./tabs/CheckboxTab"; +import ClickableTab from "./tabs/ClickableTab"; +import ColorPickerTab from "./tabs/ColorPickerTab"; +import ComboboxTab from "./tabs/ComboboxTab"; +import LayerModalTab from "./tabs/LayerModalTab"; +import ListboxTab from "./tabs/ListboxTab"; +import ModalTab from "./tabs/ModalTab"; +import NoticeTab from "./tabs/NoticeTab"; +import PaginatorTab from "./tabs/PaginatorTab"; +import PopoverTab from "./tabs/PopoverTab"; +import ProgressBarTab from "./tabs/ProgressBarTab"; +import RadioGroupTab from "./tabs/RadioGroupTab"; +import RichTooltipTab from "./tabs/RichTooltipTab"; +import SearchBarTab from "./tabs/SearchBarTab"; +import SelectTab from "./tabs/SelectTab"; +import SliderTab from "./tabs/SliderTab"; +import SpinnerTab from "./tabs/SpinnerTab"; +import SwitchTab from "./tabs/SwitchTab"; +import TabBarTab from "./tabs/TabBarTab"; +import TextAreaTab from "./tabs/TextAreaTab"; +import TextButtonTab from "./tabs/TextButtonTab"; +import TextInputTab from "./tabs/TextInputTab"; +import TooltipTab from "./tabs/TooltipTab"; +import TypographyTab from "./tabs/TypographyTab"; + +const TABS = [ + { id: "anchor", label: "Anchor" }, + { id: "avatar", label: "Avatar" }, + { id: "badge", label: "Badge" }, + { id: "buttons", label: "Buttons" }, + { id: "calendar", label: "Calendar" }, + { id: "checkbox", label: "Checkbox" }, + { id: "checkboxgroup", label: "CheckboxGroup" }, + { id: "clickable", label: "Clickable" }, + { id: "colorpicker", label: "ColorPicker" }, + { id: "combobox", label: "Combobox" }, + { id: "layermodal", label: "LayerModal" }, + { id: "listbox", label: "Listbox" }, + { id: "modal", label: "Modal" }, + { id: "notice", label: "Notice" }, + { id: "paginator", label: "Paginator" }, + { id: "popover", label: "Popover" }, + { id: "progressbar", label: "ProgressBar" }, + { id: "radiogroup", label: "RadioGroup" }, + { id: "richtooltip", label: "RichTooltip" }, + { id: "searchbar", label: "SearchBar" }, + { id: "select", label: "Select" }, + { id: "slider", label: "Slider" }, + { id: "spinner", label: "Spinner" }, + { id: "switch", label: "Switch" }, + { id: "tabbar", label: "TabBar" }, + { id: "textarea", label: "TextArea" }, + { id: "textbutton", label: "TextButton" }, + { id: "textinput", label: "TextInput" }, + { id: "tooltip", label: "Tooltip" }, + { id: "typography", label: "Typography" }, +].sort((a, b) => a.label.localeCompare(b.label)); + +type TabId = typeof TABS[number]["id"]; + +const TAB_COMPONENTS: Record = { + anchor: AnchorTab, + avatar: AvatarTab, + badge: BadgeTab, + buttons: ButtonsTab, + calendar: CalendarTab, + checkbox: CheckboxTab, + checkboxgroup: CheckboxGroupTab, + clickable: ClickableTab, + colorpicker: ColorPickerTab, + combobox: ComboboxTab, + layermodal: LayerModalTab, + listbox: ListboxTab, + modal: ModalTab, + notice: NoticeTab, + paginator: PaginatorTab, + popover: PopoverTab, + progressbar: ProgressBarTab, + radiogroup: RadioGroupTab, + richtooltip: RichTooltipTab, + searchbar: SearchBarTab, + select: SelectTab, + slider: SliderTab, + spinner: SpinnerTab, + switch: SwitchTab, + tabbar: TabBarTab, + textarea: TextAreaTab, + textbutton: TextButtonTab, + textinput: TextInputTab, + tooltip: TooltipTab, + typography: TypographyTab, +}; + +function ComponentsTab() { + const [currentTab, setCurrentTab] = useState("avatar"); + const TabComponent = TAB_COMPONENTS[currentTab]; + + return ( + + Components + + {TABS.map(tab => ( + + {tab.label} + + ))} + +
+ +
+
+ ); +} + +export default wrapTab(ComponentsTab, "ComponentsTab"); diff --git a/src/equicordplugins/components.dev/components/SectionWrapper.tsx b/src/equicordplugins/components.dev/components/SectionWrapper.tsx new file mode 100644 index 000000000..be236a9c3 --- /dev/null +++ b/src/equicordplugins/components.dev/components/SectionWrapper.tsx @@ -0,0 +1,23 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Heading } from "@components/Heading"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; + +interface SectionWrapperProps { + title: string; + children: React.ReactNode; +} + +export function SectionWrapper({ title, children }: SectionWrapperProps) { + return ( +
+ {title} + {children} +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/icons/EquicordIcon.tsx b/src/equicordplugins/components.dev/components/icons/EquicordIcon.tsx new file mode 100644 index 000000000..4999f587b --- /dev/null +++ b/src/equicordplugins/components.dev/components/icons/EquicordIcon.tsx @@ -0,0 +1,16 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export function EquicordIcon({ className, size, color }: { className?: string; size?: string; color?: string; }) { + const dimension = size === "xs" ? 12 : size === "sm" ? 16 : size === "lg" ? 24 : 20; + return ( + + + + + + ); +} diff --git a/src/equicordplugins/components.dev/components/index.tsx b/src/equicordplugins/components.dev/components/index.tsx new file mode 100644 index 000000000..2fe042eec --- /dev/null +++ b/src/equicordplugins/components.dev/components/index.tsx @@ -0,0 +1,189 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { proxyLazy } from "@utils/lazy"; +import { findByCodeLazy, findComponentByCodeLazy, findLazy } from "@webpack"; + +import { + AnchorProps, + AvatarProps, + BadgeShapesType, + CheckboxGroupOption, + CircleBadgeProps, + ClickableProps, + ColorPickerProps, + ColorPickerWithSwatchesProps, + ColorSwatchProps, + CustomColorButtonProps, + DefaultColorButtonProps, + GradientColor, + IconBadgeProps, + ListboxItem, + ManaBaseRadioGroupProps, + ManaButtonProps, + ManaCalendarProps, + ManaCheckboxGroupProps, + ManaCheckboxProps, + ManaComboboxProps, + ManaDatePickerProps, + ManaListboxProps, + ManaPopoverProps, + ManaRichTooltipProps, + ManaSelectOption, + ManaSelectProps, + ManaSwitchProps, + ManaTextAreaProps, + ManaTextButtonProps, + ManaTextInputProps, + ManaTooltipProps, + NoticeProps, + NoticeType, + NumberBadgeProps, + PaginatorProps, + PopoverAction, + ProgressBarProps, + RadioOption, + SearchBarProps, + SliderProps, + SpinnerProps, + StandaloneRadioIndicatorProps, + TabBarComponent, + TabBarHeaderProps, + TabBarItemProps, + TabBarProps, + TabBarSeparatorProps, + TextBadgeProps +} from "../types"; + +export type { + AnchorProps, + AvatarProps, + BadgeShapesType, + CheckboxGroupOption, + CircleBadgeProps, + ColorPickerProps, + ColorPickerWithSwatchesProps, + ColorSwatchProps, + CustomColorButtonProps, + DefaultColorButtonProps, + GradientColor, + IconBadgeProps, + ListboxItem, + ManaBaseRadioGroupProps, + ManaButtonProps, + ManaCalendarProps, + ManaCheckboxGroupProps, + ManaCheckboxProps, + ManaComboboxProps, + ManaDatePickerProps, + ManaListboxProps, + ManaPopoverProps, + ManaRichTooltipProps, + ManaSelectOption, + ManaSelectProps, + ManaSwitchProps, + ManaTextAreaProps, + ManaTextButtonProps, + ManaTextInputProps, + ManaTooltipProps, + NoticeProps, + NoticeType, + NumberBadgeProps, + PaginatorProps, + PopoverAction, + ProgressBarProps, + RadioOption, + SearchBarProps, + SliderProps, + SpinnerProps, + StandaloneRadioIndicatorProps, + TabBarComponent, + TabBarHeaderProps, + TabBarItemProps, + TabBarProps, + TabBarSeparatorProps, + TextBadgeProps +}; + +export const Anchor = findComponentByCodeLazy("anchorUnderlineOnHover", "useDefaultUnderlineStyles") as React.ComponentType; + +export const ManaButton = findComponentByCodeLazy('"data-mana-component":"button"') as React.ComponentType; +export const ManaTextButton = findComponentByCodeLazy('"data-mana-component":"text-button"') as React.ComponentType; +export const ManaSwitch = findComponentByCodeLazy('"data-mana-component":"switch"') as React.ComponentType; +export const ManaTextInput = findComponentByCodeLazy('"data-mana-component":"text-input"') as React.ComponentType; +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 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; +export const ManaCombobox = findComponentByCodeLazy("itemToString", "multiSelect", "maxVisibleItems") as React.ComponentType; +export const ManaListbox = findComponentByCodeLazy('"data-mana-component":"listbox"') as React.ComponentType; +export const ManaCheckboxGroup = findComponentByCodeLazy('"data-mana-component":"checkbox-group"') as React.ComponentType; +export const ManaBaseRadioGroup = findComponentByCodeLazy('"data-mana-component":"BaseRadioGroup"') as React.ComponentType; +export const StandaloneRadioIndicator = findComponentByCodeLazy("standaloneRadioIndicator", "animateIn", "animateOut") as React.ComponentType; +export const ManaPopover = findComponentByCodeLazy('"data-mana-component":"popover"') as React.ComponentType; + +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 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; +export const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", "showEyeDropper") as React.ComponentType; + +const ColorPickerModule = findLazy(m => { + const values = Object.values(m); + return values.length === 4 && + values.some(v => typeof v === "function" && v.toString?.().includes('id:"color-picker"')) && + values.some(v => typeof v === "function" && v.toString?.().includes("isDefault:!0")); +}); + +function getColorPickerExport(check: (fn: Function) => boolean): React.ComponentType { + for (const value of Object.values(ColorPickerModule)) { + if (typeof value === "function" && check(value)) return value as React.ComponentType; + } + return (() => null) as React.ComponentType; +} + +export const ColorPickerWithSwatches: React.ComponentType = proxyLazy(() => + getColorPickerExport(fn => fn.toString().includes('id:"color-picker"')) +); +export const ColorSwatch: React.ComponentType = proxyLazy(() => + getColorPickerExport(fn => fn.toString().includes("isDefault") && fn.toString().includes("isCustom") && fn.toString().includes("colorPickerSwatch")) +); +export const DefaultColorButton: React.ComponentType = proxyLazy(() => + getColorPickerExport(fn => fn.toString().includes("isDefault:!0") && fn.toString().includes("allowBlackCustomColor")) +); +export const CustomColorButton: React.ComponentType = proxyLazy(() => + getColorPickerExport(fn => fn.toString().includes("isCustom:!0") && fn.toString().includes("presets")) +); + +const BadgeModule = findLazy(m => { + const values = Object.values(m); + return values.length < 15 && + values.some(v => typeof v === "function" && v.toString?.().includes("numberBadge")) && + values.some(v => typeof v === "object" && (v as BadgeShapesType)?.ROUND && (v as BadgeShapesType)?.ROUND_LEFT); +}); + +function getBadgeExport(check: (fn: Function) => boolean): React.ComponentType { + for (const value of Object.values(BadgeModule)) { + if (typeof value === "function" && check(value)) return value as React.ComponentType; + } + return (() => null) as React.ComponentType; +} + +export const NumberBadge: React.ComponentType = proxyLazy(() => getBadgeExport(fn => fn.toString().includes("numberBadge"))); +export const TextBadge: React.ComponentType = proxyLazy(() => getBadgeExport(fn => fn.toString().includes("textBadge"))); +export const IconBadge: React.ComponentType = proxyLazy(() => getBadgeExport(fn => fn.toString().includes("iconBadge"))); +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; diff --git a/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx b/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx new file mode 100644 index 000000000..6a2208eec --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx @@ -0,0 +1,112 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; + +import { Anchor } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function AnchorTab() { + return ( +
+ + + A styled anchor/link component with underline on hover. + + + Discord Homepage + + + + + + Disable the default underline-on-hover behavior. + + + No underline on hover + + + + + + Open in same tab instead of new tab. + + + Opens in same tab + + + + + + Hover to see the title tooltip. + + + Hover for tooltip + + + + + + Custom styles applied via style prop. + + + Green styled link + + + + + + Custom click handler (prevents default navigation). + + { + e.preventDefault(); + alert("Link clicked!"); + }} + > + Click me (shows alert) + + + + + + Links in a sentence. + + + Check out the Terms of Service and{" "} + Privacy Policy for more info. + + + + + + Anchor - Styled link component (also known as MaskedLink) + + • href?: string - Link URL + • onClick?: MouseEventHandler - Click handler + • className?: string - Additional CSS classes + • children?: ReactNode - Link content + • rel?: string - Link relationship (auto-set for external) + • target?: string - Link target (auto-set to _blank for external) + • useDefaultUnderlineStyles?: boolean - Show underline on hover (default: true) + • title?: string - Tooltip text on hover + • style?: CSSProperties - Inline styles + • focusProps?: object - Focus ring configuration + • ref?: Ref - Forward ref to anchor element + + Behavior + + • External links automatically get rel="noreferrer noopener" and target="_blank" + • If no onClick is provided but href is set, uses Discord's default link interceptor + • Untrusted external links show a warning modal before navigating + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx b/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx new file mode 100644 index 000000000..d96ac1ad1 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx @@ -0,0 +1,122 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { UserStore } from "@webpack/common"; + +import { Avatar } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const SIZES = ["SIZE_16", "SIZE_20", "SIZE_24", "SIZE_32", "SIZE_40", "SIZE_48", "SIZE_56", "SIZE_80", "SIZE_120"] as const; + +export default function AvatarTab() { + const currentUser = UserStore.getCurrentUser(); + const avatarUrl = currentUser?.getAvatarURL?.(undefined, 128); + + return ( +
+ + + Avatar component from @webpack/common with various sizes. + +
+ {SIZES.map(size => ( +
+ + + {size.replace("SIZE_", "")} + +
+ ))} +
+
+ + + + Avatar with status indicator. + +
+
+ + Online +
+
+ + Idle +
+
+ + DND +
+
+ + Offline +
+
+
+ + + + Avatar with mobile indicator. + +
+ +
+
+ + + + Avatar with speaking ring indicator. + +
+
+ + Speaking +
+
+
+ + + • src - Avatar image URL + • size - SIZE_16 to SIZE_120 + • status - online, idle, dnd, offline + • isMobile - Show mobile indicator + • isTyping - Show typing indicator + • isSpeaking - Show speaking ring + • statusTooltip - Show status on hover + • aria-label - Accessibility label + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx b/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx new file mode 100644 index 000000000..45dd21b98 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx @@ -0,0 +1,106 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; + +import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, TextBadge } from ".."; +import { EquicordIcon } from "../icons/EquicordIcon"; +import { SectionWrapper } from "../SectionWrapper"; + +export default function BadgeTab() { + return ( +
+ + + Displays a count value. Automatically abbreviates large numbers. + +
+ + + + + +
+
+ + + + Displays text content. + +
+ + + +
+
+ + + + Displays an icon inside a badge. + +
+ + + +
+
+ + + + A simple circular badge indicator. + +
+ + + +
+
+ + + + Badges support different shapes via the shape prop. + +
+ + + + +
+
+ + + + Custom colors can be applied. Use disableColor for no background. + +
+ + + + + +
+
+ + + + NumberBadge: count, maxDigits?, color?, disableColor?, shape?, className?, style? + + + TextBadge: text, color?, disableColor?, shape?, className?, style? + + + IconBadge: icon, color?, disableColor?, shape?, className?, style? + + + CircleBadge: color?, disableColor?, shape?, className?, style? + + + BadgeShapes: ROUND, ROUND_LEFT, ROUND_RIGHT, SQUARE + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx b/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx new file mode 100644 index 000000000..61ed9d174 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx @@ -0,0 +1,77 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaButton } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const VARIANTS = ["primary", "secondary", "critical-primary", "critical-secondary", "overlay-primary", "overlay-secondary", "expressive"] as const; +const SIZES = ["xs", "sm", "md"] as const; + +export default function ButtonsTab() { + const [loading, setLoading] = useState(false); + + return ( +
+ +
+ {VARIANTS.map(variant => ( + + ))} +
+
+ + +
+ {SIZES.map(size => ( + + ))} +
+
+ + +
+ + { + setLoading(true); + setTimeout(() => setLoading(false), 2000); + }} + /> + + +
+
+ + +
+ {VARIANTS.map(variant => ( + + ))} +
+
+ + + • text - Button label text + • variant - primary, secondary, critical-primary, critical-secondary, overlay-primary, overlay-secondary, expressive + • size - xs, sm, md + • disabled - Disable button + • loading - Show loading spinner + • fullWidth - Expand to container width + • rounded - Rounded corners + • icon - Icon component + • iconPosition - start or end + • onClick - Click handler + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx b/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx new file mode 100644 index 000000000..72b3a092a --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx @@ -0,0 +1,159 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaCalendar, ManaDatePicker } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function CalendarTab() { + const [calendarValue, setCalendarValue] = useState(null); + const [datePickerValue, setDatePickerValue] = useState(null); + + return ( +
+ + + Basic calendar component for date selection. + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+ + + + Date picker with input field and calendar popover. + +
+ +
+
+ + + + Date picker with hour/minute granularity. + +
+ +
+
+ + +
+ + + +
+
+ + + + ManaCalendar + + + • value: any - Selected date value + + + • onChange?: (value: any) => void - Selection callback + + + • disabled?: boolean - Disable calendar + + + • readOnly?: boolean - Read-only mode + + + • aria-label?: string - Accessibility label + + + ManaDatePicker + + + • value: any - Selected date value + + + • onChange?: (value: any) => void - Selection callback + + + • label?: string - Input label + + + • description?: string - Description text + + + • granularity?: "day" | "hour" | "minute" - Time precision + + + • hourCycle?: 12 | 24 - Hour format + + + • disabled?: boolean - Disable picker + + + • required?: boolean - Mark as required + + + • errorMessage?: string - Error text to display + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx b/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx new file mode 100644 index 000000000..67daa050e --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx @@ -0,0 +1,123 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { CheckboxGroupOption, ManaCheckboxGroup } from ".."; +import { EquicordIcon } from "../icons/EquicordIcon"; +import { SectionWrapper } from "../SectionWrapper"; + +const BASIC_OPTIONS: CheckboxGroupOption[] = [ + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, + { value: "option3", label: "Option 3" }, +]; + +const OPTIONS_WITH_DESCRIPTIONS: CheckboxGroupOption[] = [ + { value: "notifications", label: "Notifications", description: "Receive push notifications" }, + { value: "sounds", label: "Sounds", description: "Play sounds for events" }, + { value: "badges", label: "Badges", description: "Show unread badges" }, +]; + +const OPTIONS_WITH_ICONS: CheckboxGroupOption[] = [ + { value: "item1", label: "Item with icon", description: "This option has an icon", leadingIcon: EquicordIcon }, + { value: "item2", label: "Another with icon", leadingIcon: EquicordIcon }, + { value: "item3", label: "Third with icon", leadingIcon: EquicordIcon }, +]; + +const OPTIONS_MIXED: CheckboxGroupOption[] = [ + { value: "enabled", label: "Enabled option" }, + { value: "disabled", label: "Disabled option", disabled: true }, + { value: "another", label: "Another enabled" }, +]; + +export default function CheckboxGroupTab() { + const [basicSelected, setBasicSelected] = useState<(string | number)[]>(["option1"]); + const [descSelected, setDescSelected] = useState<(string | number)[]>(["notifications", "sounds"]); + const [iconSelected, setIconSelected] = useState<(string | number)[]>(["item1"]); + const [mixedSelected, setMixedSelected] = useState<(string | number)[]>(["enabled"]); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + CheckboxGroup + + + • options: CheckboxGroupOption[] - Array of options + + + • selectedValues: (string | number)[] - Currently selected values + + + • onChange?: (values: (string | number)[]) => void - Called when selection changes + + + • disabled?: boolean - Disable entire group + + + CheckboxGroupOption + + + • value: string | number - Option value + + + • label: string - Option label + + + • description?: string - Optional description + + + • disabled?: boolean - Disable individual option + + + • leadingIcon?: React.ComponentType - Icon before checkbox + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx new file mode 100644 index 000000000..f69a81698 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx @@ -0,0 +1,153 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaCheckbox } from ".."; +import { EquicordIcon } from "../icons/EquicordIcon"; +import { SectionWrapper } from "../SectionWrapper"; + +export default function CheckboxTab() { + const [basicChecked, setBasicChecked] = useState(true); + const [basicUnchecked, setBasicUnchecked] = useState(false); + + const [withDesc, setWithDesc] = useState(true); + + const [labelPrimary, setLabelPrimary] = useState(true); + const [labelSecondary, setLabelSecondary] = useState(false); + + const [withIcon, setWithIcon] = useState(true); + + const [usageSingle, setUsageSingle] = useState(true); + const [usageIndicator, setUsageIndicator] = useState(false); + + return ( +
+ +
+ + +
+
+ + +
+ +
+
+ + +
+ + +
+
+ + +
+ +
+
+ + +
+ + + +
+
+ + +
+ + +
+
+ + + + • checked: boolean - Current checked state + + + • onChange?: (checked: boolean) => void - Called when toggled + + + • label?: string - Label text + + + • description?: string - Description below label + + + • labelType?: "primary" | "secondary" - Label styling + + + • leadingIcon?: React.ComponentType - Icon before checkbox + + + • usageVariant?: "single" | "indicator" - Visual style + + + • disabled?: boolean - Disable interaction + + + • displayOnly?: boolean - Display without interaction + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx b/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx new file mode 100644 index 000000000..b3b79cf4b --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx @@ -0,0 +1,230 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { Clickable } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function ClickableTab() { + const [clickCount, setClickCount] = useState(0); + const [lastAction, setLastAction] = useState("None"); + + return ( +
+ + + Default clickable element. Click count: {clickCount} + + setClickCount(c => c + 1)} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-secondary)", + borderRadius: 4, + cursor: "pointer" + }} + > + Click me! + + + + + + Clickable can render as different HTML elements using the tag prop. + +
+ setLastAction("Clicked div")} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-tertiary)", + borderRadius: 4, + cursor: "pointer" + }} + > + tag="div" + + setLastAction("Clicked span")} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-tertiary)", + borderRadius: 4, + cursor: "pointer" + }} + > + tag="span" + + setLastAction("Clicked button")} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-tertiary)", + borderRadius: 4, + cursor: "pointer", + border: "none", + color: "inherit" + }} + > + tag="button" + +
+ + Last action: {lastAction} + +
+ + + + Accessibility props for keyboard navigation. + +
+ setLastAction("Role: button")} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-secondary)", + borderRadius: 4, + cursor: "pointer" + }} + > + role="button" tabIndex=0 + + setLastAction("Role: link")} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-secondary)", + borderRadius: 4, + cursor: "pointer" + }} + > + role="link" tabIndex=0 + +
+
+ + + + When ignoreKeyPress is true, Enter/Space won't trigger onClick. + +
+ setLastAction("Normal (keys work)")} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-secondary)", + borderRadius: 4, + cursor: "pointer" + }} + > + Normal (try Enter/Space) + + setLastAction("Ignored (keys blocked)")} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-secondary)", + borderRadius: 4, + cursor: "pointer" + }} + > + ignoreKeyPress (keys blocked) + +
+
+ + + + When no onClick is provided, renders as non-interactive element. + + + Non-interactive (no onClick) + + + + + + Clickable with different visual styles. + +
+ setLastAction("Card clicked")} + style={{ + padding: 16, + backgroundColor: "var(--background-secondary)", + borderRadius: 8, + cursor: "pointer", + width: 120, + textAlign: "center" + }} + > +
📁
+
Card Style
+
+ setLastAction("Chip clicked")} + style={{ + padding: "4px 12px", + backgroundColor: "var(--brand-experiment)", + borderRadius: 16, + cursor: "pointer", + color: "white" + }} + > + Chip Style + + setLastAction("List item clicked")} + style={{ + padding: "8px 16px", + backgroundColor: "var(--background-secondary)", + cursor: "pointer", + width: 200 + }} + > + List Item Style → + +
+
+ + + + Clickable - Interactive element wrapper + + • tag?: keyof JSX.IntrinsicElements - HTML element to render (default: div) + • onClick?: MouseEventHandler - Click handler + • onKeyPress?: KeyboardEventHandler - Key press handler + • focusProps?: object - Props for FocusLock wrapper + • innerRef?: Ref - Ref to the inner element + • role?: string - ARIA role attribute + • tabIndex?: number - Tab index for keyboard navigation + • ignoreKeyPress?: boolean - Disable Enter/Space triggering onClick + • href?: string - Optional href (affects key handling) + • className?: string - CSS class name + • children?: ReactNode - Child elements + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx b/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx new file mode 100644 index 000000000..4e09667e2 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx @@ -0,0 +1,267 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ColorPicker, ColorPickerWithSwatches, ColorSwatch, CustomColorButton, DefaultColorButton } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const DEFAULT_COLOR = 0x5865F2; + +const PRESET_COLORS = [ + 0x1ABC9C, 0x2ECC71, 0x3498DB, 0x9B59B6, 0xE91E63, + 0xF1C40F, 0xE67E22, 0xE74C3C, 0x95A5A6, 0x607D8B, + 0x11806A, 0x1F8B4C, 0x206694, 0x71368A, 0xAD1457, + 0xC27C0E, 0xA84300, 0x992D22, 0x979C9F, 0x546E7A, +]; + +function intToHex(color: number): string { + return "#" + color.toString(16).padStart(6, "0"); +} + +export default function ColorPickerTab() { + const [basicColor, setBasicColor] = useState(DEFAULT_COLOR); + const [suggestedColor, setSuggestedColor] = useState(0xE74C3C); + const [swatchColor, setSwatchColor] = useState(0x3498DB); + const [swatchSelected, setSwatchSelected] = useState(0x2ECC71); + const [customSwatchColor, setCustomSwatchColor] = useState(0x9B59B6); + const [defaultBtnColor, setDefaultBtnColor] = useState(DEFAULT_COLOR); + const [customBtnColor, setCustomBtnColor] = useState(0xE91E63); + + return ( +
+ + + Click the swatch to open a full color picker popout. Selected: {intToHex(basicColor)} + + + + + + + Includes the eye dropper tool for picking colors from screen. + + + + + + + Popout shows suggested color swatches. Selected: {intToHex(suggestedColor)} + + + + + + { }} + disabled + /> + + + + + Individual color swatch buttons. Click to select. + +
+ {PRESET_COLORS.slice(0, 10).map(color => ( + setSwatchSelected(color)} + /> + ))} +
+
+ + + + Different swatch states: default, custom, selected, disabled, no color. + +
+
+ + Normal +
+
+ + Selected +
+
+ + Default +
+
+ + Custom +
+
+ + Disabled +
+
+ + No Color +
+
+
+ + + + Gradient swatches with customizable angle. + +
+ + + +
+
+ + + + The "default" button used in ColorPickerWithSwatches. Shows checkmark when selected. + +
+ + Selected: {defaultBtnColor === DEFAULT_COLOR ? "Yes" : "No"} +
+
+ + + + The "custom" button for custom color selection. Shows dropper icon. + + + + + + + Full swatch grid with preset colors. Selected: {intToHex(swatchColor)} + + null} + renderCustomButton={() => null} + /> + + + + + Full picker with default reset button and custom color picker. Selected: {intToHex(customSwatchColor)} + + ( + + )} + renderCustomButton={props => ( + + )} + /> + + + + + ColorPicker - Full color picker with popout + + • color: number - Current color as integer + • onChange: (color: number) ={">"} void - Change callback + • onClose?: () ={">"} void - Called when picker closes + • suggestedColors?: string[] - Hex color strings for suggestions + • disabled?: boolean - Disable interaction + • label?: ReactNode - Optional label next to swatch + • colorPickerMiddle?: ReactNode - Content in middle of popout + • colorPickerFooter?: ReactNode - Content in footer of popout + • showEyeDropper?: boolean - Show eye dropper tool + + + ColorSwatch - Individual color swatch button + + • color?: number - Color value (null for "no color") + • isDefault?: boolean - Larger "default" style + • isCustom?: boolean - Shows dropper icon + • isSelected?: boolean - Shows checkmark + • disabled?: boolean - Disable interaction + • onClick?: (color) ={">"} void - Click callback + • isGradient?: boolean - Gradient mode + • gradientStart/End?: number - Gradient colors + • gradientDegrees?: number - Gradient angle (default 180) + + + ColorPickerWithSwatches - Swatch grid with render props + + • defaultColor: number - Default/reset color + • customColor?: number - Current custom color + • colors: number[] - Array of preset colors + • value: number - Selected color + • onChange: (color: number) ={">"} void - Change callback + • renderDefaultButton?: (props) ={">"} ReactNode + • renderCustomButton?: (props) ={">"} ReactNode + • isGradient?: boolean - Gradient mode + • gradientDegrees?: number - Gradient angle + • allowBlackCustomColor?: boolean - Allow #000000 + + + Color Conversion + + • Hex to int: parseInt(hex.replace("#", ""), 16) + • Int to hex: "#" + color.toString(16).padStart(6, "0") + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx new file mode 100644 index 000000000..9ee345d98 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx @@ -0,0 +1,140 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaButton, ManaCombobox } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const SAMPLE_ITEMS = [ + { id: "1", label: "Apple" }, + { id: "2", label: "Banana" }, + { id: "3", label: "Cherry" }, + { id: "4", label: "Date" }, + { id: "5", label: "Elderberry" }, + { id: "6", label: "Fig" }, + { id: "7", label: "Grape" }, + { id: "8", label: "Honeydew" }, +]; + +function ComboboxItem({ item, isSelected, onClick }: { item: { id: string; label: string; }; isSelected: boolean; onClick: () => void; }) { + return ( +
(e.currentTarget.style.background = "var(--background-modifier-hover)")} + onMouseLeave={e => (e.currentTarget.style.background = isSelected ? "var(--background-modifier-selected)" : "transparent")} + > + {item.label} +
+ ); +} + +export default function ComboboxTab() { + const [singleValue, setSingleValue] = useState(""); + const [multiValue, setMultiValue] = useState([]); + const [showDemo, setShowDemo] = useState(false); + + const filterItems = (query: string) => + SAMPLE_ITEMS.filter(item => + item.label.toLowerCase().includes(query.toLowerCase()) + ); + + return ( +
+ + + Searchable dropdown with render props pattern. Children is a function + that receives the search query and returns filterable items. + + setShowDemo(!showDemo)} + /> + + + {showDemo && ( + <> + + + Selected: {singleValue || "None"} + +
+ setSingleValue(v as string)} + aria-label="Select a fruit" + maxVisibleItems={5} + emptyStateHeader="No results" + emptyStateText="Try a different search term" + > + {query => filterItems(query).map(item => ( + setSingleValue(item.id)} + /> + ))} + +
+
+ + + + Selected: {multiValue.length ? multiValue.join(", ") : "None"} + +
+ setMultiValue(v as string[])} + multiSelect + aria-label="Select fruits" + maxVisibleItems={5} + > + {query => filterItems(query).map(item => ( + { + if (multiValue.includes(item.id)) { + setMultiValue(multiValue.filter(v => v !== item.id)); + } else { + setMultiValue([...multiValue, item.id]); + } + }} + /> + ))} + +
+
+ + )} + + + • placeholder - Input placeholder text + • value - Selected value(s) + • onChange - Selection change callback + • multiSelect - Enable multiple selection + • autoFocus - Focus input on mount + • maxVisibleItems - Max items before scroll (default 5) + • emptyStateHeader - Header when no results + • emptyStateText - Text when no results + • children - Function: (query: string) => ReactNode[] + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx b/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx new file mode 100644 index 000000000..2f5bcab5f --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx @@ -0,0 +1,140 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Heading } from "@components/Heading"; +import { Paragraph } from "@components/Paragraph"; +import { openModal } from "@utils/modal"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { useState } from "@webpack/common"; + +import { ManaButton } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const LayerModal = findComponentByCodeLazy('"data-mana-component":"layer-modal"') as React.ComponentType<{ + transitionState: number; + animationVariant?: "default" | string; + returnRef?: React.Ref; + "aria-label"?: string; + onClose: () => void; + children: React.ReactNode; +}>; + +const TransitionState = findByPropsLazy("ENTERING", "ENTERED", "EXITING") as { + ENTERING: number; + ENTERED: number; + EXITING: number; + EXITED: number; + HIDDEN: number; +}; + +function LayerModalDemo({ onClose }: { onClose: () => void; }) { + return ( + +
+ Layer Modal + + This is a layer modal. It's used for layered/stacked modals + with different animation behavior than regular modals. + + +
+
+ ); +} + +export default function LayerModalTab() { + const [showInline, setShowInline] = useState(false); + + return ( +
+ + + Layer modals are used for stacked/layered modal experiences. + They have different animation variants and are typically used + when you need to show a modal on top of another modal. + +
+ openModal(props => ( + + ))} + /> +
+
+ + + + Toggle to see the layer modal rendered inline (not recommended for production). + +
+ setShowInline(!showInline)} + /> +
+ {showInline && ( +
+ setShowInline(false)} + aria-label="Inline Demo" + > +
+ Inline layer modal content +
+
+
+ )} +
+ + + + LayerModal + + + • transitionState: number - Use TransitionState.ENTERED for visible + + + • onClose: () => void - Close handler + + + • children: ReactNode - Modal content + + + • animationVariant?: "default" | string - Animation style + + + • returnRef?: React.Ref - Ref for the modal element + + + • aria-label?: string - Accessibility label + + + TransitionState - findByPropsLazy("ENTERING", "ENTERED", "EXITING") + + + • ENTERING, ENTERED, EXITING, EXITED, HIDDEN + + + Usage Notes + + + • Use with openModal() for proper backdrop and positioning + + + • Has built-in animation handling based on transitionState + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx new file mode 100644 index 000000000..d6e175cd9 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx @@ -0,0 +1,146 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ListboxItem, ManaListbox } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const SAMPLE_ITEMS: ListboxItem[] = [ + { id: "1", label: "Apple" }, + { id: "2", label: "Banana" }, + { id: "3", label: "Cherry" }, + { id: "4", label: "Date" }, + { id: "5", label: "Elderberry" }, +]; + +const ITEMS_WITH_DISABLED: ListboxItem[] = [ + { id: "1", label: "Available option" }, + { id: "2", label: "Disabled option", disabled: true }, + { id: "3", label: "Another available" }, + { id: "4", label: "Also disabled", disabled: true }, + { id: "5", label: "Last option" }, +]; + +const MANY_ITEMS: ListboxItem[] = Array.from({ length: 10 }, (_, i) => ({ + id: String(i + 1), + label: `Item ${i + 1}`, +})); + +export default function ListboxTab() { + const [singleSelected, setSingleSelected] = useState([SAMPLE_ITEMS[0]]); + const [multiSelected, setMultiSelected] = useState([SAMPLE_ITEMS[0], SAMPLE_ITEMS[2]]); + const [disabledSelected, setDisabledSelected] = useState([]); + const [manySelected, setManySelected] = useState([]); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Listbox + + + • items: ListboxItem[] - Array of items + + + • selectedItems?: ListboxItem[] - Currently selected items + + + • onSelectionChange?: (items: ListboxItem[]) => void - Selection callback + + + • selectionMode?: "single" | "multiple" - Selection mode + + + • maxVisibleItems?: number - Max visible before scroll + + + • disabled?: boolean - Disable entire listbox + + + • loading?: boolean - Show loading state + + + • typeahead?: boolean - Enable keyboard search + + + • shouldFocusWrap?: boolean - Wrap focus at ends + + + ListboxItem + + + • id: string - Unique item ID + + + • label: string - Display label + + + • disabled?: boolean - Disable individual item + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx b/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx new file mode 100644 index 000000000..fc7795ba5 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx @@ -0,0 +1,161 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Heading } from "@components/Heading"; +import { Paragraph } from "@components/Paragraph"; +import { classNameFactory } from "@utils/css"; +import { + ModalContent, + ModalFooter, + ModalHeader, + ModalRoot, + ModalSize, + openModal +} from "@utils/modal"; +import { findComponentByCodeLazy } from "@webpack"; + +import { ManaButton } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const cl = classNameFactory("vc-compfinder-modal-"); + +const ModalCloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL") as React.ComponentType<{ + onClick: () => void; + className?: string; + withCircleBackground?: boolean; + hideOnFullscreen?: boolean; +}>; + +const SIZES = [ + { size: ModalSize.SMALL, label: "Small" }, + { size: ModalSize.MEDIUM, label: "Medium" }, + { size: ModalSize.LARGE, label: "Large" }, + { size: ModalSize.DYNAMIC, label: "Dynamic" }, +] as const; + +function openDemoModal(size: ModalSize, label: string, showCloseButton = true, showFooter = true) { + openModal(props => ( + + +
+ + {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 && ( + + + + + )} +
+ )); +} + +export default function ModalTab() { + return ( +
+ + + Uses ModalRoot, ModalHeader, ModalContent, ModalFooter from @utils/modal + and native ModalCloseButton via findComponentByCodeLazy("CLOSE_BUTTON_LABEL") + +
+ {SIZES.map(({ size, label }) => ( + openDemoModal(size, label)} + /> + ))} +
+
+ + +
+ openDemoModal(ModalSize.SMALL, "No Close Button", false, true)} + /> +
+
+ + +
+ openDemoModal(ModalSize.SMALL, "No Footer", true, false)} + /> +
+
+ + +
+ openDemoModal(ModalSize.DYNAMIC, "Minimal", false, false)} + /> +
+
+ + + + 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 + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx b/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx new file mode 100644 index 000000000..ae5778324 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx @@ -0,0 +1,132 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Button } from "@components/Button"; +import { Paragraph } from "@components/Paragraph"; + +import { Notice, NoticeType } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const NOTICE_TYPES: NoticeType[] = ["info", "warn", "danger", "positive", "preview"]; + +export default function NoticeTab() { + return ( +
+ + + Notice component for displaying informational messages with different severity levels. + +
+ {NOTICE_TYPES.map(type => ( +
+ + This is a {type} notice message. + + + {type} + +
+ ))} +
+
+ + + + Notices can include action buttons for user interaction. + +
+ Action} + > + Notice with an action button. + + Fix Now} + > + Warning that requires attention. + +
+
+ + + + The hidden prop controls visibility without removing from DOM. + +
+
+ + + hidden=false + +
+
+ + + hidden=true + +
+
+
+ + + + Custom text styling via the textVariant prop. + +
+
+ + Default text variant. + + + text-sm/medium (default) + +
+
+ + Larger text variant. + + + text-md/normal + +
+
+
+ + + + • children: ReactNode - Notice content + + + • messageType: "info" | "warn" | "danger" | "positive" | "preview" + + + • action?: ReactNode - Action button or element + + + • hidden?: boolean - Hide the notice + + + • textColor?: string - Custom text color + + + • textVariant?: string - Text size/weight variant + + + • icon?: ComponentType - Custom icon component + + + • className?: string - Additional CSS class + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx b/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx new file mode 100644 index 000000000..58fa5ecac --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx @@ -0,0 +1,125 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { Paginator2 as Paginator } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function PaginatorTab() { + const [page1, setPage1] = useState(1); + const [page2, setPage2] = useState(1); + const [page3, setPage3] = useState(1); + const [page4, setPage4] = useState(1); + + return ( +
+ + + Pagination component for navigating through pages of content. + + + + + + + Control how many page numbers are visible at once. + +
+
+ + + 3 visible + +
+
+ + + 7 visible + +
+
+
+ + + + Additional configuration options for the paginator. + +
+
+ + + hideMaxPage + +
+
+ + + disablePaginationGap + +
+
+
+ + + + • currentPage: number - Current active page + + + • totalCount: number - Total number of items + + + • pageSize: number - Items per page + + + • onPageChange?: (page: number) => void - Page change callback + + + • maxVisiblePages?: number - Max page numbers shown + + + • hideMaxPage?: boolean - Hide last page number + + + • disablePaginationGap?: boolean - Remove ellipsis gaps + + + • className?: string - Additional CSS class + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx b/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx new file mode 100644 index 000000000..ed5271da8 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx @@ -0,0 +1,153 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useRef, useState } from "@webpack/common"; + +import { ManaButton, ManaPopover, PopoverAction } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +function PopoverDemo({ title, body, size, actions, position, label }: { title: string; body: string; size?: "sm" | "md" | "lg"; actions?: PopoverAction[]; position?: "top" | "bottom" | "left" | "right"; label: string; }) { + const wrapperRef = useRef(null); + const [show, setShow] = useState(false); + + return ( + <> +
+ setShow(!show)} + /> +
+ setShow(false)} + title={title} + body={body} + size={size} + actions={actions} + position={position} + /> + + ); +} + +export default function PopoverTab() { + return ( +
+ +
+ +
+
+ + +
+ + + +
+
+ + +
+ + + + +
+
+ + +
+ +
+
+ + + + • targetElementRef: React.RefObject - Ref to trigger element + + + • shouldShow: boolean - Controls visibility + + + • onRequestClose?: (reason: string) => void - Close callback + + + • title?: string - Popover title + + + • body?: string - Popover body text + + + • size?: "sm" | "md" | "lg" - Popover size + + + • position?: "top" | "bottom" | "left" | "right" - Position + + + • actions?: PopoverAction[] - Action buttons + + + • textLink?: {"{"} text: string, onClick?: () => void {"}"} - Link + + + • graphic?: {"{"} src: string, aspectRatio?: string {"}"} - Image + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx new file mode 100644 index 000000000..90b28593d --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx @@ -0,0 +1,119 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useEffect, useState } from "@webpack/common"; + +import { ProgressBar } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function ProgressBarTab() { + const [animatedProgress, setAnimatedProgress] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setAnimatedProgress(prev => (prev >= 100 ? 0 : prev + 5)); + }, 200); + return () => clearInterval(interval); + }, []); + + return ( +
+ + + Blue (default) and Orange variants. + +
+
+ Blue: + +
+
+ Orange: + +
+
+
+ + +
+
+ 0%: + +
+
+ 25%: + +
+
+ 50%: + +
+
+ 75%: + +
+
+ 100%: + +
+
+
+ + + + Progress: {animatedProgress}% + + + + + + + Using minimum=0 and maximum=200, progress=150 (75% filled). + + + + + + + Using variant="unset" with custom gradient colors. + + + + + + + • progress: number - Current progress value + + + • minimum?: number - Minimum value (default: 0) + + + • maximum?: number - Maximum value (default: 100) + + + • variant?: "blue" | "orange" | "unset" - Color variant + + + • override?: object - Custom colors when variant="unset" + + + • labelledBy?: string - ID of labelling element for a11y + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx b/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx new file mode 100644 index 000000000..1d85bb22e --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx @@ -0,0 +1,193 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaBaseRadioGroup, RadioOption, StandaloneRadioIndicator } from ".."; +import { EquicordIcon } from "../icons/EquicordIcon"; +import { SectionWrapper } from "../SectionWrapper"; + +const BASIC_OPTIONS: RadioOption[] = [ + { value: "option1", name: "Option 1" }, + { value: "option2", name: "Option 2" }, + { value: "option3", name: "Option 3" }, +]; + +const OPTIONS_WITH_DESCRIPTIONS: RadioOption[] = [ + { value: "low", name: "Low Quality", desc: "Faster loading, uses less data" }, + { value: "medium", name: "Medium Quality", desc: "Balanced quality and performance" }, + { value: "high", name: "High Quality", desc: "Best quality, uses more data" }, +]; + +const OPTIONS_WITH_ICONS: RadioOption[] = [ + { value: "item1", name: "Item with icon", desc: "This option has an icon", leadingIcon: EquicordIcon }, + { value: "item2", name: "Another with icon", leadingIcon: EquicordIcon }, + { value: "item3", name: "Third with icon", leadingIcon: EquicordIcon }, +]; + +const OPTIONS_MIXED: RadioOption[] = [ + { value: "enabled", name: "Enabled option" }, + { value: "disabled", name: "Disabled option", disabled: true }, + { value: "another", name: "Another enabled" }, +]; + +const NUMBERED_OPTIONS: RadioOption[] = [ + { value: 1, name: "First choice" }, + { value: 2, name: "Second choice" }, + { value: 3, name: "Third choice" }, +]; + +export default function RadioGroupTab() { + const [basicValue, setBasicValue] = useState("option1"); + const [descValue, setDescValue] = useState("medium"); + const [iconValue, setIconValue] = useState("item1"); + const [mixedValue, setMixedValue] = useState("enabled"); + const [numberedValue, setNumberedValue] = useState(1); + const [unselectedValue, setUnselectedValue] = useState(undefined); + const [standaloneSelected, setStandaloneSelected] = useState(false); + + return ( +
+ + + Simple radio group with text-only options. + + + + + + + Radio options with descriptive text using the desc prop. + + + + + + + Radio options with icons using leadingIcon prop. + + + + + + + Individual options can be disabled with disabled: true. + + + + + + + Radio options can use numeric values. + + setNumberedValue(v as number)} + /> + + + + + Radio group with no initial selection (value undefined). + + + + + + + The entire group can be disabled with disabled prop. + + + + + + + A standalone animated radio indicator component. Click to toggle. + +
setStandaloneSelected(!standaloneSelected)} + style={{ cursor: "pointer", display: "inline-block" }} + > + +
+
+ + + + BaseRadioGroup + + + • options: RadioOption[] - Array of radio options + + + • value?: string | number - Currently selected value + + + • onChange?: (value: string | number) => void - Called when selection changes + + + • disabled?: boolean - Disable entire group + + + • aria-labelledby?: string - Accessibility label reference + + + RadioOption + + + • value: string | number - Option value + + + • name: string - Option label text + + + • desc?: string - Optional description text + + + • disabled?: boolean - Disable individual option + + + • leadingIcon?: React.ComponentType - Icon before radio + + + StandaloneRadioIndicator + + + • isSelected: boolean - Whether the indicator is selected + + + • disabled?: boolean - Disable the indicator + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx b/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx new file mode 100644 index 000000000..74ea02343 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx @@ -0,0 +1,180 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; + +import { ManaButton, ManaRichTooltip } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function RichTooltipTab() { + return ( +
+ + + Rich tooltip with title and body text. + +
+ + + +
+
+ + + + Rich tooltip without title. + +
+ + + +
+
+ + +
+ + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + +
+
+ + +
+ + + + + + +
+
+ + +
+ + + + + + +
+
+ + + + • title?: string - Tooltip title (bold) + + + • body?: string - Tooltip body text + + + • children: ReactNode - Trigger element + + + • position?: "top" | "bottom" | "left" | "right" - Position + + + • align?: "start" | "center" | "end" - Alignment + + + • spacing?: number - Distance from trigger (px) + + + • asContainer?: boolean - Wrap children in container + + + • element?: "span" | "div" - Container element type + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx new file mode 100644 index 000000000..48287ee13 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx @@ -0,0 +1,81 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { SearchBar } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function SearchBarTab() { + const [query1, setQuery1] = useState(""); + const [query2, setQuery2] = useState(""); + const [query3, setQuery3] = useState("prefilled"); + + return ( +
+ + + Default search bar with clear button. + + setQuery1("")} + placeholder="Search..." + /> + + + + + Available sizes: sm, md (default). + +
+ setQuery2("")} + placeholder="Small size" + size="sm" + /> + setQuery2("")} + placeholder="Medium size" + size="md" + /> +
+
+ + + + Disabled and prefilled states. + +
+ { }} + placeholder="Disabled" + disabled + /> + setQuery3("")} + placeholder="Prefilled" + /> +
+
+ + + + query, onChange, onClear?, placeholder?, size?, autoFocus?, disabled?, onKeyDown?, onBlur?, onFocus?, autoComplete?, inputProps?, aria-label? + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx b/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx new file mode 100644 index 000000000..7b4ec098b --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx @@ -0,0 +1,123 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaSelect, ManaSelectOption } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const SAMPLE_OPTIONS: ManaSelectOption[] = [ + { id: "1", value: "option1", label: "Option 1" }, + { id: "2", value: "option2", label: "Option 2" }, + { id: "3", value: "option3", label: "Option 3" }, + { id: "4", value: "option4", label: "Option 4" }, +]; + +export default function SelectTab() { + const [singleValue, setSingleValue] = useState("option1"); + const [multiValue, setMultiValue] = useState(["option1", "option2"]); + const [clearableValue, setClearableValue] = useState("option1"); + const [placeholderValue, setPlaceholderValue] = useState(""); + + return ( +
+ + setSingleValue(v as string)} + selectionMode="single" + /> + + + + setMultiValue(v as string[])} + selectionMode="multiple" + /> + + + + setPlaceholderValue(v as string)} + placeholder="Select an option..." + /> + + + + setClearableValue(v as string)} + clearable + /> + + + + + + + + + + + + + ManaSelect - Note: Uses onSelectionChange, NOT onChange. + + + • options: ManaSelectOption[] - Array of options + + + • value?: string | string[] | null - Selected value(s) + + + • onSelectionChange?: (value) => void - Called when selection changes + + + • selectionMode?: "single" | "multiple" - Selection mode + + + • placeholder?: string - Placeholder text + + + • clearable?: boolean - Allow clearing selection + + + • fullWidth?: boolean - Expand to full width + + + • disabled?: boolean - Disable select + + + ManaSelectOption + + + • id: string - Unique option ID + + + • value: string - Option value + + + • label: string - Display label + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx b/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx new file mode 100644 index 000000000..0ccfea90e --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx @@ -0,0 +1,110 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { Slider } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function SliderTab() { + const [basicValue, setBasicValue] = useState(50); + const [markerValue, setMarkerValue] = useState(50); + const [miniValue, setMiniValue] = useState(30); + + return ( +
+ + + Value: {basicValue} + + + + + + + Value: {markerValue} + + `${v}%`} + /> + + + + + Value snaps to markers. + + `${v}%`} + /> + + + + + Value: {miniValue} + +
+ +
+
+ + + + + + + + Range: -50 to 50 + + v.toString()} + /> + + + + • initialValue / value - Current value + • minValue / maxValue - Range bounds + • onValueChange - Change callback + • markers - Array of marker values + • stickToMarkers - Snap to markers + • onMarkerRender - Custom marker labels + • mini - Compact style + • hideBubble - Hide value tooltip + • disabled - Disable interaction + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx b/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx new file mode 100644 index 000000000..d2312488f --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx @@ -0,0 +1,95 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaButton, Spinner } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const SPINNER_TYPES = [ + "wanderingCubes", + "chasingDots", + "pulsingEllipsis", + "spinningCircle", + "spinningCircleSimple", + "lowMotion" +] as const; + +export default function SpinnerTab() { + const [animated, setAnimated] = useState(true); + + return ( +
+ + + All available spinner variants. + +
+ {SPINNER_TYPES.map(type => ( +
+ + + {type} + +
+ ))} +
+
+ + + + Spinners can be paused with animated=false. + +
+ setAnimated(!animated)} + /> +
+
+
+ + + animated={animated.toString()} + +
+
+
+ + + + lowMotion type is used when user prefers reduced motion. + The component automatically switches to this when reducedMotion is enabled. + +
+
+ +
+
+
+ + + + • type?: "wanderingCubes" | "chasingDots" | "pulsingEllipsis" | "spinningCircle" | "spinningCircleSimple" | "lowMotion" + + + • animated?: boolean - Enable/disable animation (default: true) + + + • className?: string - Container class + + + • itemClassName?: string - Inner item class + + + • aria-label?: string - Accessibility label + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx b/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx new file mode 100644 index 000000000..fd495e786 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx @@ -0,0 +1,131 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { findComponentByCodeLazy } from "@webpack"; +import { useState } from "@webpack/common"; + +import { ManaSwitch } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +interface SwitchWithLabelProps { + checked: boolean; + onChange?: (checked: boolean) => void; + disabled?: boolean; + label?: string; + description?: string; +} + +const SwitchWithLabel = findComponentByCodeLazy('"data-toggleable-component":"switch"') as React.ComponentType; + +export default function SwitchTab() { + const [basicOn, setBasicOn] = useState(true); + const [basicOff, setBasicOff] = useState(false); + + const [labelOn, setLabelOn] = useState(true); + const [labelOff, setLabelOff] = useState(false); + + const [withDesc, setWithDesc] = useState(true); + + return ( +
+ +
+
+ On: + +
+
+ Off: + +
+
+ Disabled On: + +
+
+ Disabled Off: + +
+
+
+ + +
+ + +
+
+ + +
+ +
+
+ + +
+ + +
+
+ + + + ManaSwitch - Basic toggle switch without label. + + + • checked: boolean - Current switch state + + + • onChange?: (checked: boolean) => void - Called when toggled + + + • disabled?: boolean - Disable interaction + + + SwitchWithLabel - Switch with label and description. + + + • label?: string - Label text displayed next to switch + + + • description?: string - Description text below label + + + • checked: boolean - Current switch state + + + • onChange?: (checked: boolean) => void - Called when toggled + + + • disabled?: boolean - Disable interaction + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx new file mode 100644 index 000000000..72e3bd856 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx @@ -0,0 +1,191 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { TabBar } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function TabBarTab() { + const [sideTab, setSideTab] = useState("tab1"); + const [topTab, setTopTab] = useState("tab1"); + const [topPillTab, setTopPillTab] = useState("tab1"); + const [brandTab, setBrandTab] = useState("tab1"); + const [colorTab, setColorTab] = useState("tab1"); + + return ( +
+ + + Default side-style tab bar with grey look. Selected: {sideTab} + + + First Tab + Second Tab + Third Tab + + + + + + Top-style tab bar with underline indicator. Selected: {topTab} + + + Overview + Settings + Members + + + + + + Pill-style tabs for top navigation. Selected: {topPillTab} + + + All + Online + Pending + Blocked + + + + + + Brand-colored tab bar style. Selected: {brandTab} + + + Home + Explore + Library + + + + + + Tab bar with header section and separator. + + + User Settings + My Account + Privacy + + App Settings + Appearance + Notifications + + + + + + Some tabs can be disabled. + + + Active + Disabled + Another Active + + + + + + Individual tabs can have custom colors. Selected: {colorTab} + + + Green + Yellow + Red + + + + + + Tab with destructive/danger styling. + + + Settings + Privacy + Log Out + + + + + + TabBar - Container component + + • type?: "side" | "top" | "top-pill" - Tab bar style + • look?: "brand" | "grey" - Color scheme + • selectedItem?: string - Currently selected tab ID + • onItemSelect?: (id) ={">"} void - Selection callback + • orientation?: "horizontal" | "vertical" - Layout direction + • className?: string - Additional CSS classes + • style?: CSSProperties - Inline styles + • aria-label?: string - Accessibility label + + + TabBar.Item - Individual tab + + • id: string - Unique tab identifier (required) + • disabled?: boolean - Disable the tab + • color?: string - Custom tab color + • variant?: "destructive" - Danger styling + • disableItemStyles?: boolean - Remove default styles + • onClick?: MouseEventHandler - Custom click handler + + + TabBar.Header - Section header + + • children: ReactNode - Header text + • onClick?: MouseEventHandler - Click handler + + + TabBar.Separator - Visual divider + + • style?: CSSProperties - Custom styles + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx b/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx new file mode 100644 index 000000000..8527658a4 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx @@ -0,0 +1,80 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaTextArea } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function TextAreaTab() { + const [basic, setBasic] = useState("Hello world\nThis is a textarea"); + const [withPlaceholder, setWithPlaceholder] = useState(""); + const [autosize, setAutosize] = useState("This textarea will grow as you type more content..."); + const [fixedRows, setFixedRows] = useState("Fixed 5 rows"); + const [charCount, setCharCount] = useState("Count characters"); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + • value: string - Textarea value + + + • onChange?: (value: string) => void - Called on input change + + + • placeholder?: string - Placeholder text + + + • rows?: number - Fixed number of rows + + + • autosize?: boolean - Auto-grow with content + + + • showCharacterCount?: boolean - Display character count + + + • maxLength?: number - Maximum character limit + + + • disabled?: boolean - Disable textarea + + + • readOnly?: boolean - Make textarea read-only + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx b/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx new file mode 100644 index 000000000..0434ce029 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx @@ -0,0 +1,76 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; + +import { ManaTextButton } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const VARIANTS = ["primary", "secondary", "always-white", "critical"] as const; + +const TEXT_VARIANTS = [ + "text-xs/normal", + "text-sm/normal", + "text-sm/medium", + "text-md/normal", + "text-md/medium", + "text-lg/medium", +] as const; + +export default function TextButtonTab() { + return ( +
+ +
+ {VARIANTS.map(variant => ( + + ))} +
+
+ + +
+ {TEXT_VARIANTS.map(textVariant => ( + + ))} +
+
+ + +
+ + +
+
+ + +
+ +
+
+ + + • text - Button label text + • variant - primary, secondary, always-white, critical + • textVariant - Typography variant (e.g., text-sm/medium) + • lineClamp - Max lines before truncation + • disabled - Disable button + • onClick - Click handler + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TextInputTab.tsx b/src/equicordplugins/components.dev/components/tabs/TextInputTab.tsx new file mode 100644 index 000000000..e5d115645 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/TextInputTab.tsx @@ -0,0 +1,92 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; +import { useState } from "@webpack/common"; + +import { ManaTextInput } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function TextInputTab() { + const [basic, setBasic] = useState("Hello world"); + const [sizeSm, setSizeSm] = useState("Small input"); + const [sizeMd, setSizeMd] = useState("Medium input"); + const [withPlaceholder, setWithPlaceholder] = useState(""); + const [clearable, setClearable] = useState("Clear me"); + const [charCount, setCharCount] = useState("Count chars"); + const [error, setError] = useState("Invalid input"); + + return ( +
+ + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + • value: string - Input value + + + • onChange?: (value: string) => void - Called on input change + + + • placeholder?: string - Placeholder text + + + • size?: "sm" | "md" - Input size (default: "md") + + + • clearable?: boolean - Show clear button + + + • showCharacterCount?: boolean - Display character count + + + • maxLength?: number - Maximum character limit + + + • error?: boolean - Show error state styling + + + • disabled?: boolean - Disable input + + + • readOnly?: boolean - Make input read-only + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx b/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx new file mode 100644 index 000000000..961b753a4 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx @@ -0,0 +1,99 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; + +import { ManaButton, ManaTooltip } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +const POSITIONS = ["top", "bottom", "left", "right"] as const; +const COLORS = ["primary", "black", "grey", "brand", "green", "yellow", "red"] as const; + +export default function TooltipTab() { + return ( +
+ +
+ {POSITIONS.map(position => ( + + {props => ( + + )} + + ))} +
+
+ + +
+ {COLORS.map(color => ( + + {props => ( + + )} + + ))} +
+
+ + +
+ + {props => ( + + )} + + + {props => ( + + )} + + + {props => ( + + )} + +
+
+ + +
+ + {props => ( + + )} + + + {props => ( + + )} + +
+
+ + + + • text: string - Tooltip content + + + • children: (props) => ReactNode - Render function for trigger element + + + • position?: "top" | "bottom" | "left" | "right" - Tooltip position + + + • color?: "primary" | "black" | "grey" | "brand" | "green" | "yellow" | "red" + + + • delay?: number - Show delay in milliseconds + + + • hideOnClick?: boolean - Hide tooltip when trigger is clicked + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx b/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx new file mode 100644 index 000000000..f13cddd9e --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx @@ -0,0 +1,188 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Paragraph } from "@components/Paragraph"; + +import { SectionWrapper } from "../SectionWrapper"; + +const TEXT_COLORS = [ + "text-default", + "text-muted", + "text-subtle", + "text-strong", + "text-normal", + "text-link", + "text-brand", + "text-disabled", + "text-invert", + "text-feedback-critical", + "text-feedback-warning", + "text-feedback-info", + "text-feedback-positive", + "text-status-online", + "text-status-idle", + "text-status-dnd", + "text-status-offline", + "text-overlay-dark", + "text-overlay-light", +] as const; + +const TEXT_SIZES = [ + { name: "xxs", value: "0.625rem (10px)" }, + { name: "xs", value: "0.75rem (12px)" }, + { name: "sm", value: "0.875rem (14px)" }, + { name: "md", value: "1rem (16px)" }, + { name: "lg", value: "1.25rem (20px)" }, + { name: "xl", value: "1.5rem (24px)" }, + { name: "xxl", value: "2rem (32px)" }, +] as const; + +const TEXT_WEIGHTS = [ + { name: "thin", value: "100" }, + { name: "extralight", value: "200" }, + { name: "light", value: "300" }, + { name: "normal", value: "400" }, + { name: "medium", value: "500" }, + { name: "semibold", value: "600" }, + { name: "bold", value: "700" }, + { name: "extrabold", value: "800" }, +] as const; + +const FONT_SIZE_MAP: Record = { + 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 => ( +
+ + {color} + + + var(--{color}) + +
+ ))} +
+
+ + +
+ {TEXT_SIZES.map(({ name, value }) => ( +
+ + {name} + + + {value} + +
+ ))} +
+
+ + +
+ {TEXT_WEIGHTS.map(({ name, value }) => ( +
+ + {name} + + + font-weight: {value} + +
+ ))} +
+
+ + +
+ + {"import { BaseText } from \"@components/BaseText\";"} + + + {"Text"} + +
+
+ + +
+ {[ + "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 => ( +
+ + {color} + + + var(--{color}) + +
+ ))} +
+
+ + + + BaseText - from @components/BaseText + + + • size?: "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl" + + + • weight?: "thin" | "extralight" | "light" | "normal" | "medium" | "semibold" | "bold" | "extrabold" + + + • color?: string - CSS variable name (e.g., "text-muted") + + + • tag?: "span" | "p" | "div" | "h1" - etc. + + + Heading - from @components/Heading + + + • tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" (default: h5) + + + Paragraph - from @components/Paragraph + + + • color?: string - CSS variable name + + + Span - from @components/Span + + + • Inline text with default styling + + +
+ ); +} diff --git a/src/equicordplugins/components.dev/index.tsx b/src/equicordplugins/components.dev/index.tsx new file mode 100644 index 000000000..44000b166 --- /dev/null +++ b/src/equicordplugins/components.dev/index.tsx @@ -0,0 +1,74 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import SettingsPlugin from "@plugins/_core/settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { StartAt } from "@utils/types"; +import { openUserSettingsPanel } from "@webpack/common"; + +import ComponentsTab from "./components/ComponentsTab"; + +function ComponentsIcon(props: React.SVGProps) { + return ( + + + + + ); +} + +export default definePlugin({ + name: "Components", + description: "Adds a new tab to settings to browse Discord components.", + authors: [EquicordDevs.Prism], + dependencies: ["Settings"], + startAt: StartAt.WebpackReady, + toolboxActions: { + "Open Components Tab"() { + openUserSettingsPanel("equicord_components"); + }, + }, + start() { + const { customEntries, customSections } = SettingsPlugin; + + customEntries.push({ + key: "equicord_components", + title: "Components", + Component: ComponentsTab, + Icon: ComponentsIcon + }); + + customSections.push(() => ({ + section: "EquicordDiscordComponents", + label: "Components", + element: ComponentsTab, + className: "vc-discord-components", + id: "Components" + })); + }, + stop() { + const { customEntries, customSections } = SettingsPlugin; + const entryIdx = customEntries.findIndex(e => e.key === "equicord_components"); + const sectionIdx = customSections.findIndex(s => s({} as any).id === "Components"); + if (entryIdx !== -1) customEntries.splice(entryIdx, 1); + if (sectionIdx !== -1) customSections.splice(sectionIdx, 1); + }, +}); diff --git a/src/equicordplugins/components.dev/styles.css b/src/equicordplugins/components.dev/styles.css new file mode 100644 index 000000000..fadb6a224 --- /dev/null +++ b/src/equicordplugins/components.dev/styles.css @@ -0,0 +1,89 @@ +.vc-compfinder-tabbar { + margin-bottom: 16px; + flex-wrap: wrap !important; + gap: 8px 16px; +} + +.vc-compfinder-tabbar > div { + flex-wrap: wrap !important; + gap: 8px 16px; +} + +.vc-compfinder-tab { + padding: 8px 12px; + margin: 0 !important; +} + +.vc-compfinder-content { + padding-top: 16px; +} + +.vc-compfinder-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.vc-compfinder-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.vc-compfinder-grid-vertical { + display: flex; + flex-direction: column; + gap: 8px; +} + +.vc-compfinder-switch-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--background-secondary); + border-radius: 4px; +} + +.vc-compfinder-demo-button { + padding: 8px 16px; + background: var(--background-secondary); + border: 1px solid var(--border-subtle); + border-radius: 4px; + color: var(--text-normal); + cursor: pointer; +} + +.vc-compfinder-demo-button:hover { + background: var(--background-secondary-alt); +} + +.vc-compfinder-modal-header { + display: flex; + align-items: flex-start; + position: relative; +} + +.vc-compfinder-modal-header-content { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} + +.vc-compfinder-modal-close { + position: absolute; + top: 0; + right: 0; +} + +.vc-compfinder-modal-content { + padding-bottom: var(--space-16); +} + +.vc-compfinder-modal-footer { + display: flex; + flex-direction: row-reverse; + gap: 8px; +} \ No newline at end of file diff --git a/src/equicordplugins/components.dev/types/anchor.d.ts b/src/equicordplugins/components.dev/types/anchor.d.ts new file mode 100644 index 000000000..17d29c018 --- /dev/null +++ b/src/equicordplugins/components.dev/types/anchor.d.ts @@ -0,0 +1,19 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface AnchorProps extends React.AnchorHTMLAttributes { + href?: string; + onClick?: React.MouseEventHandler; + className?: string; + children?: React.ReactNode; + rel?: string; + target?: string; + useDefaultUnderlineStyles?: boolean; + title?: string; + style?: React.CSSProperties; + focusProps?: Record; + ref?: React.Ref; +} diff --git a/src/equicordplugins/components.dev/types/avatar.d.ts b/src/equicordplugins/components.dev/types/avatar.d.ts new file mode 100644 index 000000000..da8c17e18 --- /dev/null +++ b/src/equicordplugins/components.dev/types/avatar.d.ts @@ -0,0 +1,25 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export type AvatarSize = "SIZE_16" | "SIZE_20" | "SIZE_24" | "SIZE_32" | "SIZE_40" | "SIZE_48" | "SIZE_56" | "SIZE_80" | "SIZE_120"; + +export type AvatarStatus = "online" | "idle" | "dnd" | "offline" | "streaming"; + +export interface AvatarProps { + src?: string; + size?: AvatarSize; + status?: AvatarStatus; + statusColor?: string; + isMobile?: boolean; + isTyping?: boolean; + isSpeaking?: boolean; + statusTooltip?: boolean; + statusTooltipDelay?: number; + "aria-hidden"?: boolean; + "aria-label"?: string; + imageClassName?: string; + className?: string; +} diff --git a/src/equicordplugins/components.dev/types/badge.d.ts b/src/equicordplugins/components.dev/types/badge.d.ts new file mode 100644 index 000000000..3b828d935 --- /dev/null +++ b/src/equicordplugins/components.dev/types/badge.d.ts @@ -0,0 +1,48 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface BadgeShapesType { + ROUND: string; + ROUND_LEFT: string; + ROUND_RIGHT: string; + SQUARE: string; +} + +export interface NumberBadgeProps { + count: number; + maxDigits?: number; + color?: string; + disableColor?: boolean; + shape?: string; + className?: string; + style?: React.CSSProperties; +} + +export interface TextBadgeProps { + text: string; + color?: string; + disableColor?: boolean; + shape?: string; + className?: string; + style?: React.CSSProperties; +} + +export interface IconBadgeProps { + icon: React.ComponentType<{ className?: string; color?: string; }>; + color?: string; + disableColor?: boolean; + shape?: string; + className?: string; + style?: React.CSSProperties; +} + +export interface CircleBadgeProps { + color?: string; + disableColor?: boolean; + shape?: string; + className?: string; + style?: React.CSSProperties; +} diff --git a/src/equicordplugins/components.dev/types/button.d.ts b/src/equicordplugins/components.dev/types/button.d.ts new file mode 100644 index 000000000..536bcf776 --- /dev/null +++ b/src/equicordplugins/components.dev/types/button.d.ts @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface ManaButtonProps { + text?: string; + variant?: "primary" | "secondary" | "critical-primary" | "critical-secondary" | "overlay-primary" | "overlay-secondary" | "expressive"; + size?: "xs" | "sm" | "md"; + disabled?: boolean; + loading?: boolean; + fullWidth?: boolean; + rounded?: boolean; + icon?: React.ComponentType; + iconPosition?: "start" | "end"; + onClick?: () => void; + className?: string; + style?: React.CSSProperties; +} + +export interface ManaTextButtonProps { + text: string; + variant?: "primary" | "secondary" | "always-white" | "critical"; + textVariant?: string; + lineClamp?: number; + onClick?: () => void; + disabled?: boolean; +} diff --git a/src/equicordplugins/components.dev/types/calendar.d.ts b/src/equicordplugins/components.dev/types/calendar.d.ts new file mode 100644 index 000000000..3d0300d7e --- /dev/null +++ b/src/equicordplugins/components.dev/types/calendar.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 interface ManaCalendarProps { + value?: any; + onChange?: (value: any) => void; + minValue?: any; + maxValue?: any; + disabled?: boolean; + readOnly?: boolean; + className?: string; + "aria-label"?: string; +} + +export interface ManaDatePickerProps { + value?: any; + onChange?: (value: any) => void; + minValue?: any; + maxValue?: any; + placeholderValue?: any; + granularity?: "day" | "hour" | "minute" | "second"; + hourCycle?: 12 | 24; + hideTimeZone?: boolean; + disabled?: boolean; + required?: boolean; + label?: string; + description?: string; + errorMessage?: string; +} diff --git a/src/equicordplugins/components.dev/types/checkbox.d.ts b/src/equicordplugins/components.dev/types/checkbox.d.ts new file mode 100644 index 000000000..12483e45b --- /dev/null +++ b/src/equicordplugins/components.dev/types/checkbox.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 interface ManaCheckboxProps { + checked: boolean; + onChange?: (checked: boolean) => void; + disabled?: boolean; + displayOnly?: boolean; + label?: string; + description?: string; + leadingIcon?: React.ComponentType; + labelType?: "primary" | "secondary"; + usageVariant?: "single" | "indicator"; +} + +export interface CheckboxGroupOption { + value: string | number; + label: string; + description?: string; + disabled?: boolean; + leadingIcon?: React.ComponentType; +} + +export interface ManaCheckboxGroupProps { + options: CheckboxGroupOption[]; + selectedValues: (string | number)[]; + onChange?: (values: (string | number)[]) => void; + disabled?: boolean; +} diff --git a/src/equicordplugins/components.dev/types/clickable.d.ts b/src/equicordplugins/components.dev/types/clickable.d.ts new file mode 100644 index 000000000..c6ab2b8eb --- /dev/null +++ b/src/equicordplugins/components.dev/types/clickable.d.ts @@ -0,0 +1,19 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface ClickableProps extends React.HTMLAttributes { + tag?: keyof React.JSX.IntrinsicElements; + focusProps?: Record; + innerRef?: React.Ref; + onClick?: React.MouseEventHandler; + role?: string; + tabIndex?: number; + ignoreKeyPress?: boolean; + className?: string; + children?: React.ReactNode; + href?: string; + onKeyPress?: React.KeyboardEventHandler; +} diff --git a/src/equicordplugins/components.dev/types/colorpicker.d.ts b/src/equicordplugins/components.dev/types/colorpicker.d.ts new file mode 100644 index 000000000..3fa8f5c8f --- /dev/null +++ b/src/equicordplugins/components.dev/types/colorpicker.d.ts @@ -0,0 +1,77 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface ColorPickerProps { + color: number; + onChange: (color: number) => void; + onClose?: () => void; + suggestedColors?: string[]; + disabled?: boolean; + label?: React.ReactNode; + colorPickerMiddle?: React.ReactNode; + colorPickerFooter?: React.ReactNode; + showEyeDropper?: boolean; +} + +export interface ColorSwatchProps { + color?: number; + isDefault?: boolean; + isCustom?: boolean; + isSelected?: boolean; + disabled?: boolean; + style?: React.CSSProperties; + onClick?: (colorOrGradient: number | { start: number; end: number; }) => void; + isGradient?: boolean; + "aria-label"?: string; + gradientStart?: number; + gradientEnd?: number; + gradientDegrees?: number; +} + +export interface DefaultColorButtonProps { + color: number; + onChange: (color: number) => void; + value: number; + disabled?: boolean; + allowBlackCustomColor?: boolean; +} + +export interface CustomColorButtonProps { + customColor?: number; + value: number; + disabled?: boolean; + "aria-label"?: string; + presets: number[]; +} + +export interface GradientColor { + start: number; + end: number; + name?: string; +} + +export interface ColorPickerWithSwatchesProps { + className?: string; + defaultColor: number; + customColor?: number; + colors: number[] | GradientColor[]; + value: number; + secondaryValue?: number; + disabled?: boolean; + onChange: (color: number, secondaryColor?: number) => void; + renderDefaultButton?: (props: DefaultColorButtonProps) => React.ReactNode; + renderCustomButton?: (props: CustomColorButtonProps) => React.ReactNode; + colorContainerClassName?: string; + isGradient?: boolean; + renderGradientCustomButton?: (props: { + value: number; + startColor: number; + endColor: number; + disabled?: boolean; + }) => React.ReactNode; + gradientDegrees?: number; + allowBlackCustomColor?: boolean; +} diff --git a/src/equicordplugins/components.dev/types/index.ts b/src/equicordplugins/components.dev/types/index.ts new file mode 100644 index 000000000..85033ddb5 --- /dev/null +++ b/src/equicordplugins/components.dev/types/index.ts @@ -0,0 +1,25 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export * from "./anchor"; +export * from "./avatar"; +export * from "./badge"; +export * from "./button"; +export * from "./calendar"; +export * from "./checkbox"; +export * from "./clickable"; +export * from "./colorpicker"; +export * from "./input"; +export * from "./notice"; +export * from "./paginator"; +export * from "./progress"; +export * from "./radio"; +export * from "./select"; +export * from "./slider"; +export * from "./spinner"; +export * from "./switch"; +export * from "./tabbar"; +export * from "./tooltip"; diff --git a/src/equicordplugins/components.dev/types/input.d.ts b/src/equicordplugins/components.dev/types/input.d.ts new file mode 100644 index 000000000..a55a38de2 --- /dev/null +++ b/src/equicordplugins/components.dev/types/input.d.ts @@ -0,0 +1,46 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface ManaTextInputProps { + value: string; + onChange?: (value: string) => void; + placeholder?: string; + disabled?: boolean; + readOnly?: boolean; + size?: "sm" | "md"; + error?: boolean; + clearable?: boolean; + showCharacterCount?: boolean; + maxLength?: number; +} + +export interface ManaTextAreaProps { + value: string; + onChange?: (value: string) => void; + placeholder?: string; + disabled?: boolean; + readOnly?: boolean; + rows?: number; + autosize?: boolean; + maxLength?: number; + showCharacterCount?: boolean; +} + +export interface SearchBarProps { + query: string; + onChange: (value: string) => void; + onClear?: () => void; + placeholder?: string; + size?: "sm" | "md"; + autoFocus?: boolean; + disabled?: boolean; + onKeyDown?: (e: React.KeyboardEvent) => void; + onBlur?: () => void; + onFocus?: () => void; + autoComplete?: string; + inputProps?: Record; + "aria-label"?: string; +} diff --git a/src/equicordplugins/components.dev/types/notice.d.ts b/src/equicordplugins/components.dev/types/notice.d.ts new file mode 100644 index 000000000..9ff2e5a94 --- /dev/null +++ b/src/equicordplugins/components.dev/types/notice.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 type NoticeType = "warn" | "info" | "danger" | "positive" | "preview"; + +export interface NoticeProps { + children: React.ReactNode; + messageType: NoticeType; + action?: React.ReactNode; + className?: string; + textColor?: string; + textVariant?: string; + icon?: React.ComponentType<{ className?: string; color?: string; }>; + hidden?: boolean; +} diff --git a/src/equicordplugins/components.dev/types/paginator.d.ts b/src/equicordplugins/components.dev/types/paginator.d.ts new file mode 100644 index 000000000..8a2694519 --- /dev/null +++ b/src/equicordplugins/components.dev/types/paginator.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 PaginatorPageWrapperProps { + key: string; + targetPage: number; + selected: boolean; + navigateToPage: () => void; +} + +export interface PaginatorProps { + currentPage: number; + totalCount: number; + pageSize: number; + maxVisiblePages?: number; + disablePaginationGap?: boolean; + onPageChange?: (page: number) => void; + hideMaxPage?: boolean; + className?: string; + renderPageWrapper?: (props: PaginatorPageWrapperProps, defaultRender: React.ReactNode) => React.ReactNode; +} diff --git a/src/equicordplugins/components.dev/types/progress.d.ts b/src/equicordplugins/components.dev/types/progress.d.ts new file mode 100644 index 000000000..cf9eaa5c6 --- /dev/null +++ b/src/equicordplugins/components.dev/types/progress.d.ts @@ -0,0 +1,25 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export type ProgressBarVariant = "blue" | "orange" | "unset"; + +export interface ProgressBarOverride { + background?: string; + gradientStart?: string; + gradientEnd?: string; +} + +export interface ProgressBarProps { + progress: number; + minimum?: number; + maximum?: number; + variant?: ProgressBarVariant; + override?: { + default?: ProgressBarOverride; + [key: string]: ProgressBarOverride | undefined; + }; + labelledBy?: string; +} diff --git a/src/equicordplugins/components.dev/types/radio.d.ts b/src/equicordplugins/components.dev/types/radio.d.ts new file mode 100644 index 000000000..99a0b3f08 --- /dev/null +++ b/src/equicordplugins/components.dev/types/radio.d.ts @@ -0,0 +1,26 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface RadioOption { + value: string | number; + name: string; + desc?: string; + disabled?: boolean; + leadingIcon?: React.ComponentType<{ className?: string; size?: string; color?: string; }>; +} + +export interface ManaBaseRadioGroupProps { + options: RadioOption[]; + value?: string | number; + onChange?: (value: string | number) => void; + disabled?: boolean; + "aria-labelledby"?: string; +} + +export interface StandaloneRadioIndicatorProps { + isSelected: boolean; + disabled?: boolean; +} diff --git a/src/equicordplugins/components.dev/types/select.d.ts b/src/equicordplugins/components.dev/types/select.d.ts new file mode 100644 index 000000000..34c09ac8f --- /dev/null +++ b/src/equicordplugins/components.dev/types/select.d.ts @@ -0,0 +1,66 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface ManaSelectOption { + id: string; + value: string; + label: string; +} + +export interface ManaSelectProps { + options: ManaSelectOption[]; + value?: string | string[] | null; + onSelectionChange?: (value: string | string[] | null) => void; + selectionMode?: "single" | "multiple"; + placeholder?: string; + disabled?: boolean; + readOnly?: boolean; + clearable?: boolean; + fullWidth?: boolean; + autoFocus?: boolean; + closeOnSelect?: boolean; + shouldFocusWrap?: boolean; + maxOptionsVisible?: number; + wrapTags?: boolean; +} + +export interface ManaComboboxProps { + placeholder?: string; + value?: string | string[]; + onChange?: (value: string | string[]) => void; + multiSelect?: boolean; + autoFocus?: boolean; + maxVisibleItems?: number; + emptyStateText?: string; + emptyStateHeader?: string; + onQueryChange?: (query: string) => void; + "aria-label"?: string; + className?: string; + listClassName?: string; + children: (query: string) => React.ReactNode[]; +} + +export interface ListboxItem { + id: string; + label: string; + disabled?: boolean; +} + +export interface ManaListboxProps { + items: ListboxItem[]; + selectedItems?: ListboxItem[]; + defaultSelectedItems?: ListboxItem[]; + onSelectionChange?: (items: ListboxItem[]) => void; + selectionMode?: "single" | "multiple"; + disabled?: boolean; + required?: boolean; + loading?: boolean; + maxVisibleItems?: number; + shouldFocusWrap?: boolean; + typeahead?: boolean; + renderListItem?: (item: ListboxItem) => React.ReactNode; + renderEmptyState?: () => React.ReactNode; +} diff --git a/src/equicordplugins/components.dev/types/slider.d.ts b/src/equicordplugins/components.dev/types/slider.d.ts new file mode 100644 index 000000000..ec02f942d --- /dev/null +++ b/src/equicordplugins/components.dev/types/slider.d.ts @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface SliderProps { + initialValue?: number; + value?: number; + minValue?: number; + maxValue?: number; + onValueChange?: (value: number) => void; + onValueRender?: (value: number) => string; + markers?: number[]; + stickToMarkers?: boolean; + onMarkerRender?: (value: number) => string; + renderMarker?: (value: number) => React.ReactNode; + disabled?: boolean; + mini?: boolean; + hideBubble?: boolean; + defaultValue?: number; + orientation?: "horizontal" | "vertical"; + barClassName?: string; + grabberClassName?: string; + className?: string; + "aria-label"?: string; + "aria-labelledby"?: string; + "aria-describedby"?: string; +} diff --git a/src/equicordplugins/components.dev/types/spinner.d.ts b/src/equicordplugins/components.dev/types/spinner.d.ts new file mode 100644 index 000000000..a35708b4f --- /dev/null +++ b/src/equicordplugins/components.dev/types/spinner.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 type SpinnerType = "wanderingCubes" | "chasingDots" | "pulsingEllipsis" | "spinningCircle" | "spinningCircleSimple" | "lowMotion"; + +export interface SpinnerProps { + type?: SpinnerType; + animated?: boolean; + className?: string; + itemClassName?: string; + "aria-label"?: string; +} diff --git a/src/equicordplugins/components.dev/types/switch.d.ts b/src/equicordplugins/components.dev/types/switch.d.ts new file mode 100644 index 000000000..5283de252 --- /dev/null +++ b/src/equicordplugins/components.dev/types/switch.d.ts @@ -0,0 +1,11 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface ManaSwitchProps { + checked: boolean; + onChange?: (checked: boolean) => void; + disabled?: boolean; +} diff --git a/src/equicordplugins/components.dev/types/tabbar.d.ts b/src/equicordplugins/components.dev/types/tabbar.d.ts new file mode 100644 index 000000000..78baba9d7 --- /dev/null +++ b/src/equicordplugins/components.dev/types/tabbar.d.ts @@ -0,0 +1,55 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface TabBarProps { + className?: string; + children?: React.ReactNode; + type?: "side" | "top" | "top-pill"; + look?: "brand" | "grey"; + style?: React.CSSProperties; + selectedItem?: string; + onItemSelect?: (id: string) => void; + orientation?: "horizontal" | "vertical"; + "aria-label"?: string; +} + +export interface TabBarItemProps { + id: string; + className?: string; + children?: React.ReactNode; + selectedItem?: string; + onItemSelect?: (id: string) => void; + itemType?: "side" | "top" | "top-pill"; + look?: "brand" | "grey"; + color?: string; + variant?: "destructive"; + disabled?: boolean; + disableItemStyles?: boolean; + onClick?: React.MouseEventHandler; + onContextMenu?: React.MouseEventHandler; + clickableRef?: React.Ref; + clickableInnerRef?: React.Ref; + "aria-label"?: string; +} + +export interface TabBarHeaderProps { + className?: string; + children?: React.ReactNode; + onClick?: React.MouseEventHandler; + "aria-expanded"?: boolean; + "aria-controls"?: string; +} + +export interface TabBarSeparatorProps { + style?: React.CSSProperties; +} + +export type TabBarComponent = React.ComponentType & { + Item: React.ComponentType; + Header: React.ComponentType; + Separator: React.ComponentType; + Panel: React.ComponentType<{ children?: React.ReactNode; }>; +}; diff --git a/src/equicordplugins/components.dev/types/tooltip.d.ts b/src/equicordplugins/components.dev/types/tooltip.d.ts new file mode 100644 index 000000000..fe276427e --- /dev/null +++ b/src/equicordplugins/components.dev/types/tooltip.d.ts @@ -0,0 +1,53 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface ManaTooltipProps { + text: string; + position?: "top" | "bottom" | "left" | "right"; + align?: "start" | "center" | "end"; + color?: "primary" | "black" | "grey" | "brand" | "green" | "yellow" | "red"; + spacing?: number; + hideOnClick?: boolean; + delay?: number; + children: (props: any) => React.ReactNode; +} + +export interface ManaRichTooltipProps { + title?: string; + body: string; + asset?: React.ReactNode | { src: string; alt?: string; }; + assetSize?: number; + asContainer?: boolean; + element?: "span" | "div"; + position?: "top" | "bottom" | "left" | "right"; + align?: "start" | "center" | "end"; + spacing?: number; + ariaHidden?: boolean; + children: React.ReactNode | ((props: any) => React.ReactNode); +} + +export interface PopoverAction { + text: string; + onClick?: () => void; + variant?: "primary" | "secondary"; +} + +export interface ManaPopoverProps { + title?: string; + body?: string; + badge?: React.ReactNode; + graphic?: { src: string; aspectRatio?: string; }; + size?: "sm" | "md" | "lg"; + actions?: PopoverAction[]; + textLink?: { text: string; onClick?: () => void; }; + gradientColor?: string; + onRequestClose?: (reason: string) => void; + targetElementRef: React.RefObject; + shouldShow: boolean; + position?: "top" | "bottom" | "left" | "right"; + alignmentStrategy?: "edge" | "trigger-center"; + align?: "left" | "center" | "right"; +} diff --git a/src/equicordplugins/followVoiceUser/index.tsx b/src/equicordplugins/followVoiceUser/index.tsx index 13dabbdf4..81c0c6ecf 100644 --- a/src/equicordplugins/followVoiceUser/index.tsx +++ b/src/equicordplugins/followVoiceUser/index.tsx @@ -6,7 +6,7 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { Channel, User, VoiceState } from "@vencord/discord-types"; @@ -78,9 +78,9 @@ export default definePlugin({ authors: [EquicordDevs.TheArmagan], settings, settingsAboutComponent: () => ( - + This Plugin is used to follow a Friend/Friends into voice chat(s). - + ), flux: { async VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { diff --git a/src/equicordplugins/neverPausePreviews/index.tsx b/src/equicordplugins/neverPausePreviews/index.tsx index 6758e933e..3c237c788 100644 --- a/src/equicordplugins/neverPausePreviews/index.tsx +++ b/src/equicordplugins/neverPausePreviews/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { EquicordDevs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -25,9 +25,9 @@ export default definePlugin({ description: "Prevents in-call/PiP previews (screenshare, streams, etc) from pausing even if the client loses focus", authors: [EquicordDevs.vappstar], settingsAboutComponent: () => ( - + This plugin will cause discord to use more resources than normal - + ), patches: [ { diff --git a/src/equicordplugins/platformSpoofer/index.tsx b/src/equicordplugins/platformSpoofer/index.tsx index 161e6c67a..7817d3dbf 100644 --- a/src/equicordplugins/platformSpoofer/index.tsx +++ b/src/equicordplugins/platformSpoofer/index.tsx @@ -5,7 +5,7 @@ */ import { definePluginSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { UserStore } from "@webpack/common"; @@ -50,9 +50,9 @@ export default definePlugin({ description: "Spoof what platform or device you're on", authors: [EquicordDevs.Drag], settingsAboutComponent: () => ( - + We can't guarantee this plugin won't get you warned or banned. - + ), settings: settings, patches: [ diff --git a/src/equicordplugins/questCompleter.discordDesktop/index.tsx b/src/equicordplugins/questCompleter.discordDesktop/index.tsx index a74bbc5a7..caa46470d 100644 --- a/src/equicordplugins/questCompleter.discordDesktop/index.tsx +++ b/src/equicordplugins/questCompleter.discordDesktop/index.tsx @@ -21,7 +21,7 @@ import "./style.css"; import { HeaderBarButton } from "@api/HeaderBar"; import { showNotification } from "@api/Notifications"; import { definePluginSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { Devs } from "@utils/constants"; import { getTheme, Theme } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; @@ -220,9 +220,9 @@ export default definePlugin({ authors: [Devs.amia], settings, settingsAboutComponent: () => ( - + You must manually accept the quest first before clicking the button. - + ), headerBarButton: { icon: QuestIcon, diff --git a/src/equicordplugins/questFocused/index.tsx b/src/equicordplugins/questFocused/index.tsx index 695a4f023..f29fddc61 100644 --- a/src/equicordplugins/questFocused/index.tsx +++ b/src/equicordplugins/questFocused/index.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { EquicordDevs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -12,9 +12,9 @@ export default definePlugin({ name: "QuestFocused", description: "Prevent the quests player from pausing and possibly skip it all together.", settingsAboutComponent: () => ( - + You might need to spam left mouse button on the video to skip it. - + ), authors: [EquicordDevs.secp192k1], patches: [ diff --git a/src/equicordplugins/universalMention/index.tsx b/src/equicordplugins/universalMention/index.tsx index 4b3c75f20..1e893d2c2 100644 --- a/src/equicordplugins/universalMention/index.tsx +++ b/src/equicordplugins/universalMention/index.tsx @@ -5,7 +5,7 @@ */ import { definePluginSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { User } from "@vencord/discord-types"; @@ -41,9 +41,9 @@ export default definePlugin({ description: "Mention any user, regardless of channel access.", settings, settingsAboutComponent: () => ( - + Using Global Mention can cause performance issues and show an absurd amount of users in the autocomplete. - + ), patches: [