diff --git a/src/equicordplugins/components.dev/components/DocPage.tsx b/src/equicordplugins/components.dev/components/DocPage.tsx new file mode 100644 index 000000000..59de63cae --- /dev/null +++ b/src/equicordplugins/components.dev/components/DocPage.tsx @@ -0,0 +1,258 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { CodeBlock } from "@components/CodeBlock"; +import { classNameFactory } from "@utils/css"; + +import { DiscordHeading, DiscordText, Divider, InlineCode, Notice, Paragraph } from "."; + +const cl = classNameFactory("vc-compfinder-"); + +export interface PropDef { + name: string; + type: string; + default?: string; + required?: boolean; + internal?: boolean; + description: string; +} + +export interface DocNotice { + type: "info" | "warn" | "danger" | "positive"; + children: React.ReactNode; +} + +export interface DemoSection { + title: string; + description?: string; + children: React.ReactNode; + code?: string; + relevantProps?: string[]; + notice?: DocNotice; + id?: string; +} + +export interface PropGroup { + title: string; + props: PropDef[]; +} + +export interface CodeExample { + label: string; + code: string; +} + +export interface DocPageConfig { + overview: string; + importPath: string; + sections?: DemoSection[]; + demos?: DemoSection[]; + props: PropDef[] | PropGroup[]; + examples?: CodeExample[]; + componentName?: string; + notices?: DocNotice[]; + showToc?: boolean; +} + +function isGrouped(props: PropDef[] | PropGroup[]): props is PropGroup[] { + return props.length > 0 && "props" in props[0]; +} + +function allProps(props: PropDef[] | PropGroup[]): PropDef[] { + return isGrouped(props) ? props.flatMap(g => g.props) : props; +} + +function generateId(title: string): string { + return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); +} + +function PropsTable({ props, showLegend }: { props: PropDef[]; showLegend?: boolean; }) { + const hasRequired = props.some(p => p.required); + const hasInternal = props.some(p => p.internal); + return ( + <> +
+ + + + + + + + + + + {props.map(p => ( + + + + + + + ))} + +
FIELDTYPEDEFAULTDESCRIPTION
+ {p.name} + {p.type}{p.default ?? -}{p.description}
+
+ {showLegend && (hasRequired || hasInternal) && ( +
+ {hasRequired && Required} + {hasInternal && Internal} +
+ )} + + ); +} + +function TableOfContents({ entries }: { entries: { id: string; label: string; }[]; }) { + return ( +
+ + ON THIS PAGE + +
+ {entries.map(entry => ( + { + e.preventDefault(); + document.getElementById(entry.id)?.scrollIntoView({ behavior: "smooth", block: "start" }); + }} + > + {entry.label} + + ))} +
+
+ ); +} + +function MajorSection({ title, id, children }: { title: string; id: string; children: React.ReactNode; }) { + return ( +
+ {title} + {children} +
+ ); +} + +function DemoSectionBlock({ section, relevantProps }: { section: DemoSection; relevantProps: PropDef[] | null; }) { + const id = section.id ?? generateId(section.title); + return ( +
+ {section.title} + {section.notice && ( + {section.notice.children} + )} + {section.description && ( + {section.description} + )} +
+ {section.children} +
+ {section.code && ( + + )} + {relevantProps && ( +
+ +
+ )} +
+ ); +} + +export function DocPage({ componentName, overview, importPath, sections, demos, props, examples, notices, showToc }: DocPageConfig) { + const items = sections ?? demos ?? []; + const flat = allProps(props); + + const tocEntries = [ + { id: "overview", label: "Overview" }, + { id: "import", label: "Import" }, + ...items.map(s => ({ id: s.id ?? generateId(s.title), label: s.title })), + { id: "api-reference", label: "API Reference" }, + ...(examples?.length ? [{ id: "examples", label: "Examples" }] : []), + ]; + + const shouldShowToc = showToc ?? items.length >= 4; + + return ( +
+ {componentName && ( + + {componentName} + + )} + +
+ Overview + {overview} +
+ + {notices?.map((notice, i) => ( + {notice.children} + ))} + + {shouldShowToc && } + + + +
+
+ + IMPORT + + +
+
+ + {items.map(section => { + const relevant = section.relevantProps?.length + ? flat.filter(p => section.relevantProps!.includes(p.name)) + : null; + + return [ + , + , + ]; + })} + + + + + {isGrouped(props) ? ( + props.map(group => ( +
+ + {group.title} + + +
+ )) + ) : ( + + )} +
+ + {examples?.length ? ( + <> + + + {examples.map(ex => ( +
+ {ex.label} + +
+ ))} +
+ + ) : null} +
+ ); +} diff --git a/src/equicordplugins/components.dev/components/index.tsx b/src/equicordplugins/components.dev/components/index.tsx index 663f0f649..d55c8b8dc 100644 --- a/src/equicordplugins/components.dev/components/index.tsx +++ b/src/equicordplugins/components.dev/components/index.tsx @@ -75,6 +75,7 @@ import type { NumberBadgeProps, OpenModalOptions, PaginatorProps, + PremiumBadgeProps, ProgressBarProps, SearchBarProps, SimpleErrorBoundaryProps, @@ -226,11 +227,28 @@ export const CircleBadge: React.ComponentType = proxyLazy(() = !fn.toString().includes("premiumBadge") ) ); +export const PremiumBadge: React.ComponentType = proxyLazy(() => + getBadgeExport(fn => fn.toString().includes("premiumBadge")) +); export const BadgeShapes = proxyLazy(() => Object.values(BadgeModule).find(v => typeof v === "object" && (v as BadgeShapesType)?.ROUND) as BadgeShapesType ?? { ROUND: "", ROUND_LEFT: "", ROUND_RIGHT: "", SQUARE: "" } ); +export const getBadgeWidthForValue: (count: number) => number = proxyLazy(() => { + for (const value of Object.values(BadgeModule)) { + if (typeof value === "function" && value.toString().includes("e<10?16:e<100")) return value as (count: number) => number; + } + return (count: number) => count < 10 ? 16 : count < 100 ? 22 : 30; +}); + +export const getBadgeCountString: (count: number) => string = proxyLazy(() => { + for (const value of Object.values(BadgeModule)) { + if (typeof value === "function" && value.toString().includes("e<1e3")) return value as (count: number) => string; + } + return (count: number) => count < 1000 ? `${count}` : `${Math.min(Math.floor(count / 1000), 9)}k+`; +}); + const ToastsModule = findByPropsLazy("showToast", "popToast"); export const Toasts: ToastsModule = proxyLazy(() => ({ Type: ToastType, diff --git a/src/equicordplugins/components.dev/components/tabs/AccordionTab.tsx b/src/equicordplugins/components.dev/components/tabs/AccordionTab.tsx index 177fde0c5..320f26008 100644 --- a/src/equicordplugins/components.dev/components/tabs/AccordionTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/AccordionTab.tsx @@ -5,69 +5,98 @@ */ import { Accordion, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function AccordionTab() { +const ACCORDION_PROPS: PropDef[] = [ + { name: "title", type: "string", required: true, description: "Header title text. Rendered as text-sm/medium." }, + { name: "children", type: "ReactNode", required: true, description: "Content revealed when expanded." }, + { name: "subtitle", type: "string", description: "Optional subtitle below the title. Rendered as text-xs/medium in text-subtle." }, + { name: "icon", type: "ReactNode", description: "Optional icon displayed before the title." }, + { name: "defaultExpanded", type: "boolean", default: "false", description: "Initial expanded state for uncontrolled usage." }, + { name: "isExpanded", type: "boolean", description: "Controlled expanded state. When set, the component becomes controlled." }, + { name: "onExpandedChange", type: "(expanded: boolean) => void", description: "Called when expanded state changes. Works in both controlled and uncontrolled modes." }, + { name: "onOpen", type: "() => void", description: "Called only when the accordion opens, not when it closes." }, + { name: "maxHeight", type: "number | string", description: "Maximum content height when expanded. Numbers are converted to px. Without this, height is based on scrollHeight." }, + { name: "className", type: "string", description: "Additional CSS class name on the root element." }, +]; + +function ControlledDemo() { const [controlled, setControlled] = useState(false); return ( -
- - - Click to expand/collapse content. - - - This is the accordion content that is revealed when expanded. - - - - - - Content with a subtitle in the header. - - - - - - This accordion starts expanded by default. - - - - - - Use isExpanded and onExpandedChange for controlled state. - - - This accordion's state is controlled externally. - - - - - - Line 1 - Line 2 - Line 3 - Line 4 - Line 5 - Line 6 - - - - - • title: string - Header title - • subtitle?: string - Optional subtitle - • icon?: ReactNode - Optional icon - • children: ReactNode - Content - • defaultExpanded?: boolean - Initial state (default: false) - • isExpanded?: boolean - Controlled expanded state - • onExpandedChange?: (expanded: boolean) => void - • onOpen?: () => void - Called when opened - • maxHeight?: number | string - Max content height - -
+ + This accordion's state is controlled externally. + + ); +} + +export default function AccordionTab() { + return ( + + This is the accordion content that is revealed when expanded. + + ), + code: '\n Hidden content here.\n', + relevantProps: ["title", "children"] + }, + { + title: "With Subtitle", + description: "A subtitle renders below the title in smaller, subtler text.", + children: ( + + Content with a subtitle in the header. + + ), + relevantProps: ["subtitle", "icon"] + }, + { + title: "Default Expanded", + description: "Starts open on mount using defaultExpanded.", + children: ( + + This accordion starts expanded by default. + + ), + relevantProps: ["defaultExpanded"] + }, + { + title: "Controlled", + description: "Use isExpanded and onExpandedChange to manage state externally.", + children: , + code: '\n Controlled content.\n', + relevantProps: ["isExpanded", "onExpandedChange"] + }, + { + title: "With Max Height", + description: "Constrains expanded content height. Overflowing content is clipped.", + children: ( + + Line 1 + Line 2 + Line 3 + Line 4 + Line 5 + Line 6 + + ), + code: '} maxHeight={200}>\n Scrollable content area.\n', + relevantProps: ["maxHeight"] + }, + ]} + props={ACCORDION_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/AlertTab.tsx b/src/equicordplugins/components.dev/components/tabs/AlertTab.tsx index ae0d240e4..089f9fb5b 100644 --- a/src/equicordplugins/components.dev/components/tabs/AlertTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/AlertTab.tsx @@ -4,126 +4,126 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Alerts, Button, Paragraph, showToast, ToastType } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { Alerts, Button, showToast, ToastType } from ".."; +import { DocPage, type PropDef } from "../DocPage"; + +const ALERT_PROPS: PropDef[] = [ + { name: "title", type: "string", required: true, description: "Alert dialog title." }, + { name: "body", type: "ReactNode", required: true, description: "Alert body content. Can be a string or JSX." }, + { name: "confirmText", type: "string", description: "Text for the confirm button. Defaults to a localized \"Okay\" string." }, + { name: "cancelText", type: "string", description: "Text for the cancel button. Only shown when set to a non-empty string." }, + { name: "confirmVariant", type: '"primary" | "critical-primary" | "expressive"', default: '"primary"', description: "Visual variant for the confirm button." }, + { name: "onConfirm", type: "() => void", description: "Called when the confirm button is clicked." }, + { name: "onCancel", type: "() => void", description: "Called when the cancel button is clicked." }, + { name: "onCloseCallback", type: "() => void", description: "Called when the alert closes, regardless of which button was pressed." }, + { name: "contextKey", type: "string", description: "Optional context key passed to the modal system." }, +]; 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 showExpressiveAlert = () => { - Alerts.show({ - title: "Premium Feature", - body: "This feature requires a premium subscription.", - confirmText: "Upgrade Now", - confirmVariant: "expressive", - cancelText: "Maybe Later", - onConfirm: () => showToast("Upgrading!", ToastType.SUCCESS), - }); - }; - return ( -
- - - Simple alert with just a title and body. - - - - - - - Alert with confirm and cancel buttons and callbacks. - - - - - - - Alert with red danger button for destructive actions. - - - - - - - Alert with premium/expressive styled button. - - - - - - - Alerts.show(options) - Show an alert dialog - - - Alerts.close() - Close the current alert - - - Alerts.confirm(options) - Show a confirm dialog - - - - - - title - Alert title text - - - body - Alert body content (string or ReactNode) - - - confirmText - Text for confirm button - - - cancelText - Text for cancel button (shows cancel if set) - - - confirmVariant - "primary", "critical-primary", or "expressive" - - - onConfirm - Callback when confirm clicked - - - onCancel - Callback when cancel clicked - - - onCloseCallback - Callback when alert closes - - -
+ that cleanly integrates with async flows." }, + ]} + importPath={'import { Alerts } from "../components";'} + sections={[ + { + title: "Basic Alert", + description: "Simple alert with just a title and body. Only shows a confirm button.", + children: ( + + ), + code: 'Alerts.show({ title: "Done", body: "Operation completed successfully." });', + relevantProps: ["title", "body"] + }, + { + title: "Confirm Alert", + description: "Alert with confirm and cancel buttons. Both fire callbacks on click.", + children: ( + + ), + relevantProps: ["confirmText", "cancelText", "onConfirm", "onCancel"] + }, + { + title: "Danger Alert", + description: "Uses critical-primary variant for destructive actions.", + children: ( + + ), + code: 'Alerts.show({\n title: "Delete?",\n body: "This cannot be undone.",\n confirmText: "Delete",\n confirmVariant: "critical-primary",\n cancelText: "Cancel",\n onConfirm: () => deleteItem(),\n});', + relevantProps: ["confirmVariant"] + }, + { + title: "Expressive Alert", + description: "Uses the expressive variant for premium or promotional actions.", + children: ( + + ), + relevantProps: ["confirmVariant"] + }, + { + title: "Promise-based Confirm", + description: "Alerts.confirm() returns a Promise. Resolves true on confirm, false on cancel.", + children: ( + + ), + code: 'const confirmed = await Alerts.confirm({\n title: "Proceed?",\n body: "Are you sure?",\n confirmText: "Yes",\n cancelText: "No",\n});\nif (confirmed) doSomething();' + }, + ]} + props={ALERT_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx b/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx index cb258fffa..1a5f568b9 100644 --- a/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/AnchorTab.tsx @@ -4,107 +4,106 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Anchor , Paragraph } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { Anchor, Paragraph } from ".."; +import { DocPage, type PropDef } from "../DocPage"; + +const ANCHOR_PROPS: PropDef[] = [ + { name: "href", type: "string", description: "Link URL. External links automatically get rel=\"noreferrer noopener\" and target=\"_blank\"." }, + { name: "children", type: "ReactNode", description: "Link content." }, + { name: "onClick", type: "MouseEventHandler", description: "Click handler. If omitted and href is set, uses Discord's default link interceptor." }, + { name: "useDefaultUnderlineStyles", type: "boolean", default: "true", description: "Whether to show underline on hover." }, + { name: "title", type: "string", description: "Tooltip text shown on hover." }, + { name: "target", type: "string", description: "Link target. Auto-set to _blank for external URLs." }, + { name: "rel", type: "string", description: "Link relationship. Auto-set to \"noreferrer noopener\" for external URLs." }, + { name: "focusProps", type: "Record", internal: true, description: "Props forwarded to the focus management wrapper." }, + { name: "ref", type: "Ref", description: "Ref to the underlying anchor element." }, + { name: "className", type: "string", description: "Additional CSS class name." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, +]; export default function AnchorTab() { return ( -
- - - A styled anchor/link component with underline on hover. - - - Discord Homepage - - - - - - Disable the default underline-on-hover behavior. - - - No underline on hover - - - - - - Open in same tab instead of new tab. - - - Opens in same tab - - - - - - Hover to see the title tooltip. - - - Hover for tooltip - - - - - - Custom styles applied via style prop. - - - Green styled link - - - - - - Custom click handler (prevents default navigation). - - { - e.preventDefault(); - alert("Link clicked!"); - }} - > - Click me (shows alert) - - - - - - Links in a sentence. - - - Check out the Terms of Service and{" "} - Privacy Policy for more info. - - - - - - Anchor - Styled link component (also known as MaskedLink) - - • href?: string - Link URL - • onClick?: MouseEventHandler - Click handler - • className?: string - Additional CSS classes - • children?: ReactNode - Link content - • rel?: string - Link relationship (auto-set for external) - • target?: string - Link target (auto-set to _blank for external) - • useDefaultUnderlineStyles?: boolean - Show underline on hover (default: true) - • title?: string - Tooltip text on hover - • style?: CSSProperties - Inline styles - • focusProps?: object - Focus ring configuration - • ref?: Ref - Forward ref to anchor element - - Behavior - - • External links automatically get rel="noreferrer noopener" and target="_blank" - • If no onClick is provided but href is set, uses Discord's default link interceptor - • Untrusted external links show a warning modal before navigating - -
+ + Discord Homepage + + ), + code: 'Visit Example', + relevantProps: ["href", "children"] + }, + { + title: "Without Underline", + description: "Disable the default underline-on-hover behavior with useDefaultUnderlineStyles={false}.", + children: ( + + No underline on hover + + ), + relevantProps: ["useDefaultUnderlineStyles"] + }, + { + title: "With Title Tooltip", + description: "Hover to see the native title tooltip.", + children: ( + + Hover for tooltip + + ), + relevantProps: ["title"] + }, + { + title: "Custom Styling", + children: ( + + Green styled link + + ), + relevantProps: ["style", "className"] + }, + { + title: "With onClick Handler", + description: "Custom click handler prevents default navigation.", + children: ( + { + e.preventDefault(); + alert("Link clicked!"); + }} + > + Click me (shows alert) + + ), + code: ' { e.preventDefault(); doSomething(); }}>\n Click me\n', + relevantProps: ["onClick"] + }, + { + title: "Inline in Text", + description: "Links used naturally within a paragraph.", + children: ( + + Check out the Terms of Service and{" "} + Privacy Policy for more info. + + ), + code: '\n Read the documentation.\n' + }, + ]} + props={ANCHOR_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/AnimationsTab.tsx b/src/equicordplugins/components.dev/components/tabs/AnimationsTab.tsx index 1cb8ae335..05602a228 100644 --- a/src/equicordplugins/components.dev/components/tabs/AnimationsTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/AnimationsTab.tsx @@ -5,12 +5,21 @@ */ import { Animations, ManaButton, Paragraph, SpringConfigs, useSpring, useState, useTrail, useTransition } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; const { animated } = Animations; const TRAIL_ITEMS = ["Discord", "uses", "react-spring", "for", "animations"]; +const SPRING_CONFIG_PROPS: PropDef[] = [ + { name: "default", type: "{ tension: 170, friction: 26 }", description: "Standard spring. Balanced speed and bounce." }, + { name: "gentle", type: "{ tension: 120, friction: 14 }", description: "Soft, slow spring with more bounce." }, + { name: "wobbly", type: "{ tension: 180, friction: 12 }", description: "Bouncy, playful spring with high overshoot." }, + { name: "stiff", type: "{ tension: 210, friction: 20 }", description: "Quick, snappy spring with minimal bounce." }, + { name: "slow", type: "{ tension: 280, friction: 60 }", description: "High tension with heavy damping. Slow and deliberate." }, + { name: "molasses", type: "{ tension: 280, friction: 120 }", description: "Extremely slow and heavily damped. No bounce." }, +]; + function SpringDemo() { const [toggled, setToggled] = useState(false); const [config, setConfig] = useState("default"); @@ -23,10 +32,7 @@ function SpringDemo() { }); return ( - - - Animate values with spring physics. Click the box or change config. - + <>
{(Object.keys(SpringConfigs) as (keyof typeof SpringConfigs)[]).map(c => ( Click me - + ); } @@ -69,22 +75,13 @@ function TransitionDemo() { config: SpringConfigs.gentle }); - const addItem = () => { - setItems([...items, nextId]); - setNextId(nextId + 1); - }; - - const removeItem = (id: number) => { - setItems(items.filter(i => i !== id)); - }; - return ( - - - Animate items entering and leaving a list. - + <>
- + { + setItems([...items, nextId]); + setNextId(nextId + 1); + }} /> removeItem(item)} + onClick={() => setItems(items.filter(i => i !== item))} > Item {item} ))}
-
+ ); } @@ -123,10 +120,7 @@ function TrailDemo() { }) as object[]; return ( - - - Staggered animations for multiple items. Each item animates slightly after the previous. - + <>
))}
-
+ ); } @@ -166,67 +160,61 @@ function AnimatedElementsDemo() { }); return ( - - - Use animated.div, animated.span, animated.svg, etc. for spring-animated elements. - -
- 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" - }} - > - Hover me - -
-
+ 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" + }} + > + Hover me + ); } export default function AnimationsTab() { return ( -
- - - - - - - - Preset spring configurations for different animation feels. - - • default - Standard spring - • gentle - Soft, slow spring - • wobbly - Bouncy, playful spring - • stiff - Quick, snappy spring - • slow - Very slow spring - • molasses - Extremely slow spring - - - - • useSpring - Single spring animation - • useSprings - Multiple springs - • useTransition - Enter/leave animations - • useTrail - Staggered animations - • useChain - Sequence animations - - - - • Transition - Declarative transitions - • Spring - Declarative spring - • Trail - Declarative trail - • animated.* - Animated HTML/SVG elements - -
+ , + code: "const spring = useSpring({ opacity: visible ? 1 : 0, config: SpringConfigs.gentle });\nreturn Content;" + }, + { + title: "useTransition", + description: "Animate items entering and leaving a list. Click items to remove them, or use the buttons to add/clear.", + children: , + code: "const transitions = useTransition(items, {\n from: { opacity: 0 },\n enter: { opacity: 1 },\n leave: { opacity: 0 },\n keys: item => item.id,\n});\nreturn transitions((style, item) => {item.name});" + }, + { + title: "useTrail", + description: "Staggered animations for multiple items. Each item animates slightly after the previous one.", + children: , + code: 'const trail = useTrail(items.length, {\n opacity: show ? 1 : 0,\n transform: show ? "translateY(0)" : "translateY(20px)",\n config: SpringConfigs.gentle,\n});' + }, + { + title: "animated Elements", + description: "Use animated.div, animated.span, animated.svg etc. to create spring-animated HTML elements. Hover the box below.", + children: + }, + ]} + props={SPRING_CONFIG_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx b/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx index f6bdd076b..8a721c967 100644 --- a/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/AvatarTab.tsx @@ -4,114 +4,154 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Avatar, AvatarSizes, Paragraph, UserStore } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { Avatar, AvatarSizes, AvatarStatuses, Paragraph, UserStore } from ".."; +import { DocPage, type PropDef } from "../DocPage"; + +const AVATAR_PROPS: PropDef[] = [ + { name: "src", type: "string", description: "Avatar image URL." }, + { name: "size", type: "AvatarSize", description: "Avatar size constant (SIZE_16 through SIZE_152). Controls the SVG viewBox dimensions." }, + { name: "status", type: "AvatarStatus", description: "User presence status. Renders a colored indicator dot." }, + { name: "statusColor", type: "string", description: "Override the status indicator color." }, + { name: "statusBackdropColor", type: "string", description: "Background color behind the status indicator." }, + { name: "isMobile", type: "boolean", default: "false", description: "Shows the mobile phone status indicator instead of a dot." }, + { name: "isVR", type: "boolean", default: "false", description: "Shows the VR headset status indicator." }, + { name: "isTyping", type: "boolean", default: "false", description: "Shows a typing animation in the status area." }, + { name: "isSpeaking", type: "boolean", default: "false", description: "Renders an animated speaking ring around the avatar." }, + { name: "isLatched", type: "boolean", default: "false", description: "Latched speaking state for voice activity." }, + { name: "voiceDb", type: "number", description: "Voice volume level in dB for speaking animation intensity." }, + { name: "speakingStylesConfig", type: "Record", description: "Custom styles config for the speaking ring animation." }, + { name: "statusTooltip", type: "boolean", default: "false", description: "Shows a tooltip with the status text on hover." }, + { name: "statusTooltipDelay", type: "number", description: "Delay in ms before the status tooltip appears." }, + { name: "typingIndicatorRef", type: "Ref", description: "Ref for the typing indicator element." }, + { name: "avatarContentRef", type: "Ref", description: "Ref for the avatar image foreignObject content." }, + { name: "CutoutIcon", type: "ComponentType", description: "Custom icon component rendered as a cutout overlay on the avatar." }, + { name: "avatarTooltipAsset", type: "ReactNode", description: "Custom tooltip asset displayed on the avatar." }, + { name: "avatarTooltipText", type: "string", description: "Tooltip text for the avatar overlay." }, + { name: "avatarTooltipTitle", type: "string", description: "Tooltip title for the avatar overlay." }, + { name: "avatarDecoration", type: "string", description: "URL for the avatar decoration overlay image." }, + { name: "aria-hidden", type: "boolean", default: "false", description: "Hides the avatar from screen readers." }, + { name: "aria-label", type: "string", description: "Accessibility label for the avatar." }, + { name: "imageClassName", type: "string", description: "CSS class applied to the avatar image element." }, + { name: "className", type: "string", description: "CSS class applied to the root wrapper." }, + { name: "onClick", type: "MouseEventHandler", description: "Click handler on the avatar wrapper. Makes the avatar interactive." }, + { name: "onMouseDown", type: "MouseEventHandler", description: "Mouse down handler on the avatar wrapper." }, + { name: "onKeyDown", type: "KeyboardEventHandler", description: "Key down handler on the avatar wrapper." }, + { name: "onContextMenu", type: "MouseEventHandler", description: "Context menu handler on the avatar wrapper." }, + { name: "onMouseEnter", type: "MouseEventHandler", description: "Mouse enter handler on the avatar wrapper." }, + { name: "onMouseLeave", type: "MouseEventHandler", description: "Mouse leave handler on the avatar wrapper." }, + { name: "tabIndex", type: "number", description: "Tab index for keyboard navigation on the avatar wrapper." }, + { name: "ref", type: "Ref", description: "Ref to the root SVG element." }, +]; export default function AvatarTab() { const currentUser = UserStore.getCurrentUser(); const avatarUrl = currentUser?.getAvatarURL?.(undefined, 128); return ( -
- - - Avatar component from @webpack/common with various sizes. - -
- {AvatarSizes.map(size => ( -
- - - {size.replace("SIZE_", "")} - + + {AvatarSizes.map(size => ( +
+ + + {size.replace("SIZE_", "")} + +
+ ))}
- ))} -
-
- - - - Avatar with status indicator. - -
-
- - Online -
-
- - Idle -
-
- - DND -
-
- - Offline -
-
-
- - - - Avatar with mobile indicator. - -
- -
-
- - - - Avatar with speaking ring indicator. - -
-
- - Speaking -
-
-
- - - • src - Avatar image URL - • size - SIZE_16, SIZE_20, SIZE_24, SIZE_32, SIZE_40, SIZE_44, SIZE_48, SIZE_56, SIZE_72, SIZE_80, SIZE_96, SIZE_120, SIZE_152 - • status - online, idle, dnd, offline - • isMobile - Show mobile indicator - • isTyping - Show typing indicator - • isSpeaking - Show speaking ring - • statusTooltip - Show status on hover - • aria-label - Accessibility label - -
+ ) + }, + { + title: "Statuses", + description: "All status indicator variants.", + code: '', + relevantProps: ["status", "statusColor", "statusBackdropColor"], + children: ( +
+ {AvatarStatuses.map(status => ( +
+ + + {status} + +
+ ))} +
+ ) + }, + { + title: "Mobile Status", + description: "Shows the mobile phone icon instead of a status dot when isMobile is true.", + code: '', + relevantProps: ["isMobile", "isVR"], + children: ( +
+
+ + Mobile +
+
+ + VR +
+
+ ) + }, + { + title: "Speaking State", + description: "Animated ring indicator for voice activity.", + code: '', + relevantProps: ["isSpeaking", "isLatched", "voiceDb"], + children: ( +
+
+ + Speaking +
+
+ ) + }, + { + title: "Typing Indicator", + description: "Shows a typing animation in the status area.", + relevantProps: ["isTyping"], + children: ( +
+
+ + Typing +
+
+ ) + }, + { + title: "Status Tooltip", + description: "Hover to see the status text tooltip.", + relevantProps: ["statusTooltip", "statusTooltipDelay"], + children: ( +
+ {AvatarStatuses.map(status => ( +
+ + + {status} + +
+ ))} +
+ ) + }, + ]} + props={AVATAR_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx b/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx index dad9add36..29aa9ce53 100644 --- a/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/BadgeTab.tsx @@ -4,101 +4,139 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, Paragraph, TextBadge } from ".."; +import { BadgeShapes, CircleBadge, IconBadge, NumberBadge, PremiumBadge, TextBadge } from ".."; +import { DocPage, type PropDef } from "../DocPage"; import { EquicordIcon } from "../icons/EquicordIcon"; -import { SectionWrapper } from "../SectionWrapper"; + +const SHARED_PROPS: PropDef[] = [ + { name: "color", type: "string", default: "BADGE_NOTIFICATION_BACKGROUND", description: "Background color. Defaults to the notification red for most badges. CircleBadge defaults to INTERACTIVE_TEXT_ACTIVE instead." }, + { name: "disableColor", type: "boolean", default: "false", description: "Removes the background color entirely when true." }, + { name: "shape", type: "BadgeShapes", default: "ROUND", description: "Controls border radius. ROUND (pill), ROUND_LEFT, ROUND_RIGHT, or SQUARE." }, + { name: "className", type: "string", description: "Additional CSS class name." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, +]; + +const ALL_PROPS: PropDef[] = [ + { name: "count", type: "number", required: true, description: "NumberBadge only. The count to display. Auto-abbreviates large values (1000+ becomes \"1k+\", capped at \"9k+\")." }, + { name: "renderBadgeCount", type: "(count: number) => string", description: "NumberBadge only. Custom formatter for the count display. Defaults to the built-in abbreviation." }, + { name: "text", type: "string", required: true, description: "TextBadge and PremiumBadge. The text content to display." }, + { name: "icon", type: "ComponentType<{ className?: string; color?: string; }>", required: true, description: "IconBadge only. Icon component to render inside the badge." }, + ...SHARED_PROPS, +]; export default function BadgeTab() { return ( -
- - - Displays a count value. Automatically abbreviates large numbers. - -
- - - - - -
-
- - - - Displays text content. - -
- - - -
-
- - - - Displays an icon inside a badge. - -
- - - -
-
- - - - A simple circular badge indicator. - -
- - - -
-
- - - - Badges support different shapes via the shape prop. - -
- - - - -
-
- - - - Custom colors can be applied. Use disableColor for no background. - -
- - - - - -
-
- - - - NumberBadge: count, color?, disableColor?, shape?, className?, style?, renderBadgeCount? - - - TextBadge: text, color?, disableColor?, shape?, className?, style? - - - IconBadge: icon, color?, disableColor?, shape?, className?, style? - - - CircleBadge: color?, disableColor?, shape?, className?, style? - - - BadgeShapes: ROUND, ROUND_LEFT, ROUND_RIGHT, SQUARE - - -
+ ", + relevantProps: ["count", "renderBadgeCount"], + children: ( +
+ + + + + +
+ ) + }, + { + title: "TextBadge", + description: "Displays text content inside a badge.", + relevantProps: ["text"], + children: ( +
+ + + +
+ ) + }, + { + title: "PremiumBadge", + description: "A TextBadge variant with an additional premium styling class applied.", + relevantProps: ["text"], + children: ( +
+ + + +
+ ) + }, + { + title: "IconBadge", + description: "Renders an icon component inside a badge. The icon receives color=\"currentColor\".", + code: '', + relevantProps: ["icon"], + children: ( +
+ + + +
+ ) + }, + { + title: "CircleBadge", + description: "A simple circular dot indicator. Defaults to INTERACTIVE_TEXT_ACTIVE color unlike other badges.", + children: ( +
+ + + + +
+ ) + }, + { + title: "Shapes", + description: "All four shape variants. SQUARE removes border radius entirely.", + code: '', + relevantProps: ["shape"], + children: ( +
+ + + + +
+ ) + }, + { + title: "Custom Colors", + description: "Any CSS color value works. Use disableColor to remove the background entirely.", + relevantProps: ["color", "disableColor"], + children: ( +
+ + + + + +
+ ) + }, + { + title: "Custom Render", + description: "NumberBadge accepts a custom renderBadgeCount function to format the display.", + code: ' n > 99 ? "99+" : String(n)}\n/>', + children: ( +
+ `#${n}`} /> + `${n}%`} /> + "!"} /> +
+ ) + }, + ]} + props={ALL_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx b/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx index 2e95636a8..1cd50c2cc 100644 --- a/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ButtonsTab.tsx @@ -5,67 +5,138 @@ */ import { ManaButton, ManaButtonSizes, ManaButtonVariants, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function ButtonsTab() { +const BUTTON_PROPS: PropDef[] = [ + { name: "text", type: "string", description: "Button label text." }, + { name: "variant", type: "ManaButtonVariant", default: '"primary"', description: "Visual style variant." }, + { name: "size", type: "ManaButtonSize", default: '"md"', description: "Button size. Also controls icon and text variant sizing internally." }, + { name: "disabled", type: "boolean", default: "false", description: "Disables the button. When true, all event handlers are suppressed." }, + { name: "loading", type: "boolean", default: "false", description: "Shows a loading spinner overlay and suppresses event handlers like disabled." }, + { name: "loadingStartedLabel", type: "string", description: "Accessibility label announced via live region when loading begins." }, + { name: "loadingFinishedLabel", type: "string", description: "Accessibility label announced via live region when loading ends." }, + { name: "fullWidth", type: "boolean", default: "false", description: "Expands the button to fill its container width. Can be overridden by a parent ButtonGroup context." }, + { name: "rounded", type: "boolean", default: "false", description: "Applies fully rounded pill-shaped corners." }, + { name: "icon", type: "ManaButtonIconType", description: "Icon to display. Accepts a component, or an object with type \"icon\", \"rive\", or \"sticker\"." }, + { name: "iconPosition", type: '"start" | "end"', default: '"start"', description: "Which side of the text the icon appears on." }, + { name: "iconOpticalOffsetMargin", type: "number", default: "0", description: "Pixel offset margin for optical icon alignment." }, + { name: "minWidth", type: "number | string", description: "Minimum button width. Numbers are converted to px." }, + { name: "role", type: "string", default: '"button"', description: "ARIA role attribute." }, + { name: "type", type: '"button" | "submit" | "reset"', default: '"button"', description: "HTML button type attribute." }, + { name: "rel", type: "string", description: "Relationship attribute for link-style buttons." }, + { name: "onClick", type: "(e: MouseEvent) => void", description: "Click handler. Suppressed when disabled or loading." }, + { name: "onDoubleClick", type: "(e: MouseEvent) => void", description: "Double-click handler. Suppressed when disabled or loading." }, + { name: "onMouseEnter", type: "(e: MouseEvent) => void", description: "Mouse enter handler. Suppressed when disabled or loading." }, + { name: "onMouseLeave", type: "(e: MouseEvent) => void", description: "Mouse leave handler. Suppressed when disabled or loading." }, + { name: "onMouseUp", type: "(e: MouseEvent) => void", description: "Mouse up handler. Suppressed when disabled or loading." }, + { name: "onMouseDown", type: "(e: MouseEvent) => void", description: "Mouse down handler. Suppressed when disabled or loading." }, + { name: "onKeyDown", type: "(e: KeyboardEvent) => void", description: "Keyboard handler. Suppressed when disabled or loading." }, + { name: "focusProps", type: "Record", internal: true, description: "Props forwarded to the focus management wrapper." }, + { name: "buttonRef", type: "Ref", internal: true, description: "Ref attached to the underlying button element." }, + { name: "className", type: "string", description: "Additional CSS class name." }, + { name: "style", type: "CSSProperties", description: "Inline styles applied to the button." }, +]; + +function StatesDemo() { const [loading, setLoading] = useState(false); return ( -
- -
- {ManaButtonVariants.map(variant => ( - - ))} -
-
- - -
- {ManaButtonSizes.map(size => ( - - ))} -
-
- - -
- - { - setLoading(true); - setTimeout(() => setLoading(false), 2000); - }} - /> - - -
-
- - -
- {ManaButtonVariants.map(variant => ( - - ))} -
-
- - - • text - Button label text - • variant - primary, secondary, critical-primary, critical-secondary, overlay-primary, overlay-secondary, expressive - • size - xs, sm, md - • disabled - Disable button - • loading - Show loading spinner - • fullWidth - Expand to container width - • rounded - Rounded corners - • icon - Icon component - • iconPosition - start or end - • onClick - Click handler - +
+ + { + setLoading(true); + setTimeout(() => setLoading(false), 2000); + }} + /> + +
); } + +export default function ButtonsTab() { + return ( + + {ManaButtonVariants.map(variant => ( + + ))} +
+ ), + code: ' save()} />', + relevantProps: ["variant"], + }, + { + title: "Sizes", + description: "Each size maps to a specific text variant internally (xs to text-xs/medium, sm to text-sm/medium, md to text-md/medium) and a corresponding icon size.", + children: ( +
+ {ManaButtonSizes.map(size => ( + + ))} +
+ ), + code: '', + relevantProps: ["size"], + }, + { + title: "States", + description: "Disabled and loading both suppress all event handlers. Loading shows a spinner overlay with accessibility announcements.", + children: , + code: '', + relevantProps: ["disabled", "loading", "loadingStartedLabel", "loadingFinishedLabel", "rounded", "fullWidth"], + }, + { + title: "All Variants Disabled", + description: "How each variant looks in its disabled state.", + children: ( +
+ {ManaButtonVariants.map(variant => ( + + ))} +
+ ), + }, + { + title: "Size \u00d7 Variant Matrix", + description: "Every combination of size and variant for comparison.", + children: ( + <> + {ManaButtonSizes.map(size => ( +
+ + Size: {size} + +
+ {ManaButtonVariants.map(variant => ( + + ))} +
+
+ ))} + + ), + }, + ]} + props={BUTTON_PROPS} + examples={[ + { + label: "Destructive action with critical variant:", + code: '', + }, + ]} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx b/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx index 7319826a5..fd1cb27be 100644 --- a/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/CalendarTab.tsx @@ -4,153 +4,189 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaCalendar, ManaDatePicker, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaCalendar, ManaDatePicker, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; -export default function CalendarTab() { - const [calendarValue, setCalendarValue] = useState(null); - const [datePickerValue, setDatePickerValue] = useState(null); +const CALENDAR_PROPS: PropDef[] = [ + { name: "value", type: "CalendarDate | null", description: "The currently selected date value." }, + { name: "onChange", type: "(value: CalendarDate) => void", description: "Called when a date is selected." }, + { name: "minValue", type: "CalendarDate", description: "Minimum selectable date." }, + { name: "maxValue", type: "CalendarDate", description: "Maximum selectable date." }, + { name: "disabled", type: "boolean", default: "false", description: "Disables the entire calendar." }, + { name: "readOnly", type: "boolean", default: "false", description: "Makes the calendar read-only. Dates are visible but not selectable." }, + { name: "className", type: "string", description: "Additional CSS class on the root element." }, + { name: "aria-label", type: "string", description: "Accessibility label for the calendar." }, +]; + +const DATEPICKER_PROPS: PropDef[] = [ + { name: "value", type: "CalendarDate | CalendarDateTime | null", description: "The currently selected date/time value." }, + { name: "onChange", type: "(value: CalendarDate | CalendarDateTime) => void", description: "Called when the value changes. Also closes the popover." }, + { name: "minValue", type: "CalendarDate", description: "Minimum selectable date." }, + { name: "maxValue", type: "CalendarDate", description: "Maximum selectable date." }, + { name: "placeholderValue", type: "CalendarDate | CalendarDateTime", description: "Placeholder date shown in the input when no value is selected." }, + { name: "granularity", type: '"day" | "hour" | "minute" | "second"', default: '"day"', description: "Time precision level. Day shows only date fields, others add time segments." }, + { name: "hourCycle", type: "12 | 24", description: "Whether to use 12-hour or 24-hour time format." }, + { name: "hideTimeZone", type: "boolean", default: "false", description: "Hides the time zone display when using time granularity." }, + { name: "label", type: "string", description: "Input field label." }, + { name: "hideLabel", type: "boolean", description: "Visually hides the label while keeping it accessible." }, + { name: "description", type: "string", description: "Help text shown below the label." }, + { name: "helperText", type: "string", description: "Additional help text shown below the input." }, + { name: "errorMessage", type: "string", description: "Error text shown below the input. Triggers error styling when non-empty." }, + { name: "successMessage", type: "string", description: "Success text shown below the input." }, + { name: "disabled", type: "boolean", description: "Disables the date picker input and calendar." }, + { name: "required", type: "boolean", default: "false", description: "Marks the field as required." }, + { name: "badge", type: "ReactNode", description: "Badge element displayed next to the label." }, + { name: "icon", type: "ReactNode", description: "Icon displayed in the field." }, + { name: "id", type: "string", description: "Custom ID for the input control." }, + { name: "layout", type: "string", description: "Field layout variant." }, + { name: "layoutConfig", type: "Record", description: "Configuration for the field layout." }, +]; + +function CalendarDemo() { + const [value, setValue] = useState(null); return ( -
- - - Basic calendar component for date selection. - -
- -
-
+ + ); +} - -
- -
-
+function DatePickerDemo() { + const [value, setValue] = useState(null); - -
- -
-
- - - - Date picker with input field and calendar popover. - -
- -
-
- - - - Date picker with hour/minute granularity. - -
- -
-
- - -
- - - -
-
- - - - ManaCalendar - - - • value: any - Selected date value - - - • onChange?: (value: any) => void - Selection callback - - - • disabled?: boolean - Disable calendar - - - • readOnly?: boolean - Read-only mode - - - • aria-label?: string - Accessibility label - - - ManaDatePicker - - - • value: any - Selected date value - - - • onChange?: (value: any) => void - Selection callback - - - • label?: string - Input label - - - • description?: string - Description text - - - • granularity?: "day" | "hour" | "minute" - Time precision - - - • hourCycle?: 12 | 24 - Hour format - - - • disabled?: boolean - Disable picker - - - • required?: boolean - Mark as required - - - • errorMessage?: string - Error text to display - - + return ( +
+
); } + +function DatePickerTimeDemo() { + const [value, setValue] = useState(null); + + return ( +
+ +
+ ); +} + +function DatePickerStatesDemo() { + const [value, setValue] = useState(null); + + return ( +
+ + + + +
+ ); +} + +export default function CalendarTab() { + return ( + ', + relevantProps: ["value", "onChange", "aria-label"], + children: + }, + { + title: "Calendar (Disabled)", + description: "All dates are visible but not interactive.", + relevantProps: ["disabled"], + children: ( + { }} + disabled + aria-label="Disabled calendar" + /> + ) + }, + { + title: "Calendar (Read Only)", + description: "Dates are visible with current selection shown but cannot be changed.", + relevantProps: ["readOnly"], + children: ( + { }} + readOnly + aria-label="Read only calendar" + /> + ) + }, + { + title: "Date Picker", + description: "Text input with a calendar popover. Click the calendar icon to open.", + code: '', + relevantProps: ["label", "description", "granularity"], + children: + }, + { + title: "Date Picker with Time", + description: "Using minute granularity adds hour and minute segments to the input.", + code: '', + relevantProps: ["granularity", "hourCycle"], + children: + }, + { + title: "Date Picker States", + description: "Disabled, required, error, and helper text states.", + relevantProps: ["disabled", "required", "errorMessage", "helperText"], + children: + }, + ]} + props={[ + ...CALENDAR_PROPS.map(p => ({ ...p, name: `[Calendar] ${p.name}` })), + ...DATEPICKER_PROPS.map(p => ({ ...p, name: `[DatePicker] ${p.name}` })), + ]} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/CardTab.tsx b/src/equicordplugins/components.dev/components/tabs/CardTab.tsx index bc3029103..b3c55d07f 100644 --- a/src/equicordplugins/components.dev/components/tabs/CardTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/CardTab.tsx @@ -5,103 +5,113 @@ */ import { Card, CardVariants, Paragraph } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; + +const CARD_PROPS: PropDef[] = [ + { name: "variant", type: '"primary" | "warning" | "danger" | "success" | "brand"', default: '"primary"', description: "Visual style variant controlling background color." }, + { name: "outline", type: "boolean", default: "false", description: "Renders with a border outline instead of filled background." }, + { name: "defaultPadding", type: "boolean", description: "Adds 1em padding to the card. Defaults to true when no className is provided, false otherwise." }, + { name: "children", type: "ReactNode", description: "Card content." }, + { name: "className", type: "string", description: "Additional CSS class. When set, defaultPadding becomes false unless explicitly overridden." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, + { name: "...restProps", type: "HTMLDivAttributes", description: "All other standard div attributes are forwarded to the root element." }, +]; export default function CardTab() { return ( -
- - - Card component from @components/Card with different severity styles. - -
- {CardVariants.map(variant => ( - - - {variant.charAt(0).toUpperCase() + variant.slice(1)} Card - - - This is a {variant} card variant. Use for { - variant === "primary" ? "general content" : - variant === "warning" ? "cautionary messages" : - variant === "danger" ? "error or destructive actions" : - variant === "success" ? "positive feedback" : - "branded content" - }. - + \n Content here\n', + relevantProps: ["variant", "children"], + children: ( +
+ {CardVariants.map(variant => ( + + + {variant.charAt(0).toUpperCase() + variant.slice(1)} + + + {variant === "primary" ? "General purpose content container." : + variant === "warning" ? "Cautionary or attention-needed messages." : + variant === "danger" ? "Error states or destructive actions." : + variant === "success" ? "Positive feedback or confirmations." : + "Branded or highlighted content."} + + + ))} +
+ ) + }, + { + title: "Outline Mode", + description: "Uses a border instead of a filled background.", + code: '\n Heads up!\n', + relevantProps: ["outline"], + children: ( +
+ {CardVariants.map(variant => ( + + {variant} outline + + ))} +
+ ) + }, + { + title: "Padding Control", + description: "Cards get 1em padding by default when no className is set. You can override this behavior explicitly.", + code: '\n Keeps padding despite custom className\n', + relevantProps: ["defaultPadding", "className"], + children: ( +
+ + defaultPadding=true + + + defaultPadding=false (content adds own padding) + +
+ ) + }, + { + title: "Nested Cards", + description: "Cards can nest for complex layouts.", + children: ( + + Outer Card + + Nested warning card + + + Nested danger card + - ))} -
-
- - - - Cards have 1em padding by default when no className is passed. - -
-
- - defaultPadding=true (or no className) - - - Card with default padding + ) + }, + { + title: "Custom Styling", + description: "Cards accept custom styles and all div attributes.", + relevantProps: ["style"], + children: ( + + Card with custom border accent -
-
- - defaultPadding=false - - - Card without default padding (content adds own) - -
-
-
- - - - Cards can be nested for complex layouts. - - - Outer Card - - Nested warning card - - - Nested danger card - - - - - - - Cards accept custom className and style props. - - - Card with custom border accent - - - - - • variant - "primary" | "warning" | "danger" | "success" | "brand" (default: "primary") - • defaultPadding - Add 1em padding (default: true if no className) - • className - Custom CSS class - • style - Inline styles - • children - Card content - • ...restProps - Any other div attributes - - - - - {"import { Card } from \"@components/Card\";"} - - -
+ ) + }, + ]} + props={CARD_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx b/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx index d44b102d6..807c1154f 100644 --- a/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/CheckboxGroupTab.tsx @@ -4,9 +4,9 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { CheckboxGroupOption, ManaCheckboxGroup, Paragraph, useState } from ".."; +import { CheckboxGroupOption, ManaCheckboxGroup, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; import { EquicordIcon } from "../icons/EquicordIcon"; -import { SectionWrapper } from "../SectionWrapper"; const BASIC_OPTIONS: CheckboxGroupOption[] = [ { value: "option1", label: "Option 1" }, @@ -32,89 +32,102 @@ const OPTIONS_MIXED: CheckboxGroupOption[] = [ { value: "another", label: "Another enabled" }, ]; +const GROUP_PROPS: PropDef[] = [ + { name: "options", type: "CheckboxGroupOption[]", required: true, description: "Array of checkbox options to render." }, + { name: "selectedValues", type: "(string | number)[]", required: true, description: "Currently selected option values." }, + { name: "onChange", type: "(values: (string | number)[]) => void", description: "Called with the updated selection array when any checkbox is toggled." }, + { name: "disabled", type: "boolean", default: "false", description: "Disables the entire group. Individual option disabled flags still apply." }, + { name: "label", type: "string", description: "Field label displayed above the group." }, + { name: "hideLabel", type: "boolean", description: "Visually hides the label while keeping it accessible." }, + { name: "badge", type: "ReactNode", description: "Badge element displayed next to the label." }, + { name: "icon", type: "ReactNode", description: "Icon element displayed next to the label." }, + { name: "required", type: "boolean", description: "Marks the field as required." }, + { name: "description", type: "string", description: "Description text displayed below the label." }, + { name: "helperText", type: "string", description: "Helper text displayed below the group." }, + { name: "errorMessage", type: "string", description: "Error message displayed below the group." }, + { name: "successMessage", type: "string", description: "Success message displayed below the group." }, + { name: "id", type: "string", description: "Custom ID for the control element." }, + { name: "layout", type: "string", description: "Field layout variant." }, + { name: "layoutConfig", type: "Record", description: "Configuration for the field layout." }, +]; + +const OPTION_PROPS: PropDef[] = [ + { name: "value", type: "string | number", required: true, description: "Unique identifier for this option." }, + { name: "label", type: "string", required: true, description: "Label text displayed next to the checkbox." }, + { name: "description", type: "string", description: "Description text below the label." }, + { name: "disabled", type: "boolean", description: "Disables this individual option." }, + { name: "leadingIcon", type: "ComponentType", description: "Icon component displayed before the checkbox." }, +]; + +function BasicDemo() { + const [selected, setSelected] = useState<(string | number)[]>(["option1"]); + return ; +} + +function DescriptionDemo() { + const [selected, setSelected] = useState<(string | number)[]>(["notifications", "sounds"]); + return ; +} + +function IconDemo() { + const [selected, setSelected] = useState<(string | number)[]>(["item1"]); + return ; +} + +function MixedDemo() { + const [selected, setSelected] = useState<(string | number)[]>(["enabled"]); + return ; +} + export default function CheckboxGroupTab() { - const [basicSelected, setBasicSelected] = useState<(string | number)[]>(["option1"]); - const [descSelected, setDescSelected] = useState<(string | number)[]>(["notifications", "sounds"]); - const [iconSelected, setIconSelected] = useState<(string | number)[]>(["item1"]); - const [mixedSelected, setMixedSelected] = useState<(string | number)[]>(["enabled"]); - return ( -
- - - - - - - - - - - - - - - - - - - - - - - CheckboxGroup - - - • options: CheckboxGroupOption[] - Array of options - - - • selectedValues: (string | number)[] - Currently selected values - - - • onChange?: (values: (string | number)[]) => void - Called when selection changes - - - • disabled?: boolean - Disable entire group - - - CheckboxGroupOption - - - • value: string | number - Option value - - - • label: string - Option label - - - • description?: string - Optional description - - - • disabled?: boolean - Disable individual option - - - • leadingIcon?: React.ComponentType - Icon before checkbox - - -
+ , + code: '', + relevantProps: ["options", "selectedValues", "onChange"], + }, + { + title: "With Descriptions", + description: "Each option can have a description below its label.", + children: , + }, + { + title: "With Leading Icons", + description: "Options can display an icon component before the checkbox.", + children: , + }, + { + title: "With Disabled Option", + description: "Individual options can be disabled while others remain interactive.", + children: , + }, + { + title: "Entire Group Disabled", + description: "The disabled prop on the group disables all options.", + children: ( + + ), + relevantProps: ["disabled"], + }, + ]} + props={[ + { title: "ManaCheckboxGroup", props: GROUP_PROPS }, + { title: "CheckboxGroupOption", props: OPTION_PROPS }, + ]} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx index 3d96ca843..c9312fa16 100644 --- a/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/CheckboxTab.tsx @@ -4,147 +4,144 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaCheckbox, Paragraph, useState } from ".."; +import { ManaCheckbox, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; import { EquicordIcon } from "../icons/EquicordIcon"; -import { SectionWrapper } from "../SectionWrapper"; -export default function CheckboxTab() { - const [basicChecked, setBasicChecked] = useState(true); - const [basicUnchecked, setBasicUnchecked] = useState(false); +const CHECKBOX_PROPS: PropDef[] = [ + { name: "checked", type: "boolean", required: true, description: "Current checked state." }, + { name: "onChange", type: "(checked: boolean) => void", description: "Called when the checkbox is toggled." }, + { name: "label", type: "string", description: "Label text displayed next to the checkbox." }, + { name: "description", type: "string", description: "Description text below the label." }, + { name: "labelType", type: '"primary" | "secondary"', default: '"primary"', description: "Label text styling. Primary uses normal weight, secondary uses muted color." }, + { name: "leadingIcon", type: "ComponentType", description: "Icon component displayed before the checkbox." }, + { name: "usageVariant", type: '"single" | "indicator"', default: '"single"', internal: true, description: "Visual style. Single has full padding and spacing, indicator is compact." }, + { name: "disabled", type: "boolean", default: "false", description: "Disables the checkbox interaction." }, + { name: "displayOnly", type: "boolean", internal: true, description: "Renders the checkbox in a display-only state without interaction." }, + { name: "value", type: "string | number", description: "Value identifier used when inside a CheckboxGroup." }, + { name: "labeledBy", type: "string", internal: true, description: "ID of an external element that labels this checkbox for accessibility." }, +]; - const [withDesc, setWithDesc] = useState(true); - - const [labelPrimary, setLabelPrimary] = useState(true); - const [labelSecondary, setLabelSecondary] = useState(false); - - const [withIcon, setWithIcon] = useState(true); - - const [usageSingle, setUsageSingle] = useState(true); - const [usageIndicator, setUsageIndicator] = useState(false); +function BasicDemo() { + const [checked, setChecked] = useState(true); + const [unchecked, setUnchecked] = useState(false); return ( -
- -
- - -
-
- - -
- -
-
- - -
- - -
-
- - -
- -
-
- - -
- - - -
-
- - -
- - -
-
- - - - • checked: boolean - Current checked state - - - • onChange?: (checked: boolean) => void - Called when toggled - - - • label?: string - Label text - - - • description?: string - Description below label - - - • labelType?: "primary" | "secondary" - Label styling - - - • leadingIcon?: React.ComponentType - Icon before checkbox - - - • usageVariant?: "single" | "indicator" - Visual style - - - • disabled?: boolean - Disable interaction - - - • displayOnly?: boolean - Display without interaction - - +
+ +
); } + +function DescriptionDemo() { + const [checked, setChecked] = useState(true); + + return ( + + ); +} + +function LabelTypesDemo() { + const [primary, setPrimary] = useState(true); + const [secondary, setSecondary] = useState(false); + + return ( +
+ + +
+ ); +} + +function IconDemo() { + const [checked, setChecked] = useState(true); + + return ( + + ); +} + +function UsageVariantsDemo() { + const [single, setSingle] = useState(true); + const [indicator, setIndicator] = useState(false); + + return ( +
+ + +
+ ); +} + +export default function CheckboxTab() { + return ( + , + code: '', + relevantProps: ["checked", "onChange", "label"], + }, + { + title: "With Description", + description: "A description renders below the label in smaller text.", + children: , + code: '', + relevantProps: ["description"], + }, + { + title: "Label Types", + description: "Primary uses normal text styling, secondary uses a muted, lighter appearance.", + children: , + relevantProps: ["labelType"], + }, + { + title: "With Leading Icon", + description: "An icon component displayed before the checkbox indicator.", + children: , + code: '', + relevantProps: ["leadingIcon"], + }, + { + title: "States", + description: "Disabled prevents interaction. Display only renders without any interactive behavior.", + children: ( +
+ + + +
+ ), + relevantProps: ["disabled", "displayOnly"], + }, + { + title: "Usage Variants", + description: "Single has full padding and spacing for standalone use. Indicator is compact for inline contexts.", + children: , + relevantProps: ["usageVariant"], + }, + ]} + props={CHECKBOX_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx b/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx index 1a951824c..4122b7c58 100644 --- a/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ChipTab.tsx @@ -5,64 +5,65 @@ */ import { Chip, Paragraph } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; const CHIP_COLORS = ["blurple", "purple", "green", "orange", "yellow", "pink", "red", "gray"] as const; const CHIP_SHADES = ["Light", "Medium", "Dark"] as const; +const CHIP_PROPS: PropDef[] = [ + { name: "text", type: "string", required: true, description: "Text displayed inside the chip." }, + { name: "variant", type: "ChipVariant", default: '"grayLight"', description: "Color variant in the format {color}{shade}. 8 colors (blurple, purple, green, orange, yellow, pink, red, gray) times 3 shades (Light, Medium, Dark) for 24 total variants." }, +]; + export default function ChipTab() { return ( -
- - - All 24 chip variants: 8 colors × 3 shades. - - {CHIP_SHADES.map(shade => ( -
- - {shade} - -
- {CHIP_COLORS.map(color => ( - + + {CHIP_SHADES.map(shade => ( +
+ + {shade} + +
+ {CHIP_COLORS.map(color => ( + + ))} +
+
))} + + ), + code: '', + relevantProps: ["variant"], + }, + { + title: "Use Cases", + description: "Common patterns for status labels and tags.", + children: ( +
+ + + + + +
-
- ))} - - - -
- - - - - - -
-
- - - - • text: string - Text to display inside the chip - - - • variant?: ChipVariant - Color variant (default: "grayLight") - - - Variant Format: {"{color}{shade}"} where: - - - • Colors: blurple, purple, green, orange, yellow, pink, red, gray - - - • Shades: Light, Medium, Dark - - -
+ ), + }, + ]} + props={CHIP_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx b/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx index 6caf42699..7cc8e07ae 100644 --- a/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ClickableTab.tsx @@ -5,223 +5,137 @@ */ import { Clickable, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function ClickableTab() { - const [clickCount, setClickCount] = useState(0); - const [lastAction, setLastAction] = useState("None"); +const CLICKABLE_PROPS: PropDef[] = [ + { name: "tag", type: "keyof JSX.IntrinsicElements", default: '"div"', description: "HTML element to render as." }, + { name: "onClick", type: "MouseEventHandler", description: "Click handler. When absent, renders as a non-interactive element without keyboard handling." }, + { name: "onKeyPress", type: "KeyboardEventHandler", description: "Key press handler called after internal Enter/Space handling." }, + { name: "role", type: "string", default: '"button"', description: "ARIA role attribute." }, + { name: "tabIndex", type: "number", default: "0", description: "Tab index for keyboard navigation." }, + { name: "ignoreKeyPress", type: "boolean", description: "When true, Enter and Space keys will not trigger onClick." }, + { name: "focusProps", type: "Record", internal: true, description: "Props forwarded to the focus management wrapper." }, + { name: "innerRef", type: "Ref", internal: true, description: "Ref to the underlying DOM element." }, + { name: "href", type: "string", description: "Optional href. Affects key handling behavior (Space won't preventDefault when set)." }, + { name: "className", type: "string", description: "Additional CSS class name." }, + { name: "children", type: "ReactNode", description: "Child elements." }, +]; + +const boxStyle: React.CSSProperties = { + padding: "8px 16px", + backgroundColor: "var(--background-secondary)", + borderRadius: 4, + cursor: "pointer", +}; + +function BasicDemo() { + const [count, setCount] = useState(0); return ( -
- - - Default clickable element. Click count: {clickCount} - - setClickCount(c => c + 1)} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-secondary)", - borderRadius: 4, - cursor: "pointer" - }} - > - Click me! - - - - - - Clickable can render as different HTML elements using the tag prop. - -
- setLastAction("Clicked div")} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-tertiary)", - borderRadius: 4, - cursor: "pointer" - }} - > - tag="div" - - setLastAction("Clicked span")} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-tertiary)", - borderRadius: 4, - cursor: "pointer" - }} - > - tag="span" - - setLastAction("Clicked button")} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-tertiary)", - borderRadius: 4, - cursor: "pointer", - border: "none", - color: "inherit" - }} - > - tag="button" - -
- - Last action: {lastAction} - -
- - - - Accessibility props for keyboard navigation. - -
- setLastAction("Role: button")} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-secondary)", - borderRadius: 4, - cursor: "pointer" - }} - > - role="button" tabIndex=0 - - setLastAction("Role: link")} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-secondary)", - borderRadius: 4, - cursor: "pointer" - }} - > - role="link" tabIndex=0 - -
-
- - - - When ignoreKeyPress is true, Enter/Space won't trigger onClick. - -
- setLastAction("Normal (keys work)")} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-secondary)", - borderRadius: 4, - cursor: "pointer" - }} - > - Normal (try Enter/Space) - - setLastAction("Ignored (keys blocked)")} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-secondary)", - borderRadius: 4, - cursor: "pointer" - }} - > - ignoreKeyPress (keys blocked) - -
-
- - - - When no onClick is provided, renders as non-interactive element. - - - Non-interactive (no onClick) - - - - - - Clickable with different visual styles. - -
- setLastAction("Card clicked")} - style={{ - padding: 16, - backgroundColor: "var(--background-secondary)", - borderRadius: 8, - cursor: "pointer", - width: 120, - textAlign: "center" - }} - > -
📁
-
Card Style
-
- setLastAction("Chip clicked")} - style={{ - padding: "4px 12px", - backgroundColor: "var(--brand-experiment)", - borderRadius: 16, - cursor: "pointer", - color: "white" - }} - > - Chip Style - - setLastAction("List item clicked")} - style={{ - padding: "8px 16px", - backgroundColor: "var(--background-secondary)", - cursor: "pointer", - width: 200 - }} - > - List Item Style → - -
-
- - - - Clickable - Interactive element wrapper - - • tag?: keyof JSX.IntrinsicElements - HTML element to render (default: div) - • onClick?: MouseEventHandler - Click handler - • onKeyPress?: KeyboardEventHandler - Key press handler - • focusProps?: object - Props for FocusLock wrapper - • innerRef?: Ref - Ref to the inner element - • role?: string - ARIA role attribute - • tabIndex?: number - Tab index for keyboard navigation - • ignoreKeyPress?: boolean - Disable Enter/Space triggering onClick - • href?: string - Optional href (affects key handling) - • className?: string - CSS class name - • children?: ReactNode - Child elements - -
+ <> + Click count: {count} + setCount(c => c + 1)} style={boxStyle}> + Click me! + + + ); +} + +function TagDemo() { + const [last, setLast] = useState("None"); + + return ( + <> +
+ setLast("div")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)" }}> + tag="div" + + setLast("span")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)" }}> + tag="span" + + setLast("button")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)", border: "none", color: "inherit" }}> + tag="button" + +
+ Last clicked: {last} + + ); +} + +function KeyPressDemo() { + const [last, setLast] = useState("None"); + + return ( + <> +
+ setLast("Normal (keys work)")} style={boxStyle}> + Normal (try Enter/Space) + + setLast("Keys blocked")} style={boxStyle}> + ignoreKeyPress (keys blocked) + +
+ Last action: {last} + + ); +} + +export default function ClickableTab() { + return ( + , + code: " doSomething()} style={myStyles}>\n Click me!\n", + relevantProps: ["onClick", "children"], + }, + { + title: "Custom Tag", + description: "Clickable can render as any HTML element using the tag prop.", + children: , + code: '\n I am a button element\n', + relevantProps: ["tag"], + }, + { + title: "Role and TabIndex", + description: "Accessibility props for keyboard navigation. Defaults to role=\"button\" and tabIndex=0.", + children: ( +
+ { }} style={boxStyle}> + role="button" + + { }} style={boxStyle}> + role="link" + +
+ ), + relevantProps: ["role", "tabIndex"], + }, + { + title: "Ignore Key Press", + description: "When ignoreKeyPress is true, Enter and Space will not trigger onClick. Only mouse clicks work.", + children: , + relevantProps: ["ignoreKeyPress"], + }, + { + title: "Non-Interactive", + description: "When no onClick is provided, renders without keyboard handling or interactive behavior.", + children: ( + + Non-interactive (no onClick) + + ), + }, + ]} + props={CLICKABLE_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/CodeBlockTab.tsx b/src/equicordplugins/components.dev/components/tabs/CodeBlockTab.tsx index e1e2ff999..691ca276a 100644 --- a/src/equicordplugins/components.dev/components/tabs/CodeBlockTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/CodeBlockTab.tsx @@ -4,35 +4,19 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { findCssClassesLazy } from "@webpack"; -import { Parser } from "@webpack/common"; +import { CodeBlock } from "@components/CodeBlock"; -import { Paragraph } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { InlineCode, Paragraph } from ".."; +import { DocPage, type PropDef } from "../DocPage"; -const MarkupClasses = findCssClassesLazy("markup", "codeContainer"); +const CODEBLOCK_PROPS: PropDef[] = [ + { name: "content", type: "string", description: "The code text to render inside the block." }, + { name: "lang", type: "string", required: true, description: "Language identifier for syntax highlighting (e.g. \"tsx\", \"js\", \"css\", \"py\"). Use empty string for no highlighting." }, +]; -interface CodeBlockProps { - content: string; - language?: string; -} - -function CodeBlock({ content, language = "" }: CodeBlockProps) { - const codeBlock = `\`\`\`${language}\n${content}\n\`\`\``; - return ( -
- {Parser.parse(codeBlock)} -
- ); -} - -function InlineCode({ children }: { children: React.ReactNode; }) { - return ( - - {children} - - ); -} +const INLINECODE_PROPS: PropDef[] = [ + { name: "children", type: "ReactNode", required: true, description: "Content to render as inline code." }, +]; const JS_EXAMPLE = `function greet(name) { return \`Hello, \${name}!\`; @@ -78,65 +62,61 @@ const JSON_EXAMPLE = `{ } }`; -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 ( -
- - - Use InlineCode component for inline code snippets. - - I really like cats so yeah - Use console.log() to debug your code. - - Combine const with let and - use === for strict equality. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Use Parser.parse() from @webpack/common to - render markdown with code blocks. Wrap in markup class for proper styling. - - -
+ + I really like cats so yeah + Use console.log() to debug your code. + + Combine const with let and + use === for strict equality. + + + ), + code: "Use console.log() to debug.", + }, + { + title: "JavaScript", + children: , + code: '', + relevantProps: ["content", "lang"], + }, + { + title: "TypeScript", + children: , + }, + { + title: "CSS", + children: , + }, + { + title: "Python", + children: , + }, + { + title: "JSON", + children: , + }, + { + title: "Plain Text", + description: "Use an empty string for lang to render without syntax highlighting.", + children: , + code: '', + }, + ]} + props={[ + { title: "CodeBlock", props: CODEBLOCK_PROPS }, + { title: "InlineCode", props: INLINECODE_PROPS }, + ]} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx b/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx index 597a3fdb6..a27c59346 100644 --- a/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ColorPickerTab.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ColorPicker, ColorPickerWithSwatches, ColorSwatch, CustomColorButton, DefaultColorButton, Paragraph, PresetColors, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ColorPicker, ColorPickerWithSwatches, ColorSwatch, DefaultColorButton, Paragraph, PresetColors, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; const DEFAULT_COLOR = 0x5865F2; @@ -13,245 +13,230 @@ function intToHex(color: number): string { return "#" + color.toString(16).padStart(6, "0"); } -export default function ColorPickerTab() { - const [basicColor, setBasicColor] = useState(DEFAULT_COLOR); - const [suggestedColor, setSuggestedColor] = useState(0xE74C3C); - const [swatchColor, setSwatchColor] = useState(0x3498DB); - const [swatchSelected, setSwatchSelected] = useState(0x2ECC71); - const [customSwatchColor, setCustomSwatchColor] = useState(0x9B59B6); - const [defaultBtnColor, setDefaultBtnColor] = useState(DEFAULT_COLOR); - const [customBtnColor, setCustomBtnColor] = useState(0xE91E63); +const PICKER_PROPS: PropDef[] = [ + { name: "color", type: "number", required: true, description: "Current color as an integer (e.g. 0x5865F2)." }, + { name: "onChange", type: "(color: number) => void", required: true, description: "Called with the new color integer when changed." }, + { name: "onClose", type: "() => void", description: "Called when the picker popout closes." }, + { name: "suggestedColors", type: "string[]", description: "Array of hex color strings shown as suggestions in the popout." }, + { name: "disabled", type: "boolean", description: "Disables the color picker." }, + { name: "label", type: "ReactNode", description: "Optional label displayed next to the swatch." }, + { name: "colorPickerMiddle", type: "ReactNode", internal: true, description: "Content injected in the middle of the popout." }, + { name: "colorPickerFooter", type: "ReactNode", internal: true, description: "Content injected in the footer of the popout." }, + { name: "showEyeDropper", type: "boolean", description: "Shows the eye dropper tool in the popout." }, +]; + +const SWATCH_PROPS: PropDef[] = [ + { name: "color", type: "number", description: "Color value. Omit for a \"no color\" state." }, + { name: "isDefault", type: "boolean", description: "Renders in the larger default button style." }, + { name: "isCustom", type: "boolean", description: "Shows a dropper icon overlay." }, + { name: "isSelected", type: "boolean", description: "Shows a checkmark overlay." }, + { name: "disabled", type: "boolean", description: "Disables the swatch." }, + { name: "onClick", type: "(color) => void", description: "Click handler receiving the color or gradient object." }, + { name: "isGradient", type: "boolean", description: "Enables gradient rendering mode." }, + { name: "gradientStart", type: "number", description: "Start color for gradients." }, + { name: "gradientEnd", type: "number", description: "End color for gradients." }, + { name: "gradientDegrees", type: "number", default: "180", description: "Angle in degrees for the gradient." }, + { name: "aria-label", type: "string", description: "Accessibility label." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, +]; + +const SWATCHES_PROPS: PropDef[] = [ + { name: "defaultColor", type: "number", required: true, description: "Default/reset color value." }, + { name: "colors", type: "number[] | GradientColor[]", required: true, description: "Array of preset color values or gradient definitions." }, + { name: "value", type: "number", required: true, description: "Currently selected color." }, + { name: "onChange", type: "(color: number) => void", required: true, description: "Called when a color is selected." }, + { name: "customColor", type: "number", description: "Current custom color for the custom button." }, + { name: "secondaryValue", type: "number", description: "Secondary color value for gradient mode." }, + { name: "disabled", type: "boolean", description: "Disables all swatches." }, + { name: "renderDefaultButton", type: "(props) => ReactNode", description: "Render prop for the default/reset button." }, + { name: "renderCustomButton", type: "(props) => ReactNode", description: "Render prop for the custom color button." }, + { name: "isGradient", type: "boolean", description: "Enables gradient mode for all swatches." }, + { name: "gradientDegrees", type: "number", description: "Gradient angle for all swatches." }, + { name: "allowBlackCustomColor", type: "boolean", description: "Allows selecting pure black (#000000)." }, + { name: "className", type: "string", description: "Additional CSS class on the root." }, + { name: "colorContainerClassName", type: "string", description: "CSS class on the swatch grid container." }, +]; + +function BasicPickerDemo() { + const [color, setColor] = useState(DEFAULT_COLOR); return ( -
- - - Click the swatch to open a full color picker popout. Selected: {intToHex(basicColor)} - - - + <> + Selected: {intToHex(color)} + + + ); +} - - - Includes the eye dropper tool for picking colors from screen. - - - +function EyeDropperDemo() { + const [color, setColor] = useState(DEFAULT_COLOR); - - - Popout shows suggested color swatches. Selected: {intToHex(suggestedColor)} - - - + return ; +} - - { }} - disabled - /> - +function SuggestedDemo() { + const [color, setColor] = useState(0xE74C3C); - - - Individual color swatch buttons. Click to select. - -
- {PresetColors.slice(0, 10).map(color => ( - setSwatchSelected(color)} - /> - ))} -
-
+ return ( + <> + Selected: {intToHex(color)} + + + ); +} - - - Different swatch states: default, custom, selected, disabled, no color. - -
-
- - Normal -
-
- - Selected -
-
- - Default -
-
- - Custom -
-
- - Disabled -
-
- - No Color -
-
-
+function SwatchSelectDemo() { + const [selected, setSelected] = useState(0x2ECC71); - - - Gradient swatches with customizable angle. - -
- - - -
-
- - - - The "default" button used in ColorPickerWithSwatches. Shows checkmark when selected. - -
- - Selected: {defaultBtnColor === DEFAULT_COLOR ? "Yes" : "No"} -
-
- - - - The "custom" button for custom color selection. Shows dropper icon. - - - - - - - Full swatch grid with preset colors. Selected: {intToHex(swatchColor)} - - null} - renderCustomButton={() => null} - /> - - - - - Full picker with default reset button and custom color picker. Selected: {intToHex(customSwatchColor)} - - ( - - )} - renderCustomButton={props => ( - - )} - /> - - - - - ColorPicker - Full color picker with popout - - • color: number - Current color as integer - • onChange: (color: number) ={">"} void - Change callback - • onClose?: () ={">"} void - Called when picker closes - • suggestedColors?: string[] - Hex color strings for suggestions - • disabled?: boolean - Disable interaction - • label?: ReactNode - Optional label next to swatch - • colorPickerMiddle?: ReactNode - Content in middle of popout - • colorPickerFooter?: ReactNode - Content in footer of popout - • showEyeDropper?: boolean - Show eye dropper tool - - - ColorSwatch - Individual color swatch button - - • color?: number - Color value (null for "no color") - • isDefault?: boolean - Larger "default" style - • isCustom?: boolean - Shows dropper icon - • isSelected?: boolean - Shows checkmark - • disabled?: boolean - Disable interaction - • onClick?: (color) ={">"} void - Click callback - • isGradient?: boolean - Gradient mode - • gradientStart/End?: number - Gradient colors - • gradientDegrees?: number - Gradient angle (default 180) - - - ColorPickerWithSwatches - Swatch grid with render props - - • defaultColor: number - Default/reset color - • customColor?: number - Current custom color - • colors: number[] - Array of preset colors - • value: number - Selected color - • onChange: (color: number) ={">"} void - Change callback - • renderDefaultButton?: (props) ={">"} ReactNode - • renderCustomButton?: (props) ={">"} ReactNode - • isGradient?: boolean - Gradient mode - • gradientDegrees?: number - Gradient angle - • allowBlackCustomColor?: boolean - Allow #000000 - - - Color Conversion - - • Hex to int: parseInt(hex.replace("#", ""), 16) - • Int to hex: "#" + color.toString(16).padStart(6, "0") - + return ( +
+ {PresetColors.slice(0, 10).map(color => ( + setSelected(color)} /> + ))}
); } + +function SwatchesGridDemo() { + const [color, setColor] = useState(0x3498DB); + + return ( + <> + Selected: {intToHex(color)} + null} + renderCustomButton={() => null} + /> + + ); +} + +function FullPickerDemo() { + const [color, setColor] = useState(0x9B59B6); + + return ( + <> + Selected: {intToHex(color)} + } + renderCustomButton={() => ( + + )} + /> + + ); +} + +export default function ColorPickerTab() { + return ( + , + code: "", + relevantProps: ["color", "onChange"], + }, + { + title: "With Eye Dropper", + description: "Includes the eye dropper tool for picking colors from screen.", + children: , + relevantProps: ["showEyeDropper"], + }, + { + title: "With Suggested Colors", + description: "Shows suggested color swatches inside the popout.", + children: , + code: '', + relevantProps: ["suggestedColors"], + }, + { + title: "Disabled", + children: { }} disabled />, + relevantProps: ["disabled"], + }, + { + title: "Color Swatches", + description: "Individual swatch buttons. Click to select.", + children: , + code: " setSelected(0x5865F2)} />", + }, + { + title: "Swatch States", + description: "Normal, selected, default, custom, disabled, and no-color states.", + children: ( +
+ {([ + ["Normal", {}], + ["Selected", { isSelected: true }], + ["Default", { isDefault: true }], + ["Custom", { isCustom: true }], + ["Disabled", { disabled: true }], + ] as const).map(([label, props]) => ( +
+ + {label} +
+ ))} +
+ + No Color +
+
+ ), + relevantProps: ["isSelected", "isDefault", "isCustom", "disabled"], + }, + { + title: "Gradient Swatches", + description: "Gradient swatches with customizable angle.", + children: ( +
+ + + +
+ ), + relevantProps: ["isGradient", "gradientStart", "gradientEnd", "gradientDegrees"], + }, + { + title: "Swatch Grid", + description: "ColorPickerWithSwatches renders a grid of preset color swatches.", + children: , + code: " null}\n renderCustomButton={() => null}\n/>", + }, + { + title: "Full Picker with Buttons", + description: "Swatch grid with default reset button and custom color picker using render props.", + children: , + relevantProps: ["renderDefaultButton", "renderCustomButton"], + }, + ]} + props={[ + { title: "ColorPicker", props: PICKER_PROPS }, + { title: "ColorSwatch", props: SWATCH_PROPS }, + { title: "ColorPickerWithSwatches", props: SWATCHES_PROPS }, + ]} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx index 125a140e0..581787451 100644 --- a/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ComboboxTab.tsx @@ -4,8 +4,25 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaButton, ManaCombobox, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaCombobox, Paragraph, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; + +const COMBOBOX_PROPS: PropDef[] = [ + { name: "placeholder", type: "string", description: "Placeholder text for the search input." }, + { name: "value", type: "T | T[]", description: "Currently selected value or array of values for multi-select." }, + { name: "onChange", type: "(value: T | T[]) => void", description: "Called when the selection changes." }, + { name: "multiSelect", type: "boolean", default: "false", description: "Enable multiple selection mode." }, + { name: "autoFocus", type: "boolean", default: "false", description: "Focus the search input on mount." }, + { name: "maxVisibleItems", type: "number", default: "5", description: "Maximum items visible before scrolling." }, + { name: "itemToString", type: "(item: T) => string", description: "Converts an item to a string for display." }, + { name: "emptyStateHeader", type: "string", description: "Header text shown when no results match the query." }, + { name: "emptyStateText", type: "string", description: "Body text shown when no results match the query." }, + { name: "onQueryChange", type: "(query: string) => void", description: "Called when the search query changes. Useful for external filtering." }, + { name: "aria-label", type: "string", description: "Accessibility label for the combobox." }, + { name: "className", type: "string", description: "CSS class for the combobox container." }, + { name: "listClassName", type: "string", description: "CSS class for the dropdown list." }, + { name: "children", type: "(query: string) => ReactNode[]", required: true, description: "Render function receiving the search query. Return filterable items." }, +]; const SAMPLE_ITEMS = [ { id: "1", label: "Apple" }, @@ -36,102 +53,148 @@ function ComboboxItem({ item, isSelected, onClick }: { item: { id: string; label ); } -export default function ComboboxTab() { - const [singleValue, setSingleValue] = useState(""); - const [multiValue, setMultiValue] = useState([]); - const [showDemo, setShowDemo] = useState(false); +function filterItems(query: string) { + return SAMPLE_ITEMS.filter(item => + item.label.toLowerCase().includes(query.toLowerCase()) + ); +} - const filterItems = (query: string) => - SAMPLE_ITEMS.filter(item => - item.label.toLowerCase().includes(query.toLowerCase()) - ); +function SingleSelectDemo() { + const [value, setValue] = useState(""); return ( -
- - - Searchable dropdown with render props pattern. Children is a function - that receives the search query and returns filterable items. - - setShowDemo(!showDemo)} - /> - - - {showDemo && ( - <> - - - Selected: {singleValue || "None"} - -
- setSingleValue(v as string)} - aria-label="Select a fruit" - maxVisibleItems={5} - emptyStateHeader="No results" - emptyStateText="Try a different search term" - > - {query => filterItems(query).map(item => ( - setSingleValue(item.id)} - /> - ))} - -
-
- - - - Selected: {multiValue.length ? multiValue.join(", ") : "None"} - -
- setMultiValue(v as string[])} - multiSelect - aria-label="Select fruits" - maxVisibleItems={5} - > - {query => filterItems(query).map(item => ( - { - if (multiValue.includes(item.id)) { - setMultiValue(multiValue.filter(v => v !== item.id)); - } else { - setMultiValue([...multiValue, item.id]); - } - }} - /> - ))} - -
-
- - )} - - - • placeholder - Input placeholder text - • value - Selected value(s) - • onChange - Selection change callback - • multiSelect - Enable multiple selection - • autoFocus - Focus input on mount - • maxVisibleItems - Max items before scroll (default 5) - • emptyStateHeader - Header when no results - • emptyStateText - Text when no results - • children - Function: (query: string) => ReactNode[] - +
+ + Selected: {SAMPLE_ITEMS.find(i => i.id === value)?.label ?? "None"} + + setValue(v as string)} + aria-label="Select a fruit" + maxVisibleItems={5} + emptyStateHeader="No results" + emptyStateText="Try a different search term" + > + {query => filterItems(query).map(item => ( + setValue(item.id)} + /> + ))} +
); } + +function MultiSelectDemo() { + const [values, setValues] = useState([]); + + return ( +
+ + Selected: {values.length ? values.map(v => SAMPLE_ITEMS.find(i => i.id === v)?.label).join(", ") : "None"} + + setValues(v as string[])} + multiSelect + aria-label="Select fruits" + maxVisibleItems={5} + > + {query => filterItems(query).map(item => ( + { + if (values.includes(item.id)) { + setValues(values.filter(v => v !== item.id)); + } else { + setValues([...values, item.id]); + } + }} + /> + ))} + +
+ ); +} + +function EmptyStateDemo() { + return ( +
+ { }} + aria-label="Empty state demo" + emptyStateHeader="Nothing found" + emptyStateText="We couldn't find what you're looking for." + > + {() => []} + +
+ ); +} + +export default function ComboboxTab() { + return ( + , + code: ` setValue(v as string)} + aria-label="Select a fruit" + maxVisibleItems={5} + emptyStateHeader="No results" + emptyStateText="Try a different search term" +> + {query => filterItems(query).map(item => ( + + ))} +`, + relevantProps: ["value", "onChange", "placeholder", "maxVisibleItems"], + }, + { + title: "Multi Selection", + description: "Enable multiSelect to allow choosing multiple items at once.", + children: , + code: ` setValues(v as string[])} + multiSelect + aria-label="Select fruits" +> + {query => filterItems(query).map(item => ( + + ))} +`, + relevantProps: ["multiSelect"], + }, + { + title: "Empty State", + description: "Custom header and text shown when the children function returns an empty array.", + children: , + relevantProps: ["emptyStateHeader", "emptyStateText"], + }, + ]} + props={COMBOBOX_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/DividerTab.tsx b/src/equicordplugins/components.dev/components/tabs/DividerTab.tsx index 509c57ebd..c23200a8d 100644 --- a/src/equicordplugins/components.dev/components/tabs/DividerTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/DividerTab.tsx @@ -5,48 +5,52 @@ */ import { Divider, Paragraph } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; + +const DIVIDER_PROPS: PropDef[] = [ + { name: "className", type: "string", description: "Custom CSS class for the divider." }, + { name: "style", type: "CSSProperties", description: "Inline styles applied to the hr element." }, + { name: "...restProps", type: "HTMLAttributes
", description: "Any other standard hr element attributes." }, +]; export default function DividerTab() { return ( -
- - - Simple horizontal rule from @components/Divider. - -
- Content above divider - - Content below divider -
-
- - - - Dividers with custom margins and styles. - -
- Tight spacing (8px) - - Medium spacing (16px) - - Large spacing (24px) - - End -
-
- - - • className - Custom CSS class - • style - Inline styles - • ...restProps - Any other hr attributes - - - - - {"import { Divider } from \"@components/Divider\";"} - - -
+ + Content above divider + + Content below divider +
+ ), + code: "", + }, + { + title: "Custom Spacing", + description: "Control the spacing around dividers using inline styles.", + children: ( +
+ Tight spacing (8px) + + Medium spacing (16px) + + Large spacing (24px) + + End +
+ ), + code: '', + relevantProps: ["style"], + }, + ]} + props={DIVIDER_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/ErrorBoundaryTab.tsx b/src/equicordplugins/components.dev/components/tabs/ErrorBoundaryTab.tsx index 312bdac4c..9a96f2cb7 100644 --- a/src/equicordplugins/components.dev/components/tabs/ErrorBoundaryTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ErrorBoundaryTab.tsx @@ -5,7 +5,7 @@ */ import { Button, ErrorBoundary, LocalErrorBoundary, Paragraph, SimpleErrorBoundary, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; function ThrowError(): never { throw new Error("This is a demo error thrown intentionally!"); @@ -34,7 +34,81 @@ function CustomFallback({ error, resetErrorBoundary }: { error: Error; resetErro Error: {error.message} - +
+ ); +} + +const ERROR_BOUNDARY_PROPS: PropDef[] = [ + { name: "children", type: "ReactNode", required: true, description: "Content to wrap with error handling." }, + { name: "fallback", type: "ReactNode", description: "Static fallback content to show on error." }, + { name: "fallbackRender", type: "(props: { error, resetErrorBoundary }) => ReactNode", description: "Render function for dynamic fallback with error details and reset capability." }, + { name: "FallbackComponent", type: "ComponentType<{ error, resetErrorBoundary }>", description: "Component that receives error and reset props as fallback." }, + { name: "onError", type: "(error: Error, info: ErrorInfo) => void", description: "Callback fired when an error is caught." }, + { name: "onReset", type: "(details) => void", description: "Callback fired when the boundary is reset." }, + { name: "resetKeys", type: "any[]", description: "Array of values that trigger an automatic reset when changed." }, +]; + +const SIMPLE_PROPS: PropDef[] = [ + { name: "children", type: "ReactNode", required: true, description: "Content to wrap with error handling." }, + { name: "fallback", type: "ReactNode", required: true, description: "Static content to display when an error occurs." }, +]; + +const LOCAL_PROPS: PropDef[] = [ + { name: "children", type: "ReactNode", required: true, description: "Content to wrap. Shows inline error text on failure." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { title: "ErrorBoundary", props: ERROR_BOUNDARY_PROPS }, + { title: "SimpleErrorBoundary", props: SIMPLE_PROPS }, + { title: "LocalErrorBoundary", props: LOCAL_PROPS }, +]; + +function ErrorBoundaryDemo() { + const [key, setKey] = useState(0); + + return ( +
+ ( + + )} + onReset={() => setKey(k => k + 1)} + > + + +
+ ); +} + +function SimpleDemo() { + const [key, setKey] = useState(0); + + return ( +
+ + Something went wrong! + +
+ } + > + + +
+ ); +} + +function LocalDemo() { + const [key, setKey] = useState(0); + + return ( +
+ + + +
@@ -42,93 +116,52 @@ function CustomFallback({ error, resetErrorBoundary }: { error: Error; resetErro } export default function ErrorBoundaryTab() { - const [key1, setKey1] = useState(0); - const [key2, setKey2] = useState(0); - const [key3, setKey3] = useState(0); - return ( -
- - - Full-featured error boundary with fallbackRender, FallbackComponent, onError, onReset, and resetKeys props. - -
- ( - - )} - onError={(error, info) => console.log("ErrorBoundary caught:", error, info)} - onReset={() => setKey1(k => k + 1)} - > - - -
-
- - - - Simple error boundary with just a fallback prop. Shows fallback content when error occurs. - -
- - Something went wrong! - -
- } - > - - -
- - - - - Displays an inline error message. Used for non-critical component errors. - -
- - - - -
-
- - - - ErrorBoundary - - - fallback - Static ReactNode to show on error. - - - fallbackRender - Function receiving error and resetErrorBoundary. - - - FallbackComponent - Component receiving error and resetErrorBoundary props. - - - onError - Callback when error is caught (error, errorInfo). - - - onReset - Callback when boundary is reset. - - - resetKeys - Array of values that trigger reset when changed. - - - - - - Wrap components that may throw errors. Use ErrorBoundary for full control, - SimpleErrorBoundary for basic fallback, or LocalErrorBoundary for inline error display. - - -
+ , + code: ` ( +
+

Error: {error.message}

+ +
+ )} + onError={(error, info) => console.error(error)} + onReset={() => refetch()} +> + +
`, + relevantProps: ["fallbackRender", "onError", "onReset", "resetKeys"], + }, + { + title: "SimpleErrorBoundary", + description: "Lightweight boundary that shows static fallback content on error.", + children: , + code: `Something went wrong!

}> + +
`, + }, + { + title: "LocalErrorBoundary", + description: "Displays an inline error message for non-critical component failures. No fallback prop needed.", + children: , + code: ` + +`, + }, + ]} + props={PROP_GROUPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/FocusLockTab.tsx b/src/equicordplugins/components.dev/components/tabs/FocusLockTab.tsx index fe34629d5..f224c543a 100644 --- a/src/equicordplugins/components.dev/components/tabs/FocusLockTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/FocusLockTab.tsx @@ -5,82 +5,97 @@ */ import { FocusLock, ManaButton, ManaTextInput, Paragraph, useRef, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function FocusLockTab() { - const [focusLockEnabled, setFocusLockEnabled] = useState(false); +const FOCUSLOCK_PROPS: PropDef[] = [ + { name: "containerRef", type: "RefObject", required: true, description: "React ref pointing to the container element that defines the focus trap boundary." }, + { name: "keyboardModeEnabled", type: "boolean", description: "Enable keyboard-based focus trapping. When true, Tab key cycles through focusable elements inside the container." }, + { name: "children", type: "ReactNode", description: "Content rendered inside the focus trap." }, +]; + +function FocusLockDemo() { + const [enabled, setEnabled] = useState(false); const containerRef = useRef(null); const [inputValue, setInputValue] = useState(""); return ( -
- - - FocusLock traps keyboard focus within a container. Enable it and try tabbing - focus stays inside the box. - - setFocusLockEnabled(!focusLockEnabled)} - style={{ marginBottom: 16 }} - /> -
- {focusLockEnabled && ( - - - Focus is now trapped! Tab between these elements: - -
- - { }} - placeholder="Second input" - /> -
- { }} /> - { }} /> - setFocusLockEnabled(false)} - /> -
-
-
- )} - {!focusLockEnabled && ( - - Click "Enable Focus Lock" to activate the focus trap demo. +
+ setEnabled(!enabled)} + style={{ marginBottom: 16 }} + /> +
+ {enabled ? ( + + + Focus is trapped. Try pressing Tab to cycle through these elements: - )} -
- - - - • Modal dialogs - Keep focus within the modal - • Dropdown menus - Trap focus while open - • Settings panels - Improve keyboard navigation - • Accessibility - Ensure screen readers stay in context - - - - • containerRef - React ref to the container element - • keyboardModeEnabled - Enable keyboard focus trapping - • children - Content to render inside the focus trap - +
+ + { }} + placeholder="Second input" + /> +
+ { }} /> + { }} /> + setEnabled(false)} /> +
+
+ + ) : ( + + Click "Enable Focus Lock" to activate the focus trap demo. + + )} +
); } + +export default function FocusLockTab() { + return ( + , + code: `const containerRef = useRef(null); + +
+ + + + + +
`, + relevantProps: ["containerRef"], + }, + ]} + props={FOCUSLOCK_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/GuildIconTab.tsx b/src/equicordplugins/components.dev/components/tabs/GuildIconTab.tsx index 3615d5854..b2e9283de 100644 --- a/src/equicordplugins/components.dev/components/tabs/GuildIconTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/GuildIconTab.tsx @@ -5,107 +5,143 @@ */ import { GuildIcon, GuildIconSizes, GuildStore, ManaButton, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function GuildIconTab() { +const GUILDICON_PROPS: PropDef[] = [ + { name: "guildId", type: "string", required: true, description: "The guild's snowflake ID." }, + { name: "guildName", type: "string", required: true, description: "Guild name, used for the acronym fallback when no icon is available." }, + { name: "guildIcon", type: "string | null", required: true, description: "Icon hash from the guild object, or null for acronym fallback." }, + { name: "iconSize", type: "number", required: true, description: "Size in pixels. Common values: 16, 20, 24, 32, 40, 48, 56, 80, 96." }, + { name: "animate", type: "boolean", description: "Enable animated icons (animated GIF guild icons)." }, + { name: "className", type: "string", description: "CSS class for the icon container." }, + { name: "acronymClassName", type: "string", description: "CSS class for the acronym text fallback." }, +]; + +function SizesDemo() { const [animate, setAnimate] = useState(true); - const guilds = Object.values(GuildStore.getGuilds()).slice(0, 6); const guildWithIcon = guilds.find(g => g.icon); - const guildWithoutIcon = guilds.find(g => !g.icon) ?? guilds[0]; + + if (!guildWithIcon) { + return No guilds with icons found.; + } return ( -
- -
- setAnimate(a => !a)} - /> -
-
- - - - GuildIcon component with various sizes. Shows server icon or acronym fallback. - -
- {guildWithIcon && GuildIconSizes.map(size => ( -
- - - {size}px - -
- ))} -
-
- - - - When a guild has no icon, displays an acronym based on the guild name. - -
- {[32, 48, 80].map(size => ( -
- - - {size}px (no icon) - -
- ))} -
-
- - - - Showing icons from guilds you're in. - -
- {guilds.map(guild => ( -
- - - {guild.name.slice(0, 12)} - -
- ))} -
-
- - - • guildId - The guild's snowflake ID - • guildName - Guild name (used for acronym fallback) - • guildIcon - Icon hash or null - • iconSize - Size in pixels - • animate - Enable animated icons (GIF) - • className - Custom CSS class - • acronymClassName - Class for acronym text - - - - - {"import { GuildIcon } from \"@equicordplugins/components.dev/components\";"} - - +
+ setAnimate(a => !a)} + style={{ marginBottom: 12 }} + /> +
+ {GuildIconSizes.map(size => ( +
+ + + {size}px + +
+ ))} +
); } + +function AcronymDemo() { + const guilds = Object.values(GuildStore.getGuilds()).slice(0, 6); + const guildWithoutIcon = guilds.find(g => !g.icon); + + return ( +
+ {[32, 48, 80].map(size => ( +
+ + + {size}px (no icon) + +
+ ))} +
+ ); +} + +function YourGuildsDemo() { + const [animate, setAnimate] = useState(true); + const guilds = Object.values(GuildStore.getGuilds()).slice(0, 8); + + return ( +
+ setAnimate(a => !a)} + style={{ marginBottom: 12 }} + /> +
+ {guilds.map(guild => ( +
+ + + {guild.name.slice(0, 12)} + +
+ ))} +
+
+ ); +} + +export default function GuildIconTab() { + return ( + , + code: ``, + relevantProps: ["iconSize", "animate"], + }, + { + title: "Acronym Fallback", + description: "When guildIcon is null, the component renders an acronym based on the guild name.", + children: , + relevantProps: ["guildName", "guildIcon"], + }, + { + title: "Your Guilds", + description: "Live preview using guilds you're currently in.", + children: , + }, + ]} + props={GUILDICON_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/HeadingTab.tsx b/src/equicordplugins/components.dev/components/tabs/HeadingTab.tsx index 928b22a03..973ceaa3f 100644 --- a/src/equicordplugins/components.dev/components/tabs/HeadingTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/HeadingTab.tsx @@ -4,91 +4,115 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { DiscordHeading, DiscordText, HeadingVariants, Paragraph,TextColors, TextVariants } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DiscordHeading, DiscordText, HeadingVariants, TextColors, TextVariants } from ".."; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; + +const HEADING_PROPS: PropDef[] = [ + { name: "variant", type: "HeadingVariant", required: true, description: 'Heading size and weight, e.g. "heading-xl/bold", "heading-sm/medium".' }, + { name: "color", type: "TextColor", description: "Text color name from Discord's color system." }, + { name: "className", type: "string", description: "Custom CSS class." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, + { name: "children", type: "ReactNode", description: "Heading content." }, +]; + +const TEXT_PROPS: PropDef[] = [ + { name: "variant", type: "TextVariant", required: true, description: 'Text size and weight, e.g. "text-md/medium", "text-sm/normal".' }, + { name: "color", type: "TextColor", description: "Text color name from Discord's color system." }, + { name: "tag", type: "string", default: '"div"', description: "HTML tag to render (div, span, p, etc.)." }, + { name: "selectable", type: "boolean", description: "Allow text selection." }, + { name: "lineClamp", type: "number", description: "Number of lines before truncation with ellipsis." }, + { name: "tabularNumbers", type: "boolean", description: "Use tabular (monospace) number spacing for aligned columns." }, + { name: "scaleFontToUserSetting", type: "boolean", description: "Scale font size based on user's accessibility settings." }, + { name: "className", type: "string", description: "Custom CSS class." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, + { name: "children", type: "ReactNode", description: "Text content." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { title: "DiscordHeading", props: HEADING_PROPS }, + { title: "DiscordText", props: TEXT_PROPS }, +]; export default function HeadingTab() { return ( -
- - - Discord's Heading component with variant prop. Auto-sets h1-h6 tag based on nesting level. - -
- {HeadingVariants.slice(0, 10).map(variant => ( - - {variant} - - ))} -
-
- - -
- {HeadingVariants.slice(10).map(variant => ( - - {variant} - - ))} -
-
- - - - Discord's Text component for body text with various sizes and weights. - -
- {TextVariants.slice(0, 12).map(variant => ( - - {variant} - - ))} -
-
- - -
- {TextVariants.slice(12).map(variant => ( - - {variant} - - ))} -
-
- - - - Available color props for Text component. - -
- {TextColors.map(color => ( - - {color} - - ))} -
-
- - - • variant - "heading-SIZE/WEIGHT" (xxl, xl, lg, md, sm) - • className - Custom CSS class - • color - Text color (same as Text colors) - - - - • variant - "text-SIZE/WEIGHT" (xxl, xl, lg, md, sm, xs) - • color - Text color name - • tag - HTML tag to render (default: "div") - • selectable - Allow text selection - • lineClamp - Number of lines before truncation - • tabularNumbers - Use tabular number spacing - - - - - {"import { DiscordHeading, DiscordText, HeadingVariants, TextVariants, TextColors } from \"../components\";"} - - -
+ + {HeadingVariants.map(variant => ( + + {variant} + + ))} +
+ ), + code: 'Title', + relevantProps: ["variant"], + }, + { + title: "Text Variants", + description: "All text size/weight combinations from xxl to xs.", + children: ( +
+ {TextVariants.map(variant => ( + + {variant} + + ))} +
+ ), + code: 'Body text', + }, + { + title: "Text Colors", + description: "All available color values for the color prop.", + children: ( +
+ {TextColors.map(color => ( + + {color} + + ))} +
+ ), + code: 'Branded', + relevantProps: ["color"], + }, + { + title: "Line Clamp", + description: "Truncate text after a specified number of lines.", + children: ( +
+ + This is a long paragraph of text that will be clamped to two lines maximum. + Any content beyond the second line will be hidden and replaced with an ellipsis. + This demonstrates the lineClamp prop in action. + +
+ ), + code: '\n Long text here...\n', + relevantProps: ["lineClamp"], + }, + { + title: "Tabular Numbers", + description: "Monospaced number spacing for aligned numeric columns.", + children: ( +
+ 1,234,567 + 89,012 + 345 +
+ ), + relevantProps: ["tabularNumbers"], + }, + ]} + props={PROP_GROUPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx b/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx index 31a2a4c33..49455793e 100644 --- a/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/LayerModalTab.tsx @@ -8,7 +8,7 @@ import { openModal } from "@utils/modal"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Heading, ManaButton, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; const LayerModal = findComponentByCodeLazy('"data-mana-component":"layer-modal"') as React.ComponentType<{ transitionState: number; @@ -27,7 +27,16 @@ const TransitionState = findByPropsLazy("ENTERING", "ENTERED", "EXITING") as { HIDDEN: number; }; -function LayerModalDemo({ onClose }: { onClose: () => void; }) { +const LAYERMODAL_PROPS: PropDef[] = [ + { name: "transitionState", type: "number", required: true, description: "Animation state. Use TransitionState.ENTERED for visible." }, + { name: "onClose", type: "() => void", required: true, description: "Called when the modal should close." }, + { name: "children", type: "ReactNode", required: true, description: "Modal content." }, + { name: "animationVariant", type: '"default" | string', default: '"default"', description: "Animation style for enter/exit transitions." }, + { name: "returnRef", type: "Ref", internal: true, description: "Ref forwarded to the modal root element." }, + { name: "aria-label", type: "string", description: "Accessibility label for the modal." }, +]; + +function ModalContentDemo({ onClose }: { onClose: () => void; }) { return ( void; }) {
Layer Modal - This is a layer modal. It's used for layered/stacked modals - with different animation behavior than regular modals. + This is a layer modal opened via openModal. It supports stacked/layered + modal experiences with built-in animation handling.
@@ -46,92 +55,81 @@ function LayerModalDemo({ onClose }: { onClose: () => void; }) { ); } -export default function LayerModalTab() { - const [showInline, setShowInline] = useState(false); +function InlineDemo() { + const [visible, setVisible] = useState(false); return ( -
- - - Layer modals are used for stacked/layered modal experiences. - They have different animation variants and are typically used - when you need to show a modal on top of another modal. - -
- openModal(props => ( - - ))} - /> +
+ setVisible(!visible)} + /> + {visible && ( +
+ setVisible(false)} + aria-label="Inline Demo" + > +
+ Inline layer modal content + setVisible(false)} style={{ marginTop: 8 }} /> +
+
- - - - - Toggle to see the layer modal rendered inline (not recommended for production). - -
- setShowInline(!showInline)} - /> -
- {showInline && ( -
- setShowInline(false)} - aria-label="Inline Demo" - > -
- Inline layer modal content -
-
-
- )} -
- - - - LayerModal - - - • transitionState: number - Use TransitionState.ENTERED for visible - - - • onClose: () => void - Close handler - - - • children: ReactNode - Modal content - - - • animationVariant?: "default" | string - Animation style - - - • returnRef?: React.Ref - Ref for the modal element - - - • aria-label?: string - Accessibility label - - - TransitionState - findByPropsLazy("ENTERING", "ENTERED", "EXITING") - - - • ENTERING, ENTERED, EXITING, EXITED, HIDDEN - - - Usage Notes - - - • Use with openModal() for proper backdrop and positioning - - - • Has built-in animation handling based on transitionState - - + )}
); } + +export default function LayerModalTab() { + return ( + openModal(props => ( + + ))} + /> + ), + code: `openModal(props => ( + +
+

Modal content here

+
+
+))`, + relevantProps: ["transitionState", "onClose"], + }, + { + title: "Inline Preview", + description: "Rendered inline for preview purposes. Not recommended for production use.", + children: , + relevantProps: ["aria-label", "animationVariant"], + }, + ]} + props={LAYERMODAL_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx b/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx index dd19b4182..a76a5af19 100644 --- a/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ListboxTab.tsx @@ -4,8 +4,35 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ListboxItem, ManaListbox, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ListboxItem, ManaListbox, useState } from ".."; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; + +const LISTBOX_PROPS: PropDef[] = [ + { name: "items", type: "ListboxItem[]", required: true, description: "Array of items to display in the listbox." }, + { name: "selectedItems", type: "ListboxItem[]", description: "Currently selected items (controlled)." }, + { name: "defaultSelectedItems", type: "ListboxItem[]", description: "Initially selected items (uncontrolled)." }, + { name: "onSelectionChange", type: "(items: ListboxItem[]) => void", description: "Called when the selection changes." }, + { name: "selectionMode", type: '"single" | "multiple"', description: "Whether one or many items can be selected." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable the entire listbox." }, + { name: "required", type: "boolean", description: "Mark the listbox as required for form validation." }, + { name: "loading", type: "boolean", default: "false", description: "Show a loading state." }, + { name: "maxVisibleItems", type: "number", description: "Maximum items visible before scrolling." }, + { name: "shouldFocusWrap", type: "boolean", description: "Wrap keyboard focus when reaching the end." }, + { name: "typeahead", type: "boolean", description: "Enable keyboard search by typing item labels." }, + { name: "renderListItem", type: "(item: ListboxItem) => ReactNode", description: "Custom render function for list items." }, + { name: "renderEmptyState", type: "() => ReactNode", description: "Custom render for the empty state." }, +]; + +const ITEM_PROPS: PropDef[] = [ + { name: "id", type: "string", required: true, description: "Unique identifier for the item." }, + { name: "label", type: "string", required: true, description: "Display label for the item." }, + { name: "disabled", type: "boolean", description: "Disable this specific item." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { title: "ManaListbox", props: LISTBOX_PROPS }, + { title: "ListboxItem", props: ITEM_PROPS }, +]; const SAMPLE_ITEMS: ListboxItem[] = [ { id: "1", label: "Apple" }, @@ -28,116 +55,121 @@ const MANY_ITEMS: ListboxItem[] = Array.from({ length: 10 }, (_, i) => ({ label: `Item ${i + 1}`, })); -export default function ListboxTab() { - const [singleSelected, setSingleSelected] = useState([SAMPLE_ITEMS[0]]); - const [multiSelected, setMultiSelected] = useState([SAMPLE_ITEMS[0], SAMPLE_ITEMS[2]]); - const [disabledSelected, setDisabledSelected] = useState([]); - const [manySelected, setManySelected] = useState([]); - +function SingleSelectDemo() { + const [selected, setSelected] = useState([SAMPLE_ITEMS[0]]); return ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Listbox - - - • items: ListboxItem[] - Array of items - - - • selectedItems?: ListboxItem[] - Currently selected items - - - • onSelectionChange?: (items: ListboxItem[]) => void - Selection callback - - - • selectionMode?: "single" | "multiple" - Selection mode - - - • maxVisibleItems?: number - Max visible before scroll - - - • disabled?: boolean - Disable entire listbox - - - • loading?: boolean - Show loading state - - - • typeahead?: boolean - Enable keyboard search - - - • shouldFocusWrap?: boolean - Wrap focus at ends - - - ListboxItem - - - • id: string - Unique item ID - - - • label: string - Display label - - - • disabled?: boolean - Disable individual item - - -
+ + ); +} + +function MultiSelectDemo() { + const [selected, setSelected] = useState([SAMPLE_ITEMS[0], SAMPLE_ITEMS[2]]); + return ( + + ); +} + +function DisabledItemsDemo() { + const [selected, setSelected] = useState([]); + return ( + + ); +} + +function MaxVisibleDemo() { + const [selected, setSelected] = useState([]); + return ( + + ); +} + +export default function ListboxTab() { + return ( + , + code: ``, + relevantProps: ["selectionMode", "selectedItems", "onSelectionChange"], + }, + { + title: "Multiple Selection", + description: "Multiple items can be selected simultaneously.", + children: , + code: ``, + }, + { + title: "Disabled Items", + description: "Individual items can be disabled while the rest remain interactive.", + children: , + }, + { + title: "Max Visible Items", + description: "Limits visible items to 5 with scrolling for the rest.", + children: , + relevantProps: ["maxVisibleItems"], + }, + { + title: "Disabled State", + description: "The entire listbox is non-interactive when disabled.", + children: ( + + ), + relevantProps: ["disabled"], + }, + { + title: "Loading State", + description: "Shows a loading indicator while data is being fetched.", + children: , + relevantProps: ["loading"], + }, + { + title: "Empty State", + description: "Default empty appearance when no items are provided.", + children: , + }, + ]} + props={PROP_GROUPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/MenuTab.tsx b/src/equicordplugins/components.dev/components/tabs/MenuTab.tsx index 57ced22be..1e2ec6774 100644 --- a/src/equicordplugins/components.dev/components/tabs/MenuTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/MenuTab.tsx @@ -4,8 +4,98 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Button, ContextMenuApi, Menu, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { Button, ContextMenuApi, Menu, useState } from ".."; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; + +const MENU_PROPS: PropDef[] = [ + { name: "navId", type: "string", required: true, description: "Unique navigation ID for the menu." }, + { name: "onClose", type: "() => void", required: true, description: "Called when the menu should close." }, + { name: "children", type: "ReactNode", required: true, description: "Menu items, groups, and separators." }, +]; + +const MENUITEM_PROPS: PropDef[] = [ + { name: "id", type: "string", required: true, description: "Unique item identifier." }, + { name: "label", type: "ReactNode | ((props) => ReactNode)", required: true, description: "Display text for the item." }, + { name: "action", type: "(e: MouseEvent) => void", description: "Called when the item is clicked." }, + { name: "icon", type: "ComponentType", description: "Icon component displayed after the label." }, + { name: "iconLeft", type: "ComponentType", description: "Icon component displayed before the label." }, + { name: "iconProps", type: "MenuItemIconProps", description: "Props passed to the icon component." }, + { name: "leadingAccessory", type: "ReactNode", description: "Custom leading accessory element." }, + { name: "trailingIndicator", type: "ReactNode", description: "Trailing indicator element." }, + { name: "hint", type: "ReactNode | ((props) => ReactNode)", description: "Keyboard shortcut hint displayed on the right." }, + { name: "shortcut", type: "ReactNode", description: "Keyboard shortcut display." }, + { name: "subtext", type: "ReactNode", description: "Secondary text below the label." }, + { name: "subtextLineClamp", type: "number", description: "Max lines for subtext before truncation." }, + { name: "color", type: '"default" | "brand" | "danger" | "success" | "premium" | "premium-gradient"', description: "Item color variant." }, + { name: "loading", type: "boolean", description: "Show loading state." }, + { name: "badge", type: "ReactNode", description: "Badge element displayed alongside the label." }, + { name: "disabled", type: "boolean", description: "Disable interaction." }, + { name: "onClose", type: "() => void", description: "Called when the menu should close after this item's action." }, + { name: "onFocus", type: "() => void", description: "Called when the item receives focus." }, + { name: "dontCloseOnAction", type: "boolean", description: "Prevent menu from closing when this item is clicked." }, + { name: "dontCloseOnActionIfHoldingShiftKey", type: "boolean", description: "Prevent close on click if shift key is held." }, + { name: "className", type: "string", description: "Custom CSS class name." }, + { name: "focusedClassName", type: "string", description: "CSS class applied when focused." }, + { name: "children", type: "ReactNode", description: "Nested MenuItem elements create a submenu." }, +]; + +const CHECKBOX_PROPS: PropDef[] = [ + { name: "id", type: "string", required: true, description: "Unique item identifier." }, + { name: "label", type: "ReactNode | ((props) => ReactNode)", required: true, description: "Display text." }, + { name: "checked", type: "boolean", required: true, description: "Current checked state." }, + { name: "color", type: "MenuItemColor", default: '"default"', description: "Item color variant." }, + { name: "action", type: "(e: MouseEvent) => void", description: "Called when toggled." }, + { name: "disabled", type: "boolean", description: "Disable interaction." }, + { name: "subtext", type: "ReactNode", description: "Secondary text below the label." }, + { name: "subtextLineClamp", type: "number", description: "Max lines for subtext before truncation." }, + { name: "leftIcon", type: "ComponentType", description: "Icon component displayed on the left." }, + { name: "leadingAccessory", type: "ReactNode", description: "Custom leading accessory element." }, + { name: "className", type: "string", description: "Custom CSS class name." }, + { name: "focusedClassName", type: "string", description: "CSS class applied when focused." }, +]; + +const RADIO_PROPS: PropDef[] = [ + { name: "id", type: "string", required: true, description: "Unique item identifier." }, + { name: "label", type: "ReactNode | ((props) => ReactNode)", required: true, description: "Display text." }, + { name: "group", type: "string", required: true, description: "Radio group name for mutual exclusion." }, + { name: "checked", type: "boolean", required: true, description: "Whether this option is selected." }, + { name: "color", type: "MenuItemColor", default: '"default"', description: "Item color variant." }, + { name: "action", type: "(e: MouseEvent) => void", description: "Called when selected." }, + { name: "disabled", type: "boolean", description: "Disable interaction." }, + { name: "subtext", type: "ReactNode", description: "Secondary text below the label." }, + { name: "subtextLineClamp", type: "number", description: "Max lines for subtext before truncation." }, + { name: "leftIcon", type: "ComponentType", description: "Icon component displayed on the left." }, + { name: "leadingAccessory", type: "ReactNode", description: "Custom leading accessory element." }, +]; + +const SLIDER_PROPS: PropDef[] = [ + { name: "minValue", type: "number", required: true, description: "Minimum slider value." }, + { name: "maxValue", type: "number", required: true, description: "Maximum slider value." }, + { name: "value", type: "number", required: true, description: "Current slider value." }, + { name: "onChange", type: "(value: number) => void", required: true, description: "Called when the value changes." }, + { name: "renderValue", type: "(value: number) => string", description: "Format the value label display." }, +]; + +const SWITCH_PROPS: PropDef[] = [ + { name: "id", type: "string", required: true, description: "Unique item identifier." }, + { name: "label", type: "ReactNode", description: "Display text." }, + { name: "checked", type: "boolean", required: true, description: "Current switch state." }, + { name: "action", type: "(e: MouseEvent) => void", required: true, description: "Called when toggled." }, + { name: "color", type: "MenuItemColor", default: '"default"', description: "Item color variant." }, + { name: "disabled", type: "boolean", description: "Disable interaction." }, + { name: "subtext", type: "ReactNode", description: "Secondary text below the label." }, + { name: "subtextLineClamp", type: "number", description: "Max lines for subtext before truncation." }, + { name: "className", type: "string", description: "Custom CSS class name." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { title: "Menu.Menu", props: MENU_PROPS }, + { title: "Menu.MenuItem", props: MENUITEM_PROPS }, + { title: "Menu.MenuCheckboxItem", props: CHECKBOX_PROPS }, + { title: "Menu.MenuRadioItem", props: RADIO_PROPS }, + { title: "Menu.MenuSwitchItem", props: SWITCH_PROPS }, + { title: "Menu.MenuSliderControl", props: SLIDER_PROPS }, +]; function DemoMenu({ onClose }: { onClose: () => void; }) { const [checkboxValue, setCheckboxValue] = useState(false); @@ -15,11 +105,7 @@ function DemoMenu({ onClose }: { onClose: () => void; }) { return ( - console.log("Basic item clicked")} - /> + { }} /> void; }) { )} - action={() => console.log("Icon item clicked")} - /> - console.log("Hint item clicked")} - /> - console.log("Subtext item clicked")} - /> - { }} /> + { }} /> + { }} /> + - { }} /> - { }} /> - { }} /> - { }} /> - { }} /> - { }} /> + { }} /> + { }} /> + { }} /> + { }} /> + { }} /> + { }} /> setCheckboxValue(v => !v)} /> - setCheckboxValue(v => !v)} - /> - - - setSwitchValue(v => !v)} @@ -87,55 +149,25 @@ function DemoMenu({ onClose }: { onClose: () => void; }) { - setRadioValue("option1")} - /> - setRadioValue("option2")} - /> - setRadioValue("option3")} - /> + setRadioValue("option1")} /> + setRadioValue("option2")} /> + setRadioValue("option3")} /> - console.log("Sub item 1")} - /> - console.log("Sub item 2")} - /> - - console.log("Nested")} - /> + { }} /> + { }} /> + + { }} /> ( - - - Click or right-click the button to open a demo context menu. - -
- - -
-
- - - - Menu.Menu - Root container with navId and onClose. - - - Menu.MenuItem - Basic item with id, label, action, icon, hint, subtext, disabled, color. - - - Menu.MenuCheckboxItem - Toggleable checkbox item with checked state. - - - Menu.MenuRadioItem - Radio selection with group prop for mutual exclusion. - - - Menu.MenuSwitchItem - Toggle switch item with checked state. - - - Menu.MenuGroup - Groups items with optional label header. - - - Menu.MenuSeparator - Visual divider between items. - - - Menu.MenuControlItem - Wrapper for custom controls like sliders. - - - Menu.MenuSliderControl - Slider control inside MenuControlItem. - - - Menu.MenuSearchControl - Search input for filtering menu items. - - - - - - Available colors: default, brand, danger, success, premium, premium-gradient. - - - - - - Use ContextMenuApi.openContextMenu(event, () => <Menu />) to open menus. - For patches, use NavContextMenuPatchCallback from @api/ContextMenu. - - -
+ + + +
+ ), + code: `const openMenu = (e: React.MouseEvent) => { + ContextMenuApi.openContextMenu(e, () => ( + + { }} /> + + setOn(!on)} /> + + )); +};`, + }, + { + title: "MenuItem Colors", + description: "Items support color variants: default, brand, danger, success, premium, and premium-gradient. These are visible in the full demo above.", + children: ( + + ), + relevantProps: ["color"], + }, + ]} + props={PROP_GROUPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx b/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx index da80d56b6..e37884d9c 100644 --- a/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ModalTab.tsx @@ -17,7 +17,73 @@ import { ToastType, useState, } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; + +const MODAL_PROPS: PropDef[] = [ + { name: "title", type: "string", required: true, description: "Modal title displayed in the header." }, + { name: "onClose", type: "() => void", required: true, description: "Called when the modal should close." }, + { name: "subtitle", type: "string", description: "Subtitle text below the title." }, + { name: "size", type: '"sm" | "md"', description: "Modal width size." }, + { name: "transitionState", type: "number", description: "Animation state from openModal render props." }, + { name: "animationVariant", type: '"default" | "fade" | "slide-up"', default: '"default"', description: "Transition animation style." }, + { name: "paddingSize", type: '"sm" | "lg"', default: '"sm"', description: "Internal padding size." }, + { name: "fullScreenOnMobile", type: "boolean", default: "true", description: "Whether to go full screen on mobile." }, + { name: "maxHeight", type: '"default" | "full"', default: '"default"', description: "Maximum height constraint." }, + { name: "dismissable", type: "boolean", default: "true", description: "Whether the modal can be dismissed by clicking outside." }, + { name: "children", type: "ReactNode", description: "Modal body content." }, + { name: "actions", type: "ModalAction[]", description: "Array of action buttons in the footer." }, + { name: "actionBarInput", type: "ReactNode", description: "Input element in the action bar area." }, + { name: "actionBarInputLayout", type: '"default" | "full-width"', default: '"default"', description: "Layout of the action bar input." }, + { name: "notice", type: '{ message: string; type: "info" | "warn" | "critical" | "positive" }', description: "Notice banner shown at the top of the modal." }, + { name: "input", type: "ReactNode", description: "Input controls rendered in the header area." }, + { name: "preview", type: "ReactNode", description: "Preview content above the modal body." }, + { name: "onScroll", type: "(e: UIEvent) => void", description: "Scroll event handler for the modal body." }, + { name: "scrollerRef", type: "Ref", description: "Ref to the scroller element." }, + { name: "role", type: "string", default: '"dialog"', description: "ARIA role for the modal." }, + { name: "aria-label", type: "string", description: "ARIA label for the modal." }, + { name: "className", type: "string", description: "Custom CSS class." }, +]; + +const CONFIRM_PROPS: PropDef[] = [ + { name: "title", type: "string", required: true, description: "Modal title." }, + { name: "confirmText", type: "string", required: true, description: "Text for the confirm button." }, + { name: "onClose", type: "() => void", required: true, description: "Called when the modal should close." }, + { name: "cancelText", type: "string", description: "Text for the cancel button." }, + { name: "variant", type: '"critical" | "primary" | "secondary"', description: "Visual style of the confirm button." }, + { name: "onConfirm", type: "() => void | Promise", description: "Called when the confirm button is clicked. Can be async." }, + { name: "onCancel", type: "() => void", description: "Called when the cancel button is clicked." }, + { name: "checkboxProps", type: "{ label, checked, onChange }", description: "Optional checkbox in the modal body." }, + { name: "children", type: "ReactNode", description: "Body content." }, +]; + +const EXPRESSIVE_PROPS: PropDef[] = [ + { name: "title", type: "string", required: true, description: "Modal title." }, + { name: "onClose", type: "() => void", required: true, description: "Called when the modal should close." }, + { name: "subtitle", type: "string", description: "Subtitle text below the title." }, + { name: "size", type: '"sm" | "md"', default: '"md"', description: "Modal width size." }, + { name: "transitionState", type: "number", description: "Animation state." }, + { name: "gradientColor", type: '"purple" | "blue" | "pink" | "green"', default: '"purple"', description: "Background gradient color." }, + { name: "graphic", type: '{ type: "image"; src: string; aspectRatio?: string }', description: "Image graphic shown above the content." }, + { name: "badge", type: "ReactNode", description: "Badge element shown near the title." }, + { name: "actionBarInput", type: "ReactNode", description: "Input element in the action bar area." }, + { name: "actions", type: "ModalAction[]", description: "Action buttons." }, + { name: "children", type: "ReactNode", description: "Modal body content." }, +]; + +const ACTION_PROPS: PropDef[] = [ + { name: "text", type: "string", required: true, description: "Button text." }, + { name: "variant", type: '"primary" | "secondary" | "critical-primary" | "critical-secondary"', required: true, description: "Button style variant." }, + { name: "onClick", type: "() => void", required: true, description: "Click handler." }, + { name: "disabled", type: "boolean", description: "Disable the button." }, + { name: "loading", type: "boolean", description: "Show loading spinner." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { title: "Modal", props: MODAL_PROPS }, + { title: "ConfirmModal", props: CONFIRM_PROPS }, + { title: "ExpressiveModal", props: EXPRESSIVE_PROPS }, + { title: "ModalAction", props: ACTION_PROPS }, +]; function openBasicModal() { openModal(props => ( @@ -31,21 +97,19 @@ function openBasicModal() { { text: "Confirm", variant: "primary", onClick: () => { showToast("Confirmed!", ToastType.SUCCESS); props.onClose(); } }, ]} > - This is the modal content. You can put any React components here. + This is the modal body content. Any React components can go here. )); } -function openModalWithNotice() { +function openNoticeModal() { openModal(props => ( Modals can display notices at the top for important information. @@ -54,7 +118,6 @@ function openModalWithNotice() { function ModalWithInputContent({ onClose, transitionState }: { onClose: () => void; transitionState: number; }) { const [inputValue, setInputValue] = useState(""); - return ( vo { text: "Submit", variant: "primary", onClick: () => { showToast(`Submitted: ${inputValue}`, ToastType.SUCCESS); onClose(); } }, ]} > - - - Using ManaTextInput inside the modal content. - + ); } -function openModalWithInput() { - openModal(props => ( - - )); -} - -function openConfirmModal() { +function openConfirmCritical() { openModal(props => ( { - showToast("Item deleted!", ToastType.SUCCESS); - }} - onCancel={() => { - showToast("Cancelled", ToastType.MESSAGE); - }} + onConfirm={() => showToast("Item deleted!", ToastType.SUCCESS)} + onCancel={() => showToast("Cancelled", ToastType.MESSAGE)} onClose={props.onClose} > This action cannot be undone. The item will be permanently deleted. @@ -106,16 +152,14 @@ function openConfirmModal() { )); } -function openConfirmModalPrimary() { +function openConfirmPrimary() { openModal(props => ( { - showToast("Changes saved!", ToastType.SUCCESS); - }} + onConfirm={() => showToast("Changes saved!", ToastType.SUCCESS)} onClose={props.onClose} > Do you want to save your changes? @@ -123,15 +167,14 @@ function openConfirmModalPrimary() { )); } -function openExpressiveModal() { +function openExpressive() { openModal(props => ( )); @@ -139,96 +182,92 @@ function openExpressiveModal() { export default function ModalTab() { return ( -
- - - Simple modal with title, subtitle, content, and action buttons. - - - - - - - Modal with a notice banner (info, warn, critical, positive). - - - - - - - Modal with input controls in the header area. - - - - - - - Confirmation dialog for destructive actions. - - - - - - - Confirmation dialog for non-destructive actions. - - - - - - - Visual modal with graphic/badge support. - - - - - - - Utility to close all open modals at once. - - - - - - • title - Modal title (required) - • subtitle - Optional subtitle - • size - "sm" or "md" - • children - Modal content - • actions - Array of action buttons - • notice - {"{ message, type }"} for notices - • input - Controls in header area - • onClose - Close handler (required) - - - - • confirmText - Confirm button text - • cancelText - Cancel button text - • variant - "critical" | "primary" | "secondary" - • onConfirm - Confirm callback (can be async) - • onCancel - Cancel callback - • checkboxProps - Optional checkbox - - - - • text - Button text - • variant - "primary" | "secondary" | "critical-primary" - • onClick - Click handler - • disabled - Disable button - • loading - Show loading state - - - - • modalKey - Unique key for the modal - • dismissable - Allow clicking backdrop to close - • instant - Skip animation - • onCloseCallback - Called when modal closes - - - - - {"import { Modal, ConfirmModal, openModal, closeModal } from \"../components\";"} - - -
+ , + code: `openModal(props => ( + +

Modal content here

+
+))`, + relevantProps: ["title", "subtitle", "actions"], + }, + { + title: "Modal with Notice", + description: "A notice banner appears at the top of the modal for important info.", + children: , + relevantProps: ["notice"], + }, + { + title: "Modal with Input", + description: "Modal with form controls in the body area.", + children: ( + openModal(props => ( + + ))} + /> + ), + }, + { + title: "ConfirmModal (Critical)", + description: "Confirmation dialog for destructive actions with danger styling.", + children: , + code: `openModal(props => ( + deleteItem()} + onClose={props.onClose} + transitionState={props.transitionState} + > +

This cannot be undone.

+
+))`, + relevantProps: ["confirmText", "variant", "onConfirm"], + }, + { + title: "ConfirmModal (Primary)", + description: "Confirmation dialog for non-destructive actions.", + children: , + }, + { + title: "ExpressiveModal", + description: "Visual modal for onboarding or promotional experiences.", + children: , + relevantProps: ["subtitle", "gradientColor", "graphic", "badge"], + }, + { + title: "Close All Modals", + description: "Utility to close all open modals at once.", + children: , + code: "closeAllModals();", + }, + ]} + props={PROP_GROUPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx b/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx index cea1d5590..dacc6e57e 100644 --- a/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/NoticeTab.tsx @@ -5,123 +5,108 @@ */ import { Button, Notice, NoticeTypes, Paragraph } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; + +const NOTICE_PROPS: PropDef[] = [ + { name: "children", type: "ReactNode", required: true, description: "Notice content text." }, + { name: "messageType", type: '"info" | "warn" | "danger" | "positive" | "preview"', required: true, description: "Visual severity level of the notice." }, + { name: "action", type: "ReactNode", description: "Action element (typically a button) displayed on the right side." }, + { name: "hidden", type: "boolean", default: "false", description: "Hides the notice without removing it from the DOM." }, + { name: "textColor", type: "string", description: "Custom text color override." }, + { name: "textVariant", type: "string", default: '"text-sm/medium"', description: 'Text size/weight variant, e.g. "text-md/normal".' }, + { name: "icon", type: "ComponentType<{ className?, color? }>", description: "Custom icon component to replace the default." }, + { name: "className", type: "string", description: "Additional CSS class." }, +]; export default function NoticeTab() { return ( -
- - - Notice component for displaying informational messages with different severity levels. - -
- {NoticeTypes.map(type => ( -
- - This is a {type} notice message. - - - {type} - + + {NoticeTypes.map(type => ( + + This is a {type} notice message. + + ))}
- ))} -
-
- - - - Notices can include action buttons for user interaction. - -
- Action} - > - Notice with an action button. - - Fix Now} - > - Warning that requires attention. - -
-
- - - - The hidden prop controls visibility without removing from DOM. - -
-
- - - hidden=false - -
-
- - - hidden=true - -
-
-
- - - - Custom text styling via the textVariant prop. - -
-
- - Default text variant. - - - text-sm/medium (default) - -
-
- - Larger text variant. - - - text-md/normal - -
-
-
- - - - • children: ReactNode - Notice content - - - • messageType: "info" | "warn" | "danger" | "positive" | "preview" - - - • action?: ReactNode - Action button or element - - - • hidden?: boolean - Hide the notice - - - • textColor?: string - Custom text color - - - • textVariant?: string - Text size/weight variant - - - • icon?: ComponentType - Custom icon component - - - • className?: string - Additional CSS class - - -
+ ), + code: 'Informational message.', + relevantProps: ["messageType"], + }, + { + title: "With Action Button", + description: "Notices can include action buttons for user interaction.", + children: ( +
+ Action} + > + Notice with an action button. + + Fix Now} + > + Warning that requires attention. + +
+ ), + code: `Fix Now} +> + Warning that requires attention. +`, + relevantProps: ["action"], + }, + { + title: "Hidden State", + description: "The hidden prop controls visibility without removing from the DOM.", + children: ( +
+
+ hidden=false: + +
+
+ hidden=true (invisible below): + +
+
+ ), + relevantProps: ["hidden"], + }, + { + title: "Text Variants", + description: "Customize text sizing with the textVariant prop.", + children: ( +
+
+ Default (text-sm/medium): + Default text variant. +
+
+ text-md/normal: + Larger text variant. +
+
+ ), + relevantProps: ["textVariant"], + }, + ]} + props={NOTICE_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx b/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx index c3b2b3490..2b994f6d2 100644 --- a/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/PaginatorTab.tsx @@ -5,118 +5,101 @@ */ import { Paginator, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function PaginatorTab() { - const [page1, setPage1] = useState(1); - const [page2, setPage2] = useState(1); - const [page3, setPage3] = useState(1); - const [page4, setPage4] = useState(1); +const PAGINATOR_PROPS: PropDef[] = [ + { name: "currentPage", type: "number", required: true, description: "Currently active page (1-based)." }, + { name: "totalCount", type: "number", required: true, description: "Total number of items across all pages." }, + { name: "pageSize", type: "number", required: true, description: "Number of items per page." }, + { name: "onPageChange", type: "(page: number) => void", description: "Called when the user navigates to a different page." }, + { name: "maxVisiblePages", type: "number", description: "Maximum number of page buttons visible at once." }, + { name: "hideMaxPage", type: "boolean", default: "false", description: "Hide the last page number." }, + { name: "disablePaginationGap", type: "boolean", default: "false", description: "Remove the ellipsis gaps between page numbers." }, + { name: "className", type: "string", description: "Custom CSS class." }, + { name: "renderPageWrapper", type: "(props, defaultRender) => ReactNode", description: "Custom render wrapper for each page button." }, +]; +function BasicDemo() { + const [page, setPage] = useState(1); return ( -
- - - Pagination component for navigating through pages of content. - - - - - - - Control how many page numbers are visible at once. - -
-
- - - 3 visible - -
-
- - - 7 visible - -
-
-
- - - - Additional configuration options for the paginator. - -
-
- - - hideMaxPage - -
-
- - - disablePaginationGap - -
-
-
- - - - • currentPage: number - Current active page - - - • totalCount: number - Total number of items - - - • pageSize: number - Items per page - - - • onPageChange?: (page: number) => void - Page change callback - - - • maxVisiblePages?: number - Max page numbers shown - - - • hideMaxPage?: boolean - Hide last page number - - - • disablePaginationGap?: boolean - Remove ellipsis gaps - - - • className?: string - Additional CSS class - - +
+ Page {page} of 10 +
); } + +function MaxVisibleDemo() { + const [page3, setPage3] = useState(1); + const [page7, setPage7] = useState(1); + + return ( +
+
+ maxVisiblePages=3: + +
+
+ maxVisiblePages=7: + +
+
+ ); +} + +function OptionsDemo() { + const [page, setPage] = useState(1); + + return ( +
+
+ hideMaxPage: + +
+
+ disablePaginationGap: + +
+
+ ); +} + +export default function PaginatorTab() { + return ( + , + code: ``, + relevantProps: ["currentPage", "totalCount", "pageSize", "onPageChange"], + }, + { + title: "Max Visible Pages", + description: "Control how many page numbers are visible before ellipsis appears.", + children: , + relevantProps: ["maxVisiblePages"], + }, + { + title: "Options", + description: "Hide the last page number or remove ellipsis gaps entirely.", + children: , + relevantProps: ["hideMaxPage", "disablePaginationGap"], + }, + ]} + props={PAGINATOR_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx b/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx index cd7bc6c19..a27d78ec1 100644 --- a/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/PopoverTab.tsx @@ -4,21 +4,45 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaButton, ManaPopover, Paragraph, PopoverAction, useRef, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaButton, ManaPopover, PopoverAction, useRef, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; -function PopoverDemo({ title, body, size, actions, position, label }: { title: string; body: string; size?: "sm" | "md" | "lg"; actions?: PopoverAction[]; position?: "top" | "bottom" | "left" | "right"; label: string; }) { +const POPOVER_PROPS: PropDef[] = [ + { name: "targetElementRef", type: "RefObject", required: true, description: "Ref to the element the popover is anchored to." }, + { name: "shouldShow", type: "boolean", required: true, description: "Controls popover visibility." }, + { name: "onRequestClose", type: "(reason: string) => void", description: "Called when the popover wants to close (click outside, escape, etc.)." }, + { name: "title", type: "string", description: "Popover title text." }, + { name: "body", type: "string", description: "Popover body text." }, + { name: "size", type: '"sm" | "md" | "lg"', description: "Popover width size." }, + { name: "position", type: '"top" | "bottom" | "left" | "right"', description: "Position relative to the target element." }, + { name: "actions", type: "PopoverAction[]", description: "Action buttons rendered in the popover footer." }, + { name: "textLink", type: "{ text: string; onClick?: () => void }", description: "Text link displayed below the body." }, + { name: "graphic", type: "{ src: string; aspectRatio?: string }", description: "Image graphic shown in the popover." }, + { name: "badge", type: "ReactNode", description: "Badge element displayed near the title." }, + { name: "gradientColor", type: "string", description: "Background gradient color." }, + { name: "scrollBehavior", type: "string", description: "Scroll behavior for popover content." }, + { name: "hasVideo", type: "boolean", description: "Whether the popover contains video content." }, + { name: "popoverRef", type: "Ref", description: "Ref forwarded to the popover container." }, + { name: "caretConfig", type: '{ align?: "start" | "center" | "end" }', description: "Configuration for the caret arrow." }, + { name: "alignmentStrategy", type: '"edge" | "trigger-center"', description: "How the popover aligns to the target element." }, + { name: "align", type: '"left" | "center" | "right"', description: 'Alignment when alignmentStrategy is "edge".' }, +]; + +function PopoverDemo({ label, title, body, size, actions, position }: { + label: string; + title: string; + body: string; + size?: "sm" | "md" | "lg"; + actions?: PopoverAction[]; + position?: "top" | "bottom" | "left" | "right"; +}) { const wrapperRef = useRef(null); const [show, setShow] = useState(false); return ( <>
- setShow(!show)} - /> + setShow(!show)} />
- -
- -
-
+ + ), + code: `const ref = useRef(null); +const [show, setShow] = useState(false); - -
- - - -
-
- - -
- - - - -
-
- - -
- -
-
- - - - • targetElementRef: React.RefObject - Ref to trigger element - - - • shouldShow: boolean - Controls visibility - - - • onRequestClose?: (reason: string) => void - Close callback - - - • title?: string - Popover title - - - • body?: string - Popover body text - - - • size?: "sm" | "md" | "lg" - Popover size - - - • position?: "top" | "bottom" | "left" | "right" - Position - - - • actions?: PopoverAction[] - Action buttons - - - • textLink?: {"{"} text: string, onClick?: () => void {"}"} - Link - - - • graphic?: {"{"} src: string, aspectRatio?: string {"}"} - Image - - -
+
+ +
+ setShow(false)} + title="Popover Title" + body="Body text here." +/>`, + relevantProps: ["targetElementRef", "shouldShow", "onRequestClose", "title", "body"], + }, + { + title: "Sizes", + description: "Three size variants: sm, md, and lg.", + children: ( +
+ + + +
+ ), + relevantProps: ["size"], + }, + { + title: "Positions", + description: "Popover can be positioned on any side of the target element.", + children: ( +
+ + + + +
+ ), + relevantProps: ["position"], + }, + { + title: "With Actions", + description: "Popover with action buttons in the footer.", + children: ( + + ), + relevantProps: ["actions"], + }, + ]} + props={POPOVER_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx index 605d08eb1..bc288f701 100644 --- a/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ProgressBarTab.tsx @@ -5,112 +5,155 @@ */ import { Paragraph, ProgressBar, useEffect, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function ProgressBarTab() { - const [animatedProgress, setAnimatedProgress] = useState(0); +const PROGRESSBAR_PROPS: PropDef[] = [ + { name: "progress", type: "number", required: true, description: "Current progress value." }, + { name: "minimum", type: "number", default: "0", description: "Minimum range value." }, + { name: "maximum", type: "number", default: "100", description: "Maximum range value." }, + { name: "variant", type: '"blue" | "orange" | "unset"', default: '"blue"', description: 'Color variant. Use "unset" with override for custom colors.' }, + { name: "override", type: "{ default?: { background, gradientStart, gradientEnd } }", description: 'Custom colors when variant is "unset".' }, + { name: "weight", type: '"light" | "medium"', default: '"light"', description: "Visual weight of the progress bar." }, + { name: "glowing", type: "boolean", default: "true", description: "Whether the progress bar has a glow effect." }, + { name: "labelledBy", type: "string", internal: true, description: "ID of the labelling element for accessibility." }, +]; + +function AnimatedDemo() { + const [progress, setProgress] = useState(0); useEffect(() => { const interval = setInterval(() => { - setAnimatedProgress(prev => (prev >= 100 ? 0 : prev + 5)); + setProgress(prev => (prev >= 100 ? 0 : prev + 5)); }, 200); return () => clearInterval(interval); }, []); return ( -
- - - Blue (default) and Orange variants. - -
-
- Blue: - -
-
- Orange: - -
-
-
- - -
-
- 0%: - -
-
- 25%: - -
-
- 50%: - -
-
- 75%: - -
-
- 100%: - -
-
-
- - - - Progress: {animatedProgress}% - - - - - - - Using minimum=0 and maximum=200, progress=150 (75% filled). - - - - - - - Using variant="unset" with custom gradient colors. - - - - - - - • progress: number - Current progress value - - - • minimum?: number - Minimum value (default: 0) - - - • maximum?: number - Maximum value (default: 100) - - - • variant?: "blue" | "orange" | "unset" - Color variant - - - • override?: object - Custom colors when variant="unset" - - - • labelledBy?: string - ID of labelling element for a11y - - +
+ Progress: {progress}% +
); } + +export default function ProgressBarTab() { + return ( + +
+ Blue: + +
+
+ Orange: + +
+
+ ), + code: '', + relevantProps: ["variant"], + }, + { + title: "Progress Values", + description: "Various progress levels from 0% to 100%.", + children: ( +
+ {[0, 25, 50, 75, 100].map(v => ( +
+ {v}%: + +
+ ))} +
+ ), + relevantProps: ["progress"], + }, + { + title: "Animated", + description: "Progress bar that increments automatically.", + children: , + }, + { + title: "Weight", + description: "Light (default) and medium weight variants.", + children: ( +
+
+ Light (default): + +
+
+ Medium: + +
+
+ ), + code: '', + relevantProps: ["weight"], + }, + { + title: "Glowing", + description: "Glow effect is enabled by default. Set glowing={false} to disable.", + children: ( +
+
+ Glowing (default): + +
+
+ No glow: + +
+
+ ), + code: "", + relevantProps: ["glowing"], + }, + { + title: "Custom Range", + description: "Using minimum=0 and maximum=200 with progress=150 (75% filled).", + children: , + code: '', + relevantProps: ["minimum", "maximum"], + }, + { + title: "Custom Override", + description: 'Using variant="unset" with custom gradient colors.', + children: ( + + ), + code: ``, + relevantProps: ["override"], + }, + ]} + props={PROGRESSBAR_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx b/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx index c4385ce6a..fc8852e52 100644 --- a/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/RadioGroupTab.tsx @@ -4,9 +4,45 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaBaseRadioGroup, Paragraph, RadioOption, StandaloneRadioIndicator, useState } from ".."; +import { ManaBaseRadioGroup, RadioOption, StandaloneRadioIndicator, useState } from ".."; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; import { EquicordIcon } from "../icons/EquicordIcon"; -import { SectionWrapper } from "../SectionWrapper"; + +const RADIOGROUP_PROPS: PropDef[] = [ + { name: "options", type: "RadioOption[]", required: true, description: "Array of radio options to display." }, + { name: "value", type: "string | number | null", description: "Currently selected value." }, + { name: "onChange", type: "(value: string | number) => void", description: "Called when the selection changes." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable the entire radio group." }, + { name: "orientation", type: '"vertical" | "horizontal"', description: "Layout orientation of the radio items." }, + { name: "radioPosition", type: '"left" | "right"', default: '"left"', description: "Position of the radio indicator relative to the label." }, + { name: "withTransparentBackground", type: "boolean", description: "Use transparent background for radio items." }, + { name: "label", type: "string", description: "Field label text for the group." }, + { name: "description", type: "string", description: "Field description text." }, + { name: "required", type: "boolean", description: "Mark the field as required." }, + { name: "errorMessage", type: "string", description: "Error message displayed below the group." }, + { name: "aria-labelledby", type: "string", description: "ID of an external labelling element for accessibility." }, + { name: "className", type: "string", description: "Custom CSS class name." }, +]; + +const OPTION_PROPS: PropDef[] = [ + { name: "value", type: "string | number", required: true, description: "The value for this option." }, + { name: "name", type: "string", required: true, description: "Display label text." }, + { name: "description", type: "string", description: "Description text shown below the label." }, + { name: "disabled", type: "boolean", description: "Disable this individual option." }, + { name: "icon", type: "ComponentType", description: "Icon component displayed alongside the radio option." }, + { name: "color", type: "string", description: "Custom color for the option highlight." }, +]; + +const STANDALONE_PROPS: PropDef[] = [ + { name: "isSelected", type: "boolean", required: true, description: "Whether the indicator shows as selected." }, + { name: "disabled", type: "boolean", description: "Disable the indicator." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { title: "ManaBaseRadioGroup", props: RADIOGROUP_PROPS }, + { title: "RadioOption", props: OPTION_PROPS }, + { title: "StandaloneRadioIndicator", props: STANDALONE_PROPS }, +]; const BASIC_OPTIONS: RadioOption[] = [ { value: "option1", name: "Option 1" }, @@ -14,177 +50,113 @@ const BASIC_OPTIONS: RadioOption[] = [ { value: "option3", name: "Option 3" }, ]; -const OPTIONS_WITH_DESCRIPTIONS: RadioOption[] = [ - { value: "low", name: "Low Quality", desc: "Faster loading, uses less data" }, - { value: "medium", name: "Medium Quality", desc: "Balanced quality and performance" }, - { value: "high", name: "High Quality", desc: "Best quality, uses more data" }, +const DESC_OPTIONS: RadioOption[] = [ + { value: "low", name: "Low Quality", description: "Faster loading, uses less data" }, + { value: "medium", name: "Medium Quality", description: "Balanced quality and performance" }, + { value: "high", name: "High Quality", description: "Best quality, uses more data" }, ]; -const OPTIONS_WITH_ICONS: RadioOption[] = [ - { value: "item1", name: "Item with icon", desc: "This option has an icon", leadingIcon: EquicordIcon }, - { value: "item2", name: "Another with icon", leadingIcon: EquicordIcon }, - { value: "item3", name: "Third with icon", leadingIcon: EquicordIcon }, +const ICON_OPTIONS: RadioOption[] = [ + { value: "item1", name: "Item with icon", description: "This option has an icon", icon: EquicordIcon }, + { value: "item2", name: "Another with icon", icon: EquicordIcon }, + { value: "item3", name: "Third with icon", icon: EquicordIcon }, ]; -const OPTIONS_MIXED: RadioOption[] = [ +const MIXED_OPTIONS: RadioOption[] = [ { value: "enabled", name: "Enabled option" }, { value: "disabled", name: "Disabled option", disabled: true }, { value: "another", name: "Another enabled" }, ]; -const NUMBERED_OPTIONS: RadioOption[] = [ - { value: 1, name: "First choice" }, - { value: 2, name: "Second choice" }, - { value: 3, name: "Third choice" }, -]; +function BasicDemo() { + const [value, setValue] = useState("option1"); + return ; +} -export default function RadioGroupTab() { - const [basicValue, setBasicValue] = useState("option1"); - const [descValue, setDescValue] = useState("medium"); - const [iconValue, setIconValue] = useState("item1"); - const [mixedValue, setMixedValue] = useState("enabled"); - const [numberedValue, setNumberedValue] = useState(1); - const [unselectedValue, setUnselectedValue] = useState(undefined); - const [standaloneSelected, setStandaloneSelected] = useState(false); +function DescDemo() { + const [value, setValue] = useState("medium"); + return ; +} +function IconDemo() { + const [value, setValue] = useState("item1"); + return ; +} + +function DisabledItemDemo() { + const [value, setValue] = useState("enabled"); + return ; +} + +function UnselectedDemo() { + const [value, setValue] = useState(undefined); + return ; +} + +function StandaloneDemo() { + const [selected, setSelected] = useState(false); return ( -
- - - Simple radio group with text-only options. - - - - - - - Radio options with descriptive text using the desc prop. - - - - - - - Radio options with icons using leadingIcon prop. - - - - - - - Individual options can be disabled with disabled: true. - - - - - - - Radio options can use numeric values. - - setNumberedValue(v as number)} - /> - - - - - Radio group with no initial selection (value undefined). - - - - - - - The entire group can be disabled with disabled prop. - - - - - - - A standalone animated radio indicator component. Click to toggle. - -
setStandaloneSelected(!standaloneSelected)} - style={{ cursor: "pointer", display: "inline-block" }} - > - -
-
- - - - BaseRadioGroup - - - • options: RadioOption[] - Array of radio options - - - • value?: string | number - Currently selected value - - - • onChange?: (value: string | number) => void - Called when selection changes - - - • disabled?: boolean - Disable entire group - - - • aria-labelledby?: string - Accessibility label reference - - - RadioOption - - - • value: string | number - Option value - - - • name: string - Option label text - - - • desc?: string - Optional description text - - - • disabled?: boolean - Disable individual option - - - • leadingIcon?: React.ComponentType - Icon before radio - - - StandaloneRadioIndicator - - - • isSelected: boolean - Whether the indicator is selected - - - • disabled?: boolean - Disable the indicator - - +
setSelected(!selected)} style={{ cursor: "pointer", display: "inline-block" }}> +
); } + +export default function RadioGroupTab() { + return ( + , + code: ``, + relevantProps: ["options", "value", "onChange"], + }, + { + title: "With Descriptions", + description: "Options with descriptive text using the description field.", + children: , + }, + { + title: "With Leading Icons", + description: "Each option can have an icon component before the radio indicator.", + children: , + }, + { + title: "Disabled Option", + description: "Individual options can be disabled while others remain interactive.", + children: , + }, + { + title: "No Initial Selection", + description: "Radio group with no option initially selected.", + children: , + }, + { + title: "Entire Group Disabled", + description: "The entire group is non-interactive.", + children: , + relevantProps: ["disabled"], + }, + { + title: "Standalone Radio Indicator", + description: "An animated radio circle component for custom radio UIs. Click to toggle.", + children: , + code: "", + }, + ]} + props={PROP_GROUPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx b/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx index f2bede8bf..95ed33b63 100644 --- a/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/RichTooltipTab.tsx @@ -4,175 +4,123 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaButton, ManaRichTooltip , Paragraph } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaButton, ManaRichTooltip } from ".."; +import { DocPage, type PropDef } from "../DocPage"; + +const RICHTOOLTIP_PROPS: PropDef[] = [ + { name: "body", type: "string", required: true, description: "Tooltip body text." }, + { name: "children", type: "ReactNode | ((props) => ReactNode)", required: true, description: "Trigger element or render function." }, + { name: "title", type: "string", description: "Bold title text above the body." }, + { name: "position", type: '"top" | "bottom" | "left" | "right"', description: "Position relative to the trigger." }, + { name: "align", type: '"start" | "center" | "end"', description: "Alignment along the position axis." }, + { name: "spacing", type: "number", description: "Distance in pixels from the trigger element." }, + { name: "asContainer", type: "boolean", description: "Wrap children in a container element for hover detection." }, + { name: "element", type: '"span" | "div"', description: "HTML element type for the container when asContainer is true." }, + { name: "asset", type: "ReactNode | { src: string; alt?: string }", description: "Image or element displayed in the tooltip." }, + { name: "assetSize", type: "number", description: "Size of the asset in pixels." }, + { name: "targetElementRef", type: "RefObject", description: "Ref to an external trigger element instead of wrapping children." }, + { name: "anchorRef", type: "RefObject", description: "Ref to an anchor element for positioning." }, + { name: "positionKey", type: "string", internal: true, description: "Unique key for position caching." }, + { name: "caretConfig", type: '{ align?: "start" | "center" | "end" }', description: "Configuration for the caret arrow." }, + { name: "layerContext", type: "any", internal: true, description: "Layer context for stacking." }, + { name: "ariaHidden", type: "boolean", default: "false", description: "Hide the tooltip from screen readers." }, + { name: "shouldShow", type: "boolean", default: "true", description: "Externally control whether the tooltip is shown." }, + { name: "delay", type: "number", description: "Delay in milliseconds before the tooltip appears." }, + { name: "forceOpen", type: "boolean", default: "false", description: "Force the tooltip to stay open." }, + { name: "overflowOnly", type: "boolean", default: "false", description: "Only show when the trigger text is overflowing." }, + { name: "hideOnClick", type: "boolean", default: "true", description: "Hide when the trigger is clicked." }, + { name: "onTooltipShow", type: "() => void", description: "Callback when the tooltip becomes visible." }, + { name: "onTooltipHide", type: "() => void", description: "Callback when the tooltip is hidden." }, +]; export default function RichTooltipTab() { return ( -
- - - Rich tooltip with title and body text. - -
- - - -
-
- - - - Rich tooltip without title. - -
- - - -
-
- - -
- - - - - - - - - - - - -
-
- - -
- - - - - - - - - -
-
- - -
- - - - - - -
-
- - -
- - - - - - -
-
- - - - • title?: string - Tooltip title (bold) - - - • body?: string - Tooltip body text - - - • children: ReactNode - Trigger element - - - • position?: "top" | "bottom" | "left" | "right" - Position - - - • align?: "start" | "center" | "end" - Alignment - - - • spacing?: number - Distance from trigger (px) - - - • asContainer?: boolean - Wrap children in container - - - • element?: "span" | "div" - Container element type - - -
+ + + + + + + +
+ ), + code: ` + +`, + relevantProps: ["title", "body", "asContainer"], + }, + { + title: "Positions", + description: "Tooltip can appear on any side of the trigger element.", + children: ( +
+ {(["top", "bottom", "left", "right"] as const).map(pos => ( + + + + ))} +
+ ), + relevantProps: ["position"], + }, + { + title: "Alignment", + description: "Align the tooltip along the position axis.", + children: ( +
+ {(["start", "center", "end"] as const).map(align => ( + + + + ))} +
+ ), + relevantProps: ["align"], + }, + { + title: "Custom Spacing", + description: "Control the distance between tooltip and trigger.", + children: ( +
+ + + + + + +
+ ), + relevantProps: ["spacing"], + }, + { + title: "Container Element", + description: "Choose the wrapper element type for the trigger.", + children: ( +
+ + + + + + +
+ ), + relevantProps: ["element"], + }, + ]} + props={RICHTOOLTIP_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/ScrollerTab.tsx b/src/equicordplugins/components.dev/components/tabs/ScrollerTab.tsx index 2fc6ac317..be3dadba9 100644 --- a/src/equicordplugins/components.dev/components/tabs/ScrollerTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ScrollerTab.tsx @@ -5,11 +5,22 @@ */ import { ManaButton, Paragraph, ScrollerAuto, ScrollerNone, ScrollerThin, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; + +const SCROLLER_PROPS: PropDef[] = [ + { name: "children", type: "ReactNode", required: true, description: "Content to render inside the scrollable area." }, + { name: "orientation", type: '"vertical" | "horizontal"', default: '"vertical"', description: "Scroll direction." }, + { name: "paddingFix", type: "boolean", default: "true", description: "Apply padding fix for scroll containers." }, + { name: "fade", type: "boolean", default: "false", description: "Add a fade effect at the edges to indicate more content." }, + { name: "dir", type: '"ltr" | "rtl"', default: '"ltr"', description: "Text direction for horizontal scrolling." }, + { name: "onScroll", type: "(e: UIEvent) => void", description: "Scroll event handler." }, + { name: "className", type: "string", description: "Custom CSS class." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, +]; const SAMPLE_ITEMS = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`); -export default function ScrollerTab() { +function ScrollerDemo({ Component, label }: { Component: React.ComponentType; label: string; }) { const [orientation, setOrientation] = useState<"vertical" | "horizontal">("vertical"); const [fade, setFade] = useState(true); @@ -22,83 +33,58 @@ export default function ScrollerTab() { : { padding: "8px 16px", display: "inline-block", whiteSpace: "nowrap" as const }; return ( -
- -
- setOrientation(o => o === "vertical" ? "horizontal" : "vertical")} - /> - setFade(f => !f)} - /> -
-
- - - - Thin scrollbar, most commonly used. Ideal for sidebars and lists. - - - {SAMPLE_ITEMS.map(item => ( -
- {item} -
- ))} -
-
- - - - Standard scrollbar that appears automatically when content overflows. - - - {SAMPLE_ITEMS.map(item => ( -
- {item} -
- ))} -
-
- - - - No visible scrollbar, content still scrollable via mouse wheel/touch. - - - {SAMPLE_ITEMS.map(item => ( -
- {item} -
- ))} -
-
- - - • orientation - "vertical" | "horizontal" (default: "vertical") - • fade - boolean, adds fade effect at edges - • className - custom CSS class - • style - inline styles - • children - content to scroll - • dir - text direction ("ltr" | "rtl") - - - - • ScrollerThin - Thin scrollbar (most common) - • ScrollerAuto - Standard auto-showing scrollbar - • ScrollerNone - Hidden scrollbar - • ListScrollerThin - Virtualized list with thin scrollbar - • ListScrollerAuto - Virtualized list with auto scrollbar - - - - - {"import { ScrollerThin, ScrollerAuto, ScrollerNone } from \"@webpack/common\";"} - - +
+
+ setOrientation(o => o === "vertical" ? "horizontal" : "vertical")} + /> + setFade(f => !f)} + /> +
+ + {SAMPLE_ITEMS.map(item => ( +
+ {item} +
+ ))} +
); } + +export default function ScrollerTab() { + return ( + , + code: ` + {items.map(item =>
{item.label}
)} +
`, + relevantProps: ["orientation", "fade"], + }, + { + title: "ScrollerAuto", + description: "Standard scrollbar that appears automatically when content overflows.", + children: , + }, + { + title: "ScrollerNone", + description: "No visible scrollbar. Content is still scrollable via mouse wheel or touch.", + children: , + }, + ]} + props={SCROLLER_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx index 6a7b87094..10a702d55 100644 --- a/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SearchBarTab.tsx @@ -4,75 +4,91 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Paragraph, SearchBar, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { SearchBar, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; -export default function SearchBarTab() { - const [query1, setQuery1] = useState(""); - const [query2, setQuery2] = useState(""); - const [query3, setQuery3] = useState("prefilled"); +const SEARCHBAR_PROPS: PropDef[] = [ + { name: "query", type: "string", required: true, description: "Current search query value." }, + { name: "onChange", type: "(value: string) => void", required: true, description: "Called when the query text changes." }, + { name: "onClear", type: "() => void", description: "Called when the clear button is clicked." }, + { name: "placeholder", type: "string", description: "Placeholder text when empty." }, + { name: "size", type: '"sm" | "md"', description: "Input size variant." }, + { name: "autoFocus", type: "boolean", description: "Focus the input on mount." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable the search bar." }, + { name: "onKeyDown", type: "(e: KeyboardEvent) => void", description: "Keyboard event handler." }, + { name: "onBlur", type: "() => void", description: "Called when the input loses focus." }, + { name: "onFocus", type: "() => void", description: "Called when the input gains focus." }, + { name: "autoComplete", type: "string", description: "HTML autocomplete attribute value." }, + { name: "inputProps", type: "Record", description: "Additional props passed to the underlying input element." }, + { name: "className", type: "string", description: "Custom CSS class." }, + { name: "aria-label", type: "string", description: "Accessibility label." }, +]; +function BasicDemo() { + const [query, setQuery] = useState(""); return ( -
- - - Default search bar with clear button. - - setQuery1("")} - placeholder="Search..." - /> - + setQuery("")} + placeholder="Search..." + /> + ); +} - - - Available sizes: sm, md (default). - -
- setQuery2("")} - placeholder="Small size" - size="sm" - /> - setQuery2("")} - placeholder="Medium size" - size="md" - /> -
-
- - - - Disabled and prefilled states. - -
- { }} - placeholder="Disabled" - disabled - /> - setQuery3("")} - placeholder="Prefilled" - /> -
-
- - - - query, onChange, onClear?, placeholder?, size?, autoFocus?, disabled?, onKeyDown?, onBlur?, onFocus?, autoComplete?, inputProps?, aria-label? - - +function SizesDemo() { + const [query, setQuery] = useState(""); + return ( +
+ setQuery("")} placeholder="Small size" size="sm" /> + setQuery("")} placeholder="Medium size (default)" size="md" />
); } + +function StatesDemo() { + const [prefilled, setPrefilled] = useState("prefilled"); + return ( +
+ { }} placeholder="Disabled" disabled /> + setPrefilled("")} placeholder="Prefilled" /> +
+ ); +} + +export default function SearchBarTab() { + return ( + , + code: ` setQuery("")} + placeholder="Search..." +/>`, + relevantProps: ["query", "onChange", "onClear", "placeholder"], + }, + { + title: "Sizes", + description: "Available in sm and md (default) sizes.", + children: , + relevantProps: ["size"], + }, + { + title: "States", + description: "Disabled and prefilled states.", + children: , + relevantProps: ["disabled"], + }, + ]} + props={SEARCHBAR_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx b/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx index b50f84a1d..b4cf83680 100644 --- a/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SelectTab.tsx @@ -4,8 +4,39 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaSelect, ManaSelectOption, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaSelect, ManaSelectOption, useState } from ".."; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; + +const SELECT_PROPS: PropDef[] = [ + { name: "options", type: "ManaSelectOption[]", required: true, description: "Array of selectable options." }, + { name: "value", type: "string | string[] | null", description: "Currently selected value(s)." }, + { name: "onSelectionChange", type: "(value: string | string[] | null) => void", description: "Called when the selection changes. Note: NOT onChange." }, + { name: "selectionMode", type: '"single" | "multiple"', description: "Single or multi-select mode." }, + { name: "placeholder", type: "string", description: "Placeholder text when nothing is selected." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable the select." }, + { name: "readOnly", type: "boolean", description: "Make the select read-only." }, + { name: "clearable", type: "boolean", description: "Allow clearing the selection." }, + { name: "fullWidth", type: "boolean", description: "Expand to fill available width." }, + { name: "autoFocus", type: "boolean", description: "Focus on mount." }, + { name: "closeOnSelect", type: "boolean", description: "Close the dropdown after selecting." }, + { name: "shouldFocusWrap", type: "boolean", description: "Wrap keyboard focus at list ends." }, + { name: "maxOptionsVisible", type: "number", description: "Maximum options visible before scrolling." }, + { name: "wrapTags", type: "boolean", description: "Wrap selected tags to multiple lines in multi-select." }, + { name: "formatOption", type: "(option: ManaSelectOption) => ReactNode", description: "Custom render for each option." }, + { name: "label", type: "string", description: "Label text for the select." }, + { name: "required", type: "boolean", description: "Mark as required for form validation." }, +]; + +const OPTION_PROPS: PropDef[] = [ + { name: "id", type: "string", required: true, description: "Unique option identifier." }, + { name: "value", type: "string", required: true, description: "Option value." }, + { name: "label", type: "string", required: true, description: "Display label." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { title: "ManaSelect", props: SELECT_PROPS }, + { title: "ManaSelectOption", props: OPTION_PROPS }, +]; const SAMPLE_OPTIONS: ManaSelectOption[] = [ { id: "1", value: "option1", label: "Option 1" }, @@ -14,107 +45,111 @@ const SAMPLE_OPTIONS: ManaSelectOption[] = [ { id: "4", value: "option4", label: "Option 4" }, ]; -export default function SelectTab() { - const [singleValue, setSingleValue] = useState("option1"); - const [multiValue, setMultiValue] = useState(["option1", "option2"]); - const [clearableValue, setClearableValue] = useState("option1"); - const [placeholderValue, setPlaceholderValue] = useState(""); - +function SingleDemo() { + const [value, setValue] = useState("option1"); return ( -
- - setSingleValue(v as string)} - selectionMode="single" - /> - - - - setMultiValue(v as string[])} - selectionMode="multiple" - /> - - - - setPlaceholderValue(v as string)} - placeholder="Select an option..." - /> - - - - setClearableValue(v as string)} - clearable - /> - - - - - - - - - - - - - ManaSelect - Note: Uses onSelectionChange, NOT onChange. - - - • options: ManaSelectOption[] - Array of options - - - • value?: string | string[] | null - Selected value(s) - - - • onSelectionChange?: (value) => void - Called when selection changes - - - • selectionMode?: "single" | "multiple" - Selection mode - - - • placeholder?: string - Placeholder text - - - • clearable?: boolean - Allow clearing selection - - - • fullWidth?: boolean - Expand to full width - - - • disabled?: boolean - Disable select - - - ManaSelectOption - - - • id: string - Unique option ID - - - • value: string - Option value - - - • label: string - Display label - - -
+ setValue(v as string)} + selectionMode="single" + /> + ); +} + +function MultiDemo() { + const [value, setValue] = useState(["option1", "option2"]); + return ( + setValue(v as string[])} + selectionMode="multiple" + /> + ); +} + +function PlaceholderDemo() { + const [value, setValue] = useState(""); + return ( + setValue(v as string)} + placeholder="Select an option..." + /> + ); +} + +function ClearableDemo() { + const [value, setValue] = useState("option1"); + return ( + setValue(v as string)} + clearable + /> + ); +} + +export default function SelectTab() { + return ( + , + code: ` setValue(v as string)} + selectionMode="single" +/>`, + relevantProps: ["options", "value", "onSelectionChange", "selectionMode"], + }, + { + title: "Multiple Selection", + description: "Multi-select with tag pills for each selected option.", + children: , + }, + { + title: "With Placeholder", + description: "Shows placeholder text when no option is selected.", + children: , + relevantProps: ["placeholder"], + }, + { + title: "Clearable", + description: "Adds a clear button to reset the selection.", + children: , + relevantProps: ["clearable"], + }, + { + title: "Full Width", + description: "Expands to fill the available width.", + children: ( + + ), + relevantProps: ["fullWidth"], + }, + { + title: "Disabled", + description: "Non-interactive disabled state.", + children: ( + + ), + relevantProps: ["disabled"], + }, + ]} + props={PROP_GROUPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx b/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx index a2674042d..386c23535 100644 --- a/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SkeletonTab.tsx @@ -4,45 +4,64 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaButton, Paragraph, Skeleton, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaButton, Skeleton, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; -export default function SkeletonTab() { +const SKELETON_PROPS: PropDef[] = [ + { name: "withHeader", type: "boolean", default: "true", description: "Show a header placeholder row." }, + { name: "size", type: "number", default: "15", description: "Number of skeleton rows to display." }, +]; + +function SkeletonDemo() { const [withHeader, setWithHeader] = useState(true); const [size, setSize] = useState(5); return ( -
- - - Loading placeholder with shimmer animation. Used in Discord's member list loading state. - -
- setWithHeader(!withHeader)} - /> - setSize(Math.max(1, size - 1))} - /> - setSize(Math.min(20, size + 1))} - /> -
-
- -
-
- - - • withHeader?: boolean - Show header placeholder (default: true) - • size?: number - Number of skeleton rows to display (default: 15) - +
+
+ setWithHeader(!withHeader)} /> + setSize(Math.max(1, size - 1))} /> + setSize(Math.min(20, size + 1))} /> +
+
+ +
); } + +export default function SkeletonTab() { + return ( + , + code: "", + relevantProps: ["withHeader", "size"], + }, + { + title: "Header Only", + children: ( +
+ +
+ ), + }, + { + title: "No Header", + children: ( +
+ +
+ ), + }, + ]} + props={SKELETON_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx b/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx index 4b7432020..4f2b1024c 100644 --- a/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SliderTab.tsx @@ -5,103 +5,144 @@ */ import { Paragraph, Slider, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function SliderTab() { - const [basicValue, setBasicValue] = useState(50); - const [markerValue, setMarkerValue] = useState(50); - const [miniValue, setMiniValue] = useState(30); +const SLIDER_PROPS: PropDef[] = [ + { name: "initialValue", type: "number", description: "Initial slider value (uncontrolled)." }, + { name: "value", type: "number", description: "Current slider value (controlled)." }, + { name: "minValue", type: "number", description: "Minimum range value." }, + { name: "maxValue", type: "number", description: "Maximum range value." }, + { name: "onValueChange", type: "(value: number) => void", description: "Called when the value changes (on release)." }, + { name: "asValueChanges", type: "(value: number) => void", description: "Called continuously as the value changes (while dragging)." }, + { name: "onValueRender", type: "(value: number) => string | null", description: "Custom tooltip label for the current value." }, + { name: "markers", type: "number[]", description: "Array of values to show as tick marks." }, + { name: "stickToMarkers", type: "boolean", description: "Snap the slider value to the nearest marker." }, + { name: "equidistant", type: "boolean", description: "Space markers equally regardless of value." }, + { name: "onMarkerRender", type: "(value: number) => string | ReactNode", description: "Custom label for each marker tick." }, + { name: "keyboardStep", type: "number", description: "Step size for keyboard arrow key increments." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable interaction." }, + { name: "mini", type: "boolean", description: "Use compact visual style." }, + { name: "hideBubble", type: "boolean", description: "Hide the value tooltip bubble." }, + { name: "orientation", type: '"horizontal" | "vertical"', default: '"horizontal"', description: "Slider orientation." }, + { name: "className", type: "string", description: "Custom CSS class." }, +]; +function BasicDemo() { + const [value, setValue] = useState(50); return ( -
- - - Value: {basicValue} - - - - - - - Value: {markerValue} - - `${v}%`} - /> - - - - - Value snaps to markers. - - `${v}%`} - /> - - - - - Value: {miniValue} - -
- -
-
- - - - - - - - Range: -50 to 50 - - v.toString()} - /> - - - - • initialValue / value - Current value - • minValue / maxValue - Range bounds - • onValueChange - Change callback - • markers - Array of marker values - • stickToMarkers - Snap to markers - • onMarkerRender - Custom marker labels - • mini - Compact style - • hideBubble - Hide value tooltip - • disabled - Disable interaction - +
+ Value: {value} +
); } + +function MarkersDemo() { + const [value, setValue] = useState(50); + return ( +
+ Value: {value} + `${v}%`} + /> +
+ ); +} + +function MiniDemo() { + const [value, setValue] = useState(30); + return ( +
+ Value: {value} +
+ +
+
+ ); +} + +export default function SliderTab() { + return ( + , + code: ``, + relevantProps: ["initialValue", "minValue", "maxValue", "onValueChange"], + }, + { + title: "With Markers", + description: "Tick marks at specified values with custom labels.", + children: , + code: ` \`\${v}%\`} +/>`, + relevantProps: ["markers", "onMarkerRender"], + }, + { + title: "Stick to Markers", + description: "Value snaps to the nearest marker position.", + children: ( + `${v}%`} + /> + ), + relevantProps: ["stickToMarkers"], + }, + { + title: "Mini", + description: "Compact visual style for tight layouts.", + children: , + relevantProps: ["mini"], + }, + { + title: "Disabled", + description: "Non-interactive disabled state.", + children: , + relevantProps: ["disabled"], + }, + { + title: "Custom Range", + description: "Slider with negative to positive range.", + children: ( + v.toString()} + /> + ), + }, + ]} + props={SLIDER_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx b/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx index 45e607d75..e1bb67949 100644 --- a/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SpinnerTab.tsx @@ -5,88 +5,81 @@ */ import { ManaButton, Paragraph, Spinner, SpinnerTypes, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; -export default function SpinnerTab() { +const SPINNER_PROPS: PropDef[] = [ + { name: "type", type: "SpinnerType", description: 'Spinner variant. One of: "wanderingCubes", "chasingDots", "pulsingEllipsis", "spinningCircle", "spinningCircleSimple", "lowMotion".' }, + { name: "animated", type: "boolean", default: "true", description: "Enable or pause the animation." }, + { name: "className", type: "string", description: "CSS class for the container." }, + { name: "itemClassName", type: "string", description: "CSS class for inner animation items." }, + { name: "aria-label", type: "string", description: "Accessibility label." }, +]; + +function AnimationToggleDemo() { const [animated, setAnimated] = useState(true); - return ( -
- - - All available spinner variants. +
+ setAnimated(!animated)} + style={{ marginBottom: 16 }} + /> +
+ + + animated={animated.toString()} -
- {SpinnerTypes.map(type => ( -
- - - {type} - -
- ))} -
- - - - - Spinners can be paused with animated=false. - -
- setAnimated(!animated)} - /> -
-
-
- - - animated={animated.toString()} - -
-
-
- - - - lowMotion type is used when user prefers reduced motion. - The component automatically switches to this when reducedMotion is enabled. - -
-
- -
-
-
- - - - • type?: SpinnerType - Spinner variant - - - • animated?: boolean - Enable/disable animation (default: true) - - - • className?: string - Container class - - - • itemClassName?: string - Inner item class - - - • aria-label?: string - Accessibility label - - - - - • "wanderingCubes" - • "chasingDots" - • "pulsingEllipsis" - • "spinningCircle" - • "spinningCircleSimple" - • "lowMotion" - +
); } + +export default function SpinnerTab() { + return ( + + {SpinnerTypes.map(type => ( +
+ + + {type} + +
+ ))} +
+ ), + code: '', + relevantProps: ["type"], + }, + { + title: "Animation Toggle", + description: "Spinners can be paused with animated=false.", + children: , + relevantProps: ["animated"], + }, + { + title: "Low Motion", + description: "The lowMotion type is used when the user prefers reduced motion. The component automatically switches to this variant.", + children: ( +
+ +
+ ), + }, + ]} + props={SPINNER_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx b/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx index 766161626..399041ddd 100644 --- a/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/SwitchTab.tsx @@ -6,8 +6,8 @@ import { findComponentByCodeLazy } from "@webpack"; -import { ManaSwitch, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaSwitch, useState } from ".."; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; interface SwitchWithLabelProps { checked: boolean; @@ -19,111 +19,132 @@ interface SwitchWithLabelProps { const SwitchWithLabel = findComponentByCodeLazy('"data-toggleable-component":"switch"') as React.ComponentType; -export default function SwitchTab() { - const [basicOn, setBasicOn] = useState(true); - const [basicOff, setBasicOff] = useState(false); +const SWITCH_PROPS: PropDef[] = [ + { name: "checked", type: "boolean", required: true, description: "Current toggle state." }, + { name: "onChange", type: "(checked: boolean) => void", description: "Called when the switch is toggled." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable interaction." }, + { name: "id", type: "string", description: "HTML id attribute." }, + { name: "hasIcon", type: "boolean", description: "Show a checkmark/x icon inside the toggle." }, + { name: "describedBy", type: "string", internal: true, description: "ID of an element describing this switch." }, + { name: "labelledBy", type: "string", internal: true, description: "ID of an element labelling this switch." }, + { name: "innerRef", type: "Ref", internal: true, description: "Ref to the underlying input element." }, +]; - const [labelOn, setLabelOn] = useState(true); - const [labelOff, setLabelOff] = useState(false); +const SWITCH_WITH_LABEL_PROPS: PropDef[] = [ + { name: "checked", type: "boolean", required: true, description: "Current toggle state." }, + { name: "onChange", type: "(checked: boolean) => void", description: "Called when the switch is toggled." }, + { name: "label", type: "string", description: "Label text displayed next to the switch." }, + { name: "description", type: "string", description: "Description text below the label." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable interaction." }, +]; - const [withDesc, setWithDesc] = useState(true); +const PROP_GROUPS: PropGroup[] = [ + { title: "ManaSwitch", props: SWITCH_PROPS }, + { title: "SwitchWithLabel", props: SWITCH_WITH_LABEL_PROPS }, +]; + +function BasicDemo() { + const [on, setOn] = useState(true); + const [off, setOff] = useState(false); return ( -
- -
-
- On: - -
-
- Off: - -
-
- Disabled On: - -
-
- Disabled Off: - -
-
-
- - -
- - -
-
- - -
- -
-
- - -
- - -
-
- - - - ManaSwitch - Basic toggle switch without label. - - - • checked: boolean - Current switch state - - - • onChange?: (checked: boolean) => void - Called when toggled - - - • disabled?: boolean - Disable interaction - - - SwitchWithLabel - Switch with label and description. - - - • label?: string - Label text displayed next to switch - - - • description?: string - Description text below label - - - • checked: boolean - Current switch state - - - • onChange?: (checked: boolean) => void - Called when toggled - - - • disabled?: boolean - Disable interaction - - +
+
+ On: + +
+
+ Off: + +
+
+ Disabled On: + +
+
+ Disabled Off: + +
); } + +function LabelDemo() { + const [on, setOn] = useState(true); + const [off, setOff] = useState(false); + + return ( +
+ + +
+ ); +} + +function DescriptionDemo() { + const [checked, setChecked] = useState(true); + return ( + + ); +} + +export default function SwitchTab() { + return ( + , + code: "", + relevantProps: ["checked", "onChange", "disabled"], + }, + { + title: "Switch with Label", + description: "SwitchWithLabel renders a label next to the toggle.", + children: , + code: ``, + }, + { + title: "With Description", + description: "Label with description text below.", + children: , + code: ``, + }, + { + title: "Disabled States", + description: "Both checked and unchecked disabled states.", + children: ( +
+ + +
+ ), + }, + ]} + props={PROP_GROUPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx b/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx index cc3180e56..56f6031b5 100644 --- a/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TabBarTab.tsx @@ -5,184 +5,175 @@ */ import { Paragraph, TabBar, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef, type PropGroup } from "../DocPage"; -export default function TabBarTab() { - const [sideTab, setSideTab] = useState("tab1"); - const [topTab, setTopTab] = useState("tab1"); - const [topPillTab, setTopPillTab] = useState("tab1"); - const [brandTab, setBrandTab] = useState("tab1"); +const TABBAR_PROPS: PropDef[] = [ + { name: "type", type: '"side" | "top" | "top-pill"', description: "Tab bar visual style." }, + { name: "look", type: '"brand" | "grey"', description: "Color scheme." }, + { name: "selectedItem", type: "string", description: "ID of the currently selected tab." }, + { name: "onItemSelect", type: "(id: string) => void", description: "Called when a tab is selected." }, + { name: "orientation", type: '"horizontal" | "vertical"', description: "Layout direction." }, + { name: "className", type: "string", description: "Custom CSS class." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, + { name: "aria-label", type: "string", description: "Accessibility label." }, +]; + +const ITEM_PROPS: PropDef[] = [ + { name: "id", type: "string", required: true, description: "Unique tab identifier." }, + { name: "children", type: "ReactNode", required: true, description: "Tab label content." }, + { name: "disabled", type: "boolean", description: "Disable the tab." }, + { name: "color", type: "string", description: "Custom tab color (hex or CSS color)." }, + { name: "variant", type: '"destructive"', description: "Danger styling for actions like logout." }, + { name: "disableItemStyles", type: "boolean", description: "Remove default tab styles." }, + { name: "onClick", type: "MouseEventHandler", description: "Custom click handler." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { title: "TabBar", props: TABBAR_PROPS }, + { title: "TabBar.Item", props: ITEM_PROPS }, +]; + +function SideDemo() { + const [tab, setTab] = useState("tab1"); + return ( +
+ Selected: {tab} + + First Tab + Second Tab + Third Tab + +
+ ); +} + +function TopDemo() { + const [tab, setTab] = useState("tab1"); + return ( + + Overview + Settings + Members + + ); +} + +function TopPillDemo() { + const [tab, setTab] = useState("tab1"); + return ( + + All + Online + Pending + Blocked + + ); +} + +function BrandDemo() { + const [tab, setTab] = useState("tab1"); + return ( + + Home + Explore + Library + + ); +} + +function HeaderSeparatorDemo() { + const [tab, setTab] = useState("tab1"); + return ( + + User Settings + My Account + Privacy + + App Settings + Appearance + Notifications + + ); +} + +function FeaturesDemo() { + const [tab, setTab] = useState("tab1"); const [colorTab, setColorTab] = useState("tab1"); return ( -
- - - Default side-style tab bar with grey look. Selected: {sideTab} - - - First Tab - Second Tab - Third Tab - - - - - - Top-style tab bar with underline indicator. Selected: {topTab} - - - Overview - Settings - Members - - - - - - Pill-style tabs for top navigation. Selected: {topPillTab} - - - All - Online - Pending - Blocked - - - - - - Brand-colored tab bar style. Selected: {brandTab} - - - Home - Explore - Library - - - - - - Tab bar with header section and separator. - - - User Settings - My Account - Privacy - - App Settings - Appearance - Notifications - - - - - - Some tabs can be disabled. - - +
+
+ Disabled items: + Active Disabled Another Active - - - - - Individual tabs can have custom colors. Selected: {colorTab} - - +
+
+ Custom colors: + Green Yellow Red - - - - - Tab with destructive/danger styling. - - +
+
+ Destructive variant: + Settings - Privacy Log Out - - - - - TabBar - Container component - - • type?: "side" | "top" | "top-pill" - Tab bar style - • look?: "brand" | "grey" - Color scheme - • selectedItem?: string - Currently selected tab ID - • onItemSelect?: (id) ={">"} void - Selection callback - • orientation?: "horizontal" | "vertical" - Layout direction - • className?: string - Additional CSS classes - • style?: CSSProperties - Inline styles - • aria-label?: string - Accessibility label - - - TabBar.Item - Individual tab - - • id: string - Unique tab identifier (required) - • disabled?: boolean - Disable the tab - • color?: string - Custom tab color - • variant?: "destructive" - Danger styling - • disableItemStyles?: boolean - Remove default styles - • onClick?: MouseEventHandler - Custom click handler - - - TabBar.Header - Section header - - • children: ReactNode - Header text - • onClick?: MouseEventHandler - Click handler - - - TabBar.Separator - Visual divider - - • style?: CSSProperties - Custom styles - +
); } + +export default function TabBarTab() { + return ( + , + code: ` + First Tab + Second Tab +`, + relevantProps: ["type", "look", "selectedItem", "onItemSelect"], + }, + { + title: "Top", + description: "Horizontal tabs with underline indicator.", + children: , + }, + { + title: "Top Pill", + description: "Pill-style horizontal tabs.", + children: , + }, + { + title: "Brand Look", + description: "Brand-colored tab styling.", + children: , + }, + { + title: "Headers and Separators", + description: "TabBar.Header and TabBar.Separator for organizing groups of tabs.", + children: , + }, + { + title: "Disabled, Colors, Destructive", + description: "Items can be disabled, colored, or styled as destructive.", + children: , + relevantProps: ["disabled", "color", "variant"], + }, + ]} + props={PROP_GROUPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx b/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx index ed854a248..954bdcefa 100644 --- a/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TextAreaTab.tsx @@ -4,74 +4,97 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaTextArea, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaTextArea, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; + +const TEXTAREA_PROPS: PropDef[] = [ + { name: "value", type: "string", description: "Current textarea value (controlled)." }, + { name: "defaultValue", type: "string", description: "Initial value (uncontrolled)." }, + { name: "onChange", type: "(value: string, name?: string) => void", description: "Called when the text changes." }, + { name: "placeholder", type: "string", description: "Placeholder text when empty." }, + { name: "name", type: "string", description: "HTML name attribute for form submission." }, + { name: "rows", type: "number", description: "Fixed number of visible text rows." }, + { name: "autosize", type: "boolean", description: "Automatically grow height to fit content." }, + { name: "autoFocus", type: "boolean", description: "Focus on mount." }, + { name: "maxLength", type: "number", description: "Maximum character limit." }, + { name: "minLength", type: "number", description: "Minimum character requirement." }, + { name: "showCharacterCount", type: "boolean", description: "Display current character count." }, + { name: "showRemainingCharacterCount", type: "boolean", description: "Display remaining characters instead of current count." }, + { name: "error", type: "string | boolean", description: "Error state. String shows error message, boolean shows red border." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable the textarea." }, + { name: "readOnly", type: "boolean", description: "Make the textarea read-only." }, + { name: "fullWidth", type: "boolean", description: "Expand to full available width." }, + { name: "inputRef", type: "Ref", internal: true, description: "Ref to the underlying textarea element." }, +]; + +function BasicDemo() { + const [value, setValue] = useState("Hello world\nThis is a textarea"); + return ; +} + +function AutosizeDemo() { + const [value, setValue] = useState("This textarea will grow as you type more content..."); + return ; +} + +function CharCountDemo() { + const [value, setValue] = useState("Count characters"); + return ; +} export default function TextAreaTab() { - const [basic, setBasic] = useState("Hello world\nThis is a textarea"); - const [withPlaceholder, setWithPlaceholder] = useState(""); - const [autosize, setAutosize] = useState("This textarea will grow as you type more content..."); - const [fixedRows, setFixedRows] = useState("Fixed 5 rows"); - const [charCount, setCharCount] = useState("Count characters"); - return ( -
- - - - - - - - - - - - - - - - - - - - - -
- - -
-
- - - - • value: string - Textarea value - - - • onChange?: (value: string) => void - Called on input change - - - • placeholder?: string - Placeholder text - - - • rows?: number - Fixed number of rows - - - • autosize?: boolean - Auto-grow with content - - - • showCharacterCount?: boolean - Display character count - - - • maxLength?: number - Maximum character limit - - - • disabled?: boolean - Disable textarea - - - • readOnly?: boolean - Make textarea read-only - - -
+ , + code: "", + relevantProps: ["value", "onChange"], + }, + { + title: "With Placeholder", + children: ( + { }} placeholder="Enter your message..." /> + ), + relevantProps: ["placeholder"], + }, + { + title: "Autosize", + description: "Automatically grows to fit content as you type.", + children: , + relevantProps: ["autosize"], + }, + { + title: "Fixed Rows", + description: "Fixed height with 5 visible rows.", + children: ( + { }} rows={5} /> + ), + relevantProps: ["rows"], + }, + { + title: "Character Count", + description: "Shows character count with a max length of 200.", + children: , + relevantProps: ["showCharacterCount", "maxLength"], + }, + { + title: "States", + description: "Disabled and read-only states.", + children: ( +
+ + +
+ ), + relevantProps: ["disabled", "readOnly"], + }, + ]} + props={TEXTAREA_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx b/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx index 92d040dcb..aa6f25796 100644 --- a/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TextButtonTab.tsx @@ -4,60 +4,77 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaTextButton, ManaTextButtonTextVariants, ManaTextButtonVariants, Paragraph } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaTextButton, ManaTextButtonTextVariants, ManaTextButtonVariants } from ".."; +import { DocPage, type PropDef } from "../DocPage"; + +const TEXTBUTTON_PROPS: PropDef[] = [ + { name: "text", type: "string", required: true, description: "Button label text." }, + { name: "variant", type: '"primary" | "secondary" | "always-white" | "critical"', description: "Color variant." }, + { name: "textVariant", type: "string", description: 'Typography variant, e.g. "text-sm/medium", "text-lg/medium".' }, + { name: "lineClamp", type: "number", description: "Max lines before truncation with ellipsis." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable the button." }, + { name: "onClick", type: "(e: MouseEvent) => void", description: "Click handler." }, + { name: "role", type: "string", description: "ARIA role attribute." }, + { name: "type", type: '"button" | "submit" | "reset"', description: "HTML button type." }, + { name: "buttonRef", type: "Ref", internal: true, description: "Ref to the underlying button element." }, + { name: "className", type: "string", description: "Custom CSS class." }, + { name: "style", type: "CSSProperties", description: "Inline styles." }, +]; export default function TextButtonTab() { return ( -
- -
- {ManaTextButtonVariants.map(variant => ( - - ))} -
-
- - -
- {ManaTextButtonTextVariants.map(textVariant => ( - - ))} -
-
- - -
- - -
-
- - -
- -
-
- - - • text - Button label text - • variant - primary, secondary, always-white, critical - • textVariant - Typography variant (e.g., text-sm/medium) - • lineClamp - Max lines before truncation - • disabled - Disable button - • onClick - Click handler - -
+ + {ManaTextButtonVariants.map(variant => ( + + ))} +
+ ), + code: '', + relevantProps: ["variant"], + }, + { + title: "Text Variants", + description: "Different typography sizes and weights.", + children: ( +
+ {ManaTextButtonTextVariants.map(textVariant => ( + + ))} +
+ ), + relevantProps: ["textVariant"], + }, + { + title: "States", + description: "Normal and disabled states.", + children: ( +
+ + +
+ ), + relevantProps: ["disabled"], + }, + { + title: "Line Clamp", + description: "Truncate long text to a single line.", + children: ( +
+ +
+ ), + relevantProps: ["lineClamp"], + }, + ]} + props={TEXTBUTTON_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/TextInputTab.tsx b/src/equicordplugins/components.dev/components/tabs/TextInputTab.tsx index 3bb864daf..478b27795 100644 --- a/src/equicordplugins/components.dev/components/tabs/TextInputTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TextInputTab.tsx @@ -4,86 +4,118 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaTextInput, Paragraph, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaTextInput, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; -export default function TextInputTab() { - const [basic, setBasic] = useState("Hello world"); - const [sizeSm, setSizeSm] = useState("Small input"); - const [sizeMd, setSizeMd] = useState("Medium input"); - const [withPlaceholder, setWithPlaceholder] = useState(""); - const [clearable, setClearable] = useState("Clear me"); - const [charCount, setCharCount] = useState("Count chars"); - const [error, setError] = useState("Invalid input"); +const TEXTINPUT_PROPS: PropDef[] = [ + { name: "value", type: "string", description: "Current input value (controlled)." }, + { name: "defaultValue", type: "string", description: "Initial value (uncontrolled)." }, + { name: "onChange", type: "(value: string, name?: string) => void", description: "Called when the input value changes." }, + { name: "placeholder", type: "string", description: "Placeholder text when empty." }, + { name: "name", type: "string", description: "HTML name attribute for form submission." }, + { name: "type", type: '"text" | "password" | "email" | "number" | "search" | "tel" | "url"', default: '"text"', description: "Input type." }, + { name: "size", type: '"sm" | "md"', default: '"md"', description: "Input size variant." }, + { name: "clearable", type: "boolean | { show: boolean }", description: "Show a clear button when the input has a value." }, + { name: "showCharacterCount", type: "boolean", description: "Display current character count." }, + { name: "maxLength", type: "number", description: "Maximum character limit." }, + { name: "minLength", type: "number", description: "Minimum character requirement." }, + { name: "error", type: "string | boolean", description: "Error state. String shows error message, boolean shows red border." }, + { name: "disabled", type: "boolean", default: "false", description: "Disable the input." }, + { name: "readOnly", type: "boolean", description: "Make the input read-only." }, + { name: "fullWidth", type: "boolean", description: "Expand to full available width." }, + { name: "leading", type: "TextInputLeadingAccessory", description: "Leading accessory (icon, button, tags, or image)." }, + { name: "trailing", type: "TextInputTrailingAccessory", description: "Trailing accessory (icon or button)." }, + { name: "inputRef", type: "Ref", internal: true, description: "Ref to the underlying input element." }, +]; +function BasicDemo() { + const [value, setValue] = useState("Hello world"); + return ; +} + +function SizesDemo() { + const [sm, setSm] = useState("Small"); + const [md, setMd] = useState("Medium"); return ( -
- - - - - -
- - -
-
- - - - - - - - - - - - - - - - - - -
- - -
-
- - - - • value: string - Input value - - - • onChange?: (value: string) => void - Called on input change - - - • placeholder?: string - Placeholder text - - - • size?: "sm" | "md" - Input size (default: "md") - - - • clearable?: boolean - Show clear button - - - • showCharacterCount?: boolean - Display character count - - - • maxLength?: number - Maximum character limit - - - • error?: boolean - Show error state styling - - - • disabled?: boolean - Disable input - - - • readOnly?: boolean - Make input read-only - - +
+ +
); } + +function ClearableDemo() { + const [value, setValue] = useState("Clear me"); + return ; +} + +function CharCountDemo() { + const [value, setValue] = useState("Count chars"); + return ; +} + +function ErrorDemo() { + const [value, setValue] = useState("Invalid input"); + return ; +} + +export default function TextInputTab() { + return ( + , + code: "", + relevantProps: ["value", "onChange"], + }, + { + title: "Sizes", + description: "Available in sm and md (default) sizes.", + children: , + relevantProps: ["size"], + }, + { + title: "With Placeholder", + children: ( + { }} placeholder="Enter text here..." /> + ), + relevantProps: ["placeholder"], + }, + { + title: "Clearable", + description: "Shows a clear button when the input has a value.", + children: , + relevantProps: ["clearable"], + }, + { + title: "Character Count", + description: "Displays character count with a 50-character max.", + children: , + relevantProps: ["showCharacterCount", "maxLength"], + }, + { + title: "Error State", + description: "Red border indicating invalid input.", + children: , + relevantProps: ["error"], + }, + { + title: "States", + description: "Disabled and read-only states.", + children: ( +
+ + +
+ ), + relevantProps: ["disabled", "readOnly"], + }, + ]} + props={TEXTINPUT_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/components/tabs/TimestampTab.tsx b/src/equicordplugins/components.dev/components/tabs/TimestampTab.tsx index 6bbe18645..5804f7e15 100644 --- a/src/equicordplugins/components.dev/components/tabs/TimestampTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TimestampTab.tsx @@ -5,162 +5,154 @@ */ import { Paragraph, Timestamp, TimestampDisplayFormats, TooltipPositions } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; + +const TIMESTAMP_PROPS: PropDef[] = [ + { name: "timestamp", type: "Date | Moment", required: true, description: "The date or moment object to display." }, + { name: "timestampFormat", type: "TimestampDisplayFormat", description: 'Moment.js format string. One of: "LT", "LTS", "L", "LL", "LLL", "LLLL", "l", "ll", "lll", "llll".' }, + { name: "compact", type: "boolean", default: "false", description: "Use a shorter display style." }, + { name: "cozyAlt", type: "boolean", default: "false", description: "Use the cozy alternative layout." }, + { name: "isInline", type: "boolean", default: "false", description: "Render the timestamp inline." }, + { name: "isVisibleOnlyOnHover", type: "boolean", default: "false", description: "Hide the timestamp until the parent is hovered." }, + { name: "isEdited", type: "boolean", default: "false", description: 'Show an "(edited)" indicator after the timestamp.' }, + { name: "tooltipPosition", type: '"top" | "bottom" | "left" | "right"', description: "Position of the full-date tooltip." }, + { name: "id", type: "string", description: "HTML id attribute." }, + { name: "className", type: "string", description: "Additional CSS class." }, + { name: "children", type: "ReactNode", description: "Custom content to render instead of the formatted timestamp." }, +]; export default function TimestampTab() { const now = new Date(); return ( -
- - - All available timestamp format options using moment.js format strings. - -
- {TimestampDisplayFormats.map(({ format, description }) => ( -
- - - {format}: {description} - + + {TimestampDisplayFormats.map(({ format, description }) => ( +
+ + + {format}: {description} + +
+ ))}
- ))} -
-
- - - - Compact mode displays a shorter version of the timestamp. - -
-
- - - compact=false (default) - -
-
- - - compact=true - -
-
-
- - - - Different display modes for timestamps. - -
-
- - - isInline=false (default) - -
-
- - - isInline=true - -
-
- - - cozyAlt=true - -
-
-
- - - - Shows an "(edited)" indicator after the timestamp. - -
-
- - - isEdited=false (default) - -
-
- - - isEdited=true - -
-
-
- - - - Timestamp can be hidden until hovered. - -
-
- - Hover me: - - - isVisibleOnlyOnHover=true - -
-
-
- - - - The tooltip showing full date can be positioned differently. - -
- {TooltipPositions.map(position => ( -
- - - tooltipPosition="{position}" - + ), + code: '', + relevantProps: ["timestamp", "timestampFormat"], + }, + { + title: "Compact Mode", + description: "Compact mode shows a shorter version of the timestamp.", + children: ( +
+
+ + + compact=false (default) + +
+
+ + + compact=true + +
- ))} -
- - - - - • timestamp: Date | Moment - The timestamp to display (required) - - - • timestampFormat?: TimestampFormat - Format string (LT, LTS, L, LL, LLL, LLLL, l, ll, lll, llll) - - - • compact?: boolean - Use compact display mode - - - • cozyAlt?: boolean - Use cozy alternative display - - - • isInline?: boolean - Display inline - - - • isVisibleOnlyOnHover?: boolean - Only show on hover - - - • isEdited?: boolean - Show "(edited)" indicator - - - • tooltipPosition?: "top" | "bottom" | "left" | "right" - Tooltip position - - - • id?: string - Element ID - - - • className?: string - Additional CSS class - - - • children?: ReactNode - Custom content - - -
+ ), + relevantProps: ["compact"], + }, + { + title: "Display Variants", + description: "Inline and cozy alternative display modes.", + children: ( +
+
+ + + isInline=false (default) + +
+
+ + + isInline=true + +
+
+ + + cozyAlt=true + +
+
+ ), + relevantProps: ["isInline", "cozyAlt"], + }, + { + title: "Edited Indicator", + description: 'Shows an "(edited)" indicator after the timestamp.', + children: ( +
+
+ + + isEdited=false (default) + +
+
+ + + isEdited=true + +
+
+ ), + relevantProps: ["isEdited"], + }, + { + title: "Visibility on Hover", + description: "Timestamp can be hidden until the parent element is hovered.", + children: ( +
+
+ + Hover me: + + + isVisibleOnlyOnHover=true + +
+
+ ), + relevantProps: ["isVisibleOnlyOnHover"], + }, + { + title: "Tooltip Positions", + description: "The tooltip showing the full date can be positioned on any side.", + children: ( +
+ {TooltipPositions.map(position => ( +
+ + + tooltipPosition="{position}" + +
+ ))} +
+ ), + relevantProps: ["tooltipPosition"], + }, + ]} + props={TIMESTAMP_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx b/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx index 7c6974072..8c187fe3e 100644 --- a/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/ToastTab.tsx @@ -4,137 +4,150 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ManaButton, Paragraph, showToast, Toasts, ToastType, ToastTypeValue } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { ManaButton, Paragraph, showToast, Toasts, ToastType, type ToastTypeValue } from ".."; +import { DocPage, type PropGroup } from "../DocPage"; -const TOAST_TYPES = [ - { type: ToastType.MESSAGE, label: "MESSAGE", description: "Default message toast" }, - { type: ToastType.SUCCESS, label: "SUCCESS", description: "Green success toast" }, - { type: ToastType.FAILURE, label: "FAILURE", description: "Red failure/error toast" }, - { type: ToastType.CLIP, label: "CLIP", description: "Clip/copy toast" }, - { type: ToastType.LINK, label: "LINK", description: "Link copied toast" }, - { type: ToastType.FORWARD, label: "FORWARD", description: "Forward action toast" }, - { type: ToastType.INVITE, label: "INVITE", description: "Invite action toast" }, - { type: ToastType.BOOKMARK, label: "BOOKMARK", description: "Bookmark action toast" }, - { type: ToastType.CLOCK, label: "CLOCK", description: "Clock/timer toast" }, - { type: ToastType.AI, label: "AI", description: "AI action toast" }, -] as const; +const TOAST_TYPES: { type: ToastTypeValue; label: string; description: string; }[] = [ + { type: ToastType.MESSAGE, label: "MESSAGE", description: "Default message toast." }, + { type: ToastType.SUCCESS, label: "SUCCESS", description: "Green success toast." }, + { type: ToastType.FAILURE, label: "FAILURE", description: "Red failure/error toast." }, + { type: ToastType.CLIP, label: "CLIP", description: "Clip/copy toast." }, + { type: ToastType.LINK, label: "LINK", description: "Link copied toast." }, + { type: ToastType.FORWARD, label: "FORWARD", description: "Forward action toast." }, + { type: ToastType.BOOKMARK, label: "BOOKMARK", description: "Bookmark action toast." }, + { type: ToastType.CLOCK, label: "CLOCK", description: "Clock/timer toast." }, +]; + +const PROP_GROUPS: PropGroup[] = [ + { + title: "showToast(message, type)", + props: [ + { name: "message", type: "string", required: true, description: "Toast message text." }, + { name: "type", type: "ToastTypeValue", default: '"message"', description: "Toast type from ToastType." }, + ], + }, + { + title: "Toasts.create(message, type, options)", + props: [ + { name: "message", type: "string", required: true, description: "Toast message text." }, + { name: "type", type: "ToastTypeValue", required: true, description: "Toast type from ToastType." }, + { name: "options.duration", type: "number", description: "Display time in milliseconds." }, + { name: "options.position", type: "0 | 1", description: "Toast position. 0 = TOP, 1 = BOTTOM." }, + { name: "options.component", type: "ReactNode", description: "Custom React content for CUSTOM type toasts." }, + ], + }, + { + title: "ToastType", + props: [ + { name: "MESSAGE", type: '"message"', description: "Default message style." }, + { name: "SUCCESS", type: '"success"', description: "Green checkmark icon." }, + { name: "FAILURE", type: '"failure"', description: "Red X icon." }, + { name: "CUSTOM", type: '"custom"', description: "Custom component content." }, + { name: "CLIP", type: '"clip"', description: "Clip/scissors icon." }, + { name: "LINK", type: '"link"', description: "Link icon." }, + { name: "FORWARD", type: '"forward"', description: "Forward arrow icon." }, + { name: "BOOKMARK", type: '"bookmark"', description: "Bookmark icon." }, + { name: "CLOCK", type: '"clock"', description: "Clock icon." }, + ], + }, +]; export default function ToastTab() { - const showSimpleToast = (type: ToastTypeValue, label: string) => { - showToast(`This is a ${label} toast!`, type); - }; - - const showAdvancedToast = () => { - const toast = Toasts.create("Advanced toast with options", ToastType.SUCCESS, { - duration: 5000, - position: Toasts.Position.BOTTOM, - }); - Toasts.show(toast); - }; - - const showCustomToast = () => { - const toast = Toasts.create("Custom component toast", ToastType.CUSTOM, { - component: Custom JSX content!, - }); - Toasts.show(toast); - }; - return ( -
- - - Click buttons to show different toast types. Toasts appear at screen edges. - -
- {TOAST_TYPES.map(({ type, label, description }) => ( -
+ + {TOAST_TYPES.map(({ type, label, description }) => ( +
+ showToast(`This is a ${label} toast!`, type)} + /> + + {description} + +
+ ))} +
+ ), + code: 'showToast("Copied to clipboard!", ToastType.SUCCESS);', + }, + { + title: "Simple Usage", + description: "showToast is the quickest way to show a toast. It creates and shows a toast in one call.", + children: ( + showToast("Hello from showToast!", ToastType.SUCCESS)} + /> + ), + code: 'showToast("Hello from showToast!", ToastType.SUCCESS);', + }, + { + title: "Advanced Usage", + description: "Use Toasts.create to build a toast with custom duration, position, or component content, then Toasts.show to display it.", + children: ( +
showSimpleToast(type, label)} + text="5s Duration Toast" + onClick={() => { + const toast = Toasts.create("This lasts 5 seconds", ToastType.SUCCESS, { duration: 5000 }); + Toasts.show(toast); + }} + /> + { + const toast = Toasts.create("Bottom toast", ToastType.MESSAGE, { position: Toasts.Position.BOTTOM }); + Toasts.show(toast); + }} + /> + { + const toast = Toasts.create("Custom", ToastType.CUSTOM, { + component: Custom JSX content!, + }); + Toasts.show(toast); + }} /> - - {description} -
- ))} -
-
- - - - showToast(message, type) - Quick way to show a toast. - - showToast("Hello from showToast!", ToastType.SUCCESS)} - /> - - - - - Toasts.show() with full options object. - -
- - -
-
- - - - Toasts.pop() removes the most recent toast. - - Toasts.pop()} - /> - - - - • MESSAGE - Default message style - • SUCCESS - Green checkmark icon - • FAILURE - Red X icon - • CUSTOM - Custom component content - • CLIP - Clip/scissors icon - • LINK - Link icon - • FORWARD - Forward arrow icon - • INVITE - Invite icon - • BOOKMARK - Bookmark icon - • CLOCK - Clock icon - • AI - AI icon - - - - • TOP (0) - Toast appears at top - • BOTTOM (1) - Toast appears at bottom - - - - • message - Toast message text - • type - Toast type from ToastType - • id - Unique ID (use Toasts.genId()) - • options.duration - Display time in ms - • options.position - ToastPosition value - • options.component - Custom React component - - - - - {"import { showToast, Toasts, ToastType, ToastPosition } from \"../components\";"} - - -
+ ), + code: `const toast = Toasts.create("Done!", ToastType.SUCCESS, { + duration: 5000, + position: Toasts.Position.BOTTOM, +}); +Toasts.show(toast);`, + }, + { + title: "Pop Toast", + description: "Toasts.pop removes the most recent toast from the stack.", + children: ( + Toasts.pop()} + /> + ), + code: "Toasts.pop();", + }, + ]} + props={PROP_GROUPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx b/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx index b25c4aa56..42a8f50df 100644 --- a/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TooltipTab.tsx @@ -5,90 +5,153 @@ */ import { ManaButton, ManaTooltip, Paragraph, TooltipColors, TooltipPositions } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropDef } from "../DocPage"; + +const TOOLTIP_PROPS: PropDef[] = [ + { name: "text", type: "string | (() => ReactNode)", required: true, description: "Tooltip content. Can be a string or a render function for rich content." }, + { name: "children", type: "(props) => ReactNode", required: true, description: "Render function that receives props to spread on the trigger element." }, + { name: "position", type: '"top" | "bottom" | "left" | "right"', default: '"top"', description: "Which side of the trigger the tooltip appears on." }, + { name: "align", type: '"start" | "center" | "end"', default: '"center"', description: "Alignment along the tooltip edge." }, + { name: "color", type: '"primary" | "grey" | "brand" | "green" | "red"', default: '"primary"', description: "Tooltip background color." }, + { name: "spacing", type: "number", default: "8", description: "Gap in pixels between the trigger and tooltip." }, + { name: "delay", type: "number", description: "Delay in milliseconds before the tooltip appears." }, + { name: "hideOnClick", type: "boolean", default: "true", description: "Hide the tooltip when the trigger is clicked." }, + { name: "forceOpen", type: "boolean", description: "Force the tooltip to stay open." }, + { name: "shouldShow", type: "boolean", description: "Externally control whether the tooltip is shown." }, + { name: "allowOverflow", type: "boolean", description: "Allow the tooltip to overflow its container." }, + { name: "overflowOnly", type: "boolean", description: "Only show the tooltip when the trigger text is overflowing." }, + { name: "tooltipClassName", type: "string", description: "CSS class for the tooltip container." }, + { name: "tooltipStyle", type: "CSSProperties", description: "Inline styles for the tooltip container." }, + { name: "tooltipContentClassName", type: "string", description: "CSS class for the tooltip content." }, + { name: "tooltipPointerClassName", type: "string", description: "CSS class for the tooltip arrow pointer." }, + { name: "positionKeyStemOverride", type: "string", internal: true, description: "Override the position key stem used for animation keying." }, + { name: "clickableOnMobile", type: "boolean", description: "Make the tooltip trigger clickable on mobile devices." }, + { name: "disableTooltipPointerEvents", type: "boolean", description: "Disable pointer events on the tooltip overlay." }, + { name: "targetElementRef", type: "RefObject", description: "Ref to an external element to position the tooltip against." }, + { name: "onTooltipShow", type: "() => void", description: "Callback when the tooltip becomes visible." }, + { name: "onTooltipHide", type: "() => void", description: "Callback when the tooltip is hidden." }, + { name: "onAnimationRest", type: "() => void", description: "Callback when the tooltip show/hide animation completes." }, + { name: "aria-label", type: "string | false", description: "Accessibility label. Set to false to disable." }, +]; export default function TooltipTab() { return ( -
- -
- {TooltipPositions.map(position => ( - - {props => ( - - )} - - ))} -
-
- - -
- {TooltipColors.map(color => ( - - {props => ( - - )} - - ))} -
-
- - -
- - {props => ( - - )} - - - {props => ( - - )} - - - {props => ( - - )} - -
-
- - -
- - {props => ( - - )} - - - {props => ( - - )} - -
-
- - - - • text: string - Tooltip content - - - • children: (props) => ReactNode - Render function for trigger element - - - • position?: "top" | "bottom" | "left" | "right" - Tooltip position - - - • color?: "primary" | "black" | "grey" | "brand" | "green" | "yellow" | "red" - - - • delay?: number - Show delay in milliseconds - - - • hideOnClick?: boolean - Hide tooltip when trigger is clicked - - -
+ + {TooltipPositions.map(position => ( + + {props => ( + + )} + + ))} +
+ ), + code: ` + {props => } +`, + relevantProps: ["position"], + }, + { + title: "Colors", + description: "Five background color variants.", + children: ( +
+ {TooltipColors.map(color => ( + + {props => ( + + )} + + ))} +
+ ), + code: '\n {props => }\n', + relevantProps: ["color"], + }, + { + title: "Delay", + description: "Configure how long to wait before showing the tooltip.", + children: ( +
+ + {props => ( + + )} + + + {props => ( + + )} + + + {props => ( + + )} + +
+ ), + relevantProps: ["delay"], + }, + { + title: "Hide on Click", + description: "By default tooltips hide when the trigger is clicked. Set hideOnClick=false to keep them visible.", + children: ( +
+ + {props => ( + + )} + + + {props => ( + + )} + +
+ ), + relevantProps: ["hideOnClick"], + }, + { + title: "Rich Content", + description: "The text prop accepts a function that returns JSX for rich tooltip content.", + children: ( +
+ ( +
+ Rich Tooltip + With multiple lines of content. +
+ )}> + {props => ( + + )} +
+
+ ), + code: ` ( +
+ Title +

Description text

+
+)}> + {props => } +
`, + relevantProps: ["text"], + }, + ]} + props={TOOLTIP_PROPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx b/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx index 675fd894e..2a7acead9 100644 --- a/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/TypographyTab.tsx @@ -5,117 +5,140 @@ */ import { CodeColors, FontSizeMap, Paragraph, TextColors, TextSizes, TextWeights } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { DocPage, type PropGroup } from "../DocPage"; + +const PROP_GROUPS: PropGroup[] = [ + { + title: "Text Colors", + props: TextColors.map(color => ({ + name: color, + type: "CSS variable", + description: `var(--${color})`, + })), + }, + { + title: "Text Sizes", + props: TextSizes.map(({ name, value, pixels }) => ({ + name, + type: "font-size", + description: `${value} (${pixels}).`, + })), + }, + { + title: "Text Weights", + props: TextWeights.map(({ name, value }) => ({ + name, + type: "font-weight", + description: `font-weight: ${value}.`, + })), + }, + { + title: "Code Colors", + props: CodeColors.map(color => ({ + name: color, + type: "CSS variable", + description: `var(--${color})`, + })), + }, +]; export default function TypographyTab() { return ( -
- -
- {TextColors.map(color => ( -
- - {color} - - - var(--{color}) - + + {TextColors.map(color => ( +
+ + {color} + + + var(--{color}) + +
+ ))}
- ))} -
-
- - -
- {TextSizes.map(({ name, value, pixels }) => ( -
- - {name} - - - {value} ({pixels}) - + ), + code: 'Muted text', + }, + { + title: "Text Sizes", + description: "Font sizes from xxs (10px) to xxl (32px).", + children: ( +
+ {TextSizes.map(({ name, value, pixels }) => ( +
+ + {name} + + + {value} ({pixels}) + +
+ ))}
- ))} -
- - - -
- {TextWeights.map(({ name, value }) => ( -
- - {name} - - - font-weight: {value} - + ), + }, + { + title: "Text Weights", + description: "Font weights from thin (100) to extrabold (800).", + children: ( +
+ {TextWeights.map(({ name, value }) => ( +
+ + {name} + + + font-weight: {value} + +
+ ))}
- ))} -
- - - -
- - {"import { BaseText } from \"@components/BaseText\";"} - - - {"Text"} - -
-
- - -
- {CodeColors.map(color => ( -
- - {color} - - - var(--{color}) - + ), + }, + { + title: "Code Colors", + description: "Syntax highlighting colors for code blocks.", + children: ( +
+ {CodeColors.map(color => ( +
+ + {color} + + + var(--{color}) + +
+ ))}
- ))} -
- - - - - BaseText - from @components/BaseText - - - • size?: "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl" - - - • weight?: "thin" | "extralight" | "light" | "normal" | "medium" | "semibold" | "bold" | "extrabold" - - - • color?: string - CSS variable name (e.g., "text-muted") - - - • tag?: "span" | "p" | "div" | "h1" - etc. - - - Heading - from @components/Heading - - - • tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" (default: h5) - - - Paragraph - from @components/Paragraph - - - • color?: string - CSS variable name - - - Span - from @components/Span - - - • Inline text with default styling - - -
+ ), + }, + { + title: "Usage with Components", + description: "These values are used with Paragraph, DiscordHeading, and DiscordText. See those tabs for component-specific documentation.", + children: ( +
+ text-default: Primary readable text. + text-muted: Secondary, less prominent text. + text-link: Clickable link text. + text-brand: Brand-colored text. +
+ ), + code: `Muted text +Brand text +Bold heading`, + }, + ]} + props={PROP_GROUPS} + /> ); } diff --git a/src/equicordplugins/components.dev/components/tabs/UserSummaryItemTab.tsx b/src/equicordplugins/components.dev/components/tabs/UserSummaryItemTab.tsx index 0b6cd42dc..597ec004d 100644 --- a/src/equicordplugins/components.dev/components/tabs/UserSummaryItemTab.tsx +++ b/src/equicordplugins/components.dev/components/tabs/UserSummaryItemTab.tsx @@ -4,89 +4,146 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Paragraph, UserStore, UserSummaryItem } from ".."; -import { SectionWrapper } from "../SectionWrapper"; +import { Paragraph, UserStore, UserSummaryItem, useState } from ".."; +import { DocPage, type PropDef } from "../DocPage"; -export default function UserSummaryItemTab() { +const USER_SUMMARY_PROPS: PropDef[] = [ + { name: "users", type: "(User | null)[]", required: true, description: "Array of User objects to display as avatars. Null entries render as empty slots." }, + { name: "max", type: "number", default: "10", description: "Maximum number of avatars to show before the overflow indicator." }, + { name: "count", type: "number", description: 'Override the count used in the "+N" overflow display.' }, + { name: "size", type: "16 | 24 | 32 | 56", default: "24", description: "Avatar size in pixels." }, + { name: "guildId", type: "string", description: "Guild context for server-specific avatars." }, + { name: "showUserPopout", type: "boolean", default: "false", description: "Show user popout when an avatar is clicked." }, + { name: "showDefaultAvatarsForNullUsers", type: "boolean", description: "Render default avatars for null entries in the users array." }, + { name: "renderUser", type: "(user, isLast, index) => ReactNode", description: "Custom render function for each user avatar." }, + { name: "renderMoreUsers", type: "(text, count) => ReactNode", description: 'Custom render function for the "+N" overflow indicator.' }, + { name: "renderIcon", type: "boolean", description: "Whether to render an icon." }, + { name: "renderLeadingIcon", type: "(className) => ReactNode", description: "Render a leading icon before the user avatars." }, + { name: "dimEmptyUsers", type: "boolean", description: "Dim null user slots." }, + { name: "hideMoreUsers", type: "boolean", default: "false", description: 'Hide the "+N" overflow count.' }, + { name: "useFallbackUserForPopout", type: "boolean", description: "Use fallback user data for popout when user data is unavailable." }, + { name: "extraDetail", type: "ReactNode", description: "Additional content rendered after the avatars." }, + { name: "className", type: "string", description: "CSS class for the container." }, +]; + +function MaxUsersDemo() { const currentUser = UserStore.getCurrentUser(); - const users = currentUser ? [currentUser, currentUser, currentUser, currentUser, currentUser] : []; + const users = currentUser ? Array(5).fill(currentUser) : []; return ( -
- - - Displays a row of user avatars with overflow indicator. - -
- +
+ {[2, 3, 5].map(max => ( +
+ max={max}: +
- - - - - Available sizes: 16, 24, 32, 56. - -
-
- 16: - -
-
- 24: - -
-
- 32: - -
-
- 56: - -
-
-
- - - - Control how many avatars to show before overflow. - -
-
- max=2: - -
-
- max=3: - -
-
- max=5: - -
-
-
- - - - Enable user popout on avatar click. - -
- -
-
- - - • users - Array of User objects (or null for empty slots) - • max - Maximum avatars to show (default 10) - • count - Override count for "+N" display - • size - 16, 24, 32, or 56 (default 24) - • guildId - Guild context for avatars - • showUserPopout - Enable user popout on click - • showDefaultAvatarsForNullUsers - Show default avatar for null - • renderUser - Custom render function for each user - • renderMoreUsers - Custom render for overflow indicator - • hideMoreUsers - Hide the "+N" overflow count - + ))}
); } + +function SizesDemo() { + const currentUser = UserStore.getCurrentUser(); + const users = currentUser ? Array(5).fill(currentUser) : []; + + return ( +
+ {([16, 24, 32, 56] as const).map(size => ( +
+ {size}: + +
+ ))} +
+ ); +} + +function OverrideCountDemo() { + const currentUser = UserStore.getCurrentUser(); + const users = currentUser ? Array(3).fill(currentUser) : []; + const [count, setCount] = useState(42); + + return ( +
+
+ Default count: + +
+
+ count={count}: + +
+
+ ); +} + +export default function UserSummaryItemTab() { + const currentUser = UserStore.getCurrentUser(); + const users = currentUser ? Array(5).fill(currentUser) : []; + + return ( + + ), + code: `const users = [user1, user2, user3, user4, user5]; +`, + relevantProps: ["users", "max"], + }, + { + title: "Sizes", + description: "Four avatar sizes: 16, 24, 32, and 56 pixels.", + children: , + code: "", + relevantProps: ["size"], + }, + { + title: "Max Users", + description: "Control how many avatars are visible before the overflow count.", + children: , + relevantProps: ["max"], + }, + { + title: "Override Count", + description: 'The count prop overrides the number shown in the "+N" indicator, useful when the total is different from users.length.', + children: , + relevantProps: ["count"], + }, + { + title: "User Popout", + description: "Enable user popout on avatar click with showUserPopout.", + children: ( + + ), + code: "", + relevantProps: ["showUserPopout"], + }, + { + title: "Hide Overflow", + description: 'Hide the "+N" overflow indicator entirely.', + children: ( +
+
+ hideMoreUsers=false: + +
+
+ hideMoreUsers=true: + +
+
+ ), + relevantProps: ["hideMoreUsers"], + }, + ]} + props={USER_SUMMARY_PROPS} + /> + ); +} diff --git a/src/equicordplugins/components.dev/constants.ts b/src/equicordplugins/components.dev/constants.ts index 6577cfc0a..faad5dda5 100644 --- a/src/equicordplugins/components.dev/constants.ts +++ b/src/equicordplugins/components.dev/constants.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export const ManaButtonVariants = ["primary", "secondary", "critical-primary", "critical-secondary", "overlay-primary", "overlay-secondary", "expressive"] as const; +export const ManaButtonVariants = ["primary", "secondary", "critical-primary", "critical-secondary", "active", "overlay-primary", "overlay-secondary", "expressive"] as const; export type ManaButtonVariant = typeof ManaButtonVariants[number]; export const ManaButtonSizes = ["xs", "sm", "md"] as const; @@ -34,7 +34,7 @@ export type TooltipPosition = typeof TooltipPositions[number]; export const TooltipAligns = ["start", "center", "end"] as const; export type TooltipAlign = typeof TooltipAligns[number]; -export const TooltipColors = ["primary", "black", "grey", "brand", "green", "yellow", "red"] as const; +export const TooltipColors = ["primary", "grey", "brand", "green", "red"] as const; export type TooltipColor = typeof TooltipColors[number]; export const TextInputSizes = ["sm", "md"] as const; @@ -174,10 +174,8 @@ export const ToastType = { CLIP: "clip", LINK: "link", FORWARD: "forward", - INVITE: "invite", BOOKMARK: "bookmark", CLOCK: "clock", - AI: "ai", } as const; export const ToastPosition = { diff --git a/src/equicordplugins/components.dev/styles.css b/src/equicordplugins/components.dev/styles.css index bd5a919bc..d0033e15f 100644 --- a/src/equicordplugins/components.dev/styles.css +++ b/src/equicordplugins/components.dev/styles.css @@ -1,3 +1,5 @@ +/* ── Search & Tab Navigation ── */ + .vc-compfinder-search { margin-bottom: 40px; } @@ -22,12 +24,18 @@ padding-top: 16px; } +/* ── Shared Utility Layouts ── */ + .vc-compfinder-section { display: flex; flex-direction: column; gap: 8px; } +.vc-compfinder-section-block > :nth-child(n+3) { + margin-top: 8px; +} + .vc-compfinder-grid { display: flex; flex-wrap: wrap; @@ -46,23 +54,25 @@ align-items: center; gap: 8px; padding: 8px 12px; - background: var(--background-secondary); + background: rgb(0 0 0 / 15%); border-radius: 4px; } .vc-compfinder-demo-button { padding: 8px 16px; - background: var(--background-secondary); - border: 1px solid var(--border-subtle); + background: rgb(0 0 0 / 15%); + border: 1px solid rgb(255 255 255 / 6%); border-radius: 4px; color: var(--text-normal); cursor: pointer; } .vc-compfinder-demo-button:hover { - background: var(--background-secondary-alt); + background: rgb(0 0 0 / 25%); } +/* ── Modal Demo Helpers ── */ + .vc-compfinder-modal-header { display: flex; align-items: flex-start; @@ -96,3 +106,209 @@ color: var(--white-500); background-color: var(--button-danger-background); } + +/* ── DocPage Layout ── */ + +.vc-compfinder-doc-page { + display: flex; + flex-direction: column; + gap: 16px; +} + +.vc-compfinder-doc-page > .vc-divider { + margin: 8px 0; +} + +.vc-compfinder-doc-title { + margin-bottom: -8px; +} + +.vc-compfinder-doc-section { + display: flex; + flex-direction: column; + gap: 12px; +} + +.vc-compfinder-doc-section [id] { + scroll-margin-top: 16px; +} + +.vc-compfinder-doc-demo { + padding: 16px; +} + +.vc-compfinder-doc-relevant-props { + margin-top: 8px; +} + +.vc-compfinder-doc-prop-group { + margin-bottom: 16px; +} + +.vc-compfinder-doc-example { + display: flex; + flex-direction: column; + gap: 4px; +} + +.vc-compfinder-doc-example + .vc-compfinder-doc-example { + margin-top: 12px; +} + +/* ── Import Block ── */ + +.vc-compfinder-import-block { + display: flex; + flex-direction: column; + gap: 6px; +} + +.vc-compfinder-import-label { + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* ── Table of Contents ── */ + +.vc-compfinder-toc { + display: flex; + flex-direction: column; + gap: 6px; +} + +.vc-compfinder-toc-label { + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.vc-compfinder-toc-links { + display: flex; + flex-wrap: wrap; + gap: 2px 12px; +} + +.vc-compfinder-toc-link { + color: var(--text-link); + font-size: 0.8125rem; + text-decoration: none; + cursor: pointer; + line-height: 1.6; +} + +.vc-compfinder-toc-link:hover { + text-decoration: underline; +} + +/* ── Props Table ── */ + +.vc-compfinder-table-wrapper { + overflow-x: auto; +} + +.vc-compfinder-table { + width: 100%; + border-collapse: collapse; + font-size: 0.75rem; +} + +.vc-compfinder-table th { + text-align: left; + padding: 8px 12px; + font-size: 0.625rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + background: rgb(0 0 0 / 20%); + border-bottom: 1px solid rgb(255 255 255 / 6%); + vertical-align: middle; +} + +.vc-compfinder-table td { + padding: 8px 12px; + color: var(--text-normal); + vertical-align: middle; +} + +.vc-compfinder-table tbody tr:nth-child(odd) { + background: rgb(0 0 0 / 8%); +} + +.vc-compfinder-table tbody tr:nth-child(even) { + background: rgb(0 0 0 / 15%); +} + +.vc-compfinder-table code.inline { + padding: 1px 4px !important; + font-size: 0.625rem !important; + line-height: 1.3; + white-space: nowrap; +} + +.vc-compfinder-doc-page code.inline { + padding: 2px 5px; + font-size: 0.75rem; +} + +.vc-compfinder-doc-page pre code { + font-size: 0.75rem !important; +} + +.vc-compfinder-table td:nth-child(2) { + white-space: nowrap; +} + +.vc-compfinder-table-field { + font-weight: 600; + font-size: 0.75rem; + color: var(--text-normal); + font-family: var(--font-code); + white-space: nowrap; +} + +.vc-compfinder-table-row-required td:first-child { + border-left: 3px solid var(--status-positive); + padding-left: 9px; +} + +.vc-compfinder-table-row-internal td:first-child { + border-left: 3px solid var(--status-warning); + padding-left: 9px; +} + +.vc-compfinder-table-legend { + display: flex; + gap: 16px; + margin-top: 8px; +} + +.vc-compfinder-table-legend-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.6875rem; + color: var(--text-muted); +} + +.vc-compfinder-table-legend-bar { + width: 3px; + height: 12px; + border-radius: 1px; +} + +.vc-compfinder-table-legend-required { + background: var(--status-positive); +} + +.vc-compfinder-table-legend-internal { + background: var(--status-warning); +} + +.vc-compfinder-table-dash { + color: var(--text-faint); +} + +.vc-compfinder-table-desc { + color: var(--text-muted); + line-height: 1.4; +} diff --git a/src/equicordplugins/components.dev/types/avatar.d.ts b/src/equicordplugins/components.dev/types/avatar.d.ts index e4ff7e5e7..50fdc530a 100644 --- a/src/equicordplugins/components.dev/types/avatar.d.ts +++ b/src/equicordplugins/components.dev/types/avatar.d.ts @@ -22,6 +22,7 @@ export interface AvatarProps { statusColor?: string; statusBackdropColor?: string; isMobile?: boolean; + isVR?: boolean; isTyping?: boolean; isSpeaking?: boolean; isLatched?: boolean; @@ -35,9 +36,17 @@ export interface AvatarProps { avatarTooltipAsset?: React.ReactNode; avatarTooltipText?: string; avatarTooltipTitle?: string; + avatarDecoration?: string; "aria-hidden"?: boolean; "aria-label"?: string; imageClassName?: string; className?: string; + onClick?: React.MouseEventHandler; + onMouseDown?: React.MouseEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onContextMenu?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + tabIndex?: number; ref?: React.Ref; } diff --git a/src/equicordplugins/components.dev/types/badge.d.ts b/src/equicordplugins/components.dev/types/badge.d.ts index 7e23861d0..3f58a57d2 100644 --- a/src/equicordplugins/components.dev/types/badge.d.ts +++ b/src/equicordplugins/components.dev/types/badge.d.ts @@ -11,7 +11,7 @@ export interface BadgeShapesType { SQUARE: string; } -export interface NumberBadgeProps { +export interface NumberBadgeProps extends Omit, "children"> { count: number; color?: string; disableColor?: boolean; @@ -21,7 +21,7 @@ export interface NumberBadgeProps { renderBadgeCount?: (count: number) => string; } -export interface TextBadgeProps { +export interface TextBadgeProps extends Omit, "children"> { text: string; color?: string; disableColor?: boolean; @@ -39,10 +39,19 @@ export interface IconBadgeProps { style?: React.CSSProperties; } -export interface CircleBadgeProps { +export interface CircleBadgeProps extends Omit, "children"> { color?: string; disableColor?: boolean; shape?: string; className?: string; style?: React.CSSProperties; } + +export interface PremiumBadgeProps extends Omit, "children"> { + text: string; + className?: string; + color?: string; + disableColor?: boolean; + shape?: string; + style?: React.CSSProperties; +} diff --git a/src/equicordplugins/components.dev/types/calendar.d.ts b/src/equicordplugins/components.dev/types/calendar.d.ts index 3d0300d7e..1de325466 100644 --- a/src/equicordplugins/components.dev/types/calendar.d.ts +++ b/src/equicordplugins/components.dev/types/calendar.d.ts @@ -27,6 +27,14 @@ export interface ManaDatePickerProps { disabled?: boolean; required?: boolean; label?: string; + hideLabel?: boolean; + badge?: React.ReactNode; + icon?: React.ReactNode; description?: string; + helperText?: string; errorMessage?: string; + successMessage?: string; + id?: string; + layout?: string; + layoutConfig?: Record; } diff --git a/src/equicordplugins/components.dev/types/checkbox.d.ts b/src/equicordplugins/components.dev/types/checkbox.d.ts index 065ff1803..5b891e55a 100644 --- a/src/equicordplugins/components.dev/types/checkbox.d.ts +++ b/src/equicordplugins/components.dev/types/checkbox.d.ts @@ -35,4 +35,16 @@ export interface ManaCheckboxGroupProps { selectedValues: (string | number)[]; onChange?: (values: (string | number)[]) => void; disabled?: boolean; + label?: string; + hideLabel?: boolean; + badge?: React.ReactNode; + icon?: React.ReactNode; + required?: boolean; + description?: string; + helperText?: string; + errorMessage?: string; + successMessage?: string; + id?: string; + layout?: string; + layoutConfig?: Record; } diff --git a/src/equicordplugins/components.dev/types/input.d.ts b/src/equicordplugins/components.dev/types/input.d.ts index 2fbccadad..f438e1ebf 100644 --- a/src/equicordplugins/components.dev/types/input.d.ts +++ b/src/equicordplugins/components.dev/types/input.d.ts @@ -47,6 +47,18 @@ export interface ManaTextInputProps { defaultDirty?: boolean; focusProps?: Record; inputRef?: React.Ref; + label?: string; + hideLabel?: boolean; + badge?: React.ReactNode; + icon?: React.ReactNode; + required?: boolean; + description?: string; + helperText?: string; + errorMessage?: string; + successMessage?: string; + id?: string; + layout?: string; + layoutConfig?: Record; "aria-label"?: string; "aria-labelledby"?: string; "aria-describedby"?: string; @@ -74,6 +86,18 @@ export interface ManaTextAreaProps { fullWidth?: boolean; focusProps?: Record; inputRef?: React.Ref; + label?: string; + hideLabel?: boolean; + badge?: React.ReactNode; + icon?: React.ReactNode; + required?: boolean; + description?: string; + helperText?: string; + errorMessage?: string; + successMessage?: string; + id?: string; + layout?: string; + layoutConfig?: Record; "aria-label"?: string; "aria-labelledby"?: string; "aria-describedby"?: string; @@ -92,6 +116,6 @@ export interface SearchBarProps { onFocus?: () => void; autoComplete?: string; inputProps?: Record; - className?: string; + ref?: React.Ref; "aria-label"?: string; } diff --git a/src/equicordplugins/components.dev/types/menu.d.ts b/src/equicordplugins/components.dev/types/menu.d.ts index 035e6ad76..8f0e62e1a 100644 --- a/src/equicordplugins/components.dev/types/menu.d.ts +++ b/src/equicordplugins/components.dev/types/menu.d.ts @@ -36,12 +36,19 @@ export interface MenuItemProps { iconLeft?: ComponentType; iconLeftSize?: MenuIconSize; iconProps?: MenuItemIconProps; + leadingAccessory?: ReactNode; + trailingIndicator?: ReactNode; hint?: ReactNode | ((props: MenuItemProps) => ReactNode); + shortcut?: ReactNode; subtext?: ReactNode; subtextLineClamp?: number; hasSubmenu?: boolean; + loading?: boolean; + badge?: ReactNode; disabled?: boolean; action?: (e: MouseEvent) => void; + onClose?: () => void; + onFocus?: () => void; dontCloseOnAction?: boolean; dontCloseOnActionIfHoldingShiftKey?: boolean; className?: string; @@ -64,6 +71,9 @@ export interface MenuCheckboxItemProps { action?: (e: MouseEvent) => void; disabled?: boolean; subtext?: ReactNode; + subtextLineClamp?: number; + leftIcon?: ComponentType; + leadingAccessory?: ReactNode; className?: string; focusedClassName?: string; } @@ -77,6 +87,9 @@ export interface MenuRadioItemProps { action?: (e: MouseEvent) => void; disabled?: boolean; subtext?: ReactNode; + subtextLineClamp?: number; + leftIcon?: ComponentType; + leadingAccessory?: ReactNode; } export interface MenuSwitchItemProps { @@ -86,6 +99,8 @@ export interface MenuSwitchItemProps { checked: boolean; action: (e: MouseEvent) => void; disabled?: boolean; + subtext?: ReactNode; + subtextLineClamp?: number; className?: string; } @@ -150,6 +165,7 @@ export interface MenuSliderControlProps { minValue?: number; maxValue?: number; onChange: (value: number) => void; + onInteraction?: (type?: number) => void; renderValue?: (value: number) => string; "aria-label"?: string; } @@ -157,7 +173,9 @@ export interface MenuSliderControlProps { export interface MenuSearchControlProps { query: string; onChange: (query: string) => void; + disabled?: boolean; placeholder?: string; + onInteraction?: (type?: number) => void; "aria-label"?: string; } diff --git a/src/equicordplugins/components.dev/types/modal.d.ts b/src/equicordplugins/components.dev/types/modal.d.ts index 6568064c0..bcf51f6f3 100644 --- a/src/equicordplugins/components.dev/types/modal.d.ts +++ b/src/equicordplugins/components.dev/types/modal.d.ts @@ -21,11 +21,20 @@ export interface ModalNotice { type: "info" | "warn" | "critical" | "positive"; } +export type ModalAnimationVariant = "default" | "fade" | "slide-up"; + +export type ModalMaxHeight = "default" | "full"; + export interface ModalProps { title: string; subtitle?: string; size?: NativeModalSize; transitionState?: number; + animationVariant?: ModalAnimationVariant; + paddingSize?: "sm" | "lg"; + fullScreenOnMobile?: boolean; + maxHeight?: ModalMaxHeight; + dismissable?: boolean; children?: React.ReactNode; actions?: ModalAction[]; actionBarInput?: React.ReactNode; @@ -35,7 +44,11 @@ export interface ModalProps { listProps?: Record; notice?: ModalNotice; onClose: () => void; + onScroll?: (e: React.UIEvent) => void; + scrollerRef?: React.Ref; + returnRef?: React.Ref; role?: string; + "aria-label"?: string; className?: string; } @@ -54,13 +67,25 @@ export interface ConfirmModalProps extends Omit void; } diff --git a/src/equicordplugins/components.dev/types/progress.d.ts b/src/equicordplugins/components.dev/types/progress.d.ts index cf9eaa5c6..89cee00cc 100644 --- a/src/equicordplugins/components.dev/types/progress.d.ts +++ b/src/equicordplugins/components.dev/types/progress.d.ts @@ -12,14 +12,18 @@ export interface ProgressBarOverride { gradientEnd?: string; } +export type ProgressBarWeight = "light" | "medium"; + export interface ProgressBarProps { progress: number; minimum?: number; maximum?: number; + weight?: ProgressBarWeight; variant?: ProgressBarVariant; override?: { default?: ProgressBarOverride; [key: string]: ProgressBarOverride | undefined; }; + glowing?: boolean; labelledBy?: string; } diff --git a/src/equicordplugins/components.dev/types/radio.d.ts b/src/equicordplugins/components.dev/types/radio.d.ts index 26f14c030..d4f4133c6 100644 --- a/src/equicordplugins/components.dev/types/radio.d.ts +++ b/src/equicordplugins/components.dev/types/radio.d.ts @@ -7,18 +7,35 @@ export interface RadioOption { value: string | number; name: string; - desc?: string; + description?: string; disabled?: boolean; - leadingIcon?: React.ComponentType<{ className?: string; size?: string; color?: string; }>; + icon?: React.ComponentType<{ className?: string; size?: string; color?: string; }>; + color?: string; + radioItemIconClassName?: string; + radioBarClassName?: string; } +export type RadioPosition = "left" | "right"; + export interface ManaBaseRadioGroupProps { options: RadioOption[]; - value?: string | number; + value?: string | number | null; onChange?: (value: string | number) => void; disabled?: boolean; + size?: string; + orientation?: "vertical" | "horizontal"; + radioPosition?: RadioPosition; + withTransparentBackground?: boolean; label?: string; + description?: string; + required?: boolean; + errorMessage?: string; "aria-labelledby"?: string; + className?: string; + itemInfoClassName?: string; + itemTitleClassName?: string; + radioItemClassName?: string; + collapsibleClassName?: string; } export interface StandaloneRadioIndicatorProps { diff --git a/src/equicordplugins/components.dev/types/scroller.d.ts b/src/equicordplugins/components.dev/types/scroller.d.ts index e9170b559..9ed39c1ef 100644 --- a/src/equicordplugins/components.dev/types/scroller.d.ts +++ b/src/equicordplugins/components.dev/types/scroller.d.ts @@ -4,21 +4,43 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export interface ScrollerProps { +export interface ScrollerProps extends Omit, "dir"> { className?: string; style?: React.CSSProperties; children?: React.ReactNode; orientation?: "vertical" | "horizontal"; + paddingFix?: boolean; fade?: boolean; dir?: "ltr" | "rtl"; onScroll?: (e: React.UIEvent) => void; } -export interface ListScrollerProps extends ScrollerProps { - rowHeight: number; +export interface ListScrollerProps { + className?: string; + style?: React.CSSProperties; + dir?: "ltr" | "rtl"; + fade?: boolean; + customTheme?: boolean; + onScroll?: (e: React.UIEvent) => void; + onResize?: ((entries: ResizeObserverEntry[]) => void) | null; + onContentResize?: ((entries: ResizeObserverEntry[]) => void) | null; sections: number[]; sectionHeight: number; - renderSection: (section: number) => React.ReactNode; - renderRow: (row: { section: number; row: number }) => React.ReactNode; - role?: string; + rowHeight: number; + footerHeight?: number; + sidebarHeight?: number; + listHeaderHeight?: number; + renderSection?: (section: { type: string; section: number; }) => React.ReactNode; + renderRow: (row: { type: string; section: number; row: number; }) => React.ReactNode; + renderFooter?: (footer: { type: string; section: number; }) => React.ReactNode; + renderSidebar?: (isListVisible: boolean, isSidebarVisible: boolean) => React.ReactNode; + renderListHeader?: () => React.ReactNode; + stickyListHeader?: boolean; + wrapSection?: (section: number, children: React.ReactNode[]) => React.ReactNode; + paddingTop?: number; + paddingBottom?: number; + chunkSize?: number; + innerId?: string; + innerRole?: string; + innerAriaLabel?: string; } diff --git a/src/equicordplugins/components.dev/types/slider.d.ts b/src/equicordplugins/components.dev/types/slider.d.ts index 83abcec26..9d0271b50 100644 --- a/src/equicordplugins/components.dev/types/slider.d.ts +++ b/src/equicordplugins/components.dev/types/slider.d.ts @@ -23,6 +23,7 @@ export interface SliderProps { renderMarker?: (value: number) => React.ReactNode; getAriaValueText?: (value: number) => string; keyboardStep?: number; + handleSize?: number; disabled?: boolean; mini?: boolean; hideBubble?: boolean; @@ -39,4 +40,11 @@ export interface SliderProps { "aria-label"?: string; "aria-labelledby"?: string; "aria-describedby"?: string; + label?: string; + hideLabel?: boolean; + required?: boolean; + description?: string; + helperText?: string; + errorMessage?: string; + successMessage?: string; } diff --git a/src/equicordplugins/components.dev/types/timestamp.d.ts b/src/equicordplugins/components.dev/types/timestamp.d.ts index 4725c184f..8a0de3ef8 100644 --- a/src/equicordplugins/components.dev/types/timestamp.d.ts +++ b/src/equicordplugins/components.dev/types/timestamp.d.ts @@ -20,6 +20,7 @@ export interface TimestampProps { isInline?: boolean; isVisibleOnlyOnHover?: boolean; isEdited?: boolean; + application?: any; id?: string; className?: string; children?: React.ReactNode; diff --git a/src/equicordplugins/components.dev/types/toast.d.ts b/src/equicordplugins/components.dev/types/toast.d.ts index 398047739..27527ca90 100644 --- a/src/equicordplugins/components.dev/types/toast.d.ts +++ b/src/equicordplugins/components.dev/types/toast.d.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export type ToastTypeValue = "message" | "success" | "failure" | "custom" | "clip" | "link" | "forward" | "invite" | "bookmark" | "clock" | "ai"; +export type ToastTypeValue = "message" | "success" | "failure" | "custom" | "clip" | "link" | "forward" | "bookmark" | "clock"; export type ToastPositionValue = 0 | 1; diff --git a/src/equicordplugins/components.dev/types/tooltip.d.ts b/src/equicordplugins/components.dev/types/tooltip.d.ts index 1b755535a..27c1dfe9b 100644 --- a/src/equicordplugins/components.dev/types/tooltip.d.ts +++ b/src/equicordplugins/components.dev/types/tooltip.d.ts @@ -27,6 +27,7 @@ export interface ManaTooltipProps { tooltipStyle?: React.CSSProperties; tooltipContentClassName?: string; tooltipPointerClassName?: string; + positionKeyStemOverride?: string; onTooltipShow?: () => void; onTooltipHide?: () => void; onAnimationRest?: () => void; @@ -50,6 +51,13 @@ export interface ManaRichTooltipProps { anchorRef?: React.RefObject; positionKey?: string; ariaHidden?: boolean; + shouldShow?: boolean; + delay?: number; + onTooltipShow?: () => void; + onTooltipHide?: () => void; + forceOpen?: boolean; + overflowOnly?: boolean; + hideOnClick?: boolean; children: React.ReactNode | ((props: Record) => React.ReactNode); } @@ -68,10 +76,14 @@ export interface ManaPopoverProps { actions?: PopoverAction[]; textLink?: { text: string; onClick?: () => void; }; gradientColor?: string; + scrollBehavior?: string; + hasVideo?: boolean; onRequestClose?: (reason: string) => void; + popoverRef?: React.Ref; targetElementRef: React.RefObject; shouldShow: boolean; position?: TooltipPosition; + caretConfig?: { align?: "start" | "center" | "end"; }; alignmentStrategy?: "edge" | "trigger-center"; align?: "left" | "center" | "right"; }