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:
+51
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
+28
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
Generated
Vendored
+45
@@ -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",
|
||||
},
|
||||
];
|
||||
Generated
Vendored
+26
@@ -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",
|
||||
},
|
||||
];
|
||||
Generated
Vendored
+37
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user