feat(Components): fix skeleton, add menu, add errorboundary, add alert, add animations, add codeblock (#626)

* Components: fix skeleton

* Components: add CodeBlock

* Components: add Menu

* Components: add Alert

* Components: fix Menu, add ErrorBoundary

* Components: add Animations
This commit is contained in:
prism 2026-01-03 18:07:27 +01:00 • committed by GitHub
parent a04f8131d3
commit 2301befaba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1290 additions and 22 deletions

View file

@ -6,12 +6,16 @@
import "../styles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Heading } from "@components/Heading";
import { SettingsTab, wrapTab } from "@components/settings";
import { Margins } from "@utils/margins";
import { TabBar, useState } from "@webpack/common";
import { TabBar, useMemo, useState } from "@webpack/common";
import { SearchBar } from ".";
import AccordionTab from "./tabs/AccordionTab";
import AlertTab from "./tabs/AlertTab";
import AnimationsTab from "./tabs/AnimationsTab";
import AnchorTab from "./tabs/AnchorTab";
import AvatarTab from "./tabs/AvatarTab";
import BadgeTab from "./tabs/BadgeTab";
@ -22,14 +26,17 @@ import CheckboxGroupTab from "./tabs/CheckboxGroupTab";
import CheckboxTab from "./tabs/CheckboxTab";
import ChipTab from "./tabs/ChipTab";
import ClickableTab from "./tabs/ClickableTab";
import CodeBlockTab from "./tabs/CodeBlockTab";
import ColorPickerTab from "./tabs/ColorPickerTab";
import ComboboxTab from "./tabs/ComboboxTab";
import DividerTab from "./tabs/DividerTab";
import ErrorBoundaryTab from "./tabs/ErrorBoundaryTab";
import FocusLockTab from "./tabs/FocusLockTab";
import GuildIconTab from "./tabs/GuildIconTab";
import HeadingTab from "./tabs/HeadingTab";
import LayerModalTab from "./tabs/LayerModalTab";
import ListboxTab from "./tabs/ListboxTab";
import MenuTab from "./tabs/MenuTab";
import ModalTab from "./tabs/ModalTab";
import NoticeTab from "./tabs/NoticeTab";
import PaginatorTab from "./tabs/PaginatorTab";
@ -57,6 +64,8 @@ import UserSummaryItemTab from "./tabs/UserSummaryItemTab";
const TABS = [
{ id: "accordion", label: "Accordion" },
{ id: "alert", label: "Alert" },
{ id: "animations", label: "Animations" },
{ id: "anchor", label: "Anchor" },
{ id: "avatar", label: "Avatar" },
{ id: "badge", label: "Badge" },
@ -67,14 +76,17 @@ const TABS = [
{ id: "checkbox", label: "Checkbox" },
{ id: "checkboxgroup", label: "CheckboxGroup" },
{ id: "clickable", label: "Clickable" },
{ id: "codeblock", label: "CodeBlock" },
{ id: "colorpicker", label: "ColorPicker" },
{ id: "combobox", label: "Combobox" },
{ id: "divider", label: "Divider" },
{ id: "errorboundary", label: "ErrorBoundary" },
{ id: "focuslock", label: "FocusLock" },
{ id: "guildicon", label: "GuildIcon" },
{ id: "heading", label: "Heading" },
{ id: "layermodal", label: "LayerModal" },
{ id: "listbox", label: "Listbox" },
{ id: "menu", label: "Menu" },
{ id: "modal", label: "Modal" },
{ id: "notice", label: "Notice" },
{ id: "paginator", label: "Paginator" },
@ -106,6 +118,8 @@ type TabId = typeof TABS[number]["id"];
const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
accordion: AccordionTab,
alert: AlertTab,
animations: AnimationsTab,
anchor: AnchorTab,
avatar: AvatarTab,
badge: BadgeTab,
@ -116,14 +130,17 @@ const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
checkbox: CheckboxTab,
checkboxgroup: CheckboxGroupTab,
clickable: ClickableTab,
codeblock: CodeBlockTab,
colorpicker: ColorPickerTab,
combobox: ComboboxTab,
divider: DividerTab,
errorboundary: ErrorBoundaryTab,
focuslock: FocusLockTab,
guildicon: GuildIconTab,
heading: HeadingTab,
layermodal: LayerModalTab,
listbox: ListboxTab,
menu: MenuTab,
modal: ModalTab,
notice: NoticeTab,
paginator: PaginatorTab,
@ -152,11 +169,26 @@ const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
function ComponentsTab() {
const [currentTab, setCurrentTab] = useState<TabId>("avatar");
const [search, setSearch] = useState("");
const TabComponent = TAB_COMPONENTS[currentTab];
const filteredTabs = useMemo(() => {
if (!search.trim()) return TABS;
const query = search.toLowerCase();
return TABS.filter(tab => tab.label.toLowerCase().includes(query));
}, [search]);
return (
<SettingsTab>
<Heading className={Margins.bottom16}>Components</Heading>
<div className="vc-compfinder-search">
<SearchBar
query={search}
onChange={setSearch}
onClear={() => setSearch("")}
placeholder="Search components..."
/>
</div>
<TabBar
type="top"
look="brand"
@ -164,14 +196,16 @@ function ComponentsTab() {
onItemSelect={setCurrentTab}
className="vc-compfinder-tabbar"
>
{TABS.map(tab => (
{filteredTabs.map(tab => (
<TabBar.Item key={tab.id} id={tab.id} className="vc-compfinder-tab">
{tab.label}
</TabBar.Item>
))}
</TabBar>
<div className="vc-compfinder-content">
<TabComponent />
<ErrorBoundary message={`Failed to render ${currentTab} tab`}>
<TabComponent />
</ErrorBoundary>
</div>
</SettingsTab>
);

View file

@ -10,7 +10,7 @@ import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { TooltipContainer } from "@components/TooltipContainer";
import { proxyLazy } from "@utils/lazy";
import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findLazy, mapMangledModuleLazy } from "@webpack";
import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findLazy, mapMangledModuleLazy, waitFor, wreq } from "@webpack";
import {
GuildStore,
ListScrollerAuto,
@ -31,6 +31,7 @@ import {
import { ToastPosition, ToastType } from "../constants";
import type {
AccordionProps,
AlertsType,
AnchorProps,
AvatarProps,
BadgeShapesType,
@ -41,14 +42,17 @@ import type {
ColorPickerWithSwatchesProps,
ColorSwatchProps,
ConfirmModalProps,
ContextMenuApiType,
CustomColorButtonProps,
DefaultColorButtonProps,
DiscordHeadingProps,
DiscordTextProps,
ErrorBoundaryProps,
ExpressiveModalProps,
FocusLockProps,
GuildIconProps,
IconBadgeProps,
LocalErrorBoundaryProps,
ManaBaseRadioGroupProps,
ManaButtonProps,
ManaCalendarProps,
@ -65,6 +69,7 @@ import type {
ManaTextButtonProps,
ManaTextInputProps,
ManaTooltipProps,
MenuType,
ModalProps,
ModalRenderFn,
NoticeProps,
@ -74,6 +79,7 @@ import type {
PopoutComponent,
ProgressBarProps,
SearchBarProps,
SimpleErrorBoundaryProps,
SkeletonProps,
SliderProps,
SpinnerComponent,
@ -141,7 +147,7 @@ export const SearchBar = findComponentByCodeLazy("#{intl::SEARCH}", "clearable",
export const Paginator = findComponentByCodeLazy("#{intl::BACK}", "#{intl::NEXT}", "renderPageWrapper") as React.ComponentType<PaginatorProps>;
export const Notice = findComponentByCodeLazy("messageType", "iconDiv", "actionContainer") as React.ComponentType<NoticeProps>;
export const Chip = findComponentByCodeLazy('variant:"eyebrow"', "chip,") as React.ComponentType<ChipProps>;
export const Skeleton = findComponentByCodeLazy("productSkeletonCardContainer", "skipPulseAnimation") as React.ComponentType<SkeletonProps>;
export const Skeleton = findComponentByCodeLazy("withHeader:t=!0,size:") as React.ComponentType<SkeletonProps>;
export const Accordion = findComponentByCodeLazy("accordionContainer", "onExpandedChange", "defaultExpanded") as React.ComponentType<AccordionProps>;
export const Timestamp = findComponentByCodeLazy("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}", "isVisibleOnlyOnHover") as React.ComponentType<TimestampProps>;
export const GuildIcon = findComponentByCodeLazy("Masks.CLAN_ICON", "guildIconImage") as React.ComponentType<GuildIconProps>;
@ -150,10 +156,20 @@ export const Popout: PopoutComponent = proxyLazy(() => findLazy(m => m?.y?.Anima
export const FocusLock = findComponentByCodeLazy(".containerRef,{keyboardModeEnabled:") as React.ComponentType<FocusLockProps>;
export const UserSummaryItem = findComponentByCodeLazy("popoutUserId") as React.ComponentType<UserSummaryItemProps>;
export const Animations = mapMangledModuleLazy(".assign({colorNames:", {
Transition: filters.componentByCode('["items","children"]', ",null,"),
animated: filters.byProps("div", "text")
});
export const Animations = findByPropsLazy("useSpring", "animated", "useTransition");
export const SpringConfigs = proxyLazy(() => Animations.config) as {
default: object;
gentle: object;
wobbly: object;
stiff: object;
slow: object;
molasses: object;
};
export const useSpring = proxyLazy(() => Animations.useSpring) as (config: object) => object;
export const useTransition = proxyLazy(() => Animations.useTransition) as <T>(items: T[], config: object) => (callback: (style: object, item: T) => React.ReactNode) => React.ReactNode[];
export const useTrail = proxyLazy(() => Animations.useTrail) as (count: number, config: object) => object[];
const ColorPickerModule = findLazy(m => {
const values = Object.values(m);
@ -263,3 +279,78 @@ export function DiscordText(props: DiscordTextProps) {
export function Divider({ className, style, ...restProps }: React.ComponentPropsWithoutRef<"hr">) {
return <hr className={`vc-divider${className ? ` ${className}` : ""}`} style={style} {...restProps} />;
}
const CodeContainerClasses = findByPropsLazy("markup", "codeContainer");
export function InlineCode({ children }: { children: React.ReactNode; }) {
return (
<span className={CodeContainerClasses.markup}>
<code className="inline">{children}</code>
</span>
);
}
const AlertsModule = findByPropsLazy("show", "close", "confirm");
export const Alerts: AlertsType = {
show: options => AlertsModule.show(options),
close: () => AlertsModule.close(),
confirm: options => AlertsModule.confirm(options),
};
const MenuComponents: Record<string, React.ComponentType<any>> = {};
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
const exports = wreq(id);
for (const key in exports) {
try {
const value = exports[key];
if (typeof value === "function" && value.name?.startsWith("Menu")) {
MenuComponents[value.name] = value;
}
} catch { }
}
});
waitFor(filters.componentByCode('path:["empty"]'), m => { MenuComponents.Menu = m; });
waitFor(filters.componentByCode("sliderContainer", "slider", "handleSize:16", "=100"), m => { MenuComponents.MenuSliderControl = m; });
waitFor(filters.componentByCode(".SEARCH)", ".focus()", "query:"), m => { MenuComponents.MenuSearchControl = m; });
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
const exports = wreq(id);
for (const key in exports) {
try {
const value = exports[key];
if (typeof value !== "function") continue;
const str = value.toString();
if (str.length === 26 && str.endsWith("(e){return null}") && value.name === "l") {
Object.defineProperty(value, "name", { value: "MenuSwitchItem" });
MenuComponents.MenuSwitchItem = value;
break;
}
} catch { }
}
});
export const Menu: MenuType = proxyLazy(() => ({
Menu: MenuComponents.Menu,
MenuItem: MenuComponents.MenuItem,
MenuCheckboxItem: MenuComponents.MenuCheckboxItem,
MenuRadioItem: MenuComponents.MenuRadioItem,
MenuSwitchItem: MenuComponents.MenuSwitchItem,
MenuGroup: MenuComponents.MenuGroup,
MenuSeparator: MenuComponents.MenuSeparator,
MenuControlItem: MenuComponents.MenuControlItem,
MenuSliderControl: MenuComponents.MenuSliderControl,
MenuSearchControl: MenuComponents.MenuSearchControl,
}));
export const ContextMenuApi: ContextMenuApiType = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', {
closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"),
openContextMenu: filters.byCode("renderLazy:"),
openContextMenuLazy: e => typeof e === "function" && e.toString().length < 100
});
export const ErrorBoundary = findComponentByCodeLazy("this.resetErrorBoundary", "onReset", "FallbackComponent") as React.ComponentType<ErrorBoundaryProps>;
export const SimpleErrorBoundary = findComponentByCodeLazy("getDerivedStateFromError", "this.props.fallback") as React.ComponentType<SimpleErrorBoundaryProps>;
export const LocalErrorBoundary = findComponentByCodeLazy("LocalErrorBoundary", "text-feedback-critical") as React.ComponentType<LocalErrorBoundaryProps>;

View file

@ -0,0 +1,136 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Alerts, Button, Paragraph, showToast, ToastType } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function AlertTab() {
const showBasicAlert = () => {
Alerts.show({
title: "Basic Alert",
body: "This is a basic alert dialog with just an OK button.",
});
};
const showConfirmAlert = () => {
Alerts.show({
title: "Confirm Action",
body: "Are you sure you want to proceed with this action?",
confirmText: "Yes, proceed",
cancelText: "Cancel",
onConfirm: () => showToast("Confirmed!", ToastType.SUCCESS),
onCancel: () => showToast("Cancelled", ToastType.MESSAGE),
});
};
const showDangerAlert = () => {
Alerts.show({
title: "Delete Item",
body: "This action cannot be undone. Are you sure you want to delete this item?",
confirmText: "Delete",
confirmVariant: "critical-primary",
cancelText: "Keep it",
onConfirm: () => showToast("Deleted!", ToastType.SUCCESS),
});
};
const showSecondaryConfirmAlert = () => {
Alerts.show({
title: "Multiple Actions",
body: "This alert has a secondary confirm action.",
confirmText: "Primary Action",
secondaryConfirmText: "Secondary Action",
cancelText: "Cancel",
onConfirm: () => showToast("Primary action!", ToastType.SUCCESS),
onConfirmSecondary: () => showToast("Secondary action!", ToastType.MESSAGE),
});
};
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Alert">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Simple alert with just a title and body.
</Paragraph>
<Button onClick={showBasicAlert}>
Show Basic Alert
</Button>
</SectionWrapper>
<SectionWrapper title="Confirm Alert">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Alert with confirm and cancel buttons and callbacks.
</Paragraph>
<Button onClick={showConfirmAlert}>
Show Confirm Alert
</Button>
</SectionWrapper>
<SectionWrapper title="Danger Alert">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Alert with red danger button for destructive actions.
</Paragraph>
<Button variant="dangerPrimary" onClick={showDangerAlert}>
Show Danger Alert
</Button>
</SectionWrapper>
<SectionWrapper title="Secondary Confirm">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Alert with multiple confirm actions.
</Paragraph>
<Button variant="secondary" onClick={showSecondaryConfirmAlert}>
Show Multi-Action Alert
</Button>
</SectionWrapper>
<SectionWrapper title="Alerts API">
<Paragraph color="text-muted">
<strong>Alerts.show(options)</strong> - Show an alert dialog
</Paragraph>
<Paragraph color="text-muted">
<strong>Alerts.close()</strong> - Close the current alert
</Paragraph>
<Paragraph color="text-muted">
<strong>Alerts.confirm(options)</strong> - Show a confirm dialog
</Paragraph>
</SectionWrapper>
<SectionWrapper title="Options">
<Paragraph color="text-muted">
<strong>title</strong> - Alert title text
</Paragraph>
<Paragraph color="text-muted">
<strong>body</strong> - Alert body content (string or ReactNode)
</Paragraph>
<Paragraph color="text-muted">
<strong>confirmText</strong> - Text for confirm button
</Paragraph>
<Paragraph color="text-muted">
<strong>cancelText</strong> - Text for cancel button (shows cancel if set)
</Paragraph>
<Paragraph color="text-muted">
<strong>confirmVariant</strong> - "primary", "critical-primary", or "expressive"
</Paragraph>
<Paragraph color="text-muted">
<strong>secondaryConfirmText</strong> - Text for secondary action button
</Paragraph>
<Paragraph color="text-muted">
<strong>onConfirm</strong> - Callback when confirm clicked
</Paragraph>
<Paragraph color="text-muted">
<strong>onConfirmSecondary</strong> - Callback for secondary action
</Paragraph>
<Paragraph color="text-muted">
<strong>onCancel</strong> - Callback when cancel clicked
</Paragraph>
<Paragraph color="text-muted">
<strong>onCloseCallback</strong> - Callback when alert closes
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,232 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Animations, ManaButton, Paragraph, SpringConfigs, useSpring, useState, useTrail, useTransition } from "..";
import { SectionWrapper } from "../SectionWrapper";
const { animated } = Animations;
const TRAIL_ITEMS = ["Discord", "uses", "react-spring", "for", "animations"];
function SpringDemo() {
const [toggled, setToggled] = useState(false);
const [config, setConfig] = useState<keyof typeof SpringConfigs>("default");
const spring = useSpring({
opacity: toggled ? 1 : 0.3,
transform: toggled ? "scale(1) rotate(0deg)" : "scale(0.8) rotate(-10deg)",
backgroundColor: toggled ? "var(--brand-500)" : "var(--background-tertiary)",
config: SpringConfigs[config]
});
return (
<SectionWrapper title="useSpring">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Animate values with spring physics. Click the box or change config.
</Paragraph>
<div style={{ display: "flex", gap: 8, marginBottom: 16, flexWrap: "wrap" }}>
{(Object.keys(SpringConfigs) as (keyof typeof SpringConfigs)[]).map(c => (
<ManaButton
key={c}
variant={config === c ? "primary" : "secondary"}
size="sm"
text={c}
onClick={() => setConfig(c)}
/>
))}
</div>
<animated.div
onClick={() => setToggled(!toggled)}
style={{
...spring,
width: 120,
height: 120,
borderRadius: 12,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<Paragraph color="text-strong">Click me</Paragraph>
</animated.div>
</SectionWrapper>
);
}
function TransitionDemo() {
const [items, setItems] = useState([1, 2, 3]);
const [nextId, setNextId] = useState(4);
const transitions = useTransition(items, {
from: { opacity: 0, transform: "translateX(-20px) scale(0.9)" },
enter: { opacity: 1, transform: "translateX(0px) scale(1)" },
leave: { opacity: 0, transform: "translateX(20px) scale(0.9)" },
keys: item => item,
config: SpringConfigs.gentle
});
const addItem = () => {
setItems([...items, nextId]);
setNextId(nextId + 1);
};
const removeItem = (id: number) => {
setItems(items.filter(i => i !== id));
};
return (
<SectionWrapper title="useTransition">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Animate items entering and leaving a list.
</Paragraph>
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
<ManaButton variant="primary" size="sm" text="Add Item" onClick={addItem} />
<ManaButton
variant="secondary"
size="sm"
text="Clear All"
onClick={() => setItems([])}
disabled={items.length === 0}
/>
</div>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", minHeight: 50 }}>
{transitions((style, item) => (
<animated.div
style={{
...style,
padding: "8px 16px",
background: "var(--background-secondary)",
borderRadius: 8,
cursor: "pointer"
}}
onClick={() => removeItem(item)}
>
<Paragraph>Item {item}</Paragraph>
</animated.div>
))}
</div>
</SectionWrapper>
);
}
function TrailDemo() {
const [show, setShow] = useState(true);
const trail = useTrail(TRAIL_ITEMS.length, {
opacity: show ? 1 : 0,
transform: show ? "translateY(0px)" : "translateY(20px)",
config: SpringConfigs.gentle
}) as object[];
return (
<SectionWrapper title="useTrail">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Staggered animations for multiple items. Each item animates slightly after the previous.
</Paragraph>
<div style={{ marginBottom: 16 }}>
<ManaButton
variant="secondary"
size="sm"
text={show ? "Hide" : "Show"}
onClick={() => setShow(!show)}
/>
</div>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", minHeight: 40 }}>
{trail.map((style, i) => (
<animated.div
key={i}
style={{
...style,
padding: "8px 12px",
background: "var(--brand-500)",
borderRadius: 6
}}
>
<Paragraph color="text-strong">{TRAIL_ITEMS[i]}</Paragraph>
</animated.div>
))}
</div>
</SectionWrapper>
);
}
function AnimatedElementsDemo() {
const [hover, setHover] = useState(false);
const hoverSpring = useSpring({
scale: hover ? 1.05 : 1,
boxShadow: hover
? "0 8px 24px rgba(0,0,0,0.3)"
: "0 2px 8px rgba(0,0,0,0.1)",
config: SpringConfigs.wobbly
});
return (
<SectionWrapper title="animated Elements">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Use animated.div, animated.span, animated.svg, etc. for spring-animated elements.
</Paragraph>
<div style={{ display: "flex", gap: 16, flexWrap: "wrap" }}>
<animated.div
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
...hoverSpring,
width: 100,
height: 100,
background: "var(--background-secondary)",
borderRadius: 12,
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer"
}}
>
<Paragraph color="text-muted" style={{ fontSize: 12 }}>Hover me</Paragraph>
</animated.div>
</div>
</SectionWrapper>
);
}
export default function AnimationsTab() {
return (
<div className="vc-compfinder-section">
<SpringDemo />
<TransitionDemo />
<TrailDemo />
<AnimatedElementsDemo />
<SectionWrapper title="Spring Configs">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Preset spring configurations for different animation feels.
</Paragraph>
<Paragraph color="text-muted">• default - Standard spring</Paragraph>
<Paragraph color="text-muted">• gentle - Soft, slow spring</Paragraph>
<Paragraph color="text-muted">• wobbly - Bouncy, playful spring</Paragraph>
<Paragraph color="text-muted">• stiff - Quick, snappy spring</Paragraph>
<Paragraph color="text-muted">• slow - Very slow spring</Paragraph>
<Paragraph color="text-muted">• molasses - Extremely slow spring</Paragraph>
</SectionWrapper>
<SectionWrapper title="Available Hooks">
<Paragraph color="text-muted">• useSpring - Single spring animation</Paragraph>
<Paragraph color="text-muted">• useSprings - Multiple springs</Paragraph>
<Paragraph color="text-muted">• useTransition - Enter/leave animations</Paragraph>
<Paragraph color="text-muted">• useTrail - Staggered animations</Paragraph>
<Paragraph color="text-muted">• useChain - Sequence animations</Paragraph>
</SectionWrapper>
<SectionWrapper title="Available Components">
<Paragraph color="text-muted">• Transition - Declarative transitions</Paragraph>
<Paragraph color="text-muted">• Spring - Declarative spring</Paragraph>
<Paragraph color="text-muted">• Trail - Declarative trail</Paragraph>
<Paragraph color="text-muted">• animated.* - Animated HTML/SVG elements</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,142 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findByPropsLazy } from "@webpack";
import { Parser } from "@webpack/common";
import { Paragraph } from "..";
import { SectionWrapper } from "../SectionWrapper";
const MarkupClasses = findByPropsLazy("markup", "codeContainer");
interface CodeBlockProps {
content: string;
language?: string;
}
function CodeBlock({ content, language = "" }: CodeBlockProps) {
const codeBlock = `\`\`\`${language}\n${content}\n\`\`\``;
return (
<div className={MarkupClasses.markup}>
{Parser.parse(codeBlock)}
</div>
);
}
function InlineCode({ children }: { children: React.ReactNode; }) {
return (
<span className={MarkupClasses.markup}>
<code className="inline">{children}</code>
</span>
);
}
const JS_EXAMPLE = `function greet(name) {
return \`Hello, \${name}!\`;
}
console.log(greet("World"));`;
const TS_EXAMPLE = `interface User {
id: string;
name: string;
email?: string;
}
const getUser = (id: string): User => {
return { id, name: "John" };
};`;
const CSS_EXAMPLE = `.container {
display: flex;
gap: 8px;
padding: 16px;
background: var(--background-secondary);
border-radius: 8px;
}`;
const PYTHON_EXAMPLE = `def fibonacci(n: int) -> list[int]:
if n <= 0:
return []
sequence = [0, 1]
while len(sequence) < n:
sequence.append(sequence[-1] + sequence[-2])
return sequence[:n]
print(fibonacci(10))`;
const JSON_EXAMPLE = `{
"name": "equicord",
"version": "1.0.0",
"plugins": ["example"],
"settings": {
"enabled": true,
"theme": "dark"
}
}`;
const BASH_EXAMPLE = `#!/bin/bash
for file in *.ts; do
echo "Processing $file"
node "$file"
done`;
const PLAIN_EXAMPLE = `This is a plain code block
without syntax highlighting.
It preserves whitespace and formatting.`;
export default function CodeBlockTab() {
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Inline Code">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Use <InlineCode>InlineCode</InlineCode> component for inline code snippets.
</Paragraph>
<Paragraph>I really like <InlineCode>cats</InlineCode> so yeah</Paragraph>
<Paragraph>Use <InlineCode>console.log()</InlineCode> to debug your code.</Paragraph>
<Paragraph>
Combine <InlineCode>const</InlineCode> with <InlineCode>let</InlineCode> and
use <InlineCode>===</InlineCode> for strict equality.
</Paragraph>
</SectionWrapper>
<SectionWrapper title="JavaScript">
<CodeBlock content={JS_EXAMPLE} language="js" />
</SectionWrapper>
<SectionWrapper title="TypeScript">
<CodeBlock content={TS_EXAMPLE} language="ts" />
</SectionWrapper>
<SectionWrapper title="CSS">
<CodeBlock content={CSS_EXAMPLE} language="css" />
</SectionWrapper>
<SectionWrapper title="Python">
<CodeBlock content={PYTHON_EXAMPLE} language="py" />
</SectionWrapper>
<SectionWrapper title="JSON">
<CodeBlock content={JSON_EXAMPLE} language="json" />
</SectionWrapper>
<SectionWrapper title="Bash">
<CodeBlock content={BASH_EXAMPLE} language="bash" />
</SectionWrapper>
<SectionWrapper title="Plain Code Block">
<CodeBlock content={PLAIN_EXAMPLE} />
</SectionWrapper>
<SectionWrapper title="Usage">
<Paragraph color="text-muted">
Use <InlineCode>Parser.parse()</InlineCode> from <InlineCode>@webpack/common</InlineCode> to
render markdown with code blocks. Wrap in <InlineCode>markup</InlineCode> class for proper styling.
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,134 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button, ErrorBoundary, LocalErrorBoundary, Paragraph, SimpleErrorBoundary, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
function ThrowError(): never {
throw new Error("This is a demo error thrown intentionally!");
}
function ErrorTrigger({ label }: { label: string; }) {
const [shouldThrow, setShouldThrow] = useState(false);
if (shouldThrow) {
return <ThrowError />;
}
return (
<Button variant="dangerPrimary" onClick={() => setShouldThrow(true)}>
{label}
</Button>
);
}
function CustomFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void; }) {
return (
<div style={{ padding: 16, background: "var(--background-secondary)", borderRadius: 8 }}>
<Paragraph color="text-danger" style={{ marginBottom: 8 }}>
<strong>Custom Fallback UI</strong>
</Paragraph>
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Error: {error.message}
</Paragraph>
<Button onClick={resetErrorBoundary}>
Reset
</Button>
</div>
);
}
export default function ErrorBoundaryTab() {
const [key1, setKey1] = useState(0);
const [key2, setKey2] = useState(0);
const [key3, setKey3] = useState(0);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="ErrorBoundary">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Full-featured error boundary with fallbackRender, FallbackComponent, onError, onReset, and resetKeys props.
</Paragraph>
<div key={key1}>
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<CustomFallback error={error} resetErrorBoundary={resetErrorBoundary} />
)}
onError={(error, info) => console.log("ErrorBoundary caught:", error, info)}
onReset={() => setKey1(k => k + 1)}
>
<ErrorTrigger label="Trigger Error (with fallbackRender)" />
</ErrorBoundary>
</div>
</SectionWrapper>
<SectionWrapper title="SimpleErrorBoundary">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Simple error boundary with just a fallback prop. Shows fallback content when error occurs.
</Paragraph>
<div key={key2}>
<SimpleErrorBoundary
fallback={
<div style={{ padding: 16, background: "var(--background-secondary)", borderRadius: 8 }}>
<Paragraph color="text-danger">Something went wrong!</Paragraph>
<Button onClick={() => setKey2(k => k + 1)} style={{ marginTop: 8 }}>
Reset
</Button>
</div>
}
>
<ErrorTrigger label="Trigger Error (SimpleErrorBoundary)" />
</SimpleErrorBoundary>
</div>
</SectionWrapper>
<SectionWrapper title="LocalErrorBoundary">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Displays an inline error message. Used for non-critical component errors.
</Paragraph>
<div key={key3}>
<LocalErrorBoundary>
<ErrorTrigger label="Trigger Error (LocalErrorBoundary)" />
</LocalErrorBoundary>
<Button variant="secondary" onClick={() => setKey3(k => k + 1)} style={{ marginTop: 8 }}>
Reset LocalErrorBoundary
</Button>
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">
<strong>ErrorBoundary</strong>
</Paragraph>
<Paragraph color="text-muted">
<code>fallback</code> - Static ReactNode to show on error.
</Paragraph>
<Paragraph color="text-muted">
<code>fallbackRender</code> - Function receiving error and resetErrorBoundary.
</Paragraph>
<Paragraph color="text-muted">
<code>FallbackComponent</code> - Component receiving error and resetErrorBoundary props.
</Paragraph>
<Paragraph color="text-muted">
<code>onError</code> - Callback when error is caught (error, errorInfo).
</Paragraph>
<Paragraph color="text-muted">
<code>onReset</code> - Callback when boundary is reset.
</Paragraph>
<Paragraph color="text-muted">
<code>resetKeys</code> - Array of values that trigger reset when changed.
</Paragraph>
</SectionWrapper>
<SectionWrapper title="Usage">
<Paragraph color="text-muted">
Wrap components that may throw errors. Use ErrorBoundary for full control,
SimpleErrorBoundary for basic fallback, or LocalErrorBoundary for inline error display.
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -0,0 +1,224 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button, ContextMenuApi, Menu, Paragraph, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
function DemoMenu({ onClose }: { onClose: () => void; }) {
const [checkboxValue, setCheckboxValue] = useState(false);
const [switchValue, setSwitchValue] = useState(true);
const [radioValue, setRadioValue] = useState("option1");
const [sliderValue, setSliderValue] = useState(50);
return (
<Menu.Menu navId="demo-menu" onClose={onClose}>
<Menu.MenuItem
id="item-1"
label="Basic Item"
action={() => console.log("Basic item clicked")}
/>
<Menu.MenuItem
id="item-2"
label="Item with Icon"
icon={() => (
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
)}
action={() => console.log("Icon item clicked")}
/>
<Menu.MenuItem
id="item-with-hint"
label="Item with Hint"
hint="Ctrl+H"
action={() => console.log("Hint item clicked")}
/>
<Menu.MenuItem
id="item-with-subtext"
label="Item with Subtext"
subtext="Additional description here"
action={() => console.log("Subtext item clicked")}
/>
<Menu.MenuItem
id="item-disabled"
label="Disabled Item"
disabled
/>
<Menu.MenuSeparator />
<Menu.MenuGroup label="Colors">
<Menu.MenuItem id="color-default" label="Default" color="default" action={() => { }} />
<Menu.MenuItem id="color-brand" label="Brand" color="brand" action={() => { }} />
<Menu.MenuItem id="color-danger" label="Danger" color="danger" action={() => { }} />
<Menu.MenuItem id="color-success" label="Success" color="success" action={() => { }} />
<Menu.MenuItem id="color-premium" label="Premium" color="premium" action={() => { }} />
<Menu.MenuItem id="color-premium-gradient" label="Premium Gradient" color="premium-gradient" action={() => { }} />
</Menu.MenuGroup>
<Menu.MenuSeparator />
<Menu.MenuCheckboxItem
id="checkbox-item"
label="Checkbox Item"
checked={checkboxValue}
action={() => setCheckboxValue(v => !v)}
/>
<Menu.MenuCheckboxItem
id="checkbox-subtext"
label="Checkbox with Subtext"
checked={!checkboxValue}
subtext="Toggle this option"
action={() => setCheckboxValue(v => !v)}
/>
<Menu.MenuSeparator />
<Menu.MenuSwitchItem
id="switch-item"
label="Switch Item"
checked={switchValue}
action={() => setSwitchValue(v => !v)}
/>
<Menu.MenuSeparator />
<Menu.MenuGroup label="Radio Group">
<Menu.MenuRadioItem
id="radio-1"
group="demo-radio"
label="Option 1"
checked={radioValue === "option1"}
action={() => setRadioValue("option1")}
/>
<Menu.MenuRadioItem
id="radio-2"
group="demo-radio"
label="Option 2"
checked={radioValue === "option2"}
action={() => setRadioValue("option2")}
/>
<Menu.MenuRadioItem
id="radio-3"
group="demo-radio"
label="Option 3"
checked={radioValue === "option3"}
action={() => setRadioValue("option3")}
/>
</Menu.MenuGroup>
<Menu.MenuSeparator />
<Menu.MenuItem id="submenu" label="Submenu">
<Menu.MenuItem
id="sub-item-1"
label="Sub Item 1"
action={() => console.log("Sub item 1")}
/>
<Menu.MenuItem
id="sub-item-2"
label="Sub Item 2"
action={() => console.log("Sub item 2")}
/>
<Menu.MenuItem id="nested-submenu" label="Nested Submenu">
<Menu.MenuItem
id="nested-1"
label="Nested Item"
action={() => console.log("Nested")}
/>
</Menu.MenuItem>
</Menu.MenuItem>
<Menu.MenuSeparator />
<Menu.MenuControlItem
id="slider-item"
label="Volume"
control={() => (
<Menu.MenuSliderControl
minValue={0}
maxValue={100}
value={sliderValue}
onChange={setSliderValue}
renderValue={v => `${v}%`}
/>
)}
/>
</Menu.Menu>
);
}
export default function MenuTab() {
const openMenu = (e: React.MouseEvent) => {
ContextMenuApi.openContextMenu(e, () => (
<DemoMenu onClose={ContextMenuApi.closeContextMenu} />
));
};
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Menu Demo">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Click or right-click the button to open a demo context menu.
</Paragraph>
<div className="vc-compfinder-grid">
<Button onClick={openMenu}>
Open Menu (Click)
</Button>
<Button variant="secondary" onContextMenu={openMenu}>
Open Menu (Right-Click)
</Button>
</div>
</SectionWrapper>
<SectionWrapper title="Menu Components">
<Paragraph color="text-muted">
<strong>Menu.Menu</strong> - Root container with navId and onClose.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuItem</strong> - Basic item with id, label, action, icon, hint, subtext, disabled, color.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuCheckboxItem</strong> - Toggleable checkbox item with checked state.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuRadioItem</strong> - Radio selection with group prop for mutual exclusion.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuSwitchItem</strong> - Toggle switch item with checked state.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuGroup</strong> - Groups items with optional label header.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuSeparator</strong> - Visual divider between items.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuControlItem</strong> - Wrapper for custom controls like sliders.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuSliderControl</strong> - Slider control inside MenuControlItem.
</Paragraph>
<Paragraph color="text-muted">
<strong>Menu.MenuSearchControl</strong> - Search input for filtering menu items.
</Paragraph>
</SectionWrapper>
<SectionWrapper title="MenuItem Colors">
<Paragraph color="text-muted">
Available colors: <code>default</code>, <code>brand</code>, <code>danger</code>, <code>success</code>, <code>premium</code>, <code>premium-gradient</code>.
</Paragraph>
</SectionWrapper>
<SectionWrapper title="Usage">
<Paragraph color="text-muted">
Use ContextMenuApi.openContextMenu(event, () =&gt; &lt;Menu /&gt;) to open menus.
For patches, use NavContextMenuPatchCallback from @api/ContextMenu.
</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -8,27 +8,40 @@ import { ManaButton, Paragraph, Skeleton, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function SkeletonTab() {
const [animated, setAnimated] = useState(true);
const [withHeader, setWithHeader] = useState(true);
const [size, setSize] = useState(5);
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Skeleton">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Loading placeholder with pulse animation. Used in Discord's shop for product cards.
Loading placeholder with shimmer animation. Used in Discord's member list loading state.
</Paragraph>
<ManaButton
variant="secondary"
text={animated ? "Disable Animation" : "Enable Animation"}
onClick={() => setAnimated(!animated)}
style={{ marginBottom: 16 }}
/>
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
<ManaButton
variant="secondary"
text={withHeader ? "Hide Header" : "Show Header"}
onClick={() => setWithHeader(!withHeader)}
/>
<ManaButton
variant="secondary"
text="Fewer Items"
onClick={() => setSize(Math.max(1, size - 1))}
/>
<ManaButton
variant="secondary"
text="More Items"
onClick={() => setSize(Math.min(20, size + 1))}
/>
</div>
<div className="full-motion">
<Skeleton skipPulseAnimation={!animated} />
<Skeleton withHeader={withHeader} size={size} />
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• skipPulseAnimation?: boolean - Disable pulse animation</Paragraph>
<Paragraph color="text-muted">• withHeader?: boolean - Show header placeholder (default: true)</Paragraph>
<Paragraph color="text-muted">• size?: number - Number of skeleton rows to display (default: 15)</Paragraph>
</SectionWrapper>
</div>
);

View file

@ -1,10 +1,14 @@
.vc-compfinder-search {
margin-bottom: 40px;
}
.vc-compfinder-tabbar {
margin-bottom: 16px;
flex-wrap: wrap !important;
gap: 8px 16px;
}
.vc-compfinder-tabbar > div {
.vc-compfinder-tabbar>div {
flex-wrap: wrap !important;
gap: 8px 16px;
}
@ -86,4 +90,9 @@
display: flex;
flex-direction: row-reverse;
gap: 8px;
}
}
.vc-alert-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}

View file

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

View file

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

View file

@ -6,6 +6,7 @@
export * from "../constants";
export * from "./accordion";
export * from "./alert";
export * from "./anchor";
export * from "./avatar";
export * from "./badge";
@ -15,10 +16,12 @@ export * from "./checkbox";
export * from "./chip";
export * from "./clickable";
export * from "./colorpicker";
export * from "./errorboundary";
export * from "./focuslock";
export * from "./guildicon.d";
export * from "./heading";
export * from "./input";
export * from "./menu";
export * from "./modal";
export * from "./notice";
export * from "./paginator";

View file

@ -92,5 +92,6 @@ export interface SearchBarProps {
onFocus?: () => void;
autoComplete?: string;
inputProps?: Record<string, any>;
className?: string;
"aria-label"?: string;
}

View file

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

View file

@ -5,5 +5,6 @@
*/
export interface SkeletonProps {
skipPulseAnimation?: boolean;
withHeader?: boolean;
size?: number;
}