From f3d63106c24404f2dc68d8d0708ac89621a6a845 Mon Sep 17 00:00:00 2001 From: prism Date: Tue, 23 Dec 2025 23:28:02 +0100 Subject: [PATCH] Components: add more types, components and fixes (#591) * changes * fixes * more fixes * even more changes --- .../components/ComponentsTab.tsx | 9 + .../components.dev/components/index.tsx | 251 +++++++----------- .../components/tabs/BadgeTab.tsx | 4 +- .../components/tabs/ChipTab.tsx | 53 ++-- .../components/tabs/FocusLockTab.tsx | 86 ++++++ .../components/tabs/PaginatorTab.tsx | 2 +- .../components/tabs/PopoutTab.tsx | 124 +++++++++ .../components/tabs/SkeletonTab.tsx | 35 +-- .../components/tabs/ToastTab.tsx | 4 + .../components/tabs/UserSummaryItemTab.tsx | 92 +++++++ .../components.dev/constants.ts | 19 ++ .../components.dev/types/avatar.d.ts | 13 +- .../components.dev/types/badge.d.ts | 2 +- .../components.dev/types/checkbox.d.ts | 2 + .../components.dev/types/focuslock.d.ts | 11 + .../types/{index.ts => index.d.ts} | 3 + .../components.dev/types/input.d.ts | 3 + .../components.dev/types/popout.d.ts | 72 +++++ .../components.dev/types/radio.d.ts | 1 + .../components.dev/types/select.d.ts | 13 +- .../components.dev/types/toast.d.ts | 2 +- .../components.dev/types/tooltip.d.ts | 26 +- .../components.dev/types/usersummary.d.ts | 28 ++ 23 files changed, 634 insertions(+), 221 deletions(-) create mode 100644 src/equicordplugins/components.dev/components/tabs/FocusLockTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/PopoutTab.tsx create mode 100644 src/equicordplugins/components.dev/components/tabs/UserSummaryItemTab.tsx create mode 100644 src/equicordplugins/components.dev/types/focuslock.d.ts rename src/equicordplugins/components.dev/types/{index.ts => index.d.ts} (91%) create mode 100644 src/equicordplugins/components.dev/types/popout.d.ts create mode 100644 src/equicordplugins/components.dev/types/usersummary.d.ts diff --git a/src/equicordplugins/components.dev/components/ComponentsTab.tsx b/src/equicordplugins/components.dev/components/ComponentsTab.tsx index fc47d5df4..f3d0f141b 100644 --- a/src/equicordplugins/components.dev/components/ComponentsTab.tsx +++ b/src/equicordplugins/components.dev/components/ComponentsTab.tsx @@ -25,6 +25,7 @@ import ClickableTab from "./tabs/ClickableTab"; import ColorPickerTab from "./tabs/ColorPickerTab"; import ComboboxTab from "./tabs/ComboboxTab"; import DividerTab from "./tabs/DividerTab"; +import FocusLockTab from "./tabs/FocusLockTab"; import GuildIconTab from "./tabs/GuildIconTab"; import HeadingTab from "./tabs/HeadingTab"; import LayerModalTab from "./tabs/LayerModalTab"; @@ -32,6 +33,7 @@ import ListboxTab from "./tabs/ListboxTab"; import ModalTab from "./tabs/ModalTab"; import NoticeTab from "./tabs/NoticeTab"; import PaginatorTab from "./tabs/PaginatorTab"; +import PopoutTab from "./tabs/PopoutTab"; import PopoverTab from "./tabs/PopoverTab"; import ProgressBarTab from "./tabs/ProgressBarTab"; import RadioGroupTab from "./tabs/RadioGroupTab"; @@ -51,6 +53,7 @@ import TimestampTab from "./tabs/TimestampTab"; import ToastTab from "./tabs/ToastTab"; import TooltipTab from "./tabs/TooltipTab"; import TypographyTab from "./tabs/TypographyTab"; +import UserSummaryItemTab from "./tabs/UserSummaryItemTab"; const TABS = [ @@ -68,6 +71,7 @@ const TABS = [ { id: "colorpicker", label: "ColorPicker" }, { id: "combobox", label: "Combobox" }, { id: "divider", label: "Divider" }, + { id: "focuslock", label: "FocusLock" }, { id: "guildicon", label: "GuildIcon" }, { id: "heading", label: "Heading" }, { id: "layermodal", label: "LayerModal" }, @@ -75,6 +79,7 @@ const TABS = [ { id: "modal", label: "Modal" }, { id: "notice", label: "Notice" }, { id: "paginator", label: "Paginator" }, + { id: "popout", label: "Popout" }, { id: "popover", label: "Popover" }, { id: "progressbar", label: "ProgressBar" }, { id: "radiogroup", label: "RadioGroup" }, @@ -94,6 +99,7 @@ const TABS = [ { id: "toast", label: "Toast" }, { id: "tooltip", label: "Tooltip" }, { id: "typography", label: "Typography" }, + { id: "usersummaryitem", label: "UserSummaryItem" }, ].sort((a, b) => a.label.localeCompare(b.label)); @@ -114,6 +120,7 @@ const TAB_COMPONENTS: Record = { colorpicker: ColorPickerTab, combobox: ComboboxTab, divider: DividerTab, + focuslock: FocusLockTab, guildicon: GuildIconTab, heading: HeadingTab, layermodal: LayerModalTab, @@ -121,6 +128,7 @@ const TAB_COMPONENTS: Record = { modal: ModalTab, notice: NoticeTab, paginator: PaginatorTab, + popout: PopoutTab, popover: PopoverTab, progressbar: ProgressBarTab, radiogroup: RadioGroupTab, @@ -140,6 +148,7 @@ const TAB_COMPONENTS: Record = { toast: ToastTab, tooltip: TooltipTab, typography: TypographyTab, + usersummaryitem: UserSummaryItemTab, }; function ComponentsTab() { diff --git a/src/equicordplugins/components.dev/components/index.tsx b/src/equicordplugins/components.dev/components/index.tsx index 9f81cf3f8..bde65541e 100644 --- a/src/equicordplugins/components.dev/components/index.tsx +++ b/src/equicordplugins/components.dev/components/index.tsx @@ -9,43 +9,31 @@ import { Card } from "@components/Card"; import { Heading } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import { proxyLazy } from "@utils/lazy"; -import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findLazy } from "@webpack"; +import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findLazy, mapMangledModuleLazy } from "@webpack"; import { GuildStore, + ListScrollerAuto, + ListScrollerNone, + ListScrollerThin, + OAuth2AuthorizeModal, ScrollerAuto, ScrollerNone, ScrollerThin, + SearchableSelect, TabBar, + TooltipContainer, useEffect, useRef, UserStore, useState, } from "@webpack/common"; -export { - Button, - Card, - GuildStore, - Heading, - Paragraph, - ScrollerAuto, - ScrollerNone, - ScrollerThin, - TabBar, - useEffect, - useRef, - UserStore, - useState, -}; - -export * from "../constants"; - -import { +import { ToastPosition, ToastType } from "../constants"; +import type { AccordionProps, AnchorProps, AvatarProps, BadgeShapesType, - CheckboxGroupOption, ChipProps, CircleBadgeProps, ClickableProps, @@ -58,11 +46,9 @@ import { DiscordHeadingProps, DiscordTextProps, ExpressiveModalProps, - GradientColor, + FocusLockProps, GuildIconProps, - HeadingVariant, IconBadgeProps, - ListboxItem, ManaBaseRadioGroupProps, ManaButtonProps, ManaCalendarProps, @@ -73,137 +59,61 @@ import { ManaListboxProps, ManaPopoverProps, ManaRichTooltipProps, - ManaSelectOption, ManaSelectProps, ManaSwitchProps, ManaTextAreaProps, ManaTextButtonProps, ManaTextInputProps, ManaTooltipProps, - ModalAction, - ModalNotice, ModalProps, ModalRenderFn, NoticeProps, - NoticeType, NumberBadgeProps, OpenModalOptions, PaginatorProps, - PopoverAction, + PopoutComponent, ProgressBarProps, - RadioOption, SearchBarProps, SkeletonProps, SliderProps, SpinnerComponent, - SpinnerProps, StandaloneRadioIndicatorProps, - TabBarComponent, - TabBarHeaderProps, - TabBarItemProps, - TabBarProps, - TabBarSeparatorProps, TextBadgeProps, - TextColor, - TextVariant, TimestampProps, ToastData, ToastOptions, - ToastPositionValue, ToastsModule, ToastTypeValue, + UserSummaryItemProps, } from "../types"; -export type { - AccordionProps, - AnchorProps, - AvatarProps, - BadgeShapesType, - CheckboxGroupOption, - ChipProps, - CircleBadgeProps, - ColorPickerProps, - ColorPickerWithSwatchesProps, - ColorSwatchProps, - ConfirmModalProps, - CustomColorButtonProps, - DefaultColorButtonProps, - DiscordHeadingProps, - DiscordTextProps, - ExpressiveModalProps, - GradientColor, - GuildIconProps, - HeadingVariant, - IconBadgeProps, - ListboxItem, - ManaBaseRadioGroupProps, - ManaButtonProps, - ManaCalendarProps, - ManaCheckboxGroupProps, - ManaCheckboxProps, - ManaComboboxProps, - ManaDatePickerProps, - ManaListboxProps, - ManaPopoverProps, - ManaRichTooltipProps, - ManaSelectOption, - ManaSelectProps, - ManaSwitchProps, - ManaTextAreaProps, - ManaTextButtonProps, - ManaTextInputProps, - ManaTooltipProps, - ModalAction, - ModalNotice, - ModalProps, - ModalRenderFn, - NoticeProps, - NoticeType, - NumberBadgeProps, - OpenModalOptions, - PaginatorProps, - PopoverAction, - ProgressBarProps, - RadioOption, - SearchBarProps, - SkeletonProps, - SliderProps, - SpinnerProps, - StandaloneRadioIndicatorProps, - TabBarComponent, - TabBarHeaderProps, - TabBarItemProps, - TabBarProps, - TabBarSeparatorProps, - TextBadgeProps, - TextColor, - TextVariant, - TimestampProps, - ToastData, - ToastOptions, - ToastPositionValue, - ToastsModule, - ToastTypeValue, +export * from "../constants"; +export type * from "../types"; + +export { + Button, + Card, + GuildStore, + Heading, + ListScrollerAuto, + ListScrollerNone, + ListScrollerThin, + OAuth2AuthorizeModal, + Paragraph, + ScrollerAuto, + ScrollerNone, + ScrollerThin, + SearchableSelect, + TabBar, + TooltipContainer, + useEffect, + useRef, + UserStore, + useState, }; -export const ToastType = { - MESSAGE: "message", - SUCCESS: "success", - FAILURE: "failure", - CUSTOM: "custom", - CLIP: "clip", - LINK: "link", - FORWARD: "forward", - BOOKMARK: "bookmark", - CLOCK: "clock", -} as const; - -export const ToastPosition = { - TOP: 0, - BOTTOM: 1, -} as const; - export const Anchor = findComponentByCodeLazy("anchorUnderlineOnHover", "useDefaultUnderlineStyles") as React.ComponentType; +export const Clickable = findByCodeLazy("ignoreKeyPress", "renderNonInteractive") 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; @@ -212,25 +122,38 @@ export const ManaTextInput = findComponentByCodeLazy('"data-mana-component":"tex export const ManaTextArea = findComponentByCodeLazy('"data-mana-component":"text-area"') as React.ComponentType; export const ManaCheckbox = findComponentByCodeLazy('"data-mana-component":"checkbox"') as React.ComponentType; export const ManaSelect = findComponentByCodeLazy('"data-mana-component":"select"') as React.ComponentType; -export const ManaTooltip = findComponentByCodeLazy("VoidTooltip cannot find DOM node") as React.ComponentType; export const ManaCalendar = findComponentByCodeLazy('"data-mana-component":"calendar"') as React.ComponentType; export const ManaDatePicker = findComponentByCodeLazy('"data-mana-component":"date-picker"') as React.ComponentType; -export const ManaRichTooltip = findComponentByCodeLazy('"data-mana-component":"rich-tooltip"') as React.ComponentType; -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 ManaCombobox = findComponentByCodeLazy("itemToString", "multiSelect", "maxVisibleItems") as React.ComponentType; +export const ManaTooltip = findComponentByCodeLazy("VoidTooltip cannot find DOM node") as React.ComponentType; +export const ManaRichTooltip = findComponentByCodeLazy('"data-mana-component":"rich-tooltip"') as React.ComponentType; +export const ManaPopover = findByCodeLazy("title", "body", "badge", "graphic", "actions", "textLink", "gradientColor", "popoverRef") 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 unknown as SpinnerComponent; export const SearchBar = findComponentByCodeLazy("#{intl::SEARCH}", "clearable", "autoComplete") as React.ComponentType; -export const Paginator2 = findComponentByCodeLazy("#{intl::BACK}", "#{intl::NEXT}", "renderPageWrapper") as React.ComponentType; +export const Paginator = findComponentByCodeLazy("#{intl::BACK}", "#{intl::NEXT}", "renderPageWrapper") as React.ComponentType; export const Notice = findComponentByCodeLazy("messageType", "iconDiv", "actionContainer") as React.ComponentType; +export const Chip = findComponentByCodeLazy('variant:"eyebrow"', "chip,") as React.ComponentType; +export const Skeleton = findComponentByCodeLazy("productSkeletonCardContainer", "skipPulseAnimation") as React.ComponentType; +export const Accordion = findComponentByCodeLazy("accordionContainer", "onExpandedChange", "defaultExpanded") as React.ComponentType; +export const Timestamp = findComponentByCodeLazy("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}", "isVisibleOnlyOnHover") as React.ComponentType; +export const GuildIcon = findComponentByCodeLazy("Masks.CLAN_ICON", "guildIconImage") as React.ComponentType; export const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", "showEyeDropper") as React.ComponentType; +export const Popout: PopoutComponent = proxyLazy(() => findLazy(m => m?.y?.Animation && m.y.toString?.().includes("renderPopout")).y); +export const FocusLock = findComponentByCodeLazy(".containerRef,{keyboardModeEnabled:") as React.ComponentType; +export const UserSummaryItem = findComponentByCodeLazy("popoutUserId") as React.ComponentType; + +export const Animations = mapMangledModuleLazy(".assign({colorNames:", { + Transition: filters.componentByCode('["items","children"]', ",null,"), + animated: filters.byProps("div", "text") +}); const ColorPickerModule = findLazy(m => { const values = Object.values(m); @@ -273,23 +196,28 @@ function getBadgeExport(check: (fn: Function) => boolean): React.ComponentType 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 Clickable = findByCodeLazy("ignoreKeyPress", "renderNonInteractive") as React.ComponentType; - -export const Chip = findComponentByCodeLazy('variant:"eyebrow"', "chip,") as React.ComponentType; - -export const Skeleton = findComponentByCodeLazy("productSkeletonCardContainer", "skipPulseAnimation") as React.ComponentType; - -export const Accordion = findComponentByCodeLazy("accordionContainer", "onExpandedChange", "defaultExpanded") as React.ComponentType; - -export const Timestamp = findComponentByCodeLazy("isVisibleOnlyOnHover", "cozyAlt", "timestampFormat") as React.ComponentType; - -export const GuildIcon = findComponentByCodeLazy("getGuildIconURL", "acronym") as React.ComponentType; +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: "" } +); const ToastsModule = findByPropsLazy("showToast", "popToast"); export const Toasts: ToastsModule = proxyLazy(() => ({ @@ -300,7 +228,9 @@ export const Toasts: ToastsModule = proxyLazy(() => ({ pop: () => ToastsModule.popToast(), create: (message: string, type: ToastTypeValue, options?: ToastOptions) => ToastsModule.createToast(message, type, options), })); -export const showToast = (message: string, type: ToastTypeValue = ToastType.MESSAGE) => ToastsModule.showToast(ToastsModule.createToast(message, type)); + +export const showToast = (message: string, type: ToastTypeValue = ToastType.MESSAGE) => + ToastsModule.showToast(ToastsModule.createToast(message, type)); const ModalModule = findByPropsLazy("Modal", "ConfirmModal"); const ModalAPIModule = findByPropsLazy("openModal", "closeModal"); @@ -309,20 +239,25 @@ export const Modal: React.ComponentType = proxyLazy(() => ModalModul export const ConfirmModal: React.ComponentType = proxyLazy(() => ModalModule.ConfirmModal); export const ExpressiveModal: React.ComponentType = proxyLazy(() => ModalModule.ExpressiveModal); -export const openModal = (render: ModalRenderFn, options?: OpenModalOptions): string => ModalAPIModule.openModal(render, options); -export const closeModal = (modalKey: string): void => ModalAPIModule.closeModal(modalKey); -export const closeAllModals = (): void => ModalAPIModule.closeAllModals(); -export const hasModalOpen = (modalKey: string): boolean => ModalAPIModule.hasModalOpen(modalKey); -export const hasAnyModalOpen = (): boolean => ModalAPIModule.hasAnyModalOpen(); +export const openModal = (render: ModalRenderFn, options?: OpenModalOptions): string => + ModalAPIModule.openModal(render, options); +export const closeModal = (modalKey: string): void => + ModalAPIModule.closeModal(modalKey); +export const closeAllModals = (): void => + ModalAPIModule.closeAllModals(); +export const hasModalOpen = (modalKey: string): boolean => + ModalAPIModule.hasModalOpen(modalKey); +export const hasAnyModalOpen = (): boolean => + ModalAPIModule.hasAnyModalOpen(); const DiscordTextModule = findByPropsLazy("Heading", "Text"); + export function DiscordHeading(props: DiscordHeadingProps) { - const Component = DiscordTextModule.Heading; - return ; + return ; } + export function DiscordText(props: DiscordTextProps) { - const Component = DiscordTextModule.Text; - return ; + return ; } export function Divider({ className, style, ...restProps }: React.ComponentPropsWithoutRef<"hr">) { diff --git a/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx b/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx index 3bc72adfc..dad9add36 100644 --- a/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, Paragraph,TextBadge } from ".."; +import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, Paragraph, TextBadge } from ".."; import { EquicordIcon } from "../icons/EquicordIcon"; import { SectionWrapper } from "../SectionWrapper"; @@ -84,7 +84,7 @@ export default function BadgeTab() { - NumberBadge: count, maxDigits?, color?, disableColor?, shape?, className?, style? + NumberBadge: count, color?, disableColor?, shape?, className?, style?, renderBadgeCount? TextBadge: text, color?, disableColor?, shape?, className?, style? diff --git a/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx b/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx index ee4b0c5d6..1a951824c 100644 --- a/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx @@ -4,31 +4,35 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Chip , Paragraph } from ".."; +import { Chip, Paragraph } from ".."; import { SectionWrapper } from "../SectionWrapper"; +const CHIP_COLORS = ["blurple", "purple", "green", "orange", "yellow", "pink", "red", "gray"] as const; +const CHIP_SHADES = ["Light", "Medium", "Dark"] as const; + export default function ChipTab() { return (
- -
- - - - - - - - -
-
- - -
- - - -
+ + + All 24 chip variants: 8 colors × 3 shades. + + {CHIP_SHADES.map(shade => ( +
+ + {shade} + +
+ {CHIP_COLORS.map(color => ( + + ))} +
+
+ ))}
@@ -49,6 +53,15 @@ export default function ChipTab() { • variant?: ChipVariant - Color variant (default: "grayLight") + + Variant Format: {"{color}{shade}"} where: + + + • Colors: blurple, purple, green, orange, yellow, pink, red, gray + + + • Shades: Light, Medium, Dark +
); diff --git a/src/equicordplugins/components.dev/components/tabs/FocusLockTab.tsx b/src/equicordplugins/components.dev/components/tabs/FocusLockTab.tsx new file mode 100644 index 000000000..fe34629d5 --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/FocusLockTab.tsx @@ -0,0 +1,86 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { FocusLock, ManaButton, ManaTextInput, Paragraph, useRef, useState } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function FocusLockTab() { + const [focusLockEnabled, setFocusLockEnabled] = useState(false); + const containerRef = useRef(null); + const [inputValue, setInputValue] = useState(""); + + return ( +
+ + + FocusLock traps keyboard focus within a container. Enable it and try tabbing - focus stays inside the box. + + setFocusLockEnabled(!focusLockEnabled)} + style={{ marginBottom: 16 }} + /> +
+ {focusLockEnabled && ( + + + Focus is now trapped! Tab between these elements: + +
+ + { }} + placeholder="Second input" + /> +
+ { }} /> + { }} /> + setFocusLockEnabled(false)} + /> +
+
+
+ )} + {!focusLockEnabled && ( + + Click "Enable Focus Lock" to activate the focus trap demo. + + )} +
+
+ + + • Modal dialogs - Keep focus within the modal + • Dropdown menus - Trap focus while open + • Settings panels - Improve keyboard navigation + • Accessibility - Ensure screen readers stay in context + + + + • containerRef - React ref to the container element + • keyboardModeEnabled - Enable keyboard focus trapping + • children - Content to render inside the focus trap + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx b/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx index f019a5547..c3b2b3490 100644 --- a/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Paginator2 as Paginator, Paragraph, useState } from ".."; +import { Paginator, Paragraph, useState } from ".."; import { SectionWrapper } from "../SectionWrapper"; export default function PaginatorTab() { diff --git a/src/equicordplugins/components.dev/components/tabs/PopoutTab.tsx b/src/equicordplugins/components.dev/components/tabs/PopoutTab.tsx new file mode 100644 index 000000000..339f7086f --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/PopoutTab.tsx @@ -0,0 +1,124 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Clickable, ManaButton, Paragraph, Popout, useState } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function PopoutTab() { + const [showBasic, setShowBasic] = useState(false); + const [showPositions, setShowPositions] = useState>({}); + + const positions = ["top", "bottom", "left", "right"] as const; + + return ( +
+ + + Click the button to toggle a popout. + + setShowBasic(false)} + renderPopout={() => ( +
+ This is a popout! + setShowBasic(false)} + style={{ marginTop: 8 }} + /> +
+ )} + > + {(props, { isShown }) => ( + + setShowBasic(!showBasic)} + /> + + )} +
+
+ + + + Popout can appear in different positions. + +
+ {positions.map(position => ( + setShowPositions(p => ({ ...p, [position]: false }))} + renderPopout={() => ( +
+ Position: {position} +
+ )} + > + {(props, { isShown }) => ( + + setShowPositions(p => ({ ...p, [position]: !p[position] }))} + /> + + )} +
+ ))} +
+
+ + + + Popout supports different animation types via Popout.Animation. + +
+ {(["NONE", "TRANSLATE", "SCALE", "FADE"] as const).map(anim => ( + setShowPositions(p => ({ ...p, [`anim-${anim}`]: false }))} + renderPopout={() => ( +
+ Animation: {anim} +
+ )} + > + {(props, { isShown }) => ( + + setShowPositions(p => ({ ...p, [`anim-${anim}`]: !p[`anim-${anim}`] }))} + /> + + )} +
+ ))} +
+
+ + + • children - Render function (props, data) => ReactNode + • renderPopout - Render function for popout content + • shouldShow - Control visibility + • position - top, bottom, left, right, center, window_center + • align - left, right, center, top, bottom + • animation - Popout.Animation.NONE/TRANSLATE/SCALE/FADE + • spacing - Distance from target (default 8) + • onRequestOpen/onRequestClose - Callbacks + • autoInvert - Auto-flip if near edge (default true) + +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx b/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx index 95473b914..1e991f7a8 100644 --- a/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx @@ -12,48 +12,23 @@ export default function SkeletonTab() { return (
- + - Loading placeholder with pulse animation. Requires "full-motion" parent class for animation. - -
- - - -
-
- - - - Use skipPulseAnimation to disable the pulse effect. + Loading placeholder with pulse animation. Used in Discord's shop for product cards. setAnimated(!animated)} + style={{ marginBottom: 16 }} /> -
- +
- - • skipPulseAnimation?: boolean - Disable pulse animation (default: false) - - - - - - • This is a product card skeleton used in Discord's shop. - - - • Animation requires a parent with "full-motion" class (Discord's reduced motion setting). - - - • The skeleton has fixed dimensions designed for product cards. - + • skipPulseAnimation?: boolean - Disable pulse animation
); diff --git a/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx b/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx index edb4f5eb0..7c6974072 100644 --- a/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx @@ -14,8 +14,10 @@ const TOAST_TYPES = [ { type: ToastType.CLIP, label: "CLIP", description: "Clip/copy toast" }, { type: ToastType.LINK, label: "LINK", description: "Link copied toast" }, { type: ToastType.FORWARD, label: "FORWARD", description: "Forward action toast" }, + { type: ToastType.INVITE, label: "INVITE", description: "Invite action toast" }, { type: ToastType.BOOKMARK, label: "BOOKMARK", description: "Bookmark action toast" }, { type: ToastType.CLOCK, label: "CLOCK", description: "Clock/timer toast" }, + { type: ToastType.AI, label: "AI", description: "AI action toast" }, ] as const; export default function ToastTab() { @@ -108,8 +110,10 @@ export default function ToastTab() { • CLIP - Clip/scissors icon • LINK - Link icon • FORWARD - Forward arrow icon + • INVITE - Invite icon • BOOKMARK - Bookmark icon • CLOCK - Clock icon + • AI - AI icon
diff --git a/src/equicordplugins/components.dev/components/tabs/UserSummaryItemTab.tsx b/src/equicordplugins/components.dev/components/tabs/UserSummaryItemTab.tsx new file mode 100644 index 000000000..0b6cd42dc --- /dev/null +++ b/src/equicordplugins/components.dev/components/tabs/UserSummaryItemTab.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, UserStore, UserSummaryItem } from ".."; +import { SectionWrapper } from "../SectionWrapper"; + +export default function UserSummaryItemTab() { + const currentUser = UserStore.getCurrentUser(); + const users = currentUser ? [currentUser, currentUser, currentUser, currentUser, currentUser] : []; + + return ( +
+ + + Displays a row of user avatars with overflow indicator. + +
+ +
+
+ + + + Available sizes: 16, 24, 32, 56. + +
+
+ 16: + +
+
+ 24: + +
+
+ 32: + +
+
+ 56: + +
+
+
+ + + + Control how many avatars to show before overflow. + +
+
+ max=2: + +
+
+ max=3: + +
+
+ max=5: + +
+
+
+ + + + Enable user popout on avatar click. + +
+ +
+
+ + + • users - Array of User objects (or null for empty slots) + • max - Maximum avatars to show (default 10) + • count - Override count for "+N" display + • size - 16, 24, 32, or 56 (default 24) + • guildId - Guild context for avatars + • showUserPopout - Enable user popout on click + • showDefaultAvatarsForNullUsers - Show default avatar for null + • renderUser - Custom render function for each user + • renderMoreUsers - Custom render for overflow indicator + • hideMoreUsers - Hide the "+N" overflow count + +
+ ); +} diff --git a/src/equicordplugins/components.dev/constants.ts b/src/equicordplugins/components.dev/constants.ts index 3d9bcf18d..85e9348ca 100644 --- a/src/equicordplugins/components.dev/constants.ts +++ b/src/equicordplugins/components.dev/constants.ts @@ -165,3 +165,22 @@ export type PopoverSize = typeof PopoverSizes[number]; export const SliderOrientations = ["horizontal", "vertical"] as const; export type SliderOrientation = typeof SliderOrientations[number]; + +export const ToastType = { + MESSAGE: "message", + SUCCESS: "success", + FAILURE: "failure", + CUSTOM: "custom", + CLIP: "clip", + LINK: "link", + FORWARD: "forward", + INVITE: "invite", + BOOKMARK: "bookmark", + CLOCK: "clock", + AI: "ai", +} as const; + +export const ToastPosition = { + TOP: 0, + BOTTOM: 1, +} as const; diff --git a/src/equicordplugins/components.dev/types/avatar.d.ts b/src/equicordplugins/components.dev/types/avatar.d.ts index 7a19d364b..e4ff7e5e7 100644 --- a/src/equicordplugins/components.dev/types/avatar.d.ts +++ b/src/equicordplugins/components.dev/types/avatar.d.ts @@ -20,15 +20,24 @@ export interface AvatarProps { size?: AvatarSize; status?: AvatarStatus; statusColor?: string; + statusBackdropColor?: string; isMobile?: boolean; isTyping?: boolean; isSpeaking?: boolean; + isLatched?: boolean; + voiceDb?: number; + speakingStylesConfig?: Record; statusTooltip?: boolean; statusTooltipDelay?: number; - avatarDecoration?: string; + typingIndicatorRef?: React.Ref; + avatarContentRef?: React.Ref; + CutoutIcon?: React.ComponentType; + avatarTooltipAsset?: React.ReactNode; + avatarTooltipText?: string; + avatarTooltipTitle?: string; "aria-hidden"?: boolean; "aria-label"?: string; imageClassName?: string; className?: string; - onClick?: (e: React.MouseEvent) => void; + ref?: React.Ref; } diff --git a/src/equicordplugins/components.dev/types/badge.d.ts b/src/equicordplugins/components.dev/types/badge.d.ts index 3b828d935..7e23861d0 100644 --- a/src/equicordplugins/components.dev/types/badge.d.ts +++ b/src/equicordplugins/components.dev/types/badge.d.ts @@ -13,12 +13,12 @@ export interface BadgeShapesType { export interface NumberBadgeProps { count: number; - maxDigits?: number; color?: string; disableColor?: boolean; shape?: string; className?: string; style?: React.CSSProperties; + renderBadgeCount?: (count: number) => string; } export interface TextBadgeProps { diff --git a/src/equicordplugins/components.dev/types/checkbox.d.ts b/src/equicordplugins/components.dev/types/checkbox.d.ts index fbc0d31c7..065ff1803 100644 --- a/src/equicordplugins/components.dev/types/checkbox.d.ts +++ b/src/equicordplugins/components.dev/types/checkbox.d.ts @@ -18,6 +18,8 @@ export interface ManaCheckboxProps { leadingIcon?: React.ComponentType; labelType?: CheckboxLabelType; usageVariant?: CheckboxUsageVariant; + value?: string | number; + labeledBy?: string; } export interface CheckboxGroupOption { diff --git a/src/equicordplugins/components.dev/types/focuslock.d.ts b/src/equicordplugins/components.dev/types/focuslock.d.ts new file mode 100644 index 000000000..b1d476187 --- /dev/null +++ b/src/equicordplugins/components.dev/types/focuslock.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 FocusLockProps { + containerRef: React.RefObject; + keyboardModeEnabled?: boolean; + children?: React.ReactNode; +} diff --git a/src/equicordplugins/components.dev/types/index.ts b/src/equicordplugins/components.dev/types/index.d.ts similarity index 91% rename from src/equicordplugins/components.dev/types/index.ts rename to src/equicordplugins/components.dev/types/index.d.ts index 3277087a3..0fb9bbe57 100644 --- a/src/equicordplugins/components.dev/types/index.ts +++ b/src/equicordplugins/components.dev/types/index.d.ts @@ -15,12 +15,14 @@ export * from "./checkbox"; export * from "./chip"; export * from "./clickable"; export * from "./colorpicker"; +export * from "./focuslock"; export * from "./guildicon.d"; export * from "./heading"; export * from "./input"; export * from "./modal"; export * from "./notice"; export * from "./paginator"; +export * from "./popout"; export * from "./progress"; export * from "./radio"; export * from "./scroller"; @@ -33,3 +35,4 @@ export * from "./tabbar"; export * from "./timestamp"; export * from "./toast"; export * from "./tooltip"; +export * from "./usersummary"; diff --git a/src/equicordplugins/components.dev/types/input.d.ts b/src/equicordplugins/components.dev/types/input.d.ts index 221660b69..6c3882542 100644 --- a/src/equicordplugins/components.dev/types/input.d.ts +++ b/src/equicordplugins/components.dev/types/input.d.ts @@ -63,11 +63,14 @@ export interface ManaTextAreaProps { disabled?: boolean; readOnly?: boolean; rows?: number; + autoFocus?: boolean; autosize?: boolean; maxLength?: number; minLength?: number; showCharacterCount?: boolean; + showRemainingCharacterCount?: boolean; error?: string | boolean; + defaultDirty?: boolean; fullWidth?: boolean; focusProps?: Record; inputRef?: React.Ref; diff --git a/src/equicordplugins/components.dev/types/popout.d.ts b/src/equicordplugins/components.dev/types/popout.d.ts new file mode 100644 index 000000000..8f9db0a84 --- /dev/null +++ b/src/equicordplugins/components.dev/types/popout.d.ts @@ -0,0 +1,72 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export type PopoutAnimationType = "1" | "2" | "3" | "4"; + +export type PopoutPosition = "top" | "bottom" | "left" | "right" | "center" | "window_center"; + +export type PopoutAlign = "left" | "right" | "center" | "top" | "bottom"; + +export interface PopoutChildrenProps { + "aria-controls": string; + "aria-expanded": boolean; + onClick: (event: React.MouseEvent) => void; + onKeyDown: (event: React.KeyboardEvent) => void; + onMouseDown: (event: React.MouseEvent) => void; +} + +export interface PopoutChildrenData { + isShown: boolean; + position: PopoutPosition; +} + +export interface PopoutRenderProps { + closePopout: () => void; + isPositioned: boolean; + nudge: number; + position: PopoutPosition; + setPopoutRef: (ref: any) => void; + updatePosition: () => void; +} + +export interface PopoutProps { + children: (props: PopoutChildrenProps, data: PopoutChildrenData) => React.ReactNode; + renderPopout: (props: PopoutRenderProps) => React.ReactNode; + shouldShow?: boolean; + targetElementRef?: React.RefObject; + onRequestOpen?: () => void; + onRequestClose?: () => void; + align?: PopoutAlign; + animation?: PopoutAnimationType; + animationPosition?: PopoutPosition; + autoInvert?: boolean; + nudgeAlignIntoViewport?: boolean; + position?: PopoutPosition; + positionKey?: string; + popoutKey?: string; + spacing?: number; + preload?: boolean; + fixed?: boolean; + useRawTargetDimensions?: boolean; + onShiftClick?: () => void; + disablePointerEvents?: boolean; + ignoreModalClicks?: boolean; + scrollBehavior?: string; + useMouseEnter?: boolean; + layerContext?: any; + clickTrap?: boolean; +} + +export interface PopoutAnimationEnum { + NONE: "1"; + TRANSLATE: "2"; + SCALE: "3"; + FADE: "4"; +} + +export type PopoutComponent = React.ComponentType & { + Animation: PopoutAnimationEnum; +}; diff --git a/src/equicordplugins/components.dev/types/radio.d.ts b/src/equicordplugins/components.dev/types/radio.d.ts index 99a0b3f08..26f14c030 100644 --- a/src/equicordplugins/components.dev/types/radio.d.ts +++ b/src/equicordplugins/components.dev/types/radio.d.ts @@ -17,6 +17,7 @@ export interface ManaBaseRadioGroupProps { value?: string | number; onChange?: (value: string | number) => void; disabled?: boolean; + label?: string; "aria-labelledby"?: string; } diff --git a/src/equicordplugins/components.dev/types/select.d.ts b/src/equicordplugins/components.dev/types/select.d.ts index 34c09ac8f..d99e2b3da 100644 --- a/src/equicordplugins/components.dev/types/select.d.ts +++ b/src/equicordplugins/components.dev/types/select.d.ts @@ -25,15 +25,22 @@ export interface ManaSelectProps { shouldFocusWrap?: boolean; maxOptionsVisible?: number; wrapTags?: boolean; + formatOption?: (option: ManaSelectOption) => React.ReactNode; + name?: string; + form?: string; + autoComplete?: string; + label?: string; + required?: boolean; } -export interface ManaComboboxProps { +export interface ManaComboboxProps { placeholder?: string; - value?: string | string[]; - onChange?: (value: string | string[]) => void; + value?: T | T[]; + onChange?: (value: T | T[]) => void; multiSelect?: boolean; autoFocus?: boolean; maxVisibleItems?: number; + itemToString?: (item: T) => string; emptyStateText?: string; emptyStateHeader?: string; onQueryChange?: (query: string) => void; diff --git a/src/equicordplugins/components.dev/types/toast.d.ts b/src/equicordplugins/components.dev/types/toast.d.ts index 27527ca90..398047739 100644 --- a/src/equicordplugins/components.dev/types/toast.d.ts +++ b/src/equicordplugins/components.dev/types/toast.d.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export type ToastTypeValue = "message" | "success" | "failure" | "custom" | "clip" | "link" | "forward" | "bookmark" | "clock"; +export type ToastTypeValue = "message" | "success" | "failure" | "custom" | "clip" | "link" | "forward" | "invite" | "bookmark" | "clock" | "ai"; export type ToastPositionValue = 0 | 1; diff --git a/src/equicordplugins/components.dev/types/tooltip.d.ts b/src/equicordplugins/components.dev/types/tooltip.d.ts index ed7a372f5..1b755535a 100644 --- a/src/equicordplugins/components.dev/types/tooltip.d.ts +++ b/src/equicordplugins/components.dev/types/tooltip.d.ts @@ -9,14 +9,29 @@ import type { PopoverSize, TooltipAlign, TooltipColor, TooltipPosition } from ". export type { PopoverSize, TooltipAlign, TooltipColor, TooltipPosition }; export interface ManaTooltipProps { - text: string; + text: string | (() => React.ReactNode); position?: TooltipPosition; align?: TooltipAlign; color?: TooltipColor; spacing?: number; hideOnClick?: boolean; delay?: number; - children: (props: any) => React.ReactNode; + forceOpen?: boolean; + shouldShow?: boolean; + allowOverflow?: boolean; + overflowOnly?: boolean; + clickableOnMobile?: boolean; + disableTooltipPointerEvents?: boolean; + targetElementRef?: React.RefObject; + tooltipClassName?: string; + tooltipStyle?: React.CSSProperties; + tooltipContentClassName?: string; + tooltipPointerClassName?: string; + onTooltipShow?: () => void; + onTooltipHide?: () => void; + onAnimationRest?: () => void; + "aria-label"?: string | false; + children: (props: Record) => React.ReactNode; } export interface ManaRichTooltipProps { @@ -29,8 +44,13 @@ export interface ManaRichTooltipProps { position?: TooltipPosition; align?: TooltipAlign; spacing?: number; + caretConfig?: { align?: "start" | "center" | "end"; }; + layerContext?: any; + targetElementRef?: React.RefObject; + anchorRef?: React.RefObject; + positionKey?: string; ariaHidden?: boolean; - children: React.ReactNode | ((props: any) => React.ReactNode); + children: React.ReactNode | ((props: Record) => React.ReactNode); } export interface PopoverAction { diff --git a/src/equicordplugins/components.dev/types/usersummary.d.ts b/src/equicordplugins/components.dev/types/usersummary.d.ts new file mode 100644 index 000000000..366e7a564 --- /dev/null +++ b/src/equicordplugins/components.dev/types/usersummary.d.ts @@ -0,0 +1,28 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import type { User } from "@vencord/discord-types"; + +export type UserSummarySize = 16 | 24 | 32 | 56; + +export interface UserSummaryItemProps { + users: (User | null)[]; + max?: number; + count?: number; + size?: UserSummarySize; + guildId?: string; + showUserPopout?: boolean; + showDefaultAvatarsForNullUsers?: boolean; + renderUser?: (user: User | null, isLast: boolean, index: number) => React.ReactNode; + renderMoreUsers?: (text: string, count: number) => React.ReactNode; + renderIcon?: boolean; + renderLeadingIcon?: (className: string) => React.ReactNode; + dimEmptyUsers?: boolean; + hideMoreUsers?: boolean; + useFallbackUserForPopout?: boolean; + extraDetail?: React.ReactNode; + className?: string; +}