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>
|
||||
);
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useRef, useState, type MutableRefObject } from "react";
|
||||
|
||||
/**
|
||||
* A hook to use for determining open and closed states
|
||||
* @param initialValue Whether the component should start visible or not
|
||||
*
|
||||
* @returns {ref} A ref to be given to parent object
|
||||
* @returns {isVisible} Boolean value to decide whether to show
|
||||
* element or not
|
||||
* @returns {setIsVisible} Dispatch function to update the value of
|
||||
* isVisible
|
||||
*/
|
||||
|
||||
interface Return<T> {
|
||||
ref: MutableRefObject<T | null>;
|
||||
isVisible: boolean;
|
||||
setIsVisible: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const useIsVisible = <T = HTMLDivElement>(
|
||||
initialValue: boolean,
|
||||
): Return<T> => {
|
||||
const [isVisible, setIsVisible] = useState(initialValue);
|
||||
const ref = useRef<T | null>(null);
|
||||
|
||||
// Handles clicks outside the parent ref
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const containsElement =
|
||||
e.composedPath().indexOf(ref.current as EventTarget) !== -1;
|
||||
|
||||
if (ref.current && !containsElement) setIsVisible(false);
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
document.addEventListener("click", handleClickOutside, true);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside, true);
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
return { ref, isVisible, setIsVisible };
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
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
@@ -0,0 +1,95 @@
|
||||
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",
|
||||
},
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
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
@@ -0,0 +1,113 @@
|
||||
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
@@ -0,0 +1,30 @@
|
||||
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
@@ -0,0 +1,17 @@
|
||||
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";
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// this implementation is temporary until we can natively dynamically import css files
|
||||
// ref: https://react.dev/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022#asset-loading
|
||||
const wayOfLight = /* css */ `
|
||||
:root {
|
||||
--ttui_color-primary--main: #374045;
|
||||
--ttui_color-primary--dark: #12181c;
|
||||
--ttui_color-primary--light: #75838b;
|
||||
--ttui_color-primary--contrast: #ffffff;
|
||||
|
||||
--ttui_color-secondary--main: var(--ttui_red-500);
|
||||
--ttui_color-secondary--dark: var(--ttui_red-700);
|
||||
--ttui_color-secondary--light: var(--ttui_red-300);
|
||||
--ttui_color-secondary--contrast: var(--ttui_common-0);
|
||||
|
||||
--ttui_color-error--main: #ed6c02;
|
||||
--ttui_color-error--dark: #c77700;
|
||||
--ttui_color-error--light: #ffb547;
|
||||
--ttui_color-error--contrast: #ffffff;
|
||||
|
||||
--ttui_color-warning--main: #ed6c02;
|
||||
--ttui_color-warning--dark: #c77700;
|
||||
--ttui_color-warning--light: #ffb547;
|
||||
--ttui_color-warning--contrast: #ffffff;
|
||||
|
||||
--ttui_color-info--main: #2196f3;
|
||||
--ttui_color-info--dark: #0b79d0;
|
||||
--ttui_color-info--light: #64b6f7;
|
||||
--ttui_color-info--contrast: #ffffff;
|
||||
|
||||
--ttui_color-success--main: #4caf50;
|
||||
--ttui_color-success--dark: #3b873e;
|
||||
--ttui_color-success--light: #7bc67e;
|
||||
--ttui_color-success--contrast: #ffffff;
|
||||
|
||||
--ttui_color-bg--paper: #ffffff;
|
||||
--ttui_color-bg--default: #f4f5f5;
|
||||
--ttui_color-bg--pane: #f9f6ef;
|
||||
--ttui_color-bg--scroll: #eee8db;
|
||||
|
||||
--ttui_color-rarity--uncommon: #7ebe15;
|
||||
--ttui_color-rarity--rare: #41a9f2;
|
||||
--ttui_color-rarity--veryRare: #c364e7;
|
||||
--ttui_color-rarity--legendary: #ffb62a;
|
||||
--ttui_color-rarity--artifact: #f77558;
|
||||
--ttui_color-rarity--contrast: #000000;
|
||||
|
||||
--ttui_color-reference--character: #ee8600;
|
||||
--ttui_color-reference--magicItem: #41a9f2;
|
||||
--ttui_color-reference--monster: #c60000;
|
||||
--ttui_color-reference--skill: #7ebe15;
|
||||
--ttui_color-reference--spell: #c364e7;
|
||||
--ttui_color-reference--contrast: #000000;
|
||||
|
||||
--ttui_color-message--check: #c364e7;
|
||||
--ttui_color-message--custom: #ecedee;
|
||||
--ttui_color-message--damage: #f77558;
|
||||
--ttui_color-message--healSave: #7ebe15;
|
||||
--ttui_color-message--initiative: #ffb62a;
|
||||
--ttui_color-message--toHit: #41a9f2;
|
||||
|
||||
--ttui_color-text--primary: #12181cff;
|
||||
--ttui_color-text--secondary: #12181ca3;
|
||||
--ttui_color-text--disabled: #12181c5c;
|
||||
|
||||
--ttui_color-action--hover: var(--ttui_grey-100);
|
||||
--ttui_color-action--active: var(--ttui_grey-200);
|
||||
--ttui_color-action--selected: var(--ttui_grey-200);
|
||||
|
||||
--ttui_color-megamenu--main: #26282a;
|
||||
--ttui_color-megamenu--dark: #1c1d1e;
|
||||
--ttui_color-megamenu--contrast: var(--ttui_common-0);
|
||||
}
|
||||
`;
|
||||
|
||||
export default wayOfLight;
|
||||
Reference in New Issue
Block a user