refactor(components.dev): redesign DocPage template and update all tabs (#770)
* refactor(components.dev): redesign DocPage template and update all tabs * lint
This commit is contained in:
parent
5eefbb1d85
commit
b113d13118
65 changed files with 6093 additions and 4833 deletions
258
src/equicordplugins/components.dev/components/DocPage.tsx
Normal file
258
src/equicordplugins/components.dev/components/DocPage.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<div className={cl("table-wrapper")}>
|
||||
<table className={cl("table")}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIELD</th>
|
||||
<th>TYPE</th>
|
||||
<th>DEFAULT</th>
|
||||
<th>DESCRIPTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.map(p => (
|
||||
<tr key={p.name} className={p.required ? cl("table-row-required") : p.internal ? cl("table-row-internal") : undefined}>
|
||||
<td>
|
||||
<span className={cl("table-field")}>{p.name}</span>
|
||||
</td>
|
||||
<td><InlineCode>{p.type}</InlineCode></td>
|
||||
<td>{p.default ?? <span className={cl("table-dash")}>-</span>}</td>
|
||||
<td className={cl("table-desc")}>{p.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{showLegend && (hasRequired || hasInternal) && (
|
||||
<div className={cl("table-legend")}>
|
||||
{hasRequired && <span className={cl("table-legend-item")}><span className={cl("table-legend-bar", "table-legend-required")} /> Required</span>}
|
||||
{hasInternal && <span className={cl("table-legend-item")}><span className={cl("table-legend-bar", "table-legend-internal")} /> Internal</span>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TableOfContents({ entries }: { entries: { id: string; label: string; }[]; }) {
|
||||
return (
|
||||
<div className={cl("toc")}>
|
||||
<DiscordText variant="text-xs/semibold" color="text-muted" className={cl("toc-label")}>
|
||||
ON THIS PAGE
|
||||
</DiscordText>
|
||||
<div className={cl("toc-links")}>
|
||||
{entries.map(entry => (
|
||||
<a
|
||||
key={entry.id}
|
||||
className={cl("toc-link")}
|
||||
href={`#${entry.id}`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
document.getElementById(entry.id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}}
|
||||
>
|
||||
{entry.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MajorSection({ title, id, children }: { title: string; id: string; children: React.ReactNode; }) {
|
||||
return (
|
||||
<div id={id} className={cl("doc-section")}>
|
||||
<DiscordHeading variant="heading-lg/semibold" color="text-strong">{title}</DiscordHeading>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DemoSectionBlock({ section, relevantProps }: { section: DemoSection; relevantProps: PropDef[] | null; }) {
|
||||
const id = section.id ?? generateId(section.title);
|
||||
return (
|
||||
<div id={id} className={cl("doc-section")}>
|
||||
<DiscordHeading variant="heading-md/semibold" color="text-strong">{section.title}</DiscordHeading>
|
||||
{section.notice && (
|
||||
<Notice messageType={section.notice.type}>{section.notice.children}</Notice>
|
||||
)}
|
||||
{section.description && (
|
||||
<Paragraph color="text-muted">{section.description}</Paragraph>
|
||||
)}
|
||||
<div className={cl("doc-demo")}>
|
||||
{section.children}
|
||||
</div>
|
||||
{section.code && (
|
||||
<CodeBlock content={section.code} lang="tsx" />
|
||||
)}
|
||||
{relevantProps && (
|
||||
<div className={cl("doc-relevant-props")}>
|
||||
<PropsTable props={relevantProps} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={cl("doc-page")}>
|
||||
{componentName && (
|
||||
<DiscordHeading variant="heading-xl/bold" color="text-strong" className={cl("doc-title")}>
|
||||
{componentName}
|
||||
</DiscordHeading>
|
||||
)}
|
||||
|
||||
<div id="overview" className={cl("doc-section")}>
|
||||
<DiscordHeading variant="heading-lg/semibold" color="text-strong">Overview</DiscordHeading>
|
||||
<Paragraph color="text-muted">{overview}</Paragraph>
|
||||
</div>
|
||||
|
||||
{notices?.map((notice, i) => (
|
||||
<Notice key={i} messageType={notice.type}>{notice.children}</Notice>
|
||||
))}
|
||||
|
||||
{shouldShowToc && <TableOfContents entries={tocEntries} />}
|
||||
|
||||
<Divider />
|
||||
|
||||
<div id="import" className={cl("doc-section")}>
|
||||
<div className={cl("import-block")}>
|
||||
<DiscordText variant="text-xs/semibold" color="text-muted" className={cl("import-label")}>
|
||||
IMPORT
|
||||
</DiscordText>
|
||||
<CodeBlock content={importPath} lang="tsx" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{items.map(section => {
|
||||
const relevant = section.relevantProps?.length
|
||||
? flat.filter(p => section.relevantProps!.includes(p.name))
|
||||
: null;
|
||||
|
||||
return [
|
||||
<Divider key={`${section.title}-divider`} />,
|
||||
<DemoSectionBlock key={section.title} section={section} relevantProps={relevant} />,
|
||||
];
|
||||
})}
|
||||
|
||||
<Divider />
|
||||
|
||||
<MajorSection title="API Reference" id="api-reference">
|
||||
{isGrouped(props) ? (
|
||||
props.map(group => (
|
||||
<div key={group.title} className={cl("doc-prop-group")}>
|
||||
<DiscordHeading variant="heading-md/medium" color="text-strong">
|
||||
{group.title}
|
||||
</DiscordHeading>
|
||||
<PropsTable props={group.props} showLegend />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<PropsTable props={props} showLegend />
|
||||
)}
|
||||
</MajorSection>
|
||||
|
||||
{examples?.length ? (
|
||||
<>
|
||||
<Divider />
|
||||
<MajorSection title="Examples" id="examples">
|
||||
{examples.map(ex => (
|
||||
<div key={ex.label} className={cl("doc-example")}>
|
||||
<Paragraph color="text-muted">{ex.label}</Paragraph>
|
||||
<CodeBlock content={ex.code} lang="tsx" />
|
||||
</div>
|
||||
))}
|
||||
</MajorSection>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -75,6 +75,7 @@ import type {
|
|||
NumberBadgeProps,
|
||||
OpenModalOptions,
|
||||
PaginatorProps,
|
||||
PremiumBadgeProps,
|
||||
ProgressBarProps,
|
||||
SearchBarProps,
|
||||
SimpleErrorBoundaryProps,
|
||||
|
|
@ -226,11 +227,28 @@ export const CircleBadge: React.ComponentType<CircleBadgeProps> = proxyLazy(() =
|
|||
!fn.toString().includes("premiumBadge")
|
||||
)
|
||||
);
|
||||
export const PremiumBadge: React.ComponentType<PremiumBadgeProps> = 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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic Accordion">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Click to expand/collapse content.
|
||||
</Paragraph>
|
||||
<Accordion title="Click to expand">
|
||||
<Paragraph>This is the accordion content that is revealed when expanded.</Paragraph>
|
||||
</Accordion>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Subtitle">
|
||||
<Accordion title="Settings" subtitle="Configure options">
|
||||
<Paragraph>Content with a subtitle in the header.</Paragraph>
|
||||
</Accordion>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Default Expanded">
|
||||
<Accordion title="Already Open" defaultExpanded>
|
||||
<Paragraph>This accordion starts expanded by default.</Paragraph>
|
||||
</Accordion>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Controlled">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Use isExpanded and onExpandedChange for controlled state.
|
||||
</Paragraph>
|
||||
<Accordion
|
||||
title="Controlled Accordion"
|
||||
isExpanded={controlled}
|
||||
onExpandedChange={setControlled}
|
||||
>
|
||||
<Paragraph>This accordion's state is controlled externally.</Paragraph>
|
||||
</Accordion>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Max Height">
|
||||
<Accordion title="Limited Height" maxHeight={100}>
|
||||
<Paragraph>Line 1</Paragraph>
|
||||
<Paragraph>Line 2</Paragraph>
|
||||
<Paragraph>Line 3</Paragraph>
|
||||
<Paragraph>Line 4</Paragraph>
|
||||
<Paragraph>Line 5</Paragraph>
|
||||
<Paragraph>Line 6</Paragraph>
|
||||
</Accordion>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• title: string - Header title</Paragraph>
|
||||
<Paragraph color="text-muted">• subtitle?: string - Optional subtitle</Paragraph>
|
||||
<Paragraph color="text-muted">• icon?: ReactNode - Optional icon</Paragraph>
|
||||
<Paragraph color="text-muted">• children: ReactNode - Content</Paragraph>
|
||||
<Paragraph color="text-muted">• defaultExpanded?: boolean - Initial state (default: false)</Paragraph>
|
||||
<Paragraph color="text-muted">• isExpanded?: boolean - Controlled expanded state</Paragraph>
|
||||
<Paragraph color="text-muted">• onExpandedChange?: (expanded: boolean) => void</Paragraph>
|
||||
<Paragraph color="text-muted">• onOpen?: () => void - Called when opened</Paragraph>
|
||||
<Paragraph color="text-muted">• maxHeight?: number | string - Max content height</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<Accordion
|
||||
title="Controlled Accordion"
|
||||
isExpanded={controlled}
|
||||
onExpandedChange={setControlled}
|
||||
>
|
||||
<Paragraph>This accordion's state is controlled externally.</Paragraph>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AccordionTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="Accordion"
|
||||
overview="Accordion is a collapsible content container with a clickable header. It supports both controlled and uncontrolled modes, optional subtitle and icon, and configurable max height for overflowing content."
|
||||
importPath={'import { Accordion } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Click the header to expand and collapse.",
|
||||
children: (
|
||||
<Accordion title="Click to expand">
|
||||
<Paragraph>This is the accordion content that is revealed when expanded.</Paragraph>
|
||||
</Accordion>
|
||||
),
|
||||
code: '<Accordion title="Details">\n <Paragraph>Hidden content here.</Paragraph>\n</Accordion>',
|
||||
relevantProps: ["title", "children"]
|
||||
},
|
||||
{
|
||||
title: "With Subtitle",
|
||||
description: "A subtitle renders below the title in smaller, subtler text.",
|
||||
children: (
|
||||
<Accordion title="Settings" subtitle="Configure options">
|
||||
<Paragraph>Content with a subtitle in the header.</Paragraph>
|
||||
</Accordion>
|
||||
),
|
||||
relevantProps: ["subtitle", "icon"]
|
||||
},
|
||||
{
|
||||
title: "Default Expanded",
|
||||
description: "Starts open on mount using defaultExpanded.",
|
||||
children: (
|
||||
<Accordion title="Already Open" defaultExpanded>
|
||||
<Paragraph>This accordion starts expanded by default.</Paragraph>
|
||||
</Accordion>
|
||||
),
|
||||
relevantProps: ["defaultExpanded"]
|
||||
},
|
||||
{
|
||||
title: "Controlled",
|
||||
description: "Use isExpanded and onExpandedChange to manage state externally.",
|
||||
children: <ControlledDemo />,
|
||||
code: '<Accordion title="Settings" isExpanded={open} onExpandedChange={setOpen}>\n <Paragraph>Controlled content.</Paragraph>\n</Accordion>',
|
||||
relevantProps: ["isExpanded", "onExpandedChange"]
|
||||
},
|
||||
{
|
||||
title: "With Max Height",
|
||||
description: "Constrains expanded content height. Overflowing content is clipped.",
|
||||
children: (
|
||||
<Accordion title="Limited Height" maxHeight={100}>
|
||||
<Paragraph>Line 1</Paragraph>
|
||||
<Paragraph>Line 2</Paragraph>
|
||||
<Paragraph>Line 3</Paragraph>
|
||||
<Paragraph>Line 4</Paragraph>
|
||||
<Paragraph>Line 5</Paragraph>
|
||||
<Paragraph>Line 6</Paragraph>
|
||||
</Accordion>
|
||||
),
|
||||
code: '<Accordion title="Advanced" subtitle="Extra options" icon={<GearIcon />} maxHeight={200}>\n <Paragraph>Scrollable content area.</Paragraph>\n</Accordion>',
|
||||
relevantProps: ["maxHeight"]
|
||||
},
|
||||
]}
|
||||
props={ACCORDION_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic Alert">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Simple alert with just a title and body.
|
||||
</Paragraph>
|
||||
<Button onClick={showBasicAlert}>
|
||||
Show Basic Alert
|
||||
</Button>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Confirm Alert">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Alert with confirm and cancel buttons and callbacks.
|
||||
</Paragraph>
|
||||
<Button onClick={showConfirmAlert}>
|
||||
Show Confirm Alert
|
||||
</Button>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Danger Alert">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Alert with red danger button for destructive actions.
|
||||
</Paragraph>
|
||||
<Button variant="dangerPrimary" onClick={showDangerAlert}>
|
||||
Show Danger Alert
|
||||
</Button>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Expressive Alert">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Alert with premium/expressive styled button.
|
||||
</Paragraph>
|
||||
<Button onClick={showExpressiveAlert}>
|
||||
Show Expressive Alert
|
||||
</Button>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Alerts API">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Alerts.show(options)</strong> - Show an alert dialog
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Alerts.close()</strong> - Close the current alert
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Alerts.confirm(options)</strong> - Show a confirm dialog
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Options">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>title</strong> - Alert title text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>body</strong> - Alert body content (string or ReactNode)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>confirmText</strong> - Text for confirm button
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>cancelText</strong> - Text for cancel button (shows cancel if set)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>confirmVariant</strong> - "primary", "critical-primary", or "expressive"
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>onConfirm</strong> - Callback when confirm clicked
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>onCancel</strong> - Callback when cancel clicked
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>onCloseCallback</strong> - Callback when alert closes
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="Alerts"
|
||||
overview={"Alerts is a utility API for showing modal alert dialogs. It wraps Discord's ConfirmModal with a simple show/close/confirm interface. The confirm method returns a Promise that resolves to true or false based on the user's choice."}
|
||||
notices={[
|
||||
{ type: "warn", children: "Alerts.show() opens a modal dialog that blocks user interaction with the rest of the UI. Avoid chaining multiple alerts or showing them in rapid succession." },
|
||||
{ type: "info", children: "Prefer Alerts.confirm() over Alerts.show() when you need to act on the user's choice, since it returns a Promise<boolean> 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: (
|
||||
<Button onClick={() => {
|
||||
Alerts.show({
|
||||
title: "Basic Alert",
|
||||
body: "This is a basic alert dialog with just an OK button.",
|
||||
});
|
||||
}}>
|
||||
Show Basic Alert
|
||||
</Button>
|
||||
),
|
||||
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: (
|
||||
<Button onClick={() => {
|
||||
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),
|
||||
});
|
||||
}}>
|
||||
Show Confirm Alert
|
||||
</Button>
|
||||
),
|
||||
relevantProps: ["confirmText", "cancelText", "onConfirm", "onCancel"]
|
||||
},
|
||||
{
|
||||
title: "Danger Alert",
|
||||
description: "Uses critical-primary variant for destructive actions.",
|
||||
children: (
|
||||
<Button variant="dangerPrimary" onClick={() => {
|
||||
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),
|
||||
});
|
||||
}}>
|
||||
Show Danger Alert
|
||||
</Button>
|
||||
),
|
||||
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: (
|
||||
<Button onClick={() => {
|
||||
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),
|
||||
});
|
||||
}}>
|
||||
Show Expressive Alert
|
||||
</Button>
|
||||
),
|
||||
relevantProps: ["confirmVariant"]
|
||||
},
|
||||
{
|
||||
title: "Promise-based Confirm",
|
||||
description: "Alerts.confirm() returns a Promise<boolean>. Resolves true on confirm, false on cancel.",
|
||||
children: (
|
||||
<Button onClick={async () => {
|
||||
const result = await Alerts.confirm({
|
||||
title: "Async Confirm",
|
||||
body: "This uses Alerts.confirm() which returns a promise.",
|
||||
confirmText: "Accept",
|
||||
cancelText: "Decline",
|
||||
});
|
||||
showToast(result ? "Accepted!" : "Declined", result ? ToastType.SUCCESS : ToastType.MESSAGE);
|
||||
}}>
|
||||
Show Promise Confirm
|
||||
</Button>
|
||||
),
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>", internal: true, description: "Props forwarded to the focus management wrapper." },
|
||||
{ name: "ref", type: "Ref<HTMLAnchorElement>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic Anchor">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
A styled anchor/link component with underline on hover.
|
||||
</Paragraph>
|
||||
<Anchor href="https://discord.com">
|
||||
Discord Homepage
|
||||
</Anchor>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Without Underline Styles">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Disable the default underline-on-hover behavior.
|
||||
</Paragraph>
|
||||
<Anchor href="https://discord.com" useDefaultUnderlineStyles={false}>
|
||||
No underline on hover
|
||||
</Anchor>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Custom Target">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Open in same tab instead of new tab.
|
||||
</Paragraph>
|
||||
<Anchor href="https://discord.com" target="_self">
|
||||
Opens in same tab
|
||||
</Anchor>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Title Tooltip">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Hover to see the title tooltip.
|
||||
</Paragraph>
|
||||
<Anchor href="https://discord.com" title="Click to visit Discord">
|
||||
Hover for tooltip
|
||||
</Anchor>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Custom Styling">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Custom styles applied via style prop.
|
||||
</Paragraph>
|
||||
<Anchor
|
||||
href="https://discord.com"
|
||||
style={{ color: "var(--text-positive)", fontWeight: 600 }}
|
||||
>
|
||||
Green styled link
|
||||
</Anchor>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With onClick Handler">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Custom click handler (prevents default navigation).
|
||||
</Paragraph>
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
alert("Link clicked!");
|
||||
}}
|
||||
>
|
||||
Click me (shows alert)
|
||||
</Anchor>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Multiple Links">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Links in a sentence.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Check out the <Anchor href="https://discord.com/terms">Terms of Service</Anchor> and{" "}
|
||||
<Anchor href="https://discord.com/privacy">Privacy Policy</Anchor> for more info.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Anchor</strong> - Styled link component (also known as MaskedLink)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• href?: string - Link URL</Paragraph>
|
||||
<Paragraph color="text-muted">• onClick?: MouseEventHandler - Click handler</Paragraph>
|
||||
<Paragraph color="text-muted">• className?: string - Additional CSS classes</Paragraph>
|
||||
<Paragraph color="text-muted">• children?: ReactNode - Link content</Paragraph>
|
||||
<Paragraph color="text-muted">• rel?: string - Link relationship (auto-set for external)</Paragraph>
|
||||
<Paragraph color="text-muted">• target?: string - Link target (auto-set to _blank for external)</Paragraph>
|
||||
<Paragraph color="text-muted">• useDefaultUnderlineStyles?: boolean - Show underline on hover (default: true)</Paragraph>
|
||||
<Paragraph color="text-muted">• title?: string - Tooltip text on hover</Paragraph>
|
||||
<Paragraph color="text-muted">• style?: CSSProperties - Inline styles</Paragraph>
|
||||
<Paragraph color="text-muted">• focusProps?: object - Focus ring configuration</Paragraph>
|
||||
<Paragraph color="text-muted">• ref?: Ref - Forward ref to anchor element</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>Behavior</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• External links automatically get rel="noreferrer noopener" and target="_blank"</Paragraph>
|
||||
<Paragraph color="text-muted">• If no onClick is provided but href is set, uses Discord's default link interceptor</Paragraph>
|
||||
<Paragraph color="text-muted">• Untrusted external links show a warning modal before navigating</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="Anchor"
|
||||
overview="Anchor is Discord's styled link component. It handles external link safety (adding noreferrer/noopener, opening in new tabs), untrusted link warning modals, and integrates with Discord's focus management system. Supports underline-on-hover styling by default."
|
||||
notices={[
|
||||
{ type: "positive", children: 'Anchor automatically applies rel="noreferrer noopener" and target="_blank" for external URLs, so you don\'t need to set these manually.' },
|
||||
]}
|
||||
importPath={'import { Anchor } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic Link",
|
||||
description: "A styled link with underline on hover.",
|
||||
children: (
|
||||
<Anchor href="https://discord.com">
|
||||
Discord Homepage
|
||||
</Anchor>
|
||||
),
|
||||
code: '<Anchor href="https://example.com">Visit Example</Anchor>',
|
||||
relevantProps: ["href", "children"]
|
||||
},
|
||||
{
|
||||
title: "Without Underline",
|
||||
description: "Disable the default underline-on-hover behavior with useDefaultUnderlineStyles={false}.",
|
||||
children: (
|
||||
<Anchor href="https://discord.com" useDefaultUnderlineStyles={false}>
|
||||
No underline on hover
|
||||
</Anchor>
|
||||
),
|
||||
relevantProps: ["useDefaultUnderlineStyles"]
|
||||
},
|
||||
{
|
||||
title: "With Title Tooltip",
|
||||
description: "Hover to see the native title tooltip.",
|
||||
children: (
|
||||
<Anchor href="https://discord.com" title="Click to visit Discord">
|
||||
Hover for tooltip
|
||||
</Anchor>
|
||||
),
|
||||
relevantProps: ["title"]
|
||||
},
|
||||
{
|
||||
title: "Custom Styling",
|
||||
children: (
|
||||
<Anchor
|
||||
href="https://discord.com"
|
||||
style={{ color: "var(--text-positive)", fontWeight: 600 }}
|
||||
>
|
||||
Green styled link
|
||||
</Anchor>
|
||||
),
|
||||
relevantProps: ["style", "className"]
|
||||
},
|
||||
{
|
||||
title: "With onClick Handler",
|
||||
description: "Custom click handler prevents default navigation.",
|
||||
children: (
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
alert("Link clicked!");
|
||||
}}
|
||||
>
|
||||
Click me (shows alert)
|
||||
</Anchor>
|
||||
),
|
||||
code: '<Anchor href="#" onClick={e => { e.preventDefault(); doSomething(); }}>\n Click me\n</Anchor>',
|
||||
relevantProps: ["onClick"]
|
||||
},
|
||||
{
|
||||
title: "Inline in Text",
|
||||
description: "Links used naturally within a paragraph.",
|
||||
children: (
|
||||
<Paragraph>
|
||||
Check out the <Anchor href="https://discord.com/terms">Terms of Service</Anchor> and{" "}
|
||||
<Anchor href="https://discord.com/privacy">Privacy Policy</Anchor> for more info.
|
||||
</Paragraph>
|
||||
),
|
||||
code: '<Paragraph>\n Read the <Anchor href="/docs" useDefaultUnderlineStyles={false}>documentation</Anchor>.\n</Paragraph>'
|
||||
},
|
||||
]}
|
||||
props={ANCHOR_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<keyof typeof SpringConfigs>("default");
|
||||
|
|
@ -23,10 +32,7 @@ function SpringDemo() {
|
|||
});
|
||||
|
||||
return (
|
||||
<SectionWrapper title="useSpring">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Animate values with spring physics. Click the box or change config.
|
||||
</Paragraph>
|
||||
<>
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 16, flexWrap: "wrap" }}>
|
||||
{(Object.keys(SpringConfigs) as (keyof typeof SpringConfigs)[]).map(c => (
|
||||
<ManaButton
|
||||
|
|
@ -53,7 +59,7 @@ function SpringDemo() {
|
|||
>
|
||||
<Paragraph color="text-strong">Click me</Paragraph>
|
||||
</animated.div>
|
||||
</SectionWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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 (
|
||||
<SectionWrapper title="useTransition">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Animate items entering and leaving a list.
|
||||
</Paragraph>
|
||||
<>
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
|
||||
<ManaButton variant="primary" size="sm" text="Add Item" onClick={addItem} />
|
||||
<ManaButton variant="primary" size="sm" text="Add Item" onClick={() => {
|
||||
setItems([...items, nextId]);
|
||||
setNextId(nextId + 1);
|
||||
}} />
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
|
@ -103,13 +100,13 @@ function TransitionDemo() {
|
|||
borderRadius: 8,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={() => removeItem(item)}
|
||||
onClick={() => setItems(items.filter(i => i !== item))}
|
||||
>
|
||||
<Paragraph>Item {item}</Paragraph>
|
||||
</animated.div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -123,10 +120,7 @@ function TrailDemo() {
|
|||
}) as object[];
|
||||
|
||||
return (
|
||||
<SectionWrapper title="useTrail">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Staggered animations for multiple items. Each item animates slightly after the previous.
|
||||
</Paragraph>
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
|
|
@ -150,7 +144,7 @@ function TrailDemo() {
|
|||
</animated.div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -166,67 +160,61 @@ function AnimatedElementsDemo() {
|
|||
});
|
||||
|
||||
return (
|
||||
<SectionWrapper title="animated Elements">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Use animated.div, animated.span, animated.svg, etc. for spring-animated elements.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 16, flexWrap: "wrap" }}>
|
||||
<animated.div
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
...hoverSpring,
|
||||
width: 100,
|
||||
height: 100,
|
||||
background: "var(--background-secondary)",
|
||||
borderRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 12 }}>Hover me</Paragraph>
|
||||
</animated.div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
<animated.div
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
...hoverSpring,
|
||||
width: 100,
|
||||
height: 100,
|
||||
background: "var(--background-secondary)",
|
||||
borderRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 12 }}>Hover me</Paragraph>
|
||||
</animated.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AnimationsTab() {
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SpringDemo />
|
||||
<TransitionDemo />
|
||||
<TrailDemo />
|
||||
<AnimatedElementsDemo />
|
||||
|
||||
<SectionWrapper title="Spring Configs">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Preset spring configurations for different animation feels.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• default - Standard spring</Paragraph>
|
||||
<Paragraph color="text-muted">• gentle - Soft, slow spring</Paragraph>
|
||||
<Paragraph color="text-muted">• wobbly - Bouncy, playful spring</Paragraph>
|
||||
<Paragraph color="text-muted">• stiff - Quick, snappy spring</Paragraph>
|
||||
<Paragraph color="text-muted">• slow - Very slow spring</Paragraph>
|
||||
<Paragraph color="text-muted">• molasses - Extremely slow spring</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Available Hooks">
|
||||
<Paragraph color="text-muted">• useSpring - Single spring animation</Paragraph>
|
||||
<Paragraph color="text-muted">• useSprings - Multiple springs</Paragraph>
|
||||
<Paragraph color="text-muted">• useTransition - Enter/leave animations</Paragraph>
|
||||
<Paragraph color="text-muted">• useTrail - Staggered animations</Paragraph>
|
||||
<Paragraph color="text-muted">• useChain - Sequence animations</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Available Components">
|
||||
<Paragraph color="text-muted">• Transition - Declarative transitions</Paragraph>
|
||||
<Paragraph color="text-muted">• Spring - Declarative spring</Paragraph>
|
||||
<Paragraph color="text-muted">• Trail - Declarative trail</Paragraph>
|
||||
<Paragraph color="text-muted">• animated.* - Animated HTML/SVG elements</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="Animations"
|
||||
overview="Discord uses react-spring for physics-based animations. The Animations module exposes useSpring, useTransition, useTrail, useChain, useSprings hooks plus animated.* element wrappers and preset spring configs. All animations are interruptible and use spring physics rather than duration-based easing."
|
||||
notices={[
|
||||
{ type: "info", children: "These are re-exports of react-spring. All animations are interruptible and respect the user's reduced motion preference when used with Discord's built-in motion settings." },
|
||||
]}
|
||||
importPath={'import { Animations, useSpring, useTransition, useTrail, SpringConfigs } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "useSpring",
|
||||
description: "Animate values with spring physics. Click the box to toggle, and switch between config presets to see different animation feels.",
|
||||
children: <SpringDemo />,
|
||||
code: "const spring = useSpring({ opacity: visible ? 1 : 0, config: SpringConfigs.gentle });\nreturn <animated.div style={spring}>Content</animated.div>;"
|
||||
},
|
||||
{
|
||||
title: "useTransition",
|
||||
description: "Animate items entering and leaving a list. Click items to remove them, or use the buttons to add/clear.",
|
||||
children: <TransitionDemo />,
|
||||
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) => <animated.div style={style}>{item.name}</animated.div>);"
|
||||
},
|
||||
{
|
||||
title: "useTrail",
|
||||
description: "Staggered animations for multiple items. Each item animates slightly after the previous one.",
|
||||
children: <TrailDemo />,
|
||||
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: <AnimatedElementsDemo />
|
||||
},
|
||||
]}
|
||||
props={SPRING_CONFIG_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any>", 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<any>", description: "Ref for the typing indicator element." },
|
||||
{ name: "avatarContentRef", type: "Ref<any>", description: "Ref for the avatar image foreignObject content." },
|
||||
{ name: "CutoutIcon", type: "ComponentType<any>", 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<SVGSVGElement>", description: "Ref to the root SVG element." },
|
||||
];
|
||||
|
||||
export default function AvatarTab() {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const avatarUrl = currentUser?.getAvatarURL?.(undefined, 128);
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Avatar Sizes">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Avatar component from @webpack/common with various sizes.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
|
||||
{AvatarSizes.map(size => (
|
||||
<div key={size} style={{ textAlign: "center" }}>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
size={size}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{size.replace("SIZE_", "")}
|
||||
</Paragraph>
|
||||
<DocPage
|
||||
componentName="Avatar"
|
||||
overview="Avatar renders a user's profile picture as an SVG with optional status indicators, speaking rings, typing animations, and tooltip overlays. It handles all the visual states used throughout Discord's UI including mobile, VR, and voice activity indicators."
|
||||
importPath={'import { Avatar } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Sizes",
|
||||
description: "All 13 available sizes from 16px to 152px.",
|
||||
relevantProps: ["size", "src"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
|
||||
{AvatarSizes.map(size => (
|
||||
<div key={size} style={{ textAlign: "center" }}>
|
||||
<Avatar src={avatarUrl} size={size} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{size.replace("SIZE_", "")}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Status">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Avatar with status indicator.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
size="SIZE_40"
|
||||
status="online"
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Online</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
size="SIZE_40"
|
||||
status="idle"
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Idle</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
size="SIZE_40"
|
||||
status="dnd"
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>DND</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
size="SIZE_40"
|
||||
status="offline"
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Offline</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Mobile Status">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Avatar with mobile indicator.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
size="SIZE_40"
|
||||
status="online"
|
||||
isMobile
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Speaking State">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Avatar with speaking ring indicator.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
size="SIZE_40"
|
||||
isSpeaking
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Speaking</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• src - Avatar image URL</Paragraph>
|
||||
<Paragraph color="text-muted">• 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</Paragraph>
|
||||
<Paragraph color="text-muted">• status - online, idle, dnd, offline</Paragraph>
|
||||
<Paragraph color="text-muted">• isMobile - Show mobile indicator</Paragraph>
|
||||
<Paragraph color="text-muted">• isTyping - Show typing indicator</Paragraph>
|
||||
<Paragraph color="text-muted">• isSpeaking - Show speaking ring</Paragraph>
|
||||
<Paragraph color="text-muted">• statusTooltip - Show status on hover</Paragraph>
|
||||
<Paragraph color="text-muted">• aria-label - Accessibility label</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Statuses",
|
||||
description: "All status indicator variants.",
|
||||
code: '<Avatar src={user.getAvatarURL()} size="SIZE_40" status="online" />',
|
||||
relevantProps: ["status", "statusColor", "statusBackdropColor"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{AvatarStatuses.map(status => (
|
||||
<div key={status} style={{ textAlign: "center" }}>
|
||||
<Avatar src={avatarUrl} size="SIZE_40" status={status} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{status}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Mobile Status",
|
||||
description: "Shows the mobile phone icon instead of a status dot when isMobile is true.",
|
||||
code: '<Avatar src={avatarUrl} size="SIZE_32" status="online" isMobile statusTooltip />',
|
||||
relevantProps: ["isMobile", "isVR"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar src={avatarUrl} size="SIZE_40" status="online" isMobile />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Mobile</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar src={avatarUrl} size="SIZE_40" status="online" isVR />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>VR</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Speaking State",
|
||||
description: "Animated ring indicator for voice activity.",
|
||||
code: '<Avatar src={avatarUrl} size="SIZE_80" isSpeaking voiceDb={voiceLevel} />',
|
||||
relevantProps: ["isSpeaking", "isLatched", "voiceDb"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar src={avatarUrl} size="SIZE_40" isSpeaking />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Speaking</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Typing Indicator",
|
||||
description: "Shows a typing animation in the status area.",
|
||||
relevantProps: ["isTyping"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Avatar src={avatarUrl} size="SIZE_40" status="online" isTyping />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>Typing</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Status Tooltip",
|
||||
description: "Hover to see the status text tooltip.",
|
||||
relevantProps: ["statusTooltip", "statusTooltipDelay"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{AvatarStatuses.map(status => (
|
||||
<div key={status} style={{ textAlign: "center" }}>
|
||||
<Avatar src={avatarUrl} size="SIZE_40" status={status} statusTooltip />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{status}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
]}
|
||||
props={AVATAR_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="NumberBadge">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Displays a count value. Automatically abbreviates large numbers.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<NumberBadge count={1} />
|
||||
<NumberBadge count={9} />
|
||||
<NumberBadge count={99} />
|
||||
<NumberBadge count={999} />
|
||||
<NumberBadge count={9999} />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="TextBadge">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Displays text content.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<TextBadge text="NEW" />
|
||||
<TextBadge text="BETA" />
|
||||
<TextBadge text="PRO" />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="IconBadge">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Displays an icon inside a badge.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<IconBadge icon={EquicordIcon} />
|
||||
<IconBadge icon={EquicordIcon} color="var(--status-positive)" />
|
||||
<IconBadge icon={EquicordIcon} color="var(--status-warning)" />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="CircleBadge">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
A simple circular badge indicator.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<CircleBadge />
|
||||
<CircleBadge color="var(--status-positive)" />
|
||||
<CircleBadge color="var(--status-warning)" />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Shapes">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Badges support different shapes via the shape prop.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<TextBadge text="ROUND" shape={BadgeShapes.ROUND} />
|
||||
<TextBadge text="LEFT" shape={BadgeShapes.ROUND_LEFT} />
|
||||
<TextBadge text="RIGHT" shape={BadgeShapes.ROUND_RIGHT} />
|
||||
<TextBadge text="SQUARE" shape={BadgeShapes.SQUARE} />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Colors">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Custom colors can be applied. Use disableColor for no background.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<NumberBadge count={5} color="var(--status-danger)" />
|
||||
<NumberBadge count={5} color="var(--status-positive)" />
|
||||
<NumberBadge count={5} color="var(--status-warning)" />
|
||||
<NumberBadge count={5} color="var(--brand-500)" />
|
||||
<NumberBadge count={5} disableColor />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
NumberBadge: count, color?, disableColor?, shape?, className?, style?, renderBadgeCount?
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
TextBadge: text, color?, disableColor?, shape?, className?, style?
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
IconBadge: icon, color?, disableColor?, shape?, className?, style?
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
CircleBadge: color?, disableColor?, shape?, className?, style?
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
BadgeShapes: ROUND, ROUND_LEFT, ROUND_RIGHT, SQUARE
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="Badge"
|
||||
overview="Discord's badge system provides four visual badge components (NumberBadge, TextBadge, IconBadge, CircleBadge) plus a PremiumBadge variant. All share a common set of styling props for color, shape, and disabled state. NumberBadge auto-sizes its width based on digit count and abbreviates large numbers."
|
||||
importPath={'import { NumberBadge, TextBadge, IconBadge, CircleBadge, PremiumBadge, BadgeShapes } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "NumberBadge",
|
||||
description: "Displays a count. Auto-abbreviates at 1000+ and auto-sizes width based on digit count (16px for <10, 22px for <100, 30px otherwise).",
|
||||
code: "<NumberBadge count={unreadCount} />",
|
||||
relevantProps: ["count", "renderBadgeCount"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<NumberBadge count={1} />
|
||||
<NumberBadge count={9} />
|
||||
<NumberBadge count={99} />
|
||||
<NumberBadge count={999} />
|
||||
<NumberBadge count={9999} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "TextBadge",
|
||||
description: "Displays text content inside a badge.",
|
||||
relevantProps: ["text"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<TextBadge text="NEW" />
|
||||
<TextBadge text="BETA" />
|
||||
<TextBadge text="PRO" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "PremiumBadge",
|
||||
description: "A TextBadge variant with an additional premium styling class applied.",
|
||||
relevantProps: ["text"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<PremiumBadge text="PREMIUM" />
|
||||
<PremiumBadge text="NITRO" />
|
||||
<PremiumBadge text="BOOST" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "IconBadge",
|
||||
description: "Renders an icon component inside a badge. The icon receives color=\"currentColor\".",
|
||||
code: '<IconBadge icon={StarIcon} color="var(--status-warning)" />',
|
||||
relevantProps: ["icon"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<IconBadge icon={EquicordIcon} />
|
||||
<IconBadge icon={EquicordIcon} color="var(--status-positive)" />
|
||||
<IconBadge icon={EquicordIcon} color="var(--status-warning)" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "CircleBadge",
|
||||
description: "A simple circular dot indicator. Defaults to INTERACTIVE_TEXT_ACTIVE color unlike other badges.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<CircleBadge />
|
||||
<CircleBadge color="var(--status-positive)" />
|
||||
<CircleBadge color="var(--status-warning)" />
|
||||
<CircleBadge color="var(--status-danger)" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Shapes",
|
||||
description: "All four shape variants. SQUARE removes border radius entirely.",
|
||||
code: '<TextBadge text="NEW" shape={BadgeShapes.ROUND_LEFT} color="var(--brand-500)" />',
|
||||
relevantProps: ["shape"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<TextBadge text="ROUND" shape={BadgeShapes.ROUND} />
|
||||
<TextBadge text="LEFT" shape={BadgeShapes.ROUND_LEFT} />
|
||||
<TextBadge text="RIGHT" shape={BadgeShapes.ROUND_RIGHT} />
|
||||
<TextBadge text="SQUARE" shape={BadgeShapes.SQUARE} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Custom Colors",
|
||||
description: "Any CSS color value works. Use disableColor to remove the background entirely.",
|
||||
relevantProps: ["color", "disableColor"],
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<NumberBadge count={5} color="var(--status-danger)" />
|
||||
<NumberBadge count={5} color="var(--status-positive)" />
|
||||
<NumberBadge count={5} color="var(--status-warning)" />
|
||||
<NumberBadge count={5} color="var(--brand-500)" />
|
||||
<NumberBadge count={5} disableColor />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Custom Render",
|
||||
description: "NumberBadge accepts a custom renderBadgeCount function to format the display.",
|
||||
code: '<NumberBadge\n count={messageCount}\n renderBadgeCount={n => n > 99 ? "99+" : String(n)}\n/>',
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<NumberBadge count={42} renderBadgeCount={n => `#${n}`} />
|
||||
<NumberBadge count={100} renderBadgeCount={n => `${n}%`} />
|
||||
<NumberBadge count={5} renderBadgeCount={() => "!"} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
]}
|
||||
props={ALL_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any>", internal: true, description: "Props forwarded to the focus management wrapper." },
|
||||
{ name: "buttonRef", type: "Ref<HTMLButtonElement>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Variants (size: md)">
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaButtonVariants.map(variant => (
|
||||
<ManaButton key={variant} variant={variant} size="md" text={variant} />
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Sizes (variant: primary)">
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaButtonSizes.map(size => (
|
||||
<ManaButton key={size} variant="primary" size={size} text={`Size: ${size}`} />
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="States">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaButton variant="primary" size="md" text="Disabled" disabled />
|
||||
<ManaButton
|
||||
variant="primary"
|
||||
size="md"
|
||||
text={loading ? "Loading..." : "Click to Load"}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 2000);
|
||||
}}
|
||||
/>
|
||||
<ManaButton variant="primary" size="md" text="Rounded" rounded />
|
||||
<ManaButton variant="primary" size="md" text="Full Width" fullWidth />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="All Variants Disabled">
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaButtonVariants.map(variant => (
|
||||
<ManaButton key={variant} variant={variant} size="md" text={variant} disabled />
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• text - Button label text</Paragraph>
|
||||
<Paragraph color="text-muted">• variant - primary, secondary, critical-primary, critical-secondary, overlay-primary, overlay-secondary, expressive</Paragraph>
|
||||
<Paragraph color="text-muted">• size - xs, sm, md</Paragraph>
|
||||
<Paragraph color="text-muted">• disabled - Disable button</Paragraph>
|
||||
<Paragraph color="text-muted">• loading - Show loading spinner</Paragraph>
|
||||
<Paragraph color="text-muted">• fullWidth - Expand to container width</Paragraph>
|
||||
<Paragraph color="text-muted">• rounded - Rounded corners</Paragraph>
|
||||
<Paragraph color="text-muted">• icon - Icon component</Paragraph>
|
||||
<Paragraph color="text-muted">• iconPosition - start or end</Paragraph>
|
||||
<Paragraph color="text-muted">• onClick - Click handler</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaButton variant="primary" size="md" text="Disabled" disabled />
|
||||
<ManaButton
|
||||
variant="primary"
|
||||
size="md"
|
||||
text={loading ? "Loading..." : "Click to Load"}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 2000);
|
||||
}}
|
||||
/>
|
||||
<ManaButton variant="primary" size="md" text="Rounded" rounded />
|
||||
<ManaButton variant="primary" size="md" text="Full Width" fullWidth />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ButtonsTab() {
|
||||
return (
|
||||
<DocPage
|
||||
overview="ManaButton is Discord's primary button component used for actions and form submissions. It supports eight visual variants, three sizes, loading states, icons, and full-width layouts. Renders a native button element wrapped in a focus management provider."
|
||||
importPath={'import { ManaButton } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Variants",
|
||||
description: "All eight visual styles at the default medium size. The expressive variant includes a Rive animation background on hover.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaButtonVariants.map(variant => (
|
||||
<ManaButton key={variant} variant={variant} size="md" text={variant} />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: '<ManaButton variant="primary" size="md" text="Save" onClick={() => 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: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaButtonSizes.map(size => (
|
||||
<ManaButton key={size} variant="primary" size={size} text={`Size: ${size}`} />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: '<ManaButton variant="primary" size="sm" text="Small" />',
|
||||
relevantProps: ["size"],
|
||||
},
|
||||
{
|
||||
title: "States",
|
||||
description: "Disabled and loading both suppress all event handlers. Loading shows a spinner overlay with accessibility announcements.",
|
||||
children: <StatesDemo />,
|
||||
code: '<ManaButton\n variant="primary"\n text="Save"\n loading={saving}\n loadingStartedLabel="Saving..."\n loadingFinishedLabel="Saved!"\n onClick={handleSave}\n/>',
|
||||
relevantProps: ["disabled", "loading", "loadingStartedLabel", "loadingFinishedLabel", "rounded", "fullWidth"],
|
||||
},
|
||||
{
|
||||
title: "All Variants Disabled",
|
||||
description: "How each variant looks in its disabled state.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaButtonVariants.map(variant => (
|
||||
<ManaButton key={variant} variant={variant} size="md" text={variant} disabled />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Size \u00d7 Variant Matrix",
|
||||
description: "Every combination of size and variant for comparison.",
|
||||
children: (
|
||||
<>
|
||||
{ManaButtonSizes.map(size => (
|
||||
<div key={size} style={{ marginBottom: 8 }}>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 12, marginBottom: 4 }}>
|
||||
Size: {size}
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaButtonVariants.map(variant => (
|
||||
<ManaButton key={`${size}-${variant}`} variant={variant} size={size} text={variant} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
props={BUTTON_PROPS}
|
||||
examples={[
|
||||
{
|
||||
label: "Destructive action with critical variant:",
|
||||
code: '<ManaButton variant="critical-primary" size="sm" text="Delete" onClick={handleDelete} />',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any>(null);
|
||||
const [datePickerValue, setDatePickerValue] = useState<any>(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<string, unknown>", description: "Configuration for the field layout." },
|
||||
];
|
||||
|
||||
function CalendarDemo() {
|
||||
const [value, setValue] = useState<any>(null);
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Calendar">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Basic calendar component for date selection.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaCalendar
|
||||
value={calendarValue}
|
||||
onChange={setCalendarValue}
|
||||
aria-label="Select a date"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
<ManaCalendar
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
aria-label="Select a date"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
<SectionWrapper title="Calendar (Disabled)">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaCalendar
|
||||
value={calendarValue}
|
||||
onChange={setCalendarValue}
|
||||
disabled
|
||||
aria-label="Disabled calendar"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
function DatePickerDemo() {
|
||||
const [value, setValue] = useState<any>(null);
|
||||
|
||||
<SectionWrapper title="Calendar (Read Only)">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaCalendar
|
||||
value={calendarValue}
|
||||
onChange={setCalendarValue}
|
||||
readOnly
|
||||
aria-label="Read only calendar"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Date Picker">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Date picker with input field and calendar popover.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaDatePicker
|
||||
value={datePickerValue}
|
||||
onChange={setDatePickerValue}
|
||||
label="Select Date"
|
||||
description="Choose a date from the calendar"
|
||||
granularity="day"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Date Picker with Time">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Date picker with hour/minute granularity.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaDatePicker
|
||||
value={datePickerValue}
|
||||
onChange={setDatePickerValue}
|
||||
label="Select Date & Time"
|
||||
granularity="minute"
|
||||
hourCycle={24}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Date Picker States">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaDatePicker
|
||||
value={datePickerValue}
|
||||
onChange={setDatePickerValue}
|
||||
label="Disabled"
|
||||
disabled
|
||||
/>
|
||||
<ManaDatePicker
|
||||
value={datePickerValue}
|
||||
onChange={setDatePickerValue}
|
||||
label="Required"
|
||||
required
|
||||
/>
|
||||
<ManaDatePicker
|
||||
value={datePickerValue}
|
||||
onChange={setDatePickerValue}
|
||||
label="With Error"
|
||||
errorMessage="Please select a valid date"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>ManaCalendar</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• value: any - Selected date value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (value: any) => void - Selection callback
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable calendar
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• readOnly?: boolean - Read-only mode
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• aria-label?: string - Accessibility label
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>ManaDatePicker</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• value: any - Selected date value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (value: any) => void - Selection callback
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• label?: string - Input label
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• description?: string - Description text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• granularity?: "day" | "hour" | "minute" - Time precision
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• hourCycle?: 12 | 24 - Hour format
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable picker
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• required?: boolean - Mark as required
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• errorMessage?: string - Error text to display
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16, maxWidth: 300 }}>
|
||||
<ManaDatePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="Select Date"
|
||||
description="Choose a date from the calendar"
|
||||
granularity="day"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DatePickerTimeDemo() {
|
||||
const [value, setValue] = useState<any>(null);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16, maxWidth: 300 }}>
|
||||
<ManaDatePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="Select Date & Time"
|
||||
granularity="minute"
|
||||
hourCycle={24}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DatePickerStatesDemo() {
|
||||
const [value, setValue] = useState<any>(null);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16, maxWidth: 300 }}>
|
||||
<ManaDatePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="Disabled"
|
||||
disabled
|
||||
/>
|
||||
<ManaDatePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="Required"
|
||||
required
|
||||
/>
|
||||
<ManaDatePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="With Error"
|
||||
errorMessage="Please select a valid date"
|
||||
/>
|
||||
<ManaDatePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="With Helper Text"
|
||||
helperText="Pick any date in the future"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CalendarTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="Calendar"
|
||||
overview="ManaCalendar is a standalone calendar grid for date selection. ManaDatePicker wraps it in a text input with a popover, supporting time granularity, validation states, and field layout props. Both use react-aria internationalized date objects internally."
|
||||
notices={[
|
||||
{ type: "info", children: "Calendar values use react-aria's CalendarDate and CalendarDateTime types from @internationalized/date, not native JavaScript Date objects. You may need to convert between them." },
|
||||
]}
|
||||
importPath={'import { ManaCalendar, ManaDatePicker } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Calendar",
|
||||
description: "Basic calendar grid. Click a date to select it. Navigate months with the arrow buttons.",
|
||||
code: '<ManaCalendar value={date} onChange={setDate} aria-label="Pick a date" />',
|
||||
relevantProps: ["value", "onChange", "aria-label"],
|
||||
children: <CalendarDemo />
|
||||
},
|
||||
{
|
||||
title: "Calendar (Disabled)",
|
||||
description: "All dates are visible but not interactive.",
|
||||
relevantProps: ["disabled"],
|
||||
children: (
|
||||
<ManaCalendar
|
||||
value={null}
|
||||
onChange={() => { }}
|
||||
disabled
|
||||
aria-label="Disabled calendar"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Calendar (Read Only)",
|
||||
description: "Dates are visible with current selection shown but cannot be changed.",
|
||||
relevantProps: ["readOnly"],
|
||||
children: (
|
||||
<ManaCalendar
|
||||
value={null}
|
||||
onChange={() => { }}
|
||||
readOnly
|
||||
aria-label="Read only calendar"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Date Picker",
|
||||
description: "Text input with a calendar popover. Click the calendar icon to open.",
|
||||
code: '<ManaDatePicker\n value={date}\n onChange={setDate}\n label="Event Date"\n description="When should this happen?"\n granularity="day"\n/>',
|
||||
relevantProps: ["label", "description", "granularity"],
|
||||
children: <DatePickerDemo />
|
||||
},
|
||||
{
|
||||
title: "Date Picker with Time",
|
||||
description: "Using minute granularity adds hour and minute segments to the input.",
|
||||
code: '<ManaDatePicker\n value={date}\n onChange={setDate}\n label="Start Time"\n granularity="minute"\n hourCycle={24}\n required\n errorMessage={!date ? "Required" : ""}\n/>',
|
||||
relevantProps: ["granularity", "hourCycle"],
|
||||
children: <DatePickerTimeDemo />
|
||||
},
|
||||
{
|
||||
title: "Date Picker States",
|
||||
description: "Disabled, required, error, and helper text states.",
|
||||
relevantProps: ["disabled", "required", "errorMessage", "helperText"],
|
||||
children: <DatePickerStatesDemo />
|
||||
},
|
||||
]}
|
||||
props={[
|
||||
...CALENDAR_PROPS.map(p => ({ ...p, name: `[Calendar] ${p.name}` })),
|
||||
...DATEPICKER_PROPS.map(p => ({ ...p, name: `[DatePicker] ${p.name}` })),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Card Variants">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Card component from @components/Card with different severity styles.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{CardVariants.map(variant => (
|
||||
<Card key={variant} variant={variant}>
|
||||
<Paragraph style={{ fontWeight: 600, marginBottom: 4 }}>
|
||||
{variant.charAt(0).toUpperCase() + variant.slice(1)} Card
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
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"
|
||||
}.
|
||||
</Paragraph>
|
||||
<DocPage
|
||||
componentName="Card"
|
||||
overview="Card is a Vencord component that renders a styled container div with variant-based coloring. It supports filled and outline modes, automatic default padding, and all standard div attributes. Useful for grouping content with visual emphasis."
|
||||
notices={[
|
||||
{ type: "info", children: "Card is a Vencord component from @components/Card, not a Discord native component. Its API is stable but may differ from Discord's own card patterns." },
|
||||
]}
|
||||
importPath={'import { Card } from "@components/Card";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Variants",
|
||||
description: "Five visual styles for different content contexts.",
|
||||
code: '<Card variant="primary">\n <Paragraph>Content here</Paragraph>\n</Card>',
|
||||
relevantProps: ["variant", "children"],
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{CardVariants.map(variant => (
|
||||
<Card key={variant} variant={variant}>
|
||||
<Paragraph style={{ fontWeight: 600, marginBottom: 4 }}>
|
||||
{variant.charAt(0).toUpperCase() + variant.slice(1)}
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
{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."}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Outline Mode",
|
||||
description: "Uses a border instead of a filled background.",
|
||||
code: '<Card variant="warning" outline>\n <Paragraph>Heads up!</Paragraph>\n</Card>',
|
||||
relevantProps: ["outline"],
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{CardVariants.map(variant => (
|
||||
<Card key={variant} variant={variant} outline>
|
||||
<Paragraph>{variant} outline</Paragraph>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Padding Control",
|
||||
description: "Cards get 1em padding by default when no className is set. You can override this behavior explicitly.",
|
||||
code: '<Card variant="brand" className={myClass} defaultPadding>\n <Paragraph>Keeps padding despite custom className</Paragraph>\n</Card>',
|
||||
relevantProps: ["defaultPadding", "className"],
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Card defaultPadding>
|
||||
<Paragraph>defaultPadding=true</Paragraph>
|
||||
</Card>
|
||||
<Card defaultPadding={false}>
|
||||
<Paragraph style={{ padding: 8 }}>defaultPadding=false (content adds own padding)</Paragraph>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Nested Cards",
|
||||
description: "Cards can nest for complex layouts.",
|
||||
children: (
|
||||
<Card variant="primary">
|
||||
<Paragraph style={{ fontWeight: 600, marginBottom: 8 }}>Outer Card</Paragraph>
|
||||
<Card variant="warning" style={{ marginBottom: 8 }}>
|
||||
<Paragraph>Nested warning card</Paragraph>
|
||||
</Card>
|
||||
<Card variant="danger">
|
||||
<Paragraph>Nested danger card</Paragraph>
|
||||
</Card>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Default Padding">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Cards have 1em padding by default when no className is passed.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginBottom: 4 }}>
|
||||
defaultPadding=true (or no className)
|
||||
</Paragraph>
|
||||
<Card defaultPadding>
|
||||
<Paragraph>Card with default padding</Paragraph>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Custom Styling",
|
||||
description: "Cards accept custom styles and all div attributes.",
|
||||
relevantProps: ["style"],
|
||||
children: (
|
||||
<Card
|
||||
variant="primary"
|
||||
style={{ borderLeft: "4px solid var(--text-brand)", borderRadius: 4 }}
|
||||
>
|
||||
<Paragraph>Card with custom border accent</Paragraph>
|
||||
</Card>
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginBottom: 4 }}>
|
||||
defaultPadding=false
|
||||
</Paragraph>
|
||||
<Card defaultPadding={false}>
|
||||
<Paragraph style={{ padding: 8 }}>Card without default padding (content adds own)</Paragraph>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Nested Cards">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Cards can be nested for complex layouts.
|
||||
</Paragraph>
|
||||
<Card variant="primary">
|
||||
<Paragraph style={{ fontWeight: 600, marginBottom: 8 }}>Outer Card</Paragraph>
|
||||
<Card variant="warning" style={{ marginBottom: 8 }}>
|
||||
<Paragraph>Nested warning card</Paragraph>
|
||||
</Card>
|
||||
<Card variant="danger">
|
||||
<Paragraph>Nested danger card</Paragraph>
|
||||
</Card>
|
||||
</Card>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Custom Styling">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Cards accept custom className and style props.
|
||||
</Paragraph>
|
||||
<Card
|
||||
variant="primary"
|
||||
style={{
|
||||
borderLeft: "4px solid var(--text-brand)",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Paragraph>Card with custom border accent</Paragraph>
|
||||
</Card>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• variant - "primary" | "warning" | "danger" | "success" | "brand" (default: "primary")</Paragraph>
|
||||
<Paragraph color="text-muted">• defaultPadding - Add 1em padding (default: true if no className)</Paragraph>
|
||||
<Paragraph color="text-muted">• className - Custom CSS class</Paragraph>
|
||||
<Paragraph color="text-muted">• style - Inline styles</Paragraph>
|
||||
<Paragraph color="text-muted">• children - Card content</Paragraph>
|
||||
<Paragraph color="text-muted">• ...restProps - Any other div attributes</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Import">
|
||||
<Paragraph color="text-muted">
|
||||
{"import { Card } from \"@components/Card\";"}
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
]}
|
||||
props={CARD_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>", 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<any>", description: "Icon component displayed before the checkbox." },
|
||||
];
|
||||
|
||||
function BasicDemo() {
|
||||
const [selected, setSelected] = useState<(string | number)[]>(["option1"]);
|
||||
return <ManaCheckboxGroup options={BASIC_OPTIONS} selectedValues={selected} onChange={setSelected} />;
|
||||
}
|
||||
|
||||
function DescriptionDemo() {
|
||||
const [selected, setSelected] = useState<(string | number)[]>(["notifications", "sounds"]);
|
||||
return <ManaCheckboxGroup options={OPTIONS_WITH_DESCRIPTIONS} selectedValues={selected} onChange={setSelected} />;
|
||||
}
|
||||
|
||||
function IconDemo() {
|
||||
const [selected, setSelected] = useState<(string | number)[]>(["item1"]);
|
||||
return <ManaCheckboxGroup options={OPTIONS_WITH_ICONS} selectedValues={selected} onChange={setSelected} />;
|
||||
}
|
||||
|
||||
function MixedDemo() {
|
||||
const [selected, setSelected] = useState<(string | number)[]>(["enabled"]);
|
||||
return <ManaCheckboxGroup options={OPTIONS_MIXED} selectedValues={selected} onChange={setSelected} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic">
|
||||
<ManaCheckboxGroup
|
||||
options={BASIC_OPTIONS}
|
||||
selectedValues={basicSelected}
|
||||
onChange={setBasicSelected}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Descriptions">
|
||||
<ManaCheckboxGroup
|
||||
options={OPTIONS_WITH_DESCRIPTIONS}
|
||||
selectedValues={descSelected}
|
||||
onChange={setDescSelected}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Leading Icons">
|
||||
<ManaCheckboxGroup
|
||||
options={OPTIONS_WITH_ICONS}
|
||||
selectedValues={iconSelected}
|
||||
onChange={setIconSelected}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Disabled Option">
|
||||
<ManaCheckboxGroup
|
||||
options={OPTIONS_MIXED}
|
||||
selectedValues={mixedSelected}
|
||||
onChange={setMixedSelected}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Entire Group Disabled">
|
||||
<ManaCheckboxGroup
|
||||
options={BASIC_OPTIONS}
|
||||
selectedValues={["option1", "option2"]}
|
||||
disabled
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>CheckboxGroup</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• options: CheckboxGroupOption[] - Array of options
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• selectedValues: (string | number)[] - Currently selected values
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (values: (string | number)[]) => void - Called when selection changes
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable entire group
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>CheckboxGroupOption</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• value: string | number - Option value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• label: string - Option label
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• description?: string - Optional description
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable individual option
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• leadingIcon?: React.ComponentType - Icon before checkbox
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="ManaCheckboxGroup"
|
||||
overview="ManaCheckboxGroup renders a group of checkboxes from an options array with multi-select state management. Each option supports labels, descriptions, icons, and individual disabled state. The group can also be disabled entirely."
|
||||
notices={[
|
||||
{ type: "info", children: "CheckboxGroup manages multi-select state internally by delegating to individual ManaCheckbox instances. Use this instead of manually rendering multiple checkboxes when you need shared state tracking." },
|
||||
]}
|
||||
importPath={'import { ManaCheckboxGroup } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Simple group with three options.",
|
||||
children: <BasicDemo />,
|
||||
code: '<ManaCheckboxGroup\n options={[\n { value: "a", label: "Option A" },\n { value: "b", label: "Option B" },\n ]}\n selectedValues={selected}\n onChange={setSelected}\n/>',
|
||||
relevantProps: ["options", "selectedValues", "onChange"],
|
||||
},
|
||||
{
|
||||
title: "With Descriptions",
|
||||
description: "Each option can have a description below its label.",
|
||||
children: <DescriptionDemo />,
|
||||
},
|
||||
{
|
||||
title: "With Leading Icons",
|
||||
description: "Options can display an icon component before the checkbox.",
|
||||
children: <IconDemo />,
|
||||
},
|
||||
{
|
||||
title: "With Disabled Option",
|
||||
description: "Individual options can be disabled while others remain interactive.",
|
||||
children: <MixedDemo />,
|
||||
},
|
||||
{
|
||||
title: "Entire Group Disabled",
|
||||
description: "The disabled prop on the group disables all options.",
|
||||
children: (
|
||||
<ManaCheckboxGroup
|
||||
options={BASIC_OPTIONS}
|
||||
selectedValues={["option1", "option2"]}
|
||||
disabled
|
||||
/>
|
||||
),
|
||||
relevantProps: ["disabled"],
|
||||
},
|
||||
]}
|
||||
props={[
|
||||
{ title: "ManaCheckboxGroup", props: GROUP_PROPS },
|
||||
{ title: "CheckboxGroupOption", props: OPTION_PROPS },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox
|
||||
label="Checked"
|
||||
checked={basicChecked}
|
||||
onChange={setBasicChecked}
|
||||
/>
|
||||
<ManaCheckbox
|
||||
label="Unchecked"
|
||||
checked={basicUnchecked}
|
||||
onChange={setBasicUnchecked}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Description">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox
|
||||
label="Option with description"
|
||||
description="This is a helpful description for the option."
|
||||
checked={withDesc}
|
||||
onChange={setWithDesc}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Label Types">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox
|
||||
label="Primary label (default)"
|
||||
labelType="primary"
|
||||
checked={labelPrimary}
|
||||
onChange={setLabelPrimary}
|
||||
/>
|
||||
<ManaCheckbox
|
||||
label="Secondary label"
|
||||
labelType="secondary"
|
||||
checked={labelSecondary}
|
||||
onChange={setLabelSecondary}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Leading Icon">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox
|
||||
label="With icon"
|
||||
description="This checkbox has a leading icon."
|
||||
leadingIcon={EquicordIcon}
|
||||
checked={withIcon}
|
||||
onChange={setWithIcon}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="States">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox
|
||||
label="Disabled (checked)"
|
||||
checked={true}
|
||||
disabled
|
||||
/>
|
||||
<ManaCheckbox
|
||||
label="Disabled (unchecked)"
|
||||
checked={false}
|
||||
disabled
|
||||
/>
|
||||
<ManaCheckbox
|
||||
label="Display only"
|
||||
checked={true}
|
||||
displayOnly
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Usage Variants">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox
|
||||
label="Single (default)"
|
||||
usageVariant="single"
|
||||
checked={usageSingle}
|
||||
onChange={setUsageSingle}
|
||||
/>
|
||||
<ManaCheckbox
|
||||
label="Indicator"
|
||||
usageVariant="indicator"
|
||||
checked={usageIndicator}
|
||||
onChange={setUsageIndicator}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• checked: boolean - Current checked state
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (checked: boolean) => void - Called when toggled
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• label?: string - Label text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• description?: string - Description below label
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• labelType?: "primary" | "secondary" - Label styling
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• leadingIcon?: React.ComponentType - Icon before checkbox
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• usageVariant?: "single" | "indicator" - Visual style
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable interaction
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• displayOnly?: boolean - Display without interaction
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox label="Checked" checked={checked} onChange={setChecked} />
|
||||
<ManaCheckbox label="Unchecked" checked={unchecked} onChange={setUnchecked} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DescriptionDemo() {
|
||||
const [checked, setChecked] = useState(true);
|
||||
|
||||
return (
|
||||
<ManaCheckbox
|
||||
label="Option with description"
|
||||
description="This is a helpful description for the option."
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LabelTypesDemo() {
|
||||
const [primary, setPrimary] = useState(true);
|
||||
const [secondary, setSecondary] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox label="Primary label (default)" labelType="primary" checked={primary} onChange={setPrimary} />
|
||||
<ManaCheckbox label="Secondary label" labelType="secondary" checked={secondary} onChange={setSecondary} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconDemo() {
|
||||
const [checked, setChecked] = useState(true);
|
||||
|
||||
return (
|
||||
<ManaCheckbox
|
||||
label="With icon"
|
||||
description="This checkbox has a leading icon."
|
||||
leadingIcon={EquicordIcon}
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UsageVariantsDemo() {
|
||||
const [single, setSingle] = useState(true);
|
||||
const [indicator, setIndicator] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox label="Single (default)" usageVariant="single" checked={single} onChange={setSingle} />
|
||||
<ManaCheckbox label="Indicator" usageVariant="indicator" checked={indicator} onChange={setIndicator} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CheckboxTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="ManaCheckbox"
|
||||
overview="ManaCheckbox is Discord's checkbox component with label, description, icon support, and two visual variants. Supports controlled state, disabled and display-only modes, and integrates with CheckboxGroup for multi-select patterns."
|
||||
notices={[
|
||||
{ type: "info", children: "ManaCheckbox is a controlled component. The checked prop must always be provided, and you must update it via onChange to reflect the new state." },
|
||||
]}
|
||||
importPath={'import { ManaCheckbox } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Toggle checkboxes on and off.",
|
||||
children: <BasicDemo />,
|
||||
code: '<ManaCheckbox label="Enable feature" checked={enabled} onChange={setEnabled} />',
|
||||
relevantProps: ["checked", "onChange", "label"],
|
||||
},
|
||||
{
|
||||
title: "With Description",
|
||||
description: "A description renders below the label in smaller text.",
|
||||
children: <DescriptionDemo />,
|
||||
code: '<ManaCheckbox\n label="Notifications"\n description="Receive email notifications for new messages."\n checked={checked}\n onChange={setChecked}\n/>',
|
||||
relevantProps: ["description"],
|
||||
},
|
||||
{
|
||||
title: "Label Types",
|
||||
description: "Primary uses normal text styling, secondary uses a muted, lighter appearance.",
|
||||
children: <LabelTypesDemo />,
|
||||
relevantProps: ["labelType"],
|
||||
},
|
||||
{
|
||||
title: "With Leading Icon",
|
||||
description: "An icon component displayed before the checkbox indicator.",
|
||||
children: <IconDemo />,
|
||||
code: '<ManaCheckbox\n label="With icon"\n leadingIcon={BellIcon}\n checked={checked}\n onChange={setChecked}\n/>',
|
||||
relevantProps: ["leadingIcon"],
|
||||
},
|
||||
{
|
||||
title: "States",
|
||||
description: "Disabled prevents interaction. Display only renders without any interactive behavior.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaCheckbox label="Disabled (checked)" checked={true} disabled />
|
||||
<ManaCheckbox label="Disabled (unchecked)" checked={false} disabled />
|
||||
<ManaCheckbox label="Display only" checked={true} displayOnly />
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["disabled", "displayOnly"],
|
||||
},
|
||||
{
|
||||
title: "Usage Variants",
|
||||
description: "Single has full padding and spacing for standalone use. Indicator is compact for inline contexts.",
|
||||
children: <UsageVariantsDemo />,
|
||||
relevantProps: ["usageVariant"],
|
||||
},
|
||||
]}
|
||||
props={CHECKBOX_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="All Variants">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
All 24 chip variants: 8 colors × 3 shades.
|
||||
</Paragraph>
|
||||
{CHIP_SHADES.map(shade => (
|
||||
<div key={shade} style={{ marginBottom: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8, fontWeight: 600 }}>
|
||||
{shade}
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
{CHIP_COLORS.map(color => (
|
||||
<Chip
|
||||
key={`${color}${shade}`}
|
||||
text={color}
|
||||
variant={`${color}${shade}` as any}
|
||||
/>
|
||||
<DocPage
|
||||
componentName="Chip"
|
||||
overview="Chip is a small label component used for tags, statuses, and categories. It renders text in an eyebrow style with a colored background. Supports 24 variants across 8 colors and 3 shade levels."
|
||||
importPath={'import { Chip } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "All Variants",
|
||||
description: "All 24 chip variants: 8 colors times 3 shades.",
|
||||
children: (
|
||||
<>
|
||||
{CHIP_SHADES.map(shade => (
|
||||
<div key={shade} style={{ marginBottom: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8, fontWeight: 600 }}>
|
||||
{shade}
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
{CHIP_COLORS.map(color => (
|
||||
<Chip
|
||||
key={`${color}${shade}`}
|
||||
text={color}
|
||||
variant={`${color}${shade}` as any}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
code: '<Chip text="New" variant="blurpleLight" />',
|
||||
relevantProps: ["variant"],
|
||||
},
|
||||
{
|
||||
title: "Use Cases",
|
||||
description: "Common patterns for status labels and tags.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<Chip text="New" variant="blurpleLight" />
|
||||
<Chip text="Beta" variant="yellowMedium" />
|
||||
<Chip text="Deprecated" variant="redLight" />
|
||||
<Chip text="Stable" variant="greenLight" />
|
||||
<Chip text="Premium" variant="pinkMedium" />
|
||||
<Chip text="Experimental" variant="orangeLight" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Use Cases">
|
||||
<div className="vc-compfinder-grid">
|
||||
<Chip text="New" variant="blurpleLight" />
|
||||
<Chip text="Beta" variant="yellowMedium" />
|
||||
<Chip text="Deprecated" variant="redLight" />
|
||||
<Chip text="Stable" variant="greenLight" />
|
||||
<Chip text="Premium" variant="pinkMedium" />
|
||||
<Chip text="Experimental" variant="orangeLight" />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• text: string - Text to display inside the chip
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• variant?: ChipVariant - Color variant (default: "grayLight")
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 8 }}>
|
||||
<strong>Variant Format:</strong> {"{color}{shade}"} where:
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• Colors: blurple, purple, green, orange, yellow, pink, red, gray
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• Shades: Light, Medium, Dark
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
props={CHIP_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>", internal: true, description: "Props forwarded to the focus management wrapper." },
|
||||
{ name: "innerRef", type: "Ref<HTMLElement>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic Clickable">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Default clickable element. Click count: {clickCount}
|
||||
</Paragraph>
|
||||
<Clickable
|
||||
onClick={() => setClickCount(c => c + 1)}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
Click me!
|
||||
</Clickable>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Custom Tag">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Clickable can render as different HTML elements using the tag prop.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Clickable
|
||||
tag="div"
|
||||
onClick={() => setLastAction("Clicked div")}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-tertiary)",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
tag="div"
|
||||
</Clickable>
|
||||
<Clickable
|
||||
tag="span"
|
||||
onClick={() => setLastAction("Clicked span")}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-tertiary)",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
tag="span"
|
||||
</Clickable>
|
||||
<Clickable
|
||||
tag="button"
|
||||
onClick={() => setLastAction("Clicked button")}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-tertiary)",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer",
|
||||
border: "none",
|
||||
color: "inherit"
|
||||
}}
|
||||
>
|
||||
tag="button"
|
||||
</Clickable>
|
||||
</div>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 8 }}>
|
||||
Last action: {lastAction}
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Role and TabIndex">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Accessibility props for keyboard navigation.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Clickable
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setLastAction("Role: button")}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
role="button" tabIndex=0
|
||||
</Clickable>
|
||||
<Clickable
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
onClick={() => setLastAction("Role: link")}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
role="link" tabIndex=0
|
||||
</Clickable>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Ignore Key Press">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
When ignoreKeyPress is true, Enter/Space won't trigger onClick.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Clickable
|
||||
tabIndex={0}
|
||||
onClick={() => setLastAction("Normal (keys work)")}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
Normal (try Enter/Space)
|
||||
</Clickable>
|
||||
<Clickable
|
||||
tabIndex={0}
|
||||
ignoreKeyPress
|
||||
onClick={() => setLastAction("Ignored (keys blocked)")}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
ignoreKeyPress (keys blocked)
|
||||
</Clickable>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Without onClick">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
When no onClick is provided, renders as non-interactive element.
|
||||
</Paragraph>
|
||||
<Clickable
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-modifier-accent)",
|
||||
borderRadius: 4
|
||||
}}
|
||||
>
|
||||
Non-interactive (no onClick)
|
||||
</Clickable>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Styled Examples">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Clickable with different visual styles.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Clickable
|
||||
onClick={() => setLastAction("Card clicked")}
|
||||
style={{
|
||||
padding: 16,
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
width: 120,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 24, marginBottom: 4 }}>📁</div>
|
||||
<div>Card Style</div>
|
||||
</Clickable>
|
||||
<Clickable
|
||||
onClick={() => setLastAction("Chip clicked")}
|
||||
style={{
|
||||
padding: "4px 12px",
|
||||
backgroundColor: "var(--brand-experiment)",
|
||||
borderRadius: 16,
|
||||
cursor: "pointer",
|
||||
color: "white"
|
||||
}}
|
||||
>
|
||||
Chip Style
|
||||
</Clickable>
|
||||
<Clickable
|
||||
onClick={() => setLastAction("List item clicked")}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
cursor: "pointer",
|
||||
width: 200
|
||||
}}
|
||||
>
|
||||
List Item Style →
|
||||
</Clickable>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Clickable</strong> - Interactive element wrapper
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• tag?: keyof JSX.IntrinsicElements - HTML element to render (default: div)</Paragraph>
|
||||
<Paragraph color="text-muted">• onClick?: MouseEventHandler - Click handler</Paragraph>
|
||||
<Paragraph color="text-muted">• onKeyPress?: KeyboardEventHandler - Key press handler</Paragraph>
|
||||
<Paragraph color="text-muted">• focusProps?: object - Props for FocusLock wrapper</Paragraph>
|
||||
<Paragraph color="text-muted">• innerRef?: Ref - Ref to the inner element</Paragraph>
|
||||
<Paragraph color="text-muted">• role?: string - ARIA role attribute</Paragraph>
|
||||
<Paragraph color="text-muted">• tabIndex?: number - Tab index for keyboard navigation</Paragraph>
|
||||
<Paragraph color="text-muted">• ignoreKeyPress?: boolean - Disable Enter/Space triggering onClick</Paragraph>
|
||||
<Paragraph color="text-muted">• href?: string - Optional href (affects key handling)</Paragraph>
|
||||
<Paragraph color="text-muted">• className?: string - CSS class name</Paragraph>
|
||||
<Paragraph color="text-muted">• children?: ReactNode - Child elements</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Click count: {count}</Paragraph>
|
||||
<Clickable onClick={() => setCount(c => c + 1)} style={boxStyle}>
|
||||
Click me!
|
||||
</Clickable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TagDemo() {
|
||||
const [last, setLast] = useState("None");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Clickable tag="div" onClick={() => setLast("div")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)" }}>
|
||||
tag="div"
|
||||
</Clickable>
|
||||
<Clickable tag="span" onClick={() => setLast("span")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)" }}>
|
||||
tag="span"
|
||||
</Clickable>
|
||||
<Clickable tag="button" onClick={() => setLast("button")} style={{ ...boxStyle, backgroundColor: "var(--background-tertiary)", border: "none", color: "inherit" }}>
|
||||
tag="button"
|
||||
</Clickable>
|
||||
</div>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 8 }}>Last clicked: {last}</Paragraph>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyPressDemo() {
|
||||
const [last, setLast] = useState("None");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Clickable tabIndex={0} onClick={() => setLast("Normal (keys work)")} style={boxStyle}>
|
||||
Normal (try Enter/Space)
|
||||
</Clickable>
|
||||
<Clickable tabIndex={0} ignoreKeyPress onClick={() => setLast("Keys blocked")} style={boxStyle}>
|
||||
ignoreKeyPress (keys blocked)
|
||||
</Clickable>
|
||||
</div>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 8 }}>Last action: {last}</Paragraph>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClickableTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="Clickable"
|
||||
overview="Clickable is a generic interactive wrapper that adds click and keyboard handling to any HTML element. It handles Enter/Space key presses, integrates with Discord's focus management context, and gracefully degrades to a non-interactive element when no onClick is provided. It's a class component with configurable tag, role, and tabIndex defaults."
|
||||
notices={[
|
||||
{ type: "positive", children: 'Use Clickable instead of adding onClick to plain divs. It handles keyboard accessibility, focus management, and proper ARIA role="button" automatically.' },
|
||||
]}
|
||||
importPath={'import { Clickable } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Default clickable element. Renders as a div with role=\"button\" and tabIndex=0.",
|
||||
children: <BasicDemo />,
|
||||
code: "<Clickable onClick={() => doSomething()} style={myStyles}>\n Click me!\n</Clickable>",
|
||||
relevantProps: ["onClick", "children"],
|
||||
},
|
||||
{
|
||||
title: "Custom Tag",
|
||||
description: "Clickable can render as any HTML element using the tag prop.",
|
||||
children: <TagDemo />,
|
||||
code: '<Clickable tag="button" onClick={handleClick}>\n I am a button element\n</Clickable>',
|
||||
relevantProps: ["tag"],
|
||||
},
|
||||
{
|
||||
title: "Role and TabIndex",
|
||||
description: "Accessibility props for keyboard navigation. Defaults to role=\"button\" and tabIndex=0.",
|
||||
children: (
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Clickable role="button" tabIndex={0} onClick={() => { }} style={boxStyle}>
|
||||
role="button"
|
||||
</Clickable>
|
||||
<Clickable role="link" tabIndex={0} onClick={() => { }} style={boxStyle}>
|
||||
role="link"
|
||||
</Clickable>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["role", "tabIndex"],
|
||||
},
|
||||
{
|
||||
title: "Ignore Key Press",
|
||||
description: "When ignoreKeyPress is true, Enter and Space will not trigger onClick. Only mouse clicks work.",
|
||||
children: <KeyPressDemo />,
|
||||
relevantProps: ["ignoreKeyPress"],
|
||||
},
|
||||
{
|
||||
title: "Non-Interactive",
|
||||
description: "When no onClick is provided, renders without keyboard handling or interactive behavior.",
|
||||
children: (
|
||||
<Clickable style={{ padding: "8px 16px", backgroundColor: "var(--background-modifier-accent)", borderRadius: 4 }}>
|
||||
Non-interactive (no onClick)
|
||||
</Clickable>
|
||||
),
|
||||
},
|
||||
]}
|
||||
props={CLICKABLE_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={MarkupClasses.markup}>
|
||||
{Parser.parse(codeBlock)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineCode({ children }: { children: React.ReactNode; }) {
|
||||
return (
|
||||
<span className={MarkupClasses.markup}>
|
||||
<code className="inline">{children}</code>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Inline Code">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Use <InlineCode>InlineCode</InlineCode> component for inline code snippets.
|
||||
</Paragraph>
|
||||
<Paragraph>I really like <InlineCode>cats</InlineCode> so yeah</Paragraph>
|
||||
<Paragraph>Use <InlineCode>console.log()</InlineCode> to debug your code.</Paragraph>
|
||||
<Paragraph>
|
||||
Combine <InlineCode>const</InlineCode> with <InlineCode>let</InlineCode> and
|
||||
use <InlineCode>===</InlineCode> for strict equality.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="JavaScript">
|
||||
<CodeBlock content={JS_EXAMPLE} language="js" />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="TypeScript">
|
||||
<CodeBlock content={TS_EXAMPLE} language="ts" />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="CSS">
|
||||
<CodeBlock content={CSS_EXAMPLE} language="css" />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Python">
|
||||
<CodeBlock content={PYTHON_EXAMPLE} language="py" />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="JSON">
|
||||
<CodeBlock content={JSON_EXAMPLE} language="json" />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Bash">
|
||||
<CodeBlock content={BASH_EXAMPLE} language="bash" />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Plain Code Block">
|
||||
<CodeBlock content={PLAIN_EXAMPLE} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Usage">
|
||||
<Paragraph color="text-muted">
|
||||
Use <InlineCode>Parser.parse()</InlineCode> from <InlineCode>@webpack/common</InlineCode> to
|
||||
render markdown with code blocks. Wrap in <InlineCode>markup</InlineCode> class for proper styling.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="CodeBlock"
|
||||
overview="CodeBlock renders syntax-highlighted code using Discord's built-in markdown parser. InlineCode renders small code snippets inline within text. Both are Vencord components wrapping Discord's markup system."
|
||||
importPath={'import { CodeBlock } from "@components/CodeBlock";\nimport { InlineCode } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Inline Code",
|
||||
description: "Small code snippets rendered inline within text.",
|
||||
children: (
|
||||
<>
|
||||
<Paragraph>I really like <InlineCode>cats</InlineCode> so yeah</Paragraph>
|
||||
<Paragraph>Use <InlineCode>console.log()</InlineCode> to debug your code.</Paragraph>
|
||||
<Paragraph>
|
||||
Combine <InlineCode>const</InlineCode> with <InlineCode>let</InlineCode> and
|
||||
use <InlineCode>===</InlineCode> for strict equality.
|
||||
</Paragraph>
|
||||
</>
|
||||
),
|
||||
code: "<Paragraph>Use <InlineCode>console.log()</InlineCode> to debug.</Paragraph>",
|
||||
},
|
||||
{
|
||||
title: "JavaScript",
|
||||
children: <CodeBlock content={JS_EXAMPLE} lang="js" />,
|
||||
code: '<CodeBlock content={jsCode} lang="js" />',
|
||||
relevantProps: ["content", "lang"],
|
||||
},
|
||||
{
|
||||
title: "TypeScript",
|
||||
children: <CodeBlock content={TS_EXAMPLE} lang="ts" />,
|
||||
},
|
||||
{
|
||||
title: "CSS",
|
||||
children: <CodeBlock content={CSS_EXAMPLE} lang="css" />,
|
||||
},
|
||||
{
|
||||
title: "Python",
|
||||
children: <CodeBlock content={PYTHON_EXAMPLE} lang="py" />,
|
||||
},
|
||||
{
|
||||
title: "JSON",
|
||||
children: <CodeBlock content={JSON_EXAMPLE} lang="json" />,
|
||||
},
|
||||
{
|
||||
title: "Plain Text",
|
||||
description: "Use an empty string for lang to render without syntax highlighting.",
|
||||
children: <CodeBlock content={"This is plain text\nwithout any syntax highlighting."} lang="" />,
|
||||
code: '<CodeBlock content="plain text" lang="" />',
|
||||
},
|
||||
]}
|
||||
props={[
|
||||
{ title: "CodeBlock", props: CODEBLOCK_PROPS },
|
||||
{ title: "InlineCode", props: INLINECODE_PROPS },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic ColorPicker">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Click the swatch to open a full color picker popout. Selected: {intToHex(basicColor)}
|
||||
</Paragraph>
|
||||
<ColorPicker
|
||||
color={basicColor}
|
||||
onChange={setBasicColor}
|
||||
showEyeDropper={false}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
<>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Selected: {intToHex(color)}</Paragraph>
|
||||
<ColorPicker color={color} onChange={setColor} showEyeDropper={false} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
<SectionWrapper title="With Eye Dropper">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Includes the eye dropper tool for picking colors from screen.
|
||||
</Paragraph>
|
||||
<ColorPicker
|
||||
color={basicColor}
|
||||
onChange={setBasicColor}
|
||||
showEyeDropper={true}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
function EyeDropperDemo() {
|
||||
const [color, setColor] = useState(DEFAULT_COLOR);
|
||||
|
||||
<SectionWrapper title="With Suggested Colors">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Popout shows suggested color swatches. Selected: {intToHex(suggestedColor)}
|
||||
</Paragraph>
|
||||
<ColorPicker
|
||||
color={suggestedColor}
|
||||
onChange={setSuggestedColor}
|
||||
showEyeDropper={false}
|
||||
suggestedColors={[
|
||||
"#1ABC9C", "#2ECC71", "#3498DB", "#9B59B6", "#E91E63",
|
||||
"#F1C40F", "#E67E22", "#E74C3C", "#95A5A6", "#607D8B",
|
||||
]}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
return <ColorPicker color={color} onChange={setColor} showEyeDropper />;
|
||||
}
|
||||
|
||||
<SectionWrapper title="Disabled ColorPicker">
|
||||
<ColorPicker
|
||||
color={0x5865F2}
|
||||
onChange={() => { }}
|
||||
disabled
|
||||
/>
|
||||
</SectionWrapper>
|
||||
function SuggestedDemo() {
|
||||
const [color, setColor] = useState(0xE74C3C);
|
||||
|
||||
<SectionWrapper title="ColorSwatch - Basic">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Individual color swatch buttons. Click to select.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
{PresetColors.slice(0, 10).map(color => (
|
||||
<ColorSwatch
|
||||
key={color}
|
||||
color={color}
|
||||
isSelected={swatchSelected === color}
|
||||
onClick={() => setSwatchSelected(color)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
return (
|
||||
<>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Selected: {intToHex(color)}</Paragraph>
|
||||
<ColorPicker
|
||||
color={color}
|
||||
onChange={setColor}
|
||||
showEyeDropper={false}
|
||||
suggestedColors={["#1ABC9C", "#2ECC71", "#3498DB", "#9B59B6", "#E91E63", "#F1C40F", "#E67E22", "#E74C3C"]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
<SectionWrapper title="ColorSwatch - States">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Different swatch states: default, custom, selected, disabled, no color.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ColorSwatch color={0x5865F2} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Normal</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ColorSwatch color={0x5865F2} isSelected />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Selected</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ColorSwatch color={0x5865F2} isDefault />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Default</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ColorSwatch color={0x5865F2} isCustom />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Custom</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ColorSwatch color={0x5865F2} disabled />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>Disabled</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ColorSwatch />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>No Color</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
function SwatchSelectDemo() {
|
||||
const [selected, setSelected] = useState(0x2ECC71);
|
||||
|
||||
<SectionWrapper title="ColorSwatch - Gradient">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Gradient swatches with customizable angle.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<ColorSwatch
|
||||
isGradient
|
||||
gradientStart={0xE91E63}
|
||||
gradientEnd={0x9B59B6}
|
||||
gradientDegrees={90}
|
||||
/>
|
||||
<ColorSwatch
|
||||
isGradient
|
||||
gradientStart={0x3498DB}
|
||||
gradientEnd={0x2ECC71}
|
||||
gradientDegrees={135}
|
||||
/>
|
||||
<ColorSwatch
|
||||
isGradient
|
||||
gradientStart={0xF1C40F}
|
||||
gradientEnd={0xE74C3C}
|
||||
gradientDegrees={180}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="DefaultColorButton">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
The "default" button used in ColorPickerWithSwatches. Shows checkmark when selected.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
|
||||
<DefaultColorButton
|
||||
color={DEFAULT_COLOR}
|
||||
value={defaultBtnColor}
|
||||
onChange={setDefaultBtnColor}
|
||||
/>
|
||||
<Paragraph color="text-muted">Selected: {defaultBtnColor === DEFAULT_COLOR ? "Yes" : "No"}</Paragraph>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="CustomColorButton">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
The "custom" button for custom color selection. Shows dropper icon.
|
||||
</Paragraph>
|
||||
<CustomColorButton
|
||||
customColor={customBtnColor}
|
||||
value={customBtnColor}
|
||||
presets={[...PresetColors]}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="ColorPickerWithSwatches">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Full swatch grid with preset colors. Selected: {intToHex(swatchColor)}
|
||||
</Paragraph>
|
||||
<ColorPickerWithSwatches
|
||||
defaultColor={DEFAULT_COLOR}
|
||||
colors={[...PresetColors]}
|
||||
value={swatchColor}
|
||||
onChange={setSwatchColor}
|
||||
renderDefaultButton={() => null}
|
||||
renderCustomButton={() => null}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Default & Custom Buttons">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Full picker with default reset button and custom color picker. Selected: {intToHex(customSwatchColor)}
|
||||
</Paragraph>
|
||||
<ColorPickerWithSwatches
|
||||
defaultColor={DEFAULT_COLOR}
|
||||
customColor={customSwatchColor}
|
||||
colors={[...PresetColors]}
|
||||
value={customSwatchColor}
|
||||
onChange={setCustomSwatchColor}
|
||||
renderDefaultButton={props => (
|
||||
<DefaultColorButton {...props} />
|
||||
)}
|
||||
renderCustomButton={props => (
|
||||
<ColorPicker
|
||||
color={props.customColor ?? DEFAULT_COLOR}
|
||||
onChange={setCustomSwatchColor}
|
||||
showEyeDropper={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>ColorPicker</strong> - Full color picker with popout
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• color: number - Current color as integer</Paragraph>
|
||||
<Paragraph color="text-muted">• onChange: (color: number) ={">"} void - Change callback</Paragraph>
|
||||
<Paragraph color="text-muted">• onClose?: () ={">"} void - Called when picker closes</Paragraph>
|
||||
<Paragraph color="text-muted">• suggestedColors?: string[] - Hex color strings for suggestions</Paragraph>
|
||||
<Paragraph color="text-muted">• disabled?: boolean - Disable interaction</Paragraph>
|
||||
<Paragraph color="text-muted">• label?: ReactNode - Optional label next to swatch</Paragraph>
|
||||
<Paragraph color="text-muted">• colorPickerMiddle?: ReactNode - Content in middle of popout</Paragraph>
|
||||
<Paragraph color="text-muted">• colorPickerFooter?: ReactNode - Content in footer of popout</Paragraph>
|
||||
<Paragraph color="text-muted">• showEyeDropper?: boolean - Show eye dropper tool</Paragraph>
|
||||
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>ColorSwatch</strong> - Individual color swatch button
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• color?: number - Color value (null for "no color")</Paragraph>
|
||||
<Paragraph color="text-muted">• isDefault?: boolean - Larger "default" style</Paragraph>
|
||||
<Paragraph color="text-muted">• isCustom?: boolean - Shows dropper icon</Paragraph>
|
||||
<Paragraph color="text-muted">• isSelected?: boolean - Shows checkmark</Paragraph>
|
||||
<Paragraph color="text-muted">• disabled?: boolean - Disable interaction</Paragraph>
|
||||
<Paragraph color="text-muted">• onClick?: (color) ={">"} void - Click callback</Paragraph>
|
||||
<Paragraph color="text-muted">• isGradient?: boolean - Gradient mode</Paragraph>
|
||||
<Paragraph color="text-muted">• gradientStart/End?: number - Gradient colors</Paragraph>
|
||||
<Paragraph color="text-muted">• gradientDegrees?: number - Gradient angle (default 180)</Paragraph>
|
||||
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>ColorPickerWithSwatches</strong> - Swatch grid with render props
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• defaultColor: number - Default/reset color</Paragraph>
|
||||
<Paragraph color="text-muted">• customColor?: number - Current custom color</Paragraph>
|
||||
<Paragraph color="text-muted">• colors: number[] - Array of preset colors</Paragraph>
|
||||
<Paragraph color="text-muted">• value: number - Selected color</Paragraph>
|
||||
<Paragraph color="text-muted">• onChange: (color: number) ={">"} void - Change callback</Paragraph>
|
||||
<Paragraph color="text-muted">• renderDefaultButton?: (props) ={">"} ReactNode</Paragraph>
|
||||
<Paragraph color="text-muted">• renderCustomButton?: (props) ={">"} ReactNode</Paragraph>
|
||||
<Paragraph color="text-muted">• isGradient?: boolean - Gradient mode</Paragraph>
|
||||
<Paragraph color="text-muted">• gradientDegrees?: number - Gradient angle</Paragraph>
|
||||
<Paragraph color="text-muted">• allowBlackCustomColor?: boolean - Allow #000000</Paragraph>
|
||||
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>Color Conversion</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• Hex to int: parseInt(hex.replace("#", ""), 16)</Paragraph>
|
||||
<Paragraph color="text-muted">• Int to hex: "#" + color.toString(16).padStart(6, "0")</Paragraph>
|
||||
</SectionWrapper>
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
{PresetColors.slice(0, 10).map(color => (
|
||||
<ColorSwatch key={color} color={color} isSelected={selected === color} onClick={() => setSelected(color)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SwatchesGridDemo() {
|
||||
const [color, setColor] = useState(0x3498DB);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Selected: {intToHex(color)}</Paragraph>
|
||||
<ColorPickerWithSwatches
|
||||
defaultColor={DEFAULT_COLOR}
|
||||
colors={[...PresetColors]}
|
||||
value={color}
|
||||
onChange={setColor}
|
||||
renderDefaultButton={() => null}
|
||||
renderCustomButton={() => null}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FullPickerDemo() {
|
||||
const [color, setColor] = useState(0x9B59B6);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Selected: {intToHex(color)}</Paragraph>
|
||||
<ColorPickerWithSwatches
|
||||
defaultColor={DEFAULT_COLOR}
|
||||
customColor={color}
|
||||
colors={[...PresetColors]}
|
||||
value={color}
|
||||
onChange={setColor}
|
||||
renderDefaultButton={props => <DefaultColorButton {...props} />}
|
||||
renderCustomButton={() => (
|
||||
<ColorPicker color={color} onChange={setColor} showEyeDropper={false} />
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ColorPickerTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="ColorPicker"
|
||||
overview="Discord's color picker system includes a full-featured ColorPicker popout, individual ColorSwatch buttons, and a ColorPickerWithSwatches grid. Colors are represented as integers (e.g. 0x5865F2). Convert with parseInt(hex, 16) and color.toString(16)."
|
||||
notices={[
|
||||
{ type: "info", children: 'Colors are represented as integers, not hex strings. Convert with parseInt(hex, 16) to get an integer and color.toString(16).padStart(6, "0") to get a hex string.' },
|
||||
]}
|
||||
importPath={'import { ColorPicker, ColorPickerWithSwatches, ColorSwatch, DefaultColorButton, CustomColorButton } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic ColorPicker",
|
||||
description: "Click the swatch to open a full color picker popout.",
|
||||
children: <BasicPickerDemo />,
|
||||
code: "<ColorPicker color={color} onChange={setColor} showEyeDropper={false} />",
|
||||
relevantProps: ["color", "onChange"],
|
||||
},
|
||||
{
|
||||
title: "With Eye Dropper",
|
||||
description: "Includes the eye dropper tool for picking colors from screen.",
|
||||
children: <EyeDropperDemo />,
|
||||
relevantProps: ["showEyeDropper"],
|
||||
},
|
||||
{
|
||||
title: "With Suggested Colors",
|
||||
description: "Shows suggested color swatches inside the popout.",
|
||||
children: <SuggestedDemo />,
|
||||
code: '<ColorPicker\n color={color}\n onChange={setColor}\n suggestedColors={["#1ABC9C", "#2ECC71", "#3498DB"]}\n/>',
|
||||
relevantProps: ["suggestedColors"],
|
||||
},
|
||||
{
|
||||
title: "Disabled",
|
||||
children: <ColorPicker color={0x5865F2} onChange={() => { }} disabled />,
|
||||
relevantProps: ["disabled"],
|
||||
},
|
||||
{
|
||||
title: "Color Swatches",
|
||||
description: "Individual swatch buttons. Click to select.",
|
||||
children: <SwatchSelectDemo />,
|
||||
code: "<ColorSwatch color={0x5865F2} isSelected={selected === 0x5865F2} onClick={() => setSelected(0x5865F2)} />",
|
||||
},
|
||||
{
|
||||
title: "Swatch States",
|
||||
description: "Normal, selected, default, custom, disabled, and no-color states.",
|
||||
children: (
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
|
||||
{([
|
||||
["Normal", {}],
|
||||
["Selected", { isSelected: true }],
|
||||
["Default", { isDefault: true }],
|
||||
["Custom", { isCustom: true }],
|
||||
["Disabled", { disabled: true }],
|
||||
] as const).map(([label, props]) => (
|
||||
<div key={label} style={{ textAlign: "center" }}>
|
||||
<ColorSwatch color={0x5865F2} {...props} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>{label}</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ColorSwatch />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>No Color</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["isSelected", "isDefault", "isCustom", "disabled"],
|
||||
},
|
||||
{
|
||||
title: "Gradient Swatches",
|
||||
description: "Gradient swatches with customizable angle.",
|
||||
children: (
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<ColorSwatch isGradient gradientStart={0xE91E63} gradientEnd={0x9B59B6} gradientDegrees={90} />
|
||||
<ColorSwatch isGradient gradientStart={0x3498DB} gradientEnd={0x2ECC71} gradientDegrees={135} />
|
||||
<ColorSwatch isGradient gradientStart={0xF1C40F} gradientEnd={0xE74C3C} gradientDegrees={180} />
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["isGradient", "gradientStart", "gradientEnd", "gradientDegrees"],
|
||||
},
|
||||
{
|
||||
title: "Swatch Grid",
|
||||
description: "ColorPickerWithSwatches renders a grid of preset color swatches.",
|
||||
children: <SwatchesGridDemo />,
|
||||
code: "<ColorPickerWithSwatches\n defaultColor={DEFAULT_COLOR}\n colors={presetColors}\n value={color}\n onChange={setColor}\n renderDefaultButton={() => 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: <FullPickerDemo />,
|
||||
relevantProps: ["renderDefaultButton", "renderCustomButton"],
|
||||
},
|
||||
]}
|
||||
props={[
|
||||
{ title: "ColorPicker", props: PICKER_PROPS },
|
||||
{ title: "ColorSwatch", props: SWATCH_PROPS },
|
||||
{ title: "ColorPickerWithSwatches", props: SWATCHES_PROPS },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>("");
|
||||
const [multiValue, setMultiValue] = useState<string[]>([]);
|
||||
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<string>("");
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Combobox">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Searchable dropdown with render props pattern. Children is a function
|
||||
that receives the search query and returns filterable items.
|
||||
</Paragraph>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={showDemo ? "Hide Demo" : "Show Demo"}
|
||||
onClick={() => setShowDemo(!showDemo)}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
{showDemo && (
|
||||
<>
|
||||
<SectionWrapper title="Single Selection">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Selected: {singleValue || "None"}
|
||||
</Paragraph>
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<ManaCombobox
|
||||
placeholder="Search fruits..."
|
||||
value={singleValue}
|
||||
onChange={v => 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 => (
|
||||
<ComboboxItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={singleValue === item.id}
|
||||
onClick={() => setSingleValue(item.id)}
|
||||
/>
|
||||
))}
|
||||
</ManaCombobox>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Multi Selection">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Selected: {multiValue.length ? multiValue.join(", ") : "None"}
|
||||
</Paragraph>
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<ManaCombobox
|
||||
placeholder="Search fruits..."
|
||||
value={multiValue}
|
||||
onChange={v => setMultiValue(v as string[])}
|
||||
multiSelect
|
||||
aria-label="Select fruits"
|
||||
maxVisibleItems={5}
|
||||
>
|
||||
{query => filterItems(query).map(item => (
|
||||
<ComboboxItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={multiValue.includes(item.id)}
|
||||
onClick={() => {
|
||||
if (multiValue.includes(item.id)) {
|
||||
setMultiValue(multiValue.filter(v => v !== item.id));
|
||||
} else {
|
||||
setMultiValue([...multiValue, item.id]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ManaCombobox>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• placeholder - Input placeholder text</Paragraph>
|
||||
<Paragraph color="text-muted">• value - Selected value(s)</Paragraph>
|
||||
<Paragraph color="text-muted">• onChange - Selection change callback</Paragraph>
|
||||
<Paragraph color="text-muted">• multiSelect - Enable multiple selection</Paragraph>
|
||||
<Paragraph color="text-muted">• autoFocus - Focus input on mount</Paragraph>
|
||||
<Paragraph color="text-muted">• maxVisibleItems - Max items before scroll (default 5)</Paragraph>
|
||||
<Paragraph color="text-muted">• emptyStateHeader - Header when no results</Paragraph>
|
||||
<Paragraph color="text-muted">• emptyStateText - Text when no results</Paragraph>
|
||||
<Paragraph color="text-muted">• children - Function: (query: string) => ReactNode[]</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Selected: {SAMPLE_ITEMS.find(i => i.id === value)?.label ?? "None"}
|
||||
</Paragraph>
|
||||
<ManaCombobox
|
||||
placeholder="Search fruits..."
|
||||
value={value}
|
||||
onChange={v => 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 => (
|
||||
<ComboboxItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={value === item.id}
|
||||
onClick={() => setValue(item.id)}
|
||||
/>
|
||||
))}
|
||||
</ManaCombobox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiSelectDemo() {
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Selected: {values.length ? values.map(v => SAMPLE_ITEMS.find(i => i.id === v)?.label).join(", ") : "None"}
|
||||
</Paragraph>
|
||||
<ManaCombobox
|
||||
placeholder="Search fruits..."
|
||||
value={values}
|
||||
onChange={v => setValues(v as string[])}
|
||||
multiSelect
|
||||
aria-label="Select fruits"
|
||||
maxVisibleItems={5}
|
||||
>
|
||||
{query => filterItems(query).map(item => (
|
||||
<ComboboxItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={values.includes(item.id)}
|
||||
onClick={() => {
|
||||
if (values.includes(item.id)) {
|
||||
setValues(values.filter(v => v !== item.id));
|
||||
} else {
|
||||
setValues([...values, item.id]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ManaCombobox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyStateDemo() {
|
||||
return (
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<ManaCombobox
|
||||
placeholder="Type something that won't match..."
|
||||
value=""
|
||||
onChange={() => { }}
|
||||
aria-label="Empty state demo"
|
||||
emptyStateHeader="Nothing found"
|
||||
emptyStateText="We couldn't find what you're looking for."
|
||||
>
|
||||
{() => []}
|
||||
</ManaCombobox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ComboboxTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="ManaCombobox"
|
||||
overview="ManaCombobox is Discord's searchable dropdown component using a render props pattern. Children is a function that receives the search query and returns filterable items. Supports single and multi-select modes, custom empty states, and external query tracking."
|
||||
notices={[
|
||||
{ type: "warn", children: "ManaCombobox uses a render props pattern where children is a function. You are responsible for filtering items based on the query parameter. The component does not filter automatically." },
|
||||
]}
|
||||
importPath={'import { ManaCombobox } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Single Selection",
|
||||
description: "Basic combobox with single item selection. Type to filter the list.",
|
||||
children: <SingleSelectDemo />,
|
||||
code: `<ManaCombobox
|
||||
placeholder="Search fruits..."
|
||||
value={value}
|
||||
onChange={v => 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 => (
|
||||
<ComboboxItem key={item.id} item={item} />
|
||||
))}
|
||||
</ManaCombobox>`,
|
||||
relevantProps: ["value", "onChange", "placeholder", "maxVisibleItems"],
|
||||
},
|
||||
{
|
||||
title: "Multi Selection",
|
||||
description: "Enable multiSelect to allow choosing multiple items at once.",
|
||||
children: <MultiSelectDemo />,
|
||||
code: `<ManaCombobox
|
||||
placeholder="Search fruits..."
|
||||
value={values}
|
||||
onChange={v => setValues(v as string[])}
|
||||
multiSelect
|
||||
aria-label="Select fruits"
|
||||
>
|
||||
{query => filterItems(query).map(item => (
|
||||
<ComboboxItem key={item.id} item={item} />
|
||||
))}
|
||||
</ManaCombobox>`,
|
||||
relevantProps: ["multiSelect"],
|
||||
},
|
||||
{
|
||||
title: "Empty State",
|
||||
description: "Custom header and text shown when the children function returns an empty array.",
|
||||
children: <EmptyStateDemo />,
|
||||
relevantProps: ["emptyStateHeader", "emptyStateText"],
|
||||
},
|
||||
]}
|
||||
props={COMBOBOX_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<hr>", description: "Any other standard hr element attributes." },
|
||||
];
|
||||
|
||||
export default function DividerTab() {
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Vencord Divider">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Simple horizontal rule from @components/Divider.
|
||||
</Paragraph>
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<Paragraph>Content above divider</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph>Content below divider</Paragraph>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Custom Styled Dividers">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Dividers with custom margins and styles.
|
||||
</Paragraph>
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<Paragraph>Tight spacing (8px)</Paragraph>
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Paragraph>Medium spacing (16px)</Paragraph>
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
<Paragraph>Large spacing (24px)</Paragraph>
|
||||
<Divider style={{ margin: "24px 0" }} />
|
||||
<Paragraph>End</Paragraph>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• className - Custom CSS class</Paragraph>
|
||||
<Paragraph color="text-muted">• style - Inline styles</Paragraph>
|
||||
<Paragraph color="text-muted">• ...restProps - Any other hr attributes</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Import">
|
||||
<Paragraph color="text-muted">
|
||||
{"import { Divider } from \"@components/Divider\";"}
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="Divider"
|
||||
overview="Divider is a simple horizontal rule component from Vencord's @components/Divider. It renders an hr element with the vc-divider class and accepts standard HTML attributes."
|
||||
importPath={'import { Divider } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic Divider",
|
||||
description: "A simple horizontal rule separating content.",
|
||||
children: (
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<Paragraph>Content above divider</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph>Content below divider</Paragraph>
|
||||
</div>
|
||||
),
|
||||
code: "<Divider />",
|
||||
},
|
||||
{
|
||||
title: "Custom Spacing",
|
||||
description: "Control the spacing around dividers using inline styles.",
|
||||
children: (
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<Paragraph>Tight spacing (8px)</Paragraph>
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Paragraph>Medium spacing (16px)</Paragraph>
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
<Paragraph>Large spacing (24px)</Paragraph>
|
||||
<Divider style={{ margin: "24px 0" }} />
|
||||
<Paragraph>End</Paragraph>
|
||||
</div>
|
||||
),
|
||||
code: '<Divider style={{ margin: "16px 0" }} />',
|
||||
relevantProps: ["style"],
|
||||
},
|
||||
]}
|
||||
props={DIVIDER_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Error: {error.message}
|
||||
</Paragraph>
|
||||
<Button onClick={resetErrorBoundary}>
|
||||
<Button onClick={resetErrorBoundary}>Reset</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div key={key}>
|
||||
<ErrorBoundary
|
||||
fallbackRender={({ error, resetErrorBoundary }) => (
|
||||
<CustomFallback error={error} resetErrorBoundary={resetErrorBoundary} />
|
||||
)}
|
||||
onReset={() => setKey(k => k + 1)}
|
||||
>
|
||||
<ErrorTrigger label="Trigger Error" />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SimpleDemo() {
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
return (
|
||||
<div key={key}>
|
||||
<SimpleErrorBoundary
|
||||
fallback={
|
||||
<div style={{ padding: 16, background: "var(--background-secondary)", borderRadius: 8 }}>
|
||||
<Paragraph color="text-danger">Something went wrong!</Paragraph>
|
||||
<Button onClick={() => setKey(k => k + 1)} style={{ marginTop: 8 }}>Reset</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ErrorTrigger label="Trigger Error" />
|
||||
</SimpleErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LocalDemo() {
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
return (
|
||||
<div key={key}>
|
||||
<LocalErrorBoundary>
|
||||
<ErrorTrigger label="Trigger Error" />
|
||||
</LocalErrorBoundary>
|
||||
<Button variant="secondary" onClick={() => setKey(k => k + 1)} style={{ marginTop: 8 }}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="ErrorBoundary">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Full-featured error boundary with fallbackRender, FallbackComponent, onError, onReset, and resetKeys props.
|
||||
</Paragraph>
|
||||
<div key={key1}>
|
||||
<ErrorBoundary
|
||||
fallbackRender={({ error, resetErrorBoundary }) => (
|
||||
<CustomFallback error={error} resetErrorBoundary={resetErrorBoundary} />
|
||||
)}
|
||||
onError={(error, info) => console.log("ErrorBoundary caught:", error, info)}
|
||||
onReset={() => setKey1(k => k + 1)}
|
||||
>
|
||||
<ErrorTrigger label="Trigger Error (with fallbackRender)" />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="SimpleErrorBoundary">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Simple error boundary with just a fallback prop. Shows fallback content when error occurs.
|
||||
</Paragraph>
|
||||
<div key={key2}>
|
||||
<SimpleErrorBoundary
|
||||
fallback={
|
||||
<div style={{ padding: 16, background: "var(--background-secondary)", borderRadius: 8 }}>
|
||||
<Paragraph color="text-danger">Something went wrong!</Paragraph>
|
||||
<Button onClick={() => setKey2(k => k + 1)} style={{ marginTop: 8 }}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ErrorTrigger label="Trigger Error (SimpleErrorBoundary)" />
|
||||
</SimpleErrorBoundary>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="LocalErrorBoundary">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Displays an inline error message. Used for non-critical component errors.
|
||||
</Paragraph>
|
||||
<div key={key3}>
|
||||
<LocalErrorBoundary>
|
||||
<ErrorTrigger label="Trigger Error (LocalErrorBoundary)" />
|
||||
</LocalErrorBoundary>
|
||||
<Button variant="secondary" onClick={() => setKey3(k => k + 1)} style={{ marginTop: 8 }}>
|
||||
Reset LocalErrorBoundary
|
||||
</Button>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>ErrorBoundary</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<code>fallback</code> - Static ReactNode to show on error.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<code>fallbackRender</code> - Function receiving error and resetErrorBoundary.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<code>FallbackComponent</code> - Component receiving error and resetErrorBoundary props.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<code>onError</code> - Callback when error is caught (error, errorInfo).
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<code>onReset</code> - Callback when boundary is reset.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<code>resetKeys</code> - Array of values that trigger reset when changed.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Usage">
|
||||
<Paragraph color="text-muted">
|
||||
Wrap components that may throw errors. Use ErrorBoundary for full control,
|
||||
SimpleErrorBoundary for basic fallback, or LocalErrorBoundary for inline error display.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="ErrorBoundary"
|
||||
overview="Discord provides three error boundary components with different levels of control. ErrorBoundary is feature-rich with fallbackRender, FallbackComponent, onError/onReset callbacks, and resetKeys. SimpleErrorBoundary accepts a static fallback ReactNode. LocalErrorBoundary shows inline error text for non-critical failures."
|
||||
notices={[
|
||||
{ type: "positive", children: "Wrap plugin UI components with ErrorBoundary to prevent crashes from taking down the entire Discord UI. Use LocalErrorBoundary for non-critical elements." },
|
||||
{ type: "info", children: "ErrorBoundary only catches errors during rendering, lifecycle methods, and constructors. It does not catch errors in event handlers or async code." },
|
||||
]}
|
||||
importPath={'import { ErrorBoundary, SimpleErrorBoundary, LocalErrorBoundary } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "ErrorBoundary",
|
||||
description: "Full-featured boundary with fallbackRender for dynamic error UI with reset capability.",
|
||||
children: <ErrorBoundaryDemo />,
|
||||
code: `<ErrorBoundary
|
||||
fallbackRender={({ error, resetErrorBoundary }) => (
|
||||
<div>
|
||||
<p>Error: {error.message}</p>
|
||||
<button onClick={resetErrorBoundary}>Reset</button>
|
||||
</div>
|
||||
)}
|
||||
onError={(error, info) => console.error(error)}
|
||||
onReset={() => refetch()}
|
||||
>
|
||||
<MyComponent />
|
||||
</ErrorBoundary>`,
|
||||
relevantProps: ["fallbackRender", "onError", "onReset", "resetKeys"],
|
||||
},
|
||||
{
|
||||
title: "SimpleErrorBoundary",
|
||||
description: "Lightweight boundary that shows static fallback content on error.",
|
||||
children: <SimpleDemo />,
|
||||
code: `<SimpleErrorBoundary fallback={<p>Something went wrong!</p>}>
|
||||
<MyComponent />
|
||||
</SimpleErrorBoundary>`,
|
||||
},
|
||||
{
|
||||
title: "LocalErrorBoundary",
|
||||
description: "Displays an inline error message for non-critical component failures. No fallback prop needed.",
|
||||
children: <LocalDemo />,
|
||||
code: `<LocalErrorBoundary>
|
||||
<MyWidget />
|
||||
</LocalErrorBoundary>`,
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement | null>", 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<HTMLDivElement>(null);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="FocusLock Demo">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
FocusLock traps keyboard focus within a container. Enable it and try tabbing - focus stays inside the box.
|
||||
</Paragraph>
|
||||
<ManaButton
|
||||
variant={focusLockEnabled ? "primary" : "secondary"}
|
||||
text={focusLockEnabled ? "Disable Focus Lock" : "Enable Focus Lock"}
|
||||
onClick={() => setFocusLockEnabled(!focusLockEnabled)}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
padding: 16,
|
||||
border: `2px solid ${focusLockEnabled ? "var(--brand-500)" : "var(--background-modifier-accent)"}`,
|
||||
borderRadius: 8,
|
||||
background: "var(--background-secondary)",
|
||||
}}
|
||||
>
|
||||
{focusLockEnabled && (
|
||||
<FocusLock containerRef={containerRef}>
|
||||
<Paragraph style={{ marginBottom: 12 }}>
|
||||
Focus is now trapped! Tab between these elements:
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<ManaTextInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
placeholder="First input"
|
||||
/>
|
||||
<ManaTextInput
|
||||
value=""
|
||||
onChange={() => { }}
|
||||
placeholder="Second input"
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<ManaButton variant="secondary" text="Button 1" onClick={() => { }} />
|
||||
<ManaButton variant="secondary" text="Button 2" onClick={() => { }} />
|
||||
<ManaButton
|
||||
variant="primary"
|
||||
text="Exit Focus Lock"
|
||||
onClick={() => setFocusLockEnabled(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FocusLock>
|
||||
)}
|
||||
{!focusLockEnabled && (
|
||||
<Paragraph color="text-muted">
|
||||
Click "Enable Focus Lock" to activate the focus trap demo.
|
||||
<div>
|
||||
<ManaButton
|
||||
variant={enabled ? "primary" : "secondary"}
|
||||
text={enabled ? "Disable Focus Lock" : "Enable Focus Lock"}
|
||||
onClick={() => setEnabled(!enabled)}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
padding: 16,
|
||||
border: `2px solid ${enabled ? "var(--brand-500)" : "var(--background-modifier-accent)"}`,
|
||||
borderRadius: 8,
|
||||
background: "var(--background-secondary)",
|
||||
}}
|
||||
>
|
||||
{enabled ? (
|
||||
<FocusLock containerRef={containerRef}>
|
||||
<Paragraph style={{ marginBottom: 12 }}>
|
||||
Focus is trapped. Try pressing Tab to cycle through these elements:
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Use Cases">
|
||||
<Paragraph color="text-muted">• Modal dialogs - Keep focus within the modal</Paragraph>
|
||||
<Paragraph color="text-muted">• Dropdown menus - Trap focus while open</Paragraph>
|
||||
<Paragraph color="text-muted">• Settings panels - Improve keyboard navigation</Paragraph>
|
||||
<Paragraph color="text-muted">• Accessibility - Ensure screen readers stay in context</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• containerRef - React ref to the container element</Paragraph>
|
||||
<Paragraph color="text-muted">• keyboardModeEnabled - Enable keyboard focus trapping</Paragraph>
|
||||
<Paragraph color="text-muted">• children - Content to render inside the focus trap</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<ManaTextInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
placeholder="First input"
|
||||
/>
|
||||
<ManaTextInput
|
||||
value=""
|
||||
onChange={() => { }}
|
||||
placeholder="Second input"
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<ManaButton variant="secondary" text="Button A" onClick={() => { }} />
|
||||
<ManaButton variant="secondary" text="Button B" onClick={() => { }} />
|
||||
<ManaButton variant="primary" text="Exit Focus Lock" onClick={() => setEnabled(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</FocusLock>
|
||||
) : (
|
||||
<Paragraph color="text-muted">
|
||||
Click "Enable Focus Lock" to activate the focus trap demo.
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FocusLockTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="FocusLock"
|
||||
overview="FocusLock traps keyboard focus within a container element. When active, pressing Tab cycles through focusable elements inside the boundary without escaping. Used in modals, dropdown menus, and anywhere keyboard accessibility requires scoped navigation."
|
||||
notices={[
|
||||
{ type: "warn", children: "FocusLock traps all keyboard focus within its container. Always provide a way for the user to exit the focus trap (e.g. a close button or Escape key handler), otherwise they will be unable to navigate away." },
|
||||
{ type: "info", children: "FocusLock is typically used inside modals and dropdown menus that already handle dismiss behavior. If you use it standalone, you must implement your own dismiss logic." },
|
||||
]}
|
||||
importPath={'import { FocusLock } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Focus Trap",
|
||||
description: "Enable the focus lock and press Tab to see focus cycle within the container boundary.",
|
||||
children: <FocusLockDemo />,
|
||||
code: `const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
<div ref={containerRef}>
|
||||
<FocusLock containerRef={containerRef}>
|
||||
<input />
|
||||
<button>OK</button>
|
||||
<button>Cancel</button>
|
||||
</FocusLock>
|
||||
</div>`,
|
||||
relevantProps: ["containerRef"],
|
||||
},
|
||||
]}
|
||||
props={FOCUSLOCK_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <Paragraph color="text-muted">No guilds with icons found.</Paragraph>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Controls">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={`Animate: ${animate}`}
|
||||
onClick={() => setAnimate(a => !a)}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Icon Sizes">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
GuildIcon component with various sizes. Shows server icon or acronym fallback.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
|
||||
{guildWithIcon && GuildIconSizes.map(size => (
|
||||
<div key={size} style={{ textAlign: "center" }}>
|
||||
<GuildIcon
|
||||
guildId={guildWithIcon.id}
|
||||
guildName={guildWithIcon.name}
|
||||
guildIcon={guildWithIcon.icon ?? null}
|
||||
iconSize={size}
|
||||
animate={animate}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{size}px
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Acronym Fallback">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
When a guild has no icon, displays an acronym based on the guild name.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
|
||||
{[32, 48, 80].map(size => (
|
||||
<div key={size} style={{ textAlign: "center" }}>
|
||||
<GuildIcon
|
||||
guildId={guildWithoutIcon?.id ?? "0"}
|
||||
guildName={guildWithoutIcon?.name ?? "Test Guild"}
|
||||
guildIcon={null}
|
||||
iconSize={size}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{size}px (no icon)
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Your Guilds">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Showing icons from guilds you're in.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
{guilds.map(guild => (
|
||||
<div key={guild.id} style={{ textAlign: "center", maxWidth: 80 }}>
|
||||
<GuildIcon
|
||||
guildId={guild.id}
|
||||
guildName={guild.name}
|
||||
guildIcon={guild.icon ?? null}
|
||||
iconSize={48}
|
||||
animate={animate}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{guild.name.slice(0, 12)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• guildId - The guild's snowflake ID</Paragraph>
|
||||
<Paragraph color="text-muted">• guildName - Guild name (used for acronym fallback)</Paragraph>
|
||||
<Paragraph color="text-muted">• guildIcon - Icon hash or null</Paragraph>
|
||||
<Paragraph color="text-muted">• iconSize - Size in pixels</Paragraph>
|
||||
<Paragraph color="text-muted">• animate - Enable animated icons (GIF)</Paragraph>
|
||||
<Paragraph color="text-muted">• className - Custom CSS class</Paragraph>
|
||||
<Paragraph color="text-muted">• acronymClassName - Class for acronym text</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Import">
|
||||
<Paragraph color="text-muted">
|
||||
{"import { GuildIcon } from \"@equicordplugins/components.dev/components\";"}
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={`Animate: ${animate}`}
|
||||
onClick={() => setAnimate(a => !a)}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
|
||||
{GuildIconSizes.map(size => (
|
||||
<div key={size} style={{ textAlign: "center" }}>
|
||||
<GuildIcon
|
||||
guildId={guildWithIcon.id}
|
||||
guildName={guildWithIcon.name}
|
||||
guildIcon={guildWithIcon.icon ?? null}
|
||||
iconSize={size}
|
||||
animate={animate}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{size}px
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AcronymDemo() {
|
||||
const guilds = Object.values(GuildStore.getGuilds()).slice(0, 6);
|
||||
const guildWithoutIcon = guilds.find(g => !g.icon);
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-grid" style={{ alignItems: "flex-end" }}>
|
||||
{[32, 48, 80].map(size => (
|
||||
<div key={size} style={{ textAlign: "center" }}>
|
||||
<GuildIcon
|
||||
guildId={guildWithoutIcon?.id ?? "0"}
|
||||
guildName={guildWithoutIcon?.name ?? "Test Guild"}
|
||||
guildIcon={null}
|
||||
iconSize={size}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{size}px (no icon)
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function YourGuildsDemo() {
|
||||
const [animate, setAnimate] = useState(true);
|
||||
const guilds = Object.values(GuildStore.getGuilds()).slice(0, 8);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={`Animate: ${animate}`}
|
||||
onClick={() => setAnimate(a => !a)}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<div className="vc-compfinder-grid">
|
||||
{guilds.map(guild => (
|
||||
<div key={guild.id} style={{ textAlign: "center", maxWidth: 80 }}>
|
||||
<GuildIcon
|
||||
guildId={guild.id}
|
||||
guildName={guild.name}
|
||||
guildIcon={guild.icon ?? null}
|
||||
iconSize={48}
|
||||
animate={animate}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{guild.name.slice(0, 12)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GuildIconTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="GuildIcon"
|
||||
overview="GuildIcon renders a server icon from its hash, or falls back to an acronym derived from the guild name. Supports all standard Discord icon sizes and animated GIF icons."
|
||||
importPath={'import { GuildIcon, GuildIconSizes } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Icon Sizes",
|
||||
description: "All available GuildIconSizes from 16px to 96px.",
|
||||
children: <SizesDemo />,
|
||||
code: `<GuildIcon
|
||||
guildId={guild.id}
|
||||
guildName={guild.name}
|
||||
guildIcon={guild.icon}
|
||||
iconSize={48}
|
||||
animate={true}
|
||||
/>`,
|
||||
relevantProps: ["iconSize", "animate"],
|
||||
},
|
||||
{
|
||||
title: "Acronym Fallback",
|
||||
description: "When guildIcon is null, the component renders an acronym based on the guild name.",
|
||||
children: <AcronymDemo />,
|
||||
relevantProps: ["guildName", "guildIcon"],
|
||||
},
|
||||
{
|
||||
title: "Your Guilds",
|
||||
description: "Live preview using guilds you're currently in.",
|
||||
children: <YourGuildsDemo />,
|
||||
},
|
||||
]}
|
||||
props={GUILDICON_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Discord Heading">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Discord's Heading component with variant prop. Auto-sets h1-h6 tag based on nesting level.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{HeadingVariants.slice(0, 10).map(variant => (
|
||||
<DiscordHeading key={variant} variant={variant}>
|
||||
{variant}
|
||||
</DiscordHeading>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="More Heading Sizes">
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{HeadingVariants.slice(10).map(variant => (
|
||||
<DiscordHeading key={variant} variant={variant}>
|
||||
{variant}
|
||||
</DiscordHeading>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Discord Text">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Discord's Text component for body text with various sizes and weights.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
{TextVariants.slice(0, 12).map(variant => (
|
||||
<DiscordText key={variant} variant={variant}>
|
||||
{variant}
|
||||
</DiscordText>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Smaller Text Sizes">
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
{TextVariants.slice(12).map(variant => (
|
||||
<DiscordText key={variant} variant={variant}>
|
||||
{variant}
|
||||
</DiscordText>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Text Colors">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Available color props for Text component.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
{TextColors.map(color => (
|
||||
<DiscordText key={color} variant="text-md/medium" color={color}>
|
||||
{color}
|
||||
</DiscordText>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props - DiscordHeading">
|
||||
<Paragraph color="text-muted">• variant - "heading-SIZE/WEIGHT" (xxl, xl, lg, md, sm)</Paragraph>
|
||||
<Paragraph color="text-muted">• className - Custom CSS class</Paragraph>
|
||||
<Paragraph color="text-muted">• color - Text color (same as Text colors)</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props - DiscordText">
|
||||
<Paragraph color="text-muted">• variant - "text-SIZE/WEIGHT" (xxl, xl, lg, md, sm, xs)</Paragraph>
|
||||
<Paragraph color="text-muted">• color - Text color name</Paragraph>
|
||||
<Paragraph color="text-muted">• tag - HTML tag to render (default: "div")</Paragraph>
|
||||
<Paragraph color="text-muted">• selectable - Allow text selection</Paragraph>
|
||||
<Paragraph color="text-muted">• lineClamp - Number of lines before truncation</Paragraph>
|
||||
<Paragraph color="text-muted">• tabularNumbers - Use tabular number spacing</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Import">
|
||||
<Paragraph color="text-muted">
|
||||
{"import { DiscordHeading, DiscordText, HeadingVariants, TextVariants, TextColors } from \"../components\";"}
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="DiscordHeading"
|
||||
overview="Discord's typography system uses two components: DiscordHeading for headings (xxl through sm) and DiscordText for body text (xxl through xs). Both support multiple weight variants and Discord's color system."
|
||||
importPath={'import { DiscordHeading, DiscordText, HeadingVariants, TextVariants, TextColors } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Heading Variants",
|
||||
description: "All heading size/weight combinations from xxl to sm.",
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{HeadingVariants.map(variant => (
|
||||
<DiscordHeading key={variant} variant={variant}>
|
||||
{variant}
|
||||
</DiscordHeading>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: '<DiscordHeading variant="heading-xl/bold">Title</DiscordHeading>',
|
||||
relevantProps: ["variant"],
|
||||
},
|
||||
{
|
||||
title: "Text Variants",
|
||||
description: "All text size/weight combinations from xxl to xs.",
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
{TextVariants.map(variant => (
|
||||
<DiscordText key={variant} variant={variant}>
|
||||
{variant}
|
||||
</DiscordText>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: '<DiscordText variant="text-md/medium">Body text</DiscordText>',
|
||||
},
|
||||
{
|
||||
title: "Text Colors",
|
||||
description: "All available color values for the color prop.",
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
{TextColors.map(color => (
|
||||
<DiscordText key={color} variant="text-md/medium" color={color}>
|
||||
{color}
|
||||
</DiscordText>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: '<DiscordText variant="text-md/medium" color="text-brand">Branded</DiscordText>',
|
||||
relevantProps: ["color"],
|
||||
},
|
||||
{
|
||||
title: "Line Clamp",
|
||||
description: "Truncate text after a specified number of lines.",
|
||||
children: (
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<DiscordText variant="text-md/normal" lineClamp={2}>
|
||||
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.
|
||||
</DiscordText>
|
||||
</div>
|
||||
),
|
||||
code: '<DiscordText variant="text-md/normal" lineClamp={2}>\n Long text here...\n</DiscordText>',
|
||||
relevantProps: ["lineClamp"],
|
||||
},
|
||||
{
|
||||
title: "Tabular Numbers",
|
||||
description: "Monospaced number spacing for aligned numeric columns.",
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<DiscordText variant="text-md/medium" tabularNumbers>1,234,567</DiscordText>
|
||||
<DiscordText variant="text-md/medium" tabularNumbers>89,012</DiscordText>
|
||||
<DiscordText variant="text-md/medium" tabularNumbers>345</DiscordText>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["tabularNumbers"],
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>", 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 (
|
||||
<LayerModal
|
||||
transitionState={TransitionState.ENTERED}
|
||||
|
|
@ -37,8 +46,8 @@ function LayerModalDemo({ onClose }: { onClose: () => void; }) {
|
|||
<div style={{ padding: 24, minWidth: 300 }}>
|
||||
<Heading tag="h2" style={{ marginBottom: 8 }}>Layer Modal</Heading>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 16 }}>
|
||||
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.
|
||||
</Paragraph>
|
||||
<ManaButton variant="primary" text="Close" onClick={onClose} />
|
||||
</div>
|
||||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Layer Modal">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
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.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="Open via openModal"
|
||||
onClick={() => openModal(props => (
|
||||
<LayerModalDemo onClose={props.onClose} />
|
||||
))}
|
||||
/>
|
||||
<div>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={visible ? "Hide Inline" : "Show Inline"}
|
||||
onClick={() => setVisible(!visible)}
|
||||
/>
|
||||
{visible && (
|
||||
<div style={{ marginTop: 16, position: "relative", minHeight: 200 }}>
|
||||
<LayerModal
|
||||
transitionState={TransitionState.ENTERED}
|
||||
onClose={() => setVisible(false)}
|
||||
aria-label="Inline Demo"
|
||||
>
|
||||
<div style={{ padding: 16 }}>
|
||||
<Paragraph>Inline layer modal content</Paragraph>
|
||||
<ManaButton variant="primary" text="Close" onClick={() => setVisible(false)} style={{ marginTop: 8 }} />
|
||||
</div>
|
||||
</LayerModal>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Inline Preview">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Toggle to see the layer modal rendered inline (not recommended for production).
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={showInline ? "Hide Inline" : "Show Inline"}
|
||||
onClick={() => setShowInline(!showInline)}
|
||||
/>
|
||||
</div>
|
||||
{showInline && (
|
||||
<div style={{ marginTop: 16, position: "relative", minHeight: 200 }}>
|
||||
<LayerModal
|
||||
transitionState={TransitionState.ENTERED}
|
||||
onClose={() => setShowInline(false)}
|
||||
aria-label="Inline Demo"
|
||||
>
|
||||
<div style={{ padding: 16 }}>
|
||||
<Paragraph>Inline layer modal content</Paragraph>
|
||||
</div>
|
||||
</LayerModal>
|
||||
</div>
|
||||
)}
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>LayerModal</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• transitionState: number - Use TransitionState.ENTERED for visible
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onClose: () => void - Close handler
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• children: ReactNode - Modal content
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• animationVariant?: "default" | string - Animation style
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• returnRef?: React.Ref - Ref for the modal element
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• aria-label?: string - Accessibility label
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>TransitionState</strong> - findByPropsLazy("ENTERING", "ENTERED", "EXITING")
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• ENTERING, ENTERED, EXITING, EXITED, HIDDEN
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>Usage Notes</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• Use with openModal() for proper backdrop and positioning
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• Has built-in animation handling based on transitionState
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LayerModalTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="LayerModal"
|
||||
overview="LayerModal is Discord's component for stacked/layered modal experiences. It has different animation behavior than regular modals and is typically used with openModal() for proper backdrop and positioning. Requires TransitionState for controlling visibility."
|
||||
notices={[
|
||||
{ type: "warn", children: "LayerModal must be used with openModal() for proper backdrop, positioning, and transition state management. Rendering it inline without the modal system will result in incorrect behavior." },
|
||||
{ type: "info", children: "LayerModal is found via findComponentByCodeLazy, not exported directly. The TransitionState constants are also required and must be found separately." },
|
||||
]}
|
||||
importPath={`import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
|
||||
const LayerModal = findComponentByCodeLazy('"data-mana-component":"layer-modal"');
|
||||
const TransitionState = findByPropsLazy("ENTERING", "ENTERED", "EXITING");`}
|
||||
sections={[
|
||||
{
|
||||
title: "Open via openModal",
|
||||
description: "The recommended way to use LayerModal is with openModal() which handles backdrop and positioning.",
|
||||
children: (
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="Open Layer Modal"
|
||||
onClick={() => openModal(props => (
|
||||
<ModalContentDemo onClose={props.onClose} />
|
||||
))}
|
||||
/>
|
||||
),
|
||||
code: `openModal(props => (
|
||||
<LayerModal
|
||||
transitionState={TransitionState.ENTERED}
|
||||
onClose={props.onClose}
|
||||
aria-label="My Modal"
|
||||
>
|
||||
<div style={{ padding: 24 }}>
|
||||
<p>Modal content here</p>
|
||||
</div>
|
||||
</LayerModal>
|
||||
))`,
|
||||
relevantProps: ["transitionState", "onClose"],
|
||||
},
|
||||
{
|
||||
title: "Inline Preview",
|
||||
description: "Rendered inline for preview purposes. Not recommended for production use.",
|
||||
children: <InlineDemo />,
|
||||
relevantProps: ["aria-label", "animationVariant"],
|
||||
},
|
||||
]}
|
||||
props={LAYERMODAL_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ListboxItem[]>([SAMPLE_ITEMS[0]]);
|
||||
const [multiSelected, setMultiSelected] = useState<ListboxItem[]>([SAMPLE_ITEMS[0], SAMPLE_ITEMS[2]]);
|
||||
const [disabledSelected, setDisabledSelected] = useState<ListboxItem[]>([]);
|
||||
const [manySelected, setManySelected] = useState<ListboxItem[]>([]);
|
||||
|
||||
function SingleSelectDemo() {
|
||||
const [selected, setSelected] = useState<ListboxItem[]>([SAMPLE_ITEMS[0]]);
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Single Selection">
|
||||
<ManaListbox
|
||||
items={SAMPLE_ITEMS}
|
||||
selectedItems={singleSelected}
|
||||
onSelectionChange={setSingleSelected}
|
||||
selectionMode="single"
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Multiple Selection">
|
||||
<ManaListbox
|
||||
items={SAMPLE_ITEMS}
|
||||
selectedItems={multiSelected}
|
||||
onSelectionChange={setMultiSelected}
|
||||
selectionMode="multiple"
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Disabled Items">
|
||||
<ManaListbox
|
||||
items={ITEMS_WITH_DISABLED}
|
||||
selectedItems={disabledSelected}
|
||||
onSelectionChange={setDisabledSelected}
|
||||
selectionMode="multiple"
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Max Visible Items (5)">
|
||||
<ManaListbox
|
||||
items={MANY_ITEMS}
|
||||
selectedItems={manySelected}
|
||||
onSelectionChange={setManySelected}
|
||||
selectionMode="multiple"
|
||||
maxVisibleItems={5}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Disabled">
|
||||
<ManaListbox
|
||||
items={SAMPLE_ITEMS}
|
||||
selectedItems={[SAMPLE_ITEMS[1]]}
|
||||
disabled
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Loading">
|
||||
<ManaListbox
|
||||
items={[]}
|
||||
loading
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Empty State">
|
||||
<ManaListbox
|
||||
items={[]}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Listbox</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• items: ListboxItem[] - Array of items
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• selectedItems?: ListboxItem[] - Currently selected items
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onSelectionChange?: (items: ListboxItem[]) => void - Selection callback
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• selectionMode?: "single" | "multiple" - Selection mode
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• maxVisibleItems?: number - Max visible before scroll
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable entire listbox
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• loading?: boolean - Show loading state
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• typeahead?: boolean - Enable keyboard search
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• shouldFocusWrap?: boolean - Wrap focus at ends
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>ListboxItem</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• id: string - Unique item ID
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• label: string - Display label
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable individual item
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<ManaListbox
|
||||
items={SAMPLE_ITEMS}
|
||||
selectedItems={selected}
|
||||
onSelectionChange={setSelected}
|
||||
selectionMode="single"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiSelectDemo() {
|
||||
const [selected, setSelected] = useState<ListboxItem[]>([SAMPLE_ITEMS[0], SAMPLE_ITEMS[2]]);
|
||||
return (
|
||||
<ManaListbox
|
||||
items={SAMPLE_ITEMS}
|
||||
selectedItems={selected}
|
||||
onSelectionChange={setSelected}
|
||||
selectionMode="multiple"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DisabledItemsDemo() {
|
||||
const [selected, setSelected] = useState<ListboxItem[]>([]);
|
||||
return (
|
||||
<ManaListbox
|
||||
items={ITEMS_WITH_DISABLED}
|
||||
selectedItems={selected}
|
||||
onSelectionChange={setSelected}
|
||||
selectionMode="multiple"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MaxVisibleDemo() {
|
||||
const [selected, setSelected] = useState<ListboxItem[]>([]);
|
||||
return (
|
||||
<ManaListbox
|
||||
items={MANY_ITEMS}
|
||||
selectedItems={selected}
|
||||
onSelectionChange={setSelected}
|
||||
selectionMode="multiple"
|
||||
maxVisibleItems={5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ListboxTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="ManaListbox"
|
||||
overview="ManaListbox is Discord's listbox component for selecting one or multiple items from a list. Supports single and multiple selection modes, disabled items, loading/empty states, keyboard navigation with typeahead, and custom item rendering."
|
||||
importPath={'import { ManaListbox, ListboxItem } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Single Selection",
|
||||
description: "Only one item can be selected at a time.",
|
||||
children: <SingleSelectDemo />,
|
||||
code: `<ManaListbox
|
||||
items={items}
|
||||
selectedItems={selected}
|
||||
onSelectionChange={setSelected}
|
||||
selectionMode="single"
|
||||
/>`,
|
||||
relevantProps: ["selectionMode", "selectedItems", "onSelectionChange"],
|
||||
},
|
||||
{
|
||||
title: "Multiple Selection",
|
||||
description: "Multiple items can be selected simultaneously.",
|
||||
children: <MultiSelectDemo />,
|
||||
code: `<ManaListbox
|
||||
items={items}
|
||||
selectedItems={selected}
|
||||
onSelectionChange={setSelected}
|
||||
selectionMode="multiple"
|
||||
/>`,
|
||||
},
|
||||
{
|
||||
title: "Disabled Items",
|
||||
description: "Individual items can be disabled while the rest remain interactive.",
|
||||
children: <DisabledItemsDemo />,
|
||||
},
|
||||
{
|
||||
title: "Max Visible Items",
|
||||
description: "Limits visible items to 5 with scrolling for the rest.",
|
||||
children: <MaxVisibleDemo />,
|
||||
relevantProps: ["maxVisibleItems"],
|
||||
},
|
||||
{
|
||||
title: "Disabled State",
|
||||
description: "The entire listbox is non-interactive when disabled.",
|
||||
children: (
|
||||
<ManaListbox
|
||||
items={SAMPLE_ITEMS}
|
||||
selectedItems={[SAMPLE_ITEMS[1]]}
|
||||
disabled
|
||||
/>
|
||||
),
|
||||
relevantProps: ["disabled"],
|
||||
},
|
||||
{
|
||||
title: "Loading State",
|
||||
description: "Shows a loading indicator while data is being fetched.",
|
||||
children: <ManaListbox items={[]} loading />,
|
||||
relevantProps: ["loading"],
|
||||
},
|
||||
{
|
||||
title: "Empty State",
|
||||
description: "Default empty appearance when no items are provided.",
|
||||
children: <ManaListbox items={[]} />,
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Menu.Menu navId="demo-menu" onClose={onClose}>
|
||||
<Menu.MenuItem
|
||||
id="item-1"
|
||||
label="Basic Item"
|
||||
action={() => console.log("Basic item clicked")}
|
||||
/>
|
||||
<Menu.MenuItem id="item-1" label="Basic Item" action={() => { }} />
|
||||
<Menu.MenuItem
|
||||
id="item-2"
|
||||
label="Item with Icon"
|
||||
|
|
@ -28,57 +114,33 @@ function DemoMenu({ onClose }: { onClose: () => void; }) {
|
|||
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
</svg>
|
||||
)}
|
||||
action={() => console.log("Icon item clicked")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="item-with-hint"
|
||||
label="Item with Hint"
|
||||
hint="Ctrl+H"
|
||||
action={() => console.log("Hint item clicked")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="item-with-subtext"
|
||||
label="Item with Subtext"
|
||||
subtext="Additional description here"
|
||||
action={() => console.log("Subtext item clicked")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="item-disabled"
|
||||
label="Disabled Item"
|
||||
disabled
|
||||
action={() => { }}
|
||||
/>
|
||||
<Menu.MenuItem id="item-hint" label="Item with Hint" hint="Ctrl+H" action={() => { }} />
|
||||
<Menu.MenuItem id="item-subtext" label="Item with Subtext" subtext="Additional description" action={() => { }} />
|
||||
<Menu.MenuItem id="item-disabled" label="Disabled Item" disabled />
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
<Menu.MenuGroup label="Colors">
|
||||
<Menu.MenuItem id="color-default" label="Default" color="default" action={() => { }} />
|
||||
<Menu.MenuItem id="color-brand" label="Brand" color="brand" action={() => { }} />
|
||||
<Menu.MenuItem id="color-danger" label="Danger" color="danger" action={() => { }} />
|
||||
<Menu.MenuItem id="color-success" label="Success" color="success" action={() => { }} />
|
||||
<Menu.MenuItem id="color-premium" label="Premium" color="premium" action={() => { }} />
|
||||
<Menu.MenuItem id="color-premium-gradient" label="Premium Gradient" color="premium-gradient" action={() => { }} />
|
||||
<Menu.MenuItem id="c-default" label="Default" color="default" action={() => { }} />
|
||||
<Menu.MenuItem id="c-brand" label="Brand" color="brand" action={() => { }} />
|
||||
<Menu.MenuItem id="c-danger" label="Danger" color="danger" action={() => { }} />
|
||||
<Menu.MenuItem id="c-success" label="Success" color="success" action={() => { }} />
|
||||
<Menu.MenuItem id="c-premium" label="Premium" color="premium" action={() => { }} />
|
||||
<Menu.MenuItem id="c-premium-g" label="Premium Gradient" color="premium-gradient" action={() => { }} />
|
||||
</Menu.MenuGroup>
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
<Menu.MenuCheckboxItem
|
||||
id="checkbox-item"
|
||||
id="checkbox"
|
||||
label="Checkbox Item"
|
||||
checked={checkboxValue}
|
||||
action={() => setCheckboxValue(v => !v)}
|
||||
/>
|
||||
<Menu.MenuCheckboxItem
|
||||
id="checkbox-subtext"
|
||||
label="Checkbox with Subtext"
|
||||
checked={!checkboxValue}
|
||||
subtext="Toggle this option"
|
||||
action={() => setCheckboxValue(v => !v)}
|
||||
/>
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
<Menu.MenuSwitchItem
|
||||
id="switch-item"
|
||||
id="switch"
|
||||
label="Switch Item"
|
||||
checked={switchValue}
|
||||
action={() => setSwitchValue(v => !v)}
|
||||
|
|
@ -87,55 +149,25 @@ function DemoMenu({ onClose }: { onClose: () => void; }) {
|
|||
<Menu.MenuSeparator />
|
||||
|
||||
<Menu.MenuGroup label="Radio Group">
|
||||
<Menu.MenuRadioItem
|
||||
id="radio-1"
|
||||
group="demo-radio"
|
||||
label="Option 1"
|
||||
checked={radioValue === "option1"}
|
||||
action={() => setRadioValue("option1")}
|
||||
/>
|
||||
<Menu.MenuRadioItem
|
||||
id="radio-2"
|
||||
group="demo-radio"
|
||||
label="Option 2"
|
||||
checked={radioValue === "option2"}
|
||||
action={() => setRadioValue("option2")}
|
||||
/>
|
||||
<Menu.MenuRadioItem
|
||||
id="radio-3"
|
||||
group="demo-radio"
|
||||
label="Option 3"
|
||||
checked={radioValue === "option3"}
|
||||
action={() => setRadioValue("option3")}
|
||||
/>
|
||||
<Menu.MenuRadioItem id="r-1" group="demo" label="Option 1" checked={radioValue === "option1"} action={() => setRadioValue("option1")} />
|
||||
<Menu.MenuRadioItem id="r-2" group="demo" label="Option 2" checked={radioValue === "option2"} action={() => setRadioValue("option2")} />
|
||||
<Menu.MenuRadioItem id="r-3" group="demo" label="Option 3" checked={radioValue === "option3"} action={() => setRadioValue("option3")} />
|
||||
</Menu.MenuGroup>
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
<Menu.MenuItem id="submenu" label="Submenu">
|
||||
<Menu.MenuItem
|
||||
id="sub-item-1"
|
||||
label="Sub Item 1"
|
||||
action={() => console.log("Sub item 1")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="sub-item-2"
|
||||
label="Sub Item 2"
|
||||
action={() => console.log("Sub item 2")}
|
||||
/>
|
||||
<Menu.MenuItem id="nested-submenu" label="Nested Submenu">
|
||||
<Menu.MenuItem
|
||||
id="nested-1"
|
||||
label="Nested Item"
|
||||
action={() => console.log("Nested")}
|
||||
/>
|
||||
<Menu.MenuItem id="sub-1" label="Sub Item 1" action={() => { }} />
|
||||
<Menu.MenuItem id="sub-2" label="Sub Item 2" action={() => { }} />
|
||||
<Menu.MenuItem id="nested" label="Nested Submenu">
|
||||
<Menu.MenuItem id="nested-1" label="Nested Item" action={() => { }} />
|
||||
</Menu.MenuItem>
|
||||
</Menu.MenuItem>
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
<Menu.MenuControlItem
|
||||
id="slider-item"
|
||||
id="slider"
|
||||
label="Volume"
|
||||
control={() => (
|
||||
<Menu.MenuSliderControl
|
||||
|
|
@ -159,66 +191,45 @@ export default function MenuTab() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Menu Demo">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Click or right-click the button to open a demo context menu.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<Button onClick={openMenu}>
|
||||
Open Menu (Click)
|
||||
</Button>
|
||||
<Button variant="secondary" onContextMenu={openMenu}>
|
||||
Open Menu (Right-Click)
|
||||
</Button>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Menu Components">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.Menu</strong> - Root container with navId and onClose.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuItem</strong> - Basic item with id, label, action, icon, hint, subtext, disabled, color.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuCheckboxItem</strong> - Toggleable checkbox item with checked state.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuRadioItem</strong> - Radio selection with group prop for mutual exclusion.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuSwitchItem</strong> - Toggle switch item with checked state.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuGroup</strong> - Groups items with optional label header.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuSeparator</strong> - Visual divider between items.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuControlItem</strong> - Wrapper for custom controls like sliders.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuSliderControl</strong> - Slider control inside MenuControlItem.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
<strong>Menu.MenuSearchControl</strong> - Search input for filtering menu items.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="MenuItem Colors">
|
||||
<Paragraph color="text-muted">
|
||||
Available colors: <code>default</code>, <code>brand</code>, <code>danger</code>, <code>success</code>, <code>premium</code>, <code>premium-gradient</code>.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Usage">
|
||||
<Paragraph color="text-muted">
|
||||
Use ContextMenuApi.openContextMenu(event, () => <Menu />) to open menus.
|
||||
For patches, use NavContextMenuPatchCallback from @api/ContextMenu.
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="Menu"
|
||||
overview="Discord's Menu system provides context menus with items, checkboxes, radio buttons, switches, sliders, submenus, and groups. Open menus with ContextMenuApi.openContextMenu(). The Menu object contains all sub-components: Menu, MenuItem, MenuCheckboxItem, MenuRadioItem, MenuSwitchItem, MenuGroup, MenuSeparator, MenuControlItem, MenuSliderControl, and MenuSearchControl."
|
||||
notices={[
|
||||
{ type: "info", children: "Menus must be opened via ContextMenuApi.openContextMenu(). Set the onClose prop to ContextMenuApi.closeContextMenu to ensure proper cleanup." },
|
||||
]}
|
||||
importPath={'import { Menu, ContextMenuApi } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Full Menu Demo",
|
||||
description: "Click or right-click to open a menu showcasing all component types: basic items, icons, hints, subtext, colors, checkboxes, switches, radio groups, submenus, and sliders.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<Button onClick={openMenu}>Open Menu (Click)</Button>
|
||||
<Button variant="secondary" onContextMenu={openMenu}>Open Menu (Right-Click)</Button>
|
||||
</div>
|
||||
),
|
||||
code: `const openMenu = (e: React.MouseEvent) => {
|
||||
ContextMenuApi.openContextMenu(e, () => (
|
||||
<Menu.Menu navId="my-menu" onClose={ContextMenuApi.closeContextMenu}>
|
||||
<Menu.MenuItem id="item" label="Click me" action={() => { }} />
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuCheckboxItem id="check" label="Toggle" checked={on} action={() => setOn(!on)} />
|
||||
</Menu.Menu>
|
||||
));
|
||||
};`,
|
||||
},
|
||||
{
|
||||
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: (
|
||||
<Button variant="secondary" onClick={openMenu}>
|
||||
Open Menu to See Colors
|
||||
</Button>
|
||||
),
|
||||
relevantProps: ["color"],
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>", 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<void>", 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(); } },
|
||||
]}
|
||||
>
|
||||
<Paragraph>This is the modal content. You can put any React components here.</Paragraph>
|
||||
<Paragraph>This is the modal body content. Any React components can go here.</Paragraph>
|
||||
</Modal>
|
||||
));
|
||||
}
|
||||
|
||||
function openModalWithNotice() {
|
||||
function openNoticeModal() {
|
||||
openModal(props => (
|
||||
<Modal
|
||||
title="Modal with Notice"
|
||||
transitionState={props.transitionState}
|
||||
onClose={props.onClose}
|
||||
notice={{ message: "This is an informational notice", type: "info" }}
|
||||
actions={[
|
||||
{ text: "Got it", variant: "primary", onClick: props.onClose },
|
||||
]}
|
||||
actions={[{ text: "Got it", variant: "primary", onClick: props.onClose }]}
|
||||
>
|
||||
<Paragraph>Modals can display notices at the top for important information.</Paragraph>
|
||||
</Modal>
|
||||
|
|
@ -54,7 +118,6 @@ function openModalWithNotice() {
|
|||
|
||||
function ModalWithInputContent({ onClose, transitionState }: { onClose: () => void; transitionState: number; }) {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Modal with Input"
|
||||
|
|
@ -66,25 +129,12 @@ function ModalWithInputContent({ onClose, transitionState }: { onClose: () => vo
|
|||
{ text: "Submit", variant: "primary", onClick: () => { showToast(`Submitted: ${inputValue}`, ToastType.SUCCESS); onClose(); } },
|
||||
]}
|
||||
>
|
||||
<ManaTextInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
placeholder="Type something..."
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 8 }}>
|
||||
Using ManaTextInput inside the modal content.
|
||||
</Paragraph>
|
||||
<ManaTextInput value={inputValue} onChange={setInputValue} placeholder="Type something..." />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function openModalWithInput() {
|
||||
openModal(props => (
|
||||
<ModalWithInputContent onClose={props.onClose} transitionState={props.transitionState} />
|
||||
));
|
||||
}
|
||||
|
||||
function openConfirmModal() {
|
||||
function openConfirmCritical() {
|
||||
openModal(props => (
|
||||
<ConfirmModal
|
||||
title="Confirm Action"
|
||||
|
|
@ -93,12 +143,8 @@ function openConfirmModal() {
|
|||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
variant="critical"
|
||||
onConfirm={() => {
|
||||
showToast("Item deleted!", ToastType.SUCCESS);
|
||||
}}
|
||||
onCancel={() => {
|
||||
showToast("Cancelled", ToastType.MESSAGE);
|
||||
}}
|
||||
onConfirm={() => showToast("Item deleted!", ToastType.SUCCESS)}
|
||||
onCancel={() => showToast("Cancelled", ToastType.MESSAGE)}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<Paragraph>This action cannot be undone. The item will be permanently deleted.</Paragraph>
|
||||
|
|
@ -106,16 +152,14 @@ function openConfirmModal() {
|
|||
));
|
||||
}
|
||||
|
||||
function openConfirmModalPrimary() {
|
||||
function openConfirmPrimary() {
|
||||
openModal(props => (
|
||||
<ConfirmModal
|
||||
title="Save Changes"
|
||||
transitionState={props.transitionState}
|
||||
confirmText="Save"
|
||||
variant="primary"
|
||||
onConfirm={() => {
|
||||
showToast("Changes saved!", ToastType.SUCCESS);
|
||||
}}
|
||||
onConfirm={() => showToast("Changes saved!", ToastType.SUCCESS)}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<Paragraph>Do you want to save your changes?</Paragraph>
|
||||
|
|
@ -123,15 +167,14 @@ function openConfirmModalPrimary() {
|
|||
));
|
||||
}
|
||||
|
||||
function openExpressiveModal() {
|
||||
function openExpressive() {
|
||||
openModal(props => (
|
||||
<ExpressiveModal
|
||||
title="Welcome!"
|
||||
description="This is an expressive modal with a more visual design."
|
||||
subtitle="This is an expressive modal with a more visual design."
|
||||
transitionState={props.transitionState}
|
||||
actions={[
|
||||
{ text: "Get Started", variant: "primary", onClick: props.onClose },
|
||||
]}
|
||||
gradientColor="purple"
|
||||
actions={[{ text: "Get Started", variant: "primary", onClick: props.onClose }]}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
));
|
||||
|
|
@ -139,96 +182,92 @@ function openExpressiveModal() {
|
|||
|
||||
export default function ModalTab() {
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic Modal">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Simple modal with title, subtitle, content, and action buttons.
|
||||
</Paragraph>
|
||||
<ManaButton variant="secondary" text="Open Basic Modal" onClick={openBasicModal} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Modal with Notice">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Modal with a notice banner (info, warn, critical, positive).
|
||||
</Paragraph>
|
||||
<ManaButton variant="secondary" text="Open Modal with Notice" onClick={openModalWithNotice} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Modal with Input">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Modal with input controls in the header area.
|
||||
</Paragraph>
|
||||
<ManaButton variant="secondary" text="Open Modal with Input" onClick={openModalWithInput} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Confirm Modal (Critical)">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Confirmation dialog for destructive actions.
|
||||
</Paragraph>
|
||||
<ManaButton variant="critical-secondary" text="Open Confirm Modal" onClick={openConfirmModal} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Confirm Modal (Primary)">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Confirmation dialog for non-destructive actions.
|
||||
</Paragraph>
|
||||
<ManaButton variant="secondary" text="Open Save Dialog" onClick={openConfirmModalPrimary} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Expressive Modal">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Visual modal with graphic/badge support.
|
||||
</Paragraph>
|
||||
<ManaButton variant="secondary" text="Open Expressive Modal" onClick={openExpressiveModal} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Close All Modals">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Utility to close all open modals at once.
|
||||
</Paragraph>
|
||||
<ManaButton variant="critical-secondary" text="Close All Modals" onClick={closeAllModals} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Modal Props">
|
||||
<Paragraph color="text-muted">• title - Modal title (required)</Paragraph>
|
||||
<Paragraph color="text-muted">• subtitle - Optional subtitle</Paragraph>
|
||||
<Paragraph color="text-muted">• size - "sm" or "md"</Paragraph>
|
||||
<Paragraph color="text-muted">• children - Modal content</Paragraph>
|
||||
<Paragraph color="text-muted">• actions - Array of action buttons</Paragraph>
|
||||
<Paragraph color="text-muted">• notice - {"{ message, type }"} for notices</Paragraph>
|
||||
<Paragraph color="text-muted">• input - Controls in header area</Paragraph>
|
||||
<Paragraph color="text-muted">• onClose - Close handler (required)</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="ConfirmModal Props">
|
||||
<Paragraph color="text-muted">• confirmText - Confirm button text</Paragraph>
|
||||
<Paragraph color="text-muted">• cancelText - Cancel button text</Paragraph>
|
||||
<Paragraph color="text-muted">• variant - "critical" | "primary" | "secondary"</Paragraph>
|
||||
<Paragraph color="text-muted">• onConfirm - Confirm callback (can be async)</Paragraph>
|
||||
<Paragraph color="text-muted">• onCancel - Cancel callback</Paragraph>
|
||||
<Paragraph color="text-muted">• checkboxProps - Optional checkbox</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Action Button">
|
||||
<Paragraph color="text-muted">• text - Button text</Paragraph>
|
||||
<Paragraph color="text-muted">• variant - "primary" | "secondary" | "critical-primary"</Paragraph>
|
||||
<Paragraph color="text-muted">• onClick - Click handler</Paragraph>
|
||||
<Paragraph color="text-muted">• disabled - Disable button</Paragraph>
|
||||
<Paragraph color="text-muted">• loading - Show loading state</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="openModal Options">
|
||||
<Paragraph color="text-muted">• modalKey - Unique key for the modal</Paragraph>
|
||||
<Paragraph color="text-muted">• dismissable - Allow clicking backdrop to close</Paragraph>
|
||||
<Paragraph color="text-muted">• instant - Skip animation</Paragraph>
|
||||
<Paragraph color="text-muted">• onCloseCallback - Called when modal closes</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Import">
|
||||
<Paragraph color="text-muted">
|
||||
{"import { Modal, ConfirmModal, openModal, closeModal } from \"../components\";"}
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="Modal"
|
||||
overview="Discord's modal system includes Modal for general-purpose dialogs, ConfirmModal for confirmation flows, and ExpressiveModal for visual/onboarding experiences. All are opened via openModal() which handles backdrop, positioning, and transition states."
|
||||
notices={[
|
||||
{ type: "warn", children: "Always use openModal() to display modals. It handles the backdrop, animation state, stacking context, and cleanup. Rendering modals inline will not work correctly." },
|
||||
{ type: "info", children: "The transitionState prop from openModal's render callback must be forwarded to the modal component for enter/exit animations to work properly." },
|
||||
]}
|
||||
importPath={'import { Modal, ConfirmModal, ExpressiveModal, openModal, closeModal, closeAllModals } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic Modal",
|
||||
description: "Simple modal with title, subtitle, body content, and action buttons.",
|
||||
children: <ManaButton variant="secondary" text="Open Basic Modal" onClick={openBasicModal} />,
|
||||
code: `openModal(props => (
|
||||
<Modal
|
||||
title="My Modal"
|
||||
subtitle="Subtitle text"
|
||||
transitionState={props.transitionState}
|
||||
onClose={props.onClose}
|
||||
actions={[
|
||||
{ text: "Cancel", variant: "secondary", onClick: props.onClose },
|
||||
{ text: "Confirm", variant: "primary", onClick: handleConfirm },
|
||||
]}
|
||||
>
|
||||
<p>Modal content here</p>
|
||||
</Modal>
|
||||
))`,
|
||||
relevantProps: ["title", "subtitle", "actions"],
|
||||
},
|
||||
{
|
||||
title: "Modal with Notice",
|
||||
description: "A notice banner appears at the top of the modal for important info.",
|
||||
children: <ManaButton variant="secondary" text="Open Modal with Notice" onClick={openNoticeModal} />,
|
||||
relevantProps: ["notice"],
|
||||
},
|
||||
{
|
||||
title: "Modal with Input",
|
||||
description: "Modal with form controls in the body area.",
|
||||
children: (
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="Open Modal with Input"
|
||||
onClick={() => openModal(props => (
|
||||
<ModalWithInputContent onClose={props.onClose} transitionState={props.transitionState} />
|
||||
))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "ConfirmModal (Critical)",
|
||||
description: "Confirmation dialog for destructive actions with danger styling.",
|
||||
children: <ManaButton variant="critical-secondary" text="Open Confirm Modal" onClick={openConfirmCritical} />,
|
||||
code: `openModal(props => (
|
||||
<ConfirmModal
|
||||
title="Delete Item?"
|
||||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
variant="critical"
|
||||
onConfirm={() => deleteItem()}
|
||||
onClose={props.onClose}
|
||||
transitionState={props.transitionState}
|
||||
>
|
||||
<p>This cannot be undone.</p>
|
||||
</ConfirmModal>
|
||||
))`,
|
||||
relevantProps: ["confirmText", "variant", "onConfirm"],
|
||||
},
|
||||
{
|
||||
title: "ConfirmModal (Primary)",
|
||||
description: "Confirmation dialog for non-destructive actions.",
|
||||
children: <ManaButton variant="secondary" text="Open Save Dialog" onClick={openConfirmPrimary} />,
|
||||
},
|
||||
{
|
||||
title: "ExpressiveModal",
|
||||
description: "Visual modal for onboarding or promotional experiences.",
|
||||
children: <ManaButton variant="secondary" text="Open Expressive Modal" onClick={openExpressive} />,
|
||||
relevantProps: ["subtitle", "gradientColor", "graphic", "badge"],
|
||||
},
|
||||
{
|
||||
title: "Close All Modals",
|
||||
description: "Utility to close all open modals at once.",
|
||||
children: <ManaButton variant="critical-secondary" text="Close All Modals" onClick={closeAllModals} />,
|
||||
code: "closeAllModals();",
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Message Types">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Notice component for displaying informational messages with different severity levels.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
{NoticeTypes.map(type => (
|
||||
<div key={type} style={{ textAlign: "center" }}>
|
||||
<Notice messageType={type}>
|
||||
This is a {type} notice message.
|
||||
</Notice>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
{type}
|
||||
</Paragraph>
|
||||
<DocPage
|
||||
componentName="Notice"
|
||||
overview="Notice displays informational messages with different severity levels. Each type has a distinct color and icon. Supports action buttons, custom icons, text variants, and can be hidden without unmounting."
|
||||
importPath={'import { Notice, NoticeTypes } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Message Types",
|
||||
description: "All five severity levels with distinct colors and icons.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{NoticeTypes.map(type => (
|
||||
<Notice key={type} messageType={type}>
|
||||
This is a {type} notice message.
|
||||
</Notice>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Action">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Notices can include action buttons for user interaction.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<Notice
|
||||
messageType="info"
|
||||
action={<Button size="small" variant="secondary">Action</Button>}
|
||||
>
|
||||
Notice with an action button.
|
||||
</Notice>
|
||||
<Notice
|
||||
messageType="warn"
|
||||
action={<Button size="small" variant="primary">Fix Now</Button>}
|
||||
>
|
||||
Warning that requires attention.
|
||||
</Notice>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Hidden State">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
The hidden prop controls visibility without removing from DOM.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Notice messageType="info" hidden={false}>
|
||||
This notice is visible.
|
||||
</Notice>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
hidden=false
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Notice messageType="info" hidden={true}>
|
||||
This notice is hidden.
|
||||
</Notice>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
hidden=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Text Variants">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Custom text styling via the textVariant prop.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Notice messageType="info">
|
||||
Default text variant.
|
||||
</Notice>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
text-sm/medium (default)
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Notice messageType="info" textVariant="text-md/normal">
|
||||
Larger text variant.
|
||||
</Notice>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
text-md/normal
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• children: ReactNode - Notice content
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• messageType: "info" | "warn" | "danger" | "positive" | "preview"
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• action?: ReactNode - Action button or element
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• hidden?: boolean - Hide the notice
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• textColor?: string - Custom text color
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• textVariant?: string - Text size/weight variant
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• icon?: ComponentType - Custom icon component
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• className?: string - Additional CSS class
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
),
|
||||
code: '<Notice messageType="info">Informational message.</Notice>',
|
||||
relevantProps: ["messageType"],
|
||||
},
|
||||
{
|
||||
title: "With Action Button",
|
||||
description: "Notices can include action buttons for user interaction.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<Notice
|
||||
messageType="info"
|
||||
action={<Button size="small" variant="secondary">Action</Button>}
|
||||
>
|
||||
Notice with an action button.
|
||||
</Notice>
|
||||
<Notice
|
||||
messageType="warn"
|
||||
action={<Button size="small" variant="primary">Fix Now</Button>}
|
||||
>
|
||||
Warning that requires attention.
|
||||
</Notice>
|
||||
</div>
|
||||
),
|
||||
code: `<Notice
|
||||
messageType="warn"
|
||||
action={<Button size="small" variant="primary">Fix Now</Button>}
|
||||
>
|
||||
Warning that requires attention.
|
||||
</Notice>`,
|
||||
relevantProps: ["action"],
|
||||
},
|
||||
{
|
||||
title: "Hidden State",
|
||||
description: "The hidden prop controls visibility without removing from the DOM.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>hidden=false:</Paragraph>
|
||||
<Notice messageType="info" hidden={false}>
|
||||
This notice is visible.
|
||||
</Notice>
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>hidden=true (invisible below):</Paragraph>
|
||||
<Notice messageType="info" hidden={true}>
|
||||
This notice is hidden.
|
||||
</Notice>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["hidden"],
|
||||
},
|
||||
{
|
||||
title: "Text Variants",
|
||||
description: "Customize text sizing with the textVariant prop.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Default (text-sm/medium):</Paragraph>
|
||||
<Notice messageType="info">Default text variant.</Notice>
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>text-md/normal:</Paragraph>
|
||||
<Notice messageType="info" textVariant="text-md/normal">Larger text variant.</Notice>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["textVariant"],
|
||||
},
|
||||
]}
|
||||
props={NOTICE_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Pagination component for navigating through pages of content.
|
||||
</Paragraph>
|
||||
<Paginator
|
||||
currentPage={page1}
|
||||
totalCount={100}
|
||||
pageSize={10}
|
||||
onPageChange={setPage1}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Max Visible Pages">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Control how many page numbers are visible at once.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Paginator
|
||||
currentPage={page2}
|
||||
totalCount={100}
|
||||
pageSize={10}
|
||||
maxVisiblePages={3}
|
||||
onPageChange={setPage2}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
3 visible
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Paginator
|
||||
currentPage={page3}
|
||||
totalCount={100}
|
||||
pageSize={10}
|
||||
maxVisiblePages={7}
|
||||
onPageChange={setPage3}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
7 visible
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Options">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Additional configuration options for the paginator.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Paginator
|
||||
currentPage={page4}
|
||||
totalCount={100}
|
||||
pageSize={10}
|
||||
hideMaxPage
|
||||
onPageChange={setPage4}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
hideMaxPage
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Paginator
|
||||
currentPage={page4}
|
||||
totalCount={50}
|
||||
pageSize={10}
|
||||
disablePaginationGap
|
||||
onPageChange={setPage4}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
disablePaginationGap
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• currentPage: number - Current active page
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• totalCount: number - Total number of items
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• pageSize: number - Items per page
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onPageChange?: (page: number) => void - Page change callback
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• maxVisiblePages?: number - Max page numbers shown
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• hideMaxPage?: boolean - Hide last page number
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disablePaginationGap?: boolean - Remove ellipsis gaps
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• className?: string - Additional CSS class
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Page {page} of 10</Paragraph>
|
||||
<Paginator currentPage={page} totalCount={100} pageSize={10} onPageChange={setPage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MaxVisibleDemo() {
|
||||
const [page3, setPage3] = useState(1);
|
||||
const [page7, setPage7] = useState(1);
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>maxVisiblePages=3:</Paragraph>
|
||||
<Paginator currentPage={page3} totalCount={100} pageSize={10} maxVisiblePages={3} onPageChange={setPage3} />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>maxVisiblePages=7:</Paragraph>
|
||||
<Paginator currentPage={page7} totalCount={100} pageSize={10} maxVisiblePages={7} onPageChange={setPage7} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionsDemo() {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>hideMaxPage:</Paragraph>
|
||||
<Paginator currentPage={page} totalCount={100} pageSize={10} hideMaxPage onPageChange={setPage} />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>disablePaginationGap:</Paragraph>
|
||||
<Paginator currentPage={page} totalCount={50} pageSize={10} disablePaginationGap onPageChange={setPage} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PaginatorTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="Paginator"
|
||||
overview="Paginator provides page navigation for paginated content. Shows page numbers with back/next buttons, ellipsis gaps for large page ranges, and supports customization of visible page count and gap behavior."
|
||||
notices={[
|
||||
{ type: "info", children: "Paginator is 1-based. The currentPage value starts at 1, not 0." },
|
||||
]}
|
||||
importPath={'import { Paginator } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Standard paginator with 10 pages.",
|
||||
children: <BasicDemo />,
|
||||
code: `<Paginator
|
||||
currentPage={page}
|
||||
totalCount={100}
|
||||
pageSize={10}
|
||||
onPageChange={setPage}
|
||||
/>`,
|
||||
relevantProps: ["currentPage", "totalCount", "pageSize", "onPageChange"],
|
||||
},
|
||||
{
|
||||
title: "Max Visible Pages",
|
||||
description: "Control how many page numbers are visible before ellipsis appears.",
|
||||
children: <MaxVisibleDemo />,
|
||||
relevantProps: ["maxVisiblePages"],
|
||||
},
|
||||
{
|
||||
title: "Options",
|
||||
description: "Hide the last page number or remove ellipsis gaps entirely.",
|
||||
children: <OptionsDemo />,
|
||||
relevantProps: ["hideMaxPage", "disablePaginationGap"],
|
||||
},
|
||||
]}
|
||||
props={PAGINATOR_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>", 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<HTMLDivElement>", 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<HTMLDivElement>(null);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={wrapperRef} style={{ display: "inline-block" }}>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={label}
|
||||
onClick={() => setShow(!show)}
|
||||
/>
|
||||
<ManaButton variant="secondary" text={label} onClick={() => setShow(!show)} />
|
||||
</div>
|
||||
<ManaPopover
|
||||
targetElementRef={wrapperRef}
|
||||
|
|
@ -36,115 +60,83 @@ function PopoverDemo({ title, body, size, actions, position, label }: { title: s
|
|||
|
||||
export default function PopoverTab() {
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic Popover">
|
||||
<div className="vc-compfinder-grid">
|
||||
<PopoverDemo
|
||||
label="Click me"
|
||||
title="Popover Title"
|
||||
body="This is the popover body text with some helpful information."
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
<DocPage
|
||||
componentName="ManaPopover"
|
||||
overview="ManaPopover is Discord's popover component that anchors to a target element. Supports title, body text, action buttons, graphics, text links, and badge elements. Positioned relative to the target with configurable direction and size."
|
||||
notices={[
|
||||
{ type: "warn", children: "The targetElementRef must point to a mounted DOM element. If the ref is null when shouldShow becomes true, the popover will not position correctly." },
|
||||
{ type: "info", children: "Handle onRequestClose to dismiss the popover when the user clicks outside or presses Escape. Without it, the popover will remain visible until shouldShow is manually set to false." },
|
||||
]}
|
||||
importPath={'import { ManaPopover } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Simple popover with title and body text.",
|
||||
children: (
|
||||
<PopoverDemo
|
||||
label="Click me"
|
||||
title="Popover Title"
|
||||
body="This is the popover body text with some helpful information."
|
||||
/>
|
||||
),
|
||||
code: `const ref = useRef<HTMLDivElement>(null);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
<SectionWrapper title="Sizes">
|
||||
<div className="vc-compfinder-grid">
|
||||
<PopoverDemo
|
||||
label="sm"
|
||||
title="Small Popover"
|
||||
body="This is a small popover."
|
||||
size="sm"
|
||||
/>
|
||||
<PopoverDemo
|
||||
label="md"
|
||||
title="Medium Popover"
|
||||
body="This is a medium popover (default)."
|
||||
size="md"
|
||||
/>
|
||||
<PopoverDemo
|
||||
label="lg"
|
||||
title="Large Popover"
|
||||
body="This is a large popover with more space."
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Positions">
|
||||
<div className="vc-compfinder-grid">
|
||||
<PopoverDemo
|
||||
label="top"
|
||||
title="Top Position"
|
||||
body="Popover appears above."
|
||||
position="top"
|
||||
/>
|
||||
<PopoverDemo
|
||||
label="bottom"
|
||||
title="Bottom Position"
|
||||
body="Popover appears below."
|
||||
position="bottom"
|
||||
/>
|
||||
<PopoverDemo
|
||||
label="left"
|
||||
title="Left Position"
|
||||
body="Popover appears to the left."
|
||||
position="left"
|
||||
/>
|
||||
<PopoverDemo
|
||||
label="right"
|
||||
title="Right Position"
|
||||
body="Popover appears to the right."
|
||||
position="right"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Actions">
|
||||
<div className="vc-compfinder-grid">
|
||||
<PopoverDemo
|
||||
label="With Actions"
|
||||
title="Popover with Actions"
|
||||
body="This popover has action buttons."
|
||||
actions={[
|
||||
{ text: "Primary", variant: "primary" },
|
||||
{ text: "Secondary", variant: "secondary" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• targetElementRef: React.RefObject - Ref to trigger element
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• shouldShow: boolean - Controls visibility
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onRequestClose?: (reason: string) => void - Close callback
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• title?: string - Popover title
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• body?: string - Popover body text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• size?: "sm" | "md" | "lg" - Popover size
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• position?: "top" | "bottom" | "left" | "right" - Position
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• actions?: PopoverAction[] - Action buttons
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• textLink?: {"{"} text: string, onClick?: () => void {"}"} - Link
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• graphic?: {"{"} src: string, aspectRatio?: string {"}"} - Image
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<div ref={ref}>
|
||||
<Button onClick={() => setShow(!show)}>Click me</Button>
|
||||
</div>
|
||||
<ManaPopover
|
||||
targetElementRef={ref}
|
||||
shouldShow={show}
|
||||
onRequestClose={() => 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: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<PopoverDemo label="sm" title="Small" body="Small popover." size="sm" />
|
||||
<PopoverDemo label="md" title="Medium" body="Medium popover (default)." size="md" />
|
||||
<PopoverDemo label="lg" title="Large" body="Large popover with more space." size="lg" />
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["size"],
|
||||
},
|
||||
{
|
||||
title: "Positions",
|
||||
description: "Popover can be positioned on any side of the target element.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<PopoverDemo label="top" title="Top" body="Appears above." position="top" />
|
||||
<PopoverDemo label="bottom" title="Bottom" body="Appears below." position="bottom" />
|
||||
<PopoverDemo label="left" title="Left" body="Appears left." position="left" />
|
||||
<PopoverDemo label="right" title="Right" body="Appears right." position="right" />
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["position"],
|
||||
},
|
||||
{
|
||||
title: "With Actions",
|
||||
description: "Popover with action buttons in the footer.",
|
||||
children: (
|
||||
<PopoverDemo
|
||||
label="With Actions"
|
||||
title="Popover with Actions"
|
||||
body="This popover has action buttons."
|
||||
actions={[
|
||||
{ text: "Primary", variant: "primary" },
|
||||
{ text: "Secondary", variant: "secondary" },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
relevantProps: ["actions"],
|
||||
},
|
||||
]}
|
||||
props={POPOVER_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Variants">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Blue (default) and Orange variants.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Blue:</Paragraph>
|
||||
<ProgressBar progress={60} variant="blue" />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Orange:</Paragraph>
|
||||
<ProgressBar progress={60} variant="orange" />
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Progress Values">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>0%:</Paragraph>
|
||||
<ProgressBar progress={0} variant="blue" />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>25%:</Paragraph>
|
||||
<ProgressBar progress={25} variant="blue" />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>50%:</Paragraph>
|
||||
<ProgressBar progress={50} variant="blue" />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>75%:</Paragraph>
|
||||
<ProgressBar progress={75} variant="blue" />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>100%:</Paragraph>
|
||||
<ProgressBar progress={100} variant="blue" />
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Animated">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Progress: {animatedProgress}%
|
||||
</Paragraph>
|
||||
<ProgressBar progress={animatedProgress} variant="blue" />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Custom Range">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Using minimum=0 and maximum=200, progress=150 (75% filled).
|
||||
</Paragraph>
|
||||
<ProgressBar progress={150} minimum={0} maximum={200} variant="orange" />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Custom Override">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Using variant="unset" with custom gradient colors.
|
||||
</Paragraph>
|
||||
<ProgressBar
|
||||
progress={70}
|
||||
variant="unset"
|
||||
override={{
|
||||
default: {
|
||||
background: "rgba(88, 101, 242, 0.3)",
|
||||
gradientStart: "#5865F2",
|
||||
gradientEnd: "#EB459E"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• progress: number - Current progress value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• minimum?: number - Minimum value (default: 0)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• maximum?: number - Maximum value (default: 100)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• variant?: "blue" | "orange" | "unset" - Color variant
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• override?: object - Custom colors when variant="unset"
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• labelledBy?: string - ID of labelling element for a11y
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Progress: {progress}%</Paragraph>
|
||||
<ProgressBar progress={progress} variant="blue" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProgressBarTab() {
|
||||
return (
|
||||
<DocPage
|
||||
overview="ProgressBar displays a horizontal progress indicator with support for blue and orange color variants, light and medium weights, a glow effect, custom color overrides via gradients, and configurable min/max ranges."
|
||||
importPath={'import { ProgressBar } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Variants",
|
||||
description: "Blue (default) and orange color variants.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Blue:</Paragraph>
|
||||
<ProgressBar progress={60} variant="blue" />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Orange:</Paragraph>
|
||||
<ProgressBar progress={60} variant="orange" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
code: '<ProgressBar progress={60} variant="blue" />',
|
||||
relevantProps: ["variant"],
|
||||
},
|
||||
{
|
||||
title: "Progress Values",
|
||||
description: "Various progress levels from 0% to 100%.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{[0, 25, 50, 75, 100].map(v => (
|
||||
<div key={v}>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>{v}%:</Paragraph>
|
||||
<ProgressBar progress={v} variant="blue" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["progress"],
|
||||
},
|
||||
{
|
||||
title: "Animated",
|
||||
description: "Progress bar that increments automatically.",
|
||||
children: <AnimatedDemo />,
|
||||
},
|
||||
{
|
||||
title: "Weight",
|
||||
description: "Light (default) and medium weight variants.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Light (default):</Paragraph>
|
||||
<ProgressBar progress={60} weight="light" />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Medium:</Paragraph>
|
||||
<ProgressBar progress={60} weight="medium" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
code: '<ProgressBar progress={60} weight="medium" />',
|
||||
relevantProps: ["weight"],
|
||||
},
|
||||
{
|
||||
title: "Glowing",
|
||||
description: "Glow effect is enabled by default. Set glowing={false} to disable.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Glowing (default):</Paragraph>
|
||||
<ProgressBar progress={60} glowing={true} />
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>No glow:</Paragraph>
|
||||
<ProgressBar progress={60} glowing={false} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
code: "<ProgressBar progress={60} glowing={false} />",
|
||||
relevantProps: ["glowing"],
|
||||
},
|
||||
{
|
||||
title: "Custom Range",
|
||||
description: "Using minimum=0 and maximum=200 with progress=150 (75% filled).",
|
||||
children: <ProgressBar progress={150} minimum={0} maximum={200} variant="orange" />,
|
||||
code: '<ProgressBar progress={150} minimum={0} maximum={200} variant="orange" />',
|
||||
relevantProps: ["minimum", "maximum"],
|
||||
},
|
||||
{
|
||||
title: "Custom Override",
|
||||
description: 'Using variant="unset" with custom gradient colors.',
|
||||
children: (
|
||||
<ProgressBar
|
||||
progress={70}
|
||||
variant="unset"
|
||||
override={{
|
||||
default: {
|
||||
background: "rgba(88, 101, 242, 0.3)",
|
||||
gradientStart: "#5865F2",
|
||||
gradientEnd: "#EB459E",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
code: `<ProgressBar
|
||||
progress={70}
|
||||
variant="unset"
|
||||
override={{
|
||||
default: {
|
||||
background: "rgba(88, 101, 242, 0.3)",
|
||||
gradientStart: "#5865F2",
|
||||
gradientEnd: "#EB459E",
|
||||
},
|
||||
}}
|
||||
/>`,
|
||||
relevantProps: ["override"],
|
||||
},
|
||||
]}
|
||||
props={PROGRESSBAR_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string | number>("option1");
|
||||
return <ManaBaseRadioGroup options={BASIC_OPTIONS} value={value} onChange={setValue} />;
|
||||
}
|
||||
|
||||
export default function RadioGroupTab() {
|
||||
const [basicValue, setBasicValue] = useState<string | number>("option1");
|
||||
const [descValue, setDescValue] = useState<string | number>("medium");
|
||||
const [iconValue, setIconValue] = useState<string | number>("item1");
|
||||
const [mixedValue, setMixedValue] = useState<string | number>("enabled");
|
||||
const [numberedValue, setNumberedValue] = useState<number>(1);
|
||||
const [unselectedValue, setUnselectedValue] = useState<string | number | undefined>(undefined);
|
||||
const [standaloneSelected, setStandaloneSelected] = useState(false);
|
||||
function DescDemo() {
|
||||
const [value, setValue] = useState<string | number>("medium");
|
||||
return <ManaBaseRadioGroup options={DESC_OPTIONS} value={value} onChange={setValue} />;
|
||||
}
|
||||
|
||||
function IconDemo() {
|
||||
const [value, setValue] = useState<string | number>("item1");
|
||||
return <ManaBaseRadioGroup options={ICON_OPTIONS} value={value} onChange={setValue} />;
|
||||
}
|
||||
|
||||
function DisabledItemDemo() {
|
||||
const [value, setValue] = useState<string | number>("enabled");
|
||||
return <ManaBaseRadioGroup options={MIXED_OPTIONS} value={value} onChange={setValue} />;
|
||||
}
|
||||
|
||||
function UnselectedDemo() {
|
||||
const [value, setValue] = useState<string | number | undefined>(undefined);
|
||||
return <ManaBaseRadioGroup options={BASIC_OPTIONS} value={value} onChange={setValue} />;
|
||||
}
|
||||
|
||||
function StandaloneDemo() {
|
||||
const [selected, setSelected] = useState(false);
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Simple radio group with text-only options.
|
||||
</Paragraph>
|
||||
<ManaBaseRadioGroup
|
||||
options={BASIC_OPTIONS}
|
||||
value={basicValue}
|
||||
onChange={setBasicValue}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Descriptions">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Radio options with descriptive text using the desc prop.
|
||||
</Paragraph>
|
||||
<ManaBaseRadioGroup
|
||||
options={OPTIONS_WITH_DESCRIPTIONS}
|
||||
value={descValue}
|
||||
onChange={setDescValue}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Leading Icons">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Radio options with icons using leadingIcon prop.
|
||||
</Paragraph>
|
||||
<ManaBaseRadioGroup
|
||||
options={OPTIONS_WITH_ICONS}
|
||||
value={iconValue}
|
||||
onChange={setIconValue}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Disabled Option">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Individual options can be disabled with disabled: true.
|
||||
</Paragraph>
|
||||
<ManaBaseRadioGroup
|
||||
options={OPTIONS_MIXED}
|
||||
value={mixedValue}
|
||||
onChange={setMixedValue}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Numeric Values">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Radio options can use numeric values.
|
||||
</Paragraph>
|
||||
<ManaBaseRadioGroup
|
||||
options={NUMBERED_OPTIONS}
|
||||
value={numberedValue}
|
||||
onChange={v => setNumberedValue(v as number)}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="No Initial Selection">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Radio group with no initial selection (value undefined).
|
||||
</Paragraph>
|
||||
<ManaBaseRadioGroup
|
||||
options={BASIC_OPTIONS}
|
||||
value={unselectedValue}
|
||||
onChange={setUnselectedValue}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Entire Group Disabled">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
The entire group can be disabled with disabled prop.
|
||||
</Paragraph>
|
||||
<ManaBaseRadioGroup
|
||||
options={BASIC_OPTIONS}
|
||||
value="option2"
|
||||
disabled
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Standalone Radio Indicator">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
A standalone animated radio indicator component. Click to toggle.
|
||||
</Paragraph>
|
||||
<div
|
||||
onClick={() => setStandaloneSelected(!standaloneSelected)}
|
||||
style={{ cursor: "pointer", display: "inline-block" }}
|
||||
>
|
||||
<StandaloneRadioIndicator isSelected={standaloneSelected} />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>BaseRadioGroup</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• options: RadioOption[] - Array of radio options
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• value?: string | number - Currently selected value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (value: string | number) => void - Called when selection changes
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable entire group
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• aria-labelledby?: string - Accessibility label reference
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>RadioOption</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• value: string | number - Option value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• name: string - Option label text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• desc?: string - Optional description text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable individual option
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• leadingIcon?: React.ComponentType - Icon before radio
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>StandaloneRadioIndicator</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• isSelected: boolean - Whether the indicator is selected
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable the indicator
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div onClick={() => setSelected(!selected)} style={{ cursor: "pointer", display: "inline-block" }}>
|
||||
<StandaloneRadioIndicator isSelected={selected} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RadioGroupTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="ManaBaseRadioGroup"
|
||||
overview="ManaBaseRadioGroup renders a group of mutually exclusive radio options. Options support descriptions, leading icons, and individual disabled states. StandaloneRadioIndicator is an animated radio circle for custom radio implementations."
|
||||
importPath={'import { ManaBaseRadioGroup, StandaloneRadioIndicator, RadioOption } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Simple radio group with text-only options.",
|
||||
children: <BasicDemo />,
|
||||
code: `<ManaBaseRadioGroup
|
||||
options={[
|
||||
{ value: "a", name: "Option A" },
|
||||
{ value: "b", name: "Option B" },
|
||||
]}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
/>`,
|
||||
relevantProps: ["options", "value", "onChange"],
|
||||
},
|
||||
{
|
||||
title: "With Descriptions",
|
||||
description: "Options with descriptive text using the description field.",
|
||||
children: <DescDemo />,
|
||||
},
|
||||
{
|
||||
title: "With Leading Icons",
|
||||
description: "Each option can have an icon component before the radio indicator.",
|
||||
children: <IconDemo />,
|
||||
},
|
||||
{
|
||||
title: "Disabled Option",
|
||||
description: "Individual options can be disabled while others remain interactive.",
|
||||
children: <DisabledItemDemo />,
|
||||
},
|
||||
{
|
||||
title: "No Initial Selection",
|
||||
description: "Radio group with no option initially selected.",
|
||||
children: <UnselectedDemo />,
|
||||
},
|
||||
{
|
||||
title: "Entire Group Disabled",
|
||||
description: "The entire group is non-interactive.",
|
||||
children: <ManaBaseRadioGroup options={BASIC_OPTIONS} value="option2" disabled />,
|
||||
relevantProps: ["disabled"],
|
||||
},
|
||||
{
|
||||
title: "Standalone Radio Indicator",
|
||||
description: "An animated radio circle component for custom radio UIs. Click to toggle.",
|
||||
children: <StandaloneDemo />,
|
||||
code: "<StandaloneRadioIndicator isSelected={selected} />",
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>", description: "Ref to an external trigger element instead of wrapping children." },
|
||||
{ name: "anchorRef", type: "RefObject<HTMLElement>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Rich Tooltip - Basic">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Rich tooltip with title and body text.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip
|
||||
title="Tooltip Title"
|
||||
body="This is the body text of the rich tooltip with more detailed information."
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Hover me" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Rich Tooltip - Body Only">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Rich tooltip without title.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip
|
||||
body="This tooltip only has body text, no title."
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Body Only" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Positions">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip
|
||||
title="Top"
|
||||
body="Positioned at top"
|
||||
position="top"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Top" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip
|
||||
title="Bottom"
|
||||
body="Positioned at bottom"
|
||||
position="bottom"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Bottom" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip
|
||||
title="Left"
|
||||
body="Positioned at left"
|
||||
position="left"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Left" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip
|
||||
title="Right"
|
||||
body="Positioned at right"
|
||||
position="right"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Right" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Alignment">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip
|
||||
title="Start Aligned"
|
||||
body="Tooltip aligned to start"
|
||||
align="start"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Start" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip
|
||||
title="Center Aligned"
|
||||
body="Tooltip aligned to center"
|
||||
align="center"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Center" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip
|
||||
title="End Aligned"
|
||||
body="Tooltip aligned to end"
|
||||
align="end"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="End" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Custom Spacing">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip
|
||||
title="More Spacing"
|
||||
body="This tooltip has increased spacing from the target element."
|
||||
spacing={16}
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="16px Spacing" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip
|
||||
title="Less Spacing"
|
||||
body="This tooltip has minimal spacing."
|
||||
spacing={4}
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="4px Spacing" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Container Element Types">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip
|
||||
title="Span Container"
|
||||
body="Uses span as container element (default)"
|
||||
element="span"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Span" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip
|
||||
title="Div Container"
|
||||
body="Uses div as container element"
|
||||
element="div"
|
||||
asContainer
|
||||
>
|
||||
<ManaButton variant="secondary" text="Div" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• title?: string - Tooltip title (bold)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• body?: string - Tooltip body text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• children: ReactNode - Trigger element
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• position?: "top" | "bottom" | "left" | "right" - Position
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• align?: "start" | "center" | "end" - Alignment
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• spacing?: number - Distance from trigger (px)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• asContainer?: boolean - Wrap children in container
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• element?: "span" | "div" - Container element type
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="ManaRichTooltip"
|
||||
overview="ManaRichTooltip is Discord's rich tooltip component that displays a title, body text, and optional assets on hover. Unlike ManaTooltip which is text-only, RichTooltip supports structured content with headings and descriptions."
|
||||
notices={[
|
||||
{ type: "info", children: "Use asContainer when wrapping components that don't forward refs or hover props. Without it, the tooltip may not detect hover events on the trigger element." },
|
||||
]}
|
||||
importPath={'import { ManaRichTooltip } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Rich tooltip with title and body text. Hover to preview.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip title="Tooltip Title" body="This is the body text with more detailed information." asContainer>
|
||||
<ManaButton variant="secondary" text="Hover me" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip body="This tooltip only has body text, no title." asContainer>
|
||||
<ManaButton variant="secondary" text="Body Only" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
),
|
||||
code: `<ManaRichTooltip title="Title" body="Body text here." asContainer>
|
||||
<Button>Hover me</Button>
|
||||
</ManaRichTooltip>`,
|
||||
relevantProps: ["title", "body", "asContainer"],
|
||||
},
|
||||
{
|
||||
title: "Positions",
|
||||
description: "Tooltip can appear on any side of the trigger element.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{(["top", "bottom", "left", "right"] as const).map(pos => (
|
||||
<ManaRichTooltip key={pos} title={pos} body={`Positioned at ${pos}`} position={pos} asContainer>
|
||||
<ManaButton variant="secondary" text={pos} />
|
||||
</ManaRichTooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["position"],
|
||||
},
|
||||
{
|
||||
title: "Alignment",
|
||||
description: "Align the tooltip along the position axis.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{(["start", "center", "end"] as const).map(align => (
|
||||
<ManaRichTooltip key={align} title={`${align} aligned`} body={`Tooltip aligned to ${align}`} align={align} asContainer>
|
||||
<ManaButton variant="secondary" text={align} />
|
||||
</ManaRichTooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["align"],
|
||||
},
|
||||
{
|
||||
title: "Custom Spacing",
|
||||
description: "Control the distance between tooltip and trigger.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip title="4px spacing" body="Minimal spacing." spacing={4} asContainer>
|
||||
<ManaButton variant="secondary" text="4px" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip title="16px spacing" body="Increased spacing." spacing={16} asContainer>
|
||||
<ManaButton variant="secondary" text="16px" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["spacing"],
|
||||
},
|
||||
{
|
||||
title: "Container Element",
|
||||
description: "Choose the wrapper element type for the trigger.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaRichTooltip title="Span" body="Uses span container (default)" element="span" asContainer>
|
||||
<ManaButton variant="secondary" text="Span" />
|
||||
</ManaRichTooltip>
|
||||
<ManaRichTooltip title="Div" body="Uses div container" element="div" asContainer>
|
||||
<ManaButton variant="secondary" text="Div" />
|
||||
</ManaRichTooltip>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["element"],
|
||||
},
|
||||
]}
|
||||
props={RICHTOOLTIP_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any>; 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Controls">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={`Orientation: ${orientation}`}
|
||||
onClick={() => setOrientation(o => o === "vertical" ? "horizontal" : "vertical")}
|
||||
/>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={`Fade: ${fade}`}
|
||||
onClick={() => setFade(f => !f)}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="ScrollerThin">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Thin scrollbar, most commonly used. Ideal for sidebars and lists.
|
||||
</Paragraph>
|
||||
<ScrollerThin orientation={orientation} fade={fade} style={scrollerStyle}>
|
||||
{SAMPLE_ITEMS.map(item => (
|
||||
<div key={item} style={itemStyle}>
|
||||
<Paragraph>{item}</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</ScrollerThin>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="ScrollerAuto">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Standard scrollbar that appears automatically when content overflows.
|
||||
</Paragraph>
|
||||
<ScrollerAuto orientation={orientation} fade={fade} style={scrollerStyle}>
|
||||
{SAMPLE_ITEMS.map(item => (
|
||||
<div key={item} style={itemStyle}>
|
||||
<Paragraph>{item}</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</ScrollerAuto>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="ScrollerNone">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
No visible scrollbar, content still scrollable via mouse wheel/touch.
|
||||
</Paragraph>
|
||||
<ScrollerNone orientation={orientation} fade={fade} style={scrollerStyle}>
|
||||
{SAMPLE_ITEMS.map(item => (
|
||||
<div key={item} style={itemStyle}>
|
||||
<Paragraph>{item}</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</ScrollerNone>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• orientation - "vertical" | "horizontal" (default: "vertical")</Paragraph>
|
||||
<Paragraph color="text-muted">• fade - boolean, adds fade effect at edges</Paragraph>
|
||||
<Paragraph color="text-muted">• className - custom CSS class</Paragraph>
|
||||
<Paragraph color="text-muted">• style - inline styles</Paragraph>
|
||||
<Paragraph color="text-muted">• children - content to scroll</Paragraph>
|
||||
<Paragraph color="text-muted">• dir - text direction ("ltr" | "rtl")</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Variants">
|
||||
<Paragraph color="text-muted">• ScrollerThin - Thin scrollbar (most common)</Paragraph>
|
||||
<Paragraph color="text-muted">• ScrollerAuto - Standard auto-showing scrollbar</Paragraph>
|
||||
<Paragraph color="text-muted">• ScrollerNone - Hidden scrollbar</Paragraph>
|
||||
<Paragraph color="text-muted">• ListScrollerThin - Virtualized list with thin scrollbar</Paragraph>
|
||||
<Paragraph color="text-muted">• ListScrollerAuto - Virtualized list with auto scrollbar</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Import">
|
||||
<Paragraph color="text-muted">
|
||||
{"import { ScrollerThin, ScrollerAuto, ScrollerNone } from \"@webpack/common\";"}
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div>
|
||||
<div className="vc-compfinder-grid" style={{ marginBottom: 8 }}>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={`Orientation: ${orientation}`}
|
||||
onClick={() => setOrientation(o => o === "vertical" ? "horizontal" : "vertical")}
|
||||
/>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={`Fade: ${fade}`}
|
||||
onClick={() => setFade(f => !f)}
|
||||
/>
|
||||
</div>
|
||||
<Component orientation={orientation} fade={fade} style={scrollerStyle}>
|
||||
{SAMPLE_ITEMS.map(item => (
|
||||
<div key={item} style={itemStyle}>
|
||||
<Paragraph>{item}</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</Component>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ScrollerTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="Scroller"
|
||||
overview="Discord provides three scroller variants for different scrollbar styles: ScrollerThin (thin scrollbar, most common), ScrollerAuto (standard auto-showing scrollbar), and ScrollerNone (hidden scrollbar, still scrollable). All support vertical/horizontal orientation and edge fade effects."
|
||||
importPath={'import { ScrollerThin, ScrollerAuto, ScrollerNone } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "ScrollerThin",
|
||||
description: "Thin scrollbar, the most commonly used variant. Ideal for sidebars and lists.",
|
||||
children: <ScrollerDemo Component={ScrollerThin} label="ScrollerThin" />,
|
||||
code: `<ScrollerThin orientation="vertical" fade style={{ height: 200 }}>
|
||||
{items.map(item => <div key={item.id}>{item.label}</div>)}
|
||||
</ScrollerThin>`,
|
||||
relevantProps: ["orientation", "fade"],
|
||||
},
|
||||
{
|
||||
title: "ScrollerAuto",
|
||||
description: "Standard scrollbar that appears automatically when content overflows.",
|
||||
children: <ScrollerDemo Component={ScrollerAuto} label="ScrollerAuto" />,
|
||||
},
|
||||
{
|
||||
title: "ScrollerNone",
|
||||
description: "No visible scrollbar. Content is still scrollable via mouse wheel or touch.",
|
||||
children: <ScrollerDemo Component={ScrollerNone} label="ScrollerNone" />,
|
||||
},
|
||||
]}
|
||||
props={SCROLLER_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Default search bar with clear button.
|
||||
</Paragraph>
|
||||
<SearchBar
|
||||
query={query1}
|
||||
onChange={setQuery1}
|
||||
onClear={() => setQuery1("")}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</SectionWrapper>
|
||||
<SearchBar
|
||||
query={query}
|
||||
onChange={setQuery}
|
||||
onClear={() => setQuery("")}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
<SectionWrapper title="Sizes">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Available sizes: sm, md (default).
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<SearchBar
|
||||
query={query2}
|
||||
onChange={setQuery2}
|
||||
onClear={() => setQuery2("")}
|
||||
placeholder="Small size"
|
||||
size="sm"
|
||||
/>
|
||||
<SearchBar
|
||||
query={query2}
|
||||
onChange={setQuery2}
|
||||
onClear={() => setQuery2("")}
|
||||
placeholder="Medium size"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="States">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Disabled and prefilled states.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<SearchBar
|
||||
query=""
|
||||
onChange={() => { }}
|
||||
placeholder="Disabled"
|
||||
disabled
|
||||
/>
|
||||
<SearchBar
|
||||
query={query3}
|
||||
onChange={setQuery3}
|
||||
onClear={() => setQuery3("")}
|
||||
placeholder="Prefilled"
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
query, onChange, onClear?, placeholder?, size?, autoFocus?, disabled?, onKeyDown?, onBlur?, onFocus?, autoComplete?, inputProps?, aria-label?
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
function SizesDemo() {
|
||||
const [query, setQuery] = useState("");
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<SearchBar query={query} onChange={setQuery} onClear={() => setQuery("")} placeholder="Small size" size="sm" />
|
||||
<SearchBar query={query} onChange={setQuery} onClear={() => setQuery("")} placeholder="Medium size (default)" size="md" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatesDemo() {
|
||||
const [prefilled, setPrefilled] = useState("prefilled");
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<SearchBar query="" onChange={() => { }} placeholder="Disabled" disabled />
|
||||
<SearchBar query={prefilled} onChange={setPrefilled} onClear={() => setPrefilled("")} placeholder="Prefilled" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SearchBarTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="SearchBar"
|
||||
overview="SearchBar is Discord's search input component with a built-in search icon, clearable button, and two size variants. Used throughout Discord for filtering and searching content."
|
||||
importPath={'import { SearchBar } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Default search bar with clear button.",
|
||||
children: <BasicDemo />,
|
||||
code: `<SearchBar
|
||||
query={query}
|
||||
onChange={setQuery}
|
||||
onClear={() => setQuery("")}
|
||||
placeholder="Search..."
|
||||
/>`,
|
||||
relevantProps: ["query", "onChange", "onClear", "placeholder"],
|
||||
},
|
||||
{
|
||||
title: "Sizes",
|
||||
description: "Available in sm and md (default) sizes.",
|
||||
children: <SizesDemo />,
|
||||
relevantProps: ["size"],
|
||||
},
|
||||
{
|
||||
title: "States",
|
||||
description: "Disabled and prefilled states.",
|
||||
children: <StatesDemo />,
|
||||
relevantProps: ["disabled"],
|
||||
},
|
||||
]}
|
||||
props={SEARCHBAR_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>("option1");
|
||||
const [multiValue, setMultiValue] = useState<string[]>(["option1", "option2"]);
|
||||
const [clearableValue, setClearableValue] = useState<string>("option1");
|
||||
const [placeholderValue, setPlaceholderValue] = useState<string>("");
|
||||
|
||||
function SingleDemo() {
|
||||
const [value, setValue] = useState<string>("option1");
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Single Selection">
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value={singleValue}
|
||||
onSelectionChange={v => setSingleValue(v as string)}
|
||||
selectionMode="single"
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Multiple Selection">
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value={multiValue}
|
||||
onSelectionChange={v => setMultiValue(v as string[])}
|
||||
selectionMode="multiple"
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Placeholder">
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value={placeholderValue}
|
||||
onSelectionChange={v => setPlaceholderValue(v as string)}
|
||||
placeholder="Select an option..."
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Clearable">
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value={clearableValue}
|
||||
onSelectionChange={v => setClearableValue(v as string)}
|
||||
clearable
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Full Width">
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value="option1"
|
||||
fullWidth
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Disabled">
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value="option2"
|
||||
disabled
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>ManaSelect</strong> - Note: Uses onSelectionChange, NOT onChange.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• options: ManaSelectOption[] - Array of options
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• value?: string | string[] | null - Selected value(s)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onSelectionChange?: (value) => void - Called when selection changes
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• selectionMode?: "single" | "multiple" - Selection mode
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• placeholder?: string - Placeholder text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• clearable?: boolean - Allow clearing selection
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• fullWidth?: boolean - Expand to full width
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable select
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>ManaSelectOption</strong>
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• id: string - Unique option ID
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• value: string - Option value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• label: string - Display label
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value={value}
|
||||
onSelectionChange={v => setValue(v as string)}
|
||||
selectionMode="single"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiDemo() {
|
||||
const [value, setValue] = useState<string[]>(["option1", "option2"]);
|
||||
return (
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value={value}
|
||||
onSelectionChange={v => setValue(v as string[])}
|
||||
selectionMode="multiple"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaceholderDemo() {
|
||||
const [value, setValue] = useState<string>("");
|
||||
return (
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value={value}
|
||||
onSelectionChange={v => setValue(v as string)}
|
||||
placeholder="Select an option..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ClearableDemo() {
|
||||
const [value, setValue] = useState<string>("option1");
|
||||
return (
|
||||
<ManaSelect
|
||||
options={SAMPLE_OPTIONS}
|
||||
value={value}
|
||||
onSelectionChange={v => setValue(v as string)}
|
||||
clearable
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SelectTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="ManaSelect"
|
||||
overview="ManaSelect is Discord's dropdown select component supporting single and multi-select modes. Important: uses onSelectionChange (not onChange) for the selection callback. Supports clearable selection, placeholder text, full width, and custom option rendering."
|
||||
notices={[
|
||||
{ type: "warn", children: "ManaSelect uses onSelectionChange, not onChange. This is a common source of confusion since most other Discord form components use onChange." },
|
||||
]}
|
||||
importPath={'import { ManaSelect, ManaSelectOption } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Single Selection",
|
||||
description: "Default single-select dropdown.",
|
||||
children: <SingleDemo />,
|
||||
code: `<ManaSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onSelectionChange={v => 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: <MultiDemo />,
|
||||
},
|
||||
{
|
||||
title: "With Placeholder",
|
||||
description: "Shows placeholder text when no option is selected.",
|
||||
children: <PlaceholderDemo />,
|
||||
relevantProps: ["placeholder"],
|
||||
},
|
||||
{
|
||||
title: "Clearable",
|
||||
description: "Adds a clear button to reset the selection.",
|
||||
children: <ClearableDemo />,
|
||||
relevantProps: ["clearable"],
|
||||
},
|
||||
{
|
||||
title: "Full Width",
|
||||
description: "Expands to fill the available width.",
|
||||
children: (
|
||||
<ManaSelect options={SAMPLE_OPTIONS} value="option1" fullWidth />
|
||||
),
|
||||
relevantProps: ["fullWidth"],
|
||||
},
|
||||
{
|
||||
title: "Disabled",
|
||||
description: "Non-interactive disabled state.",
|
||||
children: (
|
||||
<ManaSelect options={SAMPLE_OPTIONS} value="option2" disabled />
|
||||
),
|
||||
relevantProps: ["disabled"],
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Skeleton">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Loading placeholder with shimmer animation. Used in Discord's member list loading state.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={withHeader ? "Hide Header" : "Show Header"}
|
||||
onClick={() => setWithHeader(!withHeader)}
|
||||
/>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="Fewer Items"
|
||||
onClick={() => setSize(Math.max(1, size - 1))}
|
||||
/>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="More Items"
|
||||
onClick={() => setSize(Math.min(20, size + 1))}
|
||||
/>
|
||||
</div>
|
||||
<div className="full-motion">
|
||||
<Skeleton withHeader={withHeader} size={size} />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• withHeader?: boolean - Show header placeholder (default: true)</Paragraph>
|
||||
<Paragraph color="text-muted">• size?: number - Number of skeleton rows to display (default: 15)</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div>
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
|
||||
<ManaButton variant="secondary" text={withHeader ? "Hide Header" : "Show Header"} onClick={() => setWithHeader(!withHeader)} />
|
||||
<ManaButton variant="secondary" text="Fewer Items" onClick={() => setSize(Math.max(1, size - 1))} />
|
||||
<ManaButton variant="secondary" text="More Items" onClick={() => setSize(Math.min(20, size + 1))} />
|
||||
</div>
|
||||
<div className="full-motion">
|
||||
<Skeleton withHeader={withHeader} size={size} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SkeletonTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="Skeleton"
|
||||
overview="Skeleton displays a loading placeholder with shimmer animation. Used in Discord's member list and other loading states. Renders rows of animated placeholder content with an optional header."
|
||||
importPath={'import { Skeleton } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Interactive Demo",
|
||||
description: "Toggle the header and adjust the number of skeleton rows.",
|
||||
children: <SkeletonDemo />,
|
||||
code: "<Skeleton withHeader={true} size={5} />",
|
||||
relevantProps: ["withHeader", "size"],
|
||||
},
|
||||
{
|
||||
title: "Header Only",
|
||||
children: (
|
||||
<div className="full-motion">
|
||||
<Skeleton withHeader={true} size={0} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "No Header",
|
||||
children: (
|
||||
<div className="full-motion">
|
||||
<Skeleton withHeader={false} size={3} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
props={SKELETON_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic Slider">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Value: {basicValue}
|
||||
</Paragraph>
|
||||
<Slider
|
||||
initialValue={basicValue}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
onValueChange={setBasicValue}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Markers">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Value: {markerValue}
|
||||
</Paragraph>
|
||||
<Slider
|
||||
initialValue={markerValue}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
markers={[0, 25, 50, 75, 100]}
|
||||
onValueChange={setMarkerValue}
|
||||
onMarkerRender={(v: number) => `${v}%`}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Stick to Markers">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Value snaps to markers.
|
||||
</Paragraph>
|
||||
<Slider
|
||||
initialValue={50}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
markers={[0, 25, 50, 75, 100]}
|
||||
stickToMarkers
|
||||
onMarkerRender={(v: number) => `${v}%`}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Mini Slider">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Value: {miniValue}
|
||||
</Paragraph>
|
||||
<div style={{ width: 200 }}>
|
||||
<Slider
|
||||
initialValue={miniValue}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
mini
|
||||
onValueChange={setMiniValue}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Disabled">
|
||||
<Slider
|
||||
initialValue={60}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
disabled
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Custom Range">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Range: -50 to 50
|
||||
</Paragraph>
|
||||
<Slider
|
||||
initialValue={0}
|
||||
minValue={-50}
|
||||
maxValue={50}
|
||||
markers={[-50, -25, 0, 25, 50]}
|
||||
onMarkerRender={(v: number) => v.toString()}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• initialValue / value - Current value</Paragraph>
|
||||
<Paragraph color="text-muted">• minValue / maxValue - Range bounds</Paragraph>
|
||||
<Paragraph color="text-muted">• onValueChange - Change callback</Paragraph>
|
||||
<Paragraph color="text-muted">• markers - Array of marker values</Paragraph>
|
||||
<Paragraph color="text-muted">• stickToMarkers - Snap to markers</Paragraph>
|
||||
<Paragraph color="text-muted">• onMarkerRender - Custom marker labels</Paragraph>
|
||||
<Paragraph color="text-muted">• mini - Compact style</Paragraph>
|
||||
<Paragraph color="text-muted">• hideBubble - Hide value tooltip</Paragraph>
|
||||
<Paragraph color="text-muted">• disabled - Disable interaction</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Value: {value}</Paragraph>
|
||||
<Slider initialValue={value} minValue={0} maxValue={100} onValueChange={setValue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MarkersDemo() {
|
||||
const [value, setValue] = useState(50);
|
||||
return (
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Value: {value}</Paragraph>
|
||||
<Slider
|
||||
initialValue={value}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
markers={[0, 25, 50, 75, 100]}
|
||||
onValueChange={setValue}
|
||||
onMarkerRender={(v: number) => `${v}%`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniDemo() {
|
||||
const [value, setValue] = useState(30);
|
||||
return (
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>Value: {value}</Paragraph>
|
||||
<div style={{ width: 200 }}>
|
||||
<Slider initialValue={value} minValue={0} maxValue={100} mini onValueChange={setValue} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SliderTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="Slider"
|
||||
overview="Slider is Discord's range input component with support for markers, snap-to-marker behavior, mini mode, value tooltips, and custom rendering. Supports both controlled and uncontrolled usage."
|
||||
notices={[
|
||||
{ type: "info", children: "Slider has two change callbacks: onValueChange fires on release (final value), while asValueChanges fires continuously during dragging. Use onValueChange for state updates to avoid excessive re-renders." },
|
||||
]}
|
||||
importPath={'import { Slider } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Simple slider with min/max range.",
|
||||
children: <BasicDemo />,
|
||||
code: `<Slider
|
||||
initialValue={50}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
onValueChange={setValue}
|
||||
/>`,
|
||||
relevantProps: ["initialValue", "minValue", "maxValue", "onValueChange"],
|
||||
},
|
||||
{
|
||||
title: "With Markers",
|
||||
description: "Tick marks at specified values with custom labels.",
|
||||
children: <MarkersDemo />,
|
||||
code: `<Slider
|
||||
initialValue={50}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
markers={[0, 25, 50, 75, 100]}
|
||||
onMarkerRender={v => \`\${v}%\`}
|
||||
/>`,
|
||||
relevantProps: ["markers", "onMarkerRender"],
|
||||
},
|
||||
{
|
||||
title: "Stick to Markers",
|
||||
description: "Value snaps to the nearest marker position.",
|
||||
children: (
|
||||
<Slider
|
||||
initialValue={50}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
markers={[0, 25, 50, 75, 100]}
|
||||
stickToMarkers
|
||||
onMarkerRender={(v: number) => `${v}%`}
|
||||
/>
|
||||
),
|
||||
relevantProps: ["stickToMarkers"],
|
||||
},
|
||||
{
|
||||
title: "Mini",
|
||||
description: "Compact visual style for tight layouts.",
|
||||
children: <MiniDemo />,
|
||||
relevantProps: ["mini"],
|
||||
},
|
||||
{
|
||||
title: "Disabled",
|
||||
description: "Non-interactive disabled state.",
|
||||
children: <Slider initialValue={60} minValue={0} maxValue={100} disabled />,
|
||||
relevantProps: ["disabled"],
|
||||
},
|
||||
{
|
||||
title: "Custom Range",
|
||||
description: "Slider with negative to positive range.",
|
||||
children: (
|
||||
<Slider
|
||||
initialValue={0}
|
||||
minValue={-50}
|
||||
maxValue={50}
|
||||
markers={[-50, -25, 0, 25, 50]}
|
||||
onMarkerRender={(v: number) => v.toString()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
props={SLIDER_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Spinner Types">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
All available spinner variants.
|
||||
<div>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={animated ? "Pause Animation" : "Resume Animation"}
|
||||
onClick={() => setAnimated(!animated)}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<div style={{ textAlign: "center", padding: 16 }}>
|
||||
<Spinner type="spinningCircle" animated={animated} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 8 }}>
|
||||
animated={animated.toString()}
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
{SpinnerTypes.map(type => (
|
||||
<div key={type} style={{ textAlign: "center", padding: 16 }}>
|
||||
<Spinner type={type} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 8 }}>
|
||||
{type}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Animation Toggle">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Spinners can be paused with animated=false.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={animated ? "Pause Animation" : "Resume Animation"}
|
||||
onClick={() => setAnimated(!animated)}
|
||||
/>
|
||||
</div>
|
||||
<div className="vc-compfinder-grid" style={{ marginTop: 16 }}>
|
||||
<div style={{ textAlign: "center", padding: 16 }}>
|
||||
<Spinner type="spinningCircle" animated={animated} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 8 }}>
|
||||
animated={animated.toString()}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Reduced Motion">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
lowMotion type is used when user prefers reduced motion.
|
||||
The component automatically switches to this when reducedMotion is enabled.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ textAlign: "center", padding: 16 }}>
|
||||
<Spinner type="lowMotion" />
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• type?: SpinnerType - Spinner variant
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• animated?: boolean - Enable/disable animation (default: true)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• className?: string - Container class
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• itemClassName?: string - Inner item class
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• aria-label?: string - Accessibility label
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Type Values">
|
||||
<Paragraph color="text-muted">• "wanderingCubes"</Paragraph>
|
||||
<Paragraph color="text-muted">• "chasingDots"</Paragraph>
|
||||
<Paragraph color="text-muted">• "pulsingEllipsis"</Paragraph>
|
||||
<Paragraph color="text-muted">• "spinningCircle"</Paragraph>
|
||||
<Paragraph color="text-muted">• "spinningCircleSimple"</Paragraph>
|
||||
<Paragraph color="text-muted">• "lowMotion"</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SpinnerTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="Spinner"
|
||||
overview="Spinner provides animated loading indicators in six visual styles. Automatically switches to the lowMotion variant when the user has reduced motion enabled. The animation can be paused via the animated prop."
|
||||
notices={[
|
||||
{ type: "positive", children: "Spinner automatically respects the user's reduced motion setting by switching to the lowMotion variant. You don't need to handle this manually." },
|
||||
]}
|
||||
importPath={'import { Spinner, SpinnerTypes } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "All Types",
|
||||
description: "All six spinner variants.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{SpinnerTypes.map(type => (
|
||||
<div key={type} style={{ textAlign: "center", padding: 16 }}>
|
||||
<Spinner type={type} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 8 }}>
|
||||
{type}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: '<Spinner type="spinningCircle" />',
|
||||
relevantProps: ["type"],
|
||||
},
|
||||
{
|
||||
title: "Animation Toggle",
|
||||
description: "Spinners can be paused with animated=false.",
|
||||
children: <AnimationToggleDemo />,
|
||||
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: (
|
||||
<div style={{ textAlign: "center", padding: 16 }}>
|
||||
<Spinner type="lowMotion" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
props={SPINNER_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SwitchWithLabelProps>;
|
||||
|
||||
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<HTMLInputElement>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic Switch (ManaSwitch)">
|
||||
<div className="vc-compfinder-grid">
|
||||
<div className="vc-compfinder-switch-item">
|
||||
<span>On:</span>
|
||||
<ManaSwitch checked={basicOn} onChange={setBasicOn} />
|
||||
</div>
|
||||
<div className="vc-compfinder-switch-item">
|
||||
<span>Off:</span>
|
||||
<ManaSwitch checked={basicOff} onChange={setBasicOff} />
|
||||
</div>
|
||||
<div className="vc-compfinder-switch-item">
|
||||
<span>Disabled On:</span>
|
||||
<ManaSwitch checked={true} disabled />
|
||||
</div>
|
||||
<div className="vc-compfinder-switch-item">
|
||||
<span>Disabled Off:</span>
|
||||
<ManaSwitch checked={false} disabled />
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Switch with Label">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<SwitchWithLabel
|
||||
label="Enabled option"
|
||||
checked={labelOn}
|
||||
onChange={setLabelOn}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label="Disabled option"
|
||||
checked={labelOff}
|
||||
onChange={setLabelOff}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Switch with Description">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<SwitchWithLabel
|
||||
label="Feature toggle"
|
||||
description="Enable this feature to unlock additional functionality."
|
||||
checked={withDesc}
|
||||
onChange={setWithDesc}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Disabled States">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<SwitchWithLabel
|
||||
label="Disabled (checked)"
|
||||
checked={true}
|
||||
disabled
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label="Disabled (unchecked)"
|
||||
checked={false}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>ManaSwitch</strong> - Basic toggle switch without label.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• checked: boolean - Current switch state
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (checked: boolean) => void - Called when toggled
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable interaction
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>SwitchWithLabel</strong> - Switch with label and description.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• label?: string - Label text displayed next to switch
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• description?: string - Description text below label
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• checked: boolean - Current switch state
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (checked: boolean) => void - Called when toggled
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable interaction
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div className="vc-compfinder-switch-item">
|
||||
<span>On:</span>
|
||||
<ManaSwitch checked={on} onChange={setOn} />
|
||||
</div>
|
||||
<div className="vc-compfinder-switch-item">
|
||||
<span>Off:</span>
|
||||
<ManaSwitch checked={off} onChange={setOff} />
|
||||
</div>
|
||||
<div className="vc-compfinder-switch-item">
|
||||
<span>Disabled On:</span>
|
||||
<ManaSwitch checked={true} disabled />
|
||||
</div>
|
||||
<div className="vc-compfinder-switch-item">
|
||||
<span>Disabled Off:</span>
|
||||
<ManaSwitch checked={false} disabled />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LabelDemo() {
|
||||
const [on, setOn] = useState(true);
|
||||
const [off, setOff] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<SwitchWithLabel label="Enabled option" checked={on} onChange={setOn} />
|
||||
<SwitchWithLabel label="Disabled option" checked={off} onChange={setOff} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DescriptionDemo() {
|
||||
const [checked, setChecked] = useState(true);
|
||||
return (
|
||||
<SwitchWithLabel
|
||||
label="Feature toggle"
|
||||
description="Enable this feature to unlock additional functionality."
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SwitchTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="ManaSwitch"
|
||||
overview="Discord provides two switch components: ManaSwitch is a bare toggle without label (used inline), and SwitchWithLabel wraps it with label and description text for settings-style layouts."
|
||||
notices={[
|
||||
{ type: "info", children: "SwitchWithLabel is found via findComponentByCodeLazy, not directly exported. Use it for settings layouts, and ManaSwitch for inline toggle scenarios." },
|
||||
]}
|
||||
importPath={`import { ManaSwitch } from "../components";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
const SwitchWithLabel = findComponentByCodeLazy('"data-toggleable-component":"switch"');`}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic Switch",
|
||||
description: "ManaSwitch in on, off, and disabled states.",
|
||||
children: <BasicDemo />,
|
||||
code: "<ManaSwitch checked={enabled} onChange={setEnabled} />",
|
||||
relevantProps: ["checked", "onChange", "disabled"],
|
||||
},
|
||||
{
|
||||
title: "Switch with Label",
|
||||
description: "SwitchWithLabel renders a label next to the toggle.",
|
||||
children: <LabelDemo />,
|
||||
code: `<SwitchWithLabel
|
||||
label="Notifications"
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
/>`,
|
||||
},
|
||||
{
|
||||
title: "With Description",
|
||||
description: "Label with description text below.",
|
||||
children: <DescriptionDemo />,
|
||||
code: `<SwitchWithLabel
|
||||
label="Feature toggle"
|
||||
description="Enable this to unlock additional functionality."
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
/>`,
|
||||
},
|
||||
{
|
||||
title: "Disabled States",
|
||||
description: "Both checked and unchecked disabled states.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<SwitchWithLabel label="Disabled (checked)" checked={true} disabled />
|
||||
<SwitchWithLabel label="Disabled (unchecked)" checked={false} disabled />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Selected: {tab}</Paragraph>
|
||||
<TabBar type="side" look="grey" selectedItem={tab} onItemSelect={setTab}>
|
||||
<TabBar.Item id="tab1">First Tab</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Second Tab</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Third Tab</TabBar.Item>
|
||||
</TabBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopDemo() {
|
||||
const [tab, setTab] = useState("tab1");
|
||||
return (
|
||||
<TabBar type="top" look="grey" selectedItem={tab} onItemSelect={setTab}>
|
||||
<TabBar.Item id="tab1">Overview</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Settings</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Members</TabBar.Item>
|
||||
</TabBar>
|
||||
);
|
||||
}
|
||||
|
||||
function TopPillDemo() {
|
||||
const [tab, setTab] = useState("tab1");
|
||||
return (
|
||||
<TabBar type="top-pill" look="grey" selectedItem={tab} onItemSelect={setTab}>
|
||||
<TabBar.Item id="tab1">All</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Online</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Pending</TabBar.Item>
|
||||
<TabBar.Item id="tab4">Blocked</TabBar.Item>
|
||||
</TabBar>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandDemo() {
|
||||
const [tab, setTab] = useState("tab1");
|
||||
return (
|
||||
<TabBar type="top" look="brand" selectedItem={tab} onItemSelect={setTab}>
|
||||
<TabBar.Item id="tab1">Home</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Explore</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Library</TabBar.Item>
|
||||
</TabBar>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderSeparatorDemo() {
|
||||
const [tab, setTab] = useState("tab1");
|
||||
return (
|
||||
<TabBar type="side" look="grey" selectedItem={tab} onItemSelect={setTab}>
|
||||
<TabBar.Header>User Settings</TabBar.Header>
|
||||
<TabBar.Item id="tab1">My Account</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Privacy</TabBar.Item>
|
||||
<TabBar.Separator />
|
||||
<TabBar.Header>App Settings</TabBar.Header>
|
||||
<TabBar.Item id="tab3">Appearance</TabBar.Item>
|
||||
<TabBar.Item id="tab4">Notifications</TabBar.Item>
|
||||
</TabBar>
|
||||
);
|
||||
}
|
||||
|
||||
function FeaturesDemo() {
|
||||
const [tab, setTab] = useState("tab1");
|
||||
const [colorTab, setColorTab] = useState("tab1");
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Side TabBar (Default)">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Default side-style tab bar with grey look. Selected: {sideTab}
|
||||
</Paragraph>
|
||||
<TabBar
|
||||
type="side"
|
||||
look="grey"
|
||||
selectedItem={sideTab}
|
||||
onItemSelect={setSideTab}
|
||||
>
|
||||
<TabBar.Item id="tab1">First Tab</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Second Tab</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Third Tab</TabBar.Item>
|
||||
</TabBar>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Top TabBar">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Top-style tab bar with underline indicator. Selected: {topTab}
|
||||
</Paragraph>
|
||||
<TabBar
|
||||
type="top"
|
||||
look="grey"
|
||||
selectedItem={topTab}
|
||||
onItemSelect={setTopTab}
|
||||
>
|
||||
<TabBar.Item id="tab1">Overview</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Settings</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Members</TabBar.Item>
|
||||
</TabBar>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Top Pill TabBar">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Pill-style tabs for top navigation. Selected: {topPillTab}
|
||||
</Paragraph>
|
||||
<TabBar
|
||||
type="top-pill"
|
||||
look="grey"
|
||||
selectedItem={topPillTab}
|
||||
onItemSelect={setTopPillTab}
|
||||
>
|
||||
<TabBar.Item id="tab1">All</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Online</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Pending</TabBar.Item>
|
||||
<TabBar.Item id="tab4">Blocked</TabBar.Item>
|
||||
</TabBar>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Brand Look">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Brand-colored tab bar style. Selected: {brandTab}
|
||||
</Paragraph>
|
||||
<TabBar
|
||||
type="top"
|
||||
look="brand"
|
||||
selectedItem={brandTab}
|
||||
onItemSelect={setBrandTab}
|
||||
>
|
||||
<TabBar.Item id="tab1">Home</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Explore</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Library</TabBar.Item>
|
||||
</TabBar>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Header and Separator">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Tab bar with header section and separator.
|
||||
</Paragraph>
|
||||
<TabBar
|
||||
type="side"
|
||||
look="grey"
|
||||
selectedItem={sideTab}
|
||||
onItemSelect={setSideTab}
|
||||
>
|
||||
<TabBar.Header>User Settings</TabBar.Header>
|
||||
<TabBar.Item id="tab1">My Account</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Privacy</TabBar.Item>
|
||||
<TabBar.Separator />
|
||||
<TabBar.Header>App Settings</TabBar.Header>
|
||||
<TabBar.Item id="tab3">Appearance</TabBar.Item>
|
||||
<TabBar.Item id="tab4">Notifications</TabBar.Item>
|
||||
</TabBar>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Disabled Items">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Some tabs can be disabled.
|
||||
</Paragraph>
|
||||
<TabBar
|
||||
type="top"
|
||||
look="grey"
|
||||
selectedItem={topTab}
|
||||
onItemSelect={setTopTab}
|
||||
>
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Disabled items:</Paragraph>
|
||||
<TabBar type="top" look="grey" selectedItem={tab} onItemSelect={setTab}>
|
||||
<TabBar.Item id="tab1">Active</TabBar.Item>
|
||||
<TabBar.Item id="tab2" disabled>Disabled</TabBar.Item>
|
||||
<TabBar.Item id="tab3">Another Active</TabBar.Item>
|
||||
</TabBar>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Custom Colors">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Individual tabs can have custom colors. Selected: {colorTab}
|
||||
</Paragraph>
|
||||
<TabBar
|
||||
type="top"
|
||||
look="grey"
|
||||
selectedItem={colorTab}
|
||||
onItemSelect={setColorTab}
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Custom colors:</Paragraph>
|
||||
<TabBar type="top" look="grey" selectedItem={colorTab} onItemSelect={setColorTab}>
|
||||
<TabBar.Item id="tab1" color="#43b581">Green</TabBar.Item>
|
||||
<TabBar.Item id="tab2" color="#faa61a">Yellow</TabBar.Item>
|
||||
<TabBar.Item id="tab3" color="#f04747">Red</TabBar.Item>
|
||||
</TabBar>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Destructive Variant">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Tab with destructive/danger styling.
|
||||
</Paragraph>
|
||||
<TabBar
|
||||
type="side"
|
||||
look="grey"
|
||||
selectedItem={sideTab}
|
||||
onItemSelect={setSideTab}
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 4 }}>Destructive variant:</Paragraph>
|
||||
<TabBar type="side" look="grey" selectedItem={tab} onItemSelect={setTab}>
|
||||
<TabBar.Item id="tab1">Settings</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Privacy</TabBar.Item>
|
||||
<TabBar.Item id="logout" variant="destructive">Log Out</TabBar.Item>
|
||||
</TabBar>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>TabBar</strong> - Container component
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• type?: "side" | "top" | "top-pill" - Tab bar style</Paragraph>
|
||||
<Paragraph color="text-muted">• look?: "brand" | "grey" - Color scheme</Paragraph>
|
||||
<Paragraph color="text-muted">• selectedItem?: string - Currently selected tab ID</Paragraph>
|
||||
<Paragraph color="text-muted">• onItemSelect?: (id) ={">"} void - Selection callback</Paragraph>
|
||||
<Paragraph color="text-muted">• orientation?: "horizontal" | "vertical" - Layout direction</Paragraph>
|
||||
<Paragraph color="text-muted">• className?: string - Additional CSS classes</Paragraph>
|
||||
<Paragraph color="text-muted">• style?: CSSProperties - Inline styles</Paragraph>
|
||||
<Paragraph color="text-muted">• aria-label?: string - Accessibility label</Paragraph>
|
||||
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>TabBar.Item</strong> - Individual tab
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• id: string - Unique tab identifier (required)</Paragraph>
|
||||
<Paragraph color="text-muted">• disabled?: boolean - Disable the tab</Paragraph>
|
||||
<Paragraph color="text-muted">• color?: string - Custom tab color</Paragraph>
|
||||
<Paragraph color="text-muted">• variant?: "destructive" - Danger styling</Paragraph>
|
||||
<Paragraph color="text-muted">• disableItemStyles?: boolean - Remove default styles</Paragraph>
|
||||
<Paragraph color="text-muted">• onClick?: MouseEventHandler - Custom click handler</Paragraph>
|
||||
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>TabBar.Header</strong> - Section header
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• children: ReactNode - Header text</Paragraph>
|
||||
<Paragraph color="text-muted">• onClick?: MouseEventHandler - Click handler</Paragraph>
|
||||
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>TabBar.Separator</strong> - Visual divider
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">• style?: CSSProperties - Custom styles</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TabBarTab() {
|
||||
return (
|
||||
<DocPage
|
||||
componentName="TabBar"
|
||||
overview="TabBar provides navigation tabs in three styles: side (vertical sidebar), top (horizontal with underline), and top-pill (horizontal pill buttons). Supports brand/grey color schemes, disabled items, custom colors, destructive variants, and section headers with separators."
|
||||
importPath={'import { TabBar } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Side (Default)",
|
||||
description: "Vertical sidebar-style tabs with grey look.",
|
||||
children: <SideDemo />,
|
||||
code: `<TabBar type="side" look="grey" selectedItem={tab} onItemSelect={setTab}>
|
||||
<TabBar.Item id="tab1">First Tab</TabBar.Item>
|
||||
<TabBar.Item id="tab2">Second Tab</TabBar.Item>
|
||||
</TabBar>`,
|
||||
relevantProps: ["type", "look", "selectedItem", "onItemSelect"],
|
||||
},
|
||||
{
|
||||
title: "Top",
|
||||
description: "Horizontal tabs with underline indicator.",
|
||||
children: <TopDemo />,
|
||||
},
|
||||
{
|
||||
title: "Top Pill",
|
||||
description: "Pill-style horizontal tabs.",
|
||||
children: <TopPillDemo />,
|
||||
},
|
||||
{
|
||||
title: "Brand Look",
|
||||
description: "Brand-colored tab styling.",
|
||||
children: <BrandDemo />,
|
||||
},
|
||||
{
|
||||
title: "Headers and Separators",
|
||||
description: "TabBar.Header and TabBar.Separator for organizing groups of tabs.",
|
||||
children: <HeaderSeparatorDemo />,
|
||||
},
|
||||
{
|
||||
title: "Disabled, Colors, Destructive",
|
||||
description: "Items can be disabled, colored, or styled as destructive.",
|
||||
children: <FeaturesDemo />,
|
||||
relevantProps: ["disabled", "color", "variant"],
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLTextAreaElement>", internal: true, description: "Ref to the underlying textarea element." },
|
||||
];
|
||||
|
||||
function BasicDemo() {
|
||||
const [value, setValue] = useState("Hello world\nThis is a textarea");
|
||||
return <ManaTextArea value={value} onChange={setValue} />;
|
||||
}
|
||||
|
||||
function AutosizeDemo() {
|
||||
const [value, setValue] = useState("This textarea will grow as you type more content...");
|
||||
return <ManaTextArea value={value} onChange={setValue} autosize />;
|
||||
}
|
||||
|
||||
function CharCountDemo() {
|
||||
const [value, setValue] = useState("Count characters");
|
||||
return <ManaTextArea value={value} onChange={setValue} showCharacterCount maxLength={200} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic">
|
||||
<ManaTextArea value={basic} onChange={setBasic} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Placeholder">
|
||||
<ManaTextArea value={withPlaceholder} onChange={setWithPlaceholder} placeholder="Enter your message..." />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Autosize">
|
||||
<ManaTextArea value={autosize} onChange={setAutosize} autosize />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Fixed Rows (5)">
|
||||
<ManaTextArea value={fixedRows} onChange={setFixedRows} rows={5} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Character Count">
|
||||
<ManaTextArea value={charCount} onChange={setCharCount} showCharacterCount maxLength={200} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="States">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaTextArea value="Disabled textarea" disabled />
|
||||
<ManaTextArea value="Read only textarea" readOnly />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• value: string - Textarea value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (value: string) => void - Called on input change
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• placeholder?: string - Placeholder text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• rows?: number - Fixed number of rows
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• autosize?: boolean - Auto-grow with content
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• showCharacterCount?: boolean - Display character count
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• maxLength?: number - Maximum character limit
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable textarea
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• readOnly?: boolean - Make textarea read-only
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
overview="ManaTextArea is Discord's multi-line text input with support for autosize, character counting, fixed row count, error states, and disabled/read-only modes."
|
||||
importPath={'import { ManaTextArea } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Default textarea with controlled value.",
|
||||
children: <BasicDemo />,
|
||||
code: "<ManaTextArea value={value} onChange={setValue} />",
|
||||
relevantProps: ["value", "onChange"],
|
||||
},
|
||||
{
|
||||
title: "With Placeholder",
|
||||
children: (
|
||||
<ManaTextArea value="" onChange={() => { }} placeholder="Enter your message..." />
|
||||
),
|
||||
relevantProps: ["placeholder"],
|
||||
},
|
||||
{
|
||||
title: "Autosize",
|
||||
description: "Automatically grows to fit content as you type.",
|
||||
children: <AutosizeDemo />,
|
||||
relevantProps: ["autosize"],
|
||||
},
|
||||
{
|
||||
title: "Fixed Rows",
|
||||
description: "Fixed height with 5 visible rows.",
|
||||
children: (
|
||||
<ManaTextArea value="Fixed 5 rows" onChange={() => { }} rows={5} />
|
||||
),
|
||||
relevantProps: ["rows"],
|
||||
},
|
||||
{
|
||||
title: "Character Count",
|
||||
description: "Shows character count with a max length of 200.",
|
||||
children: <CharCountDemo />,
|
||||
relevantProps: ["showCharacterCount", "maxLength"],
|
||||
},
|
||||
{
|
||||
title: "States",
|
||||
description: "Disabled and read-only states.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaTextArea value="Disabled textarea" disabled />
|
||||
<ManaTextArea value="Read only textarea" readOnly />
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["disabled", "readOnly"],
|
||||
},
|
||||
]}
|
||||
props={TEXTAREA_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLButtonElement>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Variants">
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaTextButtonVariants.map(variant => (
|
||||
<ManaTextButton
|
||||
key={variant}
|
||||
text={variant}
|
||||
variant={variant}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Text Variants">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{ManaTextButtonTextVariants.map(textVariant => (
|
||||
<ManaTextButton
|
||||
key={textVariant}
|
||||
text={textVariant}
|
||||
textVariant={textVariant}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="States">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaTextButton text="Normal" />
|
||||
<ManaTextButton text="Disabled" disabled />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Line Clamp">
|
||||
<div className="vc-compfinder-grid" style={{ maxWidth: 200 }}>
|
||||
<ManaTextButton
|
||||
text="This is a very long text that should be clamped to one line"
|
||||
lineClamp={1}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• text - Button label text</Paragraph>
|
||||
<Paragraph color="text-muted">• variant - primary, secondary, always-white, critical</Paragraph>
|
||||
<Paragraph color="text-muted">• textVariant - Typography variant (e.g., text-sm/medium)</Paragraph>
|
||||
<Paragraph color="text-muted">• lineClamp - Max lines before truncation</Paragraph>
|
||||
<Paragraph color="text-muted">• disabled - Disable button</Paragraph>
|
||||
<Paragraph color="text-muted">• onClick - Click handler</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
overview="ManaTextButton is Discord's text-only button component (no background or border). Supports four color variants, typography sizing via textVariant, line clamping, and disabled state."
|
||||
importPath={'import { ManaTextButton, ManaTextButtonVariants, ManaTextButtonTextVariants } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Variants",
|
||||
description: "All four color variants.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{ManaTextButtonVariants.map(variant => (
|
||||
<ManaTextButton key={variant} text={variant} variant={variant} />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: '<ManaTextButton text="Click me" variant="primary" />',
|
||||
relevantProps: ["variant"],
|
||||
},
|
||||
{
|
||||
title: "Text Variants",
|
||||
description: "Different typography sizes and weights.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{ManaTextButtonTextVariants.map(textVariant => (
|
||||
<ManaTextButton key={textVariant} text={textVariant} textVariant={textVariant} />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["textVariant"],
|
||||
},
|
||||
{
|
||||
title: "States",
|
||||
description: "Normal and disabled states.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaTextButton text="Normal" />
|
||||
<ManaTextButton text="Disabled" disabled />
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["disabled"],
|
||||
},
|
||||
{
|
||||
title: "Line Clamp",
|
||||
description: "Truncate long text to a single line.",
|
||||
children: (
|
||||
<div style={{ maxWidth: 200 }}>
|
||||
<ManaTextButton text="This is a very long text that should be clamped to one line" lineClamp={1} />
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["lineClamp"],
|
||||
},
|
||||
]}
|
||||
props={TEXTBUTTON_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLInputElement>", internal: true, description: "Ref to the underlying input element." },
|
||||
];
|
||||
|
||||
function BasicDemo() {
|
||||
const [value, setValue] = useState("Hello world");
|
||||
return <ManaTextInput value={value} onChange={setValue} />;
|
||||
}
|
||||
|
||||
function SizesDemo() {
|
||||
const [sm, setSm] = useState("Small");
|
||||
const [md, setMd] = useState("Medium");
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic">
|
||||
<ManaTextInput value={basic} onChange={setBasic} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Sizes">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaTextInput value={sizeSm} onChange={setSizeSm} size="sm" placeholder="Size: sm" />
|
||||
<ManaTextInput value={sizeMd} onChange={setSizeMd} size="md" placeholder="Size: md" />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Placeholder">
|
||||
<ManaTextInput value={withPlaceholder} onChange={setWithPlaceholder} placeholder="Enter text here..." />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Clearable">
|
||||
<ManaTextInput value={clearable} onChange={setClearable} clearable />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Character Count">
|
||||
<ManaTextInput value={charCount} onChange={setCharCount} showCharacterCount maxLength={50} />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Error State">
|
||||
<ManaTextInput value={error} onChange={setError} error />
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="States">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaTextInput value="Disabled" disabled />
|
||||
<ManaTextInput value="Read only" readOnly />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• value: string - Input value
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• onChange?: (value: string) => void - Called on input change
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• placeholder?: string - Placeholder text
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• size?: "sm" | "md" - Input size (default: "md")
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• clearable?: boolean - Show clear button
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• showCharacterCount?: boolean - Display character count
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• maxLength?: number - Maximum character limit
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• error?: boolean - Show error state styling
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• disabled?: boolean - Disable input
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• readOnly?: boolean - Make input read-only
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaTextInput value={sm} onChange={setSm} size="sm" placeholder="Size: sm" />
|
||||
<ManaTextInput value={md} onChange={setMd} size="md" placeholder="Size: md" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClearableDemo() {
|
||||
const [value, setValue] = useState("Clear me");
|
||||
return <ManaTextInput value={value} onChange={setValue} clearable />;
|
||||
}
|
||||
|
||||
function CharCountDemo() {
|
||||
const [value, setValue] = useState("Count chars");
|
||||
return <ManaTextInput value={value} onChange={setValue} showCharacterCount maxLength={50} />;
|
||||
}
|
||||
|
||||
function ErrorDemo() {
|
||||
const [value, setValue] = useState("Invalid input");
|
||||
return <ManaTextInput value={value} onChange={setValue} error />;
|
||||
}
|
||||
|
||||
export default function TextInputTab() {
|
||||
return (
|
||||
<DocPage
|
||||
overview="ManaTextInput is Discord's single-line text input with support for two sizes, clearable button, character counting, error states, leading/trailing accessories, and disabled/read-only modes."
|
||||
importPath={'import { ManaTextInput } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "Default text input with controlled value.",
|
||||
children: <BasicDemo />,
|
||||
code: "<ManaTextInput value={value} onChange={setValue} />",
|
||||
relevantProps: ["value", "onChange"],
|
||||
},
|
||||
{
|
||||
title: "Sizes",
|
||||
description: "Available in sm and md (default) sizes.",
|
||||
children: <SizesDemo />,
|
||||
relevantProps: ["size"],
|
||||
},
|
||||
{
|
||||
title: "With Placeholder",
|
||||
children: (
|
||||
<ManaTextInput value="" onChange={() => { }} placeholder="Enter text here..." />
|
||||
),
|
||||
relevantProps: ["placeholder"],
|
||||
},
|
||||
{
|
||||
title: "Clearable",
|
||||
description: "Shows a clear button when the input has a value.",
|
||||
children: <ClearableDemo />,
|
||||
relevantProps: ["clearable"],
|
||||
},
|
||||
{
|
||||
title: "Character Count",
|
||||
description: "Displays character count with a 50-character max.",
|
||||
children: <CharCountDemo />,
|
||||
relevantProps: ["showCharacterCount", "maxLength"],
|
||||
},
|
||||
{
|
||||
title: "Error State",
|
||||
description: "Red border indicating invalid input.",
|
||||
children: <ErrorDemo />,
|
||||
relevantProps: ["error"],
|
||||
},
|
||||
{
|
||||
title: "States",
|
||||
description: "Disabled and read-only states.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
<ManaTextInput value="Disabled" disabled />
|
||||
<ManaTextInput value="Read only" readOnly />
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["disabled", "readOnly"],
|
||||
},
|
||||
]}
|
||||
props={TEXTINPUT_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Timestamp Formats">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
All available timestamp format options using moment.js format strings.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
{TimestampDisplayFormats.map(({ format, description }) => (
|
||||
<div key={format} style={{ padding: 12, display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<Timestamp timestamp={now} timestampFormat={format} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>
|
||||
{format}: {description}
|
||||
</Paragraph>
|
||||
<DocPage
|
||||
componentName="Timestamp"
|
||||
overview="Timestamp renders a formatted date with a tooltip showing the full date. Supports moment.js format strings, compact mode, inline rendering, edited indicators, and hover-only visibility."
|
||||
importPath={'import { Timestamp, TimestampDisplayFormats } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Formats",
|
||||
description: "All available timestamp display formats using moment.js format strings.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{TimestampDisplayFormats.map(({ format, description }) => (
|
||||
<div key={format} style={{ padding: 12, display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<Timestamp timestamp={now} timestampFormat={format} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>
|
||||
{format}: {description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Compact Mode">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Compact mode displays a shorter version of the timestamp.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} compact={false} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
compact=false (default)
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} compact={true} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
compact=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Display Variants">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Different display modes for timestamps.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} isInline={false} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isInline=false (default)
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} isInline={true} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isInline=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} cozyAlt={true} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
cozyAlt=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Edited Indicator">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Shows an "(edited)" indicator after the timestamp.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} isEdited={false} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isEdited=false (default)
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} isEdited={true} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isEdited=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Visibility on Hover">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Timestamp can be hidden until hovered.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ padding: 12 }}>
|
||||
<span style={{ padding: 8, background: "var(--background-secondary)", borderRadius: 4 }}>
|
||||
Hover me: <Timestamp timestamp={now} isVisibleOnlyOnHover={true} />
|
||||
</span>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isVisibleOnlyOnHover=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Tooltip Positions">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
The tooltip showing full date can be positioned differently.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
{TooltipPositions.map(position => (
|
||||
<div key={position} style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} tooltipPosition={position} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
tooltipPosition="{position}"
|
||||
</Paragraph>
|
||||
),
|
||||
code: '<Timestamp timestamp={new Date()} timestampFormat="LLL" />',
|
||||
relevantProps: ["timestamp", "timestampFormat"],
|
||||
},
|
||||
{
|
||||
title: "Compact Mode",
|
||||
description: "Compact mode shows a shorter version of the timestamp.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} compact={false} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
compact=false (default)
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} compact={true} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
compact=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• timestamp: Date | Moment - The timestamp to display (required)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• timestampFormat?: TimestampFormat - Format string (LT, LTS, L, LL, LLL, LLLL, l, ll, lll, llll)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• compact?: boolean - Use compact display mode
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• cozyAlt?: boolean - Use cozy alternative display
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• isInline?: boolean - Display inline
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• isVisibleOnlyOnHover?: boolean - Only show on hover
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• isEdited?: boolean - Show "(edited)" indicator
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• tooltipPosition?: "top" | "bottom" | "left" | "right" - Tooltip position
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• id?: string - Element ID
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• className?: string - Additional CSS class
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• children?: ReactNode - Custom content
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["compact"],
|
||||
},
|
||||
{
|
||||
title: "Display Variants",
|
||||
description: "Inline and cozy alternative display modes.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} isInline={false} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isInline=false (default)
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} isInline={true} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isInline=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} cozyAlt={true} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
cozyAlt=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["isInline", "cozyAlt"],
|
||||
},
|
||||
{
|
||||
title: "Edited Indicator",
|
||||
description: 'Shows an "(edited)" indicator after the timestamp.',
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} isEdited={false} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isEdited=false (default)
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} isEdited={true} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isEdited=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["isEdited"],
|
||||
},
|
||||
{
|
||||
title: "Visibility on Hover",
|
||||
description: "Timestamp can be hidden until the parent element is hovered.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<div style={{ padding: 12 }}>
|
||||
<span style={{ padding: 8, background: "var(--background-secondary)", borderRadius: 4 }}>
|
||||
Hover me: <Timestamp timestamp={now} isVisibleOnlyOnHover={true} />
|
||||
</span>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
isVisibleOnlyOnHover=true
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["isVisibleOnlyOnHover"],
|
||||
},
|
||||
{
|
||||
title: "Tooltip Positions",
|
||||
description: "The tooltip showing the full date can be positioned on any side.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{TooltipPositions.map(position => (
|
||||
<div key={position} style={{ padding: 12 }}>
|
||||
<Timestamp timestamp={now} tooltipPosition={position} />
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10, marginTop: 4 }}>
|
||||
tooltipPosition="{position}"
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["tooltipPosition"],
|
||||
},
|
||||
]}
|
||||
props={TIMESTAMP_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <span style={{ color: "var(--text-brand)" }}>Custom JSX content!</span>,
|
||||
});
|
||||
Toasts.show(toast);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Toast Types">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Click buttons to show different toast types. Toasts appear at screen edges.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid" style={{ gap: 8 }}>
|
||||
{TOAST_TYPES.map(({ type, label, description }) => (
|
||||
<div key={label} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<DocPage
|
||||
componentName="Toasts"
|
||||
overview="Toasts are brief notification messages that appear at screen edges. Use showToast for quick one-liners, or Toasts.create + Toasts.show for full control over duration, position, and custom content. Toasts.pop removes the most recent toast."
|
||||
notices={[
|
||||
{ type: "info", children: "Use showToast() for quick one-liners. Use Toasts.create() + Toasts.show() when you need custom duration, position, or component content." },
|
||||
]}
|
||||
importPath={'import { showToast, Toasts, ToastType } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "All Types",
|
||||
description: "Click any button to show that toast type.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid" style={{ gap: 8 }}>
|
||||
{TOAST_TYPES.map(({ type, label, description }) => (
|
||||
<div key={label} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={label}
|
||||
onClick={() => showToast(`This is a ${label} toast!`, type)}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>
|
||||
{description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
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: (
|
||||
<ManaButton
|
||||
variant="primary"
|
||||
text="Show Simple Toast"
|
||||
onClick={() => 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: (
|
||||
<div className="vc-compfinder-grid" style={{ gap: 8 }}>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text={label}
|
||||
onClick={() => showSimpleToast(type, label)}
|
||||
text="5s Duration Toast"
|
||||
onClick={() => {
|
||||
const toast = Toasts.create("This lasts 5 seconds", ToastType.SUCCESS, { duration: 5000 });
|
||||
Toasts.show(toast);
|
||||
}}
|
||||
/>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="Bottom Position Toast"
|
||||
onClick={() => {
|
||||
const toast = Toasts.create("Bottom toast", ToastType.MESSAGE, { position: Toasts.Position.BOTTOM });
|
||||
Toasts.show(toast);
|
||||
}}
|
||||
/>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="Custom Component Toast"
|
||||
onClick={() => {
|
||||
const toast = Toasts.create("Custom", ToastType.CUSTOM, {
|
||||
component: <span style={{ color: "var(--text-brand)" }}>Custom JSX content!</span>,
|
||||
});
|
||||
Toasts.show(toast);
|
||||
}}
|
||||
/>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 10 }}>
|
||||
{description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Simple Usage">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
showToast(message, type) - Quick way to show a toast.
|
||||
</Paragraph>
|
||||
<ManaButton
|
||||
variant="primary"
|
||||
text="Show Simple Toast"
|
||||
onClick={() => showToast("Hello from showToast!", ToastType.SUCCESS)}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Advanced Usage">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Toasts.show() with full options object.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid" style={{ gap: 8 }}>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="Advanced Toast (5s duration)"
|
||||
onClick={showAdvancedToast}
|
||||
/>
|
||||
<ManaButton
|
||||
variant="secondary"
|
||||
text="Custom Component Toast"
|
||||
onClick={showCustomToast}
|
||||
/>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Pop Toast">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Toasts.pop() removes the most recent toast.
|
||||
</Paragraph>
|
||||
<ManaButton
|
||||
variant="critical-secondary"
|
||||
text="Pop Last Toast"
|
||||
onClick={() => Toasts.pop()}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="ToastType">
|
||||
<Paragraph color="text-muted">• MESSAGE - Default message style</Paragraph>
|
||||
<Paragraph color="text-muted">• SUCCESS - Green checkmark icon</Paragraph>
|
||||
<Paragraph color="text-muted">• FAILURE - Red X icon</Paragraph>
|
||||
<Paragraph color="text-muted">• CUSTOM - Custom component content</Paragraph>
|
||||
<Paragraph color="text-muted">• CLIP - Clip/scissors icon</Paragraph>
|
||||
<Paragraph color="text-muted">• LINK - Link icon</Paragraph>
|
||||
<Paragraph color="text-muted">• FORWARD - Forward arrow icon</Paragraph>
|
||||
<Paragraph color="text-muted">• INVITE - Invite icon</Paragraph>
|
||||
<Paragraph color="text-muted">• BOOKMARK - Bookmark icon</Paragraph>
|
||||
<Paragraph color="text-muted">• CLOCK - Clock icon</Paragraph>
|
||||
<Paragraph color="text-muted">• AI - AI icon</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="ToastPosition">
|
||||
<Paragraph color="text-muted">• TOP (0) - Toast appears at top</Paragraph>
|
||||
<Paragraph color="text-muted">• BOTTOM (1) - Toast appears at bottom</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Toasts.show() Options">
|
||||
<Paragraph color="text-muted">• message - Toast message text</Paragraph>
|
||||
<Paragraph color="text-muted">• type - Toast type from ToastType</Paragraph>
|
||||
<Paragraph color="text-muted">• id - Unique ID (use Toasts.genId())</Paragraph>
|
||||
<Paragraph color="text-muted">• options.duration - Display time in ms</Paragraph>
|
||||
<Paragraph color="text-muted">• options.position - ToastPosition value</Paragraph>
|
||||
<Paragraph color="text-muted">• options.component - Custom React component</Paragraph>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Import">
|
||||
<Paragraph color="text-muted">
|
||||
{"import { showToast, Toasts, ToastType, ToastPosition } from \"../components\";"}
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
),
|
||||
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: (
|
||||
<ManaButton
|
||||
variant="critical-secondary"
|
||||
text="Pop Last Toast"
|
||||
onClick={() => Toasts.pop()}
|
||||
/>
|
||||
),
|
||||
code: "Toasts.pop();",
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>", 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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Positions">
|
||||
<div className="vc-compfinder-grid">
|
||||
{TooltipPositions.map(position => (
|
||||
<ManaTooltip key={position} text={`Tooltip on ${position}`} position={position}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text={position} />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Colors">
|
||||
<div className="vc-compfinder-grid">
|
||||
{TooltipColors.map(color => (
|
||||
<ManaTooltip key={color} text={`${color} tooltip`} color={color}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text={color} />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Delay">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaTooltip text="No delay" delay={0}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="No delay" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
<ManaTooltip text="500ms delay" delay={500}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="500ms delay" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
<ManaTooltip text="1000ms delay" delay={1000}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="1s delay" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Hide on Click">
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaTooltip text="Hides when clicked" hideOnClick>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="Click me" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
<ManaTooltip text="Stays visible when clicked" hideOnClick={false}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="Click me" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
• text: string - Tooltip content
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• children: (props) => ReactNode - Render function for trigger element
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• position?: "top" | "bottom" | "left" | "right" - Tooltip position
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• color?: "primary" | "black" | "grey" | "brand" | "green" | "yellow" | "red"
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• delay?: number - Show delay in milliseconds
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• hideOnClick?: boolean - Hide tooltip when trigger is clicked
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
<DocPage
|
||||
componentName="ManaTooltip"
|
||||
overview="ManaTooltip displays contextual information on hover via a render-props pattern. Supports positioning on all four sides, five color variants, configurable delay, and click-to-hide behavior. The children prop receives props that must be spread on the trigger element."
|
||||
notices={[
|
||||
{ type: "info", children: "ManaTooltip uses a render-props pattern. The children function receives props that must be spread onto the trigger element for hover detection to work." },
|
||||
]}
|
||||
importPath={'import { ManaTooltip, TooltipPositions, TooltipColors } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Positions",
|
||||
description: "Tooltip can appear on any side of the trigger element.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{TooltipPositions.map(position => (
|
||||
<ManaTooltip key={position} text={`Tooltip on ${position}`} position={position}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text={position} />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: `<ManaTooltip text="Tooltip on top" position="top">
|
||||
{props => <ManaButton {...props} variant="secondary" text="Hover me" />}
|
||||
</ManaTooltip>`,
|
||||
relevantProps: ["position"],
|
||||
},
|
||||
{
|
||||
title: "Colors",
|
||||
description: "Five background color variants.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
{TooltipColors.map(color => (
|
||||
<ManaTooltip key={color} text={`${color} tooltip`} color={color}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text={color} />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
code: '<ManaTooltip text="Branded" color="brand">\n {props => <ManaButton {...props} variant="secondary" text="Brand" />}\n</ManaTooltip>',
|
||||
relevantProps: ["color"],
|
||||
},
|
||||
{
|
||||
title: "Delay",
|
||||
description: "Configure how long to wait before showing the tooltip.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaTooltip text="No delay" delay={0}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="No delay" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
<ManaTooltip text="500ms delay" delay={500}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="500ms delay" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
<ManaTooltip text="1000ms delay" delay={1000}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="1s delay" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["delay"],
|
||||
},
|
||||
{
|
||||
title: "Hide on Click",
|
||||
description: "By default tooltips hide when the trigger is clicked. Set hideOnClick=false to keep them visible.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaTooltip text="Hides when clicked" hideOnClick>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="hideOnClick=true" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
<ManaTooltip text="Stays visible when clicked" hideOnClick={false}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="hideOnClick=false" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["hideOnClick"],
|
||||
},
|
||||
{
|
||||
title: "Rich Content",
|
||||
description: "The text prop accepts a function that returns JSX for rich tooltip content.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid">
|
||||
<ManaTooltip text={() => (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<Paragraph color="text-default" style={{ fontWeight: 600 }}>Rich Tooltip</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ fontSize: 12 }}>With multiple lines of content.</Paragraph>
|
||||
</div>
|
||||
)}>
|
||||
{props => (
|
||||
<ManaButton {...props} variant="secondary" text="Rich content" />
|
||||
)}
|
||||
</ManaTooltip>
|
||||
</div>
|
||||
),
|
||||
code: `<ManaTooltip text={() => (
|
||||
<div>
|
||||
<strong>Title</strong>
|
||||
<p>Description text</p>
|
||||
</div>
|
||||
)}>
|
||||
{props => <ManaButton {...props} text="Hover me" />}
|
||||
</ManaTooltip>`,
|
||||
relevantProps: ["text"],
|
||||
},
|
||||
]}
|
||||
props={TOOLTIP_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Text Colors (CSS Variables)">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{TextColors.map(color => (
|
||||
<div key={color} style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<span style={{ color: `var(--${color})`, minWidth: 200 }}>
|
||||
{color}
|
||||
</span>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
var(--{color})
|
||||
</code>
|
||||
<DocPage
|
||||
componentName="Typography Reference"
|
||||
overview="Typography reference for Discord's design system CSS variables. Shows all available text colors, sizes, weights, and code syntax colors. Use these with the Paragraph, DiscordHeading, and DiscordText components."
|
||||
importPath={'import { TextColors, TextSizes, TextWeights, CodeColors, FontSizeMap } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Text Colors",
|
||||
description: "All available text color CSS variables. Use these as the color prop on Paragraph and DiscordText.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{TextColors.map(color => (
|
||||
<div key={color} style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<span style={{ color: `var(--${color})`, minWidth: 200 }}>
|
||||
{color}
|
||||
</span>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
var(--{color})
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Text Sizes">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{TextSizes.map(({ name, value, pixels }) => (
|
||||
<div key={name} style={{ display: "flex", alignItems: "baseline", gap: 16 }}>
|
||||
<span style={{ fontSize: FontSizeMap[name], minWidth: 200 }}>
|
||||
{name}
|
||||
</span>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
{value} ({pixels})
|
||||
</code>
|
||||
),
|
||||
code: '<Paragraph color="text-muted">Muted text</Paragraph>',
|
||||
},
|
||||
{
|
||||
title: "Text Sizes",
|
||||
description: "Font sizes from xxs (10px) to xxl (32px).",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{TextSizes.map(({ name, value, pixels }) => (
|
||||
<div key={name} style={{ display: "flex", alignItems: "baseline", gap: 16 }}>
|
||||
<span style={{ fontSize: FontSizeMap[name], minWidth: 200 }}>
|
||||
{name}
|
||||
</span>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
{value} ({pixels})
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Text Weights">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{TextWeights.map(({ name, value }) => (
|
||||
<div key={name} style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<span style={{ fontWeight: value, minWidth: 200 }}>
|
||||
{name}
|
||||
</span>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
font-weight: {value}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Text Weights",
|
||||
description: "Font weights from thin (100) to extrabold (800).",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{TextWeights.map(({ name, value }) => (
|
||||
<div key={name} style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<span style={{ fontWeight: Number(value), minWidth: 200 }}>
|
||||
{name}
|
||||
</span>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
font-weight: {value}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Usage with BaseText">
|
||||
<div className="vc-compfinder-grid-vertical" style={{ gap: 4 }}>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
{"import { BaseText } from \"@components/BaseText\";"}
|
||||
</code>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
{"<BaseText size=\"md\" weight=\"semibold\" color=\"text-default\">Text</BaseText>"}
|
||||
</code>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Code Colors">
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{CodeColors.map(color => (
|
||||
<div key={color} style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<span style={{ color: `var(--${color})`, minWidth: 200, fontFamily: "monospace" }}>
|
||||
{color}
|
||||
</span>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
var(--{color})
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Code Colors",
|
||||
description: "Syntax highlighting colors for code blocks.",
|
||||
children: (
|
||||
<div className="vc-compfinder-grid-vertical">
|
||||
{CodeColors.map(color => (
|
||||
<div key={color} style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<span style={{ color: `var(--${color})`, minWidth: 200, fontFamily: "monospace" }}>
|
||||
{color}
|
||||
</span>
|
||||
<code style={{ color: "var(--text-muted)", fontSize: "0.75rem" }}>
|
||||
var(--{color})
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">
|
||||
<strong>BaseText</strong> - from @components/BaseText
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• size?: "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl"
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• weight?: "thin" | "extralight" | "light" | "normal" | "medium" | "semibold" | "bold" | "extrabold"
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• color?: string - CSS variable name (e.g., "text-muted")
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• tag?: "span" | "p" | "div" | "h1" - etc.
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>Heading</strong> - from @components/Heading
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" (default: h5)
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>Paragraph</strong> - from @components/Paragraph
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• color?: string - CSS variable name
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted" style={{ marginTop: 12 }}>
|
||||
<strong>Span</strong> - from @components/Span
|
||||
</Paragraph>
|
||||
<Paragraph color="text-muted">
|
||||
• Inline text with default styling
|
||||
</Paragraph>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Usage with Components",
|
||||
description: "These values are used with Paragraph, DiscordHeading, and DiscordText. See those tabs for component-specific documentation.",
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<Paragraph color="text-default">text-default: Primary readable text.</Paragraph>
|
||||
<Paragraph color="text-muted">text-muted: Secondary, less prominent text.</Paragraph>
|
||||
<Paragraph color="text-link">text-link: Clickable link text.</Paragraph>
|
||||
<Paragraph color="text-brand">text-brand: Brand-colored text.</Paragraph>
|
||||
</div>
|
||||
),
|
||||
code: `<Paragraph color="text-muted">Muted text</Paragraph>
|
||||
<DiscordText variant="text-md/semibold" color="text-brand">Brand text</DiscordText>
|
||||
<DiscordHeading variant="heading-lg/bold">Bold heading</DiscordHeading>`,
|
||||
},
|
||||
]}
|
||||
props={PROP_GROUPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="vc-compfinder-section">
|
||||
<SectionWrapper title="Basic UserSummaryItem">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Displays a row of user avatars with overflow indicator.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<UserSummaryItem users={users} max={3} />
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{[2, 3, 5].map(max => (
|
||||
<div key={max} style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 80 }}>max={max}:</Paragraph>
|
||||
<UserSummaryItem users={users} max={max} />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Different Sizes">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Available sizes: 16, 24, 32, 56.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 50 }}>16:</Paragraph>
|
||||
<UserSummaryItem users={users} max={4} size={16} />
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 50 }}>24:</Paragraph>
|
||||
<UserSummaryItem users={users} max={4} size={24} />
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 50 }}>32:</Paragraph>
|
||||
<UserSummaryItem users={users} max={4} size={32} />
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 50 }}>56:</Paragraph>
|
||||
<UserSummaryItem users={users} max={4} size={56} />
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Max Users">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Control how many avatars to show before overflow.
|
||||
</Paragraph>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 80 }}>max=2:</Paragraph>
|
||||
<UserSummaryItem users={users} max={2} />
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 80 }}>max=3:</Paragraph>
|
||||
<UserSummaryItem users={users} max={3} />
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 80 }}>max=5:</Paragraph>
|
||||
<UserSummaryItem users={users} max={5} />
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="With Popout">
|
||||
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
|
||||
Enable user popout on avatar click.
|
||||
</Paragraph>
|
||||
<div className="vc-compfinder-grid">
|
||||
<UserSummaryItem users={users} max={3} showUserPopout />
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper title="Props">
|
||||
<Paragraph color="text-muted">• users - Array of User objects (or null for empty slots)</Paragraph>
|
||||
<Paragraph color="text-muted">• max - Maximum avatars to show (default 10)</Paragraph>
|
||||
<Paragraph color="text-muted">• count - Override count for "+N" display</Paragraph>
|
||||
<Paragraph color="text-muted">• size - 16, 24, 32, or 56 (default 24)</Paragraph>
|
||||
<Paragraph color="text-muted">• guildId - Guild context for avatars</Paragraph>
|
||||
<Paragraph color="text-muted">• showUserPopout - Enable user popout on click</Paragraph>
|
||||
<Paragraph color="text-muted">• showDefaultAvatarsForNullUsers - Show default avatar for null</Paragraph>
|
||||
<Paragraph color="text-muted">• renderUser - Custom render function for each user</Paragraph>
|
||||
<Paragraph color="text-muted">• renderMoreUsers - Custom render for overflow indicator</Paragraph>
|
||||
<Paragraph color="text-muted">• hideMoreUsers - Hide the "+N" overflow count</Paragraph>
|
||||
</SectionWrapper>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SizesDemo() {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const users = currentUser ? Array(5).fill(currentUser) : [];
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
{([16, 24, 32, 56] as const).map(size => (
|
||||
<div key={size} style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 50 }}>{size}:</Paragraph>
|
||||
<UserSummaryItem users={users} max={4} size={size} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OverrideCountDemo() {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const users = currentUser ? Array(3).fill(currentUser) : [];
|
||||
const [count, setCount] = useState(42);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 120 }}>Default count:</Paragraph>
|
||||
<UserSummaryItem users={users} max={2} />
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 120 }}>count={count}:</Paragraph>
|
||||
<UserSummaryItem users={users} max={2} count={count} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserSummaryItemTab() {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const users = currentUser ? Array(5).fill(currentUser) : [];
|
||||
|
||||
return (
|
||||
<DocPage
|
||||
componentName="UserSummaryItem"
|
||||
overview="UserSummaryItem displays a row of user avatars with an overflow indicator. Supports four sizes, configurable max visible count, user popout on click, and custom render functions for both avatars and the overflow indicator."
|
||||
importPath={'import { UserSummaryItem, UserStore } from "../components";'}
|
||||
sections={[
|
||||
{
|
||||
title: "Basic",
|
||||
description: "A simple row of user avatars with overflow.",
|
||||
children: (
|
||||
<UserSummaryItem users={users} max={3} />
|
||||
),
|
||||
code: `const users = [user1, user2, user3, user4, user5];
|
||||
<UserSummaryItem users={users} max={3} />`,
|
||||
relevantProps: ["users", "max"],
|
||||
},
|
||||
{
|
||||
title: "Sizes",
|
||||
description: "Four avatar sizes: 16, 24, 32, and 56 pixels.",
|
||||
children: <SizesDemo />,
|
||||
code: "<UserSummaryItem users={users} max={4} size={32} />",
|
||||
relevantProps: ["size"],
|
||||
},
|
||||
{
|
||||
title: "Max Users",
|
||||
description: "Control how many avatars are visible before the overflow count.",
|
||||
children: <MaxUsersDemo />,
|
||||
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: <OverrideCountDemo />,
|
||||
relevantProps: ["count"],
|
||||
},
|
||||
{
|
||||
title: "User Popout",
|
||||
description: "Enable user popout on avatar click with showUserPopout.",
|
||||
children: (
|
||||
<UserSummaryItem users={users} max={3} showUserPopout />
|
||||
),
|
||||
code: "<UserSummaryItem users={users} max={3} showUserPopout />",
|
||||
relevantProps: ["showUserPopout"],
|
||||
},
|
||||
{
|
||||
title: "Hide Overflow",
|
||||
description: 'Hide the "+N" overflow indicator entirely.',
|
||||
children: (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 150 }}>hideMoreUsers=false:</Paragraph>
|
||||
<UserSummaryItem users={users} max={3} />
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Paragraph color="text-muted" style={{ width: 150 }}>hideMoreUsers=true:</Paragraph>
|
||||
<UserSummaryItem users={users} max={3} hideMoreUsers />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
relevantProps: ["hideMoreUsers"],
|
||||
},
|
||||
]}
|
||||
props={USER_SUMMARY_PROPS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLElement>;
|
||||
onContextMenu?: React.MouseEventHandler<HTMLElement>;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLElement>;
|
||||
onMouseLeave?: React.MouseEventHandler<HTMLElement>;
|
||||
tabIndex?: number;
|
||||
ref?: React.Ref<SVGSVGElement>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export interface BadgeShapesType {
|
|||
SQUARE: string;
|
||||
}
|
||||
|
||||
export interface NumberBadgeProps {
|
||||
export interface NumberBadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "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<React.HTMLAttributes<HTMLDivElement>, "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<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
||||
color?: string;
|
||||
disableColor?: boolean;
|
||||
shape?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface PremiumBadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
||||
text: string;
|
||||
className?: string;
|
||||
color?: string;
|
||||
disableColor?: boolean;
|
||||
shape?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,18 @@ export interface ManaTextInputProps {
|
|||
defaultDirty?: boolean;
|
||||
focusProps?: Record<string, any>;
|
||||
inputRef?: React.Ref<HTMLInputElement>;
|
||||
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<string, unknown>;
|
||||
"aria-label"?: string;
|
||||
"aria-labelledby"?: string;
|
||||
"aria-describedby"?: string;
|
||||
|
|
@ -74,6 +86,18 @@ export interface ManaTextAreaProps {
|
|||
fullWidth?: boolean;
|
||||
focusProps?: Record<string, any>;
|
||||
inputRef?: React.Ref<HTMLTextAreaElement>;
|
||||
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<string, unknown>;
|
||||
"aria-label"?: string;
|
||||
"aria-labelledby"?: string;
|
||||
"aria-describedby"?: string;
|
||||
|
|
@ -92,6 +116,6 @@ export interface SearchBarProps {
|
|||
onFocus?: () => void;
|
||||
autoComplete?: string;
|
||||
inputProps?: Record<string, any>;
|
||||
className?: string;
|
||||
ref?: React.Ref<HTMLInputElement>;
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,19 @@ export interface MenuItemProps {
|
|||
iconLeft?: ComponentType<MenuItemIconProps>;
|
||||
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<MenuItemIconProps>;
|
||||
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<MenuItemIconProps>;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, any>;
|
||||
notice?: ModalNotice;
|
||||
onClose: () => void;
|
||||
onScroll?: (e: React.UIEvent) => void;
|
||||
scrollerRef?: React.Ref<HTMLDivElement>;
|
||||
returnRef?: React.Ref<HTMLElement>;
|
||||
role?: string;
|
||||
"aria-label"?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
|
@ -54,13 +67,25 @@ export interface ConfirmModalProps extends Omit<ModalProps, "actions" | "transit
|
|||
};
|
||||
}
|
||||
|
||||
export type ExpressiveGradientColor = "purple" | "blue" | "pink" | "green";
|
||||
|
||||
export interface ModalGraphic {
|
||||
type: "image";
|
||||
src: string;
|
||||
aspectRatio?: string;
|
||||
}
|
||||
|
||||
export interface ExpressiveModalProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
subtitle?: string;
|
||||
size?: NativeModalSize;
|
||||
transitionState?: number;
|
||||
graphic?: { src: string; aspectRatio?: string };
|
||||
gradientColor?: ExpressiveGradientColor;
|
||||
graphic?: ModalGraphic;
|
||||
badge?: React.ReactNode;
|
||||
actionBarInput?: React.ReactNode;
|
||||
actions?: ModalAction[];
|
||||
children?: React.ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -4,21 +4,43 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export interface ScrollerProps {
|
||||
export interface ScrollerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
orientation?: "vertical" | "horizontal";
|
||||
paddingFix?: boolean;
|
||||
fade?: boolean;
|
||||
dir?: "ltr" | "rtl";
|
||||
onScroll?: (e: React.UIEvent<HTMLDivElement>) => 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<HTMLDivElement>) => 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export interface TimestampProps {
|
|||
isInline?: boolean;
|
||||
isVisibleOnlyOnHover?: boolean;
|
||||
isEdited?: boolean;
|
||||
application?: any;
|
||||
id?: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>;
|
||||
positionKey?: string;
|
||||
ariaHidden?: boolean;
|
||||
shouldShow?: boolean;
|
||||
delay?: number;
|
||||
onTooltipShow?: () => void;
|
||||
onTooltipHide?: () => void;
|
||||
forceOpen?: boolean;
|
||||
overflowOnly?: boolean;
|
||||
hideOnClick?: boolean;
|
||||
children: React.ReactNode | ((props: Record<string, any>) => 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<HTMLDivElement>;
|
||||
targetElementRef: React.RefObject<HTMLElement | null>;
|
||||
shouldShow: boolean;
|
||||
position?: TooltipPosition;
|
||||
caretConfig?: { align?: "start" | "center" | "end"; };
|
||||
alignmentStrategy?: "edge" | "trigger-center";
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue