Grabbed dndbeyond's source code

```
~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js
```
This commit is contained in:
2025-05-28 11:50:03 -07:00
commit 8df9031d27
3592 changed files with 319051 additions and 0 deletions
@@ -0,0 +1,41 @@
import React from "react";
import { ThemeButton } from "../ThemeButton";
//Eventually remove and use character-components/Button/RemoveButton with theme override when needed
interface Props {
onClick?: () => void;
className?: string;
disabled?: boolean;
styledDisabled?: boolean;
block?: boolean;
size?: string;
style?: string;
active?: boolean;
stopPropagation?: boolean;
enableConfirm?: boolean;
isInteractive?: boolean;
}
export default class RemoveButton extends React.PureComponent<Props, {}> {
static defaultProps = {
size: "small",
style: "outline",
stopPropagation: true,
};
render() {
const { className, children, ...restProps } = this.props;
let classNames: Array<string> = ["ct-remove-button"];
if (className) {
classNames.push(className);
}
return (
<ThemeButton {...restProps} className={classNames.join(" ")}>
{children ? children : "Delete"}
</ThemeButton>
);
}
}
@@ -0,0 +1,4 @@
import RemoveButton from "./RemoveButton";
export default RemoveButton;
export { RemoveButton };
@@ -0,0 +1,52 @@
import React from "react";
import { Button } from "@dndbeyond/character-components/es";
interface Props {
onClick?: ((evt: React.MouseEvent<HTMLButtonElement>) => void) | (() => void);
className: string;
disabled?: boolean;
styledDisabled?: boolean;
block?: boolean;
size?: string;
style?: string;
active?: boolean;
stopPropagation?: boolean;
enableConfirm?: boolean;
confirmText?: string;
confirmDuration?: number;
isInteractive?: boolean;
}
export default class ThemeButton extends React.PureComponent<Props, {}> {
static defaultProps = {
className: "",
style: "filled",
stopPropagation: true,
isInteractive: true,
};
render() {
const { style, className, isInteractive, ...restProps } = this.props;
let classNames: Array<string> = ["ct-theme-button"];
if (className) {
classNames.push(className);
}
if (style) {
classNames.push(`ct-theme-button--${style}`);
} else {
classNames.push("ct-theme-button--filled");
}
if (isInteractive) {
classNames.push("ct-theme-button--interactive");
}
return (
<Button
{...restProps}
className={classNames.join(" ")}
isInteractive={isInteractive}
/>
);
}
}
@@ -0,0 +1,4 @@
import ThemeButton from "./ThemeButton";
export default ThemeButton;
export { ThemeButton };
@@ -0,0 +1,137 @@
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import List from "@mui/material/List";
import ListSubheader from "@mui/material/ListSubheader";
import MenuItem from "@mui/material/MenuItem";
import Popover from "@mui/material/Popover";
import React from "react";
import { GroupedMenuOption } from "@dndbeyond/character-rules-engine/es";
import { ThemeButton } from "..";
interface Props {
children: React.ReactNode;
groupedOptions: Array<GroupedMenuOption>;
className?: string;
size?: string;
buttonStyle?: string;
showSingleOption?: boolean;
onSelect: (containerDefinitionKey: string) => void;
containerEl?: HTMLElement;
}
export const ThemeButtonWithMenu: React.FC<Props> = ({
className,
children,
groupedOptions,
size = "small",
buttonStyle = "filled",
onSelect,
showSingleOption = false,
containerEl,
}) => {
const singleOption =
groupedOptions.length === 1 &&
groupedOptions[0].options?.length === 1 &&
!showSingleOption;
const classNames = [className, "ct-theme-button-with-menu"];
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (!singleOption) {
setAnchorEl(event.currentTarget);
} else if (singleOption) {
onSelect(groupedOptions[0].options[0]?.value);
}
};
const handleClose = (value?: string) => {
if (value) {
onSelect(value);
}
setAnchorEl(null);
};
return (
<span className={classNames.join(" ")}>
<ThemeButton
size={size}
style={buttonStyle}
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
handleClick(evt);
}}
data-testid={`theme-button-with-menu-${children?.toString()}`
.toLowerCase()
.replace(/\s/g, "-")}
>
{children}{" "}
{!singleOption && (
<KeyboardArrowDownIcon sx={{ width: 8, height: 8 }} />
)}
</ThemeButton>
{!singleOption && (
<Popover
anchorEl={anchorEl}
container={containerEl}
keepMounted
open={Boolean(anchorEl)}
onClose={(evt: any) => {
evt.stopPropagation();
evt.nativeEvent?.stopImmediatePropagation();
handleClose();
}}
>
<List
sx={{ bgcolor: "background.default" }}
onClick={(evt) => {
evt.nativeEvent?.stopImmediatePropagation();
evt.stopPropagation();
}}
>
{groupedOptions.map((groupedOption) => (
<React.Fragment key={groupedOption.label}>
<ListSubheader
sx={{
lineHeight: "32px",
textTransform: "uppercase",
fontWeight: 700,
fontSize: "12px",
bgcolor: "background.default",
}}
data-testid={`theme-button-with-menu-subheader-${groupedOption.label}`
.toLowerCase()
.replace(/\s/g, "-")}
>
{groupedOption.label}
</ListSubheader>
{groupedOption.options.map((option) => {
return (
<MenuItem
sx={{ bgcolor: "background.default" }}
key={option.value}
onClick={(evt) => {
evt.stopPropagation();
evt.nativeEvent?.stopImmediatePropagation();
handleClose(option.value);
}}
data-testid={`theme-button-with-menu-item-${option.label}`
.toLowerCase()
.replace(/\s/g, "-")}
>
{option.label}
</MenuItem>
);
})}
</React.Fragment>
))}
</List>
</Popover>
)}
</span>
);
};
export default ThemeButtonWithMenu;
@@ -0,0 +1,4 @@
import { ThemeButtonWithMenu } from "./ThemeButtonWithMenu";
export default ThemeButtonWithMenu;
export { ThemeButtonWithMenu };
@@ -0,0 +1,261 @@
import clsx from "clsx";
import { orderBy } from "lodash";
import React from "react";
import { Checkbox } from "~/components/Checkbox";
import { HtmlContent } from "~/components/HtmlContent";
import { Accordion } from "../../../../../../components/Accordion";
import styles from "./styles.module.css";
import { CheckboxInfo } from "./typings";
export enum CheckboxVariant {
BUILDER = "builder",
SIDEBAR = "sidebar",
DEFAULT = "default",
}
interface Props {
checkboxes: Array<CheckboxInfo>;
heading: React.ReactNode;
description: React.ReactNode;
checkUncheckAllEnabled?: boolean;
onCheckUncheckAll?: CheckboxInfo;
showAccordion?: boolean;
accordionHeading?: string;
themed?: boolean;
darkMode?: boolean;
variant?: "builder" | "sidebar" | "default";
allText?: string;
}
export const FormCheckBoxesField: React.FC<Props> = ({
heading,
description,
checkboxes,
showAccordion,
checkUncheckAllEnabled,
onCheckUncheckAll,
themed,
darkMode,
accordionHeading = "Options",
variant = CheckboxVariant.DEFAULT,
allText = "All",
}) => {
const sortedChecks = orderBy(checkboxes, "sortOrder");
const allChecksEnabled = sortedChecks.every(
(checkbox) => checkbox.initiallyEnabled
);
if (!checkboxes.length) {
return null;
}
const renderSingleCheckbox = () => {
const [checkbox] = sortedChecks;
return (
<div
className={clsx([
styles.checkboxesContainer,
variant === CheckboxVariant.SIDEBAR &&
styles.checkboxesContainerSidebar,
])}
>
<div className={styles.summary}>
<div
className={clsx([
styles.summaryHeadingGroup,
styles.summaryHeadingGroupSingle,
])}
>
<div
className={clsx([
styles.summaryHeading,
variant === CheckboxVariant.SIDEBAR &&
styles.summaryHeadingSidebar,
darkMode && styles.summaryHeadingSidebarDark,
])}
>
{heading}
</div>
<div className={styles.checkbox}>
<Checkbox
checked={checkbox.initiallyEnabled}
aria-label={checkbox.label}
onClick={checkbox.onChange}
onChangePromise={checkbox.onChangePromise}
themed={themed}
darkMode={darkMode}
disabled={checkbox.enabled === false}
variant={variant}
/>
</div>
</div>
{description && (
<div
className={clsx([
styles.summaryDescription,
variant === CheckboxVariant.SIDEBAR &&
styles.summaryDescriptionSidebar,
darkMode && styles.darkMode,
])}
>
{description}
</div>
)}
</div>
</div>
);
};
const renderCheckboxes = () => {
return (
<div className={styles.container}>
{sortedChecks.map((checkbox, idx) => (
<div
className={styles.checkboxWrapper}
key={idx}
data-testid="content-preference-checkbox"
>
<div className={styles.group} key={idx}>
<div className={styles.checkbox}>
<Checkbox
checked={checkbox.initiallyEnabled}
aria-label={checkbox.label}
onClick={checkbox.onChange}
onChangePromise={checkbox.onChangePromise}
themed={themed}
darkMode={darkMode}
disabled={checkbox.enabled === false}
variant={variant}
/>
</div>
<div
className={clsx([
styles.groupLabel,
variant === CheckboxVariant.DEFAULT &&
styles.groupLabelDefault,
])}
>
{checkbox.label}
</div>
</div>
{checkbox.description && (
<HtmlContent
html={checkbox.description}
className={clsx([
styles.description,
variant === CheckboxVariant.DEFAULT &&
styles.descriptionDefault,
])}
/>
)}
</div>
))}
</div>
);
};
return checkboxes.length > 1 ||
variant === CheckboxVariant.BUILDER ||
variant === CheckboxVariant.DEFAULT ? (
<div
className={clsx(
[styles.checkboxesContainer],
variant === CheckboxVariant.SIDEBAR && styles.checkboxesContainerSidebar
)}
>
{((variant === CheckboxVariant.SIDEBAR && !showAccordion) ||
variant === CheckboxVariant.BUILDER ||
variant === CheckboxVariant.DEFAULT) && (
<div className={styles.summary}>
<div className={styles.summaryHeadingGroup}>
{checkUncheckAllEnabled &&
onCheckUncheckAll &&
variant === CheckboxVariant.DEFAULT && (
<Checkbox
checked={allChecksEnabled}
aria-label="Check/Uncheck all"
onClick={onCheckUncheckAll.onChange}
themed={themed}
darkMode={darkMode}
variant={variant}
/>
)}
<div
className={clsx([
styles.summaryHeading,
variant === CheckboxVariant.SIDEBAR &&
styles.summaryHeadingSidebar,
darkMode && styles.summaryHeadingSidebarDark,
variant === CheckboxVariant.DEFAULT &&
styles.summaryHeadingDefault,
])}
>
{heading}
</div>
</div>
{description && (
<div
className={clsx([
styles.summaryDescription,
variant === CheckboxVariant.SIDEBAR &&
styles.summaryDescriptionSidebar,
darkMode && styles.darkMode,
])}
>
{description}
</div>
)}
</div>
)}
{variant === CheckboxVariant.BUILDER && checkUncheckAllEnabled ? (
<div className={styles.summaryBuilder}>
<div className={styles.summaryHeadingGroup}>
{checkUncheckAllEnabled && onCheckUncheckAll && (
<Checkbox
checked={allChecksEnabled}
aria-label="Check/Uncheck all"
onClick={onCheckUncheckAll.onChange}
themed={themed}
darkMode={darkMode}
variant={variant}
/>
)}
<div className={styles.summaryHeadingAll}>{allText}</div>
</div>
</div>
) : null}
{showAccordion ? (
<Accordion
summary={
variant !== CheckboxVariant.SIDEBAR ? accordionHeading : heading
}
variant="text"
resetOpen={!allChecksEnabled}
className={clsx([
styles.accordion,
variant === CheckboxVariant.BUILDER && styles.accordionBuilder,
variant === CheckboxVariant.SIDEBAR && styles.accordionSidebar,
variant === CheckboxVariant.SIDEBAR &&
darkMode &&
styles.accordionHeadingDark,
])}
>
{variant === CheckboxVariant.SIDEBAR && (
<p className={clsx([styles.accordionSidebarDescription])}>
{description}
</p>
)}
{renderCheckboxes()}
</Accordion>
) : (
renderCheckboxes()
)}
</div>
) : (
renderSingleCheckbox()
);
};
@@ -0,0 +1,139 @@
import clsx from "clsx";
import {
ChangeEvent,
FC,
FocusEvent,
HTMLAttributes,
useEffect,
useRef,
useState,
} from "react";
import { v4 as uuidv4 } from "uuid";
import { useMaxLengthErrorHandling } from "~/hooks/useErrorHandling/useMaxLengthErrorHandling";
export interface FormInputFieldProps
extends Omit<
HTMLAttributes<HTMLDivElement>,
"onChange" | "onBlur" | "onFocus"
> {
label: string;
type?: string;
placeholder?: string;
initialValue?: string | number | null;
inputAttributes?: HTMLAttributes<HTMLInputElement>;
maxLength?: number;
maxLengthErrorMsg?: string;
variant?: "default" | "small";
// Handlers
onChange?: (value: string | number | null) => void;
onBlur?: (value: string | number | null) => void;
onFocus?: (value: string | number | null) => void;
transformValueOnChange?: (value: string) => string | number | null;
transformValueOnBlur?: (value: string) => string | number | null;
transformValueOnFocus?: (value: string) => string | number | null;
}
export const FormInputField: FC<FormInputFieldProps> = ({
label, // req
type = "text",
placeholder = "",
initialValue = "",
inputAttributes = {},
maxLength,
maxLengthErrorMsg = "",
...handlers
}) => {
const prevInitialValue = useRef(initialValue);
const initialLengthErrorState = maxLength
? (initialValue?.toString().length ?? 0) >= maxLength
: false;
const { MaxLengthErrorMessage, hideError, handleMaxLengthErrorMsg } =
useMaxLengthErrorHandling(
initialLengthErrorState,
maxLength ?? null,
maxLengthErrorMsg
);
const guid = `FIF_${uuidv4()}`;
const [value, setValue] = useState(initialValue);
const handleOnChange = (evt: ChangeEvent<HTMLInputElement>): void => {
const { onChange = null, transformValueOnChange = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnChange
? transformValueOnChange?.(targetElementValue)
: targetElementValue;
onChange?.(value);
setValue(value);
if (value !== null) {
handleMaxLengthErrorMsg(value.toString());
}
};
const handleOnBlur = (evt: FocusEvent<HTMLInputElement>) => {
const { onBlur = null, transformValueOnBlur = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnBlur
? transformValueOnBlur?.(targetElementValue)
: targetElementValue;
onBlur?.(value);
if (value !== targetElementValue) {
setValue(value);
}
// Always hide any length warnings when leaving the field since you can't go over the limit anyways
hideError();
};
const handleOnFocus = (evt: FocusEvent<HTMLInputElement>) => {
const { onFocus = null, transformValueOnFocus = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnFocus
? transformValueOnFocus?.(targetElementValue)
: targetElementValue;
onFocus?.(value);
if (value !== targetElementValue) {
setValue(value);
}
if (value !== null) {
handleMaxLengthErrorMsg(value.toString());
}
};
useEffect(() => {
if (initialValue !== prevInitialValue.current) {
setValue(initialValue ?? "");
prevInitialValue.current = initialValue;
}
}, [initialValue]);
return (
<div className={clsx(["builder-field", "form-input-field"])}>
<span className="builder-field-label">
<label
className="builder-field-heading form-input-field-label"
htmlFor={guid}
>
{label}
</label>
</span>
<span className="builder-field-input">
<input
className="builder-field-value"
id={guid}
type={type}
placeholder={placeholder}
onChange={handleOnChange}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
value={value ?? ""}
maxLength={maxLength || undefined}
{...inputAttributes}
/>
</span>
<MaxLengthErrorMessage />
</div>
);
};
@@ -0,0 +1,134 @@
import uniqueId from "lodash/uniqueId";
import React from "react";
import { Select } from "@dndbeyond/character-components/es";
import styles from "./styles.module.css";
interface Props {
initialValue: string | null;
onChange?: (value: string) => void;
onChangePromise?: (
newValue: string,
oldValue: string,
accept: () => void,
reject: () => void
) => void;
heading: React.ReactNode;
clsNames: Array<string>;
initialOptionRemoved: boolean;
options: Array<any>;
description?: React.ReactNode;
block: boolean;
placeholder?: string;
}
interface State {
value: string | null;
}
export default class FormSelectField extends React.PureComponent<Props, State> {
static defaultProps = {
initialValue: null,
clsNames: [],
initialOptionRemoved: false,
block: false,
};
constructor(props: Props) {
super(props);
this.state = {
value: props.initialValue,
};
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<State>,
snapshot?: any
): void {
const { initialValue } = this.props;
if (initialValue !== prevProps.initialValue) {
this.setState({
value: initialValue,
});
}
}
handleChange = (value: string): void => {
const { onChange } = this.props;
if (onChange) {
onChange(value);
}
this.setState({
value,
});
};
//TODO test that removing newValue from state still works in builder choices
handleChangePromise = (
newValue: string,
oldValue: string,
accept: () => void,
reject: () => void
): void => {
const { onChangePromise } = this.props;
if (onChangePromise) {
onChangePromise(newValue, oldValue, accept, reject);
}
};
render() {
const { value } = this.state;
const {
onChangePromise,
onChange,
block,
heading,
options,
clsNames,
initialOptionRemoved,
description,
placeholder,
} = this.props;
const uId: string = uniqueId("qry-");
let conClsNames: Array<string> = ["builder-field", "builder-field-select"];
if (clsNames.length) {
conClsNames = [...conClsNames, ...clsNames];
}
if (block) {
conClsNames.push("builder-field-select-block");
}
return (
<div className={conClsNames.join(" ")}>
<div className="builder-field-select-summary">
<label
className="builder-field-heading form-input-field-label"
htmlFor={uId}
>
{heading}
</label>
<div className={styles.summaryDescription}>{description}</div>
</div>
<div className="builder-field-select-input">
<Select
options={options}
onChange={onChange ? this.handleChange : undefined}
onChangePromise={
onChangePromise ? this.handleChangePromise : undefined
}
value={value}
initialOptionRemoved={initialOptionRemoved}
placeholder={placeholder}
/>
</div>
</div>
);
}
}
@@ -0,0 +1,4 @@
import FormSelectField from "./FormSelectField";
export default FormSelectField;
export { FormSelectField };
@@ -0,0 +1,57 @@
import React from "react";
import { Toggle } from "~/components/Toggle";
interface Props {
initiallyEnabled: boolean;
onChange?: (isEnabled?: boolean) => void;
onChangePromise?: (
newIsEnabled: boolean,
accept: () => void,
reject: () => void
) => void;
heading: React.ReactNode;
description?: React.ReactNode;
toggleLabel: string;
isReadOnly?: boolean;
}
export default class FormToggleField extends React.PureComponent<Props, {}> {
static defaultProps = {
initiallyEnabled: false,
};
render() {
const {
heading,
description,
initiallyEnabled,
onChange,
onChangePromise,
toggleLabel,
isReadOnly,
} = this.props;
return (
<div className="builder-field builder-field-toggle">
<div className="builder-field-toggle-summary">
<div className="builder-field-toggle-heading">{heading}</div>
{description && (
<div className="builder-field-toggle-description">
{description}
</div>
)}
</div>
<div className="builder-field-toggle-input">
{onChange && (
<Toggle
checked={initiallyEnabled}
onClick={onChange}
color="secondary"
aria-label={toggleLabel}
/>
)}
</div>
</div>
);
}
}
@@ -0,0 +1,4 @@
import FormToggleField from "./FormToggleField";
export default FormToggleField;
export { FormToggleField };
@@ -0,0 +1,19 @@
import React from "react";
interface Props {
nodes: Array<React.ReactNode>;
sep?: string;
}
export default function InlineSeparatedNodes({ nodes, sep = ", " }: Props) {
return (
<React.Fragment>
{nodes.map((node, idx) => (
<React.Fragment key={idx}>
{node}
{idx < nodes.length - 1 && sep}
</React.Fragment>
))}
</React.Fragment>
);
}
@@ -0,0 +1,4 @@
import InlineSeparatedNodes from "./InlineSeparatedNodes";
export default InlineSeparatedNodes;
export { InlineSeparatedNodes };
@@ -0,0 +1,29 @@
import * as React from "react";
import LinkButton from "../LinkButton";
interface Props {
url: string;
className?: string;
block?: boolean;
size: "oversized" | "large" | "medium" | "small";
disabled?: boolean;
download?: string | boolean;
onClick?: (isDisabled: boolean) => void;
}
export default class BuilderLinkButton extends React.PureComponent<Props, {}> {
static defaultProps = {
className: "",
};
render() {
const { className, ...restProps } = this.props;
let classNames: Array<string> = ["builder-button"];
if (className) {
classNames.push(className);
}
return <LinkButton {...restProps} className={classNames.join(" ")} />;
}
}
@@ -0,0 +1,4 @@
import BuilderLinkButton from "./BuilderLinkButton";
export default BuilderLinkButton;
export { BuilderLinkButton };
@@ -0,0 +1,86 @@
import React from "react";
interface Props {
url: string;
className: string;
block: boolean;
size: "oversized" | "large" | "medium" | "small";
disabled: boolean;
download?: string | boolean;
onClick?: (isDisabled: boolean) => void;
target?: string;
}
export default class LinkButton extends React.PureComponent<Props, {}> {
static defaultProps = {
className: "",
clsNames: [],
block: false,
disabled: false,
};
handleClick = (evt: React.MouseEvent) => {
const { onClick, disabled } = this.props;
if (onClick) {
onClick(disabled);
}
};
render() {
const {
disabled,
block,
size,
className,
url,
children,
download,
target,
} = this.props;
let classNames: Array<string> = ["ct-button", className];
let buttonSizeBase: string = "character-button";
if (block) {
buttonSizeBase += "-block";
}
let buttonSizeFinal: string;
switch (size) {
case "oversized":
case "large":
case "medium":
case "small":
buttonSizeFinal = `${buttonSizeBase}-${size}`;
break;
default:
buttonSizeFinal = buttonSizeBase;
}
classNames.push(buttonSizeFinal);
if (disabled) {
classNames.push("character-button-disabled");
classNames.push(`${buttonSizeFinal}-disabled`);
}
if (disabled) {
return (
<span className={classNames.join(" ")} onClick={this.handleClick}>
{children}
</span>
);
} else {
return (
<a
href={url}
target={target}
download={download}
className={classNames.join(" ")}
onClick={this.handleClick}
>
{children}
</a>
);
}
}
}
@@ -0,0 +1,34 @@
import * as React from "react";
import LinkButton from "../LinkButton";
interface Props {
url: string;
className?: string;
block?: boolean;
size: "oversized" | "large" | "medium" | "small";
style: "filled" | "outline";
disabled?: boolean;
download?: string | boolean;
target?: string;
}
export default class ThemeLinkButton extends React.PureComponent<Props, {}> {
static defaultProps = {
className: "",
style: "filled",
};
render() {
const { className, style, ...restProps } = this.props;
let classNames: Array<string> = [
"ct-theme-button",
`ct-theme-button--${style}`,
];
if (className) {
classNames.push(className);
}
return <LinkButton {...restProps} className={classNames.join(" ")} />;
}
}
@@ -0,0 +1,4 @@
import ThemeLinkButton from "./ThemeLinkButton";
export default ThemeLinkButton;
export { ThemeLinkButton };