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}