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
+51
View File
@@ -0,0 +1,51 @@
"use client";
import clsx from "clsx";
import type {
AllHTMLAttributes,
ButtonHTMLAttributes,
FC,
PropsWithChildren,
} from "react";
import styles from "./styles/Button.module.css";
import sizeStyles from "./styles/ButtonSizes.module.css";
import variantStyles from "./styles/ButtonVariants.module.css";
export interface ButtonProps
extends PropsWithChildren,
Omit<
AllHTMLAttributes<HTMLAnchorElement | HTMLButtonElement | HTMLDivElement>,
"size" | "type"
>,
Pick<ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
className?: string;
color?: "primary" | "secondary" | "success" | "info" | "warning" | "error";
variant?: "solid" | "outline" | "text";
size?: "x-small" | "small" | "medium" | "large" | "x-large";
isDiv?: boolean;
}
export const Button: FC<ButtonProps> = ({
className,
color = "primary",
variant = "solid",
size = "medium",
isDiv,
...props
}) => {
const Element = props.href ? "a" : isDiv ? "div" : "button";
return (
<Element
className={clsx([
styles.button,
color && variantStyles[color],
variant && variantStyles[variant],
size && sizeStyles[size],
className,
])}
aria-disabled={props.disabled}
{...props}
/>
);
};
+64
View File
@@ -0,0 +1,64 @@
import clsx from "clsx";
import type { ChangeEvent, FC, ReactNode } from "react";
import styles from "./Checkbox.module.css";
export interface CheckboxProps {
id?: string;
label?: ReactNode;
defaultChecked?: boolean;
checked?: boolean;
value?: string | number | readonly string[];
name?: string;
disabled?: boolean;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
className?: string;
}
export const Checkbox: FC<CheckboxProps> = ({
id,
label,
defaultChecked,
checked,
value,
name,
disabled,
onChange,
className,
}) => (
<div className={clsx([styles.container, className])}>
<span className={clsx(styles.inputContainer)}>
<input
className={styles.checkbox}
id={id}
type="checkbox"
defaultChecked={defaultChecked}
checked={checked}
value={value}
name={name}
aria-disabled={disabled}
onChange={onChange ? (e) => !disabled && onChange?.(e) : undefined}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="none"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="M3.5 7.47 6.833 11 12.5 5"
/>
</svg>
</span>
{label && (
<label
className={clsx([styles.label, disabled && styles.disabled])}
htmlFor={id}
>
{label}
</label>
)}
</div>
);
+98
View File
@@ -0,0 +1,98 @@
import clsx from "clsx";
import { useEffect, useRef, type HTMLAttributes, type MouseEvent } from "react";
import styles from "./Dialog.module.css";
/**
* A generic dialog component to display content in a dialog (non-blocking) or
* a modal (blocking) way. This has built-in functionality like backdrop
* clicking and escape key handling. It also accepts a trigger element which
* will open the dialog when clicked. This is to allow focus to be returned to
* the trigger when the element is closed (for accessibility).
*/
export interface DialogProps extends HTMLAttributes<HTMLDialogElement> {
open: boolean;
onClose: (e?: MouseEvent) => void;
modal?: boolean;
}
export const Dialog: React.FC<DialogProps> = ({
open,
onClose,
modal,
children,
className,
...props
}) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const triggerRef = useRef<Element | null>(null);
const handleClose = () => {
onClose();
dialogRef.current?.close();
// Returns focus to trigger after close for accessibility
(triggerRef.current as HTMLButtonElement)?.focus();
};
const handleClickBackdrop = (e?: MouseEvent) => {
if (!e) return;
const rect = dialogRef.current?.getBoundingClientRect();
const isClickInDialog =
rect &&
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width;
if (!isClickInDialog) handleClose();
};
const handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
handleClose();
}
};
useEffect(
() => {
// Get the element that was clicked to open the dialog
triggerRef.current = document.activeElement;
if (modal && open) dialogRef.current?.showModal();
if (open) dialogRef.current?.focus();
if (!open) {
handleClose();
}
},
//eslint-disable-next-line react-hooks/exhaustive-deps
[dialogRef.current, open, modal],
);
useEffect(
() => {
document.addEventListener("keydown", handleEsc);
return () => {
document.removeEventListener("keydown", handleEsc);
};
},
//eslint-disable-next-line react-hooks/exhaustive-deps
[modal, open],
);
return (
<dialog
className={clsx([styles.dialog, className])}
ref={dialogRef}
onClick={modal ? handleClickBackdrop : undefined}
open={modal ? undefined : open}
aria-modal={modal}
{...props}
>
{children}
</dialog>
);
};
+49
View File
@@ -0,0 +1,49 @@
import clsx from "clsx";
import type { FC, HTMLAttributes } from "react";
import styles from "./InfoItem.module.css";
interface InfoItemProps extends HTMLAttributes<HTMLDivElement> {
center?: boolean;
color?: "primary" | "secondary" | "success" | "info" | "warning" | "error";
label: string;
inline?: boolean;
italic?: boolean;
}
export const InfoItem: FC<InfoItemProps> = ({
center,
color,
className,
inline,
italic,
label,
children,
...props
}) => (
<div
className={clsx([
center && styles.center,
inline && styles.inline,
className,
])}
{...props}
>
<p
className={clsx([
styles.label,
color && styles[color],
italic && styles.italic,
])}
>
{label}
</p>
{typeof children === "string" ? (
<p
className={clsx([styles.value, color && styles[color]])}
dangerouslySetInnerHTML={{ __html: children }}
/>
) : (
<p className={clsx([styles.value, color && styles[color]])}>{children}</p>
)}
</div>
);
@@ -0,0 +1,28 @@
import clsx from "clsx";
import type { HTMLAttributes, ReactNode } from "react";
import { Tooltip } from "../Tooltip";
import styles from "./LabelChip.module.css";
interface LabelChipProps extends HTMLAttributes<HTMLSpanElement> {
tooltipContent?: ReactNode;
tooltipClickable?: boolean;
}
export const LabelChip = ({
className = "",
children,
tooltipContent,
tooltipClickable,
...props
}: LabelChipProps) => (
<>
<span className={clsx([styles.labelChip, className])} {...props}>
{children}
</span>
{props["data-tooltip-id"] && (
<Tooltip id={props["data-tooltip-id"]} clickable={tooltipClickable}>
{tooltipContent}
</Tooltip>
)}
</>
);
@@ -0,0 +1,45 @@
const ddbImageBase = "https://media.dndbeyond.com/mega-menu";
export const cardList = [
{
label: "Classes",
imageUrl: `${ddbImageBase}/aef3e4b2ac24457aa251c761d41ff07b.jpg`,
link: "https://www.dndbeyond.com/classes",
},
{
label: "Backgrounds",
imageUrl: `${ddbImageBase}/6e31ff6ea1f04ca2af9896f3f6d1b383.jpg`,
link: "https://www.dndbeyond.com/backgrounds",
},
{
label: "Species",
imageUrl: `${ddbImageBase}/8b551f9a1c2c4db09d65cdd7c139f9d5.jpg`,
link: `https://www.dndbeyond.com/species`,
},
{
label: "Feats",
imageUrl: `${ddbImageBase}/a69ab5bf67b03308893b582dbef700e9.jpg`,
link: "https://www.dndbeyond.com/feats",
},
{
label: "Spells",
imageUrl: `${ddbImageBase}/a9eac9081b6240ca8d6325ca4766345e.png`,
link: "https://www.dndbeyond.com/spells",
},
{
label: "Equipment",
imageUrl: `${ddbImageBase}/b778ff3ca3f18e5f75ad4b348615cab5.jpg`,
link: "https://www.dndbeyond.com/equipment",
},
{
label: "Magic Items",
imageUrl: `${ddbImageBase}/c06b79eae8ee234d1cea4688e117152b.jpg`,
link: "https://www.dndbeyond.com/magic-items",
},
{
label: "Monsters",
imageUrl: `${ddbImageBase}/36ee49066331fc36e3b37147d123463a.jpg`,
imagePosition: "center 11%",
link: "https://www.dndbeyond.com/monsters",
},
];
@@ -0,0 +1,26 @@
import { twitchUrl, youtubeUrl } from "../../../shared/constants";
const ddbImageBase = "https://media.dndbeyond.com/mega-menu";
export const primaryItems = [
{
label: "Community Update",
imageUrl: `${ddbImageBase}/c2979f81837d8ee7c113a874da2909db.png`,
link: "https://www.dndbeyond.com/community-update",
},
{
label: "Twitch",
imageUrl: `${ddbImageBase}/443085c3173d345e90d29881d8442a59.png`,
link: twitchUrl,
},
{
label: "Youtube",
imageUrl: `${ddbImageBase}/d5e1b6d07482834ebfcbc1f04bd6476d.png`,
link: youtubeUrl,
},
{
label: "Changelog",
imageUrl: `${ddbImageBase}/4af3d4c196428ab0809cf71d332d540d.png`,
link: "https://www.dndbeyond.com/changelog",
},
];
@@ -0,0 +1,37 @@
import type { MegaMenuCardProps } from "../MegaMenuCard";
const ddbImageBase = "https://media.dndbeyond.com/mega-menu";
export const characterBuilder: MegaMenuCardProps = {
label: "Character Builder",
imageUrl: `${ddbImageBase}/323a928e32eff87dee85dfbe0793ce12.jpg`,
link: "https://www.dndbeyond.com/characters/builder",
};
export const primaryItems: MegaMenuCardProps[] = [
{
label: "Maps",
imageUrl: `${ddbImageBase}/049ddb9085342521d25c5230451cfd45.jpg`,
link: "https://www.dndbeyond.com/games",
flags: [{ label: "Beta", variant: "info" }],
},
{
label: "Encounters",
imageUrl: `${ddbImageBase}/e434a8385f9619f0d52480c3c3987059.jpg`,
link: "https://www.dndbeyond.com/encounter-builder",
flags: [{ label: "Beta", variant: "info" }],
},
];
export const secondaryItems: MegaMenuCardProps[] = [
{
label: "Mobile App",
imageUrl: `${ddbImageBase}/3aa58aac2d02bb52d62204e158b48ce6.jpg`,
link: "https://www.dndbeyond.com/player-app",
},
{
label: "Avrae Discord Bot",
imageUrl: `${ddbImageBase}/28d923cd45ad209411ef1ff07993721b.jpg`,
link: "https://avrae.io/",
},
];
+129
View File
@@ -0,0 +1,129 @@
"use client";
import AngleDown from "@dndbeyond/fontawesome-cache/svgs/regular/angle-down.svg";
import XMark from "@dndbeyond/fontawesome-cache/svgs/regular/xmark.svg";
import clsx from "clsx";
import type { FC, MouseEvent, SelectHTMLAttributes } from "react";
import { useIsVisible } from "../../hooks/useIsVisible/useIsVisible";
import { Checkbox } from "../Checkbox";
import styles from "./Select.module.css";
interface Option {
value: string | number;
label: string;
}
interface SelectProps
extends Omit<SelectHTMLAttributes<HTMLDivElement>, "value"> {
label: string;
options: Array<Option>;
value: string | string[] | number[];
placeholder?: string;
onDelete?: (e: MouseEvent) => void;
error?: string;
}
export const Select: FC<SelectProps> = ({
className = "",
placeholder,
name,
label,
onChange,
onDelete,
options,
value,
disabled,
error,
...props
}) => {
const { ref, isVisible, setIsVisible } = useIsVisible(false);
const multiple = Array.isArray(value) || false;
const handleDelete = (e: MouseEvent) => {
if (onDelete) {
e.stopPropagation();
onDelete(e);
}
};
const getDisplay = () => {
// If value is an array, show tags
if (multiple && Array.isArray(value) && value.length) {
return (
<span className={styles.tags}>
{value.map((v) => (
<span
className={styles.tag}
onClick={(e: MouseEvent) => handleDelete(e)}
key={v}
>
{v}
<XMark />
</span>
))}
</span>
);
}
// If value is string, show string
else if (!multiple && value) {
return value;
}
// If there is no value, show the placeholder
else {
return <span className={styles.placeholder}>{placeholder}</span>;
}
};
return (
<>
<div ref={ref} className={clsx([styles.container, className])} {...props}>
<label id={`${name}Label`}>{label}</label>
<button
className={clsx([styles.button, error && styles.buttonError])}
id={`${name}Button`}
onClick={() => setIsVisible(!isVisible)}
role="combobox"
aria-haspopup="listbox"
aria-controls={`${name}Dropdown`}
aria-labelledby={`${name}Label`}
aria-expanded={isVisible}
tabIndex={0}
disabled={disabled || false}
>
{getDisplay()}
<AngleDown />
</button>
<div
className={styles.options}
role="group"
id={`${name}Dropdown`}
tabIndex={-1}
>
{options.map((option) =>
multiple ? (
<Checkbox
id={`checkbox-${option.value}`}
label={option.label}
key={option.value}
name={name}
value={option.value}
onChange={(e) => onChange?.(e)}
checked={value.includes(option.value as never)}
/>
) : (
<label className={styles.option} key={option.value}>
<input
type="radio"
name={name}
value={option.value}
onChange={(e) => onChange?.(e)}
checked={value === option.value}
/>
{option.label}
</label>
),
)}
</div>
</div>
{error && <p className={styles.error}>{error}</p>}
</>
);
};
+66
View File
@@ -0,0 +1,66 @@
import clsx from "clsx";
import { useEffect, useRef, type DialogHTMLAttributes, type FC } from "react";
import styles from "./Toast.module.css";
/**
* Toast Usage
* const [open, setOpen] = useState(false);
*
* const onClick = () => {
* setOpen(true);
* };
*
* const onClose = () => {
* setOpen(false);
* };
*
* <Button onClick={onClick}>Toast</Button>
* <Toast
* open={open}
* autoHideDuration={6000}
* onClose={onClose}
* message="Note Archived"
* />
**/
interface ToastProps extends DialogHTMLAttributes<HTMLDialogElement> {
align?: "left" | "right";
autoHideDuration?: number;
onClose: () => void;
}
let timer: NodeJS.Timeout | undefined;
export const Toast: FC<ToastProps> = ({
align = "left",
autoHideDuration = 6000,
children,
className,
onClose,
open,
...props
}) => {
const ref = useRef<HTMLDialogElement | null>(null);
useEffect(() => {
if (open) {
// Prevent the Toast from grabbing focus
ref.current?.setAttribute("open", "open");
timer = setTimeout(onClose, autoHideDuration);
} else {
ref.current?.close?.();
timer = undefined;
}
return () => clearTimeout(timer);
}, [autoHideDuration, onClose, open]);
return (
<dialog
className={clsx([styles.toast, styles[align], className])}
ref={ref}
{...props}
>
{children}
</dialog>
);
};
+28
View File
@@ -0,0 +1,28 @@
"use client";
import type { FC } from "react";
import { Tooltip as ReactTooltip, type ITooltip } from "react-tooltip";
import styles from "./Tooltip.module.css";
export interface TooltipProps extends ITooltip {
id: string;
"data-testid"?: string;
}
export const Tooltip: FC<TooltipProps> = ({
id,
"data-testid": testId,
children,
...props
}) => {
return (
<div
className={styles.container}
{...(testId && { "data-testid": testId })}
>
<ReactTooltip id={id} className={styles.tooltip} {...props}>
{children}
</ReactTooltip>
</div>
);
};