New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
@@ -0,0 +1,80 @@
import {
arrow,
autoPlacement,
autoUpdate,
FloatingArrow,
FloatingPortal,
offset as offsetFn,
useFloating,
type Placement,
} from "@floating-ui/react";
import clsx from "clsx";
import {
cloneElement,
useRef,
type PropsWithChildren,
type ReactElement,
type Ref,
} from "react";
import styles from "./AnchoredPopover.module.css";
interface AnchoredPopoverProps<T extends HTMLElement>
extends PropsWithChildren {
referenceComponent: ReactElement<{ ref?: Ref<T> }>;
allowedPlacements: Placement[];
className?: string;
offset?: number;
open?: boolean;
floatingArrow?: boolean;
floatingArrowClassName?: string;
}
export const AnchoredPopover = <T extends HTMLElement>({
className,
children,
open,
referenceComponent,
offset,
allowedPlacements,
floatingArrow,
floatingArrowClassName,
...rest
}: AnchoredPopoverProps<T>) => {
const arrowRef = useRef(null);
const { refs, floatingStyles, context, isPositioned } = useFloating({
strategy: "fixed",
transform: false,
open,
middleware: [
offset ? offsetFn(offset) : null,
arrow({ element: arrowRef }),
autoPlacement({
allowedPlacements,
}),
].filter(Boolean),
whileElementsMounted: autoUpdate,
});
return (
<>
{cloneElement(referenceComponent, { ref: refs.setReference })}
<FloatingPortal>
<div
ref={refs.setFloating}
style={floatingStyles}
className={clsx([styles.wrapper, className])}
{...rest}
>
{children}
{floatingArrow && isPositioned && (
<FloatingArrow
ref={arrowRef}
context={context}
className={floatingArrowClassName}
/>
)}
</div>
</FloatingPortal>
</>
);
};
+1 -1
View File
@@ -21,7 +21,7 @@ export interface ButtonProps
className?: string;
color?: "primary" | "secondary" | "success" | "info" | "warning" | "error";
variant?: "solid" | "outline" | "text" | "tool";
size?: "x-small" | "small" | "medium" | "large" | "x-large";
size?: "x-small" | "smaller" | "small" | "medium" | "large" | "x-large";
isDiv?: boolean;
}
+30 -11
View File
@@ -40,6 +40,12 @@ export const Dialog: React.FC<DialogProps> = ({
const handleClickBackdrop = (e?: MouseEvent) => {
if (!e) return;
// Keyboard-triggered clicks (e.g. pressing Enter on a button) report
// clientX/clientY as 0, which falls outside the dialog rect and would
// incorrectly be treated as a backdrop click.
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
if (e.detail === 0) return;
const rect = dialogRef.current?.getBoundingClientRect();
const isClickInDialog =
rect &&
@@ -49,17 +55,14 @@ export const Dialog: React.FC<DialogProps> = ({
e.clientX <= rect.left + rect.width;
if (!isClickInDialog) {
// Stop the click from bubbling to a parent dialog's backdrop handler,
// so only the innermost dialog closes when its backdrop is clicked.
e.stopPropagation();
onBackdropClick?.();
handleClose();
}
};
const handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
handleClose();
}
};
useEffect(
() => {
// Get the element that was clicked to open the dialog
@@ -78,11 +81,27 @@ export const Dialog: React.FC<DialogProps> = ({
useEffect(
() => {
document.addEventListener("keydown", handleEsc);
return () => {
document.removeEventListener("keydown", handleEsc);
};
if (modal) {
// The native cancel event is only fired for modal dialogs opened via
// showModal(). The browser adds its own Escape key listener and ensures
// the cancel event only fires on the topmost modal, so stacked dialogs
// close one at a time.
const dialog = dialogRef.current;
const handleCancel = (e: Event) => {
e.preventDefault();
handleClose();
};
dialog?.addEventListener("cancel", handleCancel);
return () => dialog?.removeEventListener("cancel", handleCancel);
} else {
// Non-modal dialogs don't receive the cancel event, so we fall back to
// a document-level keydown listener.
const handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) handleClose();
};
document.addEventListener("keydown", handleEsc);
return () => document.removeEventListener("keydown", handleEsc);
}
},
//eslint-disable-next-line react-hooks/exhaustive-deps
[modal, open],
@@ -0,0 +1,31 @@
import clsx from "clsx";
import type { FC } from "react";
import { Button, type ButtonProps } from "../Button";
import styles from "./FancyButton.module.css";
interface FancyButtonProps extends Omit<ButtonProps, "color"> {
color: "light" | "red" | "dark";
}
export const FancyButton: FC<FancyButtonProps> = ({
className,
color = "light",
children,
...props
}) => (
<Button
className={clsx([styles.button, styles[`${color}Button`], className])}
{...props}
>
<img
className={styles.image}
src={
color === "red"
? "https://media.dndbeyond.com/images/campaigns/redButtonBorder.png"
: "https://media.dndbeyond.com/images/campaigns/goldButtonBorder.png"
}
alt=""
/>
{children}
</Button>
);
File diff suppressed because one or more lines are too long
-49
View File
@@ -1,49 +0,0 @@
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>
);
@@ -1,45 +0,0 @@
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",
},
];
@@ -1,26 +0,0 @@
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",
},
];
@@ -1,45 +0,0 @@
import type { MegaMenuCardProps } from "../MegaMenuCard";
const ddbImageBase = "https://media.dndbeyond.com/mega-menu";
export const groupOne: MegaMenuCardProps[] = [
{
label: "Character Builder",
imageUrl: `${ddbImageBase}/character_builder.png`,
link: "https://www.dndbeyond.com/characters/builder",
},
{
label: "Sigil 3D VTT",
imageUrl: `${ddbImageBase}/playtest_sigil.png`,
link: "https://www.dndbeyond.com/project-sigil",
flags: [{ label: "New", variant: "success" }],
},
];
export const groupTwo: MegaMenuCardProps[] = [
{
label: "Maps VTT",
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 groupThree: 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/",
},
];
@@ -0,0 +1,75 @@
"use client";
import type { FC, HTMLAttributes } from "react";
import { FancyButton } from "../FancyButton";
import { SpinnerIcon } from "../Icons";
import styles from "./PremadeCharacterCard.module.css";
interface PremadeCharacterCardProps extends HTMLAttributes<HTMLDivElement> {
characterLevel: number;
characterName: string;
characterId?: number;
primaryClassName: string;
speciesName: string;
longDescription: string;
imageUrl: string;
imageAlt: string;
createButtonText?: string;
showProgress: boolean;
previewLink: string;
onClaim: () => void;
disableClaim?: boolean;
}
export const PremadeCharacterCard: FC<PremadeCharacterCardProps> = ({
characterLevel,
characterName,
characterId,
primaryClassName,
speciesName,
longDescription,
imageUrl,
imageAlt,
createButtonText = "Create",
showProgress,
previewLink,
onClaim,
disableClaim = false,
...props
}) => {
return (
<div className={styles.container} {...props}>
<img className={styles.image} src={imageUrl} alt={imageAlt} />
<h2 className={styles.title} id={characterId?.toString()}>
{characterName}
</h2>
<h3 className={styles.subTitle}>
Level {characterLevel}: {speciesName}{" "}
<strong>{primaryClassName}</strong>
</h3>
<p className={styles.description}>{longDescription}</p>
<FancyButton
className={styles.button}
color="light"
href={previewLink}
aria-label={`Preview ${speciesName} ${primaryClassName}`}
>
Preview
</FancyButton>
<FancyButton
className={styles.fancyButton}
onClick={disableClaim ? undefined : onClaim}
color={"dark"}
disabled={disableClaim}
aria-label={
showProgress
? `Loading ${speciesName} ${primaryClassName}...`
: `${createButtonText} ${speciesName} ${primaryClassName}`
}
>
{showProgress ? <SpinnerIcon /> : createButtonText}
</FancyButton>
</div>
);
};
+5 -2
View File
@@ -64,7 +64,7 @@ export const Select: FC<SelectProps> = ({
}
// If value is string, show string
else if (!multiple && value) {
return value;
return options.find((option) => option.value === value)?.label || value;
}
// If there is no value, show the placeholder
else {
@@ -114,7 +114,10 @@ export const Select: FC<SelectProps> = ({
type="radio"
name={name}
value={option.value}
onChange={(e) => onChange?.(e)}
onChange={(e) => {
onChange?.(e);
setIsVisible(false);
}}
checked={value === option.value}
/>
{option.label}
-77
View File
@@ -1,77 +0,0 @@
import { ThemeOptions } from "@mui/material";
export const components: ThemeOptions["components"] = {
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
MuiDialog: {
styleOverrides: {
paper: ({ theme }) => ({
minWidth: 320,
"&.MuiPaper-rounded": {
borderRadius: "8px",
},
"&.MuiPaper-outlined": {
borderColor: theme.palette.primary.main,
},
}),
},
},
MuiGrid: {
styleOverrides: {
root: {
/**
* n.b. this is a workaround for bad types on the DialogTitle component,
* what I really want there is a Grid, but I don't want to enforce these
* styles on any other Grid components that might be passed in, so I add
* the className "DdbDialogTitle-root" to target this specific component
*/
"&.DdbDialogTitle-root": {
padding: "16px 20px 8px",
"& .MuiIconButton-root": {
padding: "4px",
marginRight: "-8px",
},
},
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
transition: "none",
"&:hover": {
transition: "none",
borderRadius: "3px",
},
},
},
},
MuiDialogContent: {
styleOverrides: {
root: {
padding: "16px 20px 0",
},
},
},
MuiDialogContentText: {
styleOverrides: {
root: ({ theme }) => ({
color: theme.palette.text.primary,
letterSpacing: "0.15px",
marginBottom: "12px",
}),
},
},
MuiButton: {
styleOverrides: {
root: {
fontSize: "15px",
lineHeight: 26 / 15,
letterSpacing: "0.45px",
},
},
},
};
-95
View File
@@ -1,95 +0,0 @@
import { ThemeOptions } from "@mui/material";
export const palette: ThemeOptions["palette"] = {
mode: "dark",
text: {
primary: "#ecedeeff",
secondary: "#ecedeea3",
disabled: "#ecedee5c",
},
primary: {
main: "#dcdfe1ff",
dark: "#75838bff",
light: "#f9fafaff",
contrastText: "#000000ff",
},
secondary: {
main: "#00daa6ff",
dark: "#00b674ff",
light: "#b3efd9ff",
contrastText: "#000000ff",
},
action: {
active: "#dcdfe1a3",
hover: "#dcdfe11f",
selected: "#dcdfe12e",
disabled: "#dcdfe166",
disabledBackground: "#dcdfe133",
focus: "#dcdfe133",
},
error: {
main: "#ed6c02ff",
dark: "#c77700ff",
light: "#ffb547ff",
contrastText: "#ffffffff",
},
warning: {
main: "#ed6c02ff",
dark: "#c77700ff",
light: "#ffb547ff",
contrastText: "#ffffffff",
},
info: {
main: "#2196f3ff",
dark: "#0b79d0ff",
light: "#64b6f7ff",
contrastText: "#ffffffff",
},
success: {
main: "#4caf50ff",
dark: "#3b873eff",
light: "#7bc67eff",
contrastText: "#ffffffff",
},
background: {
paper: "#12181cff",
default: "#232b2fff",
pane: "#12181cdb",
},
common: { white: "#ffffffff", black: "#000000ff" },
grey: {
"50": "#f4f5f5ff",
"100": "#ecedeeff",
"200": "#dcdfe1ff",
"300": "#c4cbceff",
"400": "#a2acb2ff",
"500": "#75838bff",
"600": "#525c63ff",
"700": "#374045ff",
"800": "#232b2fff",
"900": "#12181cff",
},
rarity: {
uncommon: "#7ebe15ff",
rare: "#41a9f2ff",
veryRare: "#c364e7ff",
legendary: "#ffb62aff",
artifact: "#f77558ff",
contrastText: "#000000ff",
},
reference: {
magicItem: "#41a9f2ff",
monster: "#f77558ff",
skill: "#7ebe15ff",
spell: "#c364e7ff",
contrastText: "#000000ff",
},
message: {
check: "#c364e7ff",
custom: "#ecedeea3",
damage: "#f77558ff",
healSave: "#7ebe15ff",
initiative: "#ffb62aff",
toHit: "#41a9f2ff",
},
};
@@ -1,89 +0,0 @@
import { ThemeOptions } from "@mui/material";
export const palette: ThemeOptions["palette"] = {
mode: "light",
text: {
primary: "#12181cff",
secondary: "#12181ca3",
disabled: "#12181c5c",
},
primary: {
main: "#374045ff",
dark: "#12181cff",
light: "#75838bff",
contrastText: "#ffffffff",
},
secondary: {
main: "#e40712ff",
dark: "#9f271dff",
light: "#f77558ff",
contrastText: "#ffffffff",
},
action: {
active: "#12181ca3",
hover: "#12181c14",
selected: "#12181c1f",
disabled: "#12181c5c",
disabledBackground: "#12181c29",
focus: "#12181c29",
},
error: {
main: "#ed6c02ff",
dark: "#c77700ff",
light: "#ffb547ff",
contrastText: "#ffffffff",
},
warning: {
main: "#ed6c02ff",
dark: "#c77700ff",
light: "#ffb547ff",
contrastText: "#ffffffff",
},
info: {
main: "#2196f3ff",
dark: "#0b79d0ff",
light: "#64b6f7ff",
contrastText: "#ffffffff",
},
success: {
main: "#4caf50ff",
dark: "#3b873eff",
light: "#7bc67eff",
contrastText: "#ffffffff",
},
background: {
paper: "#ffffffff",
default: "#f4f5f5ff",
parchment: "#f9f6efff",
scroll: "#eee8dbff",
},
common: { white: "#ffffffff", black: "#000000ff" },
grey: {
"50": "#f4f5f5ff",
"100": "#ecedeeff",
"200": "#dcdfe1ff",
"300": "#c4cbceff",
"400": "#a2acb2ff",
"500": "#75838bff",
"600": "#525c63ff",
"700": "#374045ff",
"800": "#232b2fff",
"900": "#12181cff",
},
rarity: {
uncommon: "#009158ff",
rare: "#077bd0ff",
veryRare: "#9605d4ff",
legendary: "#ae5e21ff",
artifact: "#c81e18ff",
contrastText: "#ffffffff",
},
reference: {
character: "#ee8600ff",
magicItem: "#077bd0ff",
monster: "#c60000ff",
skill: "#009158ff",
spell: "#9605d4ff",
contrastText: "#ffffffff",
},
};
-113
View File
@@ -1,113 +0,0 @@
import { ThemeOptions } from "@mui/material";
export const typography: ThemeOptions["typography"] = {
h1: {
fontSize: "64px",
fontFamily: "Tiamat Condensed SC",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: 1,
lineHeight: "64px",
},
h2: {
fontSize: "48px",
fontFamily: "Roboto",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: "normal",
lineHeight: "52.8px",
},
h3: {
fontSize: "36px",
fontFamily: "Roboto",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: "normal",
lineHeight: "43.2px",
},
h4: {
fontSize: "26px",
fontFamily: "Roboto",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: "normal",
lineHeight: "32.5px",
},
h5: {
fontSize: "20px",
fontFamily: "Roboto",
fontWeight: 500,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: "normal",
lineHeight: "25px",
},
h6: {
fontSize: "16px",
fontFamily: "Roboto",
fontWeight: 500,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: "normal",
lineHeight: "22.4px",
},
body1: {
fontSize: "16px",
fontFamily: "Roboto",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: 0.15,
lineHeight: "24px",
},
body2: {
fontSize: "14px",
fontFamily: "Roboto",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: 0.15,
lineHeight: "20.02px",
},
subtitle1: {
fontSize: "16px",
fontFamily: "Roboto",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: 0.15,
lineHeight: "28px",
},
subtitle2: {
fontSize: "14px",
fontFamily: "Roboto",
fontWeight: 500,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: 0.1,
lineHeight: "21.98px",
},
overline: {
fontSize: "12px",
fontFamily: "Roboto",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: 1,
lineHeight: "31.92px",
textTransform: "uppercase",
},
caption: {
fontSize: "12px",
fontFamily: "Roboto",
fontWeight: 400,
fontStyle: "normal",
fontStretch: "normal",
letterSpacing: 0.4,
lineHeight: "19.92px",
},
};
-30
View File
@@ -1,30 +0,0 @@
import { createTheme, ThemeOptions } from "@mui/material";
import { deepmerge } from "@mui/utils";
import { components } from "./config/components";
import { palette as darkThemePalette } from "./config/palette-dark";
import { palette as lightThemePalette } from "./config/palette-light";
import { typography } from "./config/typography";
export const createLightTheme = (options: ThemeOptions = {}) =>
createTheme(
deepmerge(
{
components,
palette: lightThemePalette,
typography,
},
options,
),
);
export const createDarkTheme = (options: ThemeOptions = {}) =>
createTheme(
deepmerge(
{
components,
palette: darkThemePalette,
typography,
},
options,
),
);
-17
View File
@@ -1,17 +0,0 @@
export const signInLabel = "Sign In";
export const signUpLabel = "Sign Up";
export const signOutLabel = "Sign Out";
// Social Media Links
export const discordUrl = "https://www.dndbeyond.com/discord";
export const facebookUrl = "https://www.facebook.com/dndbeyond";
export const instagramUrl = "https://www.instagram.com/dndbeyond";
export const tiktokUrl = "https://www.tiktok.com/@dnd_beyond";
export const twitchUrl = "https://www.twitch.tv/dndbeyond";
export const twitterUrl = "https://twitter.com/dndbeyond";
export const youtubeUrl =
"https://www.youtube.com/channel/UCPy-338BEVgDkQade0qJmkw";
export const googleStoreUrl =
"https://play.google.com/store/apps/details?id=com.fandom.playercompanion";
export const appleStoreUrl =
"https://apps.apple.com/us/app/d-d-beyond/id1501810129";