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:
parent
a04f8131d3
commit
2301befaba
15 changed files with 1290 additions and 22 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
136
src/equicordplugins/components.dev/components/tabs/AlertTab.tsx
Normal file
136
src/equicordplugins/components.dev/components/tabs/AlertTab.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
224
src/equicordplugins/components.dev/components/tabs/MenuTab.tsx
Normal file
224
src/equicordplugins/components.dev/components/tabs/MenuTab.tsx
Normal 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, () => <Menu />) to open menus.
|
||||
For patches, use NavContextMenuPatchCallback from @api/ContextMenu.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
24
src/equicordplugins/components.dev/types/alert.d.ts
vendored
Normal file
24
src/equicordplugins/components.dev/types/alert.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
31
src/equicordplugins/components.dev/types/errorboundary.d.ts
vendored
Normal file
31
src/equicordplugins/components.dev/types/errorboundary.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -92,5 +92,6 @@ export interface SearchBarProps {
|
|||
onFocus?: () => void;
|
||||
autoComplete?: string;
|
||||
inputProps?: Record<string, any>;
|
||||
className?: string;
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
|
|
|||
193
src/equicordplugins/components.dev/types/menu.d.ts
vendored
Normal file
193
src/equicordplugins/components.dev/types/menu.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -5,5 +5,6 @@
|
|||
*/
|
||||
|
||||
export interface SkeletonProps {
|
||||
skipPulseAnimation?: boolean;
|
||||
withHeader?: boolean;
|
||||
size?: number;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue